大众点评点餐小程序开发经验 - 视图层

4,976 阅读11分钟

作者介绍:何延希,美团点评工程师,4年Web开发经验,现在是美团点评点餐团队的一员。

接上一篇大众点评点餐小程序开发经验 - 概述,本期想要和大家分享下大众点评点餐小程序中View视图层的一些开发经验。本文部分示例来自于「大众点评点餐」小程序的菜单页面。

页面代码结构为:

menu
├── menu.html
├── menu.js
├── menu.json
└── menu.less

我们将要说的小程序的View视图层是由WXML(menu.html) 与 WXSS(menu.less) 两大部分组成,由视图最小单元 - 组件来进行展示。视图层将逻辑层的数据(menu.js+menu.json)反应成视图,同时将视图层中定义的事件发送给逻辑层,一图以蔽之。

WXML

WXML(WeiXin Markup Language)与HTML对应,用于描述页面的结构,可以类比React的JSX。项目中menu.html为WXML语法,一个页面的顶层是page节点。WXML中获取逻辑层定义的数据后,通过一系列自己的语法和逻辑展示出这些数据。结构上组件是其最小单元,通过以下方式动态渲染。

1、数据绑定

数据绑定是最简单的使用数据方式,语法采用Mustache的变量替换,用双大括号将变量包起来,如果组件的属性则需将数据放置于引号之中。

<view class="dish-item" data-id="{{dishId}}"><text class="name">{{dishName}}</text></view>

数据绑定还支持ES6规范的扩展运算符 “...”、解构赋值。

<template is="dishItem" data="{{...item, count, soldout: true }}"></template>

2、逻辑运算

双大括号中可进行算数运算、三目运算、逻辑判断、字符串拼接等操作。

<text class="{{orderBanner.type !== 0 ? 'order-banner arrow' : 'order-banner'}}">{{orderBanner.text}}</text>

3、条件渲染

与常用模板语言将渲染内容写在 if/else 判断条件之中不一样的是,小程序的条件渲染将渲染条件直接写在渲染内容组件的 wx:if/wx:else 属性中,如果渲染组件为多个,可将多个组件放在组件内,渲染条件置于<block>组件wx:if/wx:else 属性中,此时的组件只充当容器作用,页面中不会渲染。

if/else

<text wx:if="{{item.soldOut}}" class="status-soldout">已售完</text>
<template wx:else is="numberCount" data="{{count: cartSpuCount[item.spuId]}}"></template>

<block>

<block  wx:if="{{serverError}}">
    <text>点小评去吃满汉全席啦~</text>
    <button class="menu-btn" bindtap="requestMenu">重试</button>
</block>

4、列表渲染

列表渲染是将遍历元素作为渲染组件的wx:for属性值,与此相关的还有以下几个属性:

  • wx:key:遍历元素的唯一的标识符,主要用于数据动态变化时dom的更新机制,数据不变则可无视
  • wx:for-item:遍历元素的变量名,默认item
  • wx:for-index:遍历元素下标的变量名,默认index
    注意:以上属性值虽然是字符串,为正确取值单词间都不要使用-等符号连接(如dish-item在使用时{{dish-item}}会解析成减号而取不到值)。

项目中数据较为复杂,使用测试数据举例:

<block wx:for="{{testData}}"
    wx:for-item="mainitem"
    wx:key="{{mainindex}}"
    wx:for-index="mainindex">
    <view wx:for="{{mainitem}}"
        wx:for-item="subitem"
        wx:key="{{subitem.id}}"
        wx:for-index="subindex">
        <view class="dom-item">第一层index: {{mainindex}} id: {{subitem.id}} name: {{subitem.name}}</view>
    </view>
</block>

以上代码结构上分为两层:
1、第一层block循环遍历testData数组,每个遍历值变量名为mainitem;
2、第二层view循环遍历mainitem数组,每个遍历值变量名为subitem,展示第一层index,第二层id和name属性;

// 创建页面实例对象
Page({
    /**
    * 页面的初始数据
    */
    data:  {
        "testData": [
            [ {
                "id": "1-1",
                "name": "节点1 - 1"
            }, {
                "id": "1-2",
                "name": "节点1 - 2"
            }], [{
                "id": "2-1",
                "name": "节点2 - 1"
            }, {
                "id": "2-2",
                "name": "节点2 - 2"
            }]
        ]
    }
})

展示结果:

开发过程中曾碰到

  • wx:for第二层中wx:for-item和wx:for-index失效
  • wx:for渲染异常
  • wx:for中wx:index错乱
    以上问题小程序更新版本后均已修复。

注意:

  • 1、循环遍历时,除官方说明的数组类型可以循环遍历外,对象类型也可通过wx:for进行属性遍历,此时for-index为属性的key值

如将上面例子中testData换成对象类型:

// 创建页面实例对象
Page({
    /**
    * 页面的初始数据
    */
    data:  {
        "testData": {
            "a": [{
                "id": "1-1",
                "name": "节点1 - 1"
            }, {
                "id": "1-2",
                "name": "节点1 - 2"
            }],
            "b": [{
                "id": "2-1",
                "name": "节点2 - 1"
            }, {
                "id": "2-2",
                "name": "节点2 - 2"
            }]
        }
    }
})

结果为:

  • 2、循环遍历时,小程序之前还支持wx:forin遍历,功能和wx:for相似,但官方文档中未说明,现在尝试不会报错,但功能已经失效,估计后期已经合并。

5、模板 & 引用

模板类似于React中的组件component的概念,可以在模板中定义代码片段,然后在不同的地方调用,减少重复的代码。

1、定义:使用name属性,作为模板的名字,然后在<template/>内定义模板代码片段;
2、使用方式有2种:

  • 使用include方式,将目标文件除了<template/>的整个代码引入,相当于是拷贝到include位置,所以无法传入参数;
  • 通过import的方式引入定义的文件,然后通过<template/>组件的 is 属性,声明需要的使用的模板,然后将模板所需要的 data 传入,模板拥有自己的作用域,只能使用data传入的数据。

注意:

  • 只会 import 目标文件中定义的 <template/>,不能引用目标文件中引用的 <template/>。
  • React的父组件通过props将数据传入子组件中,传值方式为引用传值,子组件中可修改自身props影响父组件数据。小程序的模板只能单向使用传入的数据。

示例(单个菜品组件):

<import src="../../components/common/dish-item.wxml" />
<template is="dishItem" data="{{...item}}"></template>

6、绑定事件

事件名称为字符串,会默认传入event参数,无法定制其他参数,所以一般将所需参数通过data-属性绑定至组件后通过e.currentTarget.dataset获取。

<view class="cart-btn" data-type="1" bindtap="redirectCart">选好了</view>

WXSS

WXSS(WeiXin Style Sheet)与CSS对应,用于描述页面的样式。
定义在app.less中的样式为全局样式,作用于每一个页面;在page的wxss文件中定义的样式为局部样式,只作用在对应的页面,并会覆盖app.less中相同的选择器,如代码结构中menu.less作用于menu.html。

1、支持的特性

  • 内联样式:组件的 style 接收动态的样式,在运行时会进行解析,请尽量避免将静态的样式写进style中,以免影响渲染速度。
  • 选择器
    对于常用的选择器,目前支持的选择器有:

注:绿色背景色行表示官方文档中没有说明,但经个人亲测后确定也支持的选择器。

目前不支持的选择器有:


注意:

  • 如之前提到,页面的顶层是节点,所以作用于整个页面的样式或修改顶层节点样式请使用page选择器。
  • 小程序目前不支持Media Query。

2、扩展的特性

  • 尺寸单位rpx
    rpx为小程序自创的单位,可以根据屏幕宽度进行自适应。规定屏幕宽为750rpx。如在iPhone6上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。

建议:开发微信小程序时设计师可以用 iPhone6 作为视觉稿的标准。

注意: 由于数值较小时渲染时会存在四舍五入的情况,在较小屏幕上差距会很大,所以要求精确而较小的视图内容需避免使用此单位。

如下图所示菜品的减号操作图标,高度iPhone6(750)下是2px,iPhone4s(640)下直接渲染成了1px(实际比例值为1.7px),而加号按钮图标高度iPhone6(750)下是11px,iPhone4s(640)下渲染成了9px(实际比例值为9.48px),误差比例较小没有出现明显视觉问题,所以两者看起来会不协调。

  • 样式导入
    用@import语句可以导入外联样式表,@import后跟需要导入的外联样式表的相对路径,用;表示语句结束。

组件

如上WXML中所述,组件是视图层的基本组成单元,与HMTL中标签作用类似,基于Web Component标准,属性和内容的使用方法也和HTML标签类似,组件和属性都须小写。

1、组件列表

2、原生组件

如上统计,input、textarea、video、map、canvas为系统原生组件。原生组件相对来说性能和用户交互方面会有所提升。

以部分机型input元素fixed时唤起键盘被遮挡的问题举例,在某魅族机型上H5页面中父元素fixed的输入框会被遮挡:

同一机型小程序中,输入框不会被遮挡:

3、组件属性

支持类型

  • Boolean:布尔值
  • Number:数字
  • String:字符
  • Array:数组
  • Object:对象
  • EventHandler:事件处理函数名,事件绑定(如bindtap)属性
  • Any:任意属性(不是很明白是什么意思)

共同属性

  • id:组件的唯一标识
  • class:组件的样式类,和wxss中定义的class选择器对应
  • style:内联样式
  • hidden:组件是否显示
  • data-*:自定义属性,可传入自定义数据,逻辑层事件处理函数中通过e.currentTarget.dataset获取。
  • bind / catch:都是事件绑定,bind事件绑定不会阻止冒泡事件向上冒泡,catch事件绑定可以阻止冒泡事件向上冒泡。

特殊属性
特殊属性是各个组件自己定义的属性,如 <icon> 组件的size属性,具体各参见官方文档各组件具体说明。

兼容性

渲染机制
根据官方文档的说明:

  • 在iOS 上,小程序的javascript 代码是运行在JavaScriptCore 中,是由WKWebView来渲染的,环境有iOS8、iOS9、iOS10;
  • 在Android上,小程序的javascript代码是通过X5 JSCore来解析,是由 X5 基于Mobile Chrome 37 内核来渲染的;
  • 在开发工具上,小程序的javascript代码是运行在nwjs中,是由Chrome Webview来渲染的。

由于内核渲染表现不一致,H5开发过程中存在X5浏览器和各类机型或系统的兼容性的部分问题小程序中依旧存在。

常见问题分类

  • X5浏览器
    X5前端开发问题
    X5 Caniuse Tests
  • iOS/Android版本导致的兼容性问题:小程序在iOS/Andriod 系统中只要求微信版本 >= 6.5.3,对机型的系统版本并无限制。
  • Android机型碎片化导致的兼容性问题

性能优化

前端常用的模板方案一般有2种:

  • 1、将模板编译成js函数代码,通过字符串拼接的方式生成渲染的DOM节点,如:Mustache / tpl(点评内部开发使用),数据更改时DOM节点全部更新;
  • 2、字符串parse和compile后拼接渲染外,有自己的DOM节点更新机制, 如:Vue.js / React等,数据更改时通过DOM Diff算法更新DOM节点。

当数据改变触发渲染层重新渲染的时候,会校正带有key的组件,框架会确保他们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。

小程序对组件的渲染方式我们不得而知,只能对开发中碰到的一些问题来推测。结合小程序对列表渲染wx:key的解释可知其模板渲染属于第二种,数据更新时会根据key进行渲染优化。但小程序官方未提供相关接口或性能调试工具,所以项目中我们只能自己尝试不同方案然后对比渲染速度。以菜单页面为例,商户菜品数量多者成百上千,优化后的效果对比还是比较明显。

采取方案

  • 1、减少数据嵌套层数/减少组件嵌套层数:菜单页面将菜品数据扁平化为一层,并合理利用key值;设计组件结构时采用精简的组件结构,减少渲染时的数据遍历和组件嵌套深度带来的性能消耗。
  • 2、将数据变动的组件与数据不变的组件进行拆分,减少数据更改带来的组件更新量,如将加减按钮和菜品信息分离。
  • 3、使用动态加载等方式减小首屏渲染数据量,提升用户体验。

本文时间为2017-02-24,所提小程序暂不支持属性或碰到的bug以此时间为准,后续更新或修复请查看官方文档

参考资料:
微信小程序开发者文档