Pinia 状态管理实战 | 从 0 到 1 搭建 Vue3 项目状态层(附模块化 / 持久化)
一、为什么是 Pinia?
还记得 Vuex 吗?那个陪伴我们多年的状态管理库,有着严格的 mutations、actions 分工,写起来像在写 Java------虽然严谨,但也繁琐。
javascript
// Vuex 时代的痛
mutations: {
SET_USER(state, user) {
state.user = user
}
},
actions: {
async fetchUser({ commit }) {
const user = await api.getUser()
commit('SET_USER', user) // 绕了一大圈
}
}
而 Pinia 来了,它说:「简单点,写代码的方式简单点」
javascript
// Pinia 的快乐
export const useUserStore = defineStore('user', {
state: () => ({ user: null }),
actions: {
async fetchUser() {
this.user = await api.getUser() // 直接赋值,爽!
}
}
})
1.1 Pinia 的核心优势
| 特性 | Vuex | Pinia |
|---|---|---|
| mutations | ✅ 必须写 | ❌ 没了 |
| TypeScript 支持 | 😖 痛苦 | 😎 原生支持 |
| 代码量 | 多 | 少 30% |
| 学习曲线 | 陡峭 | 平缓 |
| DevTools | ✅ | ✅ 更好 |
二、项目初始化:从 0 开始搭建状态层
承接上一节的 Vite 项目,我们来深度拆解状态管理。
2.1 安装 Pinia
bash
npm install pinia
npm install pinia-plugin-persistedstate # 持久化插件(后面会讲)
2.2 在 main.ts 中注册
typescript
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
// 创建 Pinia 实例
const pinia = createPinia()
const app = createApp(App)
// 注册插件(顺序很重要:先 Pinia,后路由)
app.use(pinia)
app.use(router)
app.mount('#app')
三、Store 的两种写法:你pick哪一种?
Pinia 支持两种 Store 定义方式,就像 Vue 有 Options API 和 Composition API 一样。
3.1 Options Store(类似 Vuex 风格)
typescript
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state:数据源
state: () => ({
count: 0,
name: '计数器'
}),
// getters:计算属性
getters: {
doubleCount: (state) => state.count * 2,
// 使用 this 访问其他 getter
displayText(): string {
return `${this.name}: ${this.count} (翻倍后: ${this.doubleCount})`
}
},
// actions:方法(支持同步异步)
actions: {
increment(amount = 1) {
this.count += amount
},
async fetchAndSetCount() {
// 模拟异步请求
const res = await fetch('/api/count')
const data = await res.json()
this.count = data.count
}
}
})
3.2 Setup Store(Composition API 风格)⭐推荐
typescript
// stores/counter.ts (Setup Store)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state:用 ref/reactive
const count = ref(0)
const name = ref('计数器')
// getters:用 computed
const doubleCount = computed(() => count.value * 2)
const displayText = computed(() => {
return `${name.value}: ${count.value} (翻倍后: ${doubleCount.value})`
})
// actions:普通函数
function increment(amount = 1) {
count.value += amount
}
async function fetchAndSetCount() {
const res = await fetch('/api/count')
const data = await res.json()
count.value = data.count
}
// 必须返回所有暴露的内容
return {
count,
name,
doubleCount,
displayText,
increment,
fetchAndSetCount
}
})
为什么推荐 Setup Store?
- 更灵活,可以组合复用逻辑
- TypeScript 类型推导更好
- 符合 Vue3 Composition API 的心智模型
四、模块化设计:把大象装进冰箱分几步?
企业级项目最忌讳「一个大 Store 管所有」。正确的姿势是:按业务模块拆分。
4.1 推荐的项目结构
text
src/stores/
├── index.ts # 统一导出
├── modules/
│ ├── user.ts # 用户模块
│ ├── cart.ts # 购物车模块
│ ├── product.ts # 商品模块
│ └── app.ts # 应用配置(主题/语言等)
├── composables/ # 可复用的组合逻辑
│ ├── useAuth.ts
│ └── useCache.ts
└── plugins/ # Pinia 插件
└── logger.ts
4.2 用户模块(完整示例)
typescript
// stores/modules/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { UserInfo, LoginParams } from '@/types/user'
import { loginApi, getUserInfoApi } from '@/api/user'
import { ElMessage } from 'element-plus'
export const useUserStore = defineStore('user', () => {
// --- State ---
const token = ref<string | null>(localStorage.getItem('token'))
const userInfo = ref<UserInfo | null>(null)
const permissions = ref<string[]>([])
// --- Getters ---
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => userInfo.value?.name || '游客')
const userRole = computed(() => userInfo.value?.role || 'guest')
const hasPermission = computed(() => (perm: string) => {
return permissions.value.includes(perm) || userRole.value === 'admin'
})
// --- Actions ---
// 登录
async function login(params: LoginParams) {
try {
const res = await loginApi(params)
token.value = res.token
userInfo.value = res.userInfo
permissions.value = res.permissions || []
// 同步到 localStorage
localStorage.setItem('token', res.token)
ElMessage.success('登录成功')
return true
} catch (error) {
ElMessage.error('登录失败:' + (error as Error).message)
return false
}
}
// 登出
function logout() {
token.value = null
userInfo.value = null
permissions.value = []
localStorage.removeItem('token')
ElMessage.success('已退出登录')
}
// 获取用户信息
async function fetchUserInfo() {
if (!token.value) return
try {
const res = await getUserInfoApi()
userInfo.value = res.userInfo
permissions.value = res.permissions
} catch (error) {
console.error('获取用户信息失败:', error)
// token 无效,自动登出
if ((error as any).response?.status === 401) {
logout()
}
}
}
// 更新用户信息
function updateUserInfo(data: Partial<UserInfo>) {
if (userInfo.value) {
userInfo.value = { ...userInfo.value, ...data }
}
}
return {
// state
token,
userInfo,
permissions,
// getters
isLoggedIn,
userName,
userRole,
hasPermission,
// actions
login,
logout,
fetchUserInfo,
updateUserInfo
}
})
4.3 应用配置模块(主题/语言)
typescript
// stores/modules/app.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
type Theme = 'light' | 'dark'
type Language = 'zh' | 'en'
export const useAppStore = defineStore('app', () => {
// 从 localStorage 读取初始值
const getInitialTheme = (): Theme => {
const saved = localStorage.getItem('theme') as Theme
return saved || 'light'
}
const getInitialLanguage = (): Language => {
const saved = localStorage.getItem('language') as Language
return saved || 'zh'
}
// State
const theme = ref<Theme>(getInitialTheme())
const language = ref<Language>(getInitialLanguage())
const sidebarCollapsed = ref(false)
// Getters
const isDark = computed(() => theme.value === 'dark')
const currentLanguage = computed(() => language.value)
// Actions
function setTheme(newTheme: Theme) {
theme.value = newTheme
localStorage.setItem('theme', newTheme)
// 更新 HTML 的 data-theme 属性(用于 CSS 变量)
document.documentElement.setAttribute('data-theme', newTheme)
}
function toggleTheme() {
setTheme(theme.value === 'light' ? 'dark' : 'light')
}
function setLanguage(lang: Language) {
language.value = lang
localStorage.setItem('language', lang)
}
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
}
return {
theme,
language,
sidebarCollapsed,
isDark,
currentLanguage,
setTheme,
toggleTheme,
setLanguage,
toggleSidebar
}
})
4.4 统一导出(方便使用)
typescript
// stores/index.ts
export { useUserStore } from './modules/user'
export { useAppStore } from './modules/app'
export { useCartStore } from './modules/cart'
export { useProductStore } from './modules/product'
// 如果需要,可以创建一个组合多个 store 的 hook
import { useUserStore } from './modules/user'
import { useAppStore } from './modules/app'
export const useStore = () => ({
user: useUserStore(),
app: useAppStore()
})
五、持久化:让状态「记住」自己
5.1 问题场景
用户登录后刷新页面,状态丢了------这是初学者最常见的困惑。
javascript
// 刷新后,token 没了,又要重新登录
// 用户体验:???
5.2 解决方案:pinia-plugin-persistedstate
bash
npm install pinia-plugin-persistedstate
typescript
// src/main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 注册插件
5.3 基本用法
typescript
// stores/modules/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
token: null,
userInfo: null
}),
persist: true // 一键开启持久化
})
就这么简单!默认会:
- 使用
localStorage - key 为
store名(这里是 'user') - 自动同步整个 state
5.4 高级配置:按需持久化
有时候我们不想存所有东西(比如敏感信息、临时数据):
typescript
// stores/modules/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
token: null,
userInfo: null,
tempSearchKeyword: '', // 这个不想持久化
loginTime: null
}),
persist: {
key: 'user-storage', // 自定义存储 key
storage: localStorage, // 可选 sessionStorage
paths: ['token', 'userInfo'], // 只持久化这两个字段
beforeRestore: (context) => {
console.log('即将恢复状态', context)
},
afterRestore: (context) => {
console.log('状态恢复完成', context)
}
}
})
5.5 Setup Store 的持久化写法
typescript
// stores/modules/app.ts
export const useAppStore = defineStore('app', () => {
const theme = ref('light')
const language = ref('zh')
// ... 其他逻辑
return {
theme,
language
}
}, {
persist: {
key: 'app-settings',
paths: ['theme', 'language'] // 只持久化主题和语言
}
})
5.6 多标签页同步
如果你想让多个标签页的状态保持同步,可以这样配置:
typescript
// stores/modules/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
token: null
}),
persist: {
storage: localStorage,
// 监听 storage 事件,实现多标签页同步
beforeRestore: (context) => {
window.addEventListener('storage', (e) => {
if (e.key === 'user-storage') {
// 重新恢复状态
context.store.$hydrate()
}
})
}
}
})
六、Store 组合与复用(类似 Composables)
这是 Pinia 最强大的特性之一:Store 可以像组合式函数一样复用 -5。
6.1 场景:多个模块需要认证逻辑
假设你的应用有多个模块都需要用到用户认证状态,不想在每个 Store 里重复写一遍登录/登出逻辑。
typescript
// stores/composables/useAuth.ts
import { ref, computed } from 'vue'
export function useAuth() {
const isLoggedIn = ref(false)
const username = ref('')
function login(name: string) {
isLoggedIn.value = true
username.value = name
}
function logout() {
isLoggedIn.value = false
username.value = ''
}
return {
isLoggedIn,
username,
login,
logout
}
}
6.2 在 Store 中复用
typescript
// stores/modules/user.ts
import { defineStore } from 'pinia'
import { useAuth } from '../composables/useAuth'
export const useUserStore = defineStore('user', () => {
// 复用认证逻辑
const { isLoggedIn, username, login, logout } = useAuth()
// 扩展用户专属状态
const userId = ref<number | null>(null)
const avatar = ref('')
// 扩展登录方法
const loginWithId = (name: string, id: number) => {
login(name) // 调用复用的 login
userId.value = id
}
return {
isLoggedIn,
username,
userId,
avatar,
login: loginWithId,
logout
}
})
// stores/modules/admin.ts
import { defineStore } from 'pinia'
import { useAuth } from '../composables/useAuth'
export const useAdminStore = defineStore('admin', () => {
// 同样复用认证逻辑
const { isLoggedIn, username, login, logout } = useAuth()
// 管理员特有的状态
const adminLevel = ref(1)
return {
isLoggedIn,
username,
adminLevel,
login,
logout
}
})
6.3 场景:数据缓存逻辑复用
多个模块都需要缓存数据(比如商品列表、订单列表),可以封装一个通用的缓存逻辑-5:
typescript
// stores/composables/useCache.ts
import { ref } from 'vue'
export function useCache<T>(key: string, fetchFn: () => Promise<T>, expireTime = 5 * 60 * 1000) {
const cachedData = ref<T | null>(null)
const lastFetchTime = ref<number | null>(null)
const getData = async () => {
const now = Date.now()
// 如果有缓存且未过期,直接返回缓存
if (cachedData.value && lastFetchTime.value && (now - lastFetchTime.value) < expireTime) {
console.log(`[缓存命中] ${key}`)
return cachedData.value
}
// 否则重新获取
console.log(`[缓存失效] ${key},重新获取`)
const freshData = await fetchFn()
cachedData.value = freshData
lastFetchTime.value = now
return freshData
}
const clearCache = () => {
cachedData.value = null
lastFetchTime.value = null
}
return {
getData,
clearCache,
cachedData
}
}
typescript
// stores/modules/product.ts
import { defineStore } from 'pinia'
import { useCache } from '../composables/useCache'
import { fetchProductList } from '@/api/product'
export const useProductStore = defineStore('product', () => {
const { getData, clearCache, cachedData } = useCache(
'products',
fetchProductList,
10 * 60 * 1000 // 10分钟缓存
)
const loadProducts = async () => {
return await getData()
}
return {
products: cachedData,
loadProducts,
clearCache
}
})
七、在组件中使用:三种姿势
7.1 基础用法(最常用)
vue
<!-- views/Profile.vue -->
<template>
<div class="profile">
<h2>个人中心</h2>
<div v-if="userStore.isLoggedIn">
<el-avatar :src="userStore.userInfo?.avatar" />
<p>用户名:{{ userStore.userName }}</p>
<p>角色:{{ userStore.userRole }}</p>
<el-button @click="handleLogout">退出登录</el-button>
</div>
<div v-else>
<p>请先登录</p>
<el-button @click="goToLogin">去登录</el-button>
</div>
<!-- 测试权限指令 -->
<button v-if="userStore.hasPermission('product:edit')">
编辑商品
</button>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/modules/user'
import { useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
const userStore = useUserStore()
const router = useRouter()
const handleLogout = () => {
ElMessageBox.confirm('确认退出登录吗?', '提示', {
type: 'info'
}).then(() => {
userStore.logout()
router.push('/login')
})
}
const goToLogin = () => {
router.push('/login')
}
</script>
7.2 解构赋值(小心丢失响应性)
vue
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia' // 重要!
const userStore = useUserStore()
// ❌ 错误:直接解构会丢失响应性
const { userName, isLoggedIn } = userStore
// ✅ 正确:使用 storeToRefs
const { userName, isLoggedIn, userInfo } = storeToRefs(userStore)
// actions 可以直接解构(不会丢失)
const { login, logout } = userStore
</script>
7.3 在路由守卫中使用
typescript
// src/router/index.ts
import { useUserStore } from '@/stores/modules/user'
router.beforeEach((to, from, next) => {
// 需要手动获取 store 实例
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next({ path: '/login', query: { redirect: to.fullPath } })
} else {
next()
}
})
7.4 在 axios 拦截器中使用
typescript
// src/utils/request.ts
import { useUserStore } from '@/stores/modules/user'
request.interceptors.request.use((config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
})
request.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
const userStore = useUserStore()
userStore.logout() // 自动清除状态
router.push('/login')
}
return Promise.reject(error)
}
)
八、Pinia 插件开发:定制你的专属功能
8.1 日志插件:记录所有状态变化
typescript
// stores/plugins/logger.ts
import type { PiniaPluginContext } from 'pinia'
export function loggerPlugin({ store, options }: PiniaPluginContext) {
// 订阅 state 变化
store.$subscribe((mutation, state) => {
console.group(`📝 [${store.$id}] 状态变化`)
console.log('类型:', mutation.type)
console.log('载荷:', mutation.payload)
console.log('新状态:', state)
console.groupEnd()
})
// 订阅 action 调用
store.$onAction(({
name, // action 名称
store, // store 实例
args, // 参数
after, // 成功后回调
onError // 失败后回调
}) => {
console.log(`🚀 [${store.$id}] 调用 action: ${name}`, args)
after(result => {
console.log(`✅ [${store.$id}] action 成功: ${name}`, result)
})
onError(error => {
console.error(`❌ [${store.$id}] action 失败: ${name}`, error)
})
})
}
8.2 注册插件
typescript
// src/main.ts
import { loggerPlugin } from './stores/plugins/logger'
const pinia = createPinia()
pinia.use(loggerPlugin) // 全局生效
8.3 自定义持久化插件
typescript
// stores/plugins/customPersist.ts
export function customPersist({ store }: PiniaPluginContext) {
// 从 localStorage 恢复状态
const savedState = localStorage.getItem(`pinia:${store.$id}`)
if (savedState) {
store.$patch(JSON.parse(savedState))
}
// 订阅变化并保存
store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia:${store.$id}`, JSON.stringify(state))
})
}
九、性能优化与最佳实践
9.1 避免在 getter 中返回新对象
typescript
// ❌ 错误:每次访问都返回新对象,破坏缓存
getters: {
filteredList: (state) => {
return state.list.filter(item => item.active) // 每次都是新数组
}
}
// ✅ 正确:getter 本身会缓存计算结果
getters: {
activeCount: (state) => state.list.filter(item => item.active).length
}
9.2 按需加载 Store
typescript
// 在组件中动态导入(适用于大型应用)
const useUserStore = () => import('@/stores/user').then(m => m.useUserStore)
// 或者在路由懒加载时使用
const UserModule = () => import('@/views/User.vue')
9.3 使用 shallowRef 优化大对象
typescript
import { shallowRef } from 'vue'
// 对于大型对象,不需要深度响应式
const bigData = shallowRef(null)
// 只有整体替换时才触发更新
bigData.value = await fetchLargeDataset()
9.4 重置 Store 状态
typescript
// 添加重置方法
export const useUserStore = defineStore('user', () => {
const initialState = {
token: null,
userInfo: null,
permissions: []
}
const token = ref(initialState.token)
const userInfo = ref(initialState.userInfo)
const permissions = ref(initialState.permissions)
function $reset() {
token.value = initialState.token
userInfo.value = initialState.userInfo
permissions.value = initialState.permissions
localStorage.removeItem('token')
}
return {
token,
userInfo,
permissions,
$reset,
// ... 其他 actions
}
})
十、TypeScript 类型增强
10.1 为 store 添加类型
typescript
// stores/modules/user.ts
import type { UserInfo } from '@/types/user'
export interface UserState {
token: string | null
userInfo: UserInfo | null
permissions: string[]
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
token: null,
userInfo: null,
permissions: []
})
})
10.2 扩展 Pinia 类型(为所有 store 添加通用方法)
typescript
// types/pinia.d.ts
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties {
// 给所有 store 添加 $reset 方法
$reset(): void
// 添加自定义属性
readonly $id: string
}
export interface PiniaCustomStateProperties<S> {
// 给所有 state 添加 toJSON 方法
toJSON(): S
}
}
10.3 为插件添加类型
typescript
// stores/plugins/logger.ts
import type { PiniaPluginContext } from 'pinia'
export interface LoggerPluginOptions {
enabled?: boolean
filter?: (storeId: string) => boolean
}
export function loggerPlugin(options: LoggerPluginOptions = {}) {
return (context: PiniaPluginContext) => {
// 插件逻辑
}
}
十一、实战演练:完整的购物车模块
让我们把学到的知识串起来,实现一个完整的购物车模块。
11.1 购物车 Store
typescript
// stores/modules/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { CartItem, Product } from '@/types'
import { ElMessage } from 'element-plus'
export const useCartStore = defineStore('cart', () => {
// --- State ---
const items = ref<CartItem[]>([])
const loading = ref(false)
const lastUpdated = ref<Date | null>(null)
// --- Getters ---
const totalCount = computed(() => {
return items.value.reduce((sum, item) => sum + item.quantity, 0)
})
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
const isEmpty = computed(() => items.value.length === 0)
const formattedTotal = computed(() => {
return `¥${totalPrice.value.toFixed(2)}`
})
// --- Actions ---
function addItem(product: Product, quantity = 1) {
const existing = items.value.find(item => item.id === product.id)
if (existing) {
existing.quantity += quantity
} else {
items.value.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity
})
}
lastUpdated.value = new Date()
ElMessage.success(`已添加 ${product.name} 到购物车`)
}
function removeItem(productId: number) {
const index = items.value.findIndex(item => item.id === productId)
if (index > -1) {
const removed = items.value[index]
items.value.splice(index, 1)
ElMessage.success(`已移除 ${removed.name}`)
}
}
function updateQuantity(productId: number, quantity: number) {
const item = items.value.find(item => item.id === productId)
if (item) {
if (quantity <= 0) {
removeItem(productId)
} else {
item.quantity = quantity
}
}
}
function clearCart() {
items.value = []
ElMessage.success('购物车已清空')
}
async function checkout() {
if (isEmpty.value) {
ElMessage.warning('购物车是空的')
return false
}
loading.value = true
try {
// 模拟提交订单
await new Promise(resolve => setTimeout(resolve, 1500))
// 提交成功后清空购物车
clearCart()
ElMessage.success('下单成功!')
return true
} catch (error) {
ElMessage.error('下单失败,请重试')
return false
} finally {
loading.value = false
}
}
return {
// state
items,
loading,
lastUpdated,
// getters
totalCount,
totalPrice,
isEmpty,
formattedTotal,
// actions
addItem,
removeItem,
updateQuantity,
clearCart,
checkout
}
}, {
persist: {
key: 'shopping-cart',
paths: ['items'], // 只持久化商品列表
storage: localStorage
}
})
11.2 在组件中使用
vue
<!-- components/CartIcon.vue -->
<template>
<el-badge :value="cartStore.totalCount" :hidden="cartStore.isEmpty">
<el-button :icon="ShoppingCart" @click="showCartDrawer = true">
购物车
</el-button>
</el-badge>
<el-drawer v-model="showCartDrawer" title="购物车" size="400px">
<div v-loading="cartStore.loading" class="cart-content">
<template v-if="!cartStore.isEmpty">
<div v-for="item in cartStore.items" :key="item.id" class="cart-item">
<img :src="item.image" :alt="item.name" class="item-image">
<div class="item-info">
<h4>{{ item.name }}</h4>
<p class="item-price">¥{{ item.price }}</p>
</div>
<div class="item-actions">
<el-input-number
v-model="item.quantity"
:min="1"
:max="99"
size="small"
@change="handleQuantityChange(item.id, $event)"
/>
<el-button
type="danger"
:icon="Delete"
link
@click="cartStore.removeItem(item.id)"
/>
</div>
</div>
<div class="cart-footer">
<div class="total">
<span>总计:</span>
<span class="total-price">{{ cartStore.formattedTotal }}</span>
</div>
<el-button
type="primary"
:loading="cartStore.loading"
@click="handleCheckout"
>
结算
</el-button>
</div>
</template>
<el-empty v-else description="购物车空空如也" />
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ShoppingCart, Delete } from '@element-plus/icons-vue'
import { useCartStore } from '@/stores/modules/cart'
import { ElMessageBox } from 'element-plus'
const cartStore = useCartStore()
const showCartDrawer = ref(false)
const handleQuantityChange = (productId: number, quantity: number) => {
cartStore.updateQuantity(productId, quantity)
}
const handleCheckout = async () => {
ElMessageBox.confirm('确认提交订单吗?', '提示', {
type: 'info'
}).then(async () => {
const success = await cartStore.checkout()
if (success) {
showCartDrawer.value = false
}
})
}
</script>
<style scoped lang="scss">
.cart-content {
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
}
.cart-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #eee;
.item-image {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
margin-right: 12px;
}
.item-info {
flex: 1;
h4 {
margin: 0 0 4px;
font-size: 14px;
}
.item-price {
margin: 0;
color: #f56c6c;
font-weight: bold;
}
}
.item-actions {
display: flex;
align-items: center;
gap: 8px;
}
}
.cart-footer {
margin-top: auto;
padding-top: 20px;
border-top: 2px solid #eee;
.total {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
font-size: 16px;
.total-price {
color: #f56c6c;
font-size: 20px;
font-weight: bold;
}
}
}
</style>
十二、总结与进阶
12.1 Pinia 核心要点回顾
| 概念 | 作用 | 类比 |
|---|---|---|
| State | 存储数据 | 组件的 data |
| Getter | 计算派生状态 | 组件的 computed |
| Action | 修改状态的方法 | 组件的 methods |
| Plugin | 扩展功能 | 全局混入 |
| Store | 上述内容的容器 | 一个模块 |
12.2 什么时候用 Pinia?
- ✅ 多个组件共享同一份数据
- ✅ 数据需要跨路由持久化
- ✅ 有复杂的业务逻辑需要复用
- ✅ 需要 DevTools 调试状态变化
- ❌ 简单的父子组件通信(用 props/emit 就够了)
12.3 下一步学习方向
- Pinia + Vue Query:服务端状态管理
- Pinia + WebSocket:实时数据同步
- Pinia 源码阅读:理解响应式原理
- 自定义插件开发:根据项目需求定制
12.4 写在最后
从 Vuex 到 Pinia,不仅仅是 API 的简化,更是对「状态管理应该简单」这一理念的回归。就像 Evan You 说的:
"Pinia 成功地在保持清晰的设计分离的同时,提供了简单、小巧且易于上手的 API。"
掌握 Pinia,不是为了炫技,而是为了让代码更清晰、维护更简单。现在,去重构你项目里的状态管理吧!🚀