Chapter 09

SSR 与 Nuxt

在服务端渲染中安全使用 Pinia——理解注水机制、每请求隔离实例、Nuxt 集成,以及跨请求状态污染这一致命陷阱。

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 小结与下一步