微信团队推出了一个开源方案kbone,他们对kbone的描述是kbone 是一个致力于微信小程序和 Web 端同构的解决方案。
其使用方式不需要更改react、vue等的底层,直接能将框架代码运行在小程序上,听上去与如今流行的跨平台方案uni-app和tarojs实现上完全不同。所以他具体是什么实现的,是这篇文章主要探究的地方。
一、阅读文档寻找信息
了解一个库的第一步,当然是阅读文档,在kbone的文档中能找到如下关键信息。
负责提供 dom/bom api 的 js 库和负责渲染的自定义组件,也就是 kbone 中的 miniprogram-render 和 miniprogram-element,可以看到 kbone 最终生成的小程序代码里会依赖这两个 npm 包。除此之外还需要一个 webpack 插件来根据原始的 Web 端源码生成小程序代码,因为小程序代码包和 Web 端的代码不同,它有固定的结构,而这个插件就是 mp-webpack-plugin。
miniprogram-render、miniprogram-element 和 mp-webpack-plugin 这三个包即是 kbone 的核心。
kbone提供了dom和bom的接口,并且miniprogram-render、miniprogram-element 和 mp-webpack-plugin 三个包是关键。
根据名字来看,mp-webpack-plugin这个包做的是一些编译代码的活,主要需要了解的编译后代码的样子,其实现源码不必深入探究。
miniprogram-element应该就是对dom的实现,miniprogram-render则是将dom树渲染成微信小程序。
二、dom接口
通常阅读源码最好带着问题和目的去看。我们先下载一个kbone的react模板,程序最开始的代码如下
const container = document.createElement('div')
container.id = 'app'
document.body.appendChild(container)
这几行代码调用了dom的接口,调用了之后发生了什么就是接下来需要探究的事。首先我们通过miniprogram-render抛出的document对象找到createElement方法。
// miniprogram-render/src/document
/**
* 内部所有节点创建都走此接口,统一把控
*/
$$createElement(options, tree) {
const originTagName = options.tagName
const tagName = originTagName.toUpperCase()
let wxComponentName = null
tree = tree || this.$_tree
const constructorClass = CONSTRUCTOR_MAP[tagName]
if (constructorClass) {
return constructorClass.$$create(options, tree)
// eslint-disable-next-line no-cond-assign
} else if (wxComponentName = checkIsWxComponent(originTagName, this.$$notNeedPrefix)) {
// 内置组件的特殊写法,转成 wx-component 节点
options.tagName = 'wx-component'
options.attrs = options.attrs || {}
options.attrs.behavior = wxComponentName
return WxComponent.$$create(options, tree)
} else if (WX_CUSTOM_COMPONENT_MAP[originTagName]) {
// 自定义组件的特殊写法,转成 wx-custom-component 节点
options.tagName = 'wx-custom-component'
options.attrs = options.attrs || {}
options.componentName = originTagName
return WxCustomComponent.$$create(options, tree)
} else if (!tool.isTagNameSupport(tagName)) {
return NotSupport.$$create(options, tree)
} else {
return Element.$$create(options, tree)
}
}
通过tagName分成了五种分支
- 特殊处理的组件(Input、Image、Video等)。
- 微信原生组件
- 微信自定义组件
- 不受支持的tag
- 其他组件(div、span等)
分支1
应该是对于微信特殊组件的兼容的一些脏活累活,2
和 3
是对微信自己组件的一些处理,而我们最关心的应该是div、span等html标签如何处理成wxml的,所以主要看分支5
。
通过对浏览器dom的了解,我们可以猜出Element应该是仿照浏览器Element类所实现的一个类,抛出的是一个Element的实例,那么接下来就开始找appendChild干了什么,在Element类的代码miniprogram-render/src/node/element.js
中。
appendChild(node) {
if (!(node instanceof Node)) return
let nodes
let hasUpdate = false
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
// documentFragment
nodes = [].concat(node.childNodes)
} else {
nodes = [node]
}
for (const node of nodes) {
if (node === this) continue
if (node.parentNode) node.parentNode.removeChild(node)
this.$_children.push(node)
node.$$updateParent(this) // 设置 parentNode
// 更新映射表
this.$_updateChildrenExtra(node)
hasUpdate = true
}
// 触发 webview 端更新
if (hasUpdate) this.$_triggerMeUpdate()
return this
}
这段代码可以看出,将node插入到当前元素的children中,然后触发更新。和所猜的差不多,kbone既然实现了dom接口,那么肯定是讲整个页面变成了一个dom树,而appenChild,removeChild等接口则是操作这颗dom树。
那么这个triggerMeUpdate
触发后,应该是触发了渲染。顺着这个事件往下找,会发现实际上是进入到了EventTarget类中触发了$$childNodesUpdate
事件,全局搜这个事件会发现进入了
的自定义组件。
// miniprogram-element/src/index.js
this.domNode.addEventListener('$$childNodesUpdate', this.onChildNodesUpdate);
这个onChildNodesUpdate
则是个节流后的方法。
onChildNodesUpdate() {
// 判断是否已被销毁
if (!this.pageId || !this.nodeId) return
// 儿子节点有变化
const childNodes = _.filterNodes(this.domNode, DOM_SUB_TREE_LEVEL - 1)
const oldChildNodes = this.data.wxCompName || this.data.wxCustomCompName ? this.data.innerChildNodes : this.data.childNodes
if (_.checkDiffChildNodes(childNodes, oldChildNodes)) {
const dataChildNodes = _.dealWithLeafAndSimple(childNodes, this.onChildNodesUpdate)
const newData = {}
if (this.data.wxCompName || this.data.wxCustomCompName) {
// 内置组件/自定义组件
newData.innerChildNodes = dataChildNodes
newData.childNodes = []
} else {
// 普通标签
newData.innerChildNodes = []
newData.childNodes = dataChildNodes
}
this.setData(newData)
}
// 触发子节点变化
const childNodeStack = [].concat(childNodes)
let childNode = childNodeStack.pop()
while (childNode) {
if (childNode.type === 'element' && !childNode.isLeaf && !childNode.isSimple) {
childNode.domNode.$$trigger('$$childNodesUpdate')
}
if (childNode.childNodes && childNode.childNodes.length) childNode.childNodes.forEach(subChildNode => childNodeStack.push(subChildNode))
childNode = childNodeStack.pop()
}
},
这个方法就是递归通知子组件进行update,然后进行了一次setData(newData)。
三、element组件
在element中做出了一系列的操作更新了组件的data,所以我们需要着重了解element如何进行渲染的。
data: {
wxCompName: '', // 需要渲染的内置组件名
wxCustomCompName: '', // 需要渲染的自定义组件名
innerChildNodes: [], // 内置组件的孩子节点
childNodes: [], // 孩子节点
},
从data中可以看到这几个数据,源码中亲切地标上了注释,通过打断点的方式也可以看到newData具体的内容。可以看出关键就在childNodes这个描述子节点的数组,然后我们看他的wxml文件
.......
中间对大量微信小程序原生组件进行了switch操作。我们看第一行和最后一行,就是对非原生组件的处理。然后找到subtree.wxml,这段wxml是由脚本生成然后压缩的的,但是没有关系,我们用vscode格式化一下。
然后发现这是个递归渲染,就是将kbone的虚拟dom渲染成小程序原生组件。但为什么这个subTree要写这么大,重复了9级呢?因为如果只写一层的话,就是不停地递归自定义组件element了,而小程序的自定义组件是用shadow-dom实现的,会很影响性能,所以这里分了九层,并用level来控制isSimple属性,当层级低于9层时,直接通过view来进行渲染,而不需要用到自定义组件。
为了验证这个观点我写了个简单的例子
let i = 0;
let root = document.createElement('div');
let parent = root;
while (i < 40) {
const div = document.createElement('div');
const text = document.createElement('span');
text.innerHTML = `${i}`;
div.appendChild(text);
parent.appendChild(div);
parent = div;
i++;
}
document.body.appendChild(root);
发现的确如此,只有层级够深时才会递归回element。
总结
通过前面的分析,我们大致了解了kbone实现的原理,通过重写微信小程序屏蔽掉的bom和dom的接口,将react、vue框架生成的dom变为kbone的虚拟dom,然后通过自定义组件递归渲染成小程序原生组件。
kbone中还有一些其他功能,比如页面、路由、dom树操作的性能优化,这里暂时还没有探究到。
使用了微信kbone搭配微信小程序,终于可以将普通前端开发的流程:框架代码 -> react虚拟dom -> html
的流程变成了 框架代码 -> 虚拟dom -> kbone的dom树 -> wxml -> 小程序domInfo -> 微信渲染层 -> html
可喜可贺。
选择
在如何选用方案上,kbone也直言不讳说出,kbone 是使用一定的性能损耗来换取更为全面的 Web 端特性支持。
就性能方面
来说,mini-app和tarojs在流程上少了框架代码 -> 虚拟dom -> kbone的dom树
这两部,在编译阶段就直接编译成了wxml,性能上更为接近小程序原生。而且由于kbone使用递归渲染,当层级越深时,对性能影响将越大,具体差距还需要具体去探究。
跨平台方面
,目前kbone只支持微信小程序,而且以微信的风格,不大可能支持其他小程序。如果要将kbone改造成支持其他小程序,则需要改造 mp-webpack-plugin 使其能编译成其他小程序代码,和miniprogram-element 兼容其他小程序的一些特殊组件。
kbone最大的优势是可以直接运行web端的代码,但其实webview也能做到而且更灵活。唯一的应用场景大概是要将某个web应用转变为小程序,并且页面又需要在页面中使用一些小程序的原生能力。