一、小程序基础知识
小程序是基于 WEB 规范,采用 HTML、CSS 和 JS 等搭建的一套框架,微信官方给它们取的名字:WXML、WXSS,但本质上还是在整个 WEB 体系之下构建的。WXML 说到底就是 xml 的一个子集。WXML 采用微信自定义的少量标签 WXSS,大家可以理解为就是自定义的 CSS。实现逻辑部分的 JS 还是通用的 ES 规范,并且 runtime 还是 Webview(IOS WKWEBVIEW、ANDROID X5)
1、小程序的组成结构
一个完整的小程序主要由以下几部分组成:
一个入口文件:app.js
一个全局样式:app.wxss
一个全局配置:app.json
页面:pages 下,每个页面再按文件夹划分,每个页面 4 个文件
(1)视图层:wxml,wxss
(2)逻辑层:js,json(页面配置,不是必须)
注:pages 里面还可以再根据模块划分子目录,孙子目录,只需要在 app.json 里注册时填写路径就行。
2、小程序项目打包:
编辑器它本身也是基于 WEB 技术体系实现的,nwjs+react,nwjs 简单是说就是 node+webkit,node 提供给我们本地 api 能力,而 webkit 提供给我们 web 能力,两者结合就能让我们使用 JS+HTML 实现本地应用程序。既然有 nodejs,那上面的打包选项里的功能就好实现了。
(1)ES6 转 ES5:引入 babel-core 的 node 包
(2)CSS 补全:引入 postcss 和 autoprefixer 的 node 包(postcss 和 autoprefixer 的原理看这里)
(3)代码压缩:引入 uglifyjs 的 node 包
打包后目录结构:
所有的小程序基本都最后都被打成上面的结构:
(1)WAService.js 框架 JS 库,提供逻辑层基础的 API 能力
(2)WAWebview.js 框架 JS 库,提供视图层基础的 API 能力
(3)WAConsole.js 框架 JS 库,控制台
(4)app-config.js 小程序完整的配置,包含我们通过 app.json 里的所有配置,综合了默认配置型
(5)app-service.js 我们自己的 JS 代码,全部打包到这个文件
(6)page-frame.html 小程序视图的模板文件,所有的页面都使用此加载渲染,且所有的 WXML 都拆解为 JS 实现打包到这里
(7)pages 所有的页面,这个不是我们之前的 wxml 文件了,主要是处理 WXSS 转换,使用 js 插入到 header 区域。
3、与 H5 页面的区别
小程序和普通的 h5
页面到底有什么区别呢?
(1)运行环境:小程序基于浏览器内核重构的内置解析器,而 h5
的宿主环境是浏览器。所以小程序中没有 DOM
和 BOM
的相关 API
, jQuery
和一些 NPM
包都不能在小程序中使用;
普通网页开发可以使用各种浏览器提供的 DOM API,进行 DOM 操作,而小程序的逻辑层和渲染层是分开的,逻辑层运行在 JSCore 中,并没有一个完整浏览器对象,因而缺少相关的 DOM API 和 BOM API。
(2)系统权限:小程序能获得更多的系统权限,如网络通信状态、数据缓存能力等;
(3)渲染机制:小程序的逻辑层和渲染层是分开的,而 h5
页面 UI
渲染跟 JavaScript
的脚本执行都在一个单线程中,互斥。所以 h5
页面中长时间的脚本运行可能会导致页面失去响应。
普通网页开发渲染线程和脚本线程是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应,而在小程序中,二者是分开的,分别运行在不同的线程中。
此外,小程序面对的是 iOS
和 Android
微信客户端和辅助开发的小程序开发者工具。根据官方文档,这三大运行环境也是有所区别的:
所以微信小程序介于 web
端和原生 App
之间,能够丰富调用功能接口,同时又跨平台。
#二、小程序架构
1、双线程模型
微信小程序的框架包含两部分:View 视图层、App Service 逻辑层。View 层用来渲染页面结构,App Service 层用来逻辑处理、数据请求、接口调用,它们在两个进程(两个 Webview)里运行。
视图层和逻辑层通过系统层的 JSBridage 进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
小程序的渲染层和逻辑层分别由 2 个线程管理:
(1)视图层:界面渲染相关的任务全都在 WebView
线程里执行。一个小程序存在多个界面,所以渲染层存在多个 WebView
线程。
(2)逻辑层:采用 JsCore
线程运行 JS 脚本。
视图层和逻辑层通过系统层的 WeixinJsBridage
进行通信:逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
2、渲染流程
把开发者的 JS 逻辑代码放到单独的线程去运行,但在 Webview 线程里,开发者就没法直接操作 DOM。
那要怎么去实现动态更改界面呢?
如上图所示,逻辑层和试图层的通信会由 Native (微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。
这也就是说,我们可以把 DOM 的更新通过简单的数据通信来实现。
Virtual DOM 相信大家都已有了解,大概是这么个过程:用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上。
页面渲染的具体流程是:在渲染层,宿主环境会把 WXML
转化成对应的 JS
对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的 setData
方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的 Dom 树上,渲染出正确的 UI 界面。
(1)在渲染层把 WXML 转化成对应的 JS 对象。
(2)在逻辑层发生数据变更的时候,通过宿主环境提供的 setData 方法把数据从逻辑层传递到 Native,再转发到渲染层。
(3)经过对比前后差异,把差异应用在原来的 DOM 树上,更新界面。
我们通过把 WXML 转化为数据,通过 Native 进行转发,来实现逻辑层和渲染层的交互和通信。
3、双线程模型设计的好处
双线程模型是小程序框架与业界大多数前端 Web
框架不同之处。基于这个模型,可以更好地管控以及提供更安全的环境。缺点是带来了无处不在的异步问题(任何数据传递都是线程间的通信,也就是都会有一定的延时),不过小程序在框架层面已经封装好了异步带来的时序问题。
为什么要这样设计呢,前面也提到了管控和安全,为了解决这些问题,我们需要阻止开发者使用一些,例如浏览器的 window 对象,跳转页面、操作 DOM、动态执行脚本的开放性接口。
我们可以使用客户端系统的 JavaScript 引擎(iOS 下的 JavaScriptCore 框架,安卓下腾讯 x5 内核提供的 JsCore 环境),这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口,这就是小程序双线程模型的由来。
#三、组件系统
我们知道小程序是有自己的组件的,这些基本组件就是基于 Exparser
框架。 Exparser
基于 WebComponents
的 ShadowDOM
模型,但是不依赖浏览器的原生支持,而且可在 纯 JS
环境中运行。
1、Exparser 框架
Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由 Exparser 组织管理。
Exparser 的主要特点包括以下几点:
(1)基于 Shadow DOM 模型:模型上与 WebComponents 的 ShadowDOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他 API 以支持小程序组件编程。
(2)可在纯 JS 环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
(3)高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。
小程序中,所有节点树相关的操作都依赖于 Exparser,包括 WXML 到页面最终节点树的构建、createSelectorQuery 调用和自定义组件特性等。
2、内置组件
基于 Exparser 框架,小程序内置了一套组件,提供了视图容器类、表单类、导航类、媒体类、开放类等几十种组件。有了这么丰富的组件,再配合 WXSS,可以搭建出任何效果的界面。在功能层面上,也满足绝大部分需求。
3、原生组件
在内置组件中,有一些组件并不完全在 Exparser
的渲染体系下,而是由客户端原生参与组件的渲染。比如说 Map
组件,它渲染的层级比在 WebView
层渲染的普通组件要高。
#四、运行机制
(1)启动
热启动:假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,这个过程就是热启动;
冷启动:用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动,即冷启动。
小程序没有重启的概念
当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后(目前是 5 分钟)会被微信主动销毁
当短时间内(5s)连续收到两次以上收到系统内存告警,会进行小程序的销毁
(2)销毁
只有当小程序进入后台一定时间,或者系统资源占用过高,才会被真正的销毁。
(3)更新机制
开发者在后台发布新版本之后,无法立刻影响到所有现网用户,但最差情况下,也在发布之后 24 小时之内下发新版本信息到用户。
小程序每次冷启动时,都会检查是否有更新版本,如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上。
所以如果想让用户使用最新版本的小程序,可以利用 wx.getUpdateManager
做个检查更新的功能:
checkNewVersion() {
const updateManager = wx.getUpdateManager();
updateManager.onCheckForUpdate((res) => {
console.log('hasUpdate', res.hasUpdate);
// 请求完新版本信息的回调
if (res.hasUpdate) {
updateManager.onUpdateReady(() => {
this.setData({
hasNewVersion: true
});
});
}
});
}
Copied!
#五、小程序的技术实现
小程序的 UI 视图和逻辑处理是用多个 webview 实现的,逻辑处理的 JS 代码全部加载到一个 Webview 里面,称之为 AppService,整个小程序只有一个,并且整个生命周期常驻内存,而所有的视图(wxml 和 wxss)都是单独的 Webview 来承载,称之为 AppView。
所以一个小程序打开至少就会有 2 个 webview 进程,正式因为每个视图都是一个独立的 webview 进程,考虑到性能消耗,小程序不允许打开超过 5 个层级的页面,当然同是也是为了体验更好。
1、AppService
可以理解 AppService 即一个简单的页面,主要功能是负责逻辑处理部分的执行,底层提供一个 WAService.js 的文件来提供各种 api 接口,主要是以下几个部分:
消息通信封装为 WeixinJSBridge(开发环境为 window.postMessage, IOS 下为 WKWebview 的 window.webkit.messageHandlers.invokeHandler.postMessage,android 下用 WeixinJSCore.invokeHandler)
日志组件 Reporter 封装
wx 对象下面的 api 方法
全局的 App,Page,getApp,getCurrentPages 等全局方法
还有就是对 AMD 模块规范的实现
然后整个页面就是加载一堆 JS 文件,包括小程序配置 config,上面的 WAService.js(调试模式下有 asdebug.js),剩下就是我们自己写的全部的 js 文件,一次性都加载。
2、线上环境
而在上线后是应用部分会打包为 2 个文件,名称 app-config.json 和 app-service.js,然后微信会打开 webview 去加载。线上部分应该是微信自身提供了相应的模板文件,在压缩包里没有找到。
WAService.js(底层支持)
app-config.json(应用配置)
app-service.js(应用逻辑)
然后运行在 JavaScriptCore 引擎里面。
3、AppView
这里可以理解为 h5 的页面,提供 UI 渲染,底层提供一个 WAWebview.js 来提供底层的功能,具体如下:
消息通信封装为 WeixinJSBridge(开发环境为 window.postMessage, IOS 下为 WKWebview 的 window.webkit.messageHandlers.invokeHandler.postMessage,android 下用 WeixinJSCore.invokeHandler)
日志组件 Reporter 封装
wx 对象下的 api,这里的 api 跟 WAService 里的还不太一样,有几个跟那边功能差不多,但是大部分都是处理 UI 显示相关的方法
小程序组件实现和注册
VirtualDOM,Diff 和 Render UI 实现
页面事件触发
在此基础上,AppView 有一个 html 模板文件,通过这个模板文件加载具体的页面,这个模板主要就一个方法,$gwx,主要是返回指定 page 的 VirtualDOM,而在打包的时候,会事先把所有页面的 WXML 转换为 ViirtualDOM 放到模板文件里,而微信自己写了 2 个工具 wcc(把 WXML 转换为 VirtualDOM)和 wcsc(把 WXSS 转换为一个 JS 字符串的形式通过 style 标签 append 到 header 里)。
4、Service 和 View 通信
使用消息 publish 和 subscribe 机制实现两个 Webview 之间的通信,实现方式就是统一封装一个 WeixinJSBridge 对象,而不同的环境封装的接口不一样,具体实现的技术如下:
(1)windows 环境
通过 window.postMessage 实现(使用 chrome 扩展的接口注入一个 contentScript.js,它封装了 postMessage 方法,实现 webview 之间的通信,并且也它通过 chrome.runtime.connect 方式,也提供了直接操作 chrome native 原生方法的接口)
发送消息:window.postMessage(data, ‘*’); // data 里指定 webviewID 接收消息:window.addEventListener(‘message’, messageHandler);// 消息处理并分发,同样支持调用 nwjs 的原生能力。
(2)IOS
通过 WKWebview 的 window.webkit.messageHandlers.NAME.postMessage 实现微信 navite 代码里实现了两个 handler 消息处理器:
invokeHandler: 调用原生能力
publishHandler: 消息分发
#六、性能优化
主要的优化策略可以归纳为三点:
(1)精简代码,降低 WXML 结构和 JS 代码的复杂性;
(2)合理使用 setData 调用,减少 setData 次数和数据量;
(3)必要时使用分包优化。
1、setData 工作原理
小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。
在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。
当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。
2、常见的 setData 操作错误
(1)频繁的去 setData
在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去 setData,其导致了两个后果:Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;
(2)每次 setData 都传递大量新数据
由 setData 的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程
(3)后台态页面进行 setData
当页面进入后台态(用户不可见),不应该继续去进行 setData,后台态页面的渲染用户是无法感受的,另外后台态页面去 setData 也会抢占前台页面的执行。