小程序和h5的区别
1.架构层
| h5 | 小程序 |
|---|---|
| 渲染线程和脚本线程是互斥的 | 渲染线程和脚本(逻辑)线程是单独分开的两个线程 |
| 可以使用到各种浏览器暴露出来的 DOM API,进行 DOM 操作 | 逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的DOM API和BOM API,所有像jquery等一些库在小程序中是无法运行的,同 NodeJS 环境也是不尽相同,所以一些 NPM 的包在小程序中也是无法运行的; |
| 面对的环境是各式各样的浏览器 | 面对的是两大操作系统 iOS 和 Android 的微信客户端,以及用于辅助开发的小程序开发者工具 |
| 可将静态资源放在cdn上,用户访问html时去加载 | 静态资源统一托管在微信相关的服务上,都会被算作小程序的包体积 |
小程序运行环境:
微信还提供了WeUI.wxss基础样式库(button、cell、dialog、progress、toast、article、actionsheet、icon等各式原生)
2.ECMAScript集中环境对比
3.小程序开发记录
3.1、实例化App和getApp方法
Page({
data: {
time: (new Date()).toString(),
msg: 'wang'
},
onLoad: function () {
this.setData({ msg: 'hello world!' })
console.log('this', this)
console.log('getApp', getApp())
}
})
App({ // 实例化了App才能获取getApp方法
onLaunch: function(options) { console.log('onLaunch', options) },
onShow: function(options) { console.log('onShow', options) },
onLaunch: function(options) {},
onShow: function(options) {},
onHide: function() {},
onError: function(msg) {},
globalData: 'I am global data'
})
输出
实例化了App()才能获取getApp方法,app中能定义onLaunch,onShow,onHide,onError等hooks,和定义全局数据globalData。
3.2、页面布局和交互反馈
3.2.1、页面布局flex
3.2.2、交互反馈
1.<button hover-class="hover"> 点击button </button>(hover-class优化用户感知);
2.微信原生toast组件:
Page({
onLoad: function() {
wx.showToast({ // 显示Toast
title: '已发送',
icon: 'success',
duration: 1500
})
// wx.hideToast() // 隐藏Toast
}
})
3.原生模态对话框:
Page({
onLoad: function() {
wx.showModal({
title: '标题',
content: '告知当前状态,信息和解决方法',
confirmText: '主操作',
cancelText: '次要操作',
success: function(res) {
if (res.confirm) {
console.log('用户点击主操作')
} else if (res.cancel) {
console.log('用户点击次要操作')
}
}
})
}
})
............
3.2.3、关于网络通信
wx.request中url参数是当前发起请求的服务器接口地址,并且小程序宿主环境要求request发起的网络请求必须是HTTPS协议,同时wx.request请求的域名需要在小程序管理平台进行配置,否则控制台会有相应报错。但是开发阶段,开发者工具,小程序开发板和小程序体验版在某些情况下允许请求任意域名。
3.2.4、关于微信登录
1.获取微信登录凭证code
wx.login是生成一个带有时效性的凭证,就像是一个会过期的临时身份证一样,在wx.login调用时,会先在微信后台生成一张临时的身份证,其有效时间仅为5分钟,然后把这个临时身份证返回给小程序方,这个临时的身份证我们把它称为微信登录凭证code。如果5分钟内小程序的后台不拿着这个临时身份证来微信后台服务器换取微信用户id的话,那么这个身份证就会被作废,需要再调用wx.login重新生成登录凭证。由于这个临时身份证5分钟后会过期,如果黑客要冒充一个用户的话,那他就必须在5分钟内穷举所有的身份证id,然后去开发者服务器换取真实的用户身份。显然,黑客要付出非常大的成本才能获取到一个用户信息,同时,开发者服务器也可以通过一些技术手段检测到5分钟内频繁从某个ip发送过来的登录请求,从而拒绝掉这些请求。
2.发送code到开发者服务器
在wx.login的sccess回调中拿到微信登录凭证,然后会通过wx.request吧code传到开发者服务器,如果当前的微信用户还没绑定当前小程序业务的用户身份,在这次请求顺便把用户输入的账号密码一起传给后台,开发者服务器可以校验摩玛之后在和微信的用户id进行绑定。
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)
}
}
});
}
})
3.到微信服务器换取用户身份id
在这里,开发者后台拿到了wx.login生成的微信登录凭证code,拿到这个code到微信服务器换取微信用户身份,微信服务器为了确保拿code过来换取身份信息的人就是刚刚对应的小程序开发者,到微信服务器的请求要同时带上AppId和AppSecret(这两个信息在小程序管理平台的开发设置界面),AppId是公开信息,泄露AppId不会带来安全风险,但是AppSecret是开发者的隐私数据不应该泄露,如果发现泄露需要到小程序管理平台进行重置AppSecret,而code在成功换取一次信息之后也会立即失效,即便凭证code生成时间还没过期。
api.weixin.qq.com/sns/jscode2…接口返回的字段有openid(微信用户的唯一标识),session_key(会话密钥),unionid(可能有,用户在微信开放平台的唯一标识符);
session_key则是微信服务器给开发者服务器颁发的身份凭证,开发者可以用session_key请求微信服务器其他接口来获取一些其他信息,由此可以看到,session_key不应该泄露或者下发到小程序前端。(时效性更长的会话密钥)
4.绑定微信用户身份id和业务用户身份
业务侧用户还没绑定微信侧身份时,会让用户填写业务侧的用户名密码,这两个值会和微信登录凭证一起请求开发者服务器的登录接口,此时开发者后台通过校验用户名密码就拿到了业务侧的用户身份id,通过code到微信服务器获取微信侧的用户身份openid。微信会建议开发者把这两个信息的对应关系存起来,我们把这个对应关系称之为“绑定”。
有了这个绑定信息,小程序在下次需要用户登录的时候就可以不需要输入账号密码,因为通过wx.login()获取到code之后,可以拿到用户的微信身份openid,通过绑定信息就可以查出业务侧的用户身份id,这样静默授权的登录方式显得非常便捷。
5.业务登录凭证SessionId
微信侧返回的session_key是开发者服务器和微信服务器的会话密钥,同样道理,开发者服务器和开发者的小程序应该也有会话密钥,在本书中我们就把它称之为SessionId。用户登录成功之后,开发者服务器需要生成会话密钥SessionId,在服务端保持SessionId对应的用户身份信息,同时把SessionId返回给小程序。小程序后续发起的请求中携带上SessionId,开发者服务器就可以通过服务器端的Session信息查询到当前登录用户的身份,这样我们就不需要每次都重新获取code,省去了很多通信消耗。还可以利用本地数据缓存的能力把SessionId存储起来,以便在它还没过期的时候能重复利用,以提高通信的性能。
3.2.5、关于微信缓存
1.本地缓存
指的是小程序存储在当前设备硬盘上的数据,通过wx.getStorage/wx.getStorageSync读取本地缓存,通过wx.setStorage/wx.setStorageSync写数据到缓存,其中Sync后缀的接口表示是同步接口,执行完毕之后会立马返回。
小程序的缓存上限是10MB,超过之后再通过wx.setStorage写入缓存会触发fail回调;小程序的本地缓存不仅仅通过小程序这个维度来隔离空间,考虑到同一个设备可以登录不同微信用户,宿主环境还对不同用户的缓存进行了隔离,避免用户间的数据隐私泄露;用户的关键信息不建议只存在本地缓存,应该把数据放到服务器端进行持久化存储。
缓存sessionId保持用户登录态;
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发生错误')
}
3.3、小程序场景区分
小程序从群聊会话里打开,从小程序列表中打开,通过微信扫一扫二维码打开,从另外一个小程序打开当前小程序等,针对不同途径的打开方式有时需要做不同的业务处理,所以微信客户端会把打开方式带给onLaunch和onShow的调用参数options,场景文档。
3.4、要点记录
1.小程序的每个页面各有一个webview线程进行渲染,所以小程序切换页面是小程序的逻辑层的js脚本运行上下文依然在同一个jsCore线程中;(需要特别留意使用setTimeout或者setInterval定时器,然后跳转到其他页面是这些定时器并没有被清除,需要开发者在离开页面时自己清理);
2.页面构造器
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``() { }``})
page()是页面构造器,里面包含了页面的生命周期钩子。
应用的生命周期函数:
onLaunch: 小程序初始化完成时执行,全局只触发一次。可以在此进行一些初始化的工作。onShow: 小程序启动,或从后台进入前台显示时触发。onHide: 小程序从前台进入后台时触发。
App({ onLaunch: function(options) { }, onShow : function(options) { }, onHide : function() { } }) 页面的生命周期函数:
onLoad: 监听页面加载,一个页面只会调用一次。onReady: 监听页面初次渲染完成,一个页面只会调用一次。onShow: 监听页面显示,每次打开页面都会调用一次。onHide: 监听页面隐藏,当navigateTo或底部tab切换时调用。onUnload: 监听页面卸载,当redirectTo或navigateBack的时候调用。onPullDownRefresh: 监听用户下拉动作,需要在app.json的window选项中或页面配置中开启enablePullDownRefresh。onReachBottom: 监听页面上拉触底事件,可以在app.json的window选项中设置触发距离onReachBottomDistance。onShareAppMessage: 用户点击右上角分享[5]。
Page({ onLoad: function (options) { }, onReady: function () { }, onShow: function () { }, onHide: function () { }, onUnload: function () { }, onPullDownRefresh: function () { }, onReachBottom: function () { }, onShareAppMessage: function () { } })
组件也有自己的生命周期函数,包括created、attached、ready、moved和detached等。
在实际应用中,如果要改变onLoad的执行时机,需要在执行完生命周期函数后,再把它们恢复到config下。这样可以保证onShow等生命周期函数的后续正常运行。
3.关于setData:
宿主环境所提供的Page实例的原型中有setData函数,我们可以在Page实例下的方法调用this.setData把数据传递给渲染层,从而达到更新界面的目的。由于小程序的渲染层和逻辑层分别在两个线程中运行,所以setData传递数据实际是一个异步的过程,所以setData的第二个参数是一个callback回调,在这次setData对界面渲染完毕后触发。 setData其一般调用格式是 setData(data, callback),其中data是由多个key: value构成的Object对象。
4.微信组件说明;
5.微信API相关(网络、媒体、文件、数据缓存、位置、设备、界面、界面节点信息还有一些特殊的开放接口):
- wx.on* 开头的 API 是监听某个事件发生的API接口,接受一个 Callback 函数作为参数。当该事件触发时,会调用 Callback 函数。
- 如未特殊约定,多数 API 接口为异步接口 ,都接受一个Object作为参数。
- API的Object参数一般由success、fail、complete三个回调来接收接口调用结果。
- wx.get* 开头的API是获取宿主环境数据的接口。
- wx.set* 开头的API是写入数据到宿主环境的接口。
6.组件相关的事件:
Page({`` ``handleTap: ``function``(evt) {`` ``// 当点击inner节点时`` ``// evt.target 是inner view组件`` ``// evt.currentTarget 是绑定了handleTap的outer view组件`` ``// evt.type == “tap”`` ``// evt.timeStamp == 1542`` ``// evt.detail == {x: 270, y: 63}`` ``// evt.touches == [{identifier: 0, pageX: 270, pageY: 63, clientX: 270, clientY: 63}]`` ``// evt.changedTouches == [{identifier: 0, pageX: 270, pageY: 63, clientX: 270, clientY: 63}]`` ``}``})
3.5、小程序的协同工作和发布
3.5.1、小程序的版本
| 开发版本 | 使用开发者工具,可将代码上传到开发版本中。 开发版本只保留每人最新的一份上传的代码。点击提交审核,可将代码提交审核。开发版本可删除,不影响线上版本和审核中版本的代码。 |
|---|---|
| 体验版本 | 可以选择某个开发版本作为体验版,并且选取一份体验版。 |
| 审核中版本 | 只能有一份代码处于审核中。有审核结果后可以发布到线上,也可直接重新提交审核,覆盖原审核版本。 |
| 线上版本 | 线上所有用户使用的代码版本,该版本代码在新版本代码发布后被覆盖更新。 |
每个开发者都拥有自己的一个开发版本,为了能有稳定的版本体验测试允许将其中的一个开发版本设置为体验版本。
3.5.2、小程序的发布
注意事项:
- 如果小程序使用到Flex布局,并且需要兼容iOS8以下系统时,请检查上传小程序包时,开发者工具是否已经开启“上传代码时样式自动补全”。
- 小程序使用的服务器接口应该走HTTPS协议,并且对应的网络域名确保已经在小程序管理平台配置好。
- 在测试阶段不要打开小程序的调试模式进行测试,因为在调试模式下,微信不会校验域名合法性,容易导致开发者误以为测试通过,导致正式版小程序因为遇到非法域名无法正常工作。
- 发布前请检查小程序使用到的网络接口已经在现网部署好,并且评估好服务器的机器负载情况。
发布模式:
全量发布和灰度发布;
3.6、底层框架
3.6.1、渲染技术选型
渲染界面的技术有三种:
1.用纯客户端技术来渲染;
2.纯web技术来渲染;
3.介于客户端和web技术之间的(hybird)渲染;
微信使用的是JSSDK的Hybird技术,界面由成熟的web技术渲染并辅之以大量的接口提供丰富的客户端原生能力,小程序页面用不同的webView去渲染,界面渲染定义了一套内置组件统一体验并且提供一些基础和通用的能力(内置的一些复杂组件是用客户端原生渲染的)。
3.6.2、技术安全保证
JavaScript 的灵活性以及浏览器接口的丰富性,很容易遗漏一些危险的接口,而且就算被我们找到所有危险的接口,也许在下一次浏览器内核更新而新增了一个可能会在这套体系下产生漏洞的接口,这样还是无法完全避免。
要彻底解决这个问题,我们必须提供一个沙箱环境来运行开发者的JavaScript 代码。这个沙箱环境不能有任何浏览器相关接口,只提供纯JavaScript 的解释执行环境,那么像HTML5中的ServiceWorker、WebWorker特性就符合这样的条件,这两者都是启用另一线程来执行 JavaScript。但是考虑到小程序是一个多 WebView 的架构,每一个小程序页面都是不同的WebView 渲染后显示的,在这个架构下我们不好去用某个WebView中的ServiceWorker去管理所有的小程序页面。
得益于客户端系统有JavaScript 的解释引擎(在iOS下是用内置的 JavaScriptCore框架,在安卓则是用腾讯x5内核提供的JsCore环境),可以创建一个单独的线程去执行 JavaScript,在这个环境下执行的都是有关小程序业务逻辑的代码,也就是我们前面一直提到的逻辑层。而界面渲染相关的任务全都在WebView线程里执行,通过逻辑层代码去控制渲染哪些界面,那么这一层当然就是所谓的渲染层。这就是小程序双线程模型的由来。
3.6.3、小程序带来的延时
双线程模型意味着任何数据传递都是线程间的通信,都是异步的带有一定的延时性。
在渲染首屏的时候,逻辑层与渲染层会同时开始初始化工作,但是渲染层需要有逻辑层的数据才能把界面渲染出来,如果渲染层初始化工作较快完成,就要等逻辑层的指令才能进行下一步工作。因此逻辑层与渲染层需要有一定的机制保证时序正确,这些工作在小程序框架里会处理好,开发者只需要理解生命周期,以及控制合适的时机更新UI即可。
除了逻辑层与渲染层之间的通信有延时,各层与客户端原生交互同样是有延时的。以逻辑层为例,开发者的代码是跑在逻辑层这个线程之上,而客户端原生是跑在微信主线程(安卓上是线程)之上,所以注册给逻辑层有关客户端能力的接口,实际上也是跟微信主线程之间的通信,同样意味着有延时。这也是我们看到大部分提供的接口都是异步的原因。
小程序的组件系统:
小程序的视图是在webview中渲染的,搭建视图需要HTML语言,但是因为安全性不能直接提供html能力(因为可以通过a标签或者动态执行js还有很多其他的hack方式入侵),所以小程序设计了一套组件框架——Exparser,并且内置了一套组件。
Exparser的组件模型与WebComponents标准中的ShadowDOM高度相似。Exparser会维护整个页面的节点树相关信息,包括节点的属性、事件绑定等,相当于一个简化版的Shadow DOM实现。Exparser的主要特点包括以下几点:
- 基于Shadow DOM模型:模型上与WebComponents的ShadowDOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
- 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
- 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
小程序中,所有节点树相关的操作都依赖于Exparser,包括WXML到页面最终节点树的构建、createSelectorQuery调用和自定义组件特性等。
组件的节点树称为“ShadowTree”,即组件内部的实现;最终拼接成的页面节点树被称为“Composed Tree”,即将页面所有组件节点树合成之后的树。
在初始化页面时,Exparser会创建出页面根组件的一个实例,用到的其他组件也会响应创建组件实例(这是一个递归的过程)。组件创建的过程大致有以下几个要点:
- 根据组件注册信息,从组件原型上创建出组件节点的JS对象,即组件的this;
- 将组件注册信息中的data 复制一份,作为组件数据,即this.data;
- 将这份数据结合组件WXML,据此创建出Shadow Tree,由于Shadow Tree中可能引用有其他组件,因而这会递归触发其他组件创建过程;
- 将ShadowTree拼接到Composed Tree上,并生成一些缓存数据用于优化组件更新性能;
- 触发组件的created生命周期函数;
- 如果不是页面根组件,需要根据组件节点上的属性定义,来设置组件的属性值;
- 当组件实例被展示在页面上时,触发组件的attached 生命周期函数,如果Shadw Tree中有其他组件,也逐个触发它们的生命周期函数。
原生组件:
内置组件中有一些组件不完全在Exparser的渲染体系下,而是完全由客户端院上参与组件的渲染,这类组件称之为原生组件。
在原生组件内部,其节点树非常简单,基本上可以认为只有一个div元素。上面这行代码在渲染层开始运行时,会经历以下几个步聚:
- 组件被创建,包括组件属性会依次赋值。
- 组件被插入到DOM树里,浏览器内核会立即计算布局,此时我们可以读取出组件相对页面的位置(x, y坐标)、宽高。
- 组件通知客户端,客户端在相同的位置上,根据宽高插入一块原生区域,之后客户端就在这块区域渲染界面
- 当位置或宽高发生变化时,组件会通知客户端做相应的调整
原生组件在WebView这一层的渲染任务是很简单,只需要渲染一个占位元素,之后客户端在这块占位元素之上叠了一层原生界面。因此,原生组件的层级会比所有在WebView层渲染的普通组件要高。(如果实在要覆盖原生组件的话可以使用cover-view和cover-image组件,这两个也是原生组件,可以按照层级覆盖)
3.6.4、小程序的性能优化
代码包的加载: 微信会在小程序启动前为小程序准备好通用的运行环境。这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。这样可以显著减少小程序的启动时间。 小程序的代码包被下载(或从缓存中读取)完成后,小程序的代码会被加载到适当的线程中执行。此时,所有app.js、页面所在的JS文件和所有其他被require的JS文件会被自动执行一次,小程序基础库会完成所有页面的注册。在小程序代码调用Page构造器的时候,小程序基础库会记录页面的基础信息,如初始数据(data)、方法等。需要注意的是,如果一个页面被多次创建,并不会使得这个页面所在的JS文件被执行多次,而仅仅是根据初始数据多生成了一个页面实例(this),在页面JS文件中直接定义的变量,在所有这个页面的实例间是共享的。 例如,若从页面A使用wx.navigateTo跳转到页面B,再使用wx.navigateTo跳转到页面A,此时页面栈中有三个页面:A、B、A。这时两个A页面的实例将共享它的JS文件中Page构造器以外直接定义的变量。有经验的开发者可以利用这个特性,但一些开发者也会错误地共享出一些变量,因而使用时要小心。
小程序的页面层级:
小程序启动时仅有一个页面层级,每次调用wx.navigateTo,都会创建一个新的页面层级;相对地,wx.navigateBack会销毁一个页面层级。
页面层级的准备工作分为三个阶段。第一阶段是启动一个WebView,在iOS和Android系统上,操作系统启动WebView都需要一小段时间。第二阶段是在WebView中初始化基础库,此时还会进行一些基础库内部优化,以提升页面渲染性能。第三阶段是注入小程序WXML结构和WXSS样式,使小程序能在接收到页面初始数据之后马上开始渲染页面(这一阶段无法在小程序启动前执行)。
数据通信:
逻辑层向视图层发送页面数据(data和setData的内容),视图层向逻辑层反馈用户事件。
页面的初始化事件由数据的通信时间和页面的初始化渲染时间构成。(减少传输数据量是降低数据传输时间的有效方式)
更新数据:
初始渲染完毕后,视图层可以在开发者调用setData后执行界面更新。在数据传输时,逻辑层会执行一次JSON.stringify来去除掉setData数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将setData所设置的数据字段与data合并,使开发者可以用this.data读取到变更后的数据。
注意事项:
1.不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用; 2.数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用setData来设置这些数据; 3.与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下。
视图层会接受用户事件,如点击事件、触摸事件等。用户事件的通信比较简单:当一个用户事件被触发且有相关的事件监听器需要被触发时,视图层会将信息反馈给逻辑层。如果一个事件没有绑定事件回调函数,则这个事件不会被反馈给逻辑层。
注意事项:
1.去掉不必要的事件绑定(WXML中的bind和catch),从而减少通信的数据量和次数; 2.事件绑定时需要传输target和currentTarget的dataset,因而不要在节点的data前缀属性中放置过大的数据。
视图渲染:
在这整个流程中,时间开销大体上与节点树中节点的总量成正比例关系。因而减少WXML中节点的数量可以有效降低初始渲染和重渲染的时间开销,提升渲染性能。
每次应用setData数据时,都会执行重渲染来更新界面。初始渲染中得到的data和当前节点树会保留下来用于重渲染。每次重渲染时,将data和setData数据套用在WXML片段上,得到一个新节点树。然后将新节点树与当前节点树进行比较,这样可以得到哪些节点的哪些属性需要更新、哪些节点需要添加或移除。最后,将setData数据合并到data中,并用新节点树替换旧节点树,用于下一次重渲染。
原生组件更新并不会涉及到重渲染过程,数据通信过程也不同,原生组件支持使用context来更新组件,在setData的数据通信流程中,数据从逻辑层经过native层转发,传入视图层的WebView,再经过一系列渲染步骤之后传入组件。而使用context时,数据从逻辑层传到native层后,直接传入组件中,这样可以显著降低传输延迟。
3.6.5、小程序的基础库
为了让小程序业务代码能够调用wx.navigateTo等API以及组件,就需要在启动小程序后先载入基础库,接着再载入业务代码。由于小程序的渲染层和逻辑层是两个线程管理,渲染层WebView层注入的称为WebView基础库,逻辑层注入的称为AppService基础库。
so所有小程打开的时候都需要注入相同的基础库,so基础库会被内置在微信客户端,这样可以降低业务代码包的大小,也方便单独修复基础库中的bug而无需修改业务小程序的代码包。
3.6.6、微信开发者工具
由于小程序渲染和逻辑分离的运行机制与传统的网页存在差异,所以无法使用传统的网页的开发调试工具。
代码编译:
编译WXML:WXML是小程序框架设计一套标签语言,微信开发者工具内置了一个二进制的WXML的编译器,处理WXML之后输出js代码,预先注入在webview中,在运行时确认了页面的路径之后,将其作为参数传递给函数得到该页面的结构生成函数,该函数接受页面数据,然后输出一段描述页面结构的的JSON,最后通过小程序组件系统生成对应的HTML。
编译WXSS:WXSS 对 CSS 进行了扩充以及修改。与 CSS 相比,WXSS 扩展的一些特性,包括rpx尺寸单位和样式导入语法,这些特性都是WebView无法直接理解的。微信开发者工具内置了一个二进制的WXSS编译器,这个编译器接受WXSS文件列表,分析文件之间的引用关系,同时预处理rpx,输出一个样式信息数组,在运行时,根据当前的屏幕宽度,计算出1rpx对应多少像素单位,然后将样式信息数组转换成最终的样式添加到页面中。 由于样式在微信客户端存在兼容性问题,为了方便开发者,微信开发者工具提供了上传代码时样式自动补全的功能,利用PostCSS 对WXSS文件进行预处理,自动添加样式前缀。
编译js:
微信客户端在运行小程序的逻辑层的时候只需要加载一个JS文件(我们称为app-service.js),而小程序框架允许开发者将 JavaScript 代码写在不同的文件中,所以在代码上传之前,微信开发者工具会对开发者的JS 文件做一些预处理,包括ES6转ES5和代码压缩(开发者可以选择关闭预处理操作),在服务器编译过程将每个JS文件的内容分别包裹在define域中,再按一定的顺序合并成 app-service.js 。其中对于页面JS和app.js需要主动require。(ES6转ES5 => 压缩混淆 => 代码合并)
模拟器:
逻辑层模拟:
WebView在请求开发者JS代码时,开发者工具读取JS代码进行必要的预处理后,将处理结果返回,然后由WebView解析执行。虽然开发者工具上是没有对JS代码进行合并的,但是还是按照相同的加载顺序进行解析执行。
WebView是一个浏览器环境,而JsCore是一个单纯的脚本解析器,浏览器中的BOM对象无法在JSCore中使用,开发者工具做了一个很巧妙的工作,将开发者的代码包裹在define域的时候,将浏览器的BOM对象局部变量化,从而使得在开发阶段就能发现问题。
渲染层模拟:
微信开发者工具使用chrome的 <webview />标签来加载渲染层页面,每个渲染层WebView加载。
客户端模拟:
微信客户端为丰富小程序的功能提供了大量的API。在微信开发者工具上,通过借助BOM(浏览器对象模型)以及node.js访问系统资源的能力,同时模拟客户端的UI和交互流程,使得大部分的API能够正常执行。 借助BOM,如wx.request使用XMLHttpRequest 模拟、wx.connectSocket 使用 WebSocket、wx.startRecord 使用MediaRecorder、wx.playBackgroundAudio 使用 <audio/>标签; 借助node.js,如使用fs实现wx.saveFile、wx.setStorage、wx.chooseImage等API功能。 借助模拟UI和交互流程,实现wx.navigateTo、wx.showToast、wx.openSetting、wx.addCard等。
通讯模拟:
微信开发者工具的有一个消息中心底层模块维持着一个WebSocket服务器,小程序的逻辑层的WebView和渲染层页面的WebView通过WebSocket与开发者工具底层建立长连,使用WebSocket的protocol字段来区分Socket的来源。
总结: 通过编译过程我们将WXML文件和WXSS文件都处理成JS代码,使用script标签注入在一个空的html文件中(我们称为:page-frame.html);我们将所有的JS文件编译成一个单独的app-service.js。 在小程序运行时,逻辑层使用JsCore直接加载app-service.js,渲染层使用WebView加载page-frame.html,在确定页面路径之后,通过动态注入script的方式调用WXML文件和WXSS文件生成的对应页面的JS代码,再结合逻辑层的页面数据,最终渲染出指定的页面。 开发者工具使用一个隐藏着的<webview/>标签来模拟JSCore作为小程序的逻辑层运行环境,通过将JSCore中不支持的BOM对象局部变量化,使得开发者无法在小程序代码中正常使用BOM,从而避免不必要的错误。 在开发者工具底层有一个HTTP服务器来处理来自WebView的请求,并将开发者代码编译处理后的结果作为HTTP请求的返回,WebView按照普通的网页进行渲染。 开发者工具利用BOM、node.js以及模拟的UI和交互流程实现对大部分客户端API的支持。 同时开发者工具底层维护着一个WebSocket服务器,用于在WebView与开发者工具之间建立可靠的消息通讯链路,使得接口调用,事件通知,数据交换能够正常进行,从而使小程序模拟器成为一个统一的整体。 微信开发者工具使用webview.showDevTools 打开Chrome Devtools调试逻辑层WebView的JS代码,同时开发了Chrome Devtools插件 WXML 面板对渲染层页面WebView进行界面调试。WXML面板对渲染层WebView中真实的DOM树做了一个最小树算法的裁剪之后,呈现出与WXML源码一致的节点树列表。