Vue3状态管理实战:Pinia store设计与组件通信方案

PiniaVue3官方推荐的状态管理库,取代Vuex成为默认选项。相比Vuex,Pinia去掉了Mutation层,原生支持TypeScript与组合式API,模块化天然按store切分。实际项目里的问题集中在store怎么拆分、跨组件通信怎么选型、持久化怎么做,这些决定代码的可维护性。

Store拆分设计:按业务域划分与组合式写法

Store拆分遵循业务域原则:用户相关一个store、购物车一个store、订单一个store,不要按组件层级拆。每个store用组合式写法定义,代码组织更清晰。

// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
    // state
    const items = ref<CartItem[]>([])
    const couponCode = ref('')

    // getters
    const totalCount = computed(() =>
        items.value.reduce((sum, item) => sum + item.quantity, 0)
    )
    const totalPrice = computed(() =>
        items.value.reduce(
            (sum, item) => sum + item.price * item.quantity, 0
        )
    )

    // actions
    function addItem(product: Product, quantity = 1) {
        const existing = items.value.find(i => i.id === product.id)
        if (existing) {
            existing.quantity += quantity
        } else {
            items.value.push({ ...product, quantity })
        }
    }

    function removeItem(id: number) {
        items.value = items.value.filter(i => i.id !== id)
    }

    return { items, couponCode, totalCount, totalPrice, addItem, removeItem }
})

组合式API写法的store里,ref就是state,computed就是getter,普通函数就是action,三层概念一目了然。store之间可以互相调用,订单store里直接useCartStore()取购物车数据做结算,不需要像Vuex那样注册模块命名空间。

组件通信方案选型:props、provide与store的使用边界

不是所有数据都该进store。选型边界:父子组件传参用props和emit;跨层级但不全局的特征数据用provide/inject;全局共享的会话数据才进store。判断标准是数据被多少不相干的组件用到。

// 边界示例:主题色只影响部分组件,用provide
// 祖先组件
provide('theme', 'dark')

// 后代组件
const theme = inject<string>('theme', 'light')

// 登录用户信息全局多处使用,进store
const userStore = useUserStore()
const isLoggedIn = computed(() => !!userStore.token)

常见反模式是把表单的每个输入框状态都塞进store,导致store变成全局变量垃圾场,任何一处修改都触发无关组件更新。表单的临时状态留在组件内部,只在提交时把结果交给store。

持久化与SSR适配:pinia-plugin-persistedstate配置

购物车这类数据刷新后不能丢,用pinia-plugin-persistedstate做持久化。Nuxt或SSR项目要注意服务端没有localStorage,插件需要客户端启用。

// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)

// store内声明持久化配置
export const useCartStore = defineStore('cart', () => {
    // ...省略state与actions
}, {
    persist: {
        key: 'shop-cart',
        storage: localStorage,
        pick: ['items', 'couponCode']  // 只持久化需要的字段
    }
})

pick字段控制持久化范围,token类敏感字段单独评估,需要跨标签页共享就放localStorage,仅当前会话有效就改sessionStorage。SSR项目里把storage判断换成import.meta.client的条件执行,避免服务端报错。

异步action与错误处理:API调用状态管理模式

异步请求的loading、error状态建议放在store里统一管理,组件直接消费,避免每个组件都写一套try catch。

export const useOrderStore = defineStore('order', () => {
    const orders = ref<Order[]>([])
    const loading = ref(false)
    const error = ref<string | null>(null)

    async function fetchOrders() {
        loading.value = true
        error.value = null
        try {
            const res = await api.getOrders()
            orders.value = res.data
        } catch (e: any) {
            error.value = e.message ?? '加载失败'
        } finally {
            loading.value = false
        }
    }

    return { orders, loading, error, fetchOrders }
})

组件里直接绑定loading渲染骨架屏,绑定error显示重试按钮。多个组件同时触发fetchOrders时,可以在函数入口加loading判断做防重入,或者用AbortController取消上一次未完成请求,避免竞态导致旧数据覆盖新数据。

性能注意点:store解构丢失响应式与订阅调试

从store解构state会丢响应式,必须用storeToRefs。actions可以直接解构,因为它们是普通函数。

const cartStore = useCartStore()
// 正确:state与getter保持响应式
const { items, totalPrice } = storeToRefs(cartStore)
// actions直接解构没问题
const { addItem } = cartStore

// 订阅变化做调试或埋点
cartStore.$subscribe((mutation, state) => {
    console.log(mutation.type, mutation.storeId)
})

$subscribe适合做状态变更日志和埋点上报,生产环境里可以顺手把关键操作同步到监控。devtools集成开箱即用,Vue Devtools里能直接看到每个store的状态快照与时间旅行,排查状态问题比console.log高效得多。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zhuang-tai-guan-li-shi-zhan-piniastore-she-ji-yu-zu/

(0)
小编小编
上一篇 1天前
下一篇 1天前

相关推荐