小程序是期望的产物。
在没有小程序的时候,企业们都在微信用什么?答案是H5
。因为公众号,公众号是小程序的前身,但H5
也有自己的缺点,无法自己获取很多底层 APP 拥有的功能。
随着微信提供一系列的JS-SDK
给Web开发使用,(JS-SDK就是一套调用微信能力的工具包。比如微信支付等等的微信功能)
WebView
的使用频率越来越高。而Webview
的加载体验相对比较糟糕。
微信作为一个平台,具有优化用户体验的责任。微信面临的问题是如何设计一个比较好的系统,使得所有开发者在微信中都能获得比较好的体验。这个问题是之前的 JS-SDK
所处理不了的,需要一个新的系统来完成。小程序就是在这种期待中诞生的。
一方面,小程序是基于 WebView
开发的,其目的是减少开发的成本,实现异步加载
的方式,允许开发者在线的版本更新和 Bug 修复,而前面说的使用 Webview
容易导致加载体验不好,小程序使用了双线程
的方式来使页面渲染和逻辑代码加载分开,降低页面卡壳的可能性。
我们都知道,传统web的架构模型是单线程架构
,其渲染线程和脚本线程是互斥的,这也就是说为什么长时间的脚本运行可能会使页面失去响应,而小程序的架构模型有别于传统web,小程序为双线程架构
,其渲染线程和脚本线程是分开运行的。
传统web开发者可以使用各种浏览器暴露出来的DOM API
来进行DOM操作
,但由于小程序的逻辑层和渲染层是分开的,逻辑层并没有一个完整的浏览器对象,因而缺少相关的DOM API
和BOM API
传统web开发者需要面对的是各式各样的浏览器,PC
端需要面对 IE
、Chrome
、火狐
浏览器等,在移动端
需要面对Safari
、Chrome
以及 iOS
、Android
系统中的各式 WebView
。而小程序开发过程中需要面对的是两大操作系统 iOS 和 Android 的微信客户端,以及用于辅助开发的小程序开发者工具。
各平台的脚本执行环境以及渲染非原生组件的环境都是各不相同的,如下表所示:
平台 | 渲染层 | 逻辑层 |
---|---|---|
iOS | WKWebView | JsCore |
Android | chromium定制内核 | V8 |
小程序开发者工具 | Chrome WebView | NWJS |
小程序的渲染层
和逻辑层
分别由2个线程管理:渲染层
的界面使用了WebView
进行渲染,逻辑层
采用JsCore
线程运行JS脚本。
小程序中为了达到多页面体验的功能,需要使用到多个WebView
,每个WebView
对应的就是一个小程序的页面。WebView
和JsCore
这两个线程的通信会经由微信客户端
(下文中也会采用Native
来代指微信客户端
)做中转,线程之间的通讯、数据的传递以及逻辑层发送网络请求也都是经由Native
转发,小程序的通信模型下图所示。
这里需要解释一下WebView
是什么,WebView
直译是网页视图
,我们可以简单地看作一个可以嵌套到界面上的一个浏览器控件
。
我们通常是用浏览器来浏览网页,你很清楚的知道你正在使用浏览器,要么是PC客户端,要么是手机上的app。但是webview
是一个嵌入式
的浏览器,是嵌入在原生应用中的,你可能都意识不到你在用浏览器。传统浏览器分为两个部分,UI
(地址栏、导航栏)和浏览器引擎
。webview
就是原生应用中的浏览器引擎
。
简单来说 webview
是手机中内置了一款高性能 webkit 内核浏览器
,不过没有提供UI(地址栏、导航栏),只是单纯的展示一个网页界面。
接着上文说,那么为什么要用多个WebView
呢?多个WebView
可以理解为多页面应用
,其实是有区别于单页面应用SPA
的,SPA
渲染页面是通过路由识别以后去动态地将页面挂载到root节点
里,如果SPA
打开一个新页面,是需要卸载掉当前的页面,然后重新渲染。很显然,只有通过多个WebView
才能更加接近原生应用APP的用户体验。
双线程架构的另一个好处就是安全管控。我们都知道基于web技术来渲染小程序,是存在一些不可控因素和安全风险的。因为web技术非常灵活开放,我们可以使用Javascript
去任意地控制页面的跳转或者改变页面上的任何内容,Javascript
还可以通过操作DOM,直接获取小程序内部的一些敏感数据,比如用户信息等等,那么小程序将毫无安全可言。
为了解决安全管控的问题,小程序从设计上就阻止了开发者去使用一些浏览器提供的开放性api,比如说跳转页面、操作DOM等等。如果把这些东西一个一个地去加入到黑名单,那么势必会陷入一个非常糟糕的循环,因为浏览器的接口也非常丰富,那么就很容易遗漏一些危险的接口,而且就算是禁用掉了所有的接口,也防不住浏览器内核的下次更新。
所以要彻底解决这个问题,我们可以使用客户端系统的JavaScript 引擎
(iOS下使用JavaScriptCore 框架,安卓下使用腾讯 x5 内核提供的 JsCore 环境),通过提供一个沙箱环境来运行开发者的 JavaScript
代码。这个沙箱环境只提供纯 JavaScript 的解释执行环境,没有任何浏览器相关接口。
这就是小程序双线程模型的由来:
JavaScript
,在这个环境下执行的都是有关小程序业务逻辑的代码。WebView
线程里执行,通过逻辑层代码去控制渲染哪些界面。一个小程序存在多个界面,所以渲染层存在多个 WebView
线程。既然渲染层和逻辑层分别由两个线程管理,那么二者之间的交互就涉及到线程间的通信过程了。二者通信过程如下图所示:
可以看出,线程的通信是通过Native
做中转,具体表现是:
Native
分别在渲染层
和逻辑层
注入WeixinJSBridge
,渲染层
和逻辑层
可以与Native
通信。渲染层
和逻辑层
通过Native
作为中转来处理或者转发信息。上面说到WeixinJSBridge
提供了渲染层
与Native
、Native
与逻辑层
之间消息通讯的机制。机制是机制,具体的通信手段又是什么呢?
这层通信机制在iOS和安卓系统的实现方式并不一样,iOS是利用了WKWebView
的提供messageHandlers特性,而在安卓则是往WebView
的window对象中注入一个原生方法,最终会封装成WeixinJSBridge这样一个兼容层。
(在微信开发者工具中则是使用了websocket进行了封装,详情见)
回到上上文中,我们提到线程的通信是通过Native
做中转,我们知道Native
理论上是微信客户端
,但是对于微信开发者工具而言,它并不是微信客户端,没有Native
。那么它是怎么实现渲染层和逻辑层之间的通信的呢?是的,它是使用Websocket
来完成线程间通信。
微信开发者工具有一个消息中心底层模块维持着一个WebSocket
服务器,小程序逻辑层的WebView
和渲染层的WebView
通过WebSocket
与开发者工具底层建立长连,使用WebSocket
的protocol
字段来区分Socket
的来源。
// 通过userAgent中获取开发者工具WebSocket服务器监听的端口
const port = window.navigator.userAgent.match(/port\/(\d*)/)[1];
// 通过指定 protocol == 'APPSERVICE' 告知开发者工具这个链接是来自逻辑层
const 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: {}
}
}))
前面提到,Native
通过分别在渲染层
和逻辑层
注入WeixinJSBridge
来实现二者与Native
的通信,然后Native
可以根据情况进行处理或者继续向指定线程传递消息。为了保持与真实环境的一致,微信开发者工具没有新增或者删除WeixinJSBridge
的方法,只是在WeixinJSBridge
的基础上进行了方法的重构。以下是部分源码:
window.WeixinJSBridge = {
on: c,
invoke: e,
publish: t,
subscribe: o,
}
可以说小程序双线程通信离不开WeixinJSBridge
提供的上面的四个方法,下面浅浅介绍下这四个方法:
主要用来收集小程序开发者工具触发的事件回调。
当小程序开发者工具要触发渲染层
的某个动作时,借助websocket
服务向渲染层
发送command: WEBVIEW_ON_EVENT
命令,表示这个命令是来自开发者工具的,通过eventName
来告诉渲染层
执行什么事件方法。
在小程序开发者工具中,以api的方式调用开发者工具提供的基础能力,并提供对应api执行后的回调
渲染层
统一向websocket
服务发送command: WEBVIEW_INVOKE
的命令,根据参数中的api值来确定调用开发者工具具体的api方法。
调用完毕后,websocket
服务向渲染层
发送command: WEBVIEW_INVOKE_CALLBACK
的命令,渲染层
根据此标识知道api调用完毕,然后执行对应的回调
主要用来向逻辑层
发送信息,也就是说要调用逻辑层
的事件方法
该过程涉及到双线程的通信,渲染层
通过websocket
服务触发逻辑层
对应的事件方法。
需要强调的是:publish
没有收集执行的回调,只用来通知逻辑层
调用指定的方法,至于执不执行以及执行结果,渲染层
并不关注。
渲染层
统一向websocket
服务发送command: WEBVIEW_PUBLISH
的命令,websocket
服务接到命令就向逻辑层
转发消息
逻辑层
收到消息后,根据消息参数的eventName
值确定具体调用哪一个方法
主要用来收集逻辑层
触发的事件回调,和publish
配套,就像javascript中的发布订阅模式。
渲染层
通过subscribe
注册事件方法,事件方法是逻辑层
在某个时间段通知要执行。
渲染层
执行回调的时机是收到来自websocket
服务的command: APPSERVICE_PUBLISH
命令,通过eventName
来确定要执行具体什么事件方法
第一天结束辣!芜湖!
浅浅介绍了一下小程序的诞生史,比较了小程序开发与传统web开发的区别,引出了小程序的双线程架构。
中间也多次提到了双线程机制的好处,然鹅不得不说的是虽然双线程机制相比web技术对于小程序来说更加安全,性能也有明显的提升,但也并不是十全十美的。
web技术可以在线即时更新,而这种双线程机制必须将代码打包提交到微信官方进行审核后才能发布,这也限制了开发者的自由度。并且JsCore
没有一个完整的浏览器对象,无法使用DOM API
和BOM API
,导致一些我们常用的js库就失去了用武之地。
这告诉我们:技术与架构本身就没有好坏之分,每一种技术的发布,其目的都在为开发提供多元化的方式,理性看待,孰优孰劣还需要结合自身的应用环境。
浅浅期待一下学习前端的第二天叭(⑅˃◡˂⑅)
参考文章: