Skip to content

Pinia 集成

Pinia 是 Vue 官方推荐的状态管理库,Nuxt 通过 @pinia/nuxt 模块提供集成。

为什么需要 Pinia?

useState 能处理简单的状态共享,但在复杂应用中可能不够:

需求useStatePinia
简单共享状态
计算属性(getters)❌ 手动 computed✅ 内置
异步操作(actions)❌ 手动写✅ 内置
DevTools 调试✅ 有专门面板
状态持久化❌ 手动✅ 插件支持
状态快照/回滚$patch/$reset

选择建议

  • 小项目、1-2 个共享状态 → useState
  • 中大项目、多个业务模块 → Pinia
  • 不确定 → 先用 useState,后面迁移到 Pinia 很简单

安装

bash
npm install @pinia/nuxt pinia

配置

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

安装后自动做了什么?

@pinia/nuxt 模块会:

  1. 自动安装 Pinia 到 Vue 应用
  2. 支持自动导入 defineStore 等 API
  3. 自动处理 SSR 状态传递
  4. stores/ 目录下的 store 自动导入

创建 Store

Setup 语法(推荐)

ts
// 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 语法

ts
// 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

vue
<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 解构出的属性会丢失响应式!

ts
// ❌ 丢失响应式
const { count, doubled } = useCounterStore()

// ✅ 保持响应式
const counter = useCounterStore()
console.log(counter.count)  // 响应式的

// ✅ 用 storeToRefs 解构
const { count, doubled } = storeToRefs(useCounterStore())

Store 组合

ts
// 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 在服务端初始化数据:

ts
// 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:

bash
npm install pinia-plugin-persistedstate
ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@pinia/nuxt',
    'pinia-plugin-persistedstate/nuxt',
  ],
})
ts
// 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 StoreuseState
复杂度中等简单
适合场景复杂业务逻辑简单共享状态
DevTools✅ 有专门面板
插件系统
持久化✅ 插件支持手动
代码组织集中管理分散在组合式函数
学习成本需要学 Pinia零(Nuxt 内置)

简单规则:简单状态用 useState,复杂业务逻辑用 Pinia。

常见问题

Store 在 SSR 时数据不正确

确保不要在 store 外部创建模块级的 ref。所有状态都应该在 defineStore 内部。

Store 切换页面后状态重置

这是正常行为——Pinia 的状态在客户端导航时保持,但 SSR 刷新时会重新初始化。如果需要持久化,使用 persist 选项。

知识脉络

text
useState → 你在这里:Pinia 集成

             ├─→ 相关:Pinia 官方文档

             └─→ 相关:插件系统(16-模块系统)

基于 Nuxt 4 官方文档整理编写