平常小程序写的多一些,简单总结一下原理。但因为小程序也没开源,只能参考相关文档以及开发者工具慢慢理解了。
理解小程序原理的突破口就是开发者工具了,开发者工具是基于 NW.js,一个基于 Chromium 和 node.js 的应用运行时。同时暴漏了 debug 的入口。

点开后就是一个新的 devTools 的窗口,这里我们可以找到预览界面的 dom。

小程序界面是一个独立的 webview,也就是常说的视图层,可以在命令行执行 document.getElementsByTagName('webview') ,可以看到很多 webview。

我这边第 0 个就是 pages/index/index 的视图层,再通过 document.getElementsByTagName('webview')[0].showDevTools(true) 命令单独打开这个 webview 。

熟悉的感觉回来了,其实就是普通的 html/css ,小程序的原理的突破口也就在这里了。
这篇文章简单看一下页面的 dom 是怎么来的,也就是 wxml 做了什么事情。
源代码:

渲染出来的代码:

view 变成了 wx-view ,text 变成了 wx-text ,并且里边加了 <span>。两个关键信息,wx-xxx 标签以及 exparser 。
自定义标签
html 是支持我们直接写自定义名字的标签的,并且在上边设置 class 也会直接生效。

区别在于自己写的标签没有一些预制的属性,比如 div 的 display: block 。

如果我们给 wx-view 也加个 display: block ,那表现上它和 div 也就一致了。
微信已经帮我们把自定义标签的属性提前内置了。

至于为什么要把我们写的 view 转成 wx-view ,因为自定义元素中规定必须用 - 连接。
“自定义元素的名字必须包含一个破折号(
-)所以<x-tags>、<my-element>和<my-awesome-app>都是正确的名字,而<tabs>和<foo_bar>是不正确的。这样的限制使得 HTML 解析器可以分辨那些是标准元素,哪些是自定义元素。”
有 - 可以保证一定的兼容性,并且也可以和浏览器自带的元素有一定的区分。
Exparser
简单讲,就是一个仿照 Web Components 的组件系统,它会维护标签的属性、事件,提供 registerElement 方法用于注册自定义组件,提供 createElement 来渲染组件,对于自定义组件会采用 Shadow DOM 的技术。
Exparser 的相关代码在哪里呢?这就是微信传说中的基础库里了,在渲染层引入的是 WAWebview.js 。

可以右键打开这个文件,复制出来格式化一下:

由于文件比较大,用 VSCode 直接格式化可能会很卡,可以写个脚本来格式化。

1 | // chatGPT 生成 |
然后在命令行执行 node format.js ./WAWebview.js ,接下来就看到格式化的代码了:

是 2.32.3 版本,目前微信已经更到 3.x.x 了,新增了渲染引擎 Skyline,为了简单些这次就先看 2.x 的版本了。

总共有 14 万行

接下来通过搜索、折行,找一下 Exparser 的部分,因为都是压缩过的代码,逐行理解肯定不现实,就找几个关键点看一下:
提供了注册组件的方法 registerElement 。


提前注册了内置的组件:
wx-view:

wx-text :

可以看到上边最终转成了 span 标签,和我们开发者工具看到的也是一致的:

提供了 createElement 方法,将注册的组件生成为最终的 dom 。

最终会调用 document 来创建 dom 。

生成流程
再回到加载的 dom 看一下 wxml 转换成了什么:

右键打开这个文件:
定义了 $gw 这个函数,接收 path 参数。

返回一个函数:

内部有我们 wxml 的变量:

对应于原文件:

看一下调用这个函数的地方:

传入当前页面路径将生成的函数赋值给了 generateFunc ,接着用 document.dispatchEvent 触发事件 generateFuncReady,并且将 generateFunc 传入。
我们在控制台手动执行一下 generateFunc ,看下返回值:
可以看到 3 个子元素:

但因为前两个的值是在逻辑层 data 中,因为我们没有传递,所以上边前两个子元素 children 都是空字符串

这个 data 需要在调用 generateFunc 的时候传入:

现在就正常返回了标签的结构,接着渲染层内部就会利用它生成虚拟 dom ,再利用 Exparser 生成最终的 dom 元素了。
大概是下边的流程(下边的代码是最早期的基础库,目前的版本已经不是下边的结构了,目前先按下边的流程理解,后边再理清当前基础库的逻辑):

调用 virtualTree 将 generateFunc 返回的结构变为虚拟 dom ,接着调用 render ,render 内部就是调用前边介绍的 Exparser 的 createElement 方法生成真正的 dom ,最后通过 replaceChild 挂载到页面上。
当然 generateFunc 需要的 data 数据需要等待逻辑层传过来,后边的文章再介绍通信机制。
编译
剩下最后一个问题,wxml.js 是哪里来的?

和 wxss 一样,是微信提前编译生成的。编译工具可以在微信开发者工具目录搜索 wcc ,Library 是个隐藏目录。

我们把这个 wcc 文件拷贝到 index.wxml 的所在目录,然后将我们的 index.wxml 手动编译一下:
1 | ./wcc -js ./index.wxml >> wxml.js |

可以看到 $gw 函数就生成了。
总
大概过程就是上边了,先提前编译出了 $gw 函数,会返回一个函数,可以把 wxml 实例为一个 dom 的标签结构。传入当前页面的路径执行该函数生成 generateFunc 函数,将函数传给视图层。
视图层拿到逻辑层的数据后将 generateFunc 函数返回的 dom 结构生成虚拟 dom ,通过 Exparser 执行 render 生成最终的 dom 挂载到页面。
至于拿到逻辑层的数据的时机,相互通信的逻辑就放到后边的文章了,看着混淆的代码,头大。
历史文章: