Skip to content

Vue 3 Composition API 最佳实践

Vue 3 的 Composition API 为我们提供了一种全新的组件逻辑组织方式。相比于 Options API,Composition API 在代码复用、类型推导和逻辑组织方面都有显著优势。本文将深入探讨 Composition API 的最佳实践。

为什么选择 Composition API?

1. 更好的逻辑复用

在 Options API 中,我们通常使用 mixins 来复用逻辑,但这会带来命名冲突和来源不明的问题。Composition API 通过组合函数(Composables)提供了更优雅的解决方案。

javascript
// 传统的 mixin 方式
const counterMixin = {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

// Composition API 方式
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  
  return {
    count,
    increment
  }
}

2. 更好的类型推导

TypeScript 用户会发现 Composition API 提供了更好的类型推导支持:

typescript
import { ref, computed } from 'vue'

function useUser() {
  const user = ref<User | null>(null)
  const isLoggedIn = computed(() => !!user.value)
  
  const login = async (credentials: LoginCredentials) => {
    // TypeScript 能够正确推导类型
    user.value = await authService.login(credentials)
  }
  
  return {
    user,
    isLoggedIn,
    login
  }
}

核心概念详解

响应式基础

ref vs reactive

javascript
import { ref, reactive } from 'vue'

// ref: 用于基本类型和单个值
const count = ref(0)
const message = ref('Hello')

// reactive: 用于对象和数组
const state = reactive({
  user: null,
  loading: false,
  error: null
})

// 访问值的区别
console.log(count.value) // 需要 .value
console.log(state.user)  // 直接访问

toRefs 的妙用

当需要解构 reactive 对象时,使用 toRefs 保持响应性:

javascript
import { reactive, toRefs } from 'vue'

function useUserState() {
  const state = reactive({
    user: null,
    loading: false,
    error: null
  })
  
  // 保持响应性的解构
  return {
    ...toRefs(state),
    // 其他方法...
  }
}

计算属性和侦听器

computed 的高级用法

javascript
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 只读计算属性
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// 可写计算属性
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(value) {
    [firstName.value, lastName.value] = value.split(' ')
  }
})

watch 和 watchEffect

javascript
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const doubled = ref(0)

// watch: 明确指定依赖
watch(count, (newValue, oldValue) => {
  console.log(`count changed from ${oldValue} to ${newValue}`)
})

// watchEffect: 自动收集依赖
watchEffect(() => {
  doubled.value = count.value * 2
})

// 侦听多个源
watch([count, doubled], ([newCount, newDoubled]) => {
  console.log(`count: ${newCount}, doubled: ${newDoubled}`)
})

最佳实践

1. 组合函数的设计原则

单一职责

每个组合函数应该只负责一个特定的功能:

javascript
// ✅ 好的做法:单一职责
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = 0
  
  return { count, increment, decrement, reset }
}

function useLocalStorage(key, defaultValue) {
  const storedValue = localStorage.getItem(key)
  const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
  
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

// ❌ 不好的做法:职责混乱
function useCounterWithStorage() {
  // 混合了计数器和存储逻辑
}

返回值的一致性

javascript
// ✅ 推荐:返回对象,便于解构和重命名
function useApi(url) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const fetch = async () => {
    loading.value = true
    try {
      const response = await api.get(url)
      data.value = response.data
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }
  
  return {
    data,
    loading,
    error,
    fetch
  }
}

// 使用时可以重命名
const { data: userData, loading: userLoading } = useApi('/users')

2. 生命周期钩子的使用

javascript
import { onMounted, onUnmounted, ref } from 'vue'

function useEventListener(target, event, handler) {
  onMounted(() => {
    target.addEventListener(event, handler)
  })
  
  onUnmounted(() => {
    target.removeEventListener(event, handler)
  })
}

function useWindowSize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)
  
  const updateSize = () => {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }
  
  useEventListener(window, 'resize', updateSize)
  
  return { width, height }
}

3. 错误处理

javascript
function useAsyncOperation() {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const execute = async (operation) => {
    loading.value = true
    error.value = null
    
    try {
      data.value = await operation()
    } catch (err) {
      error.value = err
      console.error('Operation failed:', err)
    } finally {
      loading.value = false
    }
  }
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    execute
  }
}

4. 性能优化

使用 shallowRef 和 shallowReactive

javascript
import { shallowRef, shallowReactive } from 'vue'

// 对于大型对象,如果只需要替换而不需要深度响应
const largeData = shallowRef({})

// 对于只需要第一层响应的对象
const config = shallowReactive({
  theme: 'dark',
  language: 'zh-CN',
  features: {
    // 这一层不会是响应式的
    enableNotifications: true
  }
})

合理使用 markRaw

javascript
import { markRaw, reactive } from 'vue'

const state = reactive({
  user: null,
  // 第三方库实例不需要响应式
  chartInstance: markRaw(new Chart())
})

实际应用示例

表单处理

javascript
import { ref, reactive, computed } from 'vue'

function useForm(initialValues, validationRules) {
  const values = reactive({ ...initialValues })
  const errors = reactive({})
  const touched = reactive({})
  
  const isValid = computed(() => {
    return Object.keys(errors).length === 0
  })
  
  const validate = (field) => {
    const rule = validationRules[field]
    if (rule) {
      const error = rule(values[field])
      if (error) {
        errors[field] = error
      } else {
        delete errors[field]
      }
    }
  }
  
  const handleChange = (field, value) => {
    values[field] = value
    touched[field] = true
    validate(field)
  }
  
  const handleSubmit = (onSubmit) => {
    // 验证所有字段
    Object.keys(validationRules).forEach(validate)
    
    if (isValid.value) {
      onSubmit(values)
    }
  }
  
  return {
    values,
    errors,
    touched,
    isValid,
    handleChange,
    handleSubmit
  }
}

数据获取

javascript
function useQuery(queryFn, options = {}) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  const { immediate = true, refetchOnWindowFocus = false } = options
  
  const execute = async () => {
    loading.value = true
    error.value = null
    
    try {
      data.value = await queryFn()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }
  
  if (immediate) {
    execute()
  }
  
  if (refetchOnWindowFocus) {
    useEventListener(window, 'focus', execute)
  }
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    refetch: execute
  }
}

总结

Composition API 为 Vue 3 带来了强大的逻辑组织能力。通过遵循以下原则,我们可以写出更加清晰、可维护的代码:

  1. 单一职责:每个组合函数专注于一个功能
  2. 一致的返回值:使用对象返回,便于解构和重命名
  3. 合理的抽象:在复用性和简单性之间找到平衡
  4. 性能考虑:合理使用 shallow 系列 API
  5. 错误处理:提供完善的错误处理机制

Composition API 不是银弹,但它确实为我们提供了更多的可能性。在实际项目中,我们可以根据具体需求选择最合适的方案。


相关文章推荐:

用代码构建未来,让技术改变世界 🚀 | 专注前端技术,分享开发经验