# 「大众点评点餐」小程序开发经验 02：视图

- 作者：知晓程序
- 发布时间：2017-03-16 12:47
- 原文：https://www.ifanr.com/minapp/803083

![](https://s3.ifanr.com/wp-content/uploads/2017/03/Featured-12.jpg)

文 \| 何延希

何延希，美团点评工程师，4 年 web 开发经验，现在是美团点评点餐团队的一员。

在[上一期](http://www.ifanr.com/minapp/799963)，知晓程序（微信号 zxcx0101）与大家分享了「大众点评点餐」小程序团队带来的小程序技术分析。

本期，我们想要和大家分享下大众点评点餐小程序中，有关 View 视图层的一些开发经验。

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdpdc-qr.jpg)

本文部分示例来自于「大众点评点餐」小程序的菜单页面。

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp.jpg)

页面代码结构为：

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

我们将要说的小程序的 View 视图层，是由 WXML（`menu.html`）与 WXSS（`menu.less`）两大部分组成，由组件——也就是视图的最小单元——进行展示。

**视图层将逻辑层的数据（`menu.js` 和 `menu.json`）反应为视图，同时将视图层中定义的事件发送给逻辑层。**

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp-1.jpg)

### WXML

**WXML（WeiXin Markup Language）与 HTML 对应，用于描述页面的结构**，可以类比 React 的 JSX。项目中 `menu.html` 使用 WXML 语法，一个页面的顶层是 `page` 节点。

在 WXML 中获取逻辑层定义的数据后，我们通过一系列自己的语法和逻辑展示出这些数据。

结构上，组件是视图层的最小单元。我们可以通过以下方式，进行动态渲染。

#### 1. 数据绑定

**数据绑定是最简单的使用数据方式。**采用 Mustache 语法的变量替换，用双大括号将变量名包起来，包括组件的属性也可以使用变量。

```
<view <span class="hljs-class"><span class="hljs-keyword">class</span></span>=<span class="hljs-string">"dish-item"</span> data-id=<span class="hljs-string">"{{dishId}}"</span>>
<span class="xml"><span class="hljs-tag">  <<span class="hljs-name">text</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"name"</span>></span>{{dishName}}<span class="hljs-tag"></<span class="hljs-name">text</span>>
</span></span><span class="xml"><span class="hljs-tag"></<span class="hljs-name">view</span>></span></span>
```

小程序还支持 ES 6 规范的扩展运算符 `...` 和解构赋值。

```
<template is=<span class="hljs-string">"dishItem"</span> data=<span class="hljs-string">"{{...item, count, soldout: true }}"</span>><span class="xml"><span class="hljs-tag"></<span class="hljs-name">template</span>></span></span>
```

#### 2. 逻辑运算

**双大括号中，可进行一些简单运算操作**，包括四则运算、三目运算、逻辑判断、字符串拼接等。

```
<text <span class="hljs-class"><span class="hljs-keyword">class</span></span>=<span class="hljs-string">"{{orderBanner.type !== 0 ? 'order-banner arrow' : 'order-banner'}}"</span>>
  {{orderBanner.text}}
<<span class="hljs-regexp">/text></span>
```

#### 3. 条件渲染

与通常将渲染内容写在 `if` 或 `else` 判断条件中不同，**小程序的条件渲染，要求将条件直接写在相应组件的 `wx:if` 与 `wx:else` 属性中。**

**如果渲染组件为多个，可将多个组件放在组件内，渲染条件置于 `<block>` 组件的 `wx:if` 与 `wx: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:<span class="hljs-keyword">for</span>=<span class="hljs-string">"{{testData}}"</span>
    wx:<span class="hljs-keyword">for</span>-item=<span class="hljs-string">"mainitem"</span>
    wx:key=<span class="hljs-string">"{{mainindex}}"</span>
    wx:<span class="hljs-keyword">for</span>-index=<span class="hljs-string">"mainindex"</span>>
    <span class="xml"><span class="hljs-tag"><<span class="hljs-name">view</span> <span class="hljs-attr">wx:for</span>=<span class="hljs-string">"{{mainitem}}"</span>
        <span class="hljs-attr">wx:for-item</span>=<span class="hljs-string">"subitem"</span>
        <span class="hljs-attr">wx:key</span>=<span class="hljs-string">"{{subitem.id}}"</span>
        <span class="hljs-attr">wx:for-index</span>=<span class="hljs-string">"subindex"</span>></span>
        <span class="hljs-tag"><<span class="hljs-name">view</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"dom-item"</span>></span>第一层 index: {{mainindex}} 
                                     id: {{subitem.id}} 
                                     name: {{subitem.name}}<span class="hljs-tag"></<span class="hljs-name">view</span>></span>
    <span class="hljs-tag"></<span class="hljs-name">view</span>></span></span>
<<span class="hljs-regexp">/block></span>
```

以上代码结构上分为两层：

- 第一层 `block` 循环遍历 `testData` 数组，每个遍历值变量名为 `mainitem`。
- 第二层 `view` 循环遍历 `mainitem` 数组，每个遍历值变量名为 `subitem`，展示第一层 `index`、第二层 `id` 和 `name` 属性；

```
<span class="hljs-comment">// 创建页面实例对象</span>
Page({
    <span class="hljs-comment">/**
    * 页面的初始数据
    */</span>
    data:  {
        <span class="hljs-string">"testData"</span>: [
            [ {
                <span class="hljs-string">"id"</span>: <span class="hljs-string">"1-1"</span>,
                <span class="hljs-string">"name"</span>: <span class="hljs-string">"节点1 - 1"</span>
            }, {
                <span class="hljs-string">"id"</span>: <span class="hljs-string">"1-2"</span>,
                <span class="hljs-string">"name"</span>: <span class="hljs-string">"节点1 - 2"</span>
            }], [{
                <span class="hljs-string">"id"</span>: <span class="hljs-string">"2-1"</span>,
                <span class="hljs-string">"name"</span>: <span class="hljs-string">"节点2 - 1"</span>
            }, {
                <span class="hljs-string">"id"</span>: <span class="hljs-string">"2-2"</span>,
                <span class="hljs-string">"name"</span>: <span class="hljs-string">"节点2 - 2"</span>
            }]
        ]
    }
})
```

展示结果：

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp-2.jpg)

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

例如，在上面例子中，将 `testData` 换成对象类型：

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

结果为：

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp-3.jpg)

#### 5. 模板 &amp; 引用

小程序中的模板，概念类似于 React 中的组件（components）。

**我们可以在模板中定义代码片段，然后在不同的地方进行调用**，减少重复的代码量。

如何定义一个模板呢？**我们使用 `name` 属性，作为模板的名字，然后在 `<template/>` 内定义模板代码片段就可以了**。

定义后的使用方式有 2 种：

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

需要注意的几个地方：

- 小程序只会 `import` 目标文件中定义的 `<template/>`，不能引用目标文件中引用的 `<template/>`。
- 小程序的模板中，只能单向使用传入的数据，不像 React 可以利用 props 让父子组件进行传值。

我们以单个菜品组件为例，看看如何在小程序中使用模板：

```
<<span class="hljs-keyword">import</span> src=<span class="hljs-string">"../../components/common/dish-item.wxml"</span> />
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">template</span> <span class="hljs-attr">is</span>=<span class="hljs-string">"dishItem"</span> <span class="hljs-attr">data</span>=<span class="hljs-string">"{{...item}}"</span>></span><span class="hljs-tag"></<span class="hljs-name">template</span>></span></span>
```

#### 6. 绑定事件

**事件名称为字符串，会默认传入 `event` 参数，无法定制其他参数。**

我们一般将所需参数通过 `data-` 属性，绑定至组件，再通过 `e.currentTarget.dataset` 获取。例如这样：

```
<view <span class="hljs-class"><span class="hljs-keyword">class</span></span>=<span class="hljs-string">"cart-btn"</span> data-type=<span class="hljs-string">"1"</span> bindtap=<span class="hljs-string">"redirectCart"</span>>选好了<<span class="hljs-regexp">/view></span>
```

### WXSS

WXSS（WeiXin Style Sheet）与 CSS 对应，用于描述页面的样式。

**定义在 `app.less` 中的样式为全局样式**，可作用于每一个页面。

**在页面里的样式文件中定义的样式为局部样式**，只作用在对应的页面，并会覆盖 **`app.less` 中相同的选择器。**例如，代码结构中 `menu.less` 能且只能作用于 `menu.html`。

#### 1. 支持的特性

**WXSS 支持内联样式和选择器两种特性。**

小程序组件的 `style` 可以接收动态的样式，会在运行时会进行解析。但请尽量避免将静态的样式写进 `style` 中，以免影响渲染速度。

WXSS 支持选择器。对于常用的选择器，小程序目前支持以下这些：

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp-4.jpg)

目前不支持的选择器有：

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp-5.jpg)

此外，还有几个需要注意的地方：

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

#### 2. 扩展的特性

**在 CSS 的基础上，WXSS 还扩展了几个特性。**

首先是尺寸单位 RPX。这是小程序自创的单位，可以根据屏幕宽度进行自适应。

RPX 将所有手机的屏幕宽度规定为 750rpx。例如，在 屏幕宽度为 375 px 的 iPhone 6 上，换算出来 1 rpx = 0.5 px = 1 物理像素。

我们建议设计师在开发微信小程序时，可以用 iPhone 6 作为视觉稿的标准。

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

例如，下图所示菜品的减号操作图标的高度，iPhone 6 下是 2 px，iPhone 4s 下直接渲染成了1 px（实际比例值为 1.7 px）。

而加号按钮图标高度，在 iPhone 6 下是 11 px，iPhone 4s 下，就渲染成了 9 px（实际比例值为 9.48 px）。

这样的差距，就会让小程序在两台手机上，看起来不那么协调了。

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp-6.jpg)

关注微信号 zxcx0101，在后台回复「rpx」，一篇文章带你看懂 RPX。

此外，**在小程序中使用 `@import` 语句，可以导入外联样式表**。

具体的使用方式是：在 `@import` 后，写上需要导入的外联样式表的相对路径，用 `;` 符号表示语句结束。

### 组件

如前面 WXML 部分中所述，**组件是视图层的基本组成单元**。

它与 HTML 中的标签类似，基于 Web Component 标准，属性和内容的使用方法也和 HTML 标签类似。

组件名称和属性名称，都必须使用小写。

#### 1. 组件列表

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp-7.jpg)

#### 2. 原生组件

如上统计，**`input`、`textarea`、`video`、`map`、`canvas` 均为系统原生组件。**

原生组件相对来说性能和用户交互方面会有所提升。

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

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp-8.jpg)

在同一机型中，小程序里的输入框就不会被遮挡。

![](https://s3.ifanr.com/wp-content/uploads/2017/03/dzdp-9.jpg)

#### 3. 组件属性

小程序的组件中，支持以下的数据类型：

- `Boolean`：布尔值
- `Number`：数字
- `String`：字符
- `Array`：数组
- `Object`：对象
- `EventHandler`：事件处理函数名，事件绑定属性（如 `bindtap`）
- `Any`：任意属性（不是很明白是什么意思）

所有组件都有的共同属性：

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

此外，各个组件都有自定义的特殊属性，如 `<icon>` 组件的 `size` 属性。你可以在官方文档中查阅每个组件的不同属性。

### 兼容性

根据官方文档的说明：

- 在 iOS 上，小程序的 JavaScript 代码是运行在JavaScriptCore 中，是由 WKWebView 进行渲染，可用环境有 iOS 8、iOS 9、iOS 10。
- 在 Android 上，小程序的 JavaScript 代码通过 X5 JSCore 解析，由 X5 基于Mobile Chrome 37 内核进行渲染；
- 在开发工具上，小程序的 JavaScript 代码运行在 NW.js，由 Chrome WebView 进行渲染。

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

### 性能优化

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

- **将模板编译成 JS 函数代码，通过字符串拼接的方式生成渲染的 DOM 节点。**例如：Mustache / tpl（点评内部开发使用），数据更改时，会将 DOM 节点全部更新。
- **字符串 parse 和 compile 后拼接渲染外，有自己的 DOM 节点更新机制。**例如：Vue.js / React等，数据更改时通过 DOM Diff 算法更新 DOM 节点。

**当数据改变触发渲染层重新渲染的时候，会校正带有 key 的组件。**框架会确保他们被重新排序，而不是重新创建。

这样做，我们可以确保组件保持自身的状态，并且提高列表渲染时的效率。

小程序对组件的渲染方式我们不得而知，只能对开发中碰到的一些问题来推测。

结合小程序对列表渲染 wx:key 的解释，可知**小程序的模板渲染属于第二种，数据更新时会根据 key 进行渲染优化**。

但小程序官方未提供相关接口或性能调试工具，所以项目中我们只能自己尝试不同方案然后对比渲染速度。

以菜单页面为例，商户菜品数量多者成百上千，优化后的效果对比还是比较明显。

由以上的描述，我们可以得出以下的优化建议：

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

原文地址：https://juejin.im/post/58affbff2f301e006cfc5b46

**本文由知晓程序授权转载，关注微信号 zxcx0101，在微信后台回复「点餐」，获取「大众点评点餐」小程序全套开发经验。**

![zxcx&#95;0208](https://s3.ifanr.com/wp-content/uploads/2017/02/zxcx_0208.png)
