9.1 核心概念
- SSR(服务端渲染)
- 服务器把组件渲染成 HTML 字符串返回,首屏更快、利于 SEO;随后客户端"接管"。
- Hydration(注水)
- 客户端用服务端传来的初始 state 恢复响应式,让已有 DOM"活"起来,避免重新渲染闪烁。
- 每请求实例
- SSR 中每个 HTTP 请求必须有独立的 Pinia 实例,否则不同用户的状态会串台。
- 状态污染
- 在服务端把 store 定义成模块级单例并跨请求共享,导致 A 用户看到 B 用户数据的严重 bug。
- @pinia/nuxt
- Nuxt 官方模块,自动创建每请求 Pinia 实例、自动导入 store、处理注水。
- pinia.state.value
- Pinia 根实例的整个 state 树,SSR 时序列化它注入 HTML,客户端再读回。
9.2 手动 SSR:每请求新建实例
在自建 SSR(Vite SSR / vue-server-renderer)中,每个请求都要 createPinia(),绝不能共用:
// server entry:每个请求调用一次
import { createSSRApp } from "vue";
import { createPinia } from "pinia";
export function createApp() {
const app = createSSRApp(App);
const pinia = createPinia(); // ← 每请求独立
app.use(pinia);
return { app, pinia };
}
9.3 注水流程
// server:渲染后序列化 state 注入 HTML
const { app, pinia } = createApp();
const html = await renderToString(app);
const state = JSON.stringify(pinia.state.value);
// 注入到页面:window.__pinia = {...}
return `<div id="app">${html}</div>
<script>window.__pinia=${state}</script>`;
// client:接管前把 state 读回 Pinia
const { app, pinia } = createApp();
if (window.__pinia) {
pinia.state.value = window.__pinia; // 注水
}
app.mount("#app");
跨请求状态污染(最危险的坑)
如果在服务端模块顶层调用 const store = useUserStore(),该 store 会在所有请求间共享——A 登录后 B 请求会看到 A 的用户信息。务必在请求处理函数内部创建 pinia 并调用 use 函数。
9.4 Nuxt 集成:@pinia/nuxt
$ npm i pinia @pinia/nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@pinia/nuxt"],
});
该模块自动完成:每请求创建 Pinia、注入到 Nuxt app、注水,并支持自动导入 stores/ 下的 store(无需手动 import)。
// stores/counter.ts —— Nuxt 中直接写
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const inc = () => count.value++;
return { count, inc };
});
<!-- pages/index.vue:useCounterStore 无需 import(自动导入)-->
<script setup lang="ts">
const counter = useCounterStore();
</script>
<template>
<button @click="counter.inc()">{{ counter.count }}</button>
</template>
9.5 在 Nuxt 中做服务端数据获取
用 useAsyncData / callOnce 在服务端填充 store,注水后客户端不再重复请求:
<script setup lang="ts">
const store = useProductStore();
// SSR 时执行一次,结果随注水带到客户端
await useAsyncData("products", () => store.fetchAll());
</script>
action 里别用浏览器 API
SSR 阶段 action 运行在 Node 中,访问 window/localStorage/document 会报错。用 import.meta.client(Nuxt)或 typeof window !== "undefined" 做守卫。
9.6 持久化与 SSR
| 问题 | 说明 |
|---|---|
| localStorage 服务端不可用 | 持久化插件应只在客户端读写,SSR 阶段跳过 |
| 注水不匹配警告 | 服务端与客户端初始 state 不一致会报 hydration mismatch |
| Cookie 存储 | 需在服务端可读的状态(如 token)用 cookie,而非 localStorage |
9.7 小结与下一步
- SSR 的铁律:每请求一个 Pinia 实例,绝不模块级共享 store。
- 注水 = 服务端序列化
pinia.state.value,客户端读回后 mount。 - Nuxt 用
@pinia/nuxt一键搞定实例隔离、自动导入与注水。 - 最后一章进入 实战与最佳实践——购物车/用户 store、测试、DevTools、Vuex 迁移。