- AI 技能
- 人工智能
【免费下载链接】skills
Anthony Fu's curated collection of agent skills.
Pinia 是 Vue 官方的状态管理库,本指南聚焦其在服务端渲染(SSR)场景下的正确使用方式:如何在setup、getters、actions中安全调用 store,如何在组件之外(如路由守卫)传入pinia实例,以及如何通过devalue实现 XSS 安全的服务端状态序列化与客户端水合。读完本文,你将掌握一套可直接落地的 Pinia + SSR 架构方案,能够避免跨请求状态污染、正确编写serverPrefetch/onServerPrefetch数据预取逻辑,并理解 Nuxt 集成时自动处理 SSR 的差异点。
本文基于当前仓库 skills/pinia/SKILL.md 所述 Pinia v4.0.3(生成于 2026-09-25)及 advanced-ssr.md 参考文档展开;若你正在使用 Nuxt,建议同时阅读 Nuxt 集成指南。
为什么 Pinia 能支持 SSR
Pinia 之所以能够很好地服务 SSR,核心在于它对「当前活跃的 pinia 实例」的追踪机制。在组件上下文中,useStore()无需显式传参即可工作,因为 Pinia 能够感知 Vue 的组件注入上下文;而在组件之外,则必须显式传入pinia实例。参考文档 best-practices-outside-component.md 明确指出:
Stores need the
piniainstance, which is automatically injected in components. Outside components, you may need to provide it manually.
这条原则是全部 SSR 实践的基石:store 的调用必须发生在 pinia 实例已经创建并安装(app.use(pinia))之后,而不是模块顶层。理解了这一点,下面所有场景的写法就都顺理成章了。
基本用法:在 setup 中调用 store
Pinia 在 SSR 下工作的前提是:在setup、getters或actions的顶层调用 store。在<script setup>中直接调用useStore()是安全的,因为此时 Pinia 能够通过 Vue 的组件上下文感知到当前应用对应的 pinia 实例:
<script setup> // ✅ Works - pinia knows the app context in setup const main = useMainStore() </script>为什么不能把 store 调用放在模块顶层
把useStore()提升到模块作用域是 SSR 场景下最常见的错误。原因有二:
- 模块作用域的执行时机不可控:模块顶层代码在模块被
import时立即执行,此时 pinia 可能尚未创建或安装(app.use(pinia)尚未执行),调用必然失败或拿到错误实例; - 多个请求共享同一模块实例:服务端是长驻进程,模块顶层只执行一次,若在这里创建 store,所有并发请求会共享同一份状态,直接导致跨请求状态污染。
参考文档 core-stores.md 中关于 actions 的 SSR 规范进一步补充了一个重要细节:
SSR:Call all
useStore()before anyawait
async orderCart() { // ✅ Call stores before await const user = useUserStore() await apiOrderCart(user.token, this.items) // ❌ Don't call useStore() after await in SSR }在异步 action 中,所有useStore()调用必须放在第一个await之前。因为await之后执行上下文可能已切换到另一个请求,此时再调用 store 很可能拿不到当前请求对应的 pinia 实例。这也解释了为什么依赖注入(inject)在 Setup Store 中可用——它们与组件上下文绑定,同样必须在同步阶段完成。
在 setup() 之外使用 store:显式传入 pinia 实例
路由守卫、插件、中间件等非组件上下文无法自动获得 pinia 实例,必须显式将pinia传给useStore():
const pinia = createPinia() const app = createApp(App) app.use(router) app.use(pinia) router.beforeEach((to) => { // ✅ Pass pinia for correct SSR context const main = useMainStore(pinia) if (to.meta.requiresAuth && !main.isLoggedIn) { return '/login' } })与单页应用的对比:导航守卫内的两种写法
参考文档 best-practices-outside-component.md 给出了两种合法写法,理解它们的差异能帮你写出既适用于 SPA 又适用于 SSR 的代码:
写法一:守卫内部不传参(SPA 适用)
router.beforeEach((to) => { // ✅ Called after pinia is installed const store = useUserStore() if (to.meta.requiresAuth && !store.isLoggedIn) { return '/login' } })写法二:守卫内部显式传参(SPA / SSR 通用)
router.beforeEach((to) => { // ✅ Pass pinia instance const main = useMainStore(pinia) if (to.meta.requiresAuth && !main.isLoggedIn) { return '/login' } })关键区别在于:写法一依赖全局的「活跃 pinia」推断(适用于单页应用),写法二则通过闭包捕获的pinia实例精确绑定到当前应用,因此在 SSR 场景下始终推荐显式传参。无论哪种写法,都必须把useStore()放在守卫回调函数内部执行,而不是放在createRouter()之后的模块顶层——后者会因 import 顺序不确定而间歇性失败。
错误示范:模块顶层调用
import { createRouter } from 'vue-router' const router = createRouter({ /* ... */ }) // ❌ May fail depending on import order const store = useUserStore() router.beforeEach((to) => { if (store.isLoggedIn) { /* ... */ } })服务端数据预取:serverPrefetch() 与 onServerPrefetch()
SSR 的核心价值之一是首屏直出数据。Pinia 参考文档提供了两种在组件中触发数据预取的姿势:
选项式 API:serverPrefetch()
在选项式组件中,通过this.$pinia访问 pinia 实例:
export default { serverPrefetch() { const store = useStore(this.$pinia) return store.fetchData() }, }这里this.$pinia是 Vue 应用实例上由 Pinia 注入的全局属性,返回store.fetchData()的 Promise 可以让服务端等待数据就绪后再渲染 HTML。
组合式 API:onServerPrefetch()
在<script setup>中,onServerPrefetch()正常工作,且因为组件在 setup 阶段执行,可以直接无参调用useStore():
<script setup> const store = useStore() onServerPrefetch(async () => { await store.fetchData() }) </script>两种方式本质相同:预取动作发生在服务端渲染该组件时,数据写入 store 的 state,随后由水合流程传递给客户端。具体采用哪种,取决于你的组件是选项式还是组合式 API 风格。
状态水合(State Hydration):服务端序列化与客户端注入
SSR 的关键闭环是:服务端渲染时把 store 的 state 序列化进 HTML,客户端启动时在调用任何useStore()之前把它还原进 pinia。
服务端:序列化 state 并注入 HTML
服务端在完成渲染后,从pinia.state.value取出全部 state。参考文档明确建议使用 devalue 进行XSS 安全的序列化——相比JSON.stringify,devalue 能正确处理Date、Map、Set、循环引用等JSON无法表达的数据结构,同时避免</script>注入等安全问题:
import devalue from 'devalue' import { createPinia } from 'pinia' const pinia = createPinia() const app = createApp(App) app.use(router) app.use(pinia) // After rendering, state is available const serializedState = devalue(pinia.state.value) // Inject into HTML as global variable在 HTML 模板中,通常将serializedState嵌入为一个全局变量,例如:
<script> window.__pinia = <%- serializedState %> </script>注意这里必须使用 devalue 而非JSON.stringify直接拼接字符串,否则 store 状态中含有的特殊字符可能破坏script标签结构,形成 XSS 漏洞。
客户端:在调用任何 useStore() 之前水合
客户端在创建 pinia 后、任何组件调用useStore()之前,把window.__pinia中的数据还原进pinia.state.value:
const pinia = createPinia() const app = createApp(App) app.use(pinia) // Hydrate from serialized state (e.g., from window.__pinia) if (typeof window !== 'undefined') { pinia.state.value = JSON.parse(window.__pinia) }时序至关重要:水合动作必须发生在任何useStore()调用之前。如果某个组件先于水合执行了useStore(),store 会以初始值创建,服务端状态将被丢弃,导致首屏闪现空数据(hydration mismatch)。
避免跨请求状态污染:每个请求一个全新的 pinia
这是 SSR 中最容易踩坑、后果最严重的点。Node.js 服务端进程被多个并发请求共享,而模块顶层的const pinia = createPinia()只会执行一次——如果复用该实例,请求 A 写入的 state 会被请求 B 读取到,造成用户数据串号。
正确做法是:在服务端每个请求的入口创建一个全新的 pinia 实例,并在请求结束后随上下文一起释放:
// 服务端入口伪代码(每个请求执行一次) const pinia = createPinia() // ✅ 每个请求全新的实例 const app = createApp(App) app.use(pinia) // 渲染完成后:序列化当前请求的 state const serializedState = devalue(pinia.state.value)这正是参考文档 Key Points 中「Avoid cross-request state pollution by creating fresh pinia per request」的含义。与之呼应的是 features-plugins.md 中插件为 store 添加初始 state 时的 SSR 守卫写法:
pinia.use(({ store }) => { // guard against overriding an existing value (important for SSR) if (!Object.hasOwn(store.$state, 'hasError')) { const hasError = ref(false) store.$state.hasError = hasError } store.hasError = toRef(store.$state, 'hasError') })服务端水合后 state 已存在,插件若不检查Object.hasOwn而直接赋值,就会覆盖掉服务端渲染好的状态。同理,插件为 store 注入外部非响应式对象(如 router 实例)时应使用markRaw()包裹,避免其被意外序列化进水合数据:
import { markRaw } from 'vue' import { router } from './router' pinia.use(({ store }) => { store.router = markRaw(router) })服务端渲染的完整骨架示例
综合以上要点,一个最小的 Pinia SSR 应用可以组织为:
// 服务端(每个请求执行一次) import devalue from 'devalue' import { createPinia } from 'pinia' import { createApp } from 'vue' import App from './App.vue' import { router } from './router' export async function renderRequest(req) { const pinia = createPinia() // ① 每请求新实例,隔离状态 const app = createApp(App) app.use(router) app.use(pinia) router.beforeEach((to) => { const main = useMainStore(pinia) // ② 守卫中显式传参 if (to.meta.requiresAuth && !main.isLoggedIn) { return '/login' } }) // ③ 路由解析、组件渲染(serverPrefetch / onServerPrefetch 在此阶段执行) await router.push(req.url) await router.isReady() // ④ 序列化当前请求的 state const serializedState = devalue(pinia.state.value) return { html: /* 渲染结果 */, state: serializedState, // ⑤ 注入 window.__pinia } }客户端入口则对应执行「创建 pinia → 水合window.__pinia→ 安装应用」的顺序。
Nuxt 用户:使用官方集成
如果你在 Nuxt 3/4 中工作,参考文档 advanced-nuxt.md 说明:@pinia/nuxt模块会自动处理 SSR、序列化与 XSS 防护,你无需手工实现本文中的水合流程。
npx nuxi@latest module add pinia// nuxt.config.ts export default defineNuxtConfig({ modules: ['@pinia/nuxt'], })Nuxt 会自动注入usePinia()、defineStore()、storeToRefs()、acceptHMRUpdate()等 API,stores/目录中的 store 会被自动导入;在中间件等非组件上下文,通过useNuxtApp().$pinia获取实例:
// middleware/auth.ts export default defineNuxtRouteMiddleware((to) => { const nuxtApp = useNuxtApp() const store = useStore(nuxtApp.$pinia) if (to.meta.requiresAuth && !store.isLoggedIn) { return navigateTo('/login') } })页面数据预取则推荐使用callOnce()实现 SSR 友好的「仅执行一次」语义:
<script setup> const store = useStore() // Run once, data persists across navigations await callOnce('user', () => store.fetchUser()) </script>配套的最佳实践清单
结合参考文档,以下规则构成完整的 Pinia SSR 实践闭环:
- 在函数内部调用 store,而不是模块作用域——模块顶层调用既拿不到实例,又会造成跨请求共享状态(见 best-practices-outside-component.md);
- 在组件之外使用 store 时显式传入
pinia实例——路由守卫、中间件、插件中一律useStore(pinia); - 在调用任何
useStore()之前完成状态水合——水合时序错误会导致服务端状态丢失与 hydration mismatch; - 使用
devalue或同类工具做安全序列化——JSON.stringify无法覆盖Date、Map、循环引用等类型,且直接拼接字符串存在 XSS 风险; - 每个请求创建全新的 pinia 实例——杜绝跨请求状态污染,这是 SSR 应用数据安全的第一道防线;
- 在 async action 中,所有
useStore()调用放在第一个await之前(见 core-stores.md); - 插件添加初始 state 前检查
Object.hasOwn、外部对象用markRaw包裹,避免干扰水合(见 features-plugins.md); - 使用 Nuxt 时交给
@pinia/nuxt处理水合细节,仅在中间件中通过$pinia获取实例(见 advanced-nuxt.md)。
关键要点总结(Key Points)
- Call stores inside functions, not at module scope —— 在函数内部调用 store,而非模块作用域;
- Pass
piniainstance when using stores outside components in SSR —— 组件之外必须显式传pinia; - Hydrate state before calling any
useStore()—— 水合先于一切 store 调用; - Use
devalueor similar for safe serialization —— 用 devalue 做 XSS 安全的序列化; - Avoid cross-request state pollution by creating fresh pinia per request —— 每请求新建 pinia,隔离跨请求状态。
把这五条内化为习惯,再配合 Nuxt 集成指南 与 store 核心概念 阅读,你就掌握了 Pinia 在服务端渲染场景下的完整正确姿势。
- AI 技能
- 人工智能
【免费下载链接】skills
Anthony Fu's curated collection of agent skills.
相关推荐
Airi 中 Pinia 的服务端渲染实践:setup 中的 Store、状态水合与跨请求隔离
Airi 中 Pinia 的服务端渲染实践:setup 中的 Store、状态水合与跨请求隔离 本文以 Airi 仓库中的 Pinia SSR 参考文档 adv
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染Pinia SSR 服务端渲染指南:状态隔离、序列化与 Hydration 完整实践
Pinia SSR 服务端渲染指南:状态隔离、序列化与 Hydration 完整实践 本篇技术指南聚焦 Pinia 在服务端渲染(SSR)场景下的完整实战方案:
前端状态管理Lit Query 服务端渲染(SSR)与客户端水合(Hydration)完整实践指南
Lit Query 服务端渲染(SSR)与客户端水合(Hydration)完整实践指南 Lit Query( @tanstack/lit query )不仅面向
前端缓存状态管理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考