从Vuex转到Pinia的过程,像把厚重的行李箱换成了轻便的背包。之前维护一个电商项目,用Vuex管理购物车状态,光是定义mutation、action、getter就写了上百行代码,每次修改状态都要跳转到不同的地方。换成Pinia后,代码量减少了近40%,而且不用再写那些繁琐的mutation——这种简洁直接的开发体验,让状态管理从负担变成了享受。
Pinia是Vue官方推荐的状态管理库,被称为"Vuex 5",专为Vue3设计,支持Composition API和TypeScript。它解决了Vuex的诸多痛点:取消了mutation的概念,简化了状态管理流程;天然支持TypeScript,类型提示更友好;无需嵌套模块,结构更扁平。本文通过一个购物车案例,详解Pinia的核心用法和实践技巧,帮你快速上手这个轻量级状态管理方案。
一、先搞懂:Pinia vs Vuex 核心差异
很多人疑惑:为什么要用Pinia替代Vuex?两者的核心区别主要在以下几点:
特性 | Vuex | Pinia |
核心概念 | State、Getter、Mutation、Action、Module | State、Getter、Action(无Mutation和Module) |
写法 | 选项式API为主 | 同时支持选项式和组合式API |
TypeScript支持 | 需要额外类型声明,体验较差 | 原生支持,类型推断友好 |
模块嵌套 | 支持嵌套模块,结构复杂时较繁琐 | 无嵌套模块,通过创建多个store实现模块化 |
代码量 | 冗余(需定义mutation) | 简洁(直接在action中修改状态) |
最直观的感受是:Pinia去掉了Vuex中争议最大的mutation,允许直接在action中修改状态,大幅减少了样板代码。同时通过创建多个store实现模块化,比Vuex的嵌套模块更灵活。
二、实战步骤:用Pinia实现购物车功能
我们以电商网站的购物车为例,实现以下功能:
- 加入商品到购物车
- 修改商品数量
- 移除商品
- 计算商品总价
- 持久化存储购物车数据(刷新页面不丢失)
1. 第一步:安装并初始化Pinia
首先在Vue3项目中安装Pinia:
npm install pinia
# 或
yarn add pinia然后在入口文件(main.js)中创建Pinia实例并挂载:
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
// 创建Pinia实例
const pinia = createPinia();
const app = createApp(App);
// 挂载Pinia
app.use(pinia);
app.mount('#app');2. 第二步:定义购物车Store
Pinia中没有模块(Module)的概念,而是通过创建多个store来实现模块化。每个store都是一个独立的状态容器。
创建stores/cartStore.js文件,定义购物车状态:
// stores/cartStore.js
import { defineStore } from 'pinia';
// 定义并导出购物车store(参数1:store唯一标识,参数2:配置对象)
export const useCartStore = defineStore('cart', {
// 状态:存储购物车数据
state: () => ({
items: [], // 购物车商品列表,格式:[{ id, name, price, quantity }]
// 其他状态...
}),
// 计算属性:类似Vue的computed,用于派生状态
getters: {
// 计算商品总数
totalQuantity: (state) => {
return state.items.reduce((total, item) => total + item.quantity, 0);
},
// 计算商品总价
totalPrice: (state) => {
return state.items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0).toFixed(2);
},
// 检查商品是否已在购物车中
isInCart: (state) => (id) => {
return state.items.some(item => item.id === id);
}
},
// 方法:类似Vue的methods,用于修改状态(支持异步)
actions: {
// 加入商品到购物车
addItem(product, quantity = 1) {
// 检查商品是否已存在
const existingItem = this.items.find(item => item.id === product.id);
if (existingItem) {
// 已存在则增加数量
existingItem.quantity += quantity;
} else {
// 不存在则添加新商品
this.items.push({
...product,
quantity // 新增quantity字段
});
}
// 调用持久化方法
this.persistCart();
},
// 修改商品数量
updateQuantity(id, quantity) {
const item = this.items.find(item => item.id === id);
if (item) {
item.quantity = quantity;
this.persistCart();
}
},
// 移除商品
removeItem(id) {
this.items = this.items.filter(item => item.id !== id);
this.persistCart();
},
// 清空购物车
clearCart() {
this.items = [];
this.persistCart();
},
// 从本地存储加载购物车数据
loadCart() {
const saved = localStorage.getItem('cartData');
if (saved) {
this.items = JSON.parse(saved);
}
},
// 持久化购物车数据到本地存储
persistCart() {
localStorage.setItem('cartData', JSON.stringify(this.items));
}
}
});关键特性解析:
state:返回初始状态的函数,避免在服务端渲染时出现状态污染;getters:不仅可以计算状态,还能定义接受参数的函数(如isInCart);actions:可以直接修改state(无需像Vuex那样通过mutation),支持异步操作;- 模块化:通过
defineStore创建多个store(如cart、user、product),实现状态分离。
3. 第三步:在组件中使用Store
在Vue组件中使用Pinia store非常简单,只需导入定义好的useCartStore并调用即可:
<!-- ProductList.vue:商品列表组件 -->
<template>
<div class="product-list">
<div v-for="product in products" :key="product.id" class="product-item">
<h3>{{ product.name }}</h3>
<p>价格:¥{{ product.price.toFixed(2) }}</p>
<button
class="add-btn"
@click="addToCart(product)"
:disabled="cartStore.isInCart(product.id)"
>
{{ cartStore.isInCart(product.id) ? '已在购物车' : '加入购物车' }}
</button>
</div>
</div>
</template>
<script setup>
import { useCartStore } from '../stores/cartStore';
// 获取购物车store实例
const cartStore = useCartStore();
// 模拟商品数据
const products = [
{ id: 1, name: 'Vue3实战指南', price: 59.9 },
{ id: 2, name: 'Pinia状态管理', price: 49.9 },
{ id: 3, name: 'TypeScript入门', price: 69.9 }
];
// 加入购物车
const addToCart = (product) => {
cartStore.addItem(product);
};
</script>
<style scoped>
.product-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px;
}
.product-item {
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 15px;
}
.add-btn {
margin-top: 10px;
padding: 6px 12px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>再创建购物车组件,展示已添加的商品:
<!-- Cart.vue:购物车组件 -->
<template>
<div class="cart-container">
<h2>购物车</h2>
<div v-if="cartStore.items.length === 0" class="empty-cart">
购物车是空的,快去添加商品吧~
</div>
<div v-else>
<div class="cart-item" v-for="item in cartStore.items" :key="item.id">
<span>{{ item.name }} - ¥{{ item.price.toFixed(2) }}</span>
<div class="quantity-control">
<button @click="cartStore.updateQuantity(item.id, item.quantity - 1)" :disabled="item.quantity <= 1">
-
</button>
<span>{{ item.quantity }}</span>
<button @click="cartStore.updateQuantity(item.id, item.quantity + 1)">+</button>
</div>
<button class="remove-btn" @click="cartStore.removeItem(item.id)">
移除
</button>
</div>
<div class="cart-summary">
<p>商品总数:{{ cartStore.totalQuantity }}</p>
<p>总价:¥{{ cartStore.totalPrice }}</p>
<button class="clear-btn" @click="cartStore.clearCart">
清空购物车
</button>
</div>
</div>
</div>
</template>
<script setup>
import { useCartStore } from '../stores/cartStore';
import { onMounted } from 'vue';
const cartStore = useCartStore();
// 页面加载时从本地存储加载购物车数据
onMounted(() => {
cartStore.loadCart();
});
</script>
<style scoped>
.cart-container {
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin: 20px;
}
.empty-cart {
color: #666;
padding: 20px;
text-align: center;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px dashed #e5e7eb;
}
.quantity-control {
display: flex;
align-items: center;
gap: 10px;
}
.quantity-control button {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.remove-btn {
color: #ff4d4f;
background: transparent;
border: none;
cursor: pointer;
}
.cart-summary {
margin-top: 20px;
padding-top: 10px;
border-top: 1px solid #e5e7eb;
}
.clear-btn {
margin-top: 10px;
padding: 6px 12px;
background-color: #ff4d4f;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>使用技巧:
- 调用
useCartStore()获取store实例(每个组件只需调用一次); - 直接访问
cartStore.items获取状态,调用cartStore.addItem()执行操作; - 通过
cartStore.totalPrice访问计算属性,无需加括号; - 在
onMounted钩子中调用loadCart(),实现页面刷新后恢复购物车数据。
三、进阶特性:Pinia的高级用法
1. 组合式API写法
除了选项式API,Pinia还支持组合式API写法,更适合复杂逻辑:
// stores/userStore.js(组合式API写法)
import { defineStore } from 'pinia';
import { ref, computed, watch } from 'vue';
export const useUserStore = defineStore('user', () => {
// 状态(相当于state)
const userInfo = ref(null);
const token = ref(localStorage.getItem('token') || '');
// 计算属性(相当于getters)
const isLogin = computed(() => !!token.value);
const userName = computed(() => userInfo.value?.name || '游客');
// 方法(相当于actions)
const login = async (credentials) => {
// 模拟API请求
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
});
const data = await res.json();
token.value = data.token;
userInfo.value = data.user;
localStorage.setItem('token', data.token);
};
const logout = () => {
token.value = '';
userInfo.value = null;
localStorage.removeItem('token');
};
// 监听token变化,自动加载用户信息
watch(token, async (newToken) => {
if (newToken && !userInfo.value) {
const res = await fetch('/api/userinfo');
userInfo.value = await res.json();
}
}, { immediate: true });
// 必须返回所有需要暴露的状态、计算属性和方法
return {
userInfo,
token,
isLogin,
userName,
login,
logout
};
});组合式API写法的优势:
- 可以直接使用Vue的组合式API(ref、computed、watch等);
- 逻辑组织更灵活,适合拆分复杂逻辑;
- 更自然地支持TypeScript类型推断。
2. 状态持久化
虽然我们在购物车案例中手动实现了持久化,但Pinia有更优雅的方案——使用pinia-plugin-persistedstate插件:
npm install pinia-plugin-persistedstate在Pinia初始化时使用插件:
// main.js
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate); // 使用持久化插件然后在store中配置持久化:
// stores/cartStore.js(添加持久化配置)
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
// ...其他配置
persist: {
// 存储的键名
key: 'cartData',
// 存储方式:localStorage(默认)或sessionStorage
storage: localStorage,
// 需要持久化的状态字段(默认全部)
paths: ['items']
}
});配置后,无需手动调用persistCart和loadCart,插件会自动同步状态到本地存储。
3. 状态重置与订阅
Pinia提供了便捷的状态重置和订阅方法:
// 在组件中使用
const cartStore = useCartStore();
// 重置状态(恢复到初始值)
const resetCart = () => {
cartStore.$reset();
};
// 订阅状态变化(类似Vuex的subscribe)
cartStore.$subscribe((mutation, state) => {
console.log('购物车状态变化:', mutation, state);
// 可以在这里实现自定义的持久化逻辑
});
// 订阅action调用
cartStore.$onAction(({ name, args, after, onError }) => {
console.log(`调用action:${name},参数:${args}`);
after((result) => {
console.log(`action ${name} 成功,结果:${result}`);
});
onError((error) => {
console.log(`action ${name} 失败,错误:${error}`);
});
});四、为什么选择Pinia?实际项目中的优势
在实际项目中,Pinia相比Vuex带来的好处非常明显:
- 代码更简洁:去掉mutation后,平均每个状态修改逻辑能减少30%的代码量;
- 开发效率更高:无需在mutation、action、组件之间来回切换,状态修改更直接;
- TypeScript支持更好:不需要复杂的类型声明,就能获得完整的类型提示;
- 学习成本低:API设计简洁,熟悉Vue3的开发者能快速上手;
- 更好的DevTools支持:Pinia的DevTools集成更完善,能显示状态变化历史。
从Vuex迁移到Pinia也非常平滑,官方提供了清晰的迁移指南,大部分逻辑可以直接复用。
总结
Pinia作为Vue3官方推荐的状态管理方案,通过简化概念(去掉mutation)、优化API设计和增强TypeScript支持,解决了Vuex的诸多痛点。它的核心优势在于:
- 轻量简洁:API设计直观,减少样板代码;
- 灵活多样:同时支持选项式和组合式API写法;
- 原生支持TypeScript:类型推断友好,开发体验更佳;
- 模块化清晰:通过多个store实现模块化,比嵌套模块更灵活。
对于新的Vue3项目,Pinia应该是状态管理的首选。即使是从Vuex迁移,也能显著提升开发效率和代码可维护性。正如Vue作者Evan You所说:"Pinia是Vuex的继任者,我们已经不再推荐使用Vuex了。"










