---
url: /articles/2.8-release.md
---
# Mpx2.8 版本正式发布，使用组合式 API 开发小程序

> 作者：[hiyuki](https://github.com/hiyuki)

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

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

```html
<template>
  <view>{{ collectionName }}: {{ book.title }}({{ readersNumber }})</view>
  <button bindtap="addReaders">addReaders</button>
</template>

<script>
  import { createComponent, ref, reactive, onMounted } from '@mpxjs/core'

  createComponent({
    properties: {
      collectionName: String
    },
    setup () {
      const readersNumber = ref(0)
      const book = reactive({ title: 'Mpx' })

      onMounted(() => {
        console.log('Component mounted.')
      })

      // 暴露给 template
      return {
        readersNumber,
        book,
        addReaders () {
          readersNumber.value++
        }
      }
    }
  })
</script>
```

可以看出和 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 的初始化流程：

![composition-api-init](https://dpubstatic.udache.com/static/dpubimg/0bxxw8bB9PJxXLeiMQ__6_composition-api-init.png)

### 生命周期钩子函数

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

![composition-api-hook](https://dpubstatic.udache.com/static/dpubimg/2nmOjxvubyk6H32yP5cFc_composition-api-hook.png)

作为跨端小程序框架，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 |`null`（`setData` 执行前）|`null`（`setData` 执行前）|
| UPDATED | onUpdated |`null`（`setData` callback）|`null`（`setData` 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 中没有提供 `BEFORECREATE` 和 `CREATED` 对应的生命周期钩子函数，用户可以直接在 `setup` 中编写相关逻辑。

#### 具有副作用的页面事件

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

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

```js
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 文件当中。

关于生命周期钩子函数的更多信息可以查看[这里](../guide/composition-api/composition-api.md#lifecycle-hooks)。

### `<script setup>`

同 Vue3 一样，我们在 `.mpx` 单文件组件 / 页面中实现了 `<script setup>` 的组合式 API 编译语法糖，相较于常规的写法，`<script setup>` 具备以下优势：

* 更少的样板内容，更简洁的代码
* 能够使用纯 TypeScript 声明 props 类型
* 更好的 IDE 类型推导性能

简单使用示例如下：

```html
<script setup>
  import { ref } from '@mpxjs/core'

  const msg = ref('hello')

  function log () {
    console.log(msg.value)
  }
</script>
<template>
  <view>msg: {{msg}}</view>
  <view ontap="log">click</view>
</template>
```

可以看到使用方式与 Vue3 基本一致，不过由于 Mpx 的组合式 API 设计实现与 Vue3 存在差异，对应 `<script setup>` 也与 Vue3 中存在一些差异：

* 不支持 `import` 快捷注册组件
* 没有 `defineEmits()` 编译宏
* 没有 `useSlots()` 和 `useAttrs()` 运行时函数
* 以编译宏的形式提供了 `useContext()`，获取 `setup` 函数的第二个参数 `context`
* `defineExpose()` 编译宏的作用与 Vue3 中有所差别，能够限定模板中能访问的变量范围

> **特别注意**，受小程序底层技术限制，我们在 Mpx 的实现中无法像 Vue3 那样将模板编译的渲染函数和 `<script setup>` 放置到同一作用域下进行变量访问，而是通过静态编译分析提取出 `<script setup>` 的顶层作用域变量，再以上文中提到的动态添加数据与方法的方式将其设置到模板当中，如果 `<script setup>` 中声明了较多顶层作用域变量，它们并不一定都会被模板访问，就会带来无效的性能开销，因此我们强烈建议使用 `defineExpose()` 限定模板中能访问的变量范围，你可以把它等同于 `setup` 函数中的 `return`。

关于 `<script setup>` 的更多信息可以查看[这里](../guide/composition-api/composition-api.md#script-setup)。

### 组合式 API 与 Vue3 中的差异

我们来总结一下 Mpx 中组合式 API 与 Vue3 中的区别：

* `setup` 的 `context` 参数不同，详见[这里](../guide/composition-api/composition-api.md#context)
* `setup` 不支持返回**渲染函数**
* `setup` 不能是异步函数
* `<script setup>` 提供的宏方法不同，详见[这里](../guide/composition-api/composition-api.md#script-setup)
* `<script setup>` 不支持 `import` 快捷注册组件
* 支持的生命周期钩子不同，详见[这里](../guide/composition-api/composition-api.md#lifecycle-hooks)
* 模板引用的方式不同，详见[这里](../guide/composition-api/composition-api.md#template-ref)

关于组合式 API 的更多信息可以查看[这里](../guide/composition-api/composition-api.md)。

## 响应式 API 实现

组合式 API 的正常工作离不开响应式 API 的支持，下面我们来聊聊 Mpx 中响应式 API 的设计实现。我们知道 Vue3 中响应式 API 基于 `proxy` 进行了重构实现，但是目前 `proxy` 的浏览器兼容占比仍然无法达到我们对于线上可用性的要求，因此在 Mpx 中，我们仍然基于 `Object.defineProperty` 进行核心数据响应能力的实现，同时借鉴了 Vue3 中优秀的代码设计与实现，如 `reactiveEffect` 和 `effectScope` 等，尽可能实现与 Vue3 中响应式 API 能力对齐。

说到这里，很多同学可能会想到 `@vue/composition-api` 这个库，该库提供的关键能力正是基于 Vue2 的数据响应系统模拟实现 Vue3 中的响应式 API，我们在前期也对 `@vue/composition-api` 在 Mpx 中的复用进行了非常有价值的探索尝试。不过最终我们还是决定在 Mpx 的运行时框架中进行独立实现，原因主要在于：`@vue/composition-api` 是作为一个 Vue2 插件存在，无法直接侵入 Vue2 源码，导致部分能力无法实现或会带来额外的性能开销，例如 `flush: 'post'` 和 `ref` 自动解包等。我们也看到在最新的 Vue2.7 版本中，也是在运行时框架里重新实现了这部分内容，以规避上述问题。

下图展示了 Mpx 中响应式 API 核心模块依赖关系：

![composition-api-reactive](https://dpubstatic.udache.com/static/dpubimg/4ySdEqTRhc_DRWWCJWrhJ_composition-api-reactive.png)

### 数据响应限制带来的差异

由于 `Object.defineProperty` 的能力限制，Mpx 存在和 Vue2 一致的数据响应限制，无法感知到对象 property 的添加和删除以及数组的索引赋值，与 Vue2 一致，我们暴露了 `set` 和 `del` API 来让用户显式地进行相关操作。除此之外，由于使用方式发生了变化，我们在使用 `reactive` API 创建响应式数据时，还会遇到新的限制，我们来看一下代码示例：

```js
import { reactive, watchSyncEffect, set } from '@mpxjs/core'

const state = reactive([0, 1, 2, 3])

watchSyncEffect(() => {
  console.log(JSON.stringify(state)) // [0,1,2,3]
})

set(state, 1, 3) // 不会触发 watchEffect

state.push(4) // 不会触发 watchEffect
```

可以看出，即使我们使用了 `set` API 和数组原型方法对数组进行修改，我们仍然无法监听到数据变化。

> 相同的限制在使用 `Object.defineProperty` 的 Vue2.7 中也同样存在。

为什么会存在这个限制呢？原因在于：基于 `Object.defineProperty` 实现的数据响应系统中，我们会对对象的每个已有属性创建了一个 `Dep` 对象，在对该属性进行 `get` 访问时通过这个对象将其与依赖它的观察者 `ReactiveEffect` 关联起来，并在 `set` 操作时触发关联 `ReactiveEffect` 的更新，这是我们大家都知道的数据响应的基本原理。但是对于新增/删除对象属性和修改数组的场景，我们无法事先定义当前不存在属性的 `get/set` (当然这在 `proxy` 当中是可行的)，因此我们会把对象或者数组本身作为一个数据依赖创建 `Dep` 对象，**通过父级访问**该数据时定义的 `get/set` 将其关联到对应的 `ReactiveEffect`，并在对数据进行新增/删除属性或数组操作时通过数据本身持有的 `Dep` 对象触发关联 `ReactiveEffect` 的更新，如下图所示：

![数据响应原理](https://dpubstatic.udache.com/static/dpubimg/XkI9cQBsLy_7np-T3rt56_composition-api-reactive2.png)

需要注意的是，**通过父级访问**是建立 `Dep` 与 `ReactiveEffect` 关联关系的先决条件，在选项式 API 中，我们访问组件的响应式数据都需要通过 `this` 进行访问，相当于这些数据都存在 `this` 这个必要的**父级**，因此我们在使用 `$set/$delete` 进行对对象进行新增/删除属性或对数组进行修改时都能得到符合预期的结果，唯一的限制在于不能新增/删除根级数据属性，原因就在于 `this` 不存在访问它的父级。

但是在组合式 API 中，我们不需要通过 `this` 访问响应式数据，因此通过 `reactive()` 创建的响应式数据本身就是根级数据，我们自然无法通过上述方式感知到根级数据自身的变化（在 Vue3 中，基于 `proxy` 提供的强大能力响应式系统能够精确地感知到数据属性，甚至是当前不存在属性的访问与修改，不需要为数据自身建立 `Dep` 对象，自然也不存在相关问题）。

在这种情况下，我们就需要用 `ref()` 创建响应式数据，因为 `ref` 创建了一个包装对象，我们永远需要通过 `.value` 来访问其持有的数据（不管是显式访问还是隐式自动解包），这样就能保证 `ref` 数据自身的变化能够被响应式系统感知，因此也不会遇到上面描述的问题，如下所示：

```js
import { ref, watchSyncEffect, set } from '@mpxjs/core'

const state = ref([0, 1, 2, 3])

watchSyncEffect(() => {
  console.log(JSON.stringify(state.value)) // [0,1,2,3]
})

set(state.value, 1, 3) // [0,3,2,3]

state.value.push(4) // [0,3,2,3,4]
```

### 响应式 API 与 Vue3 中的区别

我们来总结一下 Mpx 中响应式 API 与 Vue3 中的区别：

* 不支持 `raw` 相关 API（`markRaw` 除外，我们提供了该 API 用于跳过部分数据的响应式转换）
* 不支持 `readonly` 相关 API
* 不支持 `watchEffect`、`watch`、`computed` 的调试选项
* 不支持对 `map`、`set` 等集合类型进行响应式转换
* 受到 `Object.defineProperty` 实现带来的数据响应限制影响

关于响应式 API 的更多信息可以查看[这里](../guide/composition-api/reactive-api.md)。

## 生态周边适配

除了 Mpx 运行时核心提供了组合式 API 支持外，我们对 Mpx 的周边生态能力也都进行了组合式 API 适配支持，包括 `store`、`i18n`、`fetch` 等。

### Pinia store 支持

Pinia 是基于组合式 API 设计的全新数据管理方案，目前已经取代 Vuex 成为 Vue3 官方推荐的 store，我们在研究了 pinia 的设计实现后，对其简练优雅的设计思想及其与组合式 API 的高度适配非常满意（特别是在使用 `setup` 函数创建 store 时，使用心智与编写组件完全一致，可以将其视作是没有视图的组件）。因此我们 fork 了 pinia 的源码仓库，基于 Mpx 提供的数据响应能力对其进行了适配改造，使其在 Mpx 环境下也能正常运行，目前相关代码维护在 `@mpxjs/pinia` 中，在组合式 API 中的简单使用示例如下：

```js
import { createComponent, ref, computed, toRefs } from '@mpxjs/core'
import { defineStore } from '@mpxjs/pinia'

// 使用组合式 API 创建 pinia store 的使用心智与 setup 函数完全一致，强烈推荐
const useSetupStore = defineStore('setup', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)

  function increment () {
    count.value++
  }

  return { count, doubleCount, increment }
})

createComponent({
  setup () {
    const store = useSetupStore()
    // store 同 props 类似是一个 reactive 对象，解构数据需使用 toRefs 以保持数据响应性
    const { count, doubleCount } = toRefs(store)
    // 方法可以直接解构
    const { increment } = store
  
    return { count, doubleCount, increment }
    //
  }
})
```

Mpx 中通过 `createStore` 创建的类 Vuex store 在组合式 API 中仍然可以使用，我们可以在 `setup` 函数中引用 store 实例进行[数据读取与方法调用](../guide/advance/store.md#use-store-in-composition-api)，不过整体使用体验与 pinia store 存在较大差距，我们还是推荐在组合式 API 开发中优先使用 pinia store 作为数据管理方案。

### I18n 支持

传统选项式 API 中，我们使用 `this.$t` 方法在组件内调用翻译函数，但在组合式 API 中我们无法访问 `this`，为此我们参考了 Vue I18n 最新的 9.x 版本，该版本针对 Vue3 及组合式 API 进行了重构适配，提供了全新的 `useI18n` API，简单使用示例如下：

```html
<template>
  <view>{{t('message.hello')}}</view>
  <button bindtap="changeLocale">change locale</button>
</template>

<script>
  import { createComponent, useI18n } from '@mpxjs/core'

  createComponent({
    setup () {
      // useI18n 不传参数时指向全局 i18n 对象，也可以传递 locale 和 messages 配置创建局部 i18n 对象
      const { t, locale } = useI18n()

      function changeLocale () {
        locale.value = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
      }
      // 返回的翻译方法名必须为 t，不能进行重命名
      return { t, changeLocale }
    }
  })
</script>
```

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

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

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

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

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

* [store](../guide/advance/store.md#use-store-in-composition-api)
* [i18n](../guide/advance/i18n.md#composition-api-usage)
* [fetch](../guide/extend/fetch.md#composition-api-usage)

## 输出 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 强大的条件编译能力进行上述实现，对运行时导出根据输出平台进行重定向，这样还能保障跨端输出产物干净简洁，仅包含当前输出环境下必要的逻辑，如下图所示：

![composition-api-web](https://dpubstatic.udache.com/static/dpubimg/E0FojaPKOxZOa5h5OEPQk_composition-api-web.png)

同理，我们也采用了类似的方式实现了组合式 API 周边能力对于输出 web 的适配支持，如pinia store 使用 pinia 原始版本进行适配实现，而 i18n 能力则是使用 vue-i18n@8.x + 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|

### 框架运行时体积 {#framework-runtime-size}

在 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` 等，直接将内部生命周期变量导出提供给用户使用，详情查看[这里](../api/global-api.md#built-in-lifecycle-variable);
* 为了优化 tree shaking，作为框架运行时 `default export` 的 `Mpx` 对象不再挂载 `createComponent/createStore` 等运行时方法，一律通过 `named export` 提供，`Mpx` 对象上仅保留 `set/use` 等全局 API，详情查看[这里](../api/global-api.md#global-mpx)；
* 使用 I18n 能力时，为了与新版 vue-i18n 保持对齐，`this.$i18n` 对象指向全局作用域，如需创建局部作用域需要使用组合式 API `useI18n` 的方式进行创建。
* `watch` API 不再接受第二个参数为带有 `handler` 属性的对象形式（该参数形式只应存在于 `watch` option 中），第二个参数必须为回调函数，与 [Vue](https://vuejs.org/api/reactivity-core.html#watch) 对齐。

更详细的迁移指南请点击查看[这里](../guide/migrate/2.8.md)。

## 未来规划 {#future-plan}

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

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

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