【翻译】13个让代码更优雅的Vue组合式组件技巧

5 阅读7分钟

原文链接:michaelnthiessen.com/13-vue-comp…

作者:Michael Thiessen

Vue 可组合组件功能极其强大,但稍有不慎便会变得混乱不堪且难以维护。

为此我总结了13条技巧,助你编写更优质、更易维护的可组合组件。

无论你是在构建简单的状态管理方案还是复杂的共享逻辑,这些技巧都能助你:

  • 规避导致意大利面代码的常见陷阱
  • 编写更易测试和维护的组合函数
  • 创建更灵活且可复用的共享逻辑
  • 在需要时逐步从选项 API 迁移至组合 API

您将学到的技巧包括:

  • 避免通过多个组件传递属性
  • 在无关组件间共享数据
  • 使用明确方法控制状态更新
  • 将大型组件拆分为小型函数
  • 将业务逻辑与Vue响应性分离
  • 在单个函数中处理同步与异步数据
  • 使函数参数更具描述性
  • 通过默认值防止未定义选项
  • 根据需求返回简单或复杂值
  • 将不同逻辑路径拆分为独立函数
  • 统一处理响应式与原始值
  • 简化引用解包操作
  • 逐步从选项API迁移至组合API

让我们深入解析每种模式,探索它们如何提升您的Vue应用程序!

1. 避免属性在多个组件层级间传递

数据存储模式有助于避免属性与事件在多个组件层级间传递。

例如当父子组件通过层层传递的属性与事件冒泡进行通信时:

<!-- Parent.vue -->
<template>
  <!-- But many more layers of components -->
  <Child :user="user" @change="onUserChange" />
</template>

<script setup>
const user = ref({ name: 'Alice' })
function onUserChange(updatedUser) {
  user.value = updatedUser
}
</script>

这会带来大量复杂性,因为这些 props 和事件必须在组件层级结构中来回传递。

更直接的解决方案是创建一个共享数据存储,任何组件都可以导入:

import { reactive, toRefs } from 'vue'
const state = reactive({ user: { name: 'Alice' } })

export function useUserStore() {
  return toRefs(state)
}

2. 在无关组件间共享数据

当兄弟组件或“表亲”组件需要共享相同数据却无法直接连接时,数据存储模式同样能提供解决方案。

假设两个兄弟组件都需要相同的用户对象,但通过 props 或事件传递又缺乏优雅的途径。

这通常导致通过父组件进行笨拙的数据传递,或造成状态冗余。

更优的方案是依赖单一可组合存储,供两个兄弟组件共同使用:

// SiblingA.vue
import { useUserStore } from './useUserStore'
const { user } = useUserStore()

// SiblingB.vue
import { useUserStore } from './useUserStore'
const { user } = useUserStore()

3. 使用清晰的方法控制状态更新

数据存储模式鼓励为更新共享状态提供清晰的方法。

有些开发者会将整个响应式对象暴露给外部,例如:

export const user = reactive({ darkMode: false })

这使得任何人都能直接从任意文件修改用户的darkMode属性,可能导致零散且失控的变异。

更好的做法是将状态设为只读,同时提供定义更新机制的函数:

import { reactive, readonly } from 'vue'
const state = reactive({ darkMode: false })

export function toggleDarkMode() {
  state.darkMode = !state.darkMode
}

export function useUserStore() {
  return {
    darkMode: readonly(state.darkMode),
    toggleDarkMode
  }
}

4. 将大型组件拆分为小型函数

内联组合模式通过将相关状态和逻辑聚合到小型函数中,帮助拆分大型组件。

一个庞大的组件可能将所有引用和方法集中在一个位置:

<script setup>
const count = ref(0)
const user = ref({ name: 'Alice' })
// 500 more lines of intertwined code with watchers, methods, etc.
</script>

这种设置很快就会变得难以管理。

相反,内联组合器可以将逻辑进行分组并提供本地化支持。这样我们便能将其提取到单独的文件中:

<script setup>
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

const { count, increment } = useCounter()
</script>

5. 将业务逻辑与Vue响应性分离

Thin Composables模式要求我们将原始业务逻辑与Vue响应性分离,从而简化测试和维护工作。

您可能将所有逻辑嵌入到可组合组件中:

export function useCounter() {
  const count = ref(0)
  function increment() {
    count.value = (count.value * 3) / 2
  }
  return { count, increment }
}

这迫使你必须在Vue环境中测试逻辑。

相反,应将复杂规则封装在纯函数中,让组合器仅处理响应式包装器:

// counterLogic.js
export function incrementCount(num) {
  return (num * 3) / 2
}

// useCounter.js
import { ref } from 'vue'
import { incrementCount } from './counterLogic'

export function useCounter() {
  const count = ref(0)
  function increment() {
    count.value = incrementCount(count.value)
  }
  return { count, increment }
}

6. 在单个函数中同时处理同步与异步数据

异步+同步组合模式将同步与异步行为合并到一个组合函数中,而非创建独立函数。

这与Nuxt的useAsyncData工作原理如出一辙。

此处我们拥有单个组合函数,既能返回Promise,又能为同步使用提供即时响应属性:

import { ref } from 'vue'

export function useAsyncOrSync() {
  const data = ref(null)
  const promise = fetch('/api')
    .then(res => res.json())
    .then(json => {
      data.value = json
      return { data }
    })
  return Object.assign(promise, { data })
}

7. 使函数参数更具描述性

选项对象模式通过接受单个配置对象,可简化冗长的参数列表。

此类调用方式繁琐且易出错,新增选项时还需更新函数签名:

useRefHistory(someRef, true, 10, 500, 'click', false)

每个参数的含义并不明确。

接受选项对象的组合器能保持所有内容的描述性:

useRefHistory(someRef, {
  deep: true,
  capacity: 10,
  throttle: 500,
  eventName: 'click',
  immediate: false
})

8. 通过默认值防止未定义选项

选项对象模式还建议为每个属性设置默认值。

若函数假定某些字段存在,但在调用时未传递这些字段,则可能引发问题:

export function useRefHistory(someRef, options) {
  // potential undefined references here
}

使用安全默认值解构选项效果更佳:

export function useRefHistory(someRef, {
  deep = false,
  capacity = Infinity,
  ...rest
} = {}) {
  // can safely use deep, capacity, etc.
}

9. 根据需求返回简单或复杂值

动态返回模式确保组合件既能为简单用例返回单一值,也能返回包含更高级控制功能的扩展对象。

某些方法总是返回包含所有内容的对象:

export function useLocalStorage(key, defaultValue) {
  const val = ref(defaultValue)
  function remove() { /* ... */ }
  return { val, remove }
}

仅需核心响应值的用户被迫处理额外内容。

通过组合式函数实现条件返回单个引用或对象即可解决此问题:

export function useLocalStorage(key, defaultValue, { controls = false } = {}) {
  const value = ref(defaultValue)
  function remove() { /* ... */ }
  return controls ? { value, remove } : value
}

10. 将不同逻辑路径拆分为独立函数

隐藏组合模式有助于避免在同一组合器中混杂互斥逻辑。

某些代码会将多种模式或代码路径混为一谈:

export function useUserFlow() {
  // admin logic
  // guest logic
}

将每条路径拆分为独立的可组合组件,不仅更清晰,且不影响功能实现:

export function useAdminFlow() {
  // admin-only logic
}

export function useGuestFlow() {
  // guest-only logic
}

11. 统一处理响应式数据与原始值

灵活参数模式确保组合器中的输入输出统一作为响应式数据或原始值处理,避免混淆。

部分代码会检查输入是否为引用:

export function useSomething(input) {
  if (isRef(input)) {
    // ...
  } else {
    // ...
  }
}

相反,你可以立即进行转换。

使用 ref 时,如果输入是 ref,则返回该 ref。否则,它将被转换为 ref

import { ref, toValue } from 'vue'

export function useSomething(input) {
  const asRef = ref(input)       // always a ref
}

12. 简化引用解包

灵活参数模式在需要解包时也会使用toValue方法。

若不使用该方法,代码可能需要持续进行isRef检查:

export function useLogger(msg) {
  // if (isRef(msg)) ...
  // else ...
}

调用起来要简单得多:

import { toValue } from 'vue'
export function useLogger(msg) {
  const val = toValue(msg)
  console.log(val)
}

13. 逐步从选项 API 迁移至组合 API

选项到组合模式允许您将大型选项 API 组件逐步迁移至组合 API,这种渐进式迁移方式便于理解。

经典的选项组件可能这样实现:

export default {
  data() {
    return { count: 0 }
  },
  computed: {
    double() { return this.count * 2 }
  },
  methods: {
    increment() { this.count++ }
  }
}

数据、计算属性与方法分散各处。

将其转换为脚本设置可整合这些元素,使代码更易理解,并支持以下模式:

<script setup>
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
  count.value++
}
</script>

总结

这13条技巧将帮助你编写更优质的Vue可组合组件,使其更易于维护、测试并在应用程序中复用。