主要介绍微信开发者工具如何编译小程序代码,如何实现小程序模拟器以及如何调试小程序。
虽然在开发语言层面小程序与传统的网页差别不大:是使用JavaScript 脚本语言编写逻辑代码、使用类似于HTML的WXML来描述页面的结构、使用类似于CSS的WXSS来描述节点的样式,但是由于小程序渲染和逻辑分离的运行机制与传统的网页存在差异,所以无法使用传统的网页的开发调试工具,因此我们使用小程序开发生态一站式IDE——微信开发者工具。开发者可以借助微信开发者工具完成小程序的代码开发、编译运行、界面和逻辑调试、真机预览和提交发布版本等功能。
图1 微信开发者工具
微信开发者工具是一个基于nw.js ,使用node.js、chromium以及系统API来实现底层模块,使用React、Redux等前端技术框架来搭建用户交互层,实现同一套代码跨Mac和Windows 平台使用。
图2 微信开发者工具底层框架
微信开发者工具和微信客户端都无法直接运行小程序的源码,因此我们需要对小程序的源码进行编译。代码编译过程包括本地预处理、本地编译和服务器编译。为了快速预览,微信开发者工具模拟器运行的代码只经过本地预处理、本地编译,没有服务器编译过程,而微信客户端运行的代码是额外经过服务器编译的。
WXML(WeiXin Markup Language)是小程序框架设计的一套标签语言,用于构建出页面的结构。小程序的渲染层的运行环境是一个WebView,而WebView无法直接理解WXML标签,所以需要经过编译。
微信开发者工具内置了一个二进制的WXML编译器,这个编译器接受WXML代码文件列表,处理完成之后输出JavaScript代码,这段代码是各个页面的结构生成函数。
图3 WXML的编译过程
编译过程将所有的WXML代码最终变成一个JavaScript 函数,预先注入在WebView中。在运行时确定了页面路径之后,将路径作为参数传递给这个函数得到该页面的结构生成函数,页面结构生成函数接受页面数据,输出一段描述页面结构的JSON,最终通过小程序组件系统生成对应的HTML。
代码清单1 如何使用页面结构生成函数
//$gwx 是WXML编译后得到的函数
//根据页面路径获取页面结构生成函数
var generateFun = $gwx('name.wxml')
//页面结构生成函数接受页面数据,得到描述页面结构的JSON
var virtualTree = generateFun({
name: 'miniprogram'
})
/** virtualTree == {
tag: 'view',
children: [{
tag: 'view',
children: ['miniprogram']
}]
}**/
//小程序组件系统在虚拟树对比后将结果渲染到页面上
virtualDom.render(virtualTree)
上传代码时,微信开发者工具直接将本地的WXML代码文件提交到后台,由后台进行WXML编译,后台的WXML编译器和开发者工具本地内置的WXML编译器是同一套代码生成的。
WXSS (WeiXin Style Sheets) 是一套样式语言,用来决定 WXML 的组件应该怎么显示。为了适应广大的前端开发者,WXSS 具有 CSS 大部分特性。同时为了更适合开发微信小程序,WXSS 对 CSS 进行了扩充以及修改。与 CSS 相比,WXSS 扩展的一些特性,包括rpx尺寸单位和样式导入语法,这些特性都是WebView无法直接理解的。
微信开发者工具内置了一个二进制的WXSS编译器,这个编译器接受WXSS文件列表,分析文件之间的引用关系,同时预处理rpx,输出一个样式信息数组,如图4,每个WXSS文件对应于这个数组中的一项。
图4 WXSS的编译过程
在运行时,根据当前的屏幕宽度,计算出1rpx对应多少像素单位,然后将样式信息数组转换成最终的样式添加到页面中。
由于样式在微信客户端存在兼容性问题,为了方便开发者,微信开发者工具提供了上传代码时样式自动补全的功能,利用PostCSS 对WXSS文件进行预处理,自动添加样式前缀。
微信客户端在运行小程序的逻辑层的时候只需要加载一个JS文件(我们称为app-service.js),而小程序框架允许开发者将 JavaScript 代码写在不同的文件中,所以在代码上传之前,微信开发者工具会对开发者的JS 文件做一些预处理,包括ES6转ES5和代码压缩(开发者可以选择关闭预处理操作),在服务器编译过程将每个JS文件的内容分别包裹在define域中,再按一定的顺序合并成 app-service.js 。其中对于页面JS和app.js需要主动require。
图5 JavaScript的编译过程
小程序模拟器模拟小程序在微信客户端的逻辑和界面表现,方便开发者实时查看代码效果。由于系统差异以及微信客户端特有的一些交互流程,少部分的API无法在模拟器上进行模拟,但对于绝大部分的 API 均能够在模拟器上呈现出正确的状态。同时微信开发者工具提供多种机型尺寸以及自定义机型尺寸功能,方便开发者进行界面样式的机型适配。
图6 小程序模拟器
在iOS微信客户端上,小程序的JavaScript代码是运行在JavaScriptCore中,在Android微信客户端上,小程序的JavaScript代码是通过 X5 JSCore来解析的。而在微信开发者工具上我们采用了一个隐藏着的Webivew来模拟小程序的逻辑运行环境。
图7 微信客户端小程序运行环境模型简图
图8 微信开发者工具小程序运行环境模型简图
在微信开发者工具上WebView是一个chrome的
标签。与标签不同的是,
标签是采用独立的线程运行的。
用于模拟小程序逻辑层的
加载的链接是
http://127.0.0.1:9973/appservice/appservice
我们在开发者工具底层搭建了一个本地HTTP服务器来处理小程序模拟器的网络请求。其中:
./__asdebug/asdebug.js
: 是开发者工具注入的脚本。
./__dev__/WAService.js
:是小程序逻辑层基础库。
./util.js、./app.js、./index.js
:开发者JS代码。
WebView在请求开发者JS代码时,开发者工具读取JS代码进行必要的预处理后,将处理结果返回,然后由WebView解析执行。虽然开发者工具上是没有对JS代码进行合并的,但是还是按照相同的加载顺序进行解析执行。
图9 appservice内容
WebView是一个浏览器环境,而JsCore是一个单纯的脚本解析器,浏览器中的BOM对象无法在JSCore中使用,开发者工具做了一个很巧妙的工作,将开发者的代码包裹在define域的时候,将浏览器的BOM对象局部变量化,从而使得在开发阶段就能发现问题。
图10 BOM对象局部变量化
微信开发者工具使用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()
微信客户端为丰富小程序的功能提供了大量的API。在微信开发者工具上,通过借助BOM(浏览器对象模型)以及node.js访问系统资源的能力,同时模拟客户端的UI和交互流程,使得大部分的API能够正常执行。
借助BOM,如wx.request使用XMLHttpRequest 模拟、wx.connectSocket 使用 WebSocket、wx.startRecord 使用MediaRecorder、wx.playBackgroundAudio 使用 标签;
借助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的来源。
代码清单2 逻辑层中的消息模块
// 的userAgent是可定制的
// 通过userAgent中获取开发者工具WebSocket服务器监听的端口
var port = window.navigator.userAgent.match(/port\/(\d*)/)[1]
// 通过指定 protocol == 'APPSERVICE' 告知开发者工具这个链接是来自逻辑层
var ws = new WebSocket(`ws://127.0.0.1:${port}`, 'APPSERVICE')
ws.onmessage = (evt) => {
let msg = JSON.parse(evt.data)
// …处理来自开发者工具的信息
}
// 调用API接口 wx.navigateBack
ws.send(JSON.stringify({
command: 'APPSERVICE_INVOKE',
data: {
api: 'navigateBack',
args: {}
}
}))