「大众点评点餐」小程序开发经验 【整合版】

96
鸡汤婊
0.4 2017.04.17 13:00* 字数 12959

声明:文章来源网络转载,我在这做了一个整合,方便大家查阅,持续更新。

01:概述

文 | 周中坚

美团点评工程师,4年 Web 前端开发经验,主要负责过会员卡、外卖、预订、商家平台等业务的前端开发,现在是美团点评点餐团队的一员。

如果你看过《张小龙首次全面阐述小程序》这篇文章,一定会对这句话有印象:

比如我们到一个餐馆,我们可能想排队或者说点一下菜,我们并不需要去下载这个餐馆的应用程序,我们只需要在餐馆扫一下它的二维码,然后就启动了这个餐馆的小程序,我们可以立即在小程序里排队或者点餐。

没错,我们就是做张小龙在演讲时,提到做「点餐」的大众点评点餐团队。

我们团队在去年年底开始考虑微信小程序平台,经过快速而慎重的调研、讨论、开发、测试,以及在微信同事的帮助下,「大众点评点餐」小程序终于在 2017 年 1 月上线。

我们计划在两个月内,推出 7 篇关于小程序的专栏文章。这一系列文章是,我们前端团队做小程序时积累的经验,里面不仅有小程序的原理,还有我们开发过程中遇到的问题和解决办法。

功能简介

大众点评点餐小程序是一个工具,本着方便好用的初衷,我们设计的第一版的「大众点评点餐」小程序交互流程非常简单,用户可以在小程序中完成选择菜品,确认下单,追踪订单状态这个完整的点餐流程。

小程序设计

相信这篇文章的很多读者,都有移动端开发经验。微信小程序也是移动端应用,也应该符合大部分的移动端的设计规范。

那么在设计上,微信小程序和 app、hybrid、HTML 5 又有什么区别呢?

首先由于小程序是一个平台,所以平台上的开发者必须要遵守规范。建议大家参考官方的设计文档。

除此之外,还需要注意:

用户首次使用要下载离线包,首页需要加载效果,以降低用户等待的时间感知。

小程序会记录用户的状态,当用户再次扫码时会打开之前的页面。

小程序最多可以打开 5 级页面,如果页面层级过深,旧的页面可能会被销毁。

小程序渲染长列表可能有性能问题,最好做分页。

小程序不支持与 app、HTML 5 之间的跳转。

小程序架构

微信小程序的框架包含两部分:View 视图层、App Service 逻辑层。

View 层用来渲染页面结构,使用 WebView 渲染。

App Service 层用来逻辑处理、数据请求、接口调用。

在 iOS 上,执行 App Service 解析与处理操作的解析器是 JSCore。

而在 Android 上,这个任务由 X5 JS 解析器完成。

我们使用的开发者工具,则是 NW.js + Chrome 内核。

视图层和逻辑层,通过系统层的 JSBridge 进行通信。逻辑层把数据变化通知到视图层,触发视图层页面更新;视图层把触发的事件通知到逻辑层进行业务处理。

视图层由 WXML 与 WXSS 编写,由组件来进行展示。

小程序启动时,会从 CDN 下载小程序的完整包。编译后的小程序包的大小,被微信官方限制在了 1 MB 以内。

开发选型

小程序开发和传统的 HTML 5 开发,有许多差异。比如:

小程序开发暂不支持 NPM 包管理方式,官方推荐是将依赖拷贝到项目中。

小程序视图层的 WXML 和 WXSS 文件,还是会使用 webview 进行渲染,开发者需要关心在不同平台上的兼容性。

小程序逻辑层的运行环境,对 ES6 的支持并不完美。开发者需要关心在不同平台上的兼容性。

小程序的开发者工具在补全、语法检查等方面还不如我们熟悉的 IDE(但相信微信团队会越做越好)。

可以看到,小程序和我们现在熟悉的前端开发模式还是有不小的区别,一定程度上会影响我们的开发效率。因此:

我们将工程分为开发目录和构建目录,构建目录有两部分,一部分是将要上传到 CDN 的图片,一部分是小程序的运行代码。

在开发目录中,使用.html和.css后缀(这样就可以将项目导入 IDE 了)。

再通过 Gulp 将开发代码进行处理,再写入到构建目录中。

使用 ES6 语法,配合 Eslint,快速检查语法错误。

所有异步请求使用 Promise 封装,增加代码可读性,便于捕捉错误。

小程序与 PWA

PWA 的全称是 Progressive Web Apps ,是 Google 在 2015 年提出的概念,是渐进增强理念的一个典型实践。

PWA 不是一套全新的标准,是现有 web 技术的父集。但是微信小程序可以认为是现有 web 技术的一个子集。

那为什么要在这里提到 PWA 呢?是因为 PWA 和微信小程序用不同的思路在解决很多共性的问题。

当然,在实现思路上,微信小程序和 PWA 的差别非常大,甚至让二者的本质变得非常不同。

PWA 是开放的,可分享,可搜索的;而微信小程序是封闭的,仅可在微信内分享,仅可在微信内进行非常有限的搜索,不可以跳转到别的 web 或者 app。

02:视图

文 | 何延希

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

上一期,知晓程序(微信号 zxcx0101)与大家分享了「大众点评点餐」小程序团队带来的小程序技术分析。

本期,我们想要和大家分享下大众点评点餐小程序中,有关 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 语法的变量替换,用双大括号将变量名包起来,包括组件的属性也可以使用变量。

{{dishName}}

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

2. 逻辑运算

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

  {{orderBanner.text}}

3. 条件渲染

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

如果渲染组件为多个,可将多个组件放在组件内,渲染条件置于组件的wx:if与wx:else属性中。此时的组件,只充当容器作用,页面中不会渲染。

我们来看条件渲染实际应用的例子:

已售完

用上的实例:

点小评去吃满汉全席啦~重试

4. 列表渲染

列表渲染,是将元素进行遍历,并利用wx:for属性值进行循环渲染。

与此相关的还有以下几个属性:

wx:key:遍历元素的唯一的标识符,主要用于数据动态变化时,DOM 的更新机制。数据不变,则可无视。

wx:for-item:遍历元素的变量名,默认值为item

wx:for-index:遍历元素下标的变量名,默认index

以上属性的值都可以用字符串,但值中不要使用-等符号。例如dish-item,在使用时,小程序会将{{dish-item}}中的-解析成减号,造成取值失败。

在这里,我们利用测试数据举个例子:

第一层 index: {{mainindex}}

id: {{subitem.id}}

name: {{subitem.name}}

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

第一层block循环遍历testData数组,每个遍历值变量名为mainitem。

第二层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进行属性遍历。此时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"}]        }    }})

结果为:

5. 模板 & 引用

小程序中的模板,概念类似于 React 中的组件(components)。

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

如何定义一个模板呢?我们使用name属性,作为模板的名字,然后在内定义模板代码片段就可以了

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

使用include方式,将目标文件除了部分外的整个代码引入。这样的操作,相当于是将整个文件里的代码拷贝到include位置,所以无法传入参数。

使用import方式,引入定义的文件,然后通过组件的is属性,声明需要的使用的模板,然后将模板所需要的data传入。这样的模板拥有自己的作用域,只能使用data传入的数据。

需要注意的几个地方:

小程序只会import目标文件中定义的,不能引用目标文件中引用的。

小程序的模板中,只能单向使用传入的数据,不像 React 可以利用 props 让父子组件进行传值。

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

6. 绑定事件

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

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

选好了

WXSS

WXSS(WeiXin Style Sheet)与 CSS 对应,用于描述页面的样式。

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

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

1. 支持的特性

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

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

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

目前不支持的选择器有:

此外,还有几个需要注意的地方:

如之前提到,页面的顶层是节点,所以想要修改作用于整个页面的样式、顶层节点样式,请使用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)。

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

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

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

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

组件

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

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

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

1. 组件列表

2. 原生组件

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

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

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

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

3. 组件属性

小程序的组件中,支持以下的数据类型:

Boolean:布尔值

Number:数字

String:字符

Array:数组

Object:对象

EventHandler:事件处理函数名,事件绑定属性(如bindtap)

Any:任意属性(不是很明白是什么意思)

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

id:组件的唯一标识

class:组件的样式类,和在 WXSS 中定义的类选择器对应

style:内联样式

hidden:组件隐藏或显示

data-*:自定义属性,可传入自定义数据。逻辑层事件处理函数中,可通过e.currentTarget.dataset获取。

bind和catch:都是事件绑定,差别在于:bind不会阻止事件向上冒泡,catch可以阻止事件向上冒泡。

此外,各个组件都有自定义的特殊属性,如组件的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 值。

设计组件结构时采用精简的组件结构,减少渲染时的数据遍历和组件嵌套深度带来的性能消耗。

将数据变动的组件与数据不变的组件进行拆分,减少数据更改带来的组件更新量,如将加减按钮和菜品信息分离。

使用动态加载等方式减小首屏渲染数据量,提升用户体验。

03:事件联动

文 | 李超

李超,美团点评前端开发工程师,2 年 web 开发经验,现在是美团点评点餐团队的一员。

在我们团队的小程序开发经验系列多篇文章发布以后,你是否对小程序视图层逻辑层,以及官方 API 文档有更为深入的学习和了解呢?

「纸上谈兵」很容易,「打好胜仗」才是关键。

今天,知晓程序(微信号 zxcx0101)为大家分享,开发「大众点评点餐小程序」菜单页面的过程中,遇到的问题和解决方案。

产品需求与最终效果

如果你看过我们的系列文章, 应该对我们的产品形态有了初步了解。我们是做点餐菜单服务,菜单需要分类,需要购物车模块,那么典型的「工」字型布局是我们的首选。

大体结构为:顶部商家名称,可能会出现黄色横条提示模块;下方左侧为导航菜单栏;下方右侧为每个菜单分类包含的菜品展示列表;底部可能出现购物车模块。

结合上面的图片,菜单页的结构的交互需求很容易就整理出来了:

顶部要求显示商家名称,有分享功能。

下方左侧、右侧可分开滚动,滚动左侧不影响右侧,滚动右侧左侧随之联动高亮显示所在的菜单分类。

点击下方左侧导航菜单栏,高亮显示被点击的菜单分类,下方右侧对应分类详情模块顶部与右侧滚动区的顶部重合(类似于 HTML 里的锚点功能)。

滚动下方右侧菜品分类详情时,当该分类详情模块顶部接触到滚动区域的顶部,左侧对应的导航菜单栏高亮。

若左侧高亮的导航菜单不在可视区域:

当高亮的导航菜单顶部在左侧scroll-view滚动区上方(被遮住了),则将该高亮导航菜单滚动至将高亮导航栏的顶部与左侧可滚动区域顶部重合(高亮菜单为滚动区的第一个分类)。

当高亮的导航菜单在左侧scroll-view滚动区可视区下方,将高亮导航菜单滚动到屏幕中央区域。

顶部下方可能会出现黄条提示文案模块。

底部上方可能会出现购物车模块。

顶部黄条提示文案模块吸顶,底部购物车模块吸底。

需要适配各种不同机型。

关键技术罗列

这里需要指出:产品在设计成稿之前,我们已经对小程序支持的功能做了细致的调研,确保可以通过技术手段实现产品需求,才确定 UI 以及交互设计。

从产品兼容性角度出发,我们考虑使用微信小程序的rpx作为 UI 设计的标准尺寸。

该尺寸和rem非常类似,不同点在于其对基准尺寸的设定。

rem使用文档根元素设定的尺寸作为基准尺寸,而rpx使用手机屏幕宽度为基准,决定1 rpx对应的宽度,该动态尺寸对设备的兼容性更加友好。

此外,微信还自带scroll-viewUI组件,并提供一系列组件状态操作接口。

当scroll-view组件滚动时,会触发scroll事件。所返回的event对象各项长度属性,均使用px作单位。

开始开发

菜单的页面结构如下:

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

我们在开发中使用工具对文件实时编译:

`menu.html`->`menu.wxml`

`menu.less`->`menu.wxss`

为方便代码维护以及日常的开发习惯,我们对 Less 语法进行兼容,引入了Promise组件。

WXML 页面布局

### menu.html      // 黄色横条提示模块 // 左侧分类导航 // 右侧分类详情 // 购物车模块

这里着重考虑两个scroll-view结构设计,左右的布局结构可以使用 CSS 样式属性float,或者是 CSS 3 的flex

另外,黄条提示和购物车模块,都可以用fixed属性搞定。

微信官方文档介绍,使用scroll-view组件,必须指定高度

我们实践时发现,使用scroll-view可以不指定高度,页面有滚动区存在。但这么做,滚动时无法触发scroll事件,也就无法完成联动设计

滚动区域检测

在这里,我们需要注意两点:

必须使用px作为单位。

必须在scroll-view上显式的指定其height属性

在获取滚动区高度windowScrollHeight之前,我们需要考虑其影响因素:

设备高度

黄条文案提示模块的存在

购物车模块的存在

从rpx到px的转换

设备高度可以通过微信官方 APIgetSystemInfo接口进行获取。

那么,该什么时候调用接口?

首先这是一个异步 API 接口,另外其直接受系统权限控制的影响,基于这两点因素,其结果返回的时机就不是确定的

我们可以在小程序启动时在onLaunch中调用该 API,然后将获取的结果放入到全局变量globalData中。

而globalData是挂在在全局App元素上的属性,对所有页面均可见。

现在来看看,利用系统信息接口获取到的数据是如何的:

sysInfoObject{errMsg:"getSystemInfo:ok"language:"zh_CN"model:"iPhone 6"pixelRatio:2platform:"devtools"system:"iOS 10.0.1"version:"6.3.9"windowHeight:627windowWidth:375}

这里的windowHeight,windowWidth指的是屏幕高度和宽度,且使用的单位是px。

在实际代码中,调用系统信息的接口代码就是这个样子:

// app.js// 注意这里的wxp为我们对wx的封装,它继承wx的所有属性,特点是若调起wx的异步api函数将返回一个Promise实例。getSysInfo:function(){letthat =this;if(that.globalData && that.globalData.sysInfo            && that.globalData.sysInfo.windowHeight) {// 将结果封装成Promise,后续可统一使用`then`方法returnPromise.resolve(that.globalData.sysInfo);        }returnwxp.getSystemInfo()            .then(res=>{                that.globalData.sysInfo = res;returnres;            })            .catch(e=>{// 可以尝试弹出框或toastconsole.error('[getSystemInfo]', e);            });  },// menu.jsonLoad:function(){    app.getSysInfo().then((sysInfo)=>{// transform rpx -> px and calculate scroll-view height.}}

计算fixed元素高度

黄条文案提示模块、购物车模块的高度都是已知的。

但大家应该记得这样的设计细节:所有的元素统一使用rpx做单位,而这里需要使用px作单位,必须要进行单位转换

从rpx到px的转换

varyellowBarRpxHeight =50;// 黄色文案提示模块高度varpercent = app.data.sysInfo.windowHeight /375;// 当前设备1rpx对应的px值varyellowBarHeight =Number(yellowBarRpxHeight * percent).toFixed(2);

大家对375 这个数字是否有疑问呢?该比值是否会受到设备实际像素点的影响呢?实际上,你并不需要担心它。

同样的道理,我们可以得到购物车模块的高度cartBarHeight。

通过公式:windowScrollHeight = windowHeight - yellowBarHeight - cartBarHeight,可以计算得出两个scroll-view的滚动高度。

左侧菜单栏与右侧菜品栏的联动

首先我们要做到:点击左侧导航菜单栏,右侧定位到对应的分类菜品详情。

通过查看scroll-view文档发现可以使用scroll-into-view属性;该组件自动定位右侧需要滚动到的具体位置。

首先给左侧导航菜单栏绑定tap事件监听函数,事件触发后获取event对象象的currentTarget属性,取出渲染时存放在该节点上的分类id,用此id作为唯一标识定位右侧分类详情。

然后,设置右侧scroll-view的scroll-into-view属性,这时,它会将右侧scroll-view上id属性值为该值的节点滚动到滚动区域的顶部。

点击事件监听函数

// menu.jsbindLeftTap (e) {// 由于事件是冒泡的,所以不确定点击操作是在哪个元素上触发的,但currentTarget表示当前绑定事件对应的节点,便可准确获取该节点上的datasetlet dataset = e && e.currentTarget && e.currentTarget.dataset;varLEFT_TO_RIGHT_SUFFIX ="l2r-";if(!dataset || !dataset.id)return;// targetthis.setData({            highlightCategoryId: dataset.id,// 左侧高亮的导航菜单栏rightToView: LEFT_TO_RIGHT_SUFFIX + dataset.id,// 更新右侧的scroll-to-view属性。});    }

LEFT_TO_RIGHT_SUFFIX是全局定义的常量,只是为了方便大家阅读,才将其写入函数内部,用作id拼接,保证唯一性。

在开发阶段,我们曾经尝试直接将获取到的id作为rightToView的值,也就是设定右侧scroll-view的scroll-into-view属性。

但我们发现,右侧scroll-view不会因此滚动到指定的高度

我们猜想,可能是因为获取到的dataset.id是一个数字类型字符串,其内部使用===方式导致不匹配。

另外需要注意的是,设置scroll-into-view引起的滚动操作,同样会触发scroll事件

右侧栏滚动事件与分类栏自动滚动

滑动右侧、让左侧滚动,是整个页面设计最核心的部分。

由于小程序无法获取元素的宽高,位置信息,对滚动右侧实现左侧联动效果带来挑战。

如何准确的获取右侧滚动到的具体分类,并让左侧导航菜单栏相应分类高亮,且在可视的范围内?

在设计阶段,我们和设计同学确认右侧每个视觉模块固定的高度,包括菜品模块高度、分类小灰条高度等。

这样,我们就可以根据已有的数据结构计算出每个元素距离文档区顶部的高度。

// PER_BAR_HEIGHT 分类小灰条的高度

//PER_ITEM_HEIGHT 单个菜品详情的高度varsumScrollHeight =0;varassistantCategories = spuMenuSet.map(it=> {letunitHeight = PER_BAR_HEIGHT + (it.spuMenuItemList &&it.spuMenuItemList.length ) * PER_ITEM_HEIGHT;it.scrollHeight = sumScrollHeight;    sumScrollHeight += unitHeight;returnit;});

左侧导航菜单栏高亮分类切换的边界条件为右侧分类菜单详情的分类小灰条顶部与右侧滚动区顶部重合。

我们需要做的,就是计算出每个分类小灰条距离文档顶部的高度scrollHeight,并在每次滚动事件触发时,比较当前滚动的高度与分类小灰条的滚动高度scrollHeight。

这样做,就可确定当前在哪个分类菜单详情区域内,从而实现左侧分类导航栏的高亮。

长度单位误差

在测试时发现,有些机型滚动下方右侧scroll-view时,在边界条件出现时并不会完成左侧导航菜单栏高亮分类的切换,往往存在10-100px的误差。

从产品角度,这种误差是不能容忍的。个人并不确定是什么原因导致误差的出现,但看起来并没有非常好的解决办法。

那么能用什么方案减少误差呢?我的实现思路是「人工干预自动校正」。

仔细分析滚动事件返回的event对象:

ObjectcurrentTarget:Objectdetail:ObjectdeltaX:0deltaY:-971scrollHeight:24737scrollLeft:0scrollTop:2409scrollWidth:295__proto__:Objecttarget:Objectdataset:Object__proto__:Objectid:""offsetLeft:0offsetTop:38__proto__:ObjecttimeStamp:13932type:"scroll"__proto__:Object

在这里,我们需要特别留意detail中的scrollHeight。

滚动事件会给出整个scroll-view文档内容的高度,这个高度值非常关键,我们可以这样计算出来:

scrollHeight = 单个菜品详情高度 * 菜品总数 + 单个分类小灰条高度 * 分类小灰条总数。

由于单个菜品详情高度与单个分类小灰条高度的高度比是确定的,所以上面的方程式为一元方程。

我们可以用它计算出单个菜品详情高度,以及单个分类小灰条高度,更新每个分类小灰条距离文档顶部的距离scrollTop值。

经测试发现,左侧导航菜单栏高亮分类的切换精度非常高,而且兼容性很好。

左侧高亮分类跳错

在实际开发中, 我还发现一个问题: 左侧有分类 A、B、C,点击分类 B,分类 B 高亮,右侧定位到分类 B 的详情区域,随之左侧高亮分类切换到 A 上。

想一想,这是什么原因导致的?

在上面讲解scroll-view属性时,我提到过一句话:

设置scroll-into-view引起的滚动操作,同样会触发scroll事件。

这里点击左侧分类,右侧由于scroll-into-view触发了滚动事件,而相应的滚动事件监听函数函数,计算得出当前高亮的导航菜单栏为 A,更新页面的data将高亮分类切换到了 A 上。

解决方案有这两种:

修改边界条件。

限制右侧的scroll事件函数的执行。

在这里,我推荐使用第二种方式,因为在不同机器上存在细微差别,我们无法准确的设置误差范围,元素宽高都是计算得出。

思路:若点击左侧导航菜单栏,设定全局锁定状态,若锁定则不右→左的联动操作,再解除锁定状态。

分类导航栏的可视性

通过上面「右→左」联动,我们已经可以让左侧随着右侧滚动而高亮。

但随之而来的问题是:左侧也是一个scroll-view,如何保证高亮的分类,刚好在可视区域里(屏幕上)呢?

监听右侧滚动事件,判断当前在哪一个分类上,确定该分类在左侧scroll-view的文档高度,判断是否需要滚动左侧scroll-view。

我们可以通过scroll-view的scroll-into-view,或者在scroll-top属性里完成滚动。

// 这里是伪代码实现varindex = mapId2index(id);//将id转换为对应分类的index值varperCateHeight =40;// 左侧每个分类高度为40varleftScrollTop =0;// 左侧scroll-view滚动的高度varwindowScrollHeight =1440;// 这个值为屏幕高度,可通过getSystemInfo获取到varcHeight = index * perCateHeight;// 当前分类距离文档顶部的scrollTop值if( cHeight - leftScrollTop - windowScrollHeight >0) {// 高亮的区域在屏幕底部leftScrollTop = cHeight - windowScrollHeight /2;//左侧scroll-view向上滚动半个屏幕高度leftToView =null;// 不使用scroll-into-view 属性, 必须置空, 否则会优先应用该属性而不是leftScrollTop}elseif(cHeight - leftScrollTop <0) {// 高亮的区域在屏幕顶部之上,设置scroll-into-view属性leftToView = id;    leftScrollTop = cHeight;// 需要记录下当前scroll-view滚动高度,以便下次使用}else{    leftToView =null;}

需要注意的是,若同时设置了scroll-into-view和scroll-top属性,优先使用scroll-into-view属性。

因此,如果这里使用scroll-top属性进行滚动,需要将scroll-into-viwe属性置空。

其他优化

联动功能开发完之后,我们的这款小程序遇到了性能瓶颈。

经过我们排查,我们发现:我们复用的 C 端的数据接口中,存在大量无用的对象属性,而这个数据结构直接作为页面渲染的data数据。

我们推荐的做法,就是简化data数据结构,只存放影响页面渲染的数据。这样做能够大幅度降低 UI 渲染时间,给用户更加流畅的体验。

总结与感受

微信小程序算是这两年非常火的一门新技术了。如何使用已经支持的功能特性来设计、开发产品是保障项目顺利完成的重要环节。

而在开发过程中,专注细节实现、吃透 API 文档,让用户感受到我们开发小程序的诚意是非常重要的,千万不能粗糙地做产品复制

在小程序发布那段时间,总能看到各种对小程序未来的设想,有悲观的,有观望的,也有激进的。

我个人认为,「赶鸭子上架」的思路并不可取,必须清楚自己的产品定位。

你的产品是否满足「一次性消费」理念?内容是否不足以吸引用户下载你的 app?小程序是否比你的 HTML 5 更加具有吸引力?

这些都是需要我们进行思考的。

04:逻辑层

文 | 潘逸飞

潘逸飞,美团点评工程师,2 年 Web 开发经验,现在是美团点评点餐团队的一员。

继上次谈到了视图层开发经验,本期,知晓程序(想要和大家分享大众点评点餐小程序开发中,逻辑层开发的经验。

与视图层。但小程序的逻辑代码,与我们平常编写的 JS 还是有一些区别的。

在接下来的文章中,我会根据实际代码,进行说明。

首先,我们看看逻辑层代码结构:

作为逻辑层,我们只需要关注小程序逻辑文件app.js和页面逻辑文件menu.js。

App 和 Page

App

小程序提供了App方法来注册整个小程序。在方法里,我们可以传入一个对象,指定小程序的生命周期函数以及自定义的函数或者数据。

需要注意的是,这个函数只能被调用一次。

在方法中,我们可以使用这些生命周期及功能性参数:

globalData

onLaunch

onShow

onHide

onError

其他自定义的函数

如上所示,拥有着 4 个生命周期函数。

通过这些函数,我们可以在小程序状态更变时,进行一些全局信息的获取。例如启动小程序时,获取用户信息、门店信息等等,然后存入到全局数据中。

这里的数据,可以被每个页面访问。

Page

小程序针对每个页面提供了Page的函数。

整个逻辑层大部分的代码都会写在函数中,中承接着整个页面的数据、生命周期函数,以及在视图中绑定的事件的触发函数(例如点击事件)。

整个函数允许的参数如下所示:

data

onload

onReady

onUnload

onPullDownRefresh

onReachBottom

onShareAppMessage

其他自定义函数

如上,函数因为是页面级别的,所以拥有着更多的生命函数,会有下拉刷新事件、页面到达底部的事件。

在这里,我们需要区别好各个生命周期函数。例如:

onLoad只会在初始化的时候调用一次。

是每次打开页面都会调用。

只有页面初次渲染完成才会被调用。

会在页面跳转或底部 Tab 切换时调用。

会在页面从页面堆栈中销毁前调用。

更具体的渲染过程可以参考下面这张图:

用文字简单描述这个过程,就是这样:

视图层和逻辑层同时进行初始化的操作;

视图层 ready 之后,通知逻辑层发送数据;

逻辑层执行onLoad和onShow方法,然后等待视图层的通知,在接收到视图层的通知之后发送数据给视图层,然后继续等待视图层的通知;

视图层根据数据进行初次渲染后通知逻辑层渲染完毕,逻辑层调用onReady方法。然后后续的行为逻辑层可以通过再次发送数据重新渲染视图层。

的整个工作流程可以参照下面的图:

首先,的data会被用于页面的初始化渲染,之后,用户会在页面上——也就是展示层——触发事件。

举个例子:用户在点餐小程序,产生了「点击加菜按钮」这样的事件。页面监听到这个事件之后,会触发在函数中申明的自定义事件。

在上面,我们提到内可以设置全局数据。我们在每个里,都可以通过全局函数getApp()来拿到全局的引用实例。之后,我们就可以利用它访问页面的数据。

比如我们在购物车下完单之后回到菜单页可能会需要进行菜单的刷新,我们在购物车页面就会调用().data.menuRefresh = true,然后在菜单页的onShow方法进行判断,例如:

在每个 Page 内,我们还可以用来获取当前页面栈的实例。它返回的是数组形式的数据,第一个元素为首页,最后一个元素为当前页面。

页面栈的表现情况如下表所示:

需要注意的是,我们不能手动去尝试修改页面栈,我们只能根据页面栈,来分析是使用哪种微信的 API 来跳页面。这里的跳转 API 还会在下面进行讲解。

模块化

小程序是支持模块化的,并且支持 Common.js 的模块化写法,也就是module.exports或者exports

小程序目前并不支持引入node_modules,也就是并不支持第三方的模块。当我们需要使用到外部的依赖的时候,建议直接将代码拷贝到小程序的目录中,然后通过相对路径的require函数进行引入。

微信 API

小程序作为微信的一个重要功能,微信的框架提供了非常丰富的微信原生 API,可以方便的调起微信提供的能力。

除了视图层的一些原生组件外,还有一些功能性的 API,如扫码,定位,媒体播放,本地存储以及支付功能等等。

我们这次使用的较多的是,通过微信发起网络请求,以及数据存储接口。

发送网络请求

小提示

由于小程序的框架并非运行在浏览器中,所以 JavaScript 在 web 端的一些能力都无法使用。除了上面提到的 Cookie,还有 DOM 元素操作也无法使用。

开发者所有代码最终会被打包成一份 JavaScript,在小程序启动的时候运行,直到小程序销毁。这一点类似于浏览器的 ServiceWorker,所以逻辑层也称之为 App Service。

05:解析开发工具

文 | 周中坚

作者介绍:周中坚,美团点评工程师,4年 Web 前端开发经验,主要负责过会员卡、外卖、预订、商家平台等业务的前端开发,现在是美团点评点餐团队的一员。

知晓程序(微信号 zxcx0101)已经为大家带来 4 篇来自大众点评的小程序开发文章,相信大家看了这些文章、再结合官方文档已经可以毫无压力地开发小程序了。

关注知晓程序(微信号 zxcx0101),回复「点评」,获取该系列所有文章。

但是为什么有这些坑,是不是可以绕过去,怎么排查问题,我们还想从源头——小程序的源码的角度来尝试分析,因此有了这篇源码解析。

代码结构

以 macOS 系统为例,首先进入应用程序文件夹,再右键微信开发者工具显示包内容,最后让我们进入./Contents/Resources/app.nw目录。

接下来,我们就可以查看小程序的源码了。代码结构如图:

文件夹看起来很多,但命名还算清晰。现在,让我们从开发者工具入手,来看下都用到了哪些文件吧。

开发者工具

首页

这个页面里的很多信息,可以和这个项目中的package.json对应起来,比如 name、icon、version 等。

代理

代理的设置在./app/dist/components/setting/setting.js,而用户设置的保存(包括后面要说的模拟器设备、网络等信息)是调用了./app/dist/stores/*.js方法。

菜单

上图可以看到我对菜单做的一些定制。

菜单的设置在./app/dist/common/menu/menu.js,动作在./app/dist/common/actions/actions.js,大家可以自行到代码中查看文件的require进一步分析。

设备及网络

上图可以看到我自己添加了一个设备以及一个网络类型。

模拟器的设备配置在./app/dist/config/DeviceModules.js,网络配置在./app/dist/common/jssdk/osInfoSdk.js。

调试工具

调试工具是这一节最核心的内容了。乍一看,微信的调试工具和 Chrome 的 DevTools 长的很像。查查源代码,果然就是借助它实现的。

其中 Console、Sources、Network 就是直接使用的 DevTools,而 Storage、AppData、Wxml、Sensor,都是微信自己实现的。

参照这些调试工具,我们自己弄一个自己的调试工具就很简单了。

只要在./app/dist/extensions目录下新建一个文件夹,用

html/css/js完成这个工具的功能,再将devtools.html这个工具引入

chrome.devtools.panels.create()

有趣的是,在0.15.150201这个测试版中已经发现了一个名为 Bluetooth 的开发工具。

(知晓程序注:最新版微信,已支持小程序蓝牙接口。)

WeApp

上面一节主要讲的是小程序开发者工具的源码。我们借助分析源码可以搞清楚代理是怎么设置的,模拟器的设备和网络如何添加,怎样开发一个满足自己特定需求的 DevTool。

这一节主要介绍,我们写的微信小程序的代码是如何变成页面,在用户的终端运行的

tpl文件夹下是页面模板。

onlinevendor/wcc在编译时把 WXML 文件转为 JS 文件,onlinevendor/wcsc在编译时把 wxss 文件转化为 js,这也是编译包比代码库要大不少的重要原因。

trans文件夹下有五个方法,其中transConfigToPf可以将配置转成pageFrame,trans/transWxmlToHtml将 WXML 转成 DOM 树,再进一步用 WebView 渲染,trans/transWxssToCss将 WXSS 转成 CSS,提供 View 层样式。

onlinevendor/WAService.js提供了service 层几乎一切功能。

pageFrame

首先是看一下刚才提到的pageFrame,对应的 transConfigToPf 主要用字符串替换的方式完成转换

WAService.js

WAService.js是小程序页面运行的核心方法,主要有几大功能:

内置的report方法定义

微信小程序API封装

WeixinJSBridge

appServiceEngine模块

// 内置的 report 方法定义,用于内部 API 的调用日志 & 报错记录等。 var Reporter = { surroundThirdByTryCatch, slowReport, speedReport, reportKeyValue, reportIDKey, thirdErrorReport, errorReport, log, submit, registerErrorListener, unRegisterErrorListener, triggerErrorMessage } // 微信小程序 API 封装,所有文档中的 [API](https://mp.weixin.qq.com/debug/wxadoc/dev/api/) 都在这里封装了,以 showModal 为例简单分析一下 showModal: function() { var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {} , t = { title: "", content: "", confirmText: "确定", cancelText: "取消", showCancel: !0, confirmColor: "#3CC51F", cancelColor: "#000000" };// 默认值,此处比文档准确 if (t = (0, f.extend)(t, e), a("showModal", t, {// 调用 jsbridge,见下方代码 title: "", content: "", confirmText: "", cancelText: "", confirmColor: "", cancelColor: "" })) return t.confirmText.length > 4 ? void B("showModal", e, "showModal:fail confirmText length should not large then 4") : t.cancelText.length > 4 ? void B("showModal", e, "showModal:fail cancelText length should not large then 4") : void (0, // 各种校验 u.invokeMethod)("showModal", t, { beforeSuccess: function(e) { e.confirm = Boolean(e.confirm)// 返回值处理 } }) } // 此处调用 WeixinJSBridge,此外还对每个 API 调用记 log,方便微信小程序的问题排查 function a() { var e = Array.prototype.slice.call(arguments) , t = e[1]; e[1] = function(e, n) { var o = e.data , r = e.options , i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {} , a = r && r.timestamp || 0 , s = Date.now(); "function" == typeof t && t(o, n), Reporter.speedReport({ key: "webview2AppService", data: o || {}, timeMark: { startTime: a, endTime: s, nativeTime: i.nativeTime || 0 } }) } , WeixinJSBridge.subscribe.apply(WeixinJSBridge, e) } // WeixinJSBridge 封装,底层是调用 WeixinJSCore e.WeixinJSBridge = { invoke: d, invokeCallbackHandler: p, on: h, publish: v, subscribe: g, subscribeHandler: y } // 内置的 jsbridge core WeixinJSCore = { invokeHandler, publishHandler } // setData 方法定义,逻辑层通过 setData 方法改变 virtual dom,改变 dom tree,从而改变视图层 // appServiceEngine 模块,提供 App 和 Page 相关的接口

总结

如果是为了源码分析而进行源码分析,我觉得大可不必。在小程序的场景下,源码分析的价值在于:

官方文档不一定和实际情况是对齐的,开发时碰到不一致的情况可以查阅源码,以此为准。

熟悉源码结构可以快速定位问题,提升开发效率,甚至给自己开发合适的 DevTool。

小程序可以认为是前端的一个子集,而且相对封闭,开发时会有各种约束,查阅源码可以有助于小程序的设计。

产品分析