【导读】
本文是对官方【小程序开发指南】的精简 + 解读,帮助你更好地理解小程序。理解小程序为什么出现,理解小程序的底层原理等等。
【注】
本文仅做笔记式记录,如有侵权,烦请告知。
第一部分:初识小程序
一、微信为什么要推出小程序
最初,在微信中展现 web 页面,是通过 webview。
web 调用原生的一些能力,通过微信团队未对外开放的 WeixinJSBridge。
后来,WeixinJSBridge 演变为对外开放的 JS-SDK。
JS-SDK 是对之前的 WeixinJSBrige 的一个包装,以及新能力的释放。
【存在痛点】
移动网页体验不良。
【方案 v1】
推出 JS-SDK 增强版(未对外开放)。
增加“微信 web 资源离线存储”功能。
【方案 v1 的问题】
(1)复杂页面依然会有白屏问题。如 CSS、JavaScript 较多,文件执行会占用大量 UI 线程,造成白屏。
(2)缓存方案处理较为繁杂,对开发者的要求较高。
【愿景】
设计一个比较好的系统,使得所有开发者在微信中都能获得比较好的体验。
包括:
(1)快速的加载
(2)更强大的能力(比如使用原生能力)
(3)原生的体验
(4)易用且安全的微信数据开放
(5)高效和简单的开发
【解决方案】
推出小程序。
二、小程序与普通网页开发的区别
四种主要区别:
UI 渲染和脚本执行是否使用同一线程、是否可进行 DOM 操作、运行环境、开发依赖
1、UI 渲染和脚本执行方式
网页:UI 渲染和脚本执行用同一个线程,脚本执行会阻塞 UI 渲染。
小程序:UI 渲染和逻辑执行使用不同的线程,脚本执行不会阻塞 UI 渲染。
2、DOM 操作
网页:可以使用 DOM、BOM API,执行 DOM、BOM 操作
小程序:逻辑层运行在 JSCore 中,并没有一个完整的浏览器对象,也就没有 DOM、BOM API,无法执行 DOM、BOM 操作。
【注】
JSCore 的环境不同于浏览器,也不同于 NodeJS。因此,除了无法使用 DOM、BOM API,一些 NPM 包在小程序中也无法运行。
3、运行环境
网页:各种浏览器、各式 webview
小程序:iOS、安卓、小程序开发者工具
![](https://img.haomeiwen.com/i7566087/653e283793afd6a2.png)
4、开发依赖
网页:编辑器、浏览器
小程序:小程序帐号、开发者工具
第二部分:小程序代码组成
一、JSON 配置
JSON 文件在小程序代码中扮演静态配置的作用,在小程序运行之前就决定了小程序一些表现。
【注意】
小程序是无法在运行过程中去动态更新 JSON 配置文件从而发生对应的变化的。
二、WXML 模板
1、属性大小写敏感:class 和 Class 是不同的属性。
2、变量名大小写敏感:{{name}} 和 {{Name}} 是不同的变量。
3、wx: for = "array":默认变量名 item,默认下标名 index。
4、wx:key 的两种形式:
wx: key = "uniqueKey" 或 wx:key = "*this"(代表 item 本身,要求 item 是唯一的 string / number)
5、模板:
可以在模板中定义代码片段,然后在不同的地方调用。使用 name 属性,作为模板的名字。然后在 <template/> 内定义代码片段。
定义模板:
<template name="msgItem">
<view>
<text> {{index}}: {{msg}} </text>
<text> Time: {{time}} </text>
</view>
</template>
使用模板:
<!--
item: {
index: 0,
msg: 'this is a template',
time: '2016-06-18'
}
-->
<template is="msgItem" data="{{...item}}"/>
动态使用模板:
<template name="odd">
<view> odd </view>
</template>
<template name="even">
<view> even </view>
</template>
<block wx:for="{{[1, 2, 3, 4, 5]}}">
<template is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
</block>
6、引用:
WXML 提供两种文件引用方式 import 和 include。
(1)import:用于引入 template
在 index.wxml 中 import 其他 template 文件,就可以在 index.wxml 中使用相应 template。
eg:
模板定义:
<!-- item.wxml -->
<template name="item">
<text>{{text}}</text>
</template>
模板引用与使用:
<import src="item.wxml"/>
<template is="item" data="{{text: 'forbar'}}"/>
【注】
import 有作用域的概念。
即:C 引用 B,B 引用A,在C中可以使用B定义的 template,在B中可以使用A定义的 template ,但是C不能使用A定义的template 。
(2)include:用于引入任意整块代码
include 可以将目标文件中 除了 <template/> <wxs/> 外 的整个代码引入,相当于是拷贝到 include 位置。
<!-- header.wxml -->
<view> header </view>
<!-- footer.wxml -->
<view> footer </view>
<!-- index.wxml -->
<include src="header.wxml"/>
<view> body </view>
<include src="footer.wxml"/>
7、所有 wxml 标签都支持的属性:
![](https://img.haomeiwen.com/i7566087/399e2b13d7fdf8e5.png)
三、WXSS 样式
1、rpx 单位:对于 750px 宽的设计稿,1px = 2rpx
2、样式引用:@import './test_0.wxss'
【注】
和 css 样式引用的区别:
index.css 中 @import url('./test_0.css'),请求 index.css 的时候,会多一个 test_0.css 的请求。
index.wxss 中 @import './test_0.wxss',请求 index.wxss 的时候不会多一个请求,test_0.wxss 会被编译打包到 index.wxss 中。
3、官方样式库:WeUI
四、JavaScript 脚本
1、概述
理解 JavaScript 与 ECMAScript:
JavaScript 是 ECMAScript 一种实现。小程序中的 JavaScript同浏览器中的 JavaScript 以及 NodeJS 中的 JavaScript 是不相同的。
![](https://img.haomeiwen.com/i7566087/328de821c19ed713.png)
![](https://img.haomeiwen.com/i7566087/930b4a2ab7553cbf.png)
![](https://img.haomeiwen.com/i7566087/5b34532851ca7c16.png)
【注】
1、相比浏览器环境:小程序无法操作 DOM、BOM
2、相比 NodeJS 环境:小程序中无法加载原生库,也无法直接使用大部分的 NPM 包
2、小程序的执行环境
三大执行环境:iOS平台、Android平台、小程序IDE
【注】
iOS9 和 iOS10 会不支持一些 es6 语法。
开发中,需要用开发者工具勾选“ES6 转 ES5”,才能确保代码在所有的环境都能得到很好的执行。
3、模块化
导出:
// moduleA.js
module.exports = function() {}
使用:
require('./moduleA')
4、 脚本的执行顺序
入口:app.js
执行完 app.js 及其 require 的脚本后,小程序会按照 app.json 中定义的 pages 的顺序,逐一执行。
5、作用域
(1)在文件中声明的变量和函数,只在该文件中有效。
(2)全局变量的声明:
在 app.js 中声明:
// app.js
App({
globalData: 1
})
使用:
var app = getApp()
console.log('app.globalData:', app.globalData) // 1
也可以在其他文件中设置全局变量:
var app = getApp()
app.addGlobalData = 'Sherry'
其他文件可访问到 addGlobalData:
var app = getApp()
console.log('app.addGlobalData:', app.addGlobalData) // 'Sherry'
第三部分:理解小程序宿主环境
一、渲染层和逻辑层
wxml、wxss 工作在渲染层,js 脚本工作在逻辑层。
1、通信模型
![](https://img.haomeiwen.com/i7566087/6a8606c4ab792690.png)
渲染层:使用 webview 线程渲染界面。一个小程序存在多个页面,所以渲染层存在多个 webview 线程。
逻辑层:使用 JsCore 线程运行 JS 脚本。
webview 线程和 JsCore 线程通信,通过 Native,也就是微信客户端。
2、数据驱动
数据变更,视图自动更新。即:数据驱动视图。
![](https://img.haomeiwen.com/i7566087/0447dfb1116d5351.png)
数据更新,上图 JS 对象对应的节点就会发生变化。
对比前后两个JS对象得到变化的部分,然后把这个差异应用到原来的Dom树上,从而达到更新UI的目的,这就是“数据驱动”的原理。
![](https://img.haomeiwen.com/i7566087/00972afb758c9183.png)
【个人解读】
所谓数据驱动,即不关心视图,仅操纵数据。
逻辑层的任务,仅仅是执行用户的 setData 操作,然后把更新后的数据传给渲染层而已。
渲染层拿到最新的数据后,会根据当前的 wxml 结构以及最新数据、生成新的 JS 对象,并与当前的 JS 对象进行对比,得到改动的地方,再把差异更新到原来的 Dom 树(形如:document.getElementById() 获取到需要变更的真实 Dom 节点,然后更改它的属性值 or 子节点等)。
![](https://img.haomeiwen.com/i7566087/624131ec072da2ae.png)
二、程序与页面
1、初始化
1)微信客户端初始化宿主环境,同时从网络下载或者从本地缓存中拿到小程序的代码包,把它注入到宿主环境
2)初始化完成,微信客户端就会给 App 实例派发 onLaunch 事件,onLaunch 被调用
2、退出
点击小程序右上角的关闭,或按手机设备的Home键离开小程序,小程序并没有被直接销毁。
这时,只是进入后台状态,App 构造器定义的 onHide 方法会被调用。
3、重新进入
再次回到微信或者再次打开小程序时,小程序进入前台状态,App 构造器定义的 onShow 方法会被调用。
4、全局数据
// app.js
App({
globalData: 'I am global data' // 全局共享数据
})
由于 JS 逻辑统一运行在逻辑层,仅有一个线程。因此,页面切换,切换 webview 时,逻辑层并没有发生改变,JS 的数据依然可以被访问到。
【注】
也因此,在页面中写的 setTimeout 或者 setInterval 定时器,离开页面时,并没有被删除,需要开发者手动删除。
5、页面生命周期
(1)onLoad => onShow => onReady
onLoad、onReady 在页面销毁之前,仅触发一次。
onLoad:监听页面加载。
onReady:监听页面初次渲染完成。
onReady 触发时,表示页面已经准备妥当,在逻辑层就可以和视图层进行交互了。
(2)setData
this.setData 把数据传递给渲染层,从而达到更新界面的目的。
setData 的第二个参数是一个 callback 回调,在这次 setData 对界面渲染完毕后触发。
【注】
1)我们只要保持一个原则就可以提高小程序的渲染性能:每次只设置需要改变的最小单位数据。
如:
// 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})
}
})
this.setData({"d[1].text": 'Goodbye'});
2)不要把 data 中的任意一项的 value 设为 undefined,否则可能会有引起一些不可预料的 bug。
6、小程序页面栈
小程序宿主环境限制了页面栈的最大层级为 10 层。
wx.navigateTo({ url: 'pageD' }):插入新页面。
wx.navigateBack():销毁最上层的页面。
wx.redirectTo({ url: 'pageE' }):替换最上层的页面。
wx.switchTab({ url: 'pageF' }):页面栈先清空,然后 push pageF。变为:[ pageF ]
wx. reLaunch({ url: 'pageH' }) :重启小程序,并且打开 pageH,此时页面栈为 [ pageH ]
【注】
1)wx.navigateTo 和 wx.redirectTo 只能打开非TabBar页面,wx.switchTab 只能打开 Tabbar 页面。
2)Tabbar 页面初始化之后不会被销毁
(这句话是官方给的描述,但看官方的 demo,应该只是,第一个 Tabbar 页面初始化之后不会被销毁)
3)销毁了的页面再次打开,会再次触发 onLoad、onShow、onReady,未销毁的页面再次打开、仅触发 onShow。
三、事件
事件捕获和事件冒泡触发时序:
![](https://img.haomeiwen.com/i7566087/ff5d6181a9a6adaa.png)
eg:点击 inner view,调用顺序:handleTap2、handleTap4、handleTap3、handleTap1。
<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>
bind* 和 catch* 的区别:
bind 事件绑定不会阻止冒泡事件向上冒泡,catch 事件绑定可以阻止冒泡事件向上冒泡。
capture-catch 将中断捕获阶段和取消冒泡阶段。
eg:点击 inner view,只触发 handleTap2。
<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>
四、兼容
1、wx.getSystemInfo 与 wx.getSystemInfoSync
使用 wx.getSystemInfo 或者 wx.getSystemInfoSync 来获取手机品牌、操作系统版本号、微信版本号以及小程序基础库版本号等,通过这个信息,我们可以针对不同平台做差异化的服务。
2、判断当前 api 是否存在:
if (wx.openBluetoothAdapter) {
wx.openBluetoothAdapter()
} else {
// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示
wx.showModal({
title: '提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
})
}
3、wx.canIUse
// 判断接口及其参数在宿主环境是否可用
wx.canIUse('openBluetoothAdapter')
wx.canIUse('getSystemInfoSync.return.screenWidth')
// 判断组件及其属性在宿主环境是否可用
wx.canIUse('contact-button')
wx.canIUse('text.selectable')
第四部分:场景应用
1、要兼容到iOS8以下版本,需要开启样式自动补全。(项目设置中设置)
2、一些特殊的样式设置方式:
(1)hover 样式(手指触摸时的样式)
/*page.wxss */
.hover {
background-color: gray;
}
<!--page.wxml -->
<button hover-class="hover"> 点击button </button>
(2)button 的 loading
<!--page.wxml -->
<button loading="{{loading}}" bindtap="tap">操作</button>
(3)弹窗
wx.showToast、wx.showModal
3、HTTPS 网络通信
(1)小程序要求: wx.request 发起的必须是 https 协议请求。并且,请求的域名需要在管理平台进行配置
(2)wx.request url 长度限制:不超过 1024字节
(3)设置超时时间:app.json 👇
{
"networkTimeout": {
"request": 3000
}
}
(4) 排查异常的方法参考:4.4.6 排查异常方法
4、微信登录
(1)AppId 是公开信息,泄露 AppId 不会带来安全风险
(2)code 有效期 5 分钟。但在成功换取一次用户信息之后,code 会立即失效
(3)开发者服务器用 code 去微信服务器换取用户信息,会拿到:
openid、session_key、unionid
其中,session_key 用作开发者服务器后续与微信服务器通信的凭证。
由于 session_key 有效期长于 code,因此,可以减少多次获取 code、换取用户信息的通信成本。
5、本地数据缓存:wx.getStorage / wx.getStorageSync、wx.setStorage / wx.setStorageSync
本地数据缓存是小程序存储在当前设备硬盘上的数据。
(1)缓存空间隔离:
1)不同小程序的本地缓存空间相互隔离
2)不同用户的缓存相互隔离
(2)每个小程序的缓存空间上限为10MB
(3)可以利用缓存 — 提前渲染页面:
比如商品列表数据,在用户第一次进入小程序、请求回列表数据后,缓存到本地。
用户再次进入,直接从缓存读取上一次的数据,显示到页面。同时,wx.request 请求最新列表数据,请求回之后,页面重新渲染最新数据。
注意,这种性能提升方式,仅适合数据实时性要求不高的场景。
(4)可以利用缓存 — 缓存用户登录态 SessionId
按官方文档,建议同步手动缓存一个过期时间。
wx.setStorageSync('EXPIREDTIME',expiredTime)
使用缓存的 SessionId 前,先判断是否过期,如果过期,直接 wx.login 重新登录,减少携带旧 SessionId 的不必要请求。
6、设备能力
(1)wx.scanCode:调起微信扫一扫
(2)wx.getNetworkType:获取网络状态(wifi、2G、3G、4G、5G)
(3)wx.onNetworkStatusChange:动态监听网络状态变化的接口
第五部分:小程序的发布
1、发布正式版后,正式小程序无法使用,问题排查
(1)如果小程序使用到 Flex 布局,并且需要兼容 iOS8 以下系统时,请检查上传小程序包时,开发者工具是否已经开启“上传代码时样式自动补全”。
(2)小程序使用的服务器接口应该走 HTTPS 协议,并且对应的网络域名确保已经在小程序管理平台配置好。
(3)在测试阶段不要打开小程序的调试模式进行测试,因为在调试模式下,微信不会校验域名合法性,容易导致开发者误以为测试通过,导致正式版小程序因为遇到非法域名无法正常工作。
(4)发布前请检查小程序使用到的网络接口已经在现网部署好,并且评估好服务器的机器负载情况。
2、发布模式
全量发布和分阶段发布(灰度发布)
【注】
并非全量发布之后,用户就会立即使用到最新版的小程序。
因为微信客户端有旧版本小程序包缓存。
微信客户端在某些特定的时机异步去更新最新的小程序包。
一般我们认为全量发布的24小时后,所有用户才会真正使用到最新版的小程序。
3、数据分析
(1)常规分析
开发网页和 App 应用都需要开发者自己通过编写代码来上报访问数据,小程序平台则直接内置在宿主环境底层,无需开发者新增一行代码。
开发者可以登录小程序管理平台,通过左侧“数据分析”菜单可以进入数据分析查看。
(2)自定义分析:见官网
4、监控与告警
小程序宿主环境已经内置了异常检测的模块,并且上报到小程序平台。
目前只提供了脚本错误告警,如果需要监控异常的访问或者服务接口耗时时,需要开发者自行开发监控系统,并在小程序逻辑代码加上对应的数据上报。
比较推荐的方法是通过运维中心的监控告警功能,开发者设置合理的错误阈值,再通过加入微信告警群,当小程序运行发生大量异常现象时,微信告警群会提醒开发者,此时开发者再登录小程序管理平台查阅错误日志。
第六部分:底层框架
一、双线程模型
1、什么是双线程模型?
渲染层 + 逻辑层。
渲染层负责 UI 渲染,每个页面使用一个 webview 线程来渲染。
逻辑层负责执行 JS 逻辑。
二者通过微信客户端(native)进行通信。
2、为什么是双线程模型?
1)渲染层运行环境:确保性能
由成熟的 Web 技术渲染页面,并以大量的接口提供丰富的客户端原生能力。
与逻辑层分离,页面渲染不被 JS 的执行阻塞。
2)逻辑层运行环境:确保安全
由于 web 技术的灵活性,使用 web 技术渲染页面,外部可以通过 JavaScript 操作页面,并获取到敏感组件的数据信息,缺乏安全性。
因此,需要一个安全的沙箱环境,去运行开发者的 JavaScript 代码。这个沙箱环境,仅仅提供纯 JavaScript 的解释执行环境。
由于客户端系统都具有JavaScript 的解释引擎(iOS 有内置 JavaScriptCore 框架、安卓有腾讯 x5 内核提供的 JsCore 环境),因此,我们可以创建一个单独的线程去执行 JavaScript。这个单独的线程,就是小程序的逻辑层。
二、组件系统:Exparser
Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由Exparser组织管理。
1、运行原理:
以 Component 为例(Page流程大致相仿,只是参数形式不一样)
在小程序启动时,构造器会将开发者设置的properties、data、methods等定义段,写入Exparser 的组件注册表中。
这个组件在被其它组件引用时,就可以根据这些注册信息来创建自定义组件的实例。
2、组件间通信
父 => 子:WXML 属性值
子 => 父:事件系统
1)事件冒泡:
input-with-label 的 WXML:
<label>
<input />
<slot />
</label>
页面 WXML:
<view>
<input-with-label>
<button />
</input-with-label>
</view>
l 如果事件是非冒泡的,那只能在 button 上监听到事件;
l 如果事件是在 Shadow Tree 上冒泡的,那 button 、 input-with-label 、view 可以依次监听到事件;
l 如果事件是在 Composed Tree 上冒泡的,那 button 、 slot 、label 、 input-with-label 、 view 可以依次监听到事件。
【附】
Shadow Tree 对应一个组件,Composed Tree 对应一个页面。
一个 Composed Tree 由多个 Shadow Tree 构成。
2)triggerEvent
在自定义组件中使用 triggerEvent 触发事件时,可以指定事件的 bubbles、composed 和 capturePhase 属性,用于标注事件的冒泡性质。
triggerEvent 事例:
Component({
methods: {
helloEvent: function() {
this.triggerEvent('hello', {}, {
bubbles: true, // 这是一个冒泡事件
composed: true, // 这个事件在Composed Tree 上冒泡
capturePhase: false // 这个事件没有捕获阶段
})
}
}
})
三、原生组件:由客户端渲染的组件(非 webview 渲染)
部分内置组件,是由客户端原生渲染的,如 vedio、map、canvas、picker 组件。
原生组件的层级比所有在 WebView 层渲染的普通组件要高。
限制:
一些CSS样式无法应用于原生组件。
四、小程序与客户端通信原理
1、视图层与客户端的通信
iOS:利用了WKWebView 的提供 messageHandlers 特性。
安卓:往 WebView 的 window 对象注入一个原生方法,最终会封装成 WeiXinJSBridge 这样一个兼容层。
2、逻辑层与客户端的通信
iOS:同上。另外,可以往 JavaScripCore 框架注入一个全局的原生方法
安卓:同上。
第七部分:性能优化
一、启动
1、主流程
在小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。
![](https://img.haomeiwen.com/i7566087/5dbc3a8b3cc154b4.png)
2、代码包下载
这里下载的,是经过编译、压缩、打包之后的代码包。
【性能优化点】
控制代码包大小 有助于减少小程序的启动时间。
【具体方式】
(1)精简代码,去掉不必要的 WXML 结构和未使用的 WXSS 定义。
(2)减少在代码包中直接嵌入的资源文件。
(3)压缩图片,使用适当的图片格式。
必要时,进行分包处理。
小程序启动时,只需要先将主包下载完成,就可以立刻启动小程序。
3、代码包加载
微信会在小程序启动前为小程序准备好通用的运行环境。
这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。
小程序代码包下载(或从缓存中读取)完成后,小程序的代码会被加载到适当的线程中执行。
加载过程👉 此时,所有 app.js、页面所在的 JS 文件和所有其他被 require 的 JS 文件会被自动执行一次,小程序基础库会完成所有页面的注册。
其中,页面注册、即:在小程序代码调用 Page 构造器的时候,基础库会记录页面的基础信息,如初始数据(data)、方法等。
【注意】
如果一个页面被多次创建,并不会使得这个页面所在的 JS 文件被执行多次,而仅仅是根据初始数据多生成了一个页面实例(this),在页面 JS 文件 Page 构造器外定义的变量,在所有这个页面的实例(this)间是共享的。
也就是说,Page 构造器外定义的变量,相当于当前页面的全局变量,无论创建几个页面实例,访问的都是同一个全局变量。
二、 页面层级准备
在小程序启动前,微信会提前准备好一个页面层级用于展示小程序的首页。
除此以外,每当一个页面层级被用于渲染页面,微信都会提前开始准备一个新的页面层级。
页面层级的准备工作(3个阶段):
(1)启动一个 WebView
(2)在 WebView 中初始化基础库,此时还会进行一些基础库内部优化,以提升页面渲染性能
(3)注入小程序 WXML 结构和 WXSS 样式,使小程序能在接收到页面初始数据之后马上开始渲染页面(这一阶段无法在小程序启动前执行)
![](https://img.haomeiwen.com/i7566087/ceedee441734d552.png)
【注】
小程序未加载时,微信客户端根本没拿到小程序代码,所以无法进行注入。
三、数据通信
数据通信性能提升原则:
(1)减少通信
(2)减少通信传输的数据量
1、页面初始数据通信
一个新的页面打开,逻辑层将初始 data 发送给 Native,Native 做两件事
1)将数据传给视图层
2)向用户展示一个新的页面层级(视图层在这个页面层级上进行界面绘制)
视图层拿到数据后,根据页面路径来选择合适的 WXML 结构,WXML 结构与初始数据相结合,得到页面的第一次渲染结果。
【性能瓶颈】
1)页面初始数据通信时间
2)初始渲染时间
【性能优化点】
减少页面初始数据通信时间
【具体方式】
减少传输数据量。
2、 更新数据通信
更新数据传输时,逻辑层首先执行 JSON.stringify,去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。
同时,逻辑层会将 setData 与 data 合并,更新数据。
【性能优化点】
1)setData 的调用频率
2)setData 设置的数据大小
3)data 数据的精简
【具体方式】
1)减少 setData 的调用,将多次 setData 合并成一次
2)精简 setData 设置的数据,界面无关或比较复杂的长字符串、尽量不要用 setData 设置
3)与界面渲染无关的数据最好不要设置在 data 中
3、用户事件通信
用户触发一个事件,且这个事件存在监听函数,视图层会将触发信息反馈给逻辑层。
如果事件没有绑定监听函数,则不反馈给逻辑层。
【性能优化点】
提升视图层与逻辑层的通信性能。
【具体方式】
1)去掉不必要的事件绑定(WXML中的 bind 和 catch),减少通信的数据量和次数
2)事件绑定时需要传输 target 和 currentTarget 的 dataset,因而不要在节点的 data 前缀属性中放置过大的数据
四、视图层渲染
1、初始渲染
将初始数据套用在对应的 WXML 片段上生成节点树。
【性能优化点】
减少初始渲染时间。
【具体方式】
减少 WXML 中节点的数量(时间开销大体上与节点树中节点的总量成正比)
2、重渲染
初始渲染中得到的 data 和当前节点树会保留下来用于重渲染。
【重渲染步骤】
1)将 data 和 setData 数据套用在 WXML 片段上,得到一个新节点树
2)将新节点树与当前节点树进行比较(diff)
3)将 setData 数据合并到 data 中,并用新节点树替换旧节点树
【性能优化点】
提升 diff 过程速度。
【具体方式】
1)去掉不必要设置的数据
2)减少 setData 的数据量
五、原生组件通信
一些原生组件支持使用 context 来更新组件。
与 setData 的不同:
数据从逻辑层传到 native 层后,直接传入组件中(不需要 native => 视图层,视图层再传入组件)
这种通信方式,可以显著降低传输延迟。
六、性能优化方案汇总
主要的优化策略可以归纳为三点:
1)精简代码,降低 WXML 结构和 JS 代码的复杂性
2)合理使用 setData 调用,减少 setData 次数和数据量
3)必要时使用分包优化。
第八部分:小程序基础库的更新迭代
一、什么是基础库
1、职责
包装提供组件、API,处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑。
2、载入时机
启动小程序后先载入基础库,接着再载入业务代码。
渲染层 WebView 层注入的称为 WebView 基础库,逻辑层注入的称为 AppService 基础库。
WebView 基础库 + AppService 基础库 == 小程序基础库。
由于所有小程序在微信客户端打开的时候,都需要注入相同的基础库 ,所以,小程序的基础库不会被打包在某个小程序的代码包里边,它会被提前内置在微信客户端。
这样做的好处:
1)降低业务小程序的代码包大小。
2)可以单独修复基础库中的 Bug,无需修改到业务小程序的代码包。
二、基础库的异常捕获
【可选方案】
1、try-catch方案。
2、window.onerror方案(window.addEventListener("error", function(evt){}))
【注】
1)对比 window.onerror 的方案,try-catch 的方案有个缺点:没法捕捉到全局的错误事件。
2)逻辑层不存在 window 对象,因此逻辑层 AppService 侧无法通过 window.onerror 来捕捉异常。
【最终方案】
1)在 WebView 侧使用 window.onerror 方案进行捕捉异常。
2)在逻辑层 AppService 侧通过把 App 实例和 Page 实例的各个生命周期等方法包裹在 try-catch 里进行捕捉异常。
3)在 App 构造器里提供了 onError 的回调,当业务代码运行产生异常时,这个回调被触发,同时能够拿到异常的具体信息。
第九部分:微信开发者工具
一、代码编译
微信开发者工具和微信客户端都无法直接运行小程序的源码,因此我们需要对小程序的源码进行编译。
代码编译过程:
1)本地预处理
2)本地编译
3)服务器编译
微信开发者工具模拟器运行的代码只经过本地预处理、本地编译,没有服务器编译过程。
微信客户端运行的代码是额外经过服务器编译的。
1、WXML 编译:WXML => JavaScript
微信开发者工具内置了一个二进制的 WXML 编译器,这个编译器接受 WXML 代码文件列表,处理完成之后输出 JavaScript 代码,这段代码是各个页面的结构生成函数。
编译过程将所有的 WXML 代码最终变成一个 JavaScript 函数,预先注入在 WebView 中。
(注意,是所有的 WXML,所有的,统一变成一个 JavaScript 函数,叫做:“页面结构生成函数”)
这个函数接收页面路径(pagePath)作为参数,返回“页面结构生成函数”,“页面结构生成函数”接受页面数据(pageData)作为参数,输出一段描述页面结构的 JSON。
最终,通过小程序组件系统生成对应的 HTML。
![](https://img.haomeiwen.com/i7566087/80d22e9ef29c5e4e.png)
页面结构生成函数的使用:
//$gwx 是 WXML 编译后得到的函数
//根据页面路径获取页面结构生成函数
var generateFun = $gwx('name.wxml')
//页面结构生成函数接受页面数据,得到描述页面结构的JSON
var virtualTree = generateFun({
name: 'miniprogram'
})
/** virtualTree == {
tag: 'view',
children: [{
tag: 'view',
children: ['miniprogram']
}]
}**/
//小程序组件系统在虚拟树对比后将结果渲染到页面上
virtualDom.render(virtualTree)
2、WXSS 编译:WXSS => 样式信息数组
微信开发者工具内置了一个二进制的 WXSS 编译器,这个编译器接受 WXSS 文件列表,分析文件之间的引用关系,同时预处理 rpx,输出一个样式信息数组。
![](https://img.haomeiwen.com/i7566087/a3450904c1b68bb0.png)
3、JavaScript 编译:多个 js => app-service.js
微信客户端在运行小程序的逻辑层的时候只需要加载一个 JS 文件(我们称为 app-service.js)。
也就是,我们小程序代码经过编译(es6 => es5)、打包之后,得到的 bundle.js。
【具体步骤】
1)代码上传之前的编译、压缩(预处理)
在代码上传之前,微信开发者工具会对开发者的 JS 文件做一些预处理,包括 ES6 转 ES5 和代码压缩(开发者可以选择关闭预处理操作),然后上传(上传的还是多个 JS 文件)
2)服务器编译、打包
将每个 JS 文件的内容分别包裹在 define 域中,再按一定的顺序合并成 app-service.js 。
同时,添加主动 require app.js 和页面 JS 的代码。
![](https://img.haomeiwen.com/i7566087/b88d73274af335af.png)
![](https://img.haomeiwen.com/i7566087/9ed7a3d2ce808a5a.png)
二、模拟器
1、 逻辑层模拟
逻辑层的真实运行环境:
iOS:JavaScriptCore 中
安卓:x5 的 JSCore 中
模拟运行环境:
微信开发者工具:采用一个隐藏着的 Webivew 来模拟小程序的逻辑运行环境
![](https://img.haomeiwen.com/i7566087/6bd92d652f7e815d.png)
![](https://img.haomeiwen.com/i7566087/97bcdf266efcd10d.png)
在微信开发者工具上 WebView 是一个chrome的 <webview /> 标签。与<iframe />标签不同的是,<webview/> 标签是采用独立的线程运行的。
WebView 在请求开发者 JS 代码时,开发者工具读取 JS 代码进行必要的预处理后,将处理结果返回。
然后,由 WebView 解析执行(这样,逻辑层的 JS 文件,就得到了执行)。
虽然开发者工具上是没有对 JS 代码进行合并的,但是还是按照相同的加载顺序进行解析执行。
【更好的模拟】
WebView 是一个浏览器环境,支持 BOM 操作。但逻辑层本不该支持这种操作,一旦出现,我们需要正确报错才对。
因此,开发者工具将开发者的代码包裹在 define 域的时候,将浏览器的 BOM 对象局部变量化,从而使得在开发阶段就能发现问题。
2、 渲染层模拟
微信开发者工具使用 chrome 的 <webview />标签来加载渲染层页面。
每个渲染层 WebView 加载:
http://127.0.0.1:9973/pageframe/pageframe.html
开发者工具底层搭建的 HTTP 本地服务器在收到这个请求的时候,就会编译 WXML 文件和 WXSS 文件,然后将编译结果作为 HTTP 请求的返回包。
当确定加载页面的路径之后,如 index 页面,开发工具会动态注入如下一段脚本:
// 改变当前 webview 的路径,确保之后的图片网络请求能得到正确的相对路径
history.pushState('', '', 'pageframe/index')
// 创建自定义事件,将页面结构生成函数派发出去,由小程序渲染层基础库处理
document.dispatchEvent(new CustomEvent("generateFuncReady", {
detail: {
generateFunc: $gwx('./index.wxml')
}
}))
// 注入对应页面的样式,这段函数由 WXSS 编译器生成
setCssToHead()
3、客户端模拟
在微信开发者工具上,通过借助 BOM(浏览器对象模型)以及 node.js 访问系统资源的能力,同时模拟客户端的 UI 和交互流程,使得大部分的 API 能够正常执行。
4、通讯模拟
我们需要一个有效的通讯方案使得小程序的逻辑层、渲染层和客户端之间进行数据交流,才能将这三个部分串联成为一个有机的整体。
【原理】
微信开发者工具的有一个消息中心底层模块维持着一个 WebSocket 服务器,小程序的逻辑层的 WebView 和渲染层页面的 WebView 通过 WebSocket 与开发者工具底层建立长连,使用 WebSocket 的 protocol 字段来区分 Socket 的来源。
三、调试器:界面调试 + 逻辑调试
nw.js 对 <webview/> 提供打开 Chrome Devtools 调试界面的接口,使得开发者工具具备对小程序的逻辑层和渲染层进行调试的能力。
同时,为了方便调试小程序,开发者工具在 Chrome Devtools 的基础上进行扩展和定制。
【界面调试】
微信小程序团队通过脚本注入的方式、将 Chrome Devtools 的 Element 面板隐藏,同时开发了 Chrome Devtools 插件 WXML 面板。
开发者工具会在每个渲染层的 WebView 中注入界面调试的脚本代码,负责获取 WebView 中的 DOM 树、获取节点样式、监听节点变化、高亮选中节点、处理界面调试命令。并将界面调试信息通过 WebSocket 经由开发者工具转发给 WXML 面板进行处理。
【逻辑调试】
直接使用 Chrome Devtools 的 Sources 面板调试逻辑层 JS 代码。
四、微信开发者工具原理总结
1、渲染层
通过编译过程我们将 WXML 文件和 WXSS 文件都处理成 JS 代码,使用 script 标签注入在一个空的 html文件中(我们称为:page-frame.html)
2、逻辑层
我们将所有的 JS 文件编译成一个单独的 app-service.js
3、小程序运行时
1)逻辑层使用 JsCore 直接加载 app-service.js,渲染层使用 WebView 加载 page-frame.html
2)在确定页面路径之后,通过动态注入 script 的方式调用 WXML 文件和 WXSS 文件生成的 JS 代码,再结合逻辑层的页面数据,最终渲染出指定的页面
4、开发者工具使用一个隐藏着的 <webview/> 标签来模拟 JSCore 作为小程序的逻辑层运行环境
5、开发者工具利用 BOM、node.js 以及模拟的 UI 和交互流程实现对大部分客户端 API 的支持
6、开发者工具底层有一个 HTTP 服务器来处理来自 WebView 的请求,并将开发者代码编译处理后的结果作为 HTTP 请求的返回,WebView 按照普通的网页进行渲染
7、开发者工具底层维护着一个 WebSocket 服务器,用于在 WebView 与开发者工具之间建立可靠的消息通讯链路
8、微信开发者工具使用 webview.showDevTools 打开 Chrome Devtools 调试逻辑层 WebView 的 JS 代码
9、微信小程序团队开发了 Chrome Devtools 插件 WXML 面板对渲染层页面 WebView 进行界面调试
网友评论