4.2 Pinia跨端状态方案


4.2 Pinia 跨端状态方案

useState 解决"轻状态跨端",但购物车、用户会话、多实体关联这类复杂状态需要专业的状态库:分模块、派生值、开发者工具、持久化。本节讲 Pinia 在 Nuxt 里的接入与两端协同,并给出它与 useState 的分工判据——这是架构层面的选择,直接影响 4.3 数据层封装的形态。

接入与定义 store

# 安装 Pinia 与 Nuxt 官方集成模块 npm install @pinia/nuxt pinia
// nuxt.config.ts export default defineNuxtConfig({ modules: ['@pinia/nuxt'], })

模块机制(第 7 章展开)在这里先当一个黑盒:一行配置,插件注入、自动导入全部就位。定义一个购物车 store:

// stores/cart.ts export const useCartStore = defineStore('cart', () => { // 状态:条目清单 const items = ref<{ id: number; name: string; price: number; qty: number }[]>([]) // 派生:总件数与总价——组件直接用,不用各自计算 const totalCount = computed(() => items.value.reduce((s, i) => s + i.qty, 0)) const totalPrice = computed(() => items.value.reduce((s, i) => s + i.qty * i.price, 0)) // 行为:加购与清空 function add(product: { id: number; name: string; price: number }) { const found = items.value.find(i => i.id === product.id) if (found) found.qty++ else items.value.push({ ...product, qty: 1 }) } function clear() { items.value = [] } return { items, totalCount, totalPrice, add, clear } })

组件里零成本使用:

<script setup> const cart = useCartStore() // 自动导入,无需 import </script> <template> <button @click="cart.add({ id: 1, name: '机械键盘 Pro', price: 399 })"> 加入购物车 </button> <span>共 {{ cart.totalCount }} 件,合计 {{ cart.totalPrice }} 元</span> </template>

setup 风格的 store 把状态(ref)、派生(computed)、行为(function)收在一个函数里,与组件心智一致,是当前社区的主流写法。

两端协同的关键:服务端取数进 store

跨端交接的问题照样存在:SSR 时如果购物车数据在服务端组装好,客户端水合时怎么拿到?答案是官方推荐的约定——用 useAsyncData 触发 store 的取数行为,序列化交给框架:

// composables/useCartInit.ts // 首屏需要购物车数据时的标准姿势:useAsyncData 管交接,store 管存放 export const useCartInit = () => { const cart = useCartStore() return useAsyncData('cart-init', async () => { const remote = await $fetch('/api/cart') cart.items = remote.items // 服务端执行:数据进 store return true }) }

管线视角看这个过程:SSR 时 useAsyncData 执行,store 的 items 被填充;Nuxt 的 Pinia 集成会把 store 状态序列化进 payload;客户端水合时,store 从 payload 恢复初始值,useAsyncData 命中缓存不再执行。于是两端共享同一份起点,后续的加购操作在客户端继续。

反例对照——直接在组件 setup 里 await cart.fetchCart()(store 内部自己发请求):数据照样能显示,但绕过了 useAsyncData 的 payload 通道,客户端水合时会再请求一遍。这就是第 3 章裸 fetch 问题的 store 版:状态容器不能替代交接机制,两者要配合。

useState 还是 Pinia:分工判据

判断维度 useState Pinia
状态复杂度 单个值或小对象 多实体、多 store、相互关联
派生需求 无或简单 大量 computed 组合
行为归属 少量散落函数 集中的 actions
工具需求 DevTools 时间旅行、持久化插件
引入成本 零(内置) 一个依赖加一个模块

经验法则:先 useState,痛了再迁移。计数器、主题开关、临时的 UI 状态,useState 五行解决;一旦发现状态间开始互相计算、多个页面要写同样的修改逻辑,就是迁 Pinia 的信号。两者可以共存——UI 临时状态留 useState,业务实体进 store,不必二选一。

持久化是常见追问:购物车希望刷新后还在,用社区的持久化插件在客户端把 store 写进 localStorage:

// stores/cart.ts 中追加持久化配置(使用 pinia-plugin-persistedstate 时) defineStore('cart', { /* ... */ }, { persist: true })

注意持久化发生在客户端:服务端渲染时读不到 localStorage,首屏仍以 payload 或默认值为准,水合后插件恢复本地状态——顺序上先有服务器版本再有本地覆盖,页面可能闪一次,对购物车角标这类元素通常可接受。

排错:store 在服务端被共享污染

一个隐蔽的坑值得单独讲:模块级变量在服务端是进程级共享的。如果你在 store 文件外再写一个顶层 const cache = new Map() 想做全局缓存,SSR 高并发下,A 用户的数据可能被 B 用户读到。Nuxt 为每个请求创建新的 nuxtApp 与新的 store 实例,框架内是安全的;自己造的进程级全局容器则绕开了这层保护。原则:跨请求的共享只允许通过 Nitro 的 useStorage(第 6 章)这类显式缓存 API,绝不裸写模块级可变状态

图 4-2:每请求一实例 vs 进程级共享

图 4-2:每请求一实例 vs 进程级共享

⚠️ 常见坑:store 里存了带函数的对象再指望它跨端。与 useState 同样的序列化边界——payload 只搬纯数据,行为(actions)在两端各自定义,状态通过网络传输。

💡 关键直觉:Pinia 在 Nuxt 里的正确姿势是"store 管状态的组织形式,useAsyncData 管状态的跨端运输"——组织与运输分离,两者各司其职时最顺。

本节要点回顾

  • 接入三步:装依赖、注册模块、defineStore 定义,setup 风格把状态派生行为收拢一处;
  • 服务端取数标准姿势:useAsyncData 包住 store 填充动作,payload 序列化由框架完成,客户端不重取;
  • 反模式:store 内部自发请求且不经 useAsyncData——数据能显示但客户端必然重复请求;
  • 分工判据:轻状态 useState、业务实体 Pinia,可共存不冲突;
  • 持久化在客户端:localStorage 水合后恢复,首屏以服务器版本为准;
  • 进程级共享陷阱:服务端模块级可变状态会跨用户污染,共享缓存走 Nitro useStorage。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U