组合式函数封装 #

为什么需要封装? #

将 Vuex 状态逻辑封装成组合式函数可以:

  • 提高代码复用性
  • 简化组件代码
  • 更好的类型支持
  • 便于测试

基础封装模式 #

简单封装 #

javascript
// composables/useCounter.js
import { computed } from 'vue'
import { useStore } from 'vuex'

export function useCounter() {
  const store = useStore()
  
  const count = computed(() => store.state.count)
  const doubleCount = computed(() => store.getters.doubleCount)
  
  const increment = () => store.commit('INCREMENT')
  const decrement = () => store.commit('DECREMENT')
  
  return {
    count,
    doubleCount,
    increment,
    decrement
  }
}

使用封装 #

vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

<script>
import { useCounter } from '@/composables/useCounter'

export default {
  setup() {
    return useCounter()
  }
}
</script>

模块封装 #

完整模块封装 #

javascript
// composables/useUserStore.js
import { computed, ref } from 'vue'
import { useStore } from 'vuex'

export function useUserStore() {
  const store = useStore()
  
  // ========== State ==========
  const profile = computed(() => store.state.user.profile)
  const token = computed(() => store.state.user.token)
  const loading = computed(() => store.state.user.loading)
  const error = computed(() => store.state.user.error)
  
  // ========== Getters ==========
  const isLoggedIn = computed(() => store.getters['user/isLoggedIn'])
  const userName = computed(() => store.getters['user/userName'])
  const userEmail = computed(() => store.getters['user/userEmail'])
  
  // ========== Mutations ==========
  const setProfile = (profile) => {
    store.commit('user/SET_PROFILE', profile)
  }
  
  const setToken = (token) => {
    store.commit('user/SET_TOKEN', token)
  }
  
  const clearUser = () => {
    store.commit('user/CLEAR_USER')
  }
  
  // ========== Actions ==========
  const login = async (credentials) => {
    return await store.dispatch('user/login', credentials)
  }
  
  const logout = async () => {
    await store.dispatch('user/logout')
  }
  
  const fetchProfile = async () => {
    return await store.dispatch('user/fetchProfile')
  }
  
  const updateProfile = async (data) => {
    return await store.dispatch('user/updateProfile', data)
  }
  
  return {
    // State
    profile,
    token,
    loading,
    error,
    
    // Getters
    isLoggedIn,
    userName,
    userEmail,
    
    // Mutations
    setProfile,
    setToken,
    clearUser,
    
    // Actions
    login,
    logout,
    fetchProfile,
    updateProfile
  }
}

高级封装模式 #

带参数的封装 #

javascript
// composables/useEntity.js
import { computed, watch } from 'vue'
import { useStore } from 'vuex'

export function useEntity(moduleName) {
  const store = useStore()
  
  const state = computed(() => store.state[moduleName])
  const loading = computed(() => store.state[moduleName].loading)
  const error = computed(() => store.state[moduleName].error)
  
  const fetchAll = async (params) => {
    return await store.dispatch(`${moduleName}/fetchAll`, params)
  }
  
  const fetchOne = async (id) => {
    return await store.dispatch(`${moduleName}/fetchOne`, id)
  }
  
  const create = async (data) => {
    return await store.dispatch(`${moduleName}/create`, data)
  }
  
  const update = async ({ id, data }) => {
    return await store.dispatch(`${moduleName}/update`, { id, data })
  }
  
  const remove = async (id) => {
    return await store.dispatch(`${moduleName}/remove`, id)
  }
  
  return {
    state,
    loading,
    error,
    fetchAll,
    fetchOne,
    create,
    update,
    remove
  }
}

使用带参数的封装 #

javascript
// composables/useProducts.js
import { useEntity } from './useEntity'

export function useProducts() {
  const entity = useEntity('products')
  
  // 添加特定功能
  const searchProducts = async (query) => {
    return await entity.fetchAll({ search: query })
  }
  
  return {
    ...entity,
    searchProducts
  }
}

响应式 ID 封装 #

javascript
// composables/useEntityById.js
import { computed, watch, ref } from 'vue'
import { useStore } from 'vuex'

export function useEntityById(moduleName, idRef) {
  const store = useStore()
  const loading = ref(false)
  const error = ref(null)
  
  const entity = computed(() => {
    const id = idRef.value
    if (!id) return null
    return store.state[moduleName].byId[id]
  })
  
  const fetch = async () => {
    const id = idRef.value
    if (!id) return
    
    loading.value = true
    error.value = null
    
    try {
      await store.dispatch(`${moduleName}/fetchOne`, id)
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }
  
  // ID 变化时自动获取
  watch(idRef, fetch, { immediate: true })
  
  return {
    entity,
    loading,
    error,
    refetch: fetch
  }
}

使用响应式 ID 封装 #

vue
<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">{{ error }}</div>
  <div v-else-if="product">
    <h2>{{ product.name }}</h2>
    <p>{{ product.description }}</p>
  </div>
</template>

<script>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useEntityById } from '@/composables/useEntityById'

export default {
  setup() {
    const route = useRoute()
    const productId = computed(() => route.params.id)
    
    const { entity: product, loading, error } = useEntityById('products', productId)
    
    return {
      product,
      loading,
      error
    }
  }
}
</script>

分页封装 #

javascript
// composables/usePagination.js
import { computed, ref, watch } from 'vue'
import { useStore } from 'vuex'

export function usePagination(moduleName, options = {}) {
  const store = useStore()
  
  const page = ref(options.initialPage || 1)
  const perPage = ref(options.perPage || 10)
  
  const items = computed(() => store.state[moduleName].items)
  const total = computed(() => store.state[moduleName].total)
  const totalPages = computed(() => Math.ceil(total.value / perPage.value))
  const loading = computed(() => store.state[moduleName].loading)
  
  const hasNext = computed(() => page.value < totalPages.value)
  const hasPrev = computed(() => page.value > 1)
  
  const fetch = async () => {
    await store.dispatch(`${moduleName}/fetchAll`, {
      page: page.value,
      perPage: perPage.value
    })
  }
  
  const nextPage = () => {
    if (hasNext.value) {
      page.value++
      fetch()
    }
  }
  
  const prevPage = () => {
    if (hasPrev.value) {
      page.value--
      fetch()
    }
  }
  
  const goToPage = (p) => {
    page.value = p
    fetch()
  }
  
  const refresh = () => {
    page.value = 1
    fetch()
  }
  
  return {
    items,
    page,
    perPage,
    total,
    totalPages,
    loading,
    hasNext,
    hasPrev,
    fetch,
    nextPage,
    prevPage,
    goToPage,
    refresh
  }
}

表单封装 #

javascript
// composables/useForm.js
import { ref, computed, reactive } from 'vue'
import { useStore } from 'vuex'

export function useForm(moduleName, options = {}) {
  const store = useStore()
  
  const form = reactive(options.initialValues || {})
  const errors = ref({})
  const touched = ref({})
  const dirty = ref(false)
  const submitting = ref(false)
  
  const setValue = (field, value) => {
    form[field] = value
    dirty.value = true
  }
  
  const setTouched = (field) => {
    touched.value[field] = true
  }
  
  const setError = (field, error) => {
    errors.value[field] = error
  }
  
  const clearError = (field) => {
    delete errors.value[field]
  }
  
  const clearErrors = () => {
    errors.value = {}
  }
  
  const reset = () => {
    Object.assign(form, options.initialValues || {})
    errors.value = {}
    touched.value = {}
    dirty.value = false
  }
  
  const isValid = computed(() => Object.keys(errors.value).length === 0)
  
  const submit = async (actionName) => {
    if (!isValid.value) return
    
    submitting.value = true
    
    try {
      await store.dispatch(`${moduleName}/${actionName}`, form)
      dirty.value = false
    } catch (error) {
      if (error.errors) {
        errors.value = error.errors
      }
      throw error
    } finally {
      submitting.value = false
    }
  }
  
  return {
    form,
    errors,
    touched,
    dirty,
    submitting,
    isValid,
    setValue,
    setTouched,
    setError,
    clearError,
    clearErrors,
    reset,
    submit
  }
}

TypeScript 完整示例 #

typescript
// composables/useUserStore.ts
import { computed, ComputedRef, Ref } from 'vue'
import { useStore } from 'vuex'
import { User, RootState } from '@/types'

interface UseUserStore {
  // State
  profile: ComputedRef<User | null>
  token: ComputedRef<string | null>
  loading: ComputedRef<boolean>
  error: ComputedRef<string | null>
  
  // Getters
  isLoggedIn: ComputedRef<boolean>
  userName: ComputedRef<string>
  
  // Actions
  login: (credentials: LoginCredentials) => Promise<User>
  logout: () => Promise<void>
  fetchProfile: () => Promise<User>
  updateProfile: (data: Partial<User>) => Promise<User>
}

interface LoginCredentials {
  username: string
  password: string
}

export function useUserStore(): UseUserStore {
  const store = useStore<RootState>()
  
  return {
    // State
    profile: computed(() => store.state.user.profile),
    token: computed(() => store.state.user.token),
    loading: computed(() => store.state.user.loading),
    error: computed(() => store.state.user.error),
    
    // Getters
    isLoggedIn: computed(() => store.getters['user/isLoggedIn']),
    userName: computed(() => store.getters['user/userName']),
    
    // Actions
    login: async (credentials: LoginCredentials) => {
      return await store.dispatch('user/login', credentials)
    },
    
    logout: async () => {
      await store.dispatch('user/logout')
    },
    
    fetchProfile: async () => {
      return await store.dispatch('user/fetchProfile')
    },
    
    updateProfile: async (data: Partial<User>) => {
      return await store.dispatch('user/updateProfile', data)
    }
  }
}

最佳实践 #

1. 按功能组织 #

text
composables/
├── useUserStore.js
├── useCartStore.js
├── useProductStore.js
├── usePagination.js
└── useForm.js

2. 返回值解构 #

javascript
// 推荐:只返回需要的内容
export function useUser() {
  // ...
  return {
    user,
    isLoggedIn,
    login,
    logout
  }
}

// 不推荐:返回整个 store
export function useUser() {
  return useStore()
}

3. 命名规范 #

javascript
// 推荐:以 use 开头
export function useUserStore() {}
export function useCartStore() {}

// 不推荐:不一致的命名
export function getUserStore() {}
export function cartStore() {}

总结 #

组合式函数封装要点:

要点 说明
命名 use 开头
返回值 只返回需要的内容
响应式 使用 computed 保持响应式
类型 TypeScript 提供类型支持
复用 提取通用逻辑

继续学习 项目结构,了解企业级项目组织。

最后更新:2026-03-28