645 字
2 分钟
Pinia 完全指南:Vue 3 状态管理的最佳实践
🍍 什么是 Pinia?
Pinia 是 Vue 生态系统的官方状态管理库,由 Vue.js 核心团队维护。它是 Vuex 的继任者,专为 Vue 3 设计,提供了更简洁、更强大的状态管理解决方案。
为什么选择 Pinia?
1. 开箱即用的 TypeScript 支持
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', { state: () => ({ name: '' as string, age: 0 as number, isAdmin: false }), getters: { adult: (state) => state.age >= 18 }, actions: { setName(name: string) { this.name = name } }})2. 极简的 API 设计
- 无需复杂的配置和设置
- 去除了 Vuex 的 mutations
- 支持组合式 API 和选项式 API
3. DevTools 支持
- 完整的 Vue DevTools 集成
- 时间旅行调试
- 状态快照和回滚
4. 自动化代码分割
- 每个 store 都是独立的模块
- 按需加载,优化性能
- 完美的 Tree-shaking 支持
🔄 Pinia vs Vuex
核心差异对比
| 特性 | Pinia | Vuex |
|---|---|---|
| ** mutations** | ❌ 不需要 | ✅ 必须使用 |
| ** TypeScript** | ✅ 原生支持 | ⚠️ 需要额外配置 |
| 模块化 | ✅ 自动支持 | ⚠️ 需要手动配置 |
| 组合式 API | ✅ 完美支持 | ⚠️ 有限支持 |
| 学习曲线 | 🟢 平缓 | 🟡 陡峭 |
代码对比示例
Vuex 4 写法
import { createStore } from 'vuex'
export default createStore({ state: { user: null, token: '' }, mutations: { SET_USER(state, user) { state.user = user }, SET_TOKEN(state, token) { state.token = token } }, actions: { async login({ commit }, credentials) { const { user, token } = await api.login(credentials) commit('SET_USER', user) commit('SET_TOKEN', token) } }, getters: { isAuthenticated: (state) => !!state.token }})Pinia 写法(更简洁)
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', { state: () => ({ user: null, token: '' }), actions: { async login(credentials) { const { user, token } = await api.login(credentials) this.user = user // 直接修改 state this.token = token } }, getters: { isAuthenticated: () => !!this.token }})🚀 快速开始
安装配置
# npmnpm install pinia
# yarnyarn add pinia
# pnpmpnpm add pinia基础设置
import { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'
const app = createApp(App)const pinia = createPinia()
app.use(pinia)app.mount('#app')📦 Store 的定义和使用
选项式 API(Options Store)
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', { // 状态 state: () => ({ count: 0, name: 'My Counter' }),
// 计算属性 getters: { doubleCount: (state) => state.count * 2, displayName(): string { return `${this.name} (${this.count})` } },
// 方法 actions: { increment() { this.count++ }, reset() { this.count = 0 }, async fetchCount() { const response = await fetch('/api/count') this.count = await response.json() } }})组合式 API(Setup Store)
import { defineStore } from 'pinia'import { ref, computed } from 'vue'
export const useTodoStore = defineStore('todo', () => { // state const todos = ref<Todo[]>([]) const filter = ref<'all' | 'active' | 'completed'>('all')
// getters const filteredTodos = computed(() => { switch (filter.value) { case 'active': return todos.value.filter(t => !t.completed) case 'completed': return todos.value.filter(t => t.completed) default: return todos.value } })
const stats = computed(() => ({ total: todos.value.length, active: todos.value.filter(t => !t.completed).length, completed: todos.value.filter(t => t.completed).length }))
// actions async function fetchTodos() { const response = await fetch('/api/todos') todos.value = await response.json() }
function addTodo(text: string) { todos.value.push({ id: Date.now(), text, completed: false }) }
function toggleTodo(id: number) { const todo = todos.value.find(t => t.id === id) if (todo) todo.completed = !todo.completed }
return { todos, filter, filteredTodos, stats, fetchTodos, addTodo, toggleTodo }})在组件中使用
<script setup lang="ts">import { useCounterStore } from '@/stores/counter'import { useTodoStore } from '@/stores/todo'
// 使用 storeconst counter = useCounterStore()const todo = useTodoStore()
// 直接访问状态console.log(counter.count)
// 调用 actionscounter.increment()
// 使用 gettersconsole.log(counter.doubleCount)
// 在模板中使用</script>
<template> <div> <h1>{{ counter.name }}</h1> <p>Count: {{ counter.count }}</p> <p>Double: {{ counter.doubleCount }}</p> <button @click="counter.increment">+</button> <button @click="counter.reset">Reset</button> </div></template>🎯 高级特性
1. Store 的组合使用
export const useAuthStore = defineStore('auth', { state: () => ({ user: null as User | null, token: '' }), actions: { async login(credentials) { const { user, token } = await api.login(credentials) this.user = user this.token = token } }})
// stores/settings.tsexport const useSettingsStore = defineStore('settings', { state: () => ({ theme: 'light' as 'light' | 'dark', language: 'zh-CN' })})
// 在组件中同时使用多个 store<script setup>import { useAuthStore } from '@/stores/auth'import { useSettingsStore } from '@/stores/settings'
const auth = useAuthStore()const settings = useSettingsStore()</script>2. 持久化存储
import { defineStore } from 'pinia'import { useStorage } from '@vueuse/core'
export const useMainStore = defineStore('main', { state: () => ({ userPreferences: useStorage('prefs', { theme: 'light', fontSize: 16 }), sidebarOpen: useStorage('sidebar', true) })})3. 插件系统
import { PiniaPluginContext } from 'pinia'
export function persistPlugin({ store }: PiniaPluginContext) { // 从 localStorage 恢复状态 const saved = localStorage.getItem(`pinia-${store.$id}`) if (saved) { store.$patch(JSON.parse(saved)) }
// 监听状态变化并保存 store.$subscribe(() => { localStorage.setItem( `pinia-${store.$id}`, JSON.stringify(store.$state) ) })}
// main.tsimport { createPinia } from 'pinia'import { persistPlugin } from './plugins/pinia-persist'
const pinia = createPinia()pinia.use(persistPlugin)4. Store 的监听和订阅
// 订阅状态变化const counter = useCounterStore()
// $subscribe 订阅状态变化counter.$subscribe((mutation, state) => { console.log('State changed:', mutation, state) // 可以在这里实现持久化、日志记录等})
// $onAction 订阅 action 调用counter.$onAction(({ name, args, after }) => { console.log(`Action ${name} called with`, args)
// 在 action 完成后执行 after((result) => { console.log(`Action ${name} finished with`, result) })})
// $reset 重置到初始状态counter.$reset()🔥 实战项目示例
电商购物车 Store
import { defineStore } from 'pinia'import { computed, ref } from 'vue'
export interface CartItem { id: number name: string price: number quantity: number image: string}
export const useCartStore = defineStore('cart', () => { // 状态 const items = ref<CartItem[]>([]) const coupon = ref<string | null>(null) const discount = ref(0)
// 计算属性 const subtotal = computed(() => items.value.reduce((sum, item) => sum + item.price * item.quantity, 0) )
const total = computed(() => subtotal.value - discount.value )
const itemCount = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0) )
const isEmpty = computed(() => items.value.length === 0)
// Actions function addItem(product: Product) { const existingItem = items.value.find(item => item.id === product.id)
if (existingItem) { existingItem.quantity++ } else { items.value.push({ id: product.id, name: product.name, price: product.price, quantity: 1, image: product.image }) } }
function removeItem(id: number) { const index = items.value.findIndex(item => item.id === id) if (index > -1) { items.value.splice(index, 1) } }
function updateQuantity(id: number, quantity: number) { const item = items.value.find(item => item.id === id) if (item) { item.quantity = Math.max(0, quantity) } }
function clearCart() { items.value = [] coupon.value = null discount.value = 0 }
async function applyCoupon(code: string) { const response = await fetch('/api/coupons/validate', { method: 'POST', body: JSON.stringify({ code }) }) const { valid, discount: amount } = await response.json()
if (valid) { coupon.value = code discount.value = amount } }
async function checkout() { const order = await fetch('/api/checkout', { method: 'POST', body: JSON.stringify({ items: items.value, coupon: coupon.value, total: total.value }) }).then(r => r.json())
clearCart() return order }
return { items, coupon, discount, subtotal, total, itemCount, isEmpty, addItem, removeItem, updateQuantity, clearCart, applyCoupon, checkout }})用户认证 Store
import { defineStore } from 'pinia'import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => { const user = ref<User | null>(null) const token = ref<string | null>(null) const loading = ref(false) const error = ref<string | null>(null)
const isAuthenticated = computed(() => !!token.value) const userDisplayName = computed(() => user.value ? user.value.name : 'Guest' )
async function login(credentials: LoginCredentials) { loading.value = true error.value = null
try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) })
if (!response.ok) { throw new Error('Login failed') }
const { user: userData, token: userToken } = await response.json()
user.value = userData token.value = userToken
// 保存到 localStorage localStorage.setItem('auth-token', userToken) } catch (err) { error.value = err instanceof Error ? err.message : 'Login failed' throw err } finally { loading.value = false } }
async function logout() { user.value = null token.value = null localStorage.removeItem('auth-token') }
async function fetchUser() { const savedToken = localStorage.getItem('auth-token') if (!savedToken) return
loading.value = true try { const response = await fetch('/api/auth/me', { headers: { 'Authorization': `Bearer ${savedToken}` } })
if (response.ok) { user.value = await response.json() token.value = savedToken } } finally { loading.value = false } }
return { user, token, loading, error, isAuthenticated, userDisplayName, login, logout, fetchUser }})🌟 最佳实践
1. 合理的 Store 划分
// ✅ 好的做法 - 按功能划分stores/├── auth.ts // 认证相关├── cart.ts // 购物车相关├── products.ts // 产品相关└── ui.ts // UI 状态
// ❌ 不好的做法 - 按技术划分stores/├── state.ts // 所有状态├── getters.ts // 所有计算属性└── actions.ts // 所有方法2. 使用 TypeScript 类型安全
interface User { id: number name: string email: string role: 'admin' | 'user' | 'guest'}
interface UserState { user: User | null isAuthenticated: boolean}
export const useUserStore = defineStore('user', { state: (): UserState => ({ user: null, isAuthenticated: false })})3. 组合式 API 优先
// ✅ 推荐 - 组合式 APIexport const useStore = defineStore('store', () => { const count = ref(0) const double = computed(() => count.value * 2) function increment() { count.value++ } return { count, double, increment }})
// ⚠️ 可选 - 选项式 APIexport const useStore = defineStore('store', { state: () => ({ count: 0 }), getters: { double: state => state.count * 2 }, actions: { increment() { this.count++ } }})4. 避免过度使用
<!-- ❌ 不好的做法 - 简单的本地状态不需要 Store --><script setup>import { useStore } from '@/stores/store'const store = useStore()</script>
<template> <input v-model="store.inputValue" /> <!-- 应该使用本地状态 --></template>
<!-- ✅ 好的做法 - 本地状态使用 ref --><script setup>import { ref } from 'vue'const inputValue = ref('')</script>
<template> <input v-model="inputValue" /></template>📚 性能优化
1. 懒加载 Store
// 只在需要时才加载 storeasync function loadAdminPanel() { const { useAdminStore } = await import('@/stores/admin') const admin = useAdminStore() // 使用 admin store}2. 避免不必要的响应式
// ✅ 好的做法 - 静态数据不需要响应式export const useConfigStore = defineStore('config', { state: () => ({ apiConfig: { baseUrl: import.meta.env.VITE_API_URL, timeout: 5000 } as const // 使用 as const 避免深层响应式 })})3. 使用 shallowRef
import { shallowRef } from 'vue'
export const useDataStore = defineStore('data', () => { // 大型对象使用 shallowRef const largeData = shallowRef<LargeObject>(null)
return { largeData }})🎓 总结
Pinia 作为 Vue 3 的官方状态管理库,提供了:
- ✅ 简单直观的 API,无需复杂的配置
- ✅ 完美的 TypeScript 支持,开发体验优秀
- ✅ 强大的组合能力,支持复杂的状态管理需求
- ✅ 优秀的性能表现,自动代码分割和懒加载
- ✅ 完整的 DevTools 集成,调试体验一流
无论是小型项目还是大型应用,Pinia 都是一个值得选择的现代状态管理解决方案。
TIP开始使用 Pinia 的最佳时机:
- 新项目直接使用 Pinia
- 现有项目可以考虑逐步迁移
- Vue 3 项目首选 Pinia 而非 Vuex
相关阅读:
有问题? 欢迎在评论区讨论你的 Pinia 使用经验!
分享
如果这篇文章对你有帮助,欢迎分享给更多人!
Pinia 完全指南:Vue 3 状态管理的最佳实践
https://basyc.cloud/posts/pinia-guide/ 部分信息可能已经过时
相关文章 智能推荐
1
Monorepo 入门指南:前端项目的现代化管理方式
技术分享 深入了解 Monorepo 的概念、优势和在前端开发中的应用,从零开始掌握现代化项目管理方式
2
Anime.js 完全指南:轻量级 JavaScript 动画引擎的现代化应用
技术分享 深入了解 Anime.js,一个功能强大且轻量级的 JavaScript 动画库。从基础概念到高级技巧,掌握现代化 Web 动画开发的最佳实践
3
Plane 部署指南:开源项目管理的完美解决方案
技术分享 详细教你如何使用 Docker 部署 Plane 开源项目管理工具,构建团队协作平台
4
你好,世界
生活 欢迎来到我的灵质空间,这里是我记录生活、分享技术、收藏回忆的地方




