微信小程序官方推出的Kbone,是如何解决Web 端和小程序同构痛点的?

导语 | 随着小程序的发展,Web 端和小程序同构的呼声也愈来愈大,为此微信官方提供了 Kbone 这一套方案。旨在让开发者能够用最熟悉的方式来完成一个多端 APP 的开发,下降开发门槛。本文是Kbone做者june在云加社区微信群中的分享整理总结而成(编辑:尾尾)。
html

你们好,我是来自腾讯微信小程序团队的前端开发工程师:june。小程序做为一种新兴地连接用户与服务的方式,相信你们都或多或少接触过。对于开发者来讲,它是一种相似 Web 但又不一样于 Web 的开发模式,它提供了一套自定义的 API 和文件组织方式,这无疑带给开发者必定的学习成本和维护成本,因此咱们也在尝试可否提供一个方案来抹平这个差别。前端

接下来就进入我今天要分享的话题:Kbone——微信小程序同构方案新思路。本次分享包括四个部分:背景、方案、应用和结语,首先咱们先进入背景部分。vue

1、Kbone诞生背景

之因此会有 Kbone 这个方案出现,源自于一个需求:微信开放社区当时只有 Web 端,为了让信息能够更方便地传播、分享和使用,但愿实现社区小程序版,交互体验尽可能贴近于 Web 端。react

(微信开放社区连接developers.weixin.qq.com/community/d…git

这次同构到小程序端须要考虑几个因素:多端代码复用、尽量支持已有的特性和性能要有保证。其实最主要的就是要在尽可能不改动现有代码的状况下来完成小程序的开发。github

2、具体方案实现

接下来就来探讨下具体方案的实现。vue-router

社区 Web 端是基于 Vue 实现的,使用了 Vue-router、Vuex 等插件。Vue 想必你们挺熟悉的了,它是市面上一款很是流行的 Web 框架,提供组件化等特性,其原理大体以下:小程序

Vue 模板能够认为是一种附加了一些特殊语法的 HTML 片断,通常来讲一份 Vue 模板对应一个组件,在构建阶段编译成调用 Dom 接口的 JS 函数,调用此 JS 函数就会建立出组件对应的 Dom 树片断进而渲染到浏览器上。微信小程序

小程序里是支持运行 JS 的,可是这里用到的 Dom 接口和渲染到浏览器上的功能小程序不具有,因此没法直接将 Web 端社区代码移植到小程序中。缘由就在于小程序为了安全和性能而采用了双线程的架构,运行用户 JS 代码的逻辑层是一个纯粹的 JSCore,没有任何浏览器相关的实现,这里得想办法将 Web 端代码转成小程序代码。浏览器

那么问题来了,如何将 Vue 代码转成小程序代码?这里先看下业界常见的作法:将 Vue 模板直接转成小程序的 WXML 模板。

使用作法至关于抛弃了浏览器中建 Dom 树的过程,而是直接交由小程序来对模板进行编译建立出小程序的模板树,进而渲染到小程序页面中。

通常来讲这个作法对于普通场景是够用的,可是对于一些更复杂的场景就很很差处理了,好比社区中的一个简单例子:社区帖子详情展现富文本内容,点击内容中的图片可预览。

这主要是由于 Vue 模板和 WXML 模板的语法并非直接对等的,Vue 的特性设计也和小程序的设计没法划等号,这天然就致使了部分 Vue 特性的丢失。好比像 Vue 中的 v-html 指令、ref 获取 Dom 节点、过滤器等就统统用不了。固然不止是 Vue 自身的特性,一些本来依赖 Dom/Bom 接口的 Vue 插件也没法使用,好比 Vue-router 等,而这些正是社区高度依赖的,在不对社区代码作大范围改造的话是没法使用此方案的。

此路不通,那还有其余的方法么?

答案是有的,这里咱们就得换一种思路来解决这个问题。回到最初的点上,咱们没法将 Web 端代码移植到小程序中是由于小程序没有 Dom 接口,那么咱们想办法作出一个适配层,将这个差别给抹掉不就好了么?

有了想法就要实施,仿造出 Dom 接口并不难,事实上在 Nodejs 端就有人作过相似的事,好比 jsDom 这个库的实现,让咱们能够在没有真实浏览器环境下能够对一些依赖 Dom 接口的 Web 端代码进行测试。

仿造了 Dom 接口给 Vue 调用,进而建立出了仿造 Dom 树。根据前面提到的小程序架构,用户的 JS 代码是执行在逻辑层的,也就是说咱们建立出的 Dom 树也是存在与逻辑层的内存之中,接下来要解决的难题是如何将这棵 Dom 树渲染到小程序页面中。

这里须要先简单介绍一下小程序的渲染原理:小程序的双线程架构,逻辑层会执行用户的 JS 代码进而产生一组数据,这组数据会发往视图层;视图层接收到数据后,结合用户的 WXML 模板建立出组件树,以后小程序再将组件树渲染出来。这里的组件树和 Dom 树很相似,只是它是由官方内置组件或自定义组件拼接而成而不是 Dom 节点。这里咱们能不能将仿造出来的 Dom 树映射到小程序的组件树上?

小程序组件树是根据 WXML 模板建立出来的,而仿造 Dom 树结构是不稳定的,咱们没法提早预知它会生成什么样的结构,也就没法提早准备后能够描述任意 Dom 树的 WXML 模板,除非直接将 Vue 模板转换成 WXML 模板,但这样又绕回前面的问题上了。

小程序组件树中的组件有两种:内置组件和自定义组件,内置组件是由官方提供的如 video、map 这样的组件,而自定义组件是一种支持由用户利用现有组件自行组装的组件,可否利用它来作些什么?

使用 Web 端概念来作个简单解释,内置组件就像是 div、span 这些 HTML 标签,而自定义组件就像是 Web 中的 Vue 组件。Vue 组件能够将 HTML 标签以及其余的 Vue 组件进行组装,自定义组件同理,主要用于功能模块的抽象、封装和复用。不过自定义组件有个很奇妙的特性,它支持自引用,也就是说它能够本身引用本身来进行组装。

自定义组件能够本身引用本身,那么咱们就能够利用这个特性来进行递归建立组件,进而建立出一棵组件树:

好比上图的例子,咱们封装了一个 custom-dom 组件,这个组件里面也使用了 custom-dom 组件用于渲染子组件。那么只要咱们执行一下 setData,把 children 数据传递过去就能够建立出子组件,子组件自己也是 custom-dom 组件,它一样能够执行这个逻辑把各自的子组件建立出来,这样就实现了组件的递归建立,只要咱们拥有完整的 Dom 树结构,就能够建立出相对应的一棵组件树。

这里递归的终止条件是遇到特定节点、文本节点或者孩子节点为空。而后在建立出组件树后,将 Dom 节点和自定义组件实例进行绑定以便后续的 Dom 更新和操做便可。

接下来,若是用户在界面上进行了操做,触发了一些事件的话,那么代码中要如何监听这些事件呢?小程序自己有本身的事件系统,它和 Web 端事件系统相似,可是出于如下几个缘由致使咱们没法直接使用小程序的事件系统:

  1. 小程序支持的事件表现和 Web 端不一致,好比 input 事件在小程序中不可冒泡。

  2. 小程序的捕获冒泡是在 Webview 端,所以逻辑层在整个捕获冒泡流程中各个节点接收到的事件不是同一个对象。

  3. 小程序事件对象和 Web 端事件对象结构不同。

  4. 小程序事件的捕获冒泡以及阻止冒泡等操做必须在 WXML 模板中声明,没法使用接口实现。

  5. 小程序自己是基于 Web Component 特性来实现的组件体系,其事件来源只能断定来自于当前 shadow tree 下的哪一个节点,而不能跨 shadow tree 判断。

综上所述,最好的解决方法就是把事件系统也仿造一份,在仿造 Dom 树上进行捕获冒泡。当自定义组件监听到用户的操做后,就将事件发往仿造 Dom 树,后续自定义组件监听到的同一个事件的冒泡就直接忽略。而 Dom 树接收到事件后,再进行捕获和冒泡,让事件在各个节点触发,这样的话整套体系均可以按照 Web 端的方式进行实现,对于用户来讲,只管按照 Web 端的用法来进行事件监听便可。

整套方案的大体思路即是如此,接下来介绍几个实现过程当中比较重要的细节,其一:如何将 Dom 树传递给视图层?

这其实就是自定义组件要如何作 setData 的问题。咱们一开始想到的方式是直接将整棵 Dom 树传递给自定义组件,而后自定义组件在递归建立子组件时一步步透传下去。这个作法的好处是一劳永逸,只有在最顶层的自定义组件须要管理 Dom 树和 setData,其余自定义组件只管接收数据进行渲染便可,可是这样也带了问题:每次更新须要作大范围的 diff,由于 setData 是从根组件发起的;当遇到一些局部更新时可能须要 setData 大量的数据,也就是会传输一些没必要要的数据。

那么天然而然的,咱们便想到让每一个自定义组件只 setData 当前节点的数据,每一个自定义组件只考虑当前绑定的 Dom 节点,而后建立出子节点,这样虽然会增长 setData 的数量,可是带来的好处即是能够作到最小范围 diff,同时每次 setData 的数据量也能够降到最小。

细节其二:自定义组件实例的建立实际上是会有比较大开销的,有没有办法减小一些自定义组件实例的建立?

按照先前的构想,一个自定义组件绑定一个 Dom 节点,因此自定义组件实例数量等于 Dom 节点数量。

其中一个思路是对 Dom 节点进行删减,这个实现比较简单,只要是不展现在页面上的节点,直接从 Dom 树上干掉就能够了,这样自定义组件数量也会相应减小。

另外一个思路是调整映射关系,让一个自定义组件绑定多个 Dom 节点。咱们能够对 Dom 树按照必定规则进行裁剪,拆分红多棵子树,而后每一个自定义组件管理一棵子树,这样的话也能够减小大部分自定义组件的建立。

除此以外,咱们能够考虑对叶子节点也进行一些处理。咱们使用自定义组件来渲染的初衷就是为了能够动态递归建立出子节点,而当一个节点没有子节点的状况下,咱们就不须要使用自定义组件来渲染了,因此叶子节点能够合并到父级棵子树中(如上图的蓝色节点合并到黄色节点所在的子树中),直接使用 view 内置组件来渲染便可。

固然还有其余的一些细节,好比 Dom 对象复用、对象延迟建立等等,这里就不一一展开说明了,有兴趣的朋友能够经过源码来了解。

对于这个方案,性能也须要有必定的保证,咱们随机模拟了一些相似社区首页的 Dom 树,对其首次渲染耗时进行测算,其对好比下:

能够看到在 500 节点内的两个方案自己性能差很少,不过由于自定义组件实例建立的开销,在千节点往上的状况下会落后于静态模板方案,由于 Kbone 自己是经过牺牲性能来换取更全面的 Web 端兼容,而一般一个小程序页面的节点数在 100-500 这个区间浮动,所以这个表现是符合预期的。

以上就是 Kbone 这个适配器方案的大体设计思路,咱们将其概括为两个模块:仿造接口和自定义组件。正由于这个方案是经过提供适配器的方式来仿造出 Web 环境,因此用户代码不须要作任何魔改,大部分特性均可以继续使用不须要被删减,好比 vue-router、window.location 操做等。

3、具体应用效果

方案部分以及介绍完毕,接下来讲说这个方案要如何应用到咱们一开始的背景——微信开放社区上。

前面有简单提到,本来 Web 端代码是基于 Vue 来搭建的,其中还用到了诸多插件/库,如 Vue-router、Vuex、Markdown-it 等,同时还支持了服务端渲染。可是无论 Web 端是怎么实现的,底层终究是调浏览器的那些接口,因此对于用户层面的代码咱们不作任何调整,只是将浏览器那一层替换掉便可。

整个构建流程是基于 Webpack 来实现的,使用 Kbone 构建出小程序代码也是基于 Webpack 来实现,只须要在本来 Web 端构建流程上实现一个 Webpack 插件,在构建本来 Web 端代码到小程序端时追加 Kbone 和一些小程序相关的代码便可。

在整套方案应用的过程当中,确定也会有些定制化的需求,好比但愿小程序端头部和 H5 端不一样,不一样端使用不一样的交互设计:

咱们能够构建的时候就注入环境变量,在小程序端将 process.env.isMiniprogram 设为 true,这样用户代码层面能够经过判断这个变量来判断不一样环境,进而执行不一样的逻辑。

除此以外,还但愿使用小程序的一些特性,好比小程序端支持使用小程序的分享,那么除了上述的环境变量外,还须要用到小程序的 button 内置组件来实现分享按钮。在 Kbone 上可使用一个特殊的标签 wx-button 来表示 button 内置组件,在调 Kbone 的仿造 Dom 接口时会将其 wx- 前缀的标签识别成内置组件,进而进行特殊处理。

整个社区小程序的功能完善以后,便要思忖一下代码体积的问题,由于小程序自己有个 2M 限制。缩减代码体积的方式你们应该都了解了不少了,如:压缩混淆、代码分割和公共代码复用、tree shaking、使用分包等等。

还有就是考虑到小程序端是直接复用 Web 端代码,可是并非全部 Web 端代码都须要在小程序端作到,那么在处理模块依赖时能够作点手脚。由于都使用的 Webpack 构建,因此能够编写一个 loader,在 import/require 的时候追加上,它能够根据前面注入的环境变量来判断要不要将代码进行打包。

这样就能够很方便地指定哪些代码不要构建到小程序端。

总体实现出来的效果以下,左边是 H5 端,右边是小程序端:

Web 端连接:developers.weixin.qq.com/community/d…

小程序码:

4、总结

这一整套方案的实现和应用大体如此,其原理并不算复杂,只是用了另外一种思路来实现。目前这一套方案即名为 Kbone,现已整理并开源到 GitHub 上:github.com/wechat-mini…

考虑到这个方案自己是经过最底层的适配方式来完成同构,那么除了 Vue 外,它其实也能够很轻松地移植到其余的 Web 框架上,好比 React、Preact、Omi 等,下面是一些基于这些框架的简单 demo:

在上述 GitHub 仓库内也能够找到这些框架的 demo,尽管各个 Web 框架的实现、语法都有所不一样,但毕竟其本质上是相同的,最终都会转化为 Dom 接口调用来渲染页面。

也正因如此,能够看到 Kbone 这套方案最大的优点:扩展性强、对各个特性的支持全面、对代码编写的要求少以及自由度高、不须要魔改 Web 框架的底层实现,这样对于代码的维护、升级也都更为简单方便。

个人分享就到这里了,谢谢各位!

5、群内QA


Q:目前支持到vue那个版本?Vue3.0支持吗?

A:目前主要的测试用例都是 vue 2.x 版本,大部分特性都能完整使用。vue 3.x 版本的支持在规划中,由于尚未完整的测试还不清楚直接上 vue 3.x 版本会有哪些坑,不过理论上只要底层仍旧是调用那些基础的 dom 接口,那就是支持的。


Q:小程序的插件支持吗?

A:插件目前暂不支持。


Q:请问wxs支持吗?

A:wxs 目前暂不支持,使用 wxs 有不少状况下就是为了实现过滤器和一些简单的纯函数句柄,这些 vue 自己就已经支持了,就不是颇有必要再使用 wxs 了,否则再反向兼容到 Web 端就会很困难。wxs 响应动画 =》 wxs 响应事件来实现动画

不过 wxs 响应动画这块是一个性能优化点,这个将来会考虑支持的。


Q:小程序原生对位置经纬度的获取好像不太精准,有其余好的处理方案吗?这个在我毕业设计的答辩中差点翻车。

A:增长了高精度定位的参数



Q:这块的实现对小程序事件响应的性能有影响吗?「 综上所述,最好的解决方法就是把事件系统也仿造一份,在仿造 Dom 树上进行捕获冒泡。当自定义组件监听到用户的操做后,就将事件发往仿造 Dom 树,后续自定义组件监听到的同一个事件的冒泡就直接忽略。而 Dom 树接收到事件后,再进行捕获和冒泡,让事件在各个节点触发,这样的话整套体系均可以按照 Web 端的方式进行实现,对于用户来讲,只管按照 Web 端的用法来进行事件监听便可。」

A:和原生的小程序事件相比会有一点损耗但影响不大,小程序事件自己也不是直接使用 Web 端的事件冒泡机制,而是在视图层的组件树上本身实现的一套事件系统进行冒泡。kbone 的作法至关于把最初的那一个事件接过逻辑层来本身作一遍 Dom 树上的冒泡,后续小程序本身的冒泡事件就忽略掉。简单来讲,至关于把冒泡这一套流程从视图层拿到逻辑层来作。


Q:小程序开放接口或小程序独有的API(例如:受权,文件操做等),应该如何处理?直接再vue中使用wx.***吗?

A:是的,小程序环境的接口直接照常使用便可,好比 wx.xxx 等接口。可是若是要同构兼容到 Web 端的话,可能须要判断一下环境,一般咱们能够在构建时注入一个 process.env.isMiniprogram,这样在 Vue 代码里就能够经过判断环境来作兼容处理。后续这边也会尝试提供一些兼容两个环境的 API,好比现有的 wx.setStorage 等就能够直接使用 localStorage 来代替,kbone 底层会将 localStorage 的实现转成 wx.setStorage 等 API。


Q: kbone有开发交流群或者客服群吗?

A:这个先前也有人提过,在近期会提供开发交流群来方便开发者们交流。

本文是Kbone做者june在云加社区微信群中的分享整理总结而成,加群请关注「云加社区」公众号,回复“加群”。

相关文章
相关标签/搜索