Pinia 在 Nuxt4 中的深度集成

当应用状态超出 useState 的简单场景——需要 getter 派生、action 封装、DevTools 调试、持久化存储——就该上 Pinia 了。Pinia 是 Vue 官方推荐的状态管理库,而 @pinia/nuxt 让它与 Nuxt4 无缝集成:自动注册、SSR 安全、Store 自动导入、HMR 热更新。本章从安装配置讲到 Setup Store 最佳实践、持久化 SSR 兼容,构建一套生产级状态管理方案。

1. @pinia/nuxt 配置

1.1 安装

pnpm add pinia @pinia/nuxt

1.2 注册模块

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',
  ],
  // 可选:配置 Pinia 自动导入
  pinia: {
    storesDirs: ['./app/stores/**'],  // Store 文件自动导入目录
  },
})

1.3 @pinia/nuxt 做了什么

  • ✅ 自动创建 Pinia 实例并注册到 Vue 应用
  • ✅ SSR 支持:服务端创建的 Store 状态自动序列化到 Payload
  • ✅ Store 文件自动导入(无需手动 import)
  • ✅ DevTools 集成
  • ✅ HMR 热更新

2. Store 自动导入

2.1 Store 文件结构

app/stores/
├── useUserStore.ts       → useUserStore() 自动导入
├── useVideoStore.ts      → useVideoStore() 自动导入
├── useCartStore.ts       → useCartStore() 自动导入
└── useNotificationStore.ts

配置 storesDirs 后,Store 函数可以在任何组件中直接使用,无需 import。

2.2 在组件中使用

<script setup lang="ts">
// 无需 import,直接使用
const userStore = useUserStore()
const videoStore = useVideoStore()
</script>
 
<template>
  <div v-if="userStore.isLoggedIn">
    <span>{{ userStore.displayName }}</span>
  </div>
</template>

3. Setup Store vs Options Store

3.1 Options Store

// app/stores/useUserStore.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null as User | null,
    token: null as string | null,
  }),
 
  getters: {
    isLoggedIn: (state) => !!state.user,
    displayName: (state) => state.user?.name || '游客',
  },
 
  actions: {
    async login(credentials: LoginData) {
      const { user, token } = await $fetch('/api/auth/login', {
        method: 'POST',
        body: credentials,
      })
      this.user = user
      this.token = token
    },
 
    logout() {
      this.user = null
      this.token = null
      navigateTo('/login')
    },
  },
})

3.2 Setup Store(推荐)

// app/stores/useUserStore.ts
export const useUserStore = defineStore('user', () => {
  // state
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)
 
  // getters
  const isLoggedIn = computed(() => !!user.value)
  const displayName = computed(() => user.value?.name || '游客')
 
  // actions
  async function login(credentials: LoginData) {
    const result = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials,
    })
    user.value = result.user
    token.value = result.token
  }
 
  function logout() {
    user.value = null
    token.value = null
    navigateTo('/login')
  }
 
  return { user, token, isLoggedIn, displayName, login, logout }
})

3.3 对比

特性Options StoreSetup Store
语法风格对象式(state/getters/actions)函数式(ref/computed/function)
TypeScript需要额外类型声明自动推断
Composable 复用不便✅ 可直接使用其他 Composable
灵活性固定结构自由组织
学习曲线Vuex 用户熟悉Vue 3 Composition API 风格

推荐 Setup Store——与 Nuxt4 的 Composition API 风格一致,TypeScript 体验更好。

3.4 Setup Store 中使用 Nuxt Composables

// Setup Store 最大的优势:可以直接使用 Nuxt Composables
export const useVideoStore = defineStore('video', () => {
  const config = useRuntimeConfig()
  const route = useRoute()
 
  const videos = ref<Video[]>([])
  const page = ref(1)
  const total = ref(0)
 
  const hasMore = computed(() => videos.value.length < total.value)
 
  async function fetchVideos() {
    const result = await $fetch(`${config.public.apiBase}/videos`, {
      query: { page: page.value, limit: 20 },
    })
    videos.value = result.items
    total.value = result.total
  }
 
  async function loadMore() {
    page.value++
    const result = await $fetch(`${config.public.apiBase}/videos`, {
      query: { page: page.value, limit: 20 },
    })
    videos.value.push(...result.items)
  }
 
  return { videos, page, total, hasMore, fetchVideos, loadMore }
})

4. 持久化插件 SSR 兼容

4.1 安装持久化插件

pnpm add pinia-plugin-persistedstate

4.2 注册插件

// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',
    'pinia-plugin-persistedstate/nuxt',
  ],
})

4.3 在 Store 中启用持久化

export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)
 
  return { user, token }
}, {
  persist: {
    // SSR 兼容:使用 Cookie 而非 localStorage
    storage: piniaPluginPersistedstate.cookies(),
    pick: ['token'],  // 只持久化 token,不持久化完整 user 对象
  },
})

4.4 SSR 持久化的存储选择

存储方式SSR 可用容量适用场景
cookies()~4KBToken、主题、语言
localStorage()~5MB仅客户端数据
sessionStorage()~5MB会话级数据
// 示例:不同数据使用不同存储
export const useSettingsStore = defineStore('settings', () => {
  const theme = ref<'light' | 'dark'>('light')
  const locale = ref('zh-CN')
  const recentSearches = ref<string[]>([])
 
  return { theme, locale, recentSearches }
}, {
  persist: [
    {
      // 主题和语言:Cookie 持久化(SSR 可读)
      storage: piniaPluginPersistedstate.cookies(),
      pick: ['theme', 'locale'],
    },
    {
      // 搜索历史:localStorage(仅客户端)
      storage: piniaPluginPersistedstate.localStorage(),
      pick: ['recentSearches'],
    },
  ],
})

4.5 useCookie 替代方案

对于简单的 Cookie 持久化,也可以直接用 Nuxt 的 useCookie

export const useAuthStore = defineStore('auth', () => {
  // useCookie 自动处理 SSR/CSR Cookie 读写
  const tokenCookie = useCookie('auth-token', {
    maxAge: 60 * 60 * 24 * 7,  // 7 天
    secure: true,
    httpOnly: false,
  })
 
  const user = ref<User | null>(null)
  const token = computed({
    get: () => tokenCookie.value,
    set: (v) => { tokenCookie.value = v },
  })
 
  const isLoggedIn = computed(() => !!token.value)
 
  async function login(credentials: LoginData) {
    const result = await $fetch('/api/auth/login', {
      method: 'POST',
      body: credentials,
    })
    user.value = result.user
    token.value = result.token
  }
 
  function logout() {
    user.value = null
    token.value = null
  }
 
  return { user, token, isLoggedIn, login, logout }
})

5. HMR 热更新

5.1 Pinia HMR 自动支持

@pinia/nuxt 在开发模式下自动启用 HMR——修改 Store 文件后:

  • ✅ Store 逻辑立即生效
  • ✅ 现有状态保留(不会重置)
  • ✅ 新增的 getter/action 立即可用

5.2 手动 HMR 配置

某些边界情况需要手动配置:

export const useVideoStore = defineStore('video', () => {
  // ... store 定义
})
 
// 手动启用 HMR(通常不需要,@pinia/nuxt 自动处理)
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useVideoStore, import.meta.hot))
}

5.3 跨 Store 引用

// app/stores/useCartStore.ts
export const useCartStore = defineStore('cart', () => {
  // 在一个 Store 中引用另一个 Store
  const userStore = useUserStore()
 
  const items = ref<CartItem[]>([])
 
  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )
 
  async function checkout() {
    if (!userStore.isLoggedIn) {
      navigateTo('/login')
      return
    }
 
    await $fetch('/api/orders', {
      method: 'POST',
      body: { items: items.value },
    })
    items.value = []
  }
 
  return { items, total, checkout }
})

本章小结

  • @pinia/nuxt:一行配置完成 Pinia 注册、SSR 支持、自动导入
  • Store 自动导入storesDirs 配置后,Store 函数无需手动 import
  • Setup Store:推荐函数式风格,与 Composition API 一致,可直接使用 Nuxt Composables
  • 持久化pinia-plugin-persistedstate + cookies() 实现 SSR 兼容持久化
  • useCookie 替代:简单场景可直接用 useCookie 管理 Token
  • HMR:自动热更新,修改 Store 不丢失状态
  • 跨 Store 引用:在一个 Store 中调用另一个 Store(Setup Store 中直接调用即可)

下一章用 AI 视频项目实战完整的状态架构设计。