一、背景
Vue3.x 版本的出现带来了许多令人眼前一亮的新特性,其中组合式 API(Composition API),一组附加的、基于功能的 API 被作为一种新的逻辑复用和代码组织的方式提供给了开发者,提供更加灵活的组合组件逻辑能力。
二、什么是组合式 API(Composition API )
- 组合式 API:一组低侵入式的、函数式的 API,使得我们能够更灵活地「组合」组件的逻辑
- 用一句通俗的话来说:
composition API
其实是用于解决功能、数据和业务逻辑分散的问题,使项目更益于模块化开发以及后期维护
先看一段简单的代码:
<template>
<h2>组合式API</h2>
<el-button type="primary" @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</el-button>
<div style="display: block;margin-top: 50px;">
<router-link to="/" style="margin-top: 50px">首页</router-link>
</div>
</template>
<script>
import {computed, reactive} from "vue"
export default {
setup() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
})
function increment() {
state.count++
}
return {
state,
increment
}
}
}
</script>
我们先来看下这段代码发生了啥?
import {computed, reactive} from "vue"
Component API
是以函数的形式展示组件属性,所以第一步就是导入我们需要的函数。在我们的例子中,我们用 reactive
创建响应属性,用 computed
创建计算属性。
export default {
setup() {
// ...
return {
state,
increment
}
}
还有一个 setup
函数, setup
函数是一个新的组件选项。作为在组件内使用 Composition API
的入口点,如果 setup
返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文,我们就可以在模板里使用对应的属性和方法。
组合式 API 全景
组合式 API 的使用,官方参考文档和 API 文档有详细说明,这里不过多说明。为方便大家理解和把握,根据官方的 API 文档整理了全景图。
Vue 组合式 API 可以分为五大块:
- 数据响应(复杂对象):响应性基础 API ,支持复杂对象
Object
、Array
的数据响应 - 数据响应(内部值):内部值指 JS 内置的简单数据结构,包括
String
、Number
等 computed
和watch
:基于响应式数据的数据计算与监听- 生命周期:对原生命周期封装,例如:
onMounted
、onBeforeMount
等 - 其他 API:重要支持性 API
为什么要引入组合式 API
在 Vue2 中我们采用 Options API
来写上面的代码:
<template>
<h2>Option API</h2>
<el-button type="primary" @click="increment">
Count is: {{ count }}, double is: {{ double }}
</el-button>
<div style="display: block;margin-top: 50px;">
<router-link to="/" style="margin-top: 50px">首页</router-link>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
computed: {
double() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
那在 Vue2 中如果我们要复用这个逻辑,我们可以通过诸如 mixins
或作用域插槽的模式达成。
mixins
的方式
<template>
<h2>Option API Mixins</h2>
<el-button type="primary" @click="increment">
Count is: {{ count }}, double is: {{ double }}
</el-button>
<div style="display: block;margin-top: 50px;">
<router-link to="/" style="margin-top: 50px">首页</router-link>
</div>
</template>
<script>
import CounterMixin from '@/mixins/counter'
export default {
mixins: [CounterMixin]
}
</script>
mixins
存在的问题是:
- 渲染上下文中暴露的
property
来源不清晰。例如在阅读一个运用了多个mixin
的组件代码时,很难看出某个property
是从哪一个mixin
中注入的。 - 命名空间冲突。
mixin
之间的property
和方法可能有冲突。
域插槽的方式
<template>
<h2>Option API Slot</h2>
<Counter v-slot="{ count, double, increment }">
<el-button type="primary" @click="increment">
Count is: {{ count }}, double is: {{ double }}
</el-button>
</Counter>
<div style="display: block;margin-top: 50px;">
<router-link to="/" style="margin-top: 50px">首页</router-link>
</div>
</template>
<script>
import Counter from '@/components/Counter'
export default {
components: {
Counter
}
}
</script>
优点:有了 scoped slots,我们就可以通过 v-slot 属性准确地知道我们可以访问哪些属性,这样就更容易理解代码了。
缺点:我们只能在模板中访问,而且只能在 Counter
组件作用域中使用。
组合式 API 的方式
useCounter.js
// 将这部分代码逻辑抽离出来
import {computed, reactive} from "vue"
export function useCounter() {
const state = reactive({
count: 0,
double: computed(() => state.count * 2)
});
const increment = function() { state.count++ }
return {
state,
increment
}
}
CounterSetup.vue
<template>
<h2>组合式API script setup 语法糖</h2>
<el-button type="primary" @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</el-button>
<div style="display: block;margin-top: 50px;">
<router-link to="/" style="margin-top: 50px">首页</router-link>
</div>
</template>
<script setup>
import { useCounter } from '@/libs/useCounter'
const { state, increment } = useCounter()
</script>
是不是更加优雅了,相比较而言:
- 暴露给模板的
property
来源十分清晰,因为它们都是被组合逻辑函数返回的值。 - 不存在命名空间冲突,可以通过解构任意命名
- 不再需要仅为逻辑复用而创建组件实例
- 仅依赖它的参数和
Vue
全局导出的 API,而不是依赖其微妙的this
上下文
结论
除了方便逻辑提取与复用之外,Composition API 带给我们的实际上更多的是一种新的代码编写思维。
当要去理解一个组件时,我们更加关心的是“这个组件是要干什么” (即代码背后的意图) 而不是“这个组件用到了什么选项”。基于 Options API
撰写出来的代码自然采用了后者的表述方式,然而对前者的表述并不好。
Options API
选项的强行分离为展示背后的逻辑关注点设置了障碍。此外,在处理单个逻辑关注点时,我们必须不断地在选项代码块之间“跳转”,以找到与该关注点相关的部分。
比如上面的例子中,基于 Options API
的方式我们必须在 data
、computed
、methods
三个选项中跳转,来完成这段逻辑。而通过 Composition API
的方式我们把相同逻辑关注点的代码并列在一起,形成了一个独立的逻辑函数。
组合式 API 存在的问题
当然 Composition API
的引入也存在一定的弊端。
组合式 API 在代码组织方面提供了更多的灵活性,但它也需要开发人员更多地自我克制来 “正确地完成它”,组合式 API 会让没有经验的新手编写出“面条代码”。
在 Options API
中实际上形成了一种强制的约定:
props
里面设置接收参数data
里面设置变量computed
里面设置计算属性watch
里面设置监听属性methods
里面设置事件方法
会发现 Options API
都约定了我们该在哪个位置做什么事,这在一定程度上也强制我们进行了代码分割。
现在用 Composition API
,不再这么约定了,所以代码组织非常灵活,如果作为一个新手,或者不深入思考的码农,那么在逻辑越来越复杂的情况下,setup
代码量越来越多,同样 setup
里面的 return
越来越复杂,势必会落入“面条代码”地步。
组合式 API 的 script setup 语法糖
<setup script>
就是 vue3 新出的一个语法糖,使用方法就是在 script
标签的后面加上一个 setup
修饰。
代码如下:
<template>
<h2>组合式API script setup 语法糖</h2>
<el-button type="primary" @click="increment">
Count is: {{ state.count }}, double is: {{ state.double }}
</el-button>
<div style="display: block;margin-top: 50px;">
<router-link to="/" style="margin-top: 50px">首页</router-link>
</div>
</template>
<script setup>
import { useCounter } from '@/libs/useCounter'
const { state, increment } = useCounter()
</script>
优点:
- 自动注册组件
- 属性和方法无需返回
- 更少的样板内容,更简洁的代码
- 能够使用纯
Typescript
声明props
和抛出事件 - 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)
- 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)
顶层的绑定会被暴露给模板
当使用 <script setup>
的时候,任何在 <script setup>
声明的顶层的绑定 ( 包括变量,函数声明,以及 import
引入的内容 ) 都能在模板中直接使用:
<template>
<div @click="log">{{ msg }}</div>
<div>{{ capitalize('hello') }}</div>
</template>
<script setup>
// 引入
import { capitalize } from '@/libs/helpers'
// 变量
const msg = 'Hello!'
// 函数
function log() {
console.log(msg)
}
</script>
使用 this
在 setup
中你应该避免使用 this
,因为它不会找到组件实例。setup
的调用发生在 data property
、computed property
或 methods
被解析之前,所以它们无法在 setup
中被获取。这使得 setup
在和其它 Options API
一起使用时可能会导致混淆。
steup 中的生命周期钩子(对比 Options API
)
扩展
vite 项目实现 vue3 函数和组件库的自动按需导入
在一个 vue3 项目中,我们经常需要从 vue
,vue-router
等引入相应的函数方法进单个文件中。
比如:
import { ref, reactive, toRefs, watchEffect, computed, watch, onUnmounted,} from 'vue'
import { useRouter } from 'vue-router'
这时我们可以使用基于 unplugin
项目的两个插件:
yarn add unplugin-auto-import unplugin-vue-components -D
- 然后在
vite.config.js
中配置
// vite.config.js
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()], // 这里使用ElementPlus组件库
imports: [
'vue',
'vue-router',
{
vuex: ['useStore']
}
]
}),
Components({
resolvers: [ElementPlusResolver()]
}),
]
})
配置完成后我们就可以直接在项目中这样使用
<template>
<!--el-button 无需从 Element Plus 中导入-->
<el-button type="primary">{{ text }}</el-button>
<br>
<div style="display: block;margin-top: 50px;">
<router-link to="/" style="margin-top: 50px">首页</router-link>
</div>
</template>
<script setup>
const text = ref('我是一个 Element Plus 按钮') // ref 无需从vue中导入
</script>
注意:在 .vue
文件中无需 import
,但是在单独的 js 文件中,还是需要导入的
项目地址
本文所有代码地址:
gitlab(需要注册):https://gitlab.explorexd.com/devs/vue3-demo
github:https://github.com/jiaxudonggit/vue3-demo
参考文档
https://v3.cn.vuejs.org/guide/introduction.html
https://juejin.cn/post/7027443242928979982
https://juejin.cn/post/6844904191928827912
https://juejin.cn/post/7025837638540066824
https://element-plus.gitee.io/zh-CN/guide/installation.html
https://cn.vitejs.dev/guide/#scaffolding-your-first-vite-project
评论区