Pinia 集成
Pinia 是 Vue 官方推荐的状态管理库,Nuxt 通过 @pinia/nuxt 模块提供集成。
为什么需要 Pinia?
useState 能处理简单的状态共享,但在复杂应用中可能不够:
| 需求 | useState | Pinia |
|---|---|---|
| 简单共享状态 | ✅ | ✅ |
| 计算属性(getters) | ❌ 手动 computed | ✅ 内置 |
| 异步操作(actions) | ❌ 手动写 | ✅ 内置 |
| DevTools 调试 | ❌ | ✅ 有专门面板 |
| 状态持久化 | ❌ 手动 | ✅ 插件支持 |
| 状态快照/回滚 | ❌ | ✅ $patch/$reset |
选择建议
- 小项目、1-2 个共享状态 →
useState - 中大项目、多个业务模块 → Pinia
- 不确定 → 先用
useState,后面迁移到 Pinia 很简单
安装
npm install @pinia/nuxt pinia配置
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})安装后自动做了什么?
@pinia/nuxt 模块会:
- 自动安装 Pinia 到 Vue 应用
- 支持自动导入
defineStore等 API - 自动处理 SSR 状态传递
- 在
stores/目录下的 store 自动导入
创建 Store
Setup 语法(推荐)
// stores/counter.ts
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return { count, doubled, increment, decrement }
})Setup 语法 vs Options 语法
| 特性 | Setup 语法 | Options 语法 |
|---|---|---|
| 风格 | 和 <script setup> 一样 | 和 Vue 2 的 data/computed/methods 一样 |
| 灵活性 | ✅ 更灵活 | 较固定 |
| 组合性 | ✅ 轻松组合 | 需要额外处理 |
| 推荐度 | ✅ 推荐 | 也可以 |
推荐 Setup 语法
和 Nuxt 的组合式 API 风格一致,学习成本更低。
Options 语法
// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
token: '' as string,
}),
getters: {
isAuthenticated: (state) => !!state.token,
userName: (state) => state.user?.name ?? 'Guest',
},
actions: {
async login(email: string, password: string) {
const response = await $fetch('/api/login', {
method: 'POST',
body: { email, password },
})
this.token = response.token
this.user = response.user
},
logout() {
this.token = ''
this.user = null
},
},
})Options 语法的三个部分
state:数据(类似data)getters:计算属性(类似computed)actions:方法(类似methods)
INFO
️ 在 actions 中用 this 访问 state 和 getters 不需要 .value
使用 Store
<script setup>
const counter = useCounterStore()
const user = useUserStore()
// 读取 state
console.log(counter.count)
// 读取 getter
console.log(counter.doubled)
// 调用 action
counter.increment()
await user.login('alice@example.com', 'password')
</script>
<template>
<p>计数:{{ counter.count }}</p>
<p>双倍:{{ counter.doubled }}</p>
<p>用户:{{ user.userName }}</p>
<button @click="counter.increment()">+1</button>
</template>Store 的响应式
从 Store 解构出的属性会丢失响应式!
// ❌ 丢失响应式
const { count, doubled } = useCounterStore()
// ✅ 保持响应式
const counter = useCounterStore()
console.log(counter.count) // 响应式的
// ✅ 用 storeToRefs 解构
const { count, doubled } = storeToRefs(useCounterStore())Store 组合
// stores/cart.ts
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
function addItem(product: Product) {
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
return { items, total, addItem }
})
// 在另一个 Store 中使用
export const useCheckoutStore = defineStore('checkout', () => {
const cart = useCartStore() // 在一个 store 中使用另一个 store
async function checkout() {
await $fetch('/api/orders', {
method: 'POST',
body: { items: cart.items },
})
}
return { checkout }
})Store 组合的好处
不同业务模块可以独立管理状态,需要时互相引用。比把所有状态放在一个巨大的 store 中更清晰。
SSR 注意事项
nuxtServerInit
在 Pinia 中使用 nuxtServerInit 在服务端初始化数据:
// stores/auth.ts
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as User | null,
}),
actions: {
async nuxtServerInit() {
const token = useCookie('auth-token')
if (token.value) {
try {
this.user = await $fetch('/api/me', {
headers: { Authorization: `Bearer ${token.value}` },
})
} catch {
token.value = null
}
}
},
},
})nuxtServerInit 的作用
在 SSR 渲染前自动执行,确保用户信息在服务端就准备好了。这样首次渲染的 HTML 就包含用户数据,不会出现"未登录 → 已登录"的闪烁。
INFO
️ 注意:只有根 store(直接在 stores/ 下的)的 nuxtServerInit 会被自动调用 嵌套目录下的 store 不会
状态持久化
使用 pinia-plugin-persistedstate 将状态持久化到 cookie 或 localStorage:
npm install pinia-plugin-persistedstate// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@pinia/nuxt',
'pinia-plugin-persistedstate/nuxt',
],
})// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
preferences: { theme: 'light' },
}),
persist: {
key: 'user-store',
storage: persistedState.cookiesWithOptions({
maxAge: 60 * 60 * 24 * 7, // 7 天
}),
paths: ['token', 'preferences'], // 只持久化指定字段
},
})为什么用 cookie 而不是 localStorage?
- Cookie:SSR 时可读(服务端能获取到用户的登录状态)
- localStorage:仅客户端可用(SSR 时不知道用户是否登录)
在 Nuxt SSR 项目中
推荐用 cookie 持久化 。
Store vs useState
| 特性 | Pinia Store | useState |
|---|---|---|
| 复杂度 | 中等 | 简单 |
| 适合场景 | 复杂业务逻辑 | 简单共享状态 |
| DevTools | ✅ 有专门面板 | ❌ |
| 插件系统 | ✅ | ❌ |
| 持久化 | ✅ 插件支持 | 手动 |
| 代码组织 | 集中管理 | 分散在组合式函数 |
| 学习成本 | 需要学 Pinia | 零(Nuxt 内置) |
简单规则:简单状态用 useState,复杂业务逻辑用 Pinia。
常见问题
Store 在 SSR 时数据不正确
确保不要在 store 外部创建模块级的 ref。所有状态都应该在 defineStore 内部。
Store 切换页面后状态重置
这是正常行为——Pinia 的状态在客户端导航时保持,但 SSR 刷新时会重新初始化。如果需要持久化,使用 persist 选项。
知识脉络
useState → 你在这里:Pinia 集成
│
├─→ 相关:Pinia 官方文档
│
└─→ 相关:插件系统(16-模块系统)