为什么Vue3项目需要重新审视状态管理
Vue3的组合式API(Composition API)从根本上改变了状态组织方式。ref和reactive提供了组件内部的响应式状态,Pinia作为官方状态管理库处理跨组件状态共享。但实际项目中,大量状态既不需要全局共享,又不适合散落在组件内部——它们需要在一个功能模块的多个组件间共享,且随组件卸载而清理。这正是自定义Composable发挥作用的场景。
Pinia Store设计的常见误区与修正
Pinia的扁平化Store设计比Vuex的模块系统更灵活,但也容易滥用。三个常见问题:
问题1:把所有状态塞进一个Store
// 错误:巨型Store
export const useAppStore = defineStore('app', () => {
const user = ref(null)
const token = ref('')
const products = ref([])
const cart = ref([])
const orders = ref([])
const theme = ref('light')
const notifications = ref([])
// ...50个状态和30个action
})
// 正确:按领域拆分Store
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const token = useStorage('token', '')
const isLoggedIn = computed(() => !!token.value)
return { user, token, isLoggedIn }
})
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const count = computed(() => items.value.length)
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0))
return { items, count, total }
})
问题2:Store中直接操作DOM或路由
// 错误:Store中不应有副作用
export const useUserStore = defineStore('user', () => {
const login = async (credentials) => {
const res = await api.login(credentials)
token.value = res.token
router.push('/dashboard') // 副作用!路由跳转不应在Store中
}
})
// 正确:Store只管状态,副作用在组件或中间件中处理
export const useUserStore = defineStore('user', () => {
const login = async (credentials) => {
const res = await api.login(credentials)
token.value = res.token
return res // 让调用方决定跳转
}
})
// 组件中
const handleLogin = async () => {
await userStore.login(form)
router.push('/dashboard')
}
自定义Composable:模块级状态共享的最佳方案
Composable函数的命名约定以use开头,返回响应式状态和方法。模块级状态共享的实现方式:
// composables/useProductList.ts
import { ref, computed, onScopeDispose } from 'vue'
// 模块级共享状态(在同一模块的多个组件间共享)
const products = ref([])
const loading = ref(false)
const error = ref(null)
let initialized = false
let activeConsumers = 0
export function useProductList() {
const filteredProducts = computed(() => {
return products.value.filter(p => p.status === 'active')
})
const fetchProducts = async () => {
if (initialized && products.value.length > 0) return // 避免重复请求
loading.value = true
error.value = null
try {
const res = await fetch('/api/products')
products.value = await res.json()
initialized = true
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
// 生命周期管理:最后一个消费者卸载时清理状态
activeConsumers++
onScopeDispose(() => {
activeConsumers--
if (activeConsumers <= 0) {
products.value = []
initialized = false
activeConsumers = 0
}
})
return {
products: filteredProducts,
allProducts: products,
loading,
error,
fetchProducts
}
}
Pinia + Composable的分层架构
生产项目的推荐分层方式:全局状态(用户信息、权限、主题)放Pinia Store,功能模块状态(商品列表、搜索条件、分页)放Composable,组件私有状态(表单输入、UI开关)放组件ref。
// 分层示例:商品搜索页
// 全局层 - Pinia Store
const userStore = useUserStore() // 用户信息、权限
// 模块层 - Composable
const { products, loading, fetchProducts } = useProductList()
const { filters, filtered, applyFilter } = useProductFilter(products)
// 组件层 - ref
const currentPage = ref(1)
const pageSize = ref(20)
const showFilterPanel = ref(false)
异步状态管理的防抖与缓存
Composable中处理异步状态时,竞态条件和重复请求是两大痛点:
// composables/useAsyncState.ts - 通用异步状态Composable
import { ref, shallowRef } from 'vue'
export function useAsyncState(fn, options = {}) {
const { debounceMs = 0, cacheMs = 0 } = options
const data = shallowRef(null)
const error = ref(null)
const isLoading = ref(false)
let lastFetchTime = 0
let pendingRequest = null
let debounceTimer = null
const execute = async (...args) => {
// 缓存检查
if (cacheMs > 0 && Date.now() - lastFetchTime < cacheMs && data.value) {
return data.value
}
// 取消上一次未完成的请求(防竞态)
if (pendingRequest) {
pendingRequest.aborted = true
}
const currentRequest = { aborted: false }
pendingRequest = currentRequest
// 防抖
if (debounceMs > 0) {
return new Promise((resolve) => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(async () => {
const result = await _execute(currentRequest, args)
resolve(result)
}, debounceMs)
})
}
return _execute(currentRequest, args)
}
const _execute = async (request, args) => {
isLoading.value = true
error.value = null
try {
const result = await fn(...args)
if (!request.aborted) {
data.value = result
lastFetchTime = Date.now()
}
return result
} catch (e) {
if (!request.aborted) {
error.value = e
}
throw e
} finally {
if (!request.aborted) {
isLoading.value = false
pendingRequest = null
}
}
}
return { data, error, isLoading, execute }
}
// 使用示例
const { data: searchResults, isLoading, execute: search } = useAsyncState(
(query) => fetch('/api/search?q=' + query).then(r => r.json()),
{ debounceMs: 300, cacheMs: 60000 }
)
SSR场景下的状态序列化
Nuxt3或手动SSR场景中,服务端的状态需要序列化传递到客户端。Pinia通过pinia.state.value自动处理,但自定义Composable需要手动处理:
// SSR状态注入与提取
// server端
const pinia = createPinia()
// ...执行数据获取
const state = JSON.stringify(pinia.state.value)
// 注入HTML
// <script>window.__PINIA_STATE__ = ${state}</script>
// client端
const pinia = createPinia()
if (window.__PINIA_STATE__) {
pinia.state.value = window.__PINIA_STATE__
delete window.__PINIA_STATE__
}
自定义Composable的SSR状态传递需借助useState(Nuxt3提供)或将状态提升到Pinia Store中管理。核心原则:服务端渲染期间的状态必须在组件setup阶段同步获取,异步操作需要用useAsyncData或useFetch包裹。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zhuang-tai-guan-li-mo-shi-cong-pinia-dao/