滴滴开源小程序框架 Mpx 新特性:局部运行时能力增强

Mpx 是滴滴开源的一款增强型跨端小程序框架,自 2018 年立项开源以来如今已经进入第六个年头,在这六年间,Mpx 根植于业务,与业务共同成长,针对小程序业务开发中遇到的各类痛点问题提出了解决方案,并在滴滴内部建设了完善的小程序跨端开发生态。目前,Mpx 已经覆盖支持了滴滴内部全量小程序业务开发,成为了滴滴小程序开发的统一技术标准。

本文主要探讨MPX局部运行时能力增强的方案设计。如需深入了解滴滴开源项目MPX,请参阅相关文章:

滴滴开源小程序框架Mpx,致力于提高小程序开发体验

滴滴出行小程序体积优化实践

滴滴小程序框架Mpx发布2.0:可直接转换已有微信小程序

小程序框架Mpx的下一代脚手架升级之路|滴滴开源

滴滴小程序开发标准 Mpx 推出新版本,聚焦性能与包体积优化

目前在小程序社区当中存在两种小程序上层框架技术方向:

  1. 以 Mpx 为代表的重编译框架,源码采用小程序平台的dsl,编译后直接产出满足小程序规范的代码

  2. [email protected] 为代表的重运行时框架,源码可以使用 Vue、React 作为上层dsl,在运行时当中注入框架代码,同时 polyfill 掉小程序在逻辑线程当中并不能访问的 Dom、Bom 等相关 api

由于技术选型和设计思路的差异,小程序框架的发展及开发者实际开发过程中面临的问题侧重点也有所不同。

对于小程序框架自身的迭代:

  • 重运行时框架已具备现代Web框架开发的效率和体验,主要解决Web标准与上层框架和小程序平台规范的差异性和兼容性问题

  • 重编译框架遵循小程序平台的开发规范,核心在于增强小程序能力,提高开发效率和体验,尽量与现代Web框架能力对齐

Mpx 正是在小程序能力增强的设计思路下,努力与现代 Web 框架的开发体验和能力拉齐,以全面提升用户的开发效率、体验及小程序性能。例如,近两年发布了两个 minor 版本,分别支持了 webpack5 持久化缓存来大幅提高编译构建速度以及 composition-api 特性,让用户能够使用当下最热门且备受好评的开发范式。

在实际业务开发过程中,如果想要使用 props 透传、默认插槽、事件透传等现代 Web 框架提供的功能,但由于小程序自身框架设计未支持这些特性,开发者需要编写大量冗余代码才能达到相同的效果。因此,为了尽可能与现代 Web 框架能力拉齐,我们需要在运行时层面进行能力增强,以提高用户的开发效率和体验。这就是接下来要介绍的 mpx 局部运行时方案设计。

方案设计

在整体的技术方案设计上还是按照小程序能力增强的思路进行,以最小的组件粒度来按需开启运行时渲染。

滴滴开源小程序框架 Mpx 新特性:局部运行时能力增强_第1张图片

在具体实现上分为运行时和编译两部分内容。

在编译阶段主要完成:

  1. Render Function 增强

  2. 基础模板按需生成

  3. 胶水代码注入

在运行时阶段主要完成:

  1. propertiesToComputed

  2. vdom tree 生成&视图渲染

  3. 组件实例上下维护

  4. 事件代理&分发

滴滴开源小程序框架 Mpx 新特性:局部运行时能力增强_第2张图片

核心模块设计

render 函数增强

Mpx 在视图层处理上严格遵守小程序开发规范:一个Mpx SFC文件经过编译后,其处理完成的template将被输出为符合小程序语法规范的wxml静态文件。

在性能优化方面,Mpx 引入了 render 函数。目前,render 函数的主要功能是收集响应式数据。在组件初次渲染阶段,调用 render 函数完成响应式数据的收集。当响应式数据发生变化时,重新触发 render 函数的执行,完成变更后数据的收集,然后进行数据 diff,最后通过 setData 完成页面UI视图的更新。render 函数的形式如下:

global.currentInject = {
  moduleId: "m3f28ff60",
  render: function () {
    this._c("message", this.message);
    ({ tap: [["onViewTapBubble", 'b', "__mpx_event__"]] });
    this._i(this._c("navigatorList", this.navigatorList), function (item, index) {
      ({ tap: [["jumpTo", item]] });
      item.name;
    });
    this._r();
  }
}

在运行时增强方案中,Mpx SFC 文件经过编译后,对于 template 处理最大的不同在于经过编译后是处理为一个执行后会生成 vdom tree 的 render 函数。

在组件初次渲染过程中,执行这个 render 函数,不仅完成了响应式数据的收集,还生成了 vdom tree 。这个 vdom tree 描述了 template 的内容,并将它注入到运行时的 render 函数中。因此,在页面的初次渲染阶段,setData 设置的数据是整个 vdom tree ,它描述了整个模板视图的内容。然后通过自定义容器组件 mpx-custom-element 的递归渲染来完成整个组件的渲染。

global.currentInject = {
  moduleId: 'm3f28ff60',
  render: function () {
    var vnode = this.__c('block', [
      this.__c(
        'view',
        {
          class: this.__sc('root', undefined),
          mpxbindevents: {...},
          eventconfigs: {...}
        },
        [
          this._i(
            this._c('navigatorList', this.navigatorList),
            function (item, index) {
              return this.__c(
                'view',
                {...},
                [...]
              )
            }
          )
        ]
      )
    ])
    this._r(vnode)
  }
}

在组件初次渲染时,通过 setData 传递到渲染层的数据是全量的 vdom tree。当组件响应用户操作或接口请求导致组件实例上的响应式数据发生变化时,会触发 Render Function 的重执行,以生成一个新的 vdom tree。考虑到逻辑层与渲染层通信的成本和性能,这涉及到前后两次 vdom tree 的 diff。最终,生成具体的 vdom 路径数据更新内容,并通过 setData 传递到渲染层,完成视图的局部更新。

mpx-custom-element 容器组件

mpx-custom-element.mpx 是 mpx 内置的一个自定义组件,主要用于运行时渲染。其主要功能有两个:

  1. 作为基础容器组件承载模板渲染:mpx-custom-element.mpx 主要作为一个渲染容器,接收组件传递的需动态渲染的 composed tree 数据,并在其上下文中完成真实组件的渲染。需要注意的是,mpx-custom-element.mpx 本身也是一个基于模板动态渲染的组件,其最终编译输出的内容包含 wxml(视图层)/wxss(样式)/js(逻辑层)/json(配置)。

  2. 跳脱微信小程序模板递归渲染的限制:由于微信小程序的 template 能力不支持模板的自引用递归渲染,因此需要借助一层自定义组件容器来绕开这一限制,从而提供新的渲染上下文。

基础模板

所有模板动态化组件的视图都是基于 mpx-custom-element.mpx 提供的基础模板 mpx-custom-element.wxml 进行动态渲染的。在组件的编译阶段,会按需收集使用到的组件和属性,并将其输出到基础模板中,以确保基础模板的包体积最小化。

组件的 Render Function 执行后生成的 vdom tree 描述了组件的视图层结构和样式。最终,通过组件实例的 setData 方法将 vdom tree 传递到视图层,并利用这些基础模板进行动态渲染。