今天是学习前端的第一天之小程序的双线程架构

微信小程序的诞生

小程序是期望的产物。
在没有小程序的时候,企业们都在微信用什么?答案是H5。因为公众号,公众号是小程序的前身,但H5也有自己的缺点,无法自己获取很多底层 APP 拥有的功能。

随着微信提供一系列的JS-SDK给Web开发使用,(JS-SDK就是一套调用微信能力的工具包。比如微信支付等等的微信功能)
WebView 的使用频率越来越高。而Webview的加载体验相对比较糟糕。

微信作为一个平台,具有优化用户体验的责任。微信面临的问题是如何设计一个比较好的系统,使得所有开发者在微信中都能获得比较好的体验。这个问题是之前的 JS-SDK 所处理不了的,需要一个新的系统来完成。小程序就是在这种期待中诞生的。

一方面,小程序是基于 WebView 开发的,其目的是减少开发的成本,实现异步加载的方式,允许开发者在线的版本更新和 Bug 修复,而前面说的使用 Webview 容易导致加载体验不好,小程序使用了双线程的方式来使页面渲染和逻辑代码加载分开,降低页面卡壳的可能性。

小程序与web开发的区别

我们都知道,传统web的架构模型是单线程架构,其渲染线程和脚本线程是互斥的,这也就是说为什么长时间的脚本运行可能会使页面失去响应,而小程序的架构模型有别于传统web,小程序为双线程架构,其渲染线程和脚本线程是分开运行的

传统web开发者可以使用各种浏览器暴露出来的DOM API来进行DOM操作,但由于小程序的逻辑层和渲染层是分开的,逻辑层并没有一个完整的浏览器对象,因而缺少相关的DOM APIBOM API

传统web开发者需要面对的是各式各样的浏览器,PC 端需要面对 IEChrome火狐浏览器等,在移动端需要面对SafariChrome以及 iOSAndroid 系统中的各式 WebView 。而小程序开发过程中需要面对的是两大操作系统 iOS 和 Android 的微信客户端,以及用于辅助开发的小程序开发者工具。
各平台的脚本执行环境以及渲染非原生组件的环境都是各不相同的,如下表所示:

平台 渲染层 逻辑层
iOS WKWebView JsCore
Android chromium定制内核 V8
小程序开发者工具 Chrome WebView NWJS

双线程架构

小程序的渲染层逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染,逻辑层采用JsCore线程运行JS脚本。

小程序中为了达到多页面体验的功能,需要使用到多个WebView,每个WebView对应的就是一个小程序的页面。WebViewJsCore这两个线程的通信会经由微信客户端(下文中也会采用Native来代指微信客户端)做中转,线程之间的通讯、数据的传递以及逻辑层发送网络请求也都是经由Native转发,小程序的通信模型下图所示。
今天是学习前端的第一天之小程序的双线程架构_第1张图片

WebView是什么

这里需要解释一下WebView是什么,WebView直译是网页视图,我们可以简单地看作一个可以嵌套到界面上的一个浏览器控件

我们通常是用浏览器来浏览网页,你很清楚的知道你正在使用浏览器,要么是PC客户端,要么是手机上的app。但是webview是一个嵌入式的浏览器,是嵌入在原生应用中的,你可能都意识不到你在用浏览器。传统浏览器分为两个部分,UI(地址栏、导航栏)和浏览器引擎webview就是原生应用中的浏览器引擎

简单来说 webview 是手机中内置了一款高性能 webkit 内核浏览器,不过没有提供UI(地址栏、导航栏),只是单纯的展示一个网页界面。

为什么要多个WebView

接着上文说,那么为什么要用多个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 线程。

双线程通信

既然渲染层和逻辑层分别由两个线程管理,那么二者之间的交互就涉及到线程间的通信过程了。二者通信过程如下图所示:
今天是学习前端的第一天之小程序的双线程架构_第2张图片
可以看出,线程的通信是通过Native做中转,具体表现是:

  • Native分别在渲染层逻辑层注入WeixinJSBridge渲染层逻辑层可以与Native通信。
  • 渲染层逻辑层通过Native作为中转来处理或者转发信息。

通信原理

上面说到WeixinJSBridge提供了渲染层NativeNative逻辑层之间消息通讯的机制。机制是机制,具体的通信手段又是什么呢?

这层通信机制在iOS和安卓系统的实现方式并不一样,iOS是利用了WKWebView的提供messageHandlers特性,而在安卓则是往WebView的window对象中注入一个原生方法,最终会封装成WeixinJSBridge这样一个兼容层。
(在微信开发者工具中则是使用了websocket进行了封装,详情见)

微信开发者工具双线程通信的设计

回到上上文中,我们提到线程的通信是通过Native做中转,我们知道Native理论上是微信客户端,但是对于微信开发者工具而言,它并不是微信客户端,没有Native。那么它是怎么实现渲染层和逻辑层之间的通信的呢?是的,它是使用Websocket来完成线程间通信。
今天是学习前端的第一天之小程序的双线程架构_第3张图片
微信开发者工具有一个消息中心底层模块维持着一个WebSocket服务器,小程序逻辑层的WebView渲染层的WebView通过WebSocket与开发者工具底层建立长连,使用WebSocketprotocol字段来区分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提供的上面的四个方法,下面浅浅介绍下这四个方法:

on

主要用来收集小程序开发者工具触发的事件回调
当小程序开发者工具要触发渲染层的某个动作时,借助websocket服务向渲染层发送command: WEBVIEW_ON_EVENT命令,表示这个命令是来自开发者工具的,通过eventName来告诉渲染层执行什么事件方法。

invoke

在小程序开发者工具中,以api的方式调用开发者工具提供的基础能力,并提供对应api执行后的回调

渲染层统一向websocket服务发送command: WEBVIEW_INVOKE的命令,根据参数中的api值来确定调用开发者工具具体的api方法。
调用完毕后,websocket服务向渲染层发送command: WEBVIEW_INVOKE_CALLBACK的命令,渲染层根据此标识知道api调用完毕,然后执行对应的回调

publish

主要用来向逻辑层发送信息,也就是说要调用逻辑层的事件方法

该过程涉及到双线程的通信,渲染层通过websocket服务触发逻辑层对应的事件方法。

需要强调的是:publish没有收集执行的回调,只用来通知逻辑层调用指定的方法,至于执不执行以及执行结果,渲染层并不关注。

渲染层统一向websocket服务发送command: WEBVIEW_PUBLISH的命令,websocket服务接到命令就向逻辑层转发消息
逻辑层收到消息后,根据消息参数的eventName值确定具体调用哪一个方法

subscribe

主要用来收集逻辑层触发的事件回调,和publish配套,就像javascript中的发布订阅模式。

渲染层通过subscribe注册事件方法,事件方法是逻辑层在某个时间段通知要执行。
渲染层执行回调的时机是收到来自websocket服务的command: APPSERVICE_PUBLISH命令,通过eventName来确定要执行具体什么事件方法

最后

第一天结束辣!芜湖!
浅浅介绍了一下小程序的诞生史,比较了小程序开发与传统web开发的区别,引出了小程序的双线程架构。

中间也多次提到了双线程机制的好处,然鹅不得不说的是虽然双线程机制相比web技术对于小程序来说更加安全,性能也有明显的提升,但也并不是十全十美的。
web技术可以在线即时更新,而这种双线程机制必须将代码打包提交到微信官方进行审核后才能发布,这也限制了开发者的自由度。并且JsCore没有一个完整的浏览器对象,无法使用DOM APIBOM API,导致一些我们常用的js库就失去了用武之地。

这告诉我们:技术与架构本身就没有好坏之分,每一种技术的发布,其目的都在为开发提供多元化的方式,理性看待,孰优孰劣还需要结合自身的应用环境。

浅浅期待一下学习前端的第二天叭(⑅˃◡˂⑅)


参考文章:

  • https://developers.weixin.qq.com/ebook?action=get_post_info&token=935589521&volumn=1&lang=zh_CN&book=miniprogram&docid=000a80769707d86b008602a955b80a
  • https://www.cnblogs.com/wonyun/p/10997800.html
  • https://www.jianshu.com/p/4e35cd8bafee
  • https://baijiahao.baidu.com/s?id=1697206908822380558&wfr=spider&for=pc

你可能感兴趣的:(小程序,双线程架构)