美文网首页
kbone源码粗阅,框架实现方式解析

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

作者: cloudZqy | 来源:发表于2020-03-05 12:02 被阅读0次

    微信团队推出了一个开源方案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应用转变为小程序,并且页面又需要在页面中使用一些小程序的原生能力。

    相关文章

      网友评论

          本文标题:kbone源码粗阅,框架实现方式解析

          本文链接:https://www.haomeiwen.com/subject/jbdblhtx.html