组合式函数封装 #
为什么需要封装? #
将 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