Nuxt.js Pinia状态管理 #

一、Pinia概述 #

Pinia 是 Vue.js 的官方状态管理库,Nuxt.js 提供了开箱即用的 Pinia 支持。

1.1 Pinia优势 #

  • 类型安全:完整的 TypeScript 支持
  • 模块化:每个 Store 独立
  • DevTools支持:完整的调试体验
  • 轻量级:体积小,性能好
  • Composition API:符合 Vue 3 风格

1.2 安装配置 #

bash
pnpm add @pinia/nuxt pinia

nuxt.config.ts

typescript
export default defineNuxtConfig({
  modules: ['@pinia/nuxt']
})

二、定义Store #

2.1 Setup Store(推荐) #

stores/counter.ts

typescript
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Counter')
  
  const doubleCount = computed(() => count.value * 2)
  
  function increment() {
    count.value++
  }
  
  function decrement() {
    count.value--
  }
  
  function reset() {
    count.value = 0
  }
  
  return {
    count,
    name,
    doubleCount,
    increment,
    decrement,
    reset
  }
})

2.2 Options Store #

stores/user.ts

typescript
interface User {
  id: number
  name: string
  email: string
}

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null as User | null,
    token: null as string | null,
    loading: false
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.user && !!state.token,
    userName: (state) => state.user?.name || 'Guest'
  },
  
  actions: {
    async login(credentials: { email: string; password: string }) {
      this.loading = true
      try {
        const { data } = await useFetch('/api/auth/login', {
          method: 'POST',
          body: credentials
        })
        
        if (data.value) {
          this.user = data.value.user
          this.token = data.value.token
        }
      } finally {
        this.loading = false
      }
    },
    
    logout() {
      this.user = null
      this.token = null
    }
  }
})

三、使用Store #

3.1 在组件中使用 #

vue
<script setup lang="ts">
const counterStore = useCounterStore()

const { count, doubleCount } = storeToRefs(counterStore)
const { increment, decrement } = counterStore
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

3.2 解构Store #

vue
<script setup lang="ts">
const store = useCounterStore()

const { count, name } = storeToRefs(store)
const { increment } = store

const { user, token } = storeToRefs(useUserStore())
</script>

3.3 直接修改状态 #

vue
<script setup lang="ts">
const store = useCounterStore()

store.count++
store.$patch({ count: 10, name: 'New Name' })

store.$patch((state) => {
  state.count++
  state.name = 'Updated'
})
</script>

四、Getters #

4.1 定义Getters #

typescript
export const useProductStore = defineStore('products', () => {
  const products = ref<Product[]>([])
  const selectedCategory = ref<string | null>(null)
  
  const filteredProducts = computed(() => {
    if (!selectedCategory.value) return products.value
    return products.value.filter(p => p.category === selectedCategory.value)
  })
  
  const productCount = computed(() => products.value.length)
  
  const categories = computed(() => {
    return [...new Set(products.value.map(p => p.category))]
  })
  
  const getProductById = computed(() => {
    return (id: number) => products.value.find(p => p.id === id)
  })
  
  return {
    products,
    selectedCategory,
    filteredProducts,
    productCount,
    categories,
    getProductById
  }
})

4.2 使用其他Store #

typescript
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  
  const productStore = useProductStore()
  
  const totalPrice = computed(() => {
    return items.value.reduce((total, item) => {
      const product = productStore.getProductById(item.productId)
      return total + (product?.price || 0) * item.quantity
    }, 0)
  })
  
  return { items, totalPrice }
})

五、Actions #

5.1 异步Actions #

typescript
export const usePostStore = defineStore('posts', () => {
  const posts = ref<Post[]>([])
  const loading = ref(false)
  const error = ref<Error | null>(null)
  
  async function fetchPosts() {
    loading.value = true
    error.value = null
    
    try {
      const { data } = await useFetch('/api/posts')
      posts.value = data.value || []
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }
  
  async function createPost(post: CreatePostDto) {
    loading.value = true
    
    try {
      const { data } = await useFetch('/api/posts', {
        method: 'POST',
        body: post
      })
      
      if (data.value) {
        posts.value.unshift(data.value)
      }
    } finally {
      loading.value = false
    }
  }
  
  return {
    posts,
    loading,
    error,
    fetchPosts,
    createPost
  }
})

5.2 组合Actions #

typescript
export const useAppStore = defineStore('app', () => {
  const userStore = useUserStore()
  const cartStore = useCartStore()
  const notificationStore = useNotificationStore()
  
  async function initializeApp() {
    await userStore.fetchUser()
    await cartStore.fetchCart()
  }
  
  async function checkout() {
    if (!userStore.isAuthenticated) {
      notificationStore.warning('请先登录')
      return navigateTo('/login')
    }
    
    try {
      await cartStore.checkout()
      notificationStore.success('订单提交成功')
    } catch (error) {
      notificationStore.error('订单提交失败')
    }
  }
  
  return {
    initializeApp,
    checkout
  }
})

六、状态持久化 #

6.1 使用pinia-plugin-persistedstate #

bash
pnpm add @pinia-plugin-persistedstate

plugins/pinia-persisted.ts

typescript
import { defineNuxtPlugin } from '#app'
import { PiniaPluginPersistedstate } from 'pinia-plugin-persistedstate'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.$pinia.use(PiniaPluginPersistedstate)
})

6.2 配置持久化 #

typescript
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    preferences: {
      theme: 'light',
      language: 'zh-CN'
    }
  }),
  
  persist: {
    key: 'user-store',
    storage: persistedState.cookiesWithOptions({
      maxAge: 60 * 60 * 24 * 30
    }),
    paths: ['preferences']
  }
})

6.3 自定义存储 #

typescript
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  
  persist: {
    key: 'cart',
    storage: {
      getItem: (key) => {
        if (import.meta.client) {
          return localStorage.getItem(key)
        }
        return null
      },
      setItem: (key, value) => {
        if (import.meta.client) {
          localStorage.setItem(key, value)
        }
      }
    }
  }
})

七、插件 #

7.1 创建Pinia插件 #

plugins/pinia-logger.ts

typescript
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.$pinia.use(({ store }) => {
    if (process.dev) {
      store.$onAction(({ name, args, after, onError }) => {
        console.log(`[Pinia] ${store.$id}.${name}`, args)
        
        after((result) => {
          console.log(`[Pinia] ${store.$id}.${name} finished`, result)
        })
        
        onError((error) => {
          console.error(`[Pinia] ${store.$id}.${name} failed`, error)
        })
      })
    }
  })
})

7.2 重置状态插件 #

typescript
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.$pinia.use(({ store }) => {
    const initialState = JSON.parse(JSON.stringify(store.$state))
    
    store.$resetToInitial = () => {
      store.$patch(initialState)
    }
  })
})

八、DevTools #

8.1 Vue DevTools #

Pinia 完整支持 Vue DevTools,可以:

  • 查看所有 Store
  • 检查状态
  • 追踪 Actions
  • 时间旅行调试

8.2 Nuxt DevTools #

Nuxt DevTools 也集成了 Pinia 支持。

九、最佳实践 #

9.1 Store组织 #

text
stores/
├── index.ts          # 导出所有 Store
├── user.ts           # 用户状态
├── cart.ts           # 购物车状态
├── products.ts       # 产品状态
└── app.ts            # 应用全局状态

9.2 类型定义 #

typescript
interface UserState {
  user: User | null
  token: string | null
  loading: boolean
}

interface UserGetters {
  isAuthenticated: boolean
  userName: string
}

interface UserActions {
  login: (credentials: LoginCredentials) => Promise<void>
  logout: () => void
  fetchUser: () => Promise<void>
}

export const useUserStore = defineStore<string, UserState, UserGetters, UserActions>('user', {
  // ...
})

9.3 组合式函数风格 #

typescript
export const useFeatureStore = defineStore('feature', () => {
  const state = reactive({
    data: null,
    loading: false,
    error: null
  })
  
  const getters = {
    hasData: computed(() => !!state.data),
    isLoading: computed(() => state.loading)
  }
  
  const actions = {
    async fetchData() {
      state.loading = true
      try {
        const { data } = await useFetch('/api/feature')
        state.data = data.value
      } catch (error) {
        state.error = error
      } finally {
        state.loading = false
      }
    }
  }
  
  return {
    ...toRefs(state),
    ...getters,
    ...actions
  }
})

十、完整示例 #

10.1 电商购物车 #

stores/cart.ts

typescript
interface CartItem {
  productId: number
  name: string
  price: number
  quantity: number
  image: string
}

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  
  const totalItems = computed(() => 
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )
  
  const totalPrice = computed(() => 
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )
  
  const getItem = (productId: number) => 
    items.value.find(item => item.productId === productId)
  
  const addItem = (product: Omit<CartItem, 'quantity'>) => {
    const existing = getItem(product.productId)
    
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }
  
  const removeItem = (productId: number) => {
    const index = items.value.findIndex(item => item.productId === productId)
    if (index > -1) {
      items.value.splice(index, 1)
    }
  }
  
  const updateQuantity = (productId: number, quantity: number) => {
    const item = getItem(productId)
    if (item) {
      if (quantity <= 0) {
        removeItem(productId)
      } else {
        item.quantity = quantity
      }
    }
  }
  
  const clear = () => {
    items.value = []
  }
  
  return {
    items,
    totalItems,
    totalPrice,
    getItem,
    addItem,
    removeItem,
    updateQuantity,
    clear
  }
}, {
  persist: {
    key: 'cart',
    storage: persistedState.localStorage
  }
})

使用:

vue
<script setup lang="ts">
const cartStore = useCartStore()
const { items, totalItems, totalPrice } = storeToRefs(cartStore)
const { addItem, removeItem, updateQuantity, clear } = cartStore
</script>

<template>
  <div>
    <h2>购物车 ({{ totalItems }})</h2>
    
    <div v-for="item in items" :key="item.productId" class="cart-item">
      <img :src="item.image" :alt="item.name" />
      <div>
        <h3>{{ item.name }}</h3>
        <p>¥{{ item.price }}</p>
        <div>
          <button @click="updateQuantity(item.productId, item.quantity - 1)">-</button>
          <span>{{ item.quantity }}</span>
          <button @click="updateQuantity(item.productId, item.quantity + 1)">+</button>
        </div>
      </div>
      <button @click="removeItem(item.productId)">删除</button>
    </div>
    
    <div class="total">
      <p>总计: ¥{{ totalPrice }}</p>
      <button @click="clear">清空购物车</button>
    </div>
  </div>
</template>

十一、总结 #

本章介绍了 Nuxt.js 中使用 Pinia:

  • 定义 Setup Store 和 Options Store
  • 使用 Store、Getters 和 Actions
  • 状态持久化配置
  • Pinia 插件开发
  • 最佳实践和完整示例

Pinia 是 Vue 3 推荐的状态管理方案,下一章我们将学习状态持久化的更多细节。

最后更新:2026-03-28