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/nuxt1.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 Store | Setup 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-persistedstate4.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() | ✅ | ~4KB | Token、主题、语言 |
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 视频项目实战完整的状态架构设计。