Mpx2.8 版本正式发布,使用组合式 API 开发小程序

作者:hiyuki

小程序跨端开发框架 Mpx 自18年立项开源以来,如今已经走过了第四个年头,其高性能、优体验、跨平台的特性收获了公司内外开发者用户的一致好评。

为了不辜负开发者用户对我们的信赖,更好地支持集团小程序业务开发,一方面我们对 Mpx 的稳定版本进行着高频的维护迭代,快速响应处理集团内外开发者用户在框架开发使用过程中遇到的问题;另一方面我们持续跟进探索业内最新动态,力争将更新更好的开发能力与体验带给小程序开发者用户。继年初我们在 2.7 版本中对 Mpx 的编译系统进行重构适配 Webpack5,基于持久化缓存大幅提升编译速度后,在最新的 2.8 版本中,我们对 Mpx 的运行时框架也进行了大量重构改造工作,完整支持了 Vue3 提出的组合式 API 开发范式,让用户能够使用当下最热门的开发方式进行小程序开发,我们先来简单感受一下组合式 API 的使用:



可以看出和 Vue3 组合式 API 的使用是高度类似的,利用框架导出的一系列响应式 API 和 生命周期钩子函数在 setup 中编写业务逻辑,并将模板依赖的数据与方法作为返回值返回,与传统的选项式 API 相比,组合式 API 具备以下优势:

  • 更好的逻辑复用,通过函数包装复用逻辑,显式引入调用,方便简洁且符合直觉,规避消除了 mixins 复用中存在的缺陷;
  • 更灵活的代码组织,相比于选项式 API 提前规定了代码的组织方式,组合式 API 在这方面几乎没有做任何限制与规定,更加灵活自由,在功能复杂的庞大组件中,我们能够通过组合式 API 让我们的功能代码更加内聚且有条理,不过这也会对开发者自身的代码规范意识提出更高要求;
  • 更好的类型推导,虽然基于 this 的选项式 API 通过 ThisType 类型体操的方式也能在一定程度上实现 TS 类型推导,但推导和实现成本较高,同时仍然无法完美覆盖一些复杂场景(如嵌套 mixins 等);而组合式 API 以本地变量和函数为基础,本身就是类型友好的,我们在类型方面几乎不需要做什么额外的工作就能享受到完美的类型推导。

同时与 React Hooks 相比,组合式 API 中的 setup 函数只在初始化时单次执行,在数据响应能力的加持下大大降低了理解与使用成本,基于以上原因,我们决定为 Mpx 添加组合式 API 能力,让用户能够用组合式 API 方式进行小程序开发。

组合式 API 实现

从上面的简单示例中可以看出,抛开响应式 API 和生命周期注册模式的变化,组合式 API 的实现要点在于动态添加模板依赖的数据和方法,这也是我们在小程序中实现组合式 API 可能遇到的核心技术卡点。

对于动态添加模板依赖数据,我们在过去的实践中已经充分证明了其可行性,事实上,从 Mpx 最初的版本开始,我们就充分利用了这项能力来实现我们对计算属性和 dataFn (类似于 Vue 使用函数定义 data)的支持,这项能力的关键在于存在合适的生命周期用于动态添加初始化数据,这里对于初始化数据的定义是能够影响组件树的初始渲染,举个简单的例子:存在一对父子组件 parent/child,parent 使用 props 向 child 传递数据,当我们在 parent 初始创建时使用 setData 动态添加 props 数据,同时 child 在初始创建时能够通过 props 正确获取到这部分的数据时,我们就可以将这部分动态添加的数据视作初始化数据,这是我们在小程序中实现完备数据响应支持的基础。

幸运的是,目前业内所有主流小程序平台(微信/支付宝/百度/字节/QQ)都支持了上述能力,微信从一开始就支持在 attached 生命周期中调用 setData 函数动态添加初始化数据,在上述的父子 props 传递场景中,也能够在子组件的 attached 中正确获取这部分数据,支付宝和字节小程序一开始并不支持该能力,不过支付宝在 component2 组件系统重构后,字节在橙心合作项目中与我们沟通后,都成功支持了该能力。

而对于动态返回的方法,最简单能想到的方案就是直接挂载到组件实例上,经过我们的完整测试,上述业内主流小程序平台都支持使用这种方式动态添加方法,基于以上事实,我们非常确定组合式 API 能够在小程序环境中顺利实现,下图简要展示了 Mpx 支持组合式 API 的初始化流程:

Mpx2.8 版本正式发布,使用组合式 API 开发小程序_第1张图片

生命周期钩子函数

在组合式 API 中,setup 函数只有在组件创建时初始化单次执行,因此需要提供一系列生命周期钩子函数来代替选项式 API 中的生命周期钩子选项,由于小程序原生只支持选项式的生命周期注册方式,我们通过预注册 -> 驱动的方式来实现 setup 中函数式注册生命周期钩子的语法糖,简单来讲就是使用选项式 mixins 的方式提前注册所有需要的生命周期钩子,在选项式生命周期钩子执行时驱动对应在 setup 中使用生命周期钩子函数注册的代码逻辑执行,如下图所示:

Mpx2.8 版本正式发布,使用组合式 API 开发小程序_第2张图片

作为跨端小程序框架,Mpx 需要兼容不同小程序平台不同的生命周期,在选项式 API 中,我们在框架中内置了一套统一的生命周期,将不同小程序平台的生命周期转换映射为内置生命周期后再进行统一的驱动,以抹平不同小程序平台生命周期钩子的差异,如微信小程序的 attached 钩子和支付宝小程序的 onInit 钩子,在组合式 API 中,我们沿用了同样的逻辑,设计了一套与框架内置生命周期对应的生命周期钩子函数,以相同的方式进行驱动,因此这些生命周期钩子函数天然具备了跨平台特性,下表显示了在组件 / 页面中框架生命周期与原生平台生命周期的对应关系:

框架内置生命周期 Hooks in setup 微信原生 支付宝原生
BEFORECREATE null attached(数据响应初始化前) onInit(数据响应初始化前)
CREATED null attached(数据响应初始化后) onInit(数据响应初始化后)
BEFOREMOUNT onBeforeMount ready(MOUNTED 执行前) didMount(MOUNTED 执行前)
MOUNTED onMounted ready(BEFOREMOUNT 执行后) didMount(BEFOREMOUNT 执行后)
BEFOREUPDATE onBeforeUpdate nullsetData 执行前) nullsetData 执行前)
UPDATED onUpdated nullsetData callback) nullsetData callback)
BEFOREUNMOUNT onBeforeUnmount detached(数据响应销毁前) didUnmount(数据响应销毁前)
UNMOUNTED onUnmounted detached(数据响应销毁后) didUnmount(数据响应销毁后)
ONLOAD onLoad onLoad onLoad
ONSHOW onShow onShow onShow
ONHIDE onHide onHide onHide
ONRESIZE onResize onResize events.onResize
同 Vue3 一样,Mpx 在组合式 API 中没有提供 BEFORECREATECREATED 对应的生命周期钩子函数,用户可以直接在 setup 中编写相关逻辑。

具有副作用的页面事件

在小程序中,一些页面事件的注册存在副作用,即该页面事件注册与否会产生实质性的影响,比如微信中的 onShareAppMessageonPageScroll,前者在不注册时会禁用当前页面的分享功能,而后者在注册时会带来视图与逻辑层之间的线程通信开销,对于这部分页面事件,我们无法通过预注册 -> 驱动方式提供组合式 API 的注册方式,用户可以通过选项式 API 的方式来注册使用,通过 this 访问组合式 API setup 函数的返回。

然而这种使用方式显然不够优雅,我们考虑是否可以通过一些非常规的方式提供这类副作用页面事件的组合式 API 注册支持,例如,借助编译手段。我们在运行时提供了副作用页面事件的注册函数,并在编译时通过 babel 插件的方式解析识别到当前页面中存在这些特殊注册函数的调用时,通过框架已有的编译 -> 运行时注入的方式将事件驱动逻辑添加到当前页面当中,以提供相对优雅的副作用页面事件在组合式 API 中的注册方式,同时不产生非预期的副作用影响,简单示例如下:

import { createPage, ref, onShareAppMessage } from '@mpxjs/core'

createPage({
  setup () {
    const count = ref(0)

    onShareAppMessage(() => {
      return {
        title: '页面分享'
      }
    })

    return {
      count
    }
  }
})

目前我们通过这种方式支持的页面事件如下:

页面事件 Hooks in setup 平台支持
onPullDownRefresh onPullDownRefresh 全小程序平台 + web
onReachBottom onReachBottom 全小程序平台 + web
onPageScroll onPageScroll 全小程序平台 + web
onShareAppMessage onShareAppMessage 全小程序平台
onTabItemTap onTabItemTap 微信/支付宝/百度/QQ
onAddToFavorites onAddToFavorites 微信 / QQ
onShareTimeline onShareTimeline 微信
onSaveExitState onSaveExitState 微信
特别注意,由于静态编译分析实现方式的限制,这类页面事件的组合式 API 使用需要满足页面事件注册函数(如onShareAppMessage)的调用和 createPage 的调用位于同一个 js 文件当中。

关于生命周期钩子函数的更多信息可以查看这里

可以看到使用方式与 Vue3 基本一致,不过由于 Mpx 的组合式 API 设计实现与 Vue3 存在差异,对应

上面示例代码看上去像是我们在模板上直接调用 setup() 返回的 t 翻译方法,但是熟悉小程序开发的同学都知道在小程序架构下这是不可能的,示例中的写法其实由框架通过编译 + 运行时手段实现的语法糖,我们会在模板编译时定向扫描 t/te/tm 等 i18n 方法,将其转换为计算属性注入到运行时当中,这就意味着如果我们对翻译方法进行重命名,模板编译时无法识别出 i18n 方法调用,自然也就无法正常运行。

Mpx 中 i18n 提供了两种实现模式,分别是 wxs 和 computed,可以使用编译选项中的 i18n.isComputed 进行切换,两种方式各有优劣,其中:

  • wxs 模式的优势在于逻辑层和视图层独立维护语言集,无额外运行时性能开销,且使用没有任何限制;劣势同样源于语言集同时存在于逻辑层(js)和视图层(wxs)当中,这部分的包体积占用翻倍;
  • computed 模式的优势在于语言集只存在于逻辑层中,无额外包体积占用,且可以通过动态添加语言集的方式进一步减少包体积占用;劣势则是会产生额外的运行时性能开销,且使用上存在限制,模板调用时无法直接访问 wx:for 中的 itemindex

在组合式 API 中模板上使用 useI18n() 返回的翻译函数 t/te/tm 时,为了完整实现 useI18n API的功能,会强制使用 computed 模式进行实现,这也意味着该用法会受到 computed 模式使用限制的影响。不过当你不需要使用 useI18n 接受 messages 参数创建局部语言集作用域功能时,你也完全可以在模板中继续使用原有的 $t/$tc/$te/$tm 方法,这些方法受编译选项 i18n.isComputed 的影响,同时指向全局语言集作用域。

更多关于生态周边的组合式 API 使用指南可以点击下方链接查看详情:

输出 web 适配

跨端输出 web 作为 Mpx 的一大核心特性,在业务中存在广泛使用,同时也是我们设计实现任何框架新特性需要优先考虑的事项。在本次组合式 API 支持中,我们从设计之初就考虑了跨端输出 web 的适配支持,保障使用 Mpx 组合式 API 开发的业务代码都能在 web 环境中正常运行。

我们输出 web 的整体技术方向在于尽可能复用 Vue 已有的生态能力,为了实现这个目标,我们需要提供尽可能与 Vue 保持一致的 API 设计,以降低抹平适配成本。在输出 web 时,核心组合式 API 基于 Vue2.7 版本中的已有能力进行适配提供,简单举个例子:对于 import { ref } from 'mpxjs/core' 这行语句,在小程序中会指向 Mpx 内部维护的 ref 实现,而在输出 web 时会指向 Vue 中维护的 ref 实现,两者的实现虽然不仅相同,但只要保障对外函数签名一致,对于开发者用户来说就无感知。

我们借助了 Mpx 强大的条件编译能力进行上述实现,对运行时导出根据输出平台进行重定向,这样还能保障跨端输出产物干净简洁,仅包含当前输出环境下必要的逻辑,如下图所示:

Mpx2.8 版本正式发布,使用组合式 API 开发小程序_第5张图片

同理,我们也采用了类似的方式实现了组合式 API 周边能力对于输出 web 的适配支持,如pinia store 使用 pinia 原始版本进行适配实现,而 i18n 能力则是使用 [email protected] + vue-i18n-bridge 进行适配实现。

性能表现

性能是 Mpx 一直以来的核心关注点,我们对组合式 API 的最终实现版本进行了一系列性能评估测试,我们使用组合式 API 版本对业务中的评价组件进行了重构,评价组件属于我们业务中交互及功能相对比较复杂的组件,源码行数约 1000 行,组件数据 27 项,组件方法 18 个,我们在测试项目中对选项式 API 和组合式 API 两个版本实现的组件进行了一系列测试。

组件初始化耗时

由于组合式 API 改变了原有的组件初始化流程,我们对组件的初始化耗时进行了重点测试,测试口径如下:

  • 耗时计算以挂载组件为起点,以组件 ready 执行为终点
  • 测试结果为10次手工测试排除最大最小值后求均值
  • IOS 测试机型为 iPhone 13 pro max,安卓测试机型为 OPPO R9

结果显示两个版本的组件初始化耗时大抵持平,不分优劣。

IOS 安卓
选项式 API 42.5ms 366.6ms
组合式 API 42.4ms 370.1ms

组件 JS 体积

在构建产物体积方面,由于组合式 API 的写法对于 JS 代码压缩更加有利,同样的逻辑实现下,组合式 API 版本的组件构建压缩后 JS 体积略胜一筹。

组件 JS 体积
选项式 API 15.67KB
组合式 API 13.60KB

框架运行时体积

在 Mpx2.8 版本中,我们在框架运行时中新增了组合式 API 相关实现,不过通过优化运行时导出,使其对 tree shaking 更加友好,我们的框架运行时体积在实际构建产物中没有产生太大增长。

框架运行时体积
选项式 API 51.66KB
组合式 API 57.47KB

综上所述,组合式 API 版本的运行时性能与选项式 API 大抵持平,在包体积占用方面,新版框架运行时体积占用略有提升,不过由于组合式 API 开发模式对代码压缩更友好,加上组合式 API 更易进行逻辑复用的特点,我们预计在实际业务项目中,组合式 API 的包体积占用会更小。

破坏性改变

Mpx 组合式 API 版本完全兼容原有的选项式 API 开发方式,不过我们在运行时重构过程中依然带来了少量的破坏性改变,详情如下:

  • 框架过往提供的组件增强生命周期 pageShow/pageHide 与微信原生提供的 pageLifetimes.show/hide 完全对齐,不再提供组件初始挂载时必定执行 pageShow 的保障(因为组件可能在后台页面进行挂载),相关初始化逻辑一定不要放置在 pageShow 当中;
  • 取消了框架过去提供的基于内部生命周期实现的非标准增强生命周期,如 beforeCreate/onBeforeCreate 等,直接将内部生命周期变量导出提供给用户使用,详情查看这里;
  • 为了优化 tree shaking,作为框架运行时 default exportMpx 对象不再挂载 createComponent/createStore 等运行时方法,一律通过 named export 提供,Mpx 对象上仅保留 set/use 等全局 API,详情查看这里
  • 使用 I18n 能力时,为了与新版 vue-i18n 保持对齐,this.$i18n 对象指向全局作用域,如需创建局部作用域需要使用组合式 API useI18n 的方式进行创建。
  • watch API 不再接受第二个参数为带有 handler 属性的对象形式(该参数形式只应存在于 watch option 中),第二个参数必须为回调函数,与 Vue 对齐。

更详细的迁移指南请点击查看这里

未来规划

在完成编译持久化缓存和组合式 API 支持后,我们已经完成了去年规划中最大的两个技术升级,后续我们的技术规划如下:

  • 支持使用 Vite 进行 web 构建
  • 完善 Mpx 跨端输出 Hummer 并正式 release
  • 优化运行时 render 函数,降低包体积占用
  • 内置支持原子类使用
  • Mpx-cube-ui 正式开源

最后,再次感谢所有参与 Mpx 组合式 API 技术建设的同学们,也欢迎社区同学加入 Mpx 项目开源共建。

你可能感兴趣的:(前端小程序mpxvue.js)