浅析微信小程序的底层架构原理

一、小程序基础知识

  小程序是基于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
          });
        });
      }
    });
  }

五、小程序的技术实现

  小程序的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也会抢占前台页面的执行。

参考文章:

微信小程序的底层架构原理—技术干货!

posted @ 2021-04-16 15:50  古兰精  阅读(4713)  评论(0编辑  收藏  举报