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