【导读】
本文是对官方【小程序开发指南】的精简 + 解读,帮助你更好地理解小程序。理解小程序为什么出现,理解小程序的底层原理等等。
【注】
本文仅做笔记式记录,如有侵权,烦请告知。
第一部分:初识小程序
一、微信为什么要推出小程序
最初,在微信中展现 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、安卓、小程序开发者工具
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 属性,作为模板的名字。然后在 内定义代码片段。
定义模板:
{{index}}: {{msg}}
Time: {{time}}
使用模板:
动态使用模板:
odd
even
is="{{item % 2 == 0 ? 'even' : 'odd'}}"/>
6、引用:
WXML 提供两种文件引用方式 import 和 include。
(1)import:用于引入 template
在 index.wxml 中 import 其他 template 文件,就可以在 index.wxml 中使用相应 template。
eg:
模板定义:
{{text}}
模板引用与使用:
【注】
import 有作用域的概念。
即:C 引用 B,B 引用A,在C中可以使用B定义的 template,在B中可以使用A定义的 template ,但是C不能使用A定义的template 。
(2)include:用于引入任意整块代码
include 可以将目标文件中 除了
header
footer
body
7、所有 wxml 标签都支持的属性:
三、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 是不相同的。
【注】
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、通信模型
渲染层:使用 webview 线程渲染界面。一个小程序存在多个页面,所以渲染层存在多个 webview 线程。
逻辑层:使用 JsCore 线程运行 JS 脚本。
webview 线程和 JsCore 线程通信,通过 Native,也就是微信客户端。
2、数据驱动
数据变更,视图自动更新。即:数据驱动视图。
数据更新,上图 JS 对象对应的节点就会发生变化。
对比前后两个JS对象得到变化的部分,然后把这个差异应用到原来的Dom树上,从而达到更新UI的目的,这就是“数据驱动”的原理。
【个人解读】
所谓数据驱动,即不关心视图,仅操纵数据。
逻辑层的任务,仅仅是执行用户的 setData 操作,然后把更新后的数据传给渲染层而已。
渲染层拿到最新的数据后,会根据当前的 wxml 结构以及最新数据、生成新的 JS 对象,并与当前的 JS 对象进行对比,得到改动的地方,再把差异更新到原来的 Dom 树(形如:document.getElementById() 获取到需要变更的真实 Dom 节点,然后更改它的属性值 or 子节点等)。
二、程序与页面
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。
三、事件
事件捕获和事件冒泡触发时序:
eg:点击 inner view,调用顺序:handleTap2、handleTap4、handleTap3、handleTap1。
id="outer"
bind:touchstart="handleTap1"
capture-bind:touchstart="handleTap2"
>
outer view
id="inner"
bind:touchstart="handleTap3"
capture-bind:touchstart="handleTap4"
>
inner view
bind* 和 catch* 的区别:
bind 事件绑定不会阻止冒泡事件向上冒泡,catch 事件绑定可以阻止冒泡事件向上冒泡。
capture-catch 将中断捕获阶段和取消冒泡阶段。
eg:点击 inner view,只触发 handleTap2。
id="outer"
bind:touchstart="handleTap1"
capture-catch:touchstart="handleTap2"
>
outer view
id="inner"
bind:touchstart="handleTap3"
capture-bind:touchstart="handleTap4"
>
inner 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;
}
(2)button 的 loading
(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:
页面 WXML:
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、主流程
在小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。
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 样式,使小程序能在接收到页面初始数据之后马上开始渲染页面(这一阶段无法在小程序启动前执行)
【注】
小程序未加载时,微信客户端根本没拿到小程序代码,所以无法进行注入。
三、数据通信
数据通信性能提升原则:
(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。
页面结构生成函数的使用:
//$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,输出一个样式信息数组。
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 的代码。
二、模拟器
1、 逻辑层模拟
逻辑层的真实运行环境:
iOS:JavaScriptCore 中
安卓:x5 的 JSCore 中
模拟运行环境:
微信开发者工具:采用一个隐藏着的 Webivew 来模拟小程序的逻辑运行环境
在微信开发者工具上 WebView 是一个chrome的
WebView 在请求开发者 JS 代码时,开发者工具读取 JS 代码进行必要的预处理后,将处理结果返回。
然后,由 WebView 解析执行(这样,逻辑层的 JS 文件,就得到了执行)。
虽然开发者工具上是没有对 JS 代码进行合并的,但是还是按照相同的加载顺序进行解析执行。
【更好的模拟】
WebView 是一个浏览器环境,支持 BOM 操作。但逻辑层本不该支持这种操作,一旦出现,我们需要正确报错才对。
因此,开发者工具将开发者的代码包裹在 define 域的时候,将浏览器的 BOM 对象局部变量化,从而使得在开发阶段就能发现问题。
2、 渲染层模拟
微信开发者工具使用 chrome 的
每个渲染层 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 对
同时,为了方便调试小程序,开发者工具在 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、开发者工具使用一个隐藏着的
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 进行界面调试