当我们前端切图崽网上冲浪的时候,会发现有很多技术文章都在分析vue框架,react框架,显少有分析小程序框架的。那今天就通过这篇短小精悍的文章带大家了解一下微信小程序的底层架构。(如无特殊说明,下文中提到的小程序都是微信小程序)
小程序的由来
我们先抛出一个问题,在没有小程序的时候,企业们都在微信里怎么运营? 答案就是小程序的“前身”-公众号,企业们普遍会把H5网站放在公众号作为流量转换的入口。但是h5确实让公众号遇到了一些问题。
首先就是白屏过程,对于一些复杂页面,受限于设备性能和网络速度,白屏会更加明显;再就是缺少操作反馈,比如页面切换生硬以及点击所带来的迟滞感等等;
微信团队内部通过JS-SDK以及后来的增强JS-SDK已经能够解决一些问题,但是对于上述问题是JS-SDK所处理不了的,急需一个全新的系统来完成,它需要具备以下能力:
- 快速加载
- 更强大的能力
- 原生的体验
- 易用且安全的微信数据开放
- 高效,简单的开发
于是,小程序诞生了。
双线程架构
此处点题一下,本文我们讨论的是小程序的底层架构,其实,双线程架构就是小程序的核心。
那为什么要设计成双线程架构呢?首先我们来回顾一下浏览器的线程模型,浏览器是一个单线程架构,主要原因是js允许访问操作DOM,因此js线程和渲染线程只能互斥运行。
那小程序又是如何做到双线程的呢,根本原因就是微信小程序禁止js操作DOM。
使用双线程架构的优势一目了然:
- 提高用户体验(ui和逻辑分离,避免页面长时间阻塞和卡顿)
- 优化应用性能(运行在不同的线程中,可以同时渲染或者计算)
- 开发效率更高(解耦和松散耦合)
接下来就带大家了解一下渲染层以及逻辑层的设计思路。
设计思路-渲染层
标签实现
小程序使用的是Exparser组件模型,Exparser组件模型与Web Components中的shadow DOM高度相似,微信为什么使用自定义组件框架,而不使用Web Components呢?主要还是出于安全考虑,并且方便管控。既然Exparser组件框架与shadow DOM高度相似,那么我们首先来了解一下shadow DOM。
shadow DOM: Web Components的一个重要属性是封装-可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。其中,shadow DOM接口是关键所在,它可以将一个隐藏的,独立的DOM附加到一个元素上。
shadow DOM允许将隐藏的DOM树附加到常规的DOM树中-它以shadow root节点为起始根节点,在根节点的下方,可以是任意元素,和普通的DOM一样。
以上解释来源于MDN,其实shadow DOM并不神秘,像我们非常熟悉的video标签本质上就是用shadow DOM实现的。我们先打开chrome浏览器设置中的“打开用户代理shadow DOM”,然后再点击video标签就能看到。
创建shadow DOM也非常简单,直接使用attachShadow方法就可以创建。
var shadow = Element.attachShadow({ mode: 'closed'})
Exparser组件模型:Exparser组件模型参考了shadow DOM并进行了一些修改,像事件系统就是完全复刻的,slot插槽,属性传递等都基本一致。但同时它又具有一些特点:
- 基于shadow DOM模型:模型上与Web Components的shadow DOM高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程;
- 可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力;
- 高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小;
WXML编译
了解了小程序的组件系统之后,接下来看看WXML的编译过程。小程序中的DOM编译流程与vue类似,也会先将代码字符串编译为虚拟DOM,小程序中的虚拟DOM结构如下:WXML最终会被编译为JS文件,然后插入到渲染层的script标签中。
WXSS动态适配
WXSS是小程序中使用的样式语言,WXSS具有CSS的大部分特性,同时它对CSS进行了扩充以及修改。
小程序中使用的尺寸单位为rpx(Responsive px),不同于h5中对于px的处理,需要使用postcss进行统一的转换,小程序底层已经为开发者做好了这层转换,那具体它是怎么做到的呢?
我们看它的这段源码,其实它与阿里的flexible.js方案是类似的,不同的是它做了一个精度收拢的优化,主要是为了解决1px的问题。
WXSS同样会经过编译,最终的编译产物为wxss.js,不同于WXML通过script标签的形式插入到渲染层,wxss.js则是通过eval的方式注入到渲染层代码中。
渲染层webview
全局变量: 渲染线程中存在着以下全局变量。
- webviewId:webview的唯一标识,当用户打开一个小程序页面的时候,相当于打开了一个webview,不同的webview用webviewid来区分;
- wxAppCode:整个页面的json wxss wxml编译之后都存储在这里;
- Vd_version_info:版本信息;
- ./dev/wxconfig.js:小程序默认总配置项,包括用户自定义与系统默认的整合结果。在控制台输入__wxConfig可以看出打印结果;
- ./dev/devtoolsconfig.js:小程序开发者配置,包括navigationBarHeight,标题栏的高度,状态栏高度,等等,控制台输入__devtoolsConfig可以看到其对应的信息;
- ./dev/deviceinfo.js:设备信息,包含尺寸/像素点pixelRatio;
- ./dev/jsdebug.js:debug工具;
- ./dev/WAWebview.js:渲染层底层基础库;
- ./dev/hls.js:优秀的视频流处理工具;
- ./dev/WARemoteDebug.js:底层基础库调试工具;
那小程序是如何快速启动一个webview的呢?
我们在打开pages/index/index视图页面时,发现DOM中多加载了一个__pageframe__/pageframe.html的视图层。这个视图层的作用正是小程序提前为一个新的页面层准备的。小程序每个视图层页面内容都是通过pageframe.html模板来生成的,包括小程序启动的首页。
下面来看看小程序为快速打开小程序页面做的技术优化:
- 首页启动时,即第一次通过pageframe.html生成内容后,后台服务会缓存pageframe.html模板首次生成的html内容;
- 非首次新打开页面时,页面请求的pageframe.html内容直接走后台缓存;
- 非首次新打开页面时,pageframe.html页面引入的外链js资源走本地缓存; 这样在后续新打开页面时,都会走缓存的pageframe的内容,避免重复生成,快速打开一个新页面。
视图层打开新页面的流程
在创建每个视图层页面的webview时,都会为其绑定了onLoadCommit事件(它会在页面加载完成后触发,包含当前文档的导航和副框架的文档加载)。初始时webview的src会被指定为空页面地址http://127.0.0.1:${global.proxyPort}/aboutblank?${c},其中c为对应webview的id。webview从空页面到具体页面视图的过程如下:
- 空页面地址webview加载完毕后执行事件中的reload方法,即设置webview的src为pageframe地址;
- 加载完成后,设置其src为pageframe.html, 新的src内容加载完成后再次触发onLoadCommit事件但根据条件不会执行reload方法;
- pageframe.html页面在dom ready之后触发注入并执行具体页面相关的代码,此时通过history.pushState方法修改webview的src但是webview并不会发送页面请求;
设计思路-逻辑层
接下来我们看看小程序在逻辑层都做了哪些事情。
逻辑层与视图层通信
在小程序中,逻辑层只有一个,但是渲染层有多个,渲染层和逻辑层之间是通过微信客户端进行桥接通信的。那具体是怎么实现的呢?其实它使用的就是WeixinJSBridge通信机制。
在小程序执行的过程中,微信客户端分别向渲染层和逻辑层注入WeixinJSBridge,WeixinJSBridge主要提供了以下几个方法:
- invoke:调用native API;
- invokeCallbackHandler:Native 传递 invoke 方法回调结果;
- publish:渲染层用来向逻辑业务层发送消息,也就是说要调用逻辑层的事件方法;
- subscribe:订阅逻辑层消息;
- subscribeHandler:视图层和逻辑层消息订阅转发;
- setCustomPublishHandler:自定义消息转发;
渲染层如何向逻辑层通信?
渲染层向逻辑层通信的方式就是采用事件系统,以上就是完整的事件系统流程。
开发者在DOM上通过@click绑定事件,WXML文件被编译的时候,会通过$gwx函数生成虚拟DOM,然后小程序执行的时候渲染层底层基础库会对虚拟DOM进行解析,事件绑定最终会以attr属性的形式生成到虚拟DOM中,所以底层基础库通过applyPropeties解析事件并通过addEventListener绑定到相应DOM并声明回调。
用户点击相应DOM时,Exparser组件系统接收到这个事件,然后开始执行回调。回调函数在逻辑层,事件的触发在渲染层,此时,小程序会通过setData发送数据到逻辑层,这个时候WeixinJSBridge就派上用场了,渲染层调用publish方法发送数据,逻辑层通过registercallback进行监听,并执行相应的回调。此时,渲染层到逻辑层的通信流程结束。
那逻辑层又是如何将改变后的数据回传给渲染层的呢?逻辑层改变数据之后,同样是触发setData方法,然后渲染层通过subscribe进行监听,从eventname和触发事件时候记录的回调函数来判断是哪个事件被触发了,从而获取动态数据。
第三方小程序框架
WXML,WXSS都是小程序的原生开发语言,使用原生语言开发还是存在诸多限制,尤其是17年小程序刚推出那会。因此,第三方小程序框架应运而生。第三方框架可以分为三大类。
第一类是预编译框架,预编译框架就是在执行前就进行编译。像我司在17年开发“转转二手交易网”的时候使用的wepy框架就属于预编译框架。预编译框架也有一些显而易见的缺点,这类预编译框架要么是类vue,要么是类React,如果后期vue或者React再出一些新特性的话,预编译框架就要进行扩展编写;还有一些兼容问题,对于小程序本身不支持的一些属性,预编译框架需要进行兼容;
第二类是半编译半运行框架,像美团的mpvue就是此类框架,半编译指的是vue的template需要单独编译为wxml,半运行讲的是vue整体的特性都会在逻辑层中运行。为了符合小程序的渲染框架,修改了vue的框架;
第三类是运行时框架,像Remax就是运行时框架,它可以使开发者使用完整的React语法来开发小程序。因为小程序框架本身是不支持js直接操作DOM的,那Remax框架是如何解决这个问题的呢?其实它自己复刻了一套操作DOM的API,例如appendChild,innterHtml等,但是它真正操作的并不是dom,而是data中的数据结构。从而达到了操作DOM的目的。使得自己真正成了一个运行时框架;
结语
介绍到这里,小程序的底层框架原理基本已经介绍完了,想跟大家分享的是,小程序确实和h5非常类似,其实它相当于一个借助了native强大功能的加强版h5,小程序并不神秘,除了微信小程序之外,现在各大超级APP都已经推出了自己的小程序,原理应该都大差不差。
本篇文章其实相当于一个学习笔记,作者本身非常想搞清楚微信小程序的架构,但是微信小程序并没有开源,某次偶然的机会逛掘金的时候看到这篇小册,就整个学习了一下,在此感谢原作者!
参考
https://juejin.cn/book/6982013809212784676?enter_from=course_center&utm_source=course_center