Nuxt: 状态管理 Pinia

最后更新:2026-08-26

Alice 把商品加入购物车后,切换到另一个页面购物车就空了——每个页面都有独立的组件状态。Bob 发现用户登录状态在 SSR 和客户端之间不一致。Charlie 需要 Pinia 来管理跨组件、跨页面的共享状态。

1. 你将学到


2. 一个消费者的真实故事

(1) 痛点:购物车状态"丢失"

Alice 在 MegaShop 首页加了 3 件商品到购物车,点击进入商品详情页后,购物车图标显示 0 件——状态没共享。Bob 也遇到类似问题:登录后刷新页面,登录状态消失了。

(2) Pinia 状态管理的解法

Pinia 让购物车和用户状态脱离组件生命周期,成为全局共享的响应式数据:

TYPESCRIPT
// composables/useCartStore.ts
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const total = computed(() => items.value.reduce((s, i) => s + i.price * i.quantity, 0))
  return { items, total }
})

(3) 收益:状态跨页面持久

Alice 的购物车在所有页面保持一致,Bob 的登录状态 SSR/客户端同步,不再出现状态丢失问题。


3. Pinia 安装与配置

(1) 安装步骤

BASH
npm install @pinia/nuxt pinia

▶ 示例:nuxt.config.ts 配置

TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
  // Pinia is auto-configured, no extra setup needed
})

输出:

TEXT 📖 仅展示
// 执行成功

(2) Pinia vs Vuex vs useState 对比

维度 Pinia Vuex 4 Nuxt useState
Vue 3 支持 ✅ 原生 ⚠️ 兼容模式 ✅ Nuxt 专属
TypeScript ✅ 完整推导 ❌ 需手动声明 ✅ 泛型支持
SSR 水合 ✅ 自动 ⚠️ 需配置 ✅ 自动
代码分割 ✅ 按需 ❌ 全局 ❌ 全局
DevTools ✅ 支持 ✅ 支持 ❌ 不支持
体积 ~1KB ~6KB 内置
适用场景 复杂状态 遗留迁移 简单共享

4. Store 定义方式

(1) 选项式 Store

▶ 示例:选项式购物车 Store

TYPESCRIPT
// stores/cart.ts
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as CartItem[],
    couponCode: '' as string
  }),

  getters: {
    totalItems: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),

    totalPrice: (state) => {
      return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
    },

    formattedTotal(): string {
      return new Intl.NumberFormat('en-US', {
        style: 'currency', currency: 'USD'
      }).format(this.totalPrice)
    }
  },

  actions: {
    addItem(product: Product) {
      const existing = this.items.find(i => i.id === product.id)
      if (existing) {
        existing.quantity++
      } else {
        this.items.push({ ...product, quantity: 1 })
      }
    },

    removeItem(productId: number) {
      this.items = this.items.filter(i => i.id !== productId)
    },

    updateQuantity(productId: number, quantity: number) {
      const item = this.items.find(i => i.id === productId)
      if (item) item.quantity = Math.max(0, quantity)
      this.items = this.items.filter(i => i.quantity > 0)
    },

    clearCart() {
      this.items = []
      this.couponCode = ''
    }
  }
})

interface CartItem {
  id: number; name: string; price: number; quantity: number; image: string
}
interface Product {
  id: number; name: string; price: number; image: string
}

输出:

TEXT 📖 仅展示
// 执行成功

(2) Setup 语法 Store

▶ 示例:Setup 语法用户 Store

TYPESCRIPT
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  // State
  const user = ref<User | null>(null)
  const isAuthenticated = computed(() => !!user.value)
  const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '')

  // Actions
  async function login(email: string, password: string) {
    const response = await $fetch('/api/auth/login', {
      method: 'POST',
      body: { email, password }
    })
    user.value = response.user
  }

  function logout() {
    user.value = null
  }

  async function fetchProfile() {
    const profile = await $fetch('/api/user/profile')
    user.value = profile
  }

  return {
    user, isAuthenticated, fullName,
    login, logout, fetchProfile
  }
})

interface User {
  id: number; email: string; firstName: string; lastName: string; role: 'customer' | 'admin'
}

输出:

TEXT 📖 仅展示
// 执行成功

(3) 两种方式对比

维度 选项式 Setup 语法
语法 state/getters/actions ref/computed/function
TypeScript ⚠️ 需声明接口 ✅ 自动推导
灵活性 ⚠️ 受限 ✅ 可用任何 Composable
SSR ✅ 支持 $reset ⚠️ 无 $reset(需自行实现)
适用场景 简单 Store 复杂 Store/需 Composable

5. SSR 脱水与水合

(1) Pinia SSR 状态流转

100%
flowchart LR
    A[Server: Store filled with data] --> B[Serialize state to HTML payload]
    B --> C[Client: Read payload]
    C --> D[Hydrate Store with server state]
    D --> E[Client: Store ready, no re-fetch]

▶ 示例:SSR 中初始化 Store

TYPESCRIPT
// plugins/init-pinia.server.ts
export default defineNuxtPlugin(() => {
  const cartStore = useCartStore()
  // Initialize cart from cookie on server
  const cartCookie = useCookie('cart-items')
  if (cartCookie.value) {
    cartStore.items = cartCookie.value
  }
})

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例:Pinia vs useState SSR 差异

维度 Pinia useState
SSR 水合 ✅ 自动 payload ✅ 自动 payload
多 Store ✅ 独立命名空间 ⚠️ 手动 key 管理
DevTools ✅ 可视化 ❌ 无
持久化 ✅ 插件支持 ⚠️ 需手动 cookie
复杂逻辑 ✅ actions/getters ❌ 只有 ref

6. StoreToRefs 与批量操作

▶ 示例:StoreToRefs 响应式解构

VUE
<script setup lang="ts">
const cartStore = useCartStore()

// ✅ Reactive destructure - keeps reactivity
const { items, totalPrice, totalItems } = storeToRefs(cartStore)

// ❌ Direct destructure - loses reactivity
// const { items, totalPrice } = cartStore

// Actions can be destructured directly (no reactivity needed)
const { addItem, removeItem, clearCart } = cartStore
</script>

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例:$patch 批量更新

TYPESCRIPT
// Batch update with $patch - single reactivity trigger
const cartStore = useCartStore()

// Object style
cartStore.$patch({
  couponCode: 'SAVE20',
  items: [...cartStore.items, newItem]
})

// Function style (better for array mutations)
cartStore.$patch((state) => {
  state.couponCode = 'SAVE20'
  state.items.push(newItem)
  state.items[0].quantity = 3
})

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例:购物车持久化插件

TYPESCRIPT
// plugins/pinia-persist.client.ts
export default defineNuxtPlugin(({ $pinia }) => {
  $pinia.use(({ store }) => {
    // Load from localStorage on client
    const saved = localStorage.getItem(`pinia-${store.$id}`)
    if (saved) store.$patch(JSON.parse(saved))

    // Save to localStorage on change
    store.$subscribe((mutation, state) => {
      localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
    })
  })
})

输出:

TEXT 📖 仅展示
// 执行成功

7. 综合示例:MegaShop 购物车系统

VUE
<!-- pages/cart.vue -->
<template>
  <div class="cart-page">
    <h1>Shopping Cart</h1>

    <div v-if="items.length === 0" class="empty-cart">
      <p>Your cart is empty</p>
      <NuxtLink to="/products">Continue Shopping</NuxtLink>
    </div>

    <div v-else>
      <div class="cart-items">
        <div v-for="item in items" :key="item.id" class="cart-item">
          <img :src="item.image" :alt="item.name" />
          <div class="details">
            <h3>{{ item.name }}</h3>
            <p>${{ item.price }} USD</p>
            <div class="quantity">
              <button @click="updateQuantity(item.id, item.quantity - 1)">-</button>
              <span>{{ item.quantity }}</span>
              <button @click="updateQuantity(item.id, item.quantity + 1)">+</button>
            </div>
          </div>
          <button @click="removeItem(item.id)" class="remove">Remove</button>
        </div>
      </div>

      <div class="cart-summary">
        <p>Items: {{ totalItems }}</p>
        <p class="total">Total: {{ formattedTotal }}</p>
        <button @click="clearCart" class="clear">Clear Cart</button>
        <NuxtLink to="/checkout" class="checkout">Proceed to Checkout</NuxtLink>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
const cartStore = useCartStore()
const { items, totalItems, formattedTotal } = storeToRefs(cartStore)
const { removeItem, updateQuantity, clearCart } = cartStore
</script>

❓ 常见问题

Q Pinia Store 和 Composable 怎么选?
A 需要跨组件共享状态用 Store,组件内逻辑复用用 Composable。购物车/用户状态用 Store,价格格式化用 Composable。
Q 为什么解构 Store 会丢失响应式?
A 直接解构拿到的是值的拷贝,不是 ref。用 storeToRefs 解构保持响应式,actions 可以直接解构(函数不需要响应式)。
Q SSR 时 Store 数据客户端不一致怎么办?
A 确保不在 onMounted 之前修改 Store(SSR 阶段修改的值会被水合覆盖)。Pinia 的 SSR 水合是自动的,通常不需要手动处理。
Q Pinia 的 $reset 在 Setup 语法中不可用?
A Setup 语法没有 $reset。可以手动实现:定义 $reset action,或在 $patch 中重置所有 ref。
Q 多个页面同时修改 Store 会冲突吗?
A 客户端是单线程的,不会冲突。SSR 时每个请求有独立的 Pinia 实例,也不会冲突。
Q 购物车数据怎么在刷新后保留?
A 使用持久化插件(存 localStorage)或同步到 cookie。MegaShop 推荐用 cookie 同步,SSR 时也能读取。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建 useCartStore,实现 addItem 和 removeItem,在两个不同页面上操作购物车并验证状态共享
  2. 进阶题(难度⭐⭐):用 Setup 语法定义 useUserStore,实现 login/logout/fetchProfile,验证 SSR 水合是否正常
  3. 挑战题(难度⭐⭐⭐):实现购物车持久化(刷新后数据保留),对比 localStorage 插件与 cookie 同步两种方案的优劣

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏