发布于 2026-01-05 2 阅读
0

Vue Composables 的良好实践和设计模式

Vue Composables 的良好实践和设计模式

最近我和 Vue Storefront 团队就 Vue 可组合组件的编写模式进行了一次非常棒的讨论。在我们的系统中,可组合组件负责存储主要的业务逻辑(例如计算、操作、流程),因此它们是应用程序的关键组成部分。遗憾的是,随着时间的推移,我们没有足够的时间来创建某种模式,Contract for writing Composables因此我们的一些可组合组件实际上并不算是真正的可组合组件 😉

我很高兴现在我们有时间来重构我们构建新可组合组件的方法,使它们易于维护、易于测试且真正有用。

在本文中,我将总结我们提出的想法,并将它们与我从几篇文章中读到的良好实践和设计模式相结合。

因此,本文将分为三个部分:

  1. 通用设计模式
  2. 我的推荐
  3. 延伸阅读

请尽情享受吧!也请告诉我你在项目中使用了哪些模式和方法🚀

通用设计模式

我认为学习构建可组合组件模式的最佳资源实际上是 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 }
}
Enter fullscreen mode Exit fullscreen mode

之后可以在组件中像这样使用它:

<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>
Enter fullscreen mode Exit fullscreen mode

异步可组合

为了获取数据,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 }
}
Enter fullscreen mode Exit fullscreen mode

然后就可以像这样在组件中使用它:

<script setup>
import { useFetch } from './useFetch.ts'

const { data, error } = useFetch('...')
</script>
Enter fullscreen mode Exit fullscreen mode

可组合合约

基于以上示例,所有可组合组件都应遵循以下约定:

  1. 可组合文件名应以“use”开头,例如useSomeAmazingFeature.ts
  2. 它可以接受原始类型(例如字符串)的输入参数,也可以接受引用和getter,但需要使用toValue辅助方法。
  3. Composable 应该返回一个 ref 值,该值可以在解构 Composable 之后访问,例如:const { x, y } = useMouse()
  4. 可组合对象可以保存全局状态,该状态可以在整个应用程序中访问和修改。
  5. Composable 可能会产生一些副作用,例如添加窗口事件监听器,但当组件卸载时应该清除这些副作用。
  6. Composables 只能在 `on`<script setup>或 `on`setup()钩子中调用。在这些上下文中,它们也应该同步调用。在某些情况下,你也可以在生命周期钩子中调用它们,例如 `on` onMounted()
  7. 可组合组件之间可以调用其他可组合组件。
  8. 可组合对象应该将某些逻辑封装在内部,当逻辑过于复杂时,应该将其提取到单独的可组合对象中,以便于测试。

我的推荐

我为我的工作项目和开源项目构建了多个可组合系统——NuxtAlgolia、NuxtCloudinary、NuxtMedusa,因此基于这些,我想根据我的经验在上面的合同中添加一些要点。

有状态函数和/或纯函数可组合性

在代码标准化的某个阶段,您可能会得出结论,您希望对可组合对象中保持的状态做出决定。

最容易测试的函数是那些不存储任何状态的函数(即简单的输入/输出函数),例如一个负责将字节转换为人类可读值的可组合函数。它接受一个值并返回另一个值——它不存储任何状态。

别误会,你不需要做决定OR。你可以同时保留有状态和无状态的组合组件。但最好把这个决定写下来,这样以后使用起来会更方便🙂

可组合对象单元测试

我们想用 Vitest 为前端应用程序实现单元测试。在后端开发中,单元测试的代码覆盖率非常有用,因为后端开发主要关注逻辑。然而,前端开发通常涉及视觉效果。

因此,我们认为对整个组件进行单元测试可能不是最好的方法,因为我们基本上是对框架本身进行单元测试(如果按下按钮,检查状态是否改变或模态框是否打开)。

由于我们将所有业务逻辑都移到了可组合组件(本质上是 TypeScript 函数)中,因此使用 Vitest 进行测试非常容易,也使我们能够拥有更稳定的系统。

可组合性范围

不久前,在 VueStorefront 团队中,我们开发了一套自己的可组合性实现方法(实际上,早在它们被正式命名为“可组合性”之前我们就开发了这套方法😄)。在我们的方法中,我们使用可组合性来映射电子商务的业务领域,如下所示:

const { cart, load, addItem, removeItem, remove, ... } = useCart() 
Enter fullscreen mode Exit fullscreen mode

这种方法确实很有用,因为它允许将整个领域封装在一个函数中。在一些简单的例子中,例如 `<code>`useProduct或 ` useCategory<code>`,这种方法实现和维护都相对容易。然而,正如你在这里的例子中看到的,useCart当封装的领域包含比数据获取多得多的逻辑时,这个可组合函数会变得非常难以开发和维护。

这时,我开始为 Nuxt 生态系统做贡献,那里引入了一种不同的方法。在这种新方法中,每个可组合体只负责一件事。因此,与其构建一个庞大的可组合体,不如为每个功能(例如组件、功能模块等)useCart构建单独的可组合体。useAddToCartuseFetchCartuseRemovefromCart

这样一来,维护和测试这些可组合组件就容易多了🙂

延伸阅读

我的研究就到此为止。如果您想了解更多相关信息,请务必查看以下文章:

文章来源:https://dev.to/jacobandrewsky/good-practices-and-design-patterns-for-vue-composables-24lk