简介:本文带大家深入理解组合式 API 的设计详情,同时加入我们的实践经验总结。
作者:刘斌
Vue3.x
版本的出现带来了许多令人眼前一亮的新特性,其中组合式 API(Composition API),一组附加的、基于功能的 API 被作为一种新的逻辑复用和代码组织的方式提供给了开发者,提供更加灵活的组合组件逻辑能力。同时组合式 API 通过使用简单的变量和函数,也提供了更好的类型推断,这使得通过新的 API 编写的代码即使不用 TypeScript 也可以通过 IDE 的支持方便的获得类型提示。组合式 API 具体的设计动机和详情可以参考它的 RFC。本文带大家深入理解组合式 API 的设计详情,同时加入我们的实践经验总结。
作为最重要的一项变动,Vue3.x
引入了组合式 API,它是一组附加的、基于功能的 API,用于灵活的组合组件逻辑。主要通过 setup
的生命周期进行组件的初始化,参考以下组合式 API 的基本使用方法:
<template>
<button @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</button>
</template>
<script>
import {reactive, computed} from 'vue';
export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
});
function increment() {
state.count++;
}
return {
state,
increment
};
}
};
</script>
组合式 API 的使用,官方参考文档和 API 文档有详细说明,这里不过多说明。为方便大家理解和把握,根据官方的 API 文档整理了全景图。
Vue 组合式 API 可以分为五大块,涉及共计 31 个:
数据响应(复杂对象):响应性 API 基础,支持复杂对象 Object、Array 的数据响应;
数据响应(内部值):内部值指 JS 内置的简单数据结构,包括 String、Number 等;
computed 和 watch:基于 Ref 的数据计算与监听;
生命周期:对原生命周期封装;
其他 API:重要支持性 API;
Vue 的优点在于能够简单快速的搭建中小型应用项目。然而随着 Vue 如今生态的快速发展,项目的规格也变得越来越大。而受限于 Vue 当前的 API 的设计模式,不同开发者在维护同一个项目的时候,以下问题就显露出来了:
随着功能的逐渐丰富,复杂组件的代码变得愈发难以理解,尤其是当开发者在阅读其他人编写的代码的时候。因为 Vue 现存的 API 使得代码通过 options
的方式来组织,相同逻辑的代码分散到不同的 options 里,不符合就近原则
,因此通过逻辑上就近的原则来考虑来组织代码更加合理;
多个组件之间缺乏优雅、低成本复用逻辑的机制;现在的 Mixin 混合
组件逻辑的方式,存在来源不清晰的问题;
而组合式 API 可以更加灵活的组织组件代码,不同于 Options API,代码可以通过特定功能来组织,也提供了组件间或者组件之外更加直接的提取、复用逻辑的能力。
支持大型项目开发者对 TypeScript 的需求。然而 Vue 现存 API 最初的设计并没有考虑类型推断,大部分原因是由于 Vue 依赖 this
上下文的方式暴露属性,这与原生 JS 中的 this
大相径庭,而这也对当前 Vue 与 TypeScript 的整合造成了极大的困难。
目前许多 Vue 的使用者利用 vue-class-component
通过修饰符将组件以类的方式进行编写。在设计 Vue3.x
阶段,Vue 团队尝试提供内置的 Class API 来解决类型的问题,不过经过反复的讨论发现这种方式必须依赖修饰符,这种方式的不稳定性和不确定性导致其成为了一个有风险的基础建设。
相比较,组合式 API 大多使用简单的变量和函数,类型更加友好,并且可以享受完整的类型推断提示。使用新的 API 编写的代码在 TypeScript 和 JavaScript 看起来几乎相同,因此即使不用 TypeScript ,也可以通过 IDE 的支持方便的获得类型提示。
组合式 API 通过使用引入函数的方式来替代原有的 options 选项,在实例中,reactive和refs函数替代了原有的data,computed函数代替了computed属性,watchEffect函数代替了watch属性。乍一看所有逻辑都混合在一起写在了 setup() 中,代码组织还不如使用 options,但是如果我们真正考虑代码组织的最终目标 —— 更加容易的理解代码,我们会发现仅仅知道一个复杂的组件有哪些选项并不能帮助我们阅读理解代码,从而理解整个组件的代码逻辑。当开发者们阅读其他人编写的组件代码的时候,比起「组件使用了哪些选项?」,他们更在意的是「组件想要做什么?」。
我们可以通过以下例子来对比两者之间的区别,首先用 Options API 的写法完成一个组件,组件逻辑很简单,拥有两个响应式数据 name 和 gender,两个方法 getName 和 getGender,在调用时可以获取到 people 对象中的 name 和 gender 属性,代码如下所示:
`const component = {
data() {
return {
people: {
name: 'maxuxiao',
gender: 'male'
},
name: '',
gender: ''
};
},
methods: {
getName() {
this.name = this.people.name;
},
getGender() {
this.gender = this.people.gender;
}
}
};`
我们可以发现,如果把获取名字和获取性别看做两组逻辑,把代码以逻辑进行划分的话,会是这样:
相同的逻辑用相同的颜色表示。
不难想象,如果组件逻辑更加复杂,options 更加多,包括 props、data、computed、methods、watch 以及大量的生命周期钩子的话、在阅读代码试图了解逻辑关注点的时候会更加困难。
而当使用组合式 API 的时候的代码是这样的,所有的逻辑写在了 setup
函数里:
const component = {
setup() {
const people = {
name: 'maxuxiao',
gender: 'male'
};
// 处理 name 相关的业务
const name = Vue.ref('');
const getName = () => {
name.value = people.name;
};
// 处理 gender 相关的业务
const gender = Vue.ref('');
const getGender = () => {
gender.value = people.gender;
};
return {
name,
getName,
gender,
getGender
};
}
};
如果按照逻辑关注点来整理的话可以看到相关逻辑的代码已经被组织到了一起,使得代码更具有可维护性。
当然组合式 API 的引入也存在一定的弊端,它在代码组织方面提供了更多灵活性的同时,也需要开发人员通过功能分组的的方式去降低 setup 函数的复杂度,避免 setup 代码量越来越多,return 的对象越来越复杂情况这种面条代码的产生。我们期望的是 setup 函数现在只是简单地作为调用所有组合函数的入口,参考以下功能分组的方式:
const component = {
setup() {
const people = {
name: 'maxuxiao',
gender: 'male'
};
return {
...useName(people),
...useGender(people)
};
}
};
const component2 = {
setup() {
const people = {
name: 'chenmingming',
gender: 'male'
};
return {
...useName(people),
...useGender(people)
};
}
};
// 处理 name 相关的业务
function useName(people) {
const name = Vue.ref('');
const getName = () => {
name.value = people.name;
};
return {
name,
getName
};
}
// 处理 gender 相关的业务
function useGender(people) {
const gender = Vue.ref('');
const getGender = () => {
gender.value = people.gender;
};
return {
gender,
getGender
};
}
通过以上代码我们可以看到代码被按照业务逻辑分成了多个函数,而 setup 函数负责将它们组合起来。通过这种方式也达到了组合式 API 设计的另外一个核心目的:让相同的代码逻辑在不同组件中低成本的抽取和复用。不过对 Vue3.x 来说,组合式 API 并不是默认的方案,它被定义为高级特性,意在解决大型应用程序中的复杂组件的编写。
目前业界解决组件逻辑复用和代码组织的机制主要做法有:
Mixin 不论是 Vue 还是 React,现在都不太推荐使用,主要问题是来源不清晰、容易冲突、类型推导不明确等问题。
HOC 高阶组件可以看作 React 对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
高阶组件(HOC)是 React 中的高级技术,用来重用组件逻辑。但高阶组件本身并不是 React API。它只是一种模式,这种模式是由 React 自身的组合性质必然产生的。
function visible(WrappedComponent) {
return class extends Component {
render() {
const { visible, ...props } = this.props;
if (visible === false) return null;
return ;
}
}
}
业界对组合式 API 存在一定的争议,具体内容可以参考官方的讨论:基于功能的组件 API[1] 和基于函数的组件 API(扩展讨论)[2]。我在这里帮大家提炼总结下文章的观点,大家担心的是:
原来的 options API 按 props、computed、watch、methods、生命周期等进行分组,存在一定约束,就算是低级别的程序员,也不会写出太杂乱无章的代码。但组合式 API,没有这些约束,容易导致『意大利面条式』代码。
尤大对这个问题进行详细的回复,他的观点,组合式 API 用于解决:
尤大同时承认可能导致意大利面条代码,他建议通过代码规范 / 指南 / CR 等方式解决。尤大没有说过让大家放弃使用 option API,只是说明了初衷和要解决的问题,暂时官方没有提供最佳实践。
那么业界大家对组合式 API 落地情况怎么样呢?我们调研主流的几个支持 Vue3.0 的组件库,对组合式 API 落地使用情况分析:
elment-plus:全部组件使用组合式 API,未使用 Options API
vant:全部组件使用组合式 API,未使用 Options API
同时我们也注意到部分组件的 setup 函数,代码量非常大,组件里把原来的 props、computed、watch、methods、生命周期都放在 setup 中,这显然是不合适的做法,也进一步应证了『意大利面条』的说法。
由于现阶段 Vue 团队没有发布官方的最佳实践,所以项目中是否应该使用组合式 API 来替代 Options API 就需要结合实际项目情况来考虑。在实际业务项目中,对于没有涉及到大型应用、复杂组件以及 TS 支持的情况,组合式 API 的使用也就不是那么必要。
经过实践尝试,结合 Vue 社区内的讨论结果,我们决定当遇到以下几种情况的时候,使用组合式 API,其他情况继续保持使用 Options API:
1、逻辑复杂的大型非业务组件;
2、存在大量共享逻辑的组件,包含 mixin、HOC 高阶组件、slot 等场景;
3、组件涉及到多种Options或生命周期钩子函数;
经过实践,组合式 API 的出现从根本上解决了 Vue 在逻辑复用以及代码组织上存在的问题,同时也在类型推断上有了更好的支持,这意味着用组合式 API 编写的代码可以享受完整的类型推断。另外 Vue3 也引入了 tree shaking 特性,这种按需引用 API 的使用方式可以在编译阶段将没有用到的代码进行 tree shaking 优化,从而有效减小项目打包体积。
同时,经过实际项目实践,组合式 API 的引入确实解决了 Options API 在代码组织上存在的问题,体现在:
另外,组合式 API 虽然提供了更加灵活的代码组织能力,但是缺乏经验的开发者对组合式API的滥用会使得代码更加晦涩难懂。Options API 通过约定我们该在哪个位置做什么事,一定程度上也强制我们进行了代码分隔。而没有正确进行逻辑分隔的组合式 API 会使 setup 中的代码量越来越多,导致「意大利面条代码」情况的出现。总的来说,组合式 API 在提升了代码质量上限的同时,也降低了下限。
[1] 基于功能的组件 API:https://github.com/vuejs/rfcs/pull/42
[2] 组合式 API 扩展讨论: https://github.com/vuejs/rfcs/pull/55
[3] 组合式 API 介绍: https://v3.cn.vuejs.org/guide/composition-api-introduction.html
[4] 组合式 API 文档: https://v3.cn.vuejs.org/api/composition-api.html
[5] 组合式 API RFC 提案: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md