2.2.6 模板
WXML提供模板,可以在模板中定义代码片段,然后在不同地方调用。使用name属性,作为模板的名字。然后在<template> 定义代码片段,如:
<template name="msgItem">
<view>
<text>{{ index }}: {{msg}}</text>
<text>Time: {{time}}</text>
</view>
</template>
使用 is 属性,声明需要的使用的模板,然后将模板所需要的 data 传入,如代码2-17所示。
<!--
item: {
index: 0,
msg: 'this is a template',
time: '2016-06-18'
}
-->
<template name="msgItem">
<view>
<text> {{index}}: {{msg}} </text>
<text> Time: {{time}} </text>
</view>
</template>
<template is="msgItem" data="{{...item}}"></template>
is可以动态决定具体需要渲染那个模板,如代码2-18所示
<template name="odd">
<view> odd </view>
</template>
<template name="even">
<view> even </view>
</template>
<block wx:for="{{[1,2,3,4]}}">
<template is="{{item % 2 == 0 ? 'even' : 'odd'}}">
</block>
2.2.7 引用
WXML提供两种文件引用方式import和include
import 可以在该文件中使用目标文件定义的template,如:
<!-- item.wxml -->
<template name="item">
<text>{{text}}</text>
</template>
在 index.wxml 中引用了 item.wxml,就可以使用 item模板:
<import src="item.wxml"/>
<template is="item" data="{{text: 'forbar'}}"/>
需要注意的是 import 有作用域的概念,即只会 import 目标文件中定义的 template,而不会 import 目标文件中 import 的 template,简言之就是 import 不具有递归的特性。
include 可以将目标文件中除了 <template/> 外的整个代码引入,相当于是拷贝到 include 位置,如代码2-22、代码2-23、代码2-24所示。
代码清单2-22 index.wxml
<!-- index.wxml -->
<include src="header.wxml"/>
<view> body </view>
<include src="footer.wxml"/>
代码清单2-23 header.wxml
<!-- header.wxml -->
<view> header </view>
代码清单2-24 footer.wxml
<!-- footer.wxml -->
<view> footer </view>
2.3 WXSS 样式
WXSS(WeiXin Style Sheets)是一套用于小程序的样式语言,用于描述WXML的组件样式,也就是视觉上的效果。
WXSS与Web开发中的CSS类似。为了更适合小程序开发,WXSS对CSS做了一些补充以及修改。
2.3.3 WXSS引用
在CSS中,开发者可以这样引用另一个样式文件:@import url('./test_0.css')
在小程序中,我们依然可以实现样式的引用,样式引用是这样写:
@import './test_0.wxss'
由于WXSS最终会被编译打包到目标文件中,用户只需要下载一次,在使用过程中不会因为样式的引用而产生多余的文件请求。
小程序支持动态更新内联样式:
<!--index.wxml-->
<!--可动态变化的内联样式-->
<!--
{
eleColor: 'red',
eleFontsize: '48rpx'
}
-->
<view style="color: {{eleColor}}; font-size: {{eleFontsize}}"></view>
2.4 JavaScript 脚本
小程序的主要开发语言是 JavaScript ,开发者使用 JavaScript 来开发业务逻辑以及调用小程序的 API 来完成业务需求。
浏览器中的JavaScript 是由 ECMAScript 和 BOM(浏览器对象模型)以及 DOM(文档对象模型)组成的,Web前端开发者会很熟悉这两个对象模型,它使得开发者可以去操作浏览器的一些表现,比如修改URL、修改页面呈现、记录数据等等。
file:///Users/xqls/Desktop/笔记/img/js.png

NodeJS中的JavaScript 是由 ECMAScript 和 NPM以及Native模块组成,NodeJS的开发者会非常熟悉 NPM 的包管理系统,通过各种拓展包来快速的实现一些功能,同时通过使用一些原生的模块例如 FS、HTTP、OS等等来拥有一些语言本身所不具有的能力。
file:///Users/xqls/Desktop/笔记/img/node.png

小程序中的 JavaScript 是由ECMAScript 以及小程序框架和小程序 API 来实现的。同浏览器中的JavaScript 相比没有 BOM 以及 DOM 对象,所以类似 JQuery、Zepto这种浏览器类库是无法在小程序中运行起来的,同样的缺少 Native 模块和NPM包管理的机制,小程序中无法加载原生库,也无法直接使用大部分的 NPM 包。
file:///Users/xqls/Desktop/笔记/img/wx.png

小程序目前可以运行在三大平台:
- iOS平台,包括iOS9、iOS10、iOS11
- Android平台
- 小程序IDE
2.4.3 模块化
浏览器中,所有JavaScript试运行在同一个作用域下的,定义的参数或方法可以被后续加载的脚本访问或改写。同浏览器不同,小程序中可以将任何一个JavaScript文件作为一个模块,通过module.exports或exports对外暴露接口。
代码清单2-26 模块示例
// moduleA.js
module.exprot = function(value){
return value * 2
}
代码清单2-27 引用模块A
// B.js
// 在B.js中引用模块A
var multiplyBy2 = require('./moduleA')
var result = multiplyBy2(4)
代码清单2-28 在需要使用这些模块的文件中,使用 require(path) 将公共代码引入
var common = require('common.js')
Page({
helloMINA: function() {
common.sayHello('MINA')
},
goodbyeMINA: function() {
common.sayGoodbye('MINA')
}
})
2.4.4 脚本执行顺序
2.4.5 作用域
在文件中声明的变量和函数只在该文件中有效,不同的文件中可以声明相同名字的变量和函数,不会互相影响,如代码2-36、代码2-37所示。
理解小程序宿主环境
3.1 渲染层和逻辑层
小程序的运行环境分成渲染层和逻辑层,第2章提到过 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。
3.1.1 渲染 “Hello World” 页面
<view>{{ msg }}</view>
在JS 脚本使用this.setData方法把msg字段设置成“hello world”
Page({
onLoad: function() {
this.setData({ msg: 'Hello World' })
}
})
这个例子看到3点
- 1,渲染层和数据相关
- 2,罗继承负责产生和处理数据
- 3,逻辑层通过Page实例的setData方法传递数据到渲染层
3.1.2 通信模型
小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型如图所示
file:///Users/xqls/Desktop/笔记/img/4-1.png

3.1.4 双线程下的界面渲染
小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面
file:///Users/xqls/Desktop/笔记/img/shuang.png

3.2 程序与页面
从逻辑组成来说,一个小程序是有多个页面组成的程序。这里要区别下"小程序"和"程序"的概念
3.2.1 程序
“小程序”指的是产品层面的程序,而“程序”指的是代码层面的程序实例,为了避免误解,下文采用App来代替代码层面的“程序”概念。
- 程序构造器App()
宿主环境提供了 App() 构造器用来注册一个程序App,需要留意的是App() 构造器必须写在项目根目录的app.js里,App实例是单例对象,在其他JS脚本中可以使用宿主环境提供的 getApp() 来获取程序实例。
代码清单3-3 getApp() 获取App实例
// other.js
var appInstance = getApp()
App() 的调用方式如代码清单3-4所示,App构造器接受一个Object参数,其中onLaunch/onShow/onHide三个回调是App实例的生命周期函数,我们会在后文展开;
onError我们暂时不在本章展开,我们会在第8章里详细讨论;App的其他参数我们也放在后文进行展开。
代码清单3-4 App构造器
App({
onLaunch: function(options) {},
onShow: function(options) {},
onHide: function() {},
onError: function(msg) {},
globalData: 'I am global data'
})
2,程序的生命周期和打开场景
初次进入小程序的时候,微信客户端初始化好宿主环境,同时从网络下载或者从本地缓存中拿到小程序的代码包,把它注入到宿主环境,初始化完毕后,微信客户端就会给App实例派发onLaunch事件,App构造器参数所定义的onLaunch方法会被调用。
进入小程序之后,用户可以点击右上角的关闭,或者按手机设备的Home键离开小程序,此时小程序并没有被直接销毁,我们把这种情况称为“小程序进入后台状态”,App构造器参数所定义的onHide方法会被调用。
当再次回到微信或者再次打开小程序时,微信客户端会把“后台”的小程序唤醒,我们把这种情况称为“小程序进入前台状态”,App构造器参数所定义的onShow方法会被调用。
我们可以看到,App的生命周期是由微信客户端根据用户操作主动触发的。为了避免程序上的混乱,我们不应该从其他代码里主动调用App实例的生命周期函数
在微信客户端中打开小程序有很多途径:从群聊会话里打开,从小程序列表中打开,通过微信扫一扫二维码打开,从另外一个小程序打开当前小程序等,针对不同途径的打开方式,小程序有时需要做不同的业务处理,所以微信客户端会把打开方式带给onLaunch和onShow的调用参数options,示例代码以及详细参数如代码清单3-5和表3-2所示。需要留意小程序的宿主环境在迭代更新过程会增加不少打开场景,因此要获取最新的场景值说明请查看官方文档
代码清单3-5 onLaunch和onShow带参数
App({
onLaunch: function(options) { console.log(options) },
onShow: function(options) { console.log(options) }
})
3, 小程序全局数据
我们在 3.1.2节说到小程序的JS脚本是运行在JsCore的线程里,小程序的每个页面各自有一个WebView线程进行渲染,所以小程序切换页面时,小程序逻辑层的JS脚本运行上下文依旧在同一个JsCore线程中。
// app.js
App({
globalData: 'I am global data' // 全局共享数据
})
// 其他页面脚本other.js
var appInstance = getApp()
console.log(appInstance.globalData) // 输出: I am global data
需要注意:所有的JsCore线程,页面使用setTimeout或者setInterval的定时器,然后跳转到其他页面时,这些定时器并没有被清除,需要开发者自己在页面离开的时候进行清理。
3.2.2 页面
一个小程序可以有很多页面,每个页面承载不同的功能,页面之间可以互相跳转。为了叙述简洁,我们之后讨论所涉及的“页面”概念特指“小程序页面”
1,文件构成和路径
一个页面是分三部分组成:界面、配置和逻辑。界面由WXML文件和WXSS文件来负责描述,配置由JSON文件进行描述,页面逻辑则是由JS脚本文件负责。一个页面的文件需要放置在同一个目录下,其中WXML文件和JS文件是必须存在的,JSON和WXSS文件是可选的。
页面路径需要在小程序代码目录app.js中pages字段声明,否则这个页面不会被注册到宿主环境中。
例如两个页面的文件的相对路径分别为 pages/index/page. 和 pages/other/other. (表示wxml/wxss/json/js四个文件),在app.json的pages字段的代码路径需要去除.后缀,如代码清单3-7所示,默认pages字段的第一个页面路径为小程序的首页。
代码清单3-7 app.json声明页面路径
{
"pages":[
"pages/index/page", // 第一项默认为首页
"pages/other/other"
]
}
Page构造器接受一个Object参数,参数说明如表3-4所示,其中data属性是当前页面WXML模板中可以用来做数据绑定的初始数据,
我们会在后文展开讨论;onLoad / onReady / onShow / onHide /onUnload 5个回调是Page实例的生命周期函数,我们在后文展开;onPullDownRefresh / onReachBottom / onShareAppMessage / onPageScroll 4个回调是页面的用户行为,我们也会在后文展开。
代码清单3-8 Page构造器
Page({
data: { text: "This is page data." },
onLoad: function(options) { },
onReady: function() { },
onShow: function() { },
onHide: function() { },
onUnload: function() { },
onPullDownRefresh: function() { },
onReachBottom: function() { },
onShareAppMessage: function () { },
onPageScroll: function() { }
})
以上三个事件触发的时机是onLoad早于 onShow,onShow早于onReady。
页面不可见时,Page构造器参数所定义的onHide方法会被调用,这种情况会在使用wx.navigateTo切换到其他页面、底部tab切换时触发。
当前页面使用wx.redirectTo或wx.navigateBack返回到其他页时,当前页面会被微信客户端销毁回收,此时Page构造器参数所定义的onUnload方法会被调用。
我们可以看到,Page的生命周期是由微信客户端根据用户操作主动触发的。为了避免程序上的混乱,我们不应该在其他代码中主动调用Page实例的生命周期函数。
最后我们说一下页面的打开参数query,让我们来设想这样一个场景,我们实现一个购物商城的小程序,我们需要完成一个商品列表页和商品详情页,点击商品列表页的商品就可以跳转到该商品的详情页,当然我们不可能为每个商品单独去实现它的详情页。我们只需要实现一个商品详情页的pages/detail/detail.(代表WXML/WXSS/JS/JSON文件)即可,在列表页打开商品详情页时把商品的id传递过来,详情页通过刚刚说的onLoad回调的参数option就可以拿到商品id,从而绘制出对应的商品,代码如代码清单3-9所示。
代码清单3-9 页面的打开参数Page构造器
// pages/list/list.js
// 列表也使用navigatorTo跳转到详情页
wx.navigateto({ url: 'pages/datail/detail?id=1&other=abc' })
// pages/datail/detail.js
Pages({
onLoad: function(option){
console.log(option.id)
console.log(option.other)
}
})
4 .页面的数据
3.14节讨论了小程序界面渲染的基本原理,我们知道小程序页面右WXML进行描述,WXML可以通过数据绑定从逻辑层传递过来数据字段,这里说的数据其实就是来自页面Page构造器data字段,data参数是页面第一次渲染时从逻辑层传递到渲染层的数据。
代码清单3-10 Page构造器的data参数
<!-- page.wxml -->
<view>{{ text }}</view>
<view>{{ array[0].msg }}</view>
// page.js
Page({
data: {
text: 'init data',
array: [{msg: "1"},{msg: "2"}]
}
})
宿主环境提供的Page实例的原型中有setData函数,我们可以Page实例下方法调用this.setData把数据传递到渲染层,从而达到更新的目的。
由于小程序渲染层和逻辑层分别在两个线程中运行,所以setData传递数据实际是一个异步过程,setData的第二个参数是一个callback回调,在这次setData对界面渲染完毕后触发。
setData其一般调用格式是setDta(data,callback),其中data是由多个key: value构成的Object对象。
代码清单3-11 使用setData更新渲染层数据
// page.js
Page({
onLoad: function() {
this.setData({
text: 'change data'
},function(){
//在这次setData对界面渲染完毕后触发
})
}
})
实际在开发的时候,页面的data数据会涉及相当多的字段,你并不需要每次都将整个data字段重新设置一遍,你只需要把改变的值进行设置即可,宿主环境会自动把新改动的字段合并到渲染层对应的字段中
如下代码所示。data中的key还可以非常灵活,以数据路径的形式给出,例如 this.setData({"d[0]": 100}); this.setData({"d[1].text": 'Goodbye'});
我们只要保持一个原则就可以提高小程序的渲染性能:每次只设置需要改变的最小单位数据。
// page.js
Page({
data: {
a: 1, b: 2, c: 3,
d: [1, {text: 'Hello'}, 3, 4]
}
onLoad: function(){
// a 需要改变时,只需要setData设置a字段即可
this.setData{a : 2}()
}
})
注意三点:
-
1,直接修改Page实例的this.data,而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。
-
2,由于setData是需要两个线程的一些通信消耗,为了提高性能,每次设置的数据不应超过1024kB。
-
3,不要把data中的任意一项的value设为undefined,否则可能会有引起一些不可预料的bug。
5,页面的用户行为
小程序宿主环境提供了四个也页面相关的用户行为回调:
-
1,下拉刷新 onPullDownRefresh
监听用户下拉刷新事件,需要在app.json的window选项中或页面配置page.json中设置enablePullDownRefresh为true。当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。
-
2, 上拉触底 onReachBottom
监听用户上拉触底事件。可以在app.json的window选项中或页面配置page.json中设置触发距离onReachBottomDistance。触发距离滑动期间,本事件只会被触发一次。
-
3,页面滚动 onPageScroll 监听用户页面滑动事件,参数为Object,包含 scrollTop 字段,表示页面在垂直方向已滚动的距离(单位px)
-
4,用户转发 onShareAppMessage
只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮,在用户点击转发按钮的时候会调用,此事件需要return一个Object,包含title和path两个字段,用于自定义转发内容,如代码清单3-13所示。
代码清单3-13 使用onShareAppMessage自定义转发字段
// page.js
Page({
onShareAppMessage: function () {
return {
title: '自定义转发标题',
path: '/page/user?id=123'
}
}
})
. 6,页面跳转和路由
一个小程序拥有多个页面,我们可以通过wx.navigateTo推入一个新的页面
如图3-6所示,在首页使用2次wx.navigateTo后,页面层级会有三层,我们把这样的一个页面层级称为页面栈。
file:///Users/xqls/Desktop/笔记/img/page.png

后续为了表述方便,我们采用这样的方式进行描述页面栈:[ pageA, pageB, pageC ],其中pageA在最底下,pageC在最顶上,也就是用户所看到的界面,需要注意在本书编写的时候,小程序宿主环境限制了这个页面栈的最大层级为10层 ,也就是当页面栈到达10层之后就没有办法再推入新的页面了。我们下面来通过上边这个页面栈描述以下几个和导航相关的API。
使用wx.navigateTo({url: 'pageD'}),可以往页面栈多退一入pageD,此时页面栈变成 [ pageA, pageB, pageC, pageD ]。
使用 wx.navigateBack() 可以退出当前页面栈的最顶上页面,此时页面栈变成 [ pageA, pageB, pageC ]。
使用wx.redirectTo({ url: 'pageE' }) 是替换当前页变成pageE,此时页面栈变成 [ pageA, pageB, pageE ],当页面栈到达10层没法再新增的时候,往往就是使用redirectTo这个API进行页面跳转。
小程序提供了原生的Tabbar支持,我们可以在app.json声明tabBar字段来定义Tabbar页
代码清单3-14 app.json定义小程序底部tab
{
"tabBar": {
"list": [
{ "text": "Tab1", "pagePath": "pageA" },
{ "text": "Tab1", "pagePath": "pageF" },
{ "text": "Tab1", "pagePath": "pageG" }
]
}
}
我们可以在刚刚的例子所在的页面栈中使用wx.switchTab({ url: 'pageF' }),此时原来的页面栈会被清空(除了已经声明为Tabbar页pageA外其他页面会被销毁),然后会切到pageF所在的tab页面,页面栈变成 [ pageF ],此时点击Tab1切回到pageA时,pageA不会再触发onLoad,因为pageA没有被销毁。
补充一下,wx.navigateTo和wx.redirectTo只能打开非TabBar页面,wx.switchTab只能打开Tabbar页面。
我们还可以使用 wx.reLaunch({ url: 'pageH' }) 重启小程序,并且打开pageH,此时页面栈为 [ pageH ]。表3-5罗列了详细的页面路由触发方式及页面生命周期函数的对应关系。
表3-5 页面路由触发方式及页面生命周期函数的对应关系
| 路由方式 | 触发时机 | 路由前页面生命周期 | 路由后页面生命周期 |
|---|---|---|---|
| 初始化 | 小程序打开第一个页面 | onLoad,onShow | |
| 打开新页调用 | API wx.navigateTo | onHide | onLoad,onShow |
| 页面重定向调用 | API wx.redirectTo | onUnload | onShow |
| Tab | 切换 调用 API wx.switchTab | 请参考表3-6 | 请参考表3-6 |
| 重启动 | 调用 API wx.reLaunch | onUnload | onLoad, onShow |
Tab 切换对应的生命周期(以 A、B 页面为 Tabbar 页面,C 是从 A 页面打开的页面,D 页面是从 C 页面打开的页面为例)如表3-6所示,注意Tabbar页面初始化之后不会被销毁。
表3-6 页面路由触发方式及页面生命周期函数的对应关系
| 当前页 | 路由后页面 | 触发的生命周期(按顺序) |
|---|---|---|
| A | A | 无 |
| A | B | A.onHide(), B.onLoad(), B.onShow() |
| A | B(再次打开) | A.onHide(), B.onShow() |
| C | A | C.onUnload(), A.onShow() |
| C | B | C.onUnload(), B.onLoad(), B.onShow() |
| D | B | D.onUnload(), C.onUnload(), B.onLoad(), B.onShow() |
| D(从转发进入) | A | D.onUnload(), A.onLoad(), A.onShow() |
| D(从转发进入) | A | D.onUnload(), B.onLoad(), B.onShow() |
3.3 组件
一个小程序页面可以分解成多个部分组成,组件就是小程序页面的基本组成单元。为了让开发者可以快速进行开发,小程序的宿主环境提供了一系列基础组件。
组件是在WXML模板文件声明中使用的,WXML的语法和HTML语法相似,小程序使用标签名来引用一个组件,通常包含开始标签和结束标签,该标签的属性用来描述该组件。
代码清单3-15 在WXML使用组件示例
<!-- page.wxml -->
<image mode="scaleToFill" src="img.png"></image>
需要注意,所有组件名和属性都是小写,多个单词以英文‘-’进行连接
对于一些容器组件,其内容可以声明在其开始标签和结束标签之间。
代码清单3-16 容器组件嵌套其他组件
<!-- page.wxml -->
<view>
<image mode="scaleToFill" src="img.png"></image>
<view>
<view>1</view>
<view>2</view>
<view>3</view>
</view>
</view>
所有组件都拥有表3-7列举的属性,主要涉及样式和事件绑定,我们不在此处再做额外的说明,详细可以了解2.3节以及3.5节的相关内容。
3.4 API
宿主环境提供了丰富的API,可以很方便调起微信提供的能力。
在前文的代码示例中可以看到,wx.navigateTo可以保留当前页面,然后跳转到新的页面。
这里的wx对象实际上就是小程序的宿主环境所提供的全局对象,几乎所有小程序的API都挂载在wx对象底下(除了Page/App等特殊的构造器),所以本书谈到API概念时,通常指的是wx对象底下的方法。
小程序提供的API按照功能主要分为几大类:网络、媒体、文件、数据缓存、位置、设备、界面、界面节点信息还有一些特殊的开放接口,我们介绍一下API一般调用的约定:
- wx.on* 开头的 API 是监听某个事件发生的API接口,接受一个 Callback 函数作为参数。当该事件触发时,会调用 Callback 函数。
- 如未特殊约定,多数 API 接口为异步接口 ,都接受一个Object作为参数。
- API的Object参数一般由success、fail、complete三个回调来接收接口调用结果,示例代码如代码清单3-17所示,详细说明如表3-9所示。
- wx.get* 开头的API是获取宿主环境数据的接口。
- wx.set* 开头的API是写入数据到宿主环境的接口
代码清单3-17 通过wx.request发起网络请求
wx.request({
url: 'test.php',
data: {},
header: { 'content-type': 'application/json'},
success: function(res){
//接收到https服务成功后返回
console.log(res.data)
},
fail: function(){
//发生网络错误等情况触发
},
complete: function() {
// 成功或者失败后触发
}
})
代表3-9 API接口回调说明
| 参数名字 | 类型 | 必填 | 描述 |
|---|---|---|---|
| success | Function | 否 | 接口调用成功的回调函数 |
| fail | Function | 否 | 接口调用失败的回调函数 |
| complete | Function | 否 | 接口调用结束的回调函数(调用成功,失败都会执行) |
还有需要注意到API调用大多都是异步的,其次,有部分API会拉起微信的原生界面,此时会触发Page的onHide方法,当用户从原生界面返回到小程序时,会触发Page的onShow方法。
API的数量非常多,而且随着宿主环境的迭代更新会持续新增API,在这里我们并不想一一展开叙述每一个API的含义,开发者只要了解一般调用API的技巧,再通过官方API文档 mp.weixin.qq.com/debug/wxado… 官方API文档
3.5 事件
3.5.1 什么是事件
在小程序里边,我们把这种“用户在渲染层的行为反馈”以及“组件的部分状态反馈”抽象为渲染层传递给逻辑层的“事件”,如图3-7所示。
file:///Users/xqls/Desktop/笔记/img/event.png

图3-7 渲染层产生用户交互事件传递给逻辑层
简单处理事件的小程序代码
<!-- page.wxml -->
<view bindtap="tabName"></view>
//page.js
Page({
tabName: function(event){
console.log(event)
}
})
事件通过bindtab这个属性绑定在组件上的,同时在当前页面的Page构造器中定义对应的事件处理函数tapName,当用户点击该view区域时,达到触发条件生成事件tap,该事件处理函数tapName会被执行,同时还会收到一个事件对象event。
3.5.2 事件类型和事件对象
前边说到触发事件是由“用户在渲染层的行为反馈”以及“组件的部分状态反馈”引起的,由于不同组件的状态不一致,所以我们这里不讨论组件相关的事件(组件的事件可以参考其参数说明,详情见官方文档https://mp.weixin.qq.com/debug/wxadoc/dev/component/ )
...
3.5.3 事件绑定与冒泡捕获
事件绑定的写法和组件属性一直,以key="value"的形式,其中:
1,key以bind或者catch开头,然后跟上事件的类型,如bindtap、catchtouchstart。自基础库版本1.5.0起,bind和catch后可以紧跟一个冒号,其含义不变,如bind:tap、catch:touchstart。同时bind和catch前还可以加上capture-来表示捕获阶段。
2,value是一个字符串,需要在对应的页面Page构造器中定义同名的函数,否则触发事件时在控制台会有报错信息。 bind和capture-bind的含义分别代表事件的冒泡阶段和捕获阶段,其触发的顺序如图3-8所示。
file:///Users/xqls/Desktop/笔记/img/event1.png

以下示例中,点击 inner view 会先后调用handleTap2、handleTap4、handleTap3、handleTap1。
代码清单3-20 事件的冒泡和捕获
<view
id="outer"
bind:touchstart="handleTap1"
capture-bind:touchstart="handleTap2"
>
outer view
<view
id="inner"
bind:touchstart="handleTap3"
capture-bind:touchstart="handleTap4"
>
inner view
</view>
</view>
如果将上面代码中的第一个capture-bind改为capture-catch,将只触发handleTap2(capture-catch将中断捕获阶段和取消冒泡阶段)bind事件绑定不会阻止事件冒泡,catch事件绑定可以阻止冒泡向上冒泡。
代码清单3-21 使用catch阻止事件的传递
<view
id="outer"
bind:touchstart="handleTap1"
capture-catch:touchstart="handleTap2"
>
outer view
<view
id="inner"
bind:touchstart="handleTap3"
capture-bind:touchstart="handleTap4"
>
inner view
</view>
</view>
注意,除表3-10列举的事件类型之外的其他组件自定义事件,如无特殊声明都是非冒泡事件,如
的submit事件,<input/>的input事件,的scroll事件。3.6 兼容
我们可能需要针对不同手机进行程序上的兼容,此时可以使用 wx.getSystemInfo 或者 wx.getSystemInfoSync 来获取手机品牌、操作系统版本号、微信版本号以及小程序基础库版本号等,通过这个信息,我们可以针对不同平台做差异化的服务。
3.7 本章小结
在本章中,我们介绍了小程序宿主环境的基本运行机制以及它所提供的各种能力,组合这些能力可以完成一个体验非常流畅的小程序,同时读者也了解到如何对小程序在不同环境下兼容的办法,以便给不同环境下的的微信用户提供可靠或者降级的服务。在下一章中,我们会给读者展示如何利用小程序的能力实现各类场景应用。
第4章 场景应用
在本章我们会介绍小程序的基本开发流程,结合第2章和第3章的知识,足以让你独立完成一个体验很完善的小程序。为了让开发者更加了解小程序开发,在这章中我们还会通过常见的一些应用场景介绍小程序API一些细节,及开发的一些技巧和注意事项。
4.1 开发流程基本介绍
4.2 基本布局方式 - Flex布局
你的小程序要求兼容到iOS8以下版本,需要开启样式自动补全。开启样式自动补全,在“设置”—“项目设置”—勾选“上传代码时样式自动补全”
建议用flex布局
在开始介绍flex之前,为了表述方便,我们约定以下术语:采用flex布局的元素,简称为“容器”,在代码示例中以container表示容器的类名。容器内的元素简称为“项目”,在代码示例中以item表示项目的类名。
file:///Users/xqls/Desktop/笔记/img/items.png

4.2.1 基本概念
目标是提供一种灵活的布局模型,是容器能够通过改变里面项目高度,顺序,来对可用空间实现最佳填充,方便适配不同大小的内容区域。 在不同高度信息的例子中,我们只需要在容器中设置两个属性即可实现内容不确定下垂直居中。
.container{
display: flex;
flex-direction: column;
justify-content: center;
}
flex
略过
4.3 界面常见交互反馈
用户和小程序上进行交互的时候,某些操作可能比较耗时,我们应该予以及时的反馈以舒缓用户等待的不良情绪。
4.3.1 触摸反馈
通常页面会摆放一些button按钮或者view区域,用户触摸按钮之后会触发下一步的操作。这种情况下,我们要对触摸这个行为给予用户一些响应。如图4-17所示,当我们手指触摸了button文字所在的cell区域时,对应的区域底色变成浅灰色,这样用户就可以知道小程序是有及时响应他的这次触摸操作,用户就不会很迷惑。
小程序的view容器组件和button组件提供了hover-class属性,触摸时会往该组件加上对应的class改变组件的样式。
代码清单4-1 通过hover-class属性改变触摸时的样式
/*page.wxss */
.hover{
background-color: gray;
}
<!--page.wxml -->
<button hover-class="hover"> 点击button </button>
<view hover-class="hover"> 点击view</view>
对于用户的操作及时响应是非常优秀的体验,有时候在点击button按钮处理更耗时的操作时,我们也会使用button组件的loading属性,在按钮的文字前边出现一个Loading,让用户明确的感觉到,这个操作会比较耗时,需要等待一小段时间。
代码清单4-2 设置button的loading属性
<!--page.wxml -->
<button loading="{{loading}}" bindtap="tap">操作</button>
//page.js
Page({
data: { loading: false },
tap: function() {
// 把按钮的loading状态显示出来
this.setData({
loading: true
})
// 接着做耗时的操作
}
})
4.3.2 Toast和模态对话框
在完成某个操作之后,告诉用户这次操作成功。弹出式Toast用在这样场景上,默认提示1.5s后自动消失
onLoad: function(){
wx.showToast({ // 显示Toast
title: '已发送',
icon: 'success',
duration: 1500
})
// wx.hideToast() // 隐藏Toast
}
模拟对话框,同事带进一步指引的
Page({
onLoad: function() {
wx.showModal({
title: '标题',
content: '告知当前状态,信息和解决方法',
confirmText: '主操作',
cancelText: '次要操作',
success: function(res) {
if (res.confirm) {
console.log('用户点击主操作')
} else if (res.cancel) {
console.log('用户点击次要操作')
}
}
})
}
})
4.4 发起网络HTTPS网络通信
小程序经常往服务器传递数据或者从服务器拉去信息,这个时候可以使用wx.request这个API,在这一节我们会重点讨论wx.request的使用和注意事项。为了叙述方便,假设我们的服务器域名是test.com。
如果我们需要从 test.com/getinfo 接口拉取用户信息,其代码示例如下所示,详细参数如表4-1所示。
代码清单4-7 wx.request调用示例
wx.request({
url: 'https://test.com/getinfo',
success: function(res){
console.log(res) //服务器回包信息
}
})
表4-1 wx.request详细参数
| 参数名 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
| url | String | 是 | 开发者服务接口地址 | |
| data | Object/String | 否 | 请求参数 | |
| header | Object | 否 | 设置请求的 header,header 中不能设置 Referer,默认header['content-type'] = 'application/json' | |
| method | String | 否 | GET | 需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT |
| dataType | String | 否 | json | 回包的内容格式,如果设为json,会尝试对返回的数据做一次 JSON解析 |
| success | Function | 否 | 收到开发者服务成功返回的回调函数,其参数是一个Object,见表4-2。 | |
| fali | Function | 否 | 接口调用失败的回调函数 | |
| complete | Function | 否 | 接口调用结束的回调函数(调用成功,失败都会执行) |
##4.4.2 服务器接口
url参数是当前发起请求的服务器接口地址,小程序宿主环境要求request发起的网络请求必须是https协议请求,因此开发者服务器必须提供HTTPS服务的接口,同时为了保证小程序不乱用任意域名的服务 wx.request请求的域名需要在小程序管理平台进行配置[2],如果小程序正式版使用wx.request请求未配置的域名,在控制台会有相应的报错
##4.4.3 请求参数
通过wx.request这个API,有两种方法把数据传递到服务器:通过url参数以及通过data参数。 举个例子:我们需要向服务器拿id为1的用户的信息,同时我们把当前小程序的版本带给服务器,让服务器可以做新旧版逻辑兼容,两种方法的代码示例如代码4-8所示。
//通过url参数传递数据
wx.request({
url: 'https://test.com/getinfo?id=1&version=1.0.0',
success: function(res){
console.log(res) // 服务器回包信息
}
})
// 通过data参数传递数据
wx.request({
url: 'https://test.com/getinfo',
data: { id:1, version: '1.0.0' },
success: function(res){
console.log(res) //服务器回包信息
}
})
一般建议需要传递数据时,使用data参数来传递。
单独post请求情况,并不是所有请求按照键值对key=value的形式传递到后台服务器,有时候传递比较复杂数据结构时,json比较合适。此时在wx.request的header参数设置content-type为application/json,小程序发起的请求data对应JSON字符串
wx.request发起POST请求题使用json格式
// 请求包体为:{"a":{"b":[1,2,3]},"c":{"d":"test"}}
// 请求的包体为 {"a":{"b":[1,2,3],"c":{"d":"test"}}}
wx.request({
url: 'https://test.com/postdata',
method: 'POST',
header: { 'content-type': 'application/json'},
data: {
a: {
b: [1, 2, 3],
c: { d: "test" }
}
},
success: function(res) {
console.log(res)// 服务器回包信息
}
})
4.4.4 收到回包
通过wx.request发送请求后,服务器处理请求并返回HTTP,小程序端收到回包后触发success回调,同时回调会带上一个Object信息,详细参数
wx.request的success返回参数
| 参数 | 类型 | 描述 |
|---|---|---|
| data | Object/String | 开发者服务器返回的数据 |
| statusCode | Number | 开发者服务器返回的HTTP状态码 |
| header | Object | 开发者服务器返回的HTTP Response Header |
4.4.5 一般使用技巧
1,设置超时时间
小程序app.json可以指定request的超时时间
{
"networkTimeout": {
"request": 3000
}
}
2.请求前后的状态处理
var hasClick = false;
Page({
tap: function() {
if (hasClick) {
return
}
hasClick = true
wx.showLoading()
wx.request({
url: 'https://test.com/getinfo',
method: 'POST',
header: { 'content-type':'application/json' },
data: { },
success: function (res) {
if (res.statusCode === 200) {
console.log(res.data)// 服务器回包内容
}
},
fail: function (res) {
wx.showToast({ title: '系统错误' })
},
complete: function (res) {
wx.hideLoading()
hasClick = false
}
})
}
})
为了防止用户极快速度触发两次tap回调,我们还加了一个hasClick的“锁”,在开始请求前检查是否已经发起过请求,如果没有才发起这次请求,等到请求返回之后再把锁的状态恢复回去。
4.4.6 排查异常的方法
在使用wx.request接口我们会经常遇到无法发起请求或者服务器无法收到请求的情况,我们罗列排查这个问题的一般方法:
- 查手机网络状态以及wifi连接点是否工作正常。
- 检查小程序是否为开发版或者体验版,因为开发版和体验版的小程序不会校验域名。
- 检查对应请求的HTTPS证书是否有效,同时TLS的版本必须支持1.2及以上版本,可以在开发者工具的console面板输入showRequestInfo()查看相关信息。
- 域名不要使用IP地址或者localhost,并且不能带端口号,同时域名需要经过ICP备案。
- 检查app.json配置的超时时间配置是否太短,超时时间太短会导致还没收到回报就触发fail回调。
- 检查发出去的请求是否302到其他域名的接口,这种302的情况会被视为请求别的域名接口导致无法发起请求。
#4.5 微信登录 怎么获取微信登录态;怎么把微信帐号和自己的帐号进行打通。在这一节中,我们来介绍一下如何把微信登录应用到你的小程序中
看下微信登录整个过程
file:///Users/xqls/Desktop/笔记/img/wx-login.png

4.5.1获取微信登录凭证code
4.5.2 发送code到开发者服务器
wx.login的success回调中拿到微信登录凭证 -》 紧接着会通过wx.request把code传到开发者服务器,为了可以换取微信用户身份id。如果当前微信用户还没有绑定当前小程序业务的用户身份,那在这次请求应该顺便把用户输入的帐号密码[7]一起传到后台,然后开发者服务器就可以校验账号密码之后再和微信用户id进行绑定,小程序端的示例代码如下所示
wx.login获取code后
Page({
tapLogin: function() {
wx.login({
success: function(res) {
if (res.code) {
wx.request({
url: 'https://test.com/login',
data: {
username: 'zhangsan', // 用户输入的账号
password: 'pwd123456', // 用户输入的密码
code: res.code
},
success: function(res) {
// 登录成功
if (res.statusCode === 200) {
console.log(res.data.sessionId)// 服务器回包内容
}
}
})
} else {
console.log('获取用户登录态失败!' + res.errMsg)
}
}
});
}
})
4.5.3 到微信服务器换取微信用户身份id
到了第3步,开发者的后台就拿到了前边wx.login()所生成的微信登录凭证code,此时就可以拿这个code到微信服务器换取微信用户身份。 微信服务器为了确保拿code过来换取身份信息的人就是刚刚对应的小程序开发者,到微信服务器的请求要同时带上AppId和AppSecret,由此可以看出,AppId和AppSecret是微信鉴别开发者身份的重要信息,AppId是公开信息,泄露AppId不会带来安全风险,但是AppSecret是开发者的隐私数据不应该泄露,如果发现泄露需要到小程序管理平台进行重置AppSecret,而code在成功换取一次信息之后也会立即失效,即便凭证code生成时间还没过期。
开发者服务器和微信服务器通信也是通过HTTPS协议,微信服务器提供的接口地址是:
api.weixin.qq.com/sns/jscode2…<AppId>&secret=<AppSecret>&js_code=<code>&grant_type=authorization_code
URL的query部分的参数中 <AppId>, <AppSecret>, <code> 就是前文所提到的三个信息,请求参数合法的话,接口会返回以下字段。
jscode2session 接口返回字段
| 字段 | 描述 |
|---|---|
| openid | 微信用户的唯一标识 |
| session_key | 会话密钥 |
| unionid | 用户在微信开发平台的唯一标识符。本字段在满足一定条件返回 |
我们暂时需要关注的两个字段,openid就是前文一直提到的微信用户id,可以用这个id来区分不同微信用户。
session_key则是微信给开发者颁发的身份凭证, 开发者可以通过session_key请求微信服务器其他接口获取一些其他信息,
由此可以看,session_key不应该泄露或者下发到小程序前端。
可能我们会好奇为什么要设计session_key,如果我们每次都通过小程序前端wx.login()生成微信登录凭证code去微信服务器请求信息,步骤太多造成整体耗时比较严重,因此对于一个比较可信的服务端,给开发者服务器颁发一个时效性更长的会话密钥就显得很有必要了。session_key也存在过期时间,因为篇幅关系,我们不在此展开,可以参考小程序的官方文档关于session_key的相关介绍。
4.5.4 绑定微信用户身份id和业务用户身份
在4.5.2节提到,业务侧用户还没绑定微信侧身份时,会让用户填写业务侧的用户名密码,这两个值会和微信登录凭证一起请求开发者服务器的登录接口,此时开发者后台通过校验用户名密码就拿到了业务侧的用户身份id,通过code到微信服务器获取微信侧的用户身份openid。微信会建议开发者把这两个信息的对应关系存起来,我们把这个对应关系称之为“绑定”。
有了这个绑定信息,小程序在下次需要用户登录的时候就可以不需要输入账号密码,因为通过wx.login()获取到code之后,可以拿到用户的微信身份openid,通过绑定信息就可以查出业务侧的用户身份id,这样静默授权的登录方式显得非常便捷。
4.5.5 业务登录凭证SessionId
4.5.3节已经说到微信侧返回的session_key是开发者服务器和微信服务器的会话密钥,同样道理,开发者服务器和开发者的小程序应该也有会话密钥,在本书中我们就把它称之为SessionId。用户登录成功之后,开发者服务器需要生成会话密钥SessionId,在服务端保持SessionId对应的用户身份信息,同时把SessionId返回给小程序。小程序后续发起的请求中携带上SessionId,开发者服务器就可以通过服务器端的Session信息查询到当前登录用户的身份,这样我们就不需要每次都重新获取code,省去了很多通信消耗。我们在4.6.4还会提到如何利用本地数据缓存的能力把SessionId存储起来,以便在它还没过期的时候能重复利用,以提高通信的性能。
4.6 本地数据缓存
本地数据缓存是小程序存储在当前设备上硬盘上的数据,本地数据缓存有非常多的用途,我们可以利用本地数据缓存来存储用户在小程序上产生的操作,在用户关闭小程序重新打开时可以恢复之前的状态。我们还可以利用本地缓存一些服务端非实时的数据提高小程序获取数据的速度,在特定的场景下可以提高页面的渲染速度,减少用户的等待时间。
4.6.1 读写本地数据缓存
小程序提供了读写本地数据缓存的接口,通过wx.getStorage/wx.getStorageSync 读取本地缓存,通过wx.setStorage/wx.setStorageSync写数据到缓存,其中Sync后缀的接口表示是同步接口[9],执行完毕之后会立马返回,示例代码和参数说明如下所示。
wx.getStorage/wx.getStorageSync读取本地数据缓存
wx.getStorage({
key: 'key1',
success: function(res) {
// 异步接口在success回调才能拿到返回值
var value1 = res.data
},
fail: function() {
console.log('读取key1发生错误')
}
})
try{
// 同步接口立即返回值
var value2 = wx.getStorageSync('key2')
}catch (e) {
console.log('读取key2发生错误')
}
wx.getStorage/wx.getStorageSync详细参数
| 参数名 | 类型 | 必填 | 描述 |
|---|---|---|---|
| key | String | 是 | 本地缓存指定的key |
| success | Function | 否 | 异步接口调用成功的回调函数,回调参数格式: {data: key对应的内容} |
| fail | Function | 否 | 异步接口调用失败的回调函数 |
| complete | Function | 否 | 异步接口调用结束的回调函数(调用成功、失败都会执行) |
代码清单4-14 wx.setStorage/wx.setStorageSync写入本地数据缓存
// 异步接口在success/fail回调才知道写入成功与否
wx.setStorage({
key:"key",
data:"value1"
success: function() {
console.log('写入value1成功')
},
fail: function() {
console.log('写入value1发生错误')
}
})
try{
// 同步接口立即写入
wx.setStorageSync('key', 'value2')
console.log('写入value2成功')
}catch (e) {
console.log('写入value2发生错误')
}
表4-5 wx.setStorage/wx.setStorageSync详细参数
| 参数名 | 类型 | 必填 | 描述 |
|---|---|---|---|
| key | String | 是 | 本地缓存指定key |
| data | Object/String | 是 | 需要储存的内容 |
| success | Function | 否 | 异步接口调用成功的回调函数 |
| fail | Function | 否 | 异步接口调用失败的回调函数 |
| complete | Function | 否 | 异步接口调用结束的回调函数(调用成功、失败都会执行 |
4.6.2 缓存限制和隔离
10MB:小程序宿主环境会管理不同小程序的数据缓存,不同小程序的本地缓存空间是分开的,每个小程序的缓存空间上限为10MB,如果当前缓存已经达到10MB,再通过wx.setStorage写入缓存会触发fail回调。
用户隔离:小程序的本地缓存不仅仅通过小程序这个维度来隔离空间,考虑到同一个设备可以登录不同微信用户,宿主环境还对不同用户的缓存进行了隔离,避免用户间的数据隐私泄露。
注意:由于本地缓存是存放在当前设备,用户换设备之后无法从另一个设备读取到当前设备数据,因此用户的关键信息不建议只存在本地缓存,应该把数据放到服务器端进行持久化存储。
4.6.3 利用本地缓存提前渲染界面
讨论一个需求:我们要实现了一个购物商城的小程序,首页是展示一堆商品的列表。一般的实现方法就是在页面onLoad回调之后通过wx.request向服务器发起一个请求去拉取首页的商品列表数据,等待wx.request的success回调之后把数据通过setData渲染到界面
Page({
onLoda: function () {
var that = this
wx.request({
url: 'https://test.com/getproductlist',
success: function (res) {
if (res.statusCode === 200) {
that.setData({
list: res.data.list
})
}
}
})
}
})
再次进入白屏:设想一下当用户退出小程序再进来,界面仍然会有白屏现象,因为我们需要等待拉取商品列表的请求回来才能渲染商品列表。当然我们还可以再做一些体验上的优化,例如在发请求前,可能我们会在界面上显示一个Loading提示用户在加载中,但是并没有解决这个延迟渲染的现象,这个时候我们可以利用本地缓存来提前渲染界面。
我们在拉取商品列表把列表存在本地缓存里,在onLoad发起请求前,先检查是否有缓存列表,如果有的话直接渲染界面。然后等到wx.request的success回调之后再覆盖本地缓存重新渲染新的列表
代码清单4-16 page.js利用本地缓存提前渲染界面
Page({
onLoad: function () {
var that = this
var list = wx.getStorageSync("list")
if (list) { // 本地如果有缓存列表,提前渲染
that.setData({
list: list
})
}
wx.request({
url: 'https://test.com/getProductlist',
success: function (res) {
if (res.statusCode === 200) {
list = res.data.list
that.setData({ // 再次渲染
list: list
})
wx.setStorageSync("list",list) //覆盖缓存数据
}
}
})
}
})
这种做法可以让用户体验你的小程序时感觉加载非常快,但是你还要留意这个做法的缺点,如果小程序对渲染的数据实时性要求非常高的话,用户看到一个旧数据的界面会非常困惑。因此一般在对数据实时性/一致性要求不高的页面采用这个方法来做提前渲染,用以优化小程序体验。
4.6.4 缓存用户登录态SessionId
在4.4节我们说到处理用户登录态的一般方法,通常用户在没有主动退出登录前,用户的登录态会一直保持一段时间[10],就无需用户频繁地输入账号密码。如果我们把SessionId记录在Javascript中某个内存变量,当用户关闭小程序再进来小程序时,之前内存的SessionId已经丢失,此时我们就需要利用本地缓存的能力来持久化存储SessionId。
代码清单4-17 利用本地缓存持久存储用户登录态SessionId
// page.js
var app = getApp()
Page({
onLoad: function () {
// 拿到微信登录凭证之后去自己服务器换取自己的登录凭证
wx.request({
url: 'https://test.com/login',
data: { code: res.code },
success: function (res) {
var data = res.data
// 把SessionId 和过期时间放在内存中的全局对象和本地缓存里边
app.globalData.sessionId = data.seddionId
wx.setStorageSynx('SESSIONID',data.sessionId)
// 假设登录状态保持1天
var expireTime = +new Date() + 1*24*60*60*1000
app.globalData.expiredTime = expiredTime
wx.setStorageSync('EXPIREDTIME',expiredTime)
}
})
}
})
在重新打开小程序的时候,我们把上一次存储的SessionId内容取出来,恢复到内存。
代码清单4-18 利用本地缓存恢复用户登录态SessionId
// app.js
App({
onLaunch: function (options) {
var sessionId = wx.getStorageSync('SESSIONID')
var expiredTime = wx.getStorageSync('EXPIREDTIME')
var now = +new Date()
if (now - expiredTime <=1*24*60*60*1000) {
this.globalData.sessionId = sessionId
this.globalData.expiredTime = expiredTime
},
globalData: {
sessionId: null,
expiredTime: 0
}
}
})
4.7 设备能力
我们知道PC的程序和手机的程序有很大体验不一样的地方,尤其在信息输入这个体验上差别非常大,PC端可以有键盘、鼠标等等外设来辅助用户输入很多复杂的信息,而用户要在一个小小的手机屏幕上输入复杂信息效率是很低的。小程序的宿主环境提供了非常多的操作设备能力来帮助用户在特定场景下做高效的输入,例如:扫码、操控蓝牙等等能力。当然也有很多设备能力不是为了解决输入低效问题而设计的,它们更多的是解决用户侧一些体验问题,例如:获取设备网络状态;调整屏幕亮度等等,我们在这一章节中来介绍其中几种常见的操作设备能力的场景。
4.7.1 利用微信扫码能力
为了让用户减少输入,我们可以把复杂的信息编码成一个二维码,里有宿主环境wx.scanCode这个API调用微信扫一扫,用户扫码之后,wx.scanCode的success回调会受到这个二维码所对应的的字符串信息。
例如餐厅点餐的小程序,我们给餐厅中每个餐桌编号1-100号,把这个数字编码到二维码中[11],扫码获得编号之后,就可以知道是哪一桌点的菜,大大提高点餐体验和效率。
代码清单4-19 利用wx.scanCode获取二维码的数据
// page.js
Page({
//点击扫码订餐的按钮,触发tabScan回调
tabScan: function () {
//调用wx.login获取微信登录凭证
wx.scanCode({
success: function (res) {
var num = res.result // 获取到的num就是餐桌的编号
}
})
}
})
还有很多场景可以结合微信扫码能力做到很好的体验,例如通过扫商品上的一维码做一个商品展示的小程序;通过扫共享单车上的二维码去开启单车。我们可以多思考如何利用这个扫码能力去替代一些繁琐的输入操作,让我们的小程序变得更加便捷。
4.7.2 获取网络状态
我们知道手机连接互联网有几种方式:Wifi,2G,3G,4G,包括很快来的5G,每种方式的上传速度和下载速度差异很大,它们的计费方式的差异也导致用户在使用互联网服务的时候有不同的使用习惯。
增加网络使用体验:Wifi相对于其他几种网络连接方式,其速度会更快。Wifi一般都是免费供用户使用,通过移动数据网络是需要根据使用流量进行计费的。我们考虑这样一个场景,小程序需要下载一些文档,然后通过小程序的能力去预览这个文档,这些文档可能文件体积比较大,对于某些用户来说,他们并不想耗费太多的数据流量去预览文档。考虑到这样的情况,我们可以通过小程序提供的获取网络状态的能力,做一些更友好的体验提示。
代码清单4-20利用wx.getNetwordType获取网络状态
// page.js
Page({
// 点击预览文档的按钮,触发tab回调
tab: function () {
wx.getNetworkType({
success: function(res){
// networdkType的有效值:
// wifi/2g/3g/4g/unknown(Android下不常见的网络类型)/none(无网络)
if (res.networkType == 'wifi') {
// 从网络上下载pdf文档
wx.downloadFile({
url: 'https://test.com/somefile.pdf',
success: function (res) {
filePath: res.tempFilePath
}
})
} else {
wx.showToast({
title: '当前为非Wifi环境'
})
}
}
})
}
})
某些情况下,我们手机连接网络的方式动态改变,例如手机设备连接到一个信号不稳定的wifi热点,导致手机经常从wifi切换到移动数据网络。小程序宿主环境也提供了一个可以动态监听网络状态变化的接口wx.onNetworkStatusChange,让开发者及时根据网洛情况去调整小程序的体验。
4.8 本章小结
在本章我们学习了小程序开发中经常遇到的场景:利用Flex布局来进行小程序界面布局、常见的界面交互反馈、如何进行网络通信让小程序和开发者服务器进行交互等,我们就这些场景详细介绍了相关的API的使用技巧和注意事项,我们也把遇到问题时的思考方式阐述给读者,读者可以细细去体会,通过实践多动手去理解其中的方法论,更多的组件和API使用参数和示例可以参考官方的文档[12]。
阅读完本章,我们已经具备独立完成一个小程序的能力,为了让微信用户可以真正使用到你的小程序,学会如何发布小程序是必须要了解的知识,我们在下一个章节中来阐述如何发布一个小程序以及发布前后需要做一些什么事情。
- scroll-view组件官方文档地址https://mp.weixin.qq.com/debug/wxadoc/dev/component/scroll-view.html
- 登录mp.weixin.qq.com,在设置, 开发设置里配置request的服务器域名,同时TLS版本需要支持1.2及以上。
- 开发者工具需要勾选不校验可信域名;小程序开发版和体验版需要打开调试模式。
- 由于小程序在启动时会做异步更新版本检测,所以旧版本很快就不会有人访问到,但是依旧存在这种情况。
- 注意id不是指微信用户的微信号/密码等隐私信息,可以理解为id是微信用户在服务器的一个编号。
- 编写此书时,wx.login获取的code凭证的有效时间是5分钟。
- 注意这里的帐号密码指的是用户在业务侧的帐号密码,而不是其微信的帐号密码
- 小程序管理平台地址https://mp.weixin.qq.com
- 有关于同步/异步的区别见第二章2.4.1节同步与异步
- 这段时间由开发者根据自己的业务情况定义,为了安全,这段时间不宜设置太长
- 也可以把其他信息编码进来,如餐桌人数限制,或者灯光调节等等,为了避免复杂,我们这里只考虑餐桌号。
- 官方文档链接 mp.weixin.qq.com/debug/wxado…
#第5章 小程序协同工作和发布
#5.1 协同工作
5.1.1 人员组织结构和权限分配
多数情况下,一个团队多人同时参与同一个小程序项目,每个角色所承担的工作或者权限不一样,中大公司的分工更为仔细。为了更形象的表达团队不同角色的关系以及权限的管理,我们通过虚拟一个项目成员组织结构来描述日常如何协同合作完成一个小程序的发布,组织关系如图5-1所示。
file:///Users/xqls/Desktop/笔记/img/p-message.png

项目管理成员负责统筹整个项目的进展和风险、把控小程序对外发布的节奏,产品组提出需求,设计组与产品讨论并对需求进行抽象,设计出可视化流程与图形,输出设计方案。开发组依据设计方案,进行程序代码的编写,代码编写完成后,产品组与设计组体验小程序的整体流程,测试组编写测试用例并对小程序进行各种边界测试。项目一般的成员构成与工作流程如图5-2所示
file:///Users/xqls/Desktop/笔记/img/p-p.png

需要留意,项目管理者控制整个小程序的发布、回退、下架等敏感操作,不应把敏感操作的权限分配给不相关人员。
5.1.2 小程序的版本
一般的软件开发流程,开发者编写代码自测开发版程序,直到程序达到一个稳定可体验的状态时,开发者会把这个体验版本给到产品经理和测试人员进行体验测试,最后修复完程序的Bug后发布供外部用户正式使用。小程序的版本根据这个流程设计了小程序版本的概念,如表5-2所示。
刚刚说到开发者需要真机调试开发版本时,可以点击开发者工具的预览按钮,此时开发者工具会打包当前项目,并上传到微信服务器生成一个二维码,开发者使用当前开发身份的微信扫描二维码就可以在手机上体验对应的开发版本,如图5-3
file:///Users/xqls/Desktop/笔记/img/%20preview.png

在小程序管理平台上可以选取某个开发版作为体验版本,操作成功之后可以得到一个体验版的二维码,当前项目有权限的体验者均可用其对应的微信号进行扫码体验。
5.2 用户体验审视
在小程序发布之前,我们建议开发者务必对小程序做认真严格的用户体验审视。有创造性的产品思路、友好流畅的用户体验和稳定谨慎的运营也是一款优秀小程序能够脱颖而出的关键因素。
本小节将从产品和设计两方面,帮助开发者在小程序发布之前审视自身产品设计和用户体验情况,达到较高的用户体验水准,且是对用户友好的一款产品。
5.2.1 产品和运营思路
我们希望开发者能坚持“一切以用户价值为依归” 这一产品观,让创造发挥价值。产品设计过程中,开发者亦需要保持一定的克制心态。繁复的需求和商业行为,在用户利益前都需要谨慎平衡。
在运营行为上,小程序需要遵从微信小程序运营规范。若开发者的小程序违反了其中的条款、相关平台规则或法律法规,或对公众平台、开放平台造成了影响,可能会被强制处罚。条款将根据新问题、相关法律法规或产品运营需要对其内容进行修改并更新,详细条款请查阅小程序运营文档[3]。
5.2.2 体验和设计评估
在小程序的设计和开发阶段,我们希望开发者始终将优秀的用户体验作为产品目标之一,通过积极的用户体验和设计评估,在不断的迭代中完善用户体验,用心打磨小程序,从而更好实现产品价值,激发用户正向情感。因此,我们建议小程序在开发过程中以及发布之前,开发者务必对自身产品的体验和设计做全面走查。
开发者可依据以下9点基础设计原则,对小程序的体验进行评估。
1,导航清楚
导航是确保用户在网页中浏览跳转时不迷路的最关键因素。导航需要告诉用户,当前在哪,可以去哪,如何回去等问题。开发者在小程序的设计中,应确保各个页面之间层级清晰明确。每个界面的导航,都指向清晰,有路可退,实际应用样例请参考图5-4。
2,流程明确
为了让用户能够顺畅地使用小程序,用户进行操作时,应确定当前页面只设置了单一任务,且不出现目标流程之外的内容。此举有利于让用户明确当前操作的目的,从而集中精力聚焦当前任务,并通过简单操作达到结果。在图5-5中,出现了目标流程之外的操作,使用户体验受阻。
3,重点突出
4,符合预期 对用户友好的产品,需要在其设计阶段便将自身信息架构和模型与用户的心理模型匹配,以便于用户能够依据以往的使用经验或其他生活经验,降低使用的理解和学习成本,从而快速达成使用目的。
5,等待与反馈
在设计加载等待状态时,应注意以下事项:
若载入等待时间较长,应提供取消操作,并使用进度条显示载入的进度 载入过程中,应保持动画效果;无动画效果的加载很容易让人产生该界面已经卡死的错觉; 不要在同一个页面同时使用超过1个加载动画。
file:///Users/xqls/Desktop/笔记/img/loading.png

此外,对于用户的操作结果,小程序也需要给出明确的结果反馈,以增强用户的操作信心和控制感。开发者可以根据实际情况,选择不同的反馈样式。例如,对于页面局部的操作,可在操作区域予直接反馈。对于页面级别的操作结果,可使用弹出式提示、模态对话框或结果页面展示。具体示例如图5-9所示。
file:///Users/xqls/Desktop/笔记/img/title.png

6, 异常处理
在设计任务和流程时,往往用户会因为各种原因导致操作失败。此类异常场景往往是用户最为沮丧和需要帮助的时候,因此,要注意在异常状态下的设计。在出现异常的时候需要给予用户清晰的状态提示,并告知解决方案,使其有路可退。
上文提到的模态对话框和结果页面都可以作为异常状态的提醒方式。除此之外,在表单项较多的页面中,应明确指出出错项目,以便用户修改
7,内容和文案准确友好 在产品通过文案或者页面表达内容时,需要斟酌使用的内容和文案。使用的语言应当简洁,礼貌并容易被用户理解。此外,还要注意专业术语需要被清楚解释,特有词汇全局需用统一的特定表达,重要内容能够被快速获取,且页面不存在无关文案干扰用户决策。
8,和谐统一 小程序内的设计风格应该是统一、和谐且具有延续性的,这样才能确保用户建立完整的产品品牌认知,更好地辨析不同的小程序。
9,平台适配
在小程序的设计过程中,应该充分考虑iOS与Android平台不同的设计规范,对设计进行适当调整以适应不同平台上的用户使用习惯。
设计文档内容将不断进行修改、完善并更新,可以查看线上资讯[4]获取最新小程序信息。
5.2.3 用户体验测试和完善体验
5.3发布
5.3.1 发布前最后的检查
1,如果小程序使用到Flex布局,并且需要兼容iOS8以下系统时,请检查上传小程序包时,开发者工具是否已经开启“上传代码时样式自动补全”。
2,小程序使用的服务器接口应该走HTTPS协议,并且对应的网络域名确保已经在小程序管理平台配置好。
3,在测试阶段不要打开小程序的调试模式进行测试,因为在调试模式下,微信不会校验域名合法性,容易导致开发者误以为测试通过,导致正式版小程序因为遇到非法域名无法正常工作。
4,发布前请检查小程序使用到的网络接口已经在现网部署好,并且评估好服务器负载情况。
当体验版进行充分的检查和测试后达到发布状态,项目管理者可以在小程序平台进行提交审核的操作,提交审核后,微信审核团队会根据相关的运营规范进行提审小程序的审核。审核通过之后,管理者可以随时发布自己的小程序。
5.3.2 发布模式
小程序提供了两种发布模式:全量发布和分阶段发布。全量发布是指当点击发布之后,所有用户访问小程序时都会使用当前最新的发布版本。分阶段发布是指分不同时间段来控制部分用户使用最新的发布版本,分阶段发布我们也称为灰度发布。一般来说,普通小程序发布时采用全量发布即可,当小程序承载的功能越来越多,使用的用户数越来越多时,采用分阶段发布是一个非常好的控制风险的办法。因为随着程序的复杂度提高以及影响面的扩大,新版本的代码改动或多或少会带来Bug,作为服务方当然不希望异常的服务状态一下子扩散到整个用户群体,此时应该通过分阶段发布来逐步观察服务的稳定性,再决定是否进行全量发布。
还需要留意一点,并非全量发布之后,用户就会立即使用到最新版的小程序,这是因为微信客户端存有旧版本小程序包缓存。用户在使用小程序时会优先打开本地的小程序包,微信客户端在某些特定的时机异步去更新最新的小程序包。一般我们认为全量发布的24小时后,所有用户才会真正使用到最新版的小程序。
5.3.3 小程序码
小程序码在样式上更具辨识度和视觉冲击力,相对于二维码来说,小程序主题的品牌形象更加清晰明显,可以帮助开发者更好地推广小程序。在发布小程序之后,小程序管理平台会提供对应的小程序码的预览和下载,开发者可以自行下载用于线上和线下的小程序服务推广。
5.4 运营
5.4.1 数据分析
5.4.2 运维中心
第六章 底层框架
经过前面章节的学习,相信大家对小程序开发已经非常熟悉了。从这一章开始,我们会向大家介绍在编写小程序代码背后的方方面面的细节,而在这一章里我们会先深入小程序底层,介绍底层的架构设计,一些细节原理,以及大家所熟悉的组件系统。通过这一章的学习,我们可以在之后的小程序开发中编写出更合理的代码,遇到问题可以有依据可循,能想到更好的解决办法。
6.1 双线程模型
在前面第二章中,我们就有提到过小程序是基于双线程模型的,在这个模型中,小程序的逻辑层与渲染层分开在不同的线程运行,这跟传统的Web 单线程模型有很大的不同,使得小程序架构上多了一些复杂度,也多了一些限制。至于为何选择基于双线程模型来搭建小程序,以及因此而产生的问题和解决方案,接下来我们将一一介绍。
6.1.1 技术选型
一般来说,渲染界面的技术有三种:
- 用纯客户端原生技术来渲染
- 用纯 Web 技术来渲染
- 介于客户端原生技术与 Web 技术之间的,互相结合各自特点的技术(下面统称 Hybrid 技术)来渲染
6.1.2 管控与安全
6.2 系统组件
我们设计一套组件框架——Exparser。基于这个框架,内置了一套组件,以涵盖小程序的基础功能,便于开发者快速搭建出任何界面。同时也提供了自定义组件的能力,开发者可以自行扩展更多的组件,以实现代码复用。
6.2.1 Exparser框架
Exparser是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。
Exparser的组件模型与WebComponents标准中的ShadowDOM高度相似。Exparser会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的Shadow DOM实现。Exparser的主要特点包括以下几点:
- 基于Shadow DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
- 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
- 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。
6.2.2 内置组件
我们基于Exparser框架,内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合WXSS,我们可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。
一般而言,我们会把一个组件内置到小程序框架里的一个重要原则是:这个组件是基础的。换句话说,没有这个组件的话,在小程序架构里无法实现或者实现不好某类功能。比如像一些开放类组件,有open-data组件提供展示群名称、用户信息等微信体系下的隐私信息,有button组件里open-type属性所提供分享、跳转App等敏感操作的能力。还有比如像视图容器类组件movable-view这种因双线程模型导致手势识别不好实现的组件,这是因为手势识别需要高频率捕捉手指的触摸事件,而在双线程模型中,触摸事件从渲染层发出,派发到逻辑层,这中间是有一定的延时而导致视图跟随手指运动这类交互变得有些卡顿。
6.2.3 自定义组件
自定义组件是开发者可以自行扩充的组件。开发者可以将常用的节点树结构提取成自定义组件,实现代码复用。
- ShadowTree的概念 我们以下面的代码为例来阐述Shadow Tree的概念。
代码清单6-2 页面节点树(Composed Tree)
<view>
<input-with-label>
<label>
TEXT
</label>
</input-with-label>
</view>
这里如果将input-with-label抽象成一个组件,那么可以将整个节点树拆分成两部分。
代码清单6-3 组件节点树(Shadow Tree)
<label><slot/></label>
<input/>
代码清单6-4 调用组件的节点树
<view>
<input-with-label>
TEXT
</input-with-label>
</view>
组件的节点树称为“ShadowTree”,即组件内部的实现;最终拼接成的页面节点树被称为“Composed Tree”,
即将页面所有组件节点树合成之后的树。在进行了这样的组件分离之后,整个页面节点树实质上被拆分成了若干个ShadowTree(页面的body实质上也是一个组件,因而也是一个ShadowTree)
同时,各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData调用,createSelectorQuery也将运行在Shadow Tree的层面上。关于具体如何使用自定义组件特性,这里不再详细讨论,请参阅小程序开发文档。
2.运行原理
在使用自定义组件的小程序页面中,Exparser将接管所有的自定义组件注册与实例化。从外部接口上看,小程序基础库提供有Page和Component两个构造器。以Component为例,在小程序启动时,构造器会将开发者设置的properties、data、methods等定义段,写入Exparser的组件注册表中。这个组件在被其它组件引用时,就可以根据这些注册信息来创建自定义组件的实例。Page构造器的大体运行流程与之相仿,只是参数形式不一样。这样每个页面就有一个与之对应的组件,称为“页面根组件”。
在初始化页面时,Exparser会创建出页面根组件的一个实例,用到的其他组件也会响应创建组件实例(这是一个递归的过程)。组件创建的过程大致有以下几个要点:
- 根据组件注册信息,从组件原型上创建出组件节点的JS对象,即组件的this;
- 将组件注册信息中的data 复制一份,作为组件数据,即this.data;
- 将这份数据结合组件WXML,据此创建出Shadow Tree,由于Shadow Tree中可能引用有其他组件,因而这会递归触发其他组件创建过程;
- 将ShadowTree拼接到Composed Tree上,并生成一些缓存数据用于优化组件更新性能;
- 触发组件的created生命周期函数;
- 如果不是页面根组件,需要根据组件节点上的属性定义,来设置组件的属性值;
- 当组件实例被展示在页面上时,触发组件的attached 生命周期函数,如果Shadw Tree中有其他组件,也逐个触发它们的生命周期函数。
3.组件间通信
不同组件实例间的通信有WXML属性值传递、事件系统、selectComponent和relations等方式。其中,WXML属性值传递是从父组件向子组件的基本通信方式,而事件系统是从子组件向父组件的基本通信方式。
Exparser的事件系统完全模仿Shadow DOM的事件系统。在通常的理解中,事件可以分为冒泡事件和非冒泡事件,但在ShadowDOM体系中,冒泡事件还可以划分为在Shadow Tree上冒泡的事件和在Composed Tree上冒泡的事件。如果在Shadow Tree上冒泡,则冒泡只会经过这个组件Shadow Tree上的节点,这样可以有效控制事件冒泡经过的范围。
代码清单6-5 input-with-label的WXML
<label>
<input />
<slot />
</label>
代码清单6-6 页面WXML
<view>
<input-with-label>
<button />
</input-with-label>
</view>
用上面的例子来说,当在 button 上触发一个事件时:
l 如果事件是非冒泡的,那只能在 button 上监听到事件;
l 如果事件是在 Shadow Tree 上冒泡的,那 button 、 input-with-label 、view 可以依次监听到事件;
l 如果事件是在 Composed Tree 上冒泡的,那 button 、 slot 、label 、 input-with-label 、 view 可以依次监听到事件。
在自定义组件中使用triggerEvent触发事件时,可以指定事件的bubbles、composed和capturePhase属性,用于标注事件的冒泡性质。
代码清单6-7 triggerEvent事例
Component({
methods: {
helloEvent: function() {
this.triggerEvent('hello', {}, {
bubbles: true, // 这是一个冒泡事件
composed: true, // 这个事件在Composed Tree 上冒泡
capturePhase: false // 这个事件没有捕获阶段
})
}
}
})
小程序基础库自身也会通过这套事件系统提供一些用户事件,如tap、touchstart和form组件的submit等。其中,tap等用户触摸引发的事件是在ComposedTree上的冒泡事件,其他事件大多是非冒泡事件。
6.3 原生组件
在内置组件中,有一些组件较为特殊,它们并不完全在Exparser的渲染体系下,而是由客户端原生参与组件的渲染,这类组件我们称为“原生组件”,这也是小程序Hybrid技术的一个应用。
6.3.1 原生组件运行机制
代码清单6-8 展示一个地图组件
<map latitude="39.92" longtitude="116.46"></map>
引入原生组件主要有3个好处:
- 扩展Web的能力。比如像输入框组件(input, textarea)有更好地控制键盘的能力。
- 体验更好,同时也减轻WebView的渲染工作。比如像地图组件(map)这类较复杂的组件,其渲染工作不占用WebView线程,而交给更高效的客户端原生处理。
- 绕过setData、数据通信和重渲染流程,使渲染性能更好。比如像画布组件(canvas)可直接用一套丰富的绘图接口进行绘制。
表6-1 常用的几个原生组件
| 组件名 | 名称 | 是否有context | 描述 |
|---|---|---|---|
| video | 视频 | 是 | 播放视频 |
| map | 地图 | 是 | 展示地图 |
| canvas | 画布 | 是 | 提供一个可以自由绘制的区域 |
| picker | 弹出式选择器 | 否 | 初始时没有界面,点击时弹出选择器 |
交互比较复杂的原生组件都会提供“context”,用于直接操作组件。以canvas为例,小程序提供了wx.createCanvasContext方法来创建canvas的context。
这是一个可以用于操作canvas的对象,对象下提供了很多绘图的方法,如“setFillStyle”方法可以设置填充样式,“fillRect”方法用于绘制矩形(这些方法与HTML DOM Canvas兼容)。
<canvas canvas-id="myCanvas"></canvas>
代码清单6-10 canvas组件context对象示例(JS代码)
const ctx = wx.createCanvasContext('myCanvas')
ctx.setFillStyle('red')
ctx.fillRect(10, 10, 150, 75)
ctx.draw()
这段代码可以创建WXML中对应canvas节点的context,通过调用context中的方法,在画布上绘制一个矩形。
6.3.2 原生组件渲染限制
原生组件脱离在WebView渲染流程外,这带来了一些限制。最主要的限制是一些CSS样式无法应用于原生组件,例如,不能在父级节点使用overflow:hidden来裁剪原生组件的显示区域;不能使用transformrotate让原生组件产生旋转等。
开发者最为常见的问题是,原生组件会浮于页面其他组件之上(相当于拥有正无穷大的z-index值)使其它组件不能覆盖在原生组件上展示。想要解决这个问题,可以考虑使用cover-view和cover-image组件。这两个组件也是原生组件,同样是脱离WebView的渲染流程外,而原生组件之间的层级就可以按照一定的规则控制。
#6.4 小程序与客户端通信原理
6.4.1 视图层组件
6.4.2 逻辑层接口
##6.5 本章小结
在本章中我们介绍了小程序底层框架的设计和原理,提出了一个全新的双线程模型,这是小程序框架与业界大多数前端Web框架不同之处。基于这个模型,我们可以做到更好地管控以及提供更安全的环境。但同时带来了无处不在的异步问题,不过我们在框架层面去封装好异步带来的时序问题,让开发者只需要懂得上层更易为理解的接口。此外,我们也介绍了基于双线程模型的组件框架,以及原生组件的机制,让开发者进一步理解以写出更合理的代码。最后,我们也阐述了小程序是如何与客户端通信的,这是小程序运行起来的最基本的原理。
[1] PhoneGap 是一个采用HTML,CSS和JavaScript的技术,创建移动跨平台移动应用程序的框架
[2] ReactNative 是一个 JavaScript 框架,用来编写原生的 iOS 和 Android 应用。
[3] 微信JS-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包。