kbone源码粗阅,框架实现方式解析

微信团队推出了一个开源方案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分成了五种分支

  1. 特殊处理的组件(Input、Image、Video等)。
  2. 微信原生组件
  3. 微信自定义组件
  4. 不受支持的tag
  5. 其他组件(div、span等)

分支1应该是对于微信特殊组件的兼容的一些脏活累活,23 是对微信自己组件的一些处理,而我们最关心的应该是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事件,全局搜这个事件会发现进入了<element></element>的自定义组件。

// 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文件

<import src="./template/subtree.wxml"/>

.......

<!-- 子节点 -->
<template wx:else is="subtree" data="{{childNodes, inCover}}"/>

中间对大量微信小程序原生组件进行了switch操作。我们看第一行和最后一行,就是对非原生组件的处理。然后找到subtree.wxml,这段wxml是由脚本生成然后压缩的的,但是没有关系,我们用vscode格式化一下。


image.png

然后发现这是个递归渲染,就是将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应用转变为小程序,并且页面又需要在页面中使用一些小程序的原生能力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335