Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

当我们前端切图崽网上冲浪的时候,会发现有很多技术文章都在分析 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 从空页面到具体页面视图的过程如下:

  1. 空页面地址 webview 加载完毕后执行事件中的 reload 方法,即设置 webview 的 src 为 pageframe 地址;

  2. 加载完成后,设置其 src 为 pageframe.html, 新的 src 内容加载完成后再次触发 onLoadCommit 事件但根据条件不会执行 reload 方法;

  3. 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

想了解更多转转公司的业务实践,点击关注下方的公众号吧!