Vue Composables 的良好实践和设计模式
最近我和 Vue Storefront 团队就 Vue 可组合组件的编写模式进行了一次非常棒的讨论。在我们的系统中,可组合组件负责存储主要的业务逻辑(例如计算、操作、流程),因此它们是应用程序的关键组成部分。遗憾的是,随着时间的推移,我们没有足够的时间来创建某种模式,Contract for writing Composables因此我们的一些可组合组件实际上并不算是真正的可组合组件 😉
我很高兴现在我们有时间来重构我们构建新可组合组件的方法,使它们易于维护、易于测试且真正有用。
在本文中,我将总结我们提出的想法,并将它们与我从几篇文章中读到的良好实践和设计模式相结合。
因此,本文将分为三个部分:
- 通用设计模式
- 我的推荐
- 延伸阅读
请尽情享受吧!也请告诉我你在项目中使用了哪些模式和方法🚀
通用设计模式
我认为学习构建可组合组件模式的最佳资源实际上是 Vue.js 官方文档,你可以在这里查看。
基本可组合
Vue 文档展示了以下 useMouse 可组合组件的示例:
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// by convention, composable function names start with "use"
export function useMouse() {
// state encapsulated and managed by the composable
const x = ref(0)
const y = ref(0)
// a composable can update its managed state over time.
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// a composable can also hook into its owner component's
// lifecycle to setup and teardown side effects.
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// expose managed state as return value
return { x, y }
}
之后可以在组件中像这样使用它:
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
异步可组合
为了获取数据,Vue 建议采用以下可组合的结构:
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
watchEffect(() => {
// reset state before fetching..
data.value = null
error.value = null
// toValue() unwraps potential refs or getters
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
})
return { data, error }
}
然后就可以像这样在组件中使用它:
<script setup>
import { useFetch } from './useFetch.ts'
const { data, error } = useFetch('...')
</script>
可组合合约
基于以上示例,所有可组合组件都应遵循以下约定:
- 可组合文件名应以“use”开头,例如
useSomeAmazingFeature.ts - 它可以接受原始类型(例如字符串)的输入参数,也可以接受引用和getter,但需要使用toValue辅助方法。
- Composable 应该返回一个 ref 值,该值可以在解构 Composable 之后访问,例如:
const { x, y } = useMouse() - 可组合对象可以保存全局状态,该状态可以在整个应用程序中访问和修改。
- Composable 可能会产生一些副作用,例如添加窗口事件监听器,但当组件卸载时应该清除这些副作用。
- Composables 只能在 `on`
<script setup>或 `on`setup()钩子中调用。在这些上下文中,它们也应该同步调用。在某些情况下,你也可以在生命周期钩子中调用它们,例如 `on`onMounted()。 - 可组合组件之间可以调用其他可组合组件。
- 可组合对象应该将某些逻辑封装在内部,当逻辑过于复杂时,应该将其提取到单独的可组合对象中,以便于测试。
我的推荐
我为我的工作项目和开源项目构建了多个可组合系统——NuxtAlgolia、NuxtCloudinary、NuxtMedusa,因此基于这些,我想根据我的经验在上面的合同中添加一些要点。
有状态函数和/或纯函数可组合性
在代码标准化的某个阶段,您可能会得出结论,您希望对可组合对象中保持的状态做出决定。
最容易测试的函数是那些不存储任何状态的函数(即简单的输入/输出函数),例如一个负责将字节转换为人类可读值的可组合函数。它接受一个值并返回另一个值——它不存储任何状态。
别误会,你不需要做决定OR。你可以同时保留有状态和无状态的组合组件。但最好把这个决定写下来,这样以后使用起来会更方便🙂
可组合对象单元测试
我们想用 Vitest 为前端应用程序实现单元测试。在后端开发中,单元测试的代码覆盖率非常有用,因为后端开发主要关注逻辑。然而,前端开发通常涉及视觉效果。
因此,我们认为对整个组件进行单元测试可能不是最好的方法,因为我们基本上是对框架本身进行单元测试(如果按下按钮,检查状态是否改变或模态框是否打开)。
由于我们将所有业务逻辑都移到了可组合组件(本质上是 TypeScript 函数)中,因此使用 Vitest 进行测试非常容易,也使我们能够拥有更稳定的系统。
可组合性范围
不久前,在 VueStorefront 团队中,我们开发了一套自己的可组合性实现方法(实际上,早在它们被正式命名为“可组合性”之前我们就开发了这套方法😄)。在我们的方法中,我们使用可组合性来映射电子商务的业务领域,如下所示:
const { cart, load, addItem, removeItem, remove, ... } = useCart()
这种方法确实很有用,因为它允许将整个领域封装在一个函数中。在一些简单的例子中,例如 `<code>`useProduct或 ` useCategory<code>`,这种方法实现和维护都相对容易。然而,正如你在这里的例子中看到的,useCart当封装的领域包含比数据获取多得多的逻辑时,这个可组合函数会变得非常难以开发和维护。
这时,我开始为 Nuxt 生态系统做贡献,那里引入了一种不同的方法。在这种新方法中,每个可组合体只负责一件事。因此,与其构建一个庞大的可组合体,不如为每个功能(例如,组件、功能模块等)useCart构建单独的可组合体。useAddToCartuseFetchCartuseRemovefromCart
这样一来,维护和测试这些可组合组件就容易多了🙂
延伸阅读
我的研究就到此为止。如果您想了解更多相关信息,请务必查看以下文章:
- https://vuejs.org/guide/reusability/composables.html
- https://www.youtube.com/watch?v=bcZM3EogPJE
- https://vueschool.io/articles/vuejs-tutorials/what-is-a-vue-js-composable/
- https://blog.logrocket.com/getting-started-vue-composables/
- https://macopedia.com/blog/news/how-can-vue-3-composables-make-your-life-easier