一、Pinia 是什么?为什么选择它?
1.1 Pinia 简介
Pinia(发音 /piːnjʌ/,类似英文中的 "peenya")是 Vue.js 的下一代状态管理库。它由 Vue 核心团队成员开发,旨在提供一个更简单、更直观的状态管理方案。
1.2 为什么从 Vuex 迁移到 Pinia?
| 特性 | Vuex | Pinia |
|---|---|---|
| API 复杂度 | 较高,需要理解多个概念 | 极简,更符合 Vue 3 理念 |
| TypeScript 支持 | 需要额外配置 | 一流的 TypeScript 支持 |
| 模块系统 | 需要 modules 配置 | 每个 store 都是模块 |
| Composition API | 支持但不够自然 | 为 Composition API 而生 |
| 打包体积 | 约 10KB | 约 1KB |
| 开发体验 | 良好 | 优秀 |
二、快速开始
2.1 安装
bash
# 使用 npm
npm install pinia
# 使用 yarn
yarn add pinia
# 使用 pnpm
pnpm add pinia
2.2 基本配置
javascript
// main.js / main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// 创建 Pinia 实例
const pinia = createPinia()
// 创建 Vue 应用并挂载 Pinia
const app = createApp(App)
app.use(pinia)
app.mount('#app')
三、核心概念详解
3.1 Store(仓库)
Pinia 的核心概念是 store,它包含状态、操作和 getter。
3.1.1 创建第一个 Store
typescript
// stores/counter.ts
import { defineStore } from 'pinia'
// 使用 defineStore 定义 store
// 第一个参数是 store 的唯一 ID
export const useCounterStore = defineStore('counter', {
// 状态(相当于 Vuex 的 state)
state: () => ({
count: 0,
name: 'Pinia Store'
}),
// 计算属性(相当于 Vuex 的 getters)
getters: {
doubleCount: (state) => state.count * 2,
// 使用 this 访问其他 getter
doubleCountPlusOne(): number {
return this.doubleCount + 1
}
},
// 操作方法(相当于 Vuex 的 actions + mutations)
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
// 可以异步
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.increment()
}
}
})
3.1.2 在组件中使用
vue
<template>
<div>
<h1>{{ counterStore.name }}</h1>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<p>Double Count + 1: {{ counterStore.doubleCountPlusOne }}</p>
<button @click="counterStore.increment()">+</button>
<button @click="counterStore.decrement()">-</button>
<button @click="counterStore.incrementAsync()">Async +</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
// 使用 store
const counterStore = useCounterStore()
// 重置 store
const reset = () => {
counterStore.$reset()
}
// 监听变化
counterStore.$subscribe((mutation, state) => {
console.log('状态变化:', mutation)
console.log('新状态:', state)
})
// 监听 action
counterStore.$onAction(({ name, store, args, after, onError }) => {
console.log(`开始执行 action: ${name}`)
after((result) => {
console.log(`action ${name} 执行完成`)
})
onError((error) => {
console.warn(`action ${name} 执行失败:`, error)
})
})
</script>
3.2 State(状态)
3.2.1 定义状态
typescript
// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
// 基础类型
id: null as number | null,
name: '',
email: '',
// 对象
profile: {
avatar: '',
bio: '',
location: ''
},
// 数组
permissions: [] as string[],
// 复杂嵌套
preferences: {
theme: 'light',
notifications: {
email: true,
push: false
}
}
})
})
3.2.2 访问和修改状态
vue
<template>
<div>
<p>用户名: {{ userStore.name }}</p>
<p>邮箱: {{ userStore.email }}</p>
<p>主题: {{ userStore.preferences.theme }}</p>
<!-- 直接修改 -->
<input v-model="userStore.name" />
<!-- 批量修改 -->
<button @click="updateUser">更新用户信息</button>
<!-- 替换整个 state -->
<button @click="resetUser">重置用户</button>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 1. 直接修改
const updateName = () => {
userStore.name = '新的用户名'
}
// 2. $patch 批量修改
const updateUser = () => {
// 方式1:传递对象
userStore.$patch({
name: '张三',
email: 'zhangsan@example.com'
})
// 方式2:传递函数(适合修改嵌套对象或数组)
userStore.$patch((state) => {
state.profile.avatar = '/new-avatar.jpg'
state.permissions.push('admin')
state.preferences.theme = 'dark'
})
}
// 3. 替换整个 state
const resetUser = () => {
userStore.$state = {
id: null,
name: '',
email: '',
profile: { avatar: '', bio: '', location: '' },
permissions: [],
preferences: {
theme: 'light',
notifications: { email: true, push: false }
}
}
}
// 4. 重置到初始状态
const reset = () => {
userStore.$reset()
}
</script>
3.3 Getters(计算属性)
3.3.1 定义 Getter
typescript
// stores/products.ts
export const useProductStore = defineStore('products', {
state: () => ({
products: [
{ id: 1, name: 'iPhone 13', price: 6999, category: 'phone', stock: 10 },
{ id: 2, name: 'MacBook Pro', price: 12999, category: 'laptop', stock: 5 },
{ id: 3, name: 'AirPods Pro', price: 1999, category: 'audio', stock: 20 },
{ id: 4, name: 'iPad Air', price: 4799, category: 'tablet', stock: 8 }
],
selectedCategory: 'all'
}),
getters: {
// 1. 基础 getter(自动推断类型)
totalProducts: (state) => state.products.length,
// 2. 使用 this 访问其他 getter
outOfStockProducts(state): Product[] {
return state.products.filter(p => p.stock === 0)
},
// 3. 带参数的 getter(返回函数)
productsByCategory: (state) => {
return (category: string) => {
if (category === 'all') return state.products
return state.products.filter(p => p.category === category)
}
},
// 4. 结合多个状态计算
averagePrice(): number {
if (this.totalProducts === 0) return 0
const total = this.products.reduce((sum, p) => sum + p.price, 0)
return total / this.totalProducts
},
// 5. 计算库存总值
totalInventoryValue(): number {
return this.products.reduce((sum, p) => sum + (p.price * p.stock), 0)
},
// 6. 过滤后的产品(响应式)
filteredProducts(): Product[] {
if (this.selectedCategory === 'all') {
return this.products
}
return this.products.filter(p => p.category === this.selectedCategory)
}
}
})
3.3.2 在组件中使用 Getter
vue
<template>
<div>
<h2>产品统计</h2>
<p>总产品数: {{ productStore.totalProducts }}</p>
<p>平均价格: ¥{{ productStore.averagePrice.toFixed(2) }}</p>
<p>库存总值: ¥{{ productStore.totalInventoryValue }}</p>
<h3>按分类筛选</h3>
<select v-model="selectedCategory">
<option value="all">全部</option>
<option value="phone">手机</option>
<option value="laptop">笔记本</option>
<option value="audio">音频</option>
<option value="tablet">平板</option>
</select>
<!-- 使用带参数的 getter -->
<div v-for="product in phoneProducts" :key="product.id">
{{ product.name }} - ¥{{ product.price }}
</div>
<!-- 使用响应式 getter -->
<h3>当前分类的产品</h3>
<div v-for="product in productStore.filteredProducts" :key="product.id">
{{ product.name }} (库存: {{ product.stock }})
</div>
</div>
</template>
<script setup lang="ts">
import { useProductStore } from '@/stores/products'
import { computed, ref } from 'vue'
const productStore = useProductStore()
const selectedCategory = ref('all')
// 使用带参数的 getter
const phoneProducts = computed(() => {
return productStore.productsByCategory('phone')
})
// 或者直接在模板中调用
const audioProducts = productStore.productsByCategory('audio')
</script>
3.4 Actions(操作方法)
3.4.1 定义 Action
typescript
// stores/auth.ts
import type { LoginCredentials, User, ApiResponse } from '@/types'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as User | null,
token: localStorage.getItem('token') || '',
loading: false,
error: null as string | null
}),
actions: {
// 1. 同步 action
setUser(user: User) {
this.user = user
},
// 2. 异步 action(支持 async/await)
async login(credentials: LoginCredentials): Promise<ApiResponse<User>> {
// 重置错误状态
this.error = null
this.loading = true
try {
// API 调用
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
})
if (!response.ok) {
throw new Error('登录失败')
}
const data = await response.json()
// 更新状态
this.user = data.user
this.token = data.token
// 保存到 localStorage
localStorage.setItem('token', data.token)
// 返回结果
return {
success: true,
data: data.user,
message: '登录成功'
}
} catch (error: any) {
// 错误处理
this.error = error.message || '登录失败'
return {
success: false,
data: null,
message: error.message
}
} finally {
this.loading = false
}
},
// 3. 调用其他 action
async logout() {
try {
// 调用 API
await fetch('/api/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`
}
})
} catch (error) {
console.error('登出失败:', error)
} finally {
// 清理状态
this.user = null
this.token = ''
localStorage.removeItem('token')
}
},
// 4. 组合多个操作
async registerAndLogin(userData: any) {
// 注册
const registerResult = await this.register(userData)
if (!registerResult.success) {
return registerResult
}
// 登录
const loginResult = await this.login({
email: userData.email,
password: userData.password
})
return loginResult
},
// 5. 访问其他 store 的 action
async completeLoginFlow(credentials: LoginCredentials) {
const result = await this.login(credentials)
if (result.success && this.user) {
// 获取用户购物车
const cartStore = useCartStore()
await cartStore.fetchCart(this.user.id)
// 获取用户偏好设置
const preferenceStore = usePreferenceStore()
await preferenceStore.fetchPreferences(this.user.id)
}
return result
}
}
})
四、模块化与 Store 组合
4.1 自动导入 Store(推荐)
javascript
// stores/index.ts
export { useUserStore } from './user'
export { useProductStore } from './products'
export { useCartStore } from './cart'
export { useOrderStore } from './order'
export { useNotificationStore } from './notification'
// 或者使用自动导入
// 在 main.ts 中配置
4.2 Store 间通信
typescript
// stores/cart.ts
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
total: 0
}),
actions: {
async addToCart(productId: number) {
// 1. 访问其他 store 的状态
const productStore = useProductStore()
const product = productStore.getProductById(productId)
if (!product || product.stock === 0) {
throw new Error('商品不存在或库存不足')
}
// 2. 调用其他 store 的 action
await productStore.decreaseStock(productId)
// 3. 更新购物车
const existingItem = this.items.find(item => item.productId === productId)
if (existingItem) {
existingItem.quantity++
} else {
this.items.push({
productId,
quantity: 1,
price: product.price,
name: product.name
})
}
this.calculateTotal()
// 4. 发送通知
const notificationStore = useNotificationStore()
notificationStore.show({
message: '已添加到购物车',
type: 'success'
})
},
calculateTotal() {
this.total = this.items.reduce((sum, item) => {
return sum + (item.price * item.quantity)
}, 0)
}
}
})
4.3 组合式 Store(Composition API 风格)
typescript
// stores/composables/usePagination.ts
import { ref, computed } from 'vue'
export function usePagination<T>(items: T[], itemsPerPage = 10) {
const currentPage = ref(1)
const perPage = ref(itemsPerPage)
const totalPages = computed(() => {
return Math.ceil(items.length / perPage.value)
})
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * perPage.value
const end = start + perPage.value
return items.slice(start, end)
})
const hasNextPage = computed(() => {
return currentPage.value < totalPages.value
})
const hasPrevPage = computed(() => {
return currentPage.value > 1
})
function nextPage() {
if (hasNextPage.value) {
currentPage.value++
}
}
function prevPage() {
if (hasPrevPage.value) {
currentPage.value--
}
}
function goToPage(page: number) {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
return {
currentPage,
perPage,
totalPages,
paginatedItems,
hasNextPage,
hasPrevPage,
nextPage,
prevPage,
goToPage
}
}
// 在 Store 中使用
export const useProductStore = defineStore('products', () => {
// 使用 ref 定义状态(类似 Vue 3 的 Composition API)
const products = ref<Product[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
// 使用 computed 定义 getter
const totalProducts = computed(() => products.value.length)
const featuredProducts = computed(() =>
products.value.filter(p => p.isFeatured)
)
// 使用普通函数定义 action
async function fetchProducts() {
loading.value = true
error.value = null
try {
const response = await api.getProducts()
products.value = response.data
} catch (err: any) {
error.value = err.message
} finally {
loading.value = false
}
}
// 组合使用 usePagination
const pagination = usePagination(products)
return {
// 状态
products,
loading,
error,
// Getter
totalProducts,
featuredProducts,
// Action
fetchProducts,
// 分页相关
...pagination
}
})
五、TypeScript 支持
5.1 完整的类型定义
typescript
// types/index.ts
export interface User {
id: number
name: string
email: string
avatar?: string
roles: string[]
}
export interface Product {
id: number
name: string
description: string
price: number
category: string
stock: number
images: string[]
isFeatured: boolean
createdAt: Date
updatedAt: Date
}
export interface CartItem {
productId: number
quantity: number
price: number
name: string
image?: string
}
// store 状态类型
export interface AuthState {
user: User | null
token: string
loading: boolean
error: string | null
}
// API 响应类型
export interface ApiResponse<T = any> {
success: boolean
data: T | null
message: string
code?: number
}
// stores/auth.ts
import type { AuthState, User, ApiResponse } from '@/types'
export const useAuthStore = defineStore('auth', {
state: (): AuthState => ({
user: null,
token: '',
loading: false,
error: null
}),
getters: {
// 完整类型推断
isAuthenticated: (state): boolean => !!state.user,
getUserName: (state): string => state.user?.name || '访客',
// 带参数的 getter 类型
hasPermission: (state) => {
return (permission: string): boolean => {
return state.user?.roles.includes(permission) || false
}
}
},
actions: {
// 参数和返回值类型
async login(email: string, password: string): Promise<ApiResponse<User>> {
// ... 实现
}
}
})
5.2 自动类型推断
Pinia 会自动推断大部分类型,但你也可以显式指定:
typescript
// 显式返回类型
const useStore = defineStore('store', {
state: () => ({
count: 0
}),
getters: {
// 显式指定返回类型
double(): number {
return this.count * 2
},
// 或者让 TypeScript 推断
triple() {
return this.count * 3 // 自动推断为 number
}
},
actions: {
// 参数类型
increment(amount: number = 1) {
this.count += amount
}
}
})
六、持久化存储
6.1 使用插件
bash
npm install pinia-plugin-persistedstate
6.2 配置持久化
typescript
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// 在 Store 中使用
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
token: ''
}),
// 开启持久化
persist: {
// 自定义 key
key: 'my-app-auth',
// 持久化到 localStorage
storage: localStorage,
// 只持久化部分状态
paths: ['token', 'user.id', 'user.name'],
// 自定义序列化
serializer: {
serialize: JSON.stringify,
deserialize: JSON.parse
},
// 钩子函数
beforeRestore: (ctx) => {
console.log('准备恢复状态:', ctx.store.$id)
},
afterRestore: (ctx) => {
console.log('状态恢复完成:', ctx.store.$id)
}
}
})
// 或者使用 Composition API 风格
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const total = ref(0)
return { items, total }
}, {
persist: {
key: 'shopping-cart',
storage: sessionStorage, // 使用 sessionStorage
paths: ['items'] // 只持久化 items
}
})
6.3 手动持久化
typescript
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null as User | null,
token: ''
}),
actions: {
initFromStorage() {
const saved = localStorage.getItem('auth')
if (saved) {
const { user, token } = JSON.parse(saved)
this.user = user
this.token = token
}
},
saveToStorage() {
localStorage.setItem('auth', JSON.stringify({
user: this.user,
token: this.token
}))
}
},
// 监听状态变化自动保存
onStateChange: {
handler(state) {
localStorage.setItem('auth', JSON.stringify(state))
},
deep: true
}
})