本文专为Vue3开发者打造,从Pinia基础认知入手,逐步讲解环境搭建、核心API用法,结合Vue3+TypeScript实战案例,覆盖日常开发99%场景,新手可直接套用代码,快速掌握Pinia全局状态管理,替代传统Vuex,提升开发效率。
核心定位:Pinia是Vue官方推荐的全局状态管理工具,2019年推出,旨在替代Vuex,采用组合式API风格,轻量、简洁且原生支持TS,适配Vue3和Vue2(本文重点聚焦Vue3+TS实战)。
一、Pinia基础认知(入门必看)
1.1 什么是Pinia
Pinia是一个用于跨组件、跨页面进行状态共享的全局状态管理库,功能与Vuex、Redux一致,但API更简洁,使用体验更贴近Vue3组合式API,本质上是Vuex5的最终实现形态——Vue官方团队在探索Vuex下一次迭代时,发现Pinia已满足大部分需求,最终决定用Pinia替代Vuex。
1.2 Pinia核心特点
- 完整TS支持:无需手动编写复杂类型声明,原生支持类型推断,TS开发体验拉满,补全更流畅。
- 极致轻量:压缩后体积仅1KB左右,无多余依赖,不增加项目负担。
- 简化语法:移除Vuex中繁琐的mutations,仅保留state、getters、actions,降低学习和使用成本。
- actions多支持:既支持同步操作,也支持异步操作(如接口请求),无需区分同步/异步逻辑。
- 扁平化结构:无模块嵌套,只有store概念,每个store独立存在,可自由调用,无需管理复杂的命名空间。
- 自动注册:store一旦创建,无需手动添加到全局,自动挂载,开箱即用。
- 跨版本兼容:同时支持Vue3和Vue2,除初始化安装和SSR配置外,两者API完全一致。
1.3 Pinia与Vuex的核心区别
Pinia最初是为探索Vuex下一次迭代而设计,整合了Vuex核心团队的诸多想法,最终成为Vuex的替代方案,两者核心区别如下:
| 对比维度 | Vuex | Pinia |
|---|---|---|
| 核心结构 | State、Getters、Mutations(同步)、Actions(异步) | State、Getters、Actions(同步+异步),无Mutations |
| 版本适配 | Vuex4适配Vue3,Vuex3适配Vue2,无法跨版本使用 | 最新版2.x,同时适配Vue3和Vue2 |
| TS支持 | 需创建自定义复杂包装器,类型推断不友好 | 原生支持TS,类型推断完善,无需额外配置 |
| 模块结构 | 支持模块嵌套,需配置命名空间,逻辑繁琐 | 扁平化结构,无嵌套,store独立,可自由调用 |
| 注册方式 | 需手动注册store到全局 | 自动注册,创建后即可使用 |
| API复杂度 | API繁琐,需记住mutations提交、命名空间等规则 | API简洁,贴近组合式API,上手成本低 |
1.4 适用场景
任何需要跨组件、跨页面共享状态的Vue3项目,无论是中小型项目(如个人博客、管理后台),还是大型项目(如电商平台),Pinia都能胜任,尤其适合TS开发的项目,能大幅提升开发效率和代码可维护性。
二、Vue3+Pinia环境搭建(实战第一步)
本章节以Vue3+TypeScript项目为例,讲解Pinia的安装、全局注册,步骤简洁,可直接复制命令和代码执行。
2.1 前提条件
已创建Vue3+TS项目(若未创建,执行命令:npm create vue@latest,选择TS、Pinia(可选,此处可跳过,后续手动安装))。
2.2 安装Pinia
打开终端,进入项目根目录,执行以下命令(三选一,推荐npm或yarn):
// npm 安装(推荐)
npm install pinia -S
// yarn 安装
yarn add pinia
// cnpm 安装
cnpm install pinia -S
2.3 全局注册Pinia(Vue3)
修改项目入口文件main.ts,引入并挂载Pinia实例,全局仅需配置一次:
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
// 引入Pinia的createPinia方法
import { createPinia } from 'pinia'
// 创建Pinia实例
const pinia = createPinia()
// 创建Vue应用并挂载Pinia
const app = createApp(App)
app.use(pinia) // 挂载Pinia到Vue应用
app.mount('#app')
补充:Vue2中注册方式略有不同(需引入PiniaVuePlugin),本文聚焦Vue3,Vue2用法可参考文末补充说明。
三、Pinia核心用法(Vue3+TS实战)
Pinia的核心是Store(仓库),每个Store对应一个独立的状态模块,通过defineStore方法创建,包含state(状态)、getters(计算属性)、actions(业务逻辑)三部分,以下逐一讲解。
3.1 初始化Store(核心步骤)
推荐在项目根目录下创建src/store文件夹,用于存放所有Store文件,按业务模块划分(如用户模块、购物车模块),规范命名(如userStore.ts、cartStore.ts)。
步骤:先定义Store名称枚举(避免重复),再创建Store实例。
第一步:定义Store名称枚举(可选,推荐)
创建src/store/store-name.ts,用于统一管理Store名称,避免重复(尤其多Store场景):
// src/store/store-name.ts
// 用枚举定义Store名称,唯一且直观
export const enum Names {
Test = 'TEST', // 测试Store名称
User = 'USER', // 用户Store名称
Cart = 'CART' // 购物车Store名称
}
第二步:创建Store实例
创建src/store/index.ts(或按模块拆分,如userStore.ts),使用defineStore方法创建Store,核心包含state、getters、actions:
// src/store/index.ts
import { defineStore } from 'pinia';
import { Names } from './store-name'; // 引入Store名称枚举
// defineStore接收两个参数:
// 1. 唯一标识(必须与枚举值一致,全局唯一,不可重复)
// 2. 配置对象(包含state、getters、actions)
export const useTestStore = defineStore(Names.Test, {
// 1. state:存储全局状态,必须是箭头函数(避免SSR数据污染,优化TS类型推导)
state: () => {
return {
current: 1, // 数字类型状态
name: '小马', // 字符串类型状态
list: [1, 2, 3] // 数组类型状态
};
},
// 2. getters:类似组件的computed,用于修饰状态,有缓存功能
getters: {
// 方式一:接收state作为参数(推荐,类型推断更友好)
myGetCount(state) {
// 缓存特性:页面多次使用,仅执行一次计算
console.log('getters被调用');
return state.current + 1;
},
// 方式二:不传递参数,使用this访问state(需指定返回值类型,否则TS推导失败)
myGetName(): string {
return `姓名:${this.name}`;
},
// 进阶:getters依赖其他getters
myGetCombined(): string {
return `${this.myGetName()},计数+1:${this.myGetCount}`;
}
},
// 3. actions:类似组件的methods,用于修改state,支持同步和异步
actions: {
// 同步action:修改state(不能用箭头函数,否则this指向异常)
setCurrentParam(num: number) {
this.current += num; // 直接通过this访问state并修改
},
// 同步action:批量修改多个状态
updateState(newCurrent: number, newName: string) {
this.current = newCurrent;
this.name = newName;
},
// 异步action:结合async/await(如接口请求)
async fetchData() {
// 模拟接口请求(实际开发中替换为真实接口)
const res = await new Promise((resolve) => {
setTimeout(() => {
resolve({ current: 10, name: '异步更新后' });
}, 1000);
});
// 异步请求成功后,修改state
const data = res as { current: number; name: string };
this.current = data.current;
this.name = data.name;
}
},
});
3.2 组件中使用Store(核心实战)
在Vue3组件(<script setup lang="ts">)中,引入Store实例,即可访问、修改状态,调用actions,以下是完整示例。
3.2.1 基础使用(访问state、getters)
<template>
<div class="pinia-demo">
<h3>基础使用</h3>
<!-- 直接访问state -->
<p>当前计数:{{ testStore.current }}</p>
<p>姓名:{{ testStore.name }}</p>
<!-- 访问getters(直接当作属性使用,无需调用) -->
<p>计数+1:{{ testStore.myGetCount }}</p>
<p>组合getters:{{ testStore.myGetCombined }}</p>
</div>
</template>
<script setup lang="ts">
// 1. 引入Store实例
import { useTestStore } from '@/store';
// 2. 创建Store实例(Pinia自动管理单例,多次调用返回同一个实例)
const testStore = useTestStore();
</script>
3.2.2 修改state(5种方式,实战常用)
Pinia提供多种修改state的方式,按需选择,推荐使用$patch(批量修改)和actions(业务逻辑封装)。
<template>
<div class="pinia-demo">
<h3>修改state</h3>
<p>当前计数:{{ testStore.current }}</p>
<button @click="handleDirectModify">1.直接修改</button>
<button @click="handlePatchObj">2.$patch对象批量修改</button>
<button @click="handlePatchFn">3.$patch函数自定义修改</button>
<button @click="handleReplaceState">4.$state替换整个状态</button>
<button @click="handleActionsModify">5.通过actions修改</button>
</div>
</template>
<script setup lang="ts">
import { useTestStore } from '@/store';
const testStore = useTestStore();
// 方式1:直接修改(简单场景可用,不推荐复杂场景)
const handleDirectModify = () => {
testStore.current++; // 直接修改单个状态
// testStore.name = '新姓名'; // 直接修改单个状态
};
// 方式2:$patch对象形式(批量修改多个状态,推荐简单批量场景)
const handlePatchObj = () => {
testStore.$patch({
current: 10,
name: '批量修改后',
list: [4, 5, 6]
});
};
// 方式3:$patch函数形式(自定义修改逻辑,推荐复杂场景)
const handlePatchFn = () => {
testStore.$patch((state) => {
state.current += 5; // 复杂计算修改
state.list.push(7); // 数组操作
if (state.current > 20) {
state.name = '计数超标';
}
});
};
// 方式4:$state替换整个状态(需修改所有属性,不推荐常规场景)
const handleReplaceState = () => {
testStore.$state = {
current: 0,
name: '替换整个状态',
list: []
};
};
// 方式5:通过actions修改(推荐,封装业务逻辑,便于维护和复用)
const handleActionsModify = () => {
testStore.setCurrentParam(3); // 调用同步action
// testStore.updateState(15, 'actions修改'); // 调用同步action
// testStore.fetchData(); // 调用异步action
};
</script>
3.2.3 响应式解构state(关键技巧)
直接解构state会丢失响应性(Pinia的state默认用reactive处理,与Vue3 reactive解构规则一致),需使用Pinia提供的storeToRefs方法,实现响应式解构。
<template>
<div class="pinia-demo">
<h3>响应式解构</h3>
<p>解构后计数:{{ current }}</p>
<p>解构后姓名:{{ name }}</p>
<button @click="handleChange">修改解构后的值</button>
</div>
</template>
<script setup lang="ts">
import { useTestStore } from '@/store';
import { storeToRefs } from 'pinia'; // 引入storeToRefs
const testStore = useTestStore();
// 错误写法:直接解构,失去响应性
// const { current, name } = testStore;
// 正确写法:用storeToRefs解构,保持响应性
const { current, name } = storeToRefs(testStore);
// 修改解构后的值(需用.value,因为storeToRefs会将状态转为ref)
const handleChange = () => {
current.value++;
name.value = '解构后修改';
};
</script>
3.2.4 调用异步actions(实战常用)
actions支持async/await,可直接在组件中调用异步action,处理接口请求等异步逻辑,示例如下:
<template>
<div class="pinia-demo">
<h3>异步actions</h3>
<p>当前计数:{{ testStore.current }}</p>
<p>姓名:{{ testStore.name }}</p>
<button @click="handleFetchData" :disabled="loading">
{{ loading ? '加载中...' : '异步请求更新' }}
</button>
</div>
</template>
<script setup lang="ts">
import { useTestStore } from '@/store';
import { ref } from 'vue';
const testStore = useTestStore();
const loading = ref(false);
// 调用异步action
const handleFetchData = async () => {
loading.value = true;
try {
await testStore.fetchData(); // 等待异步action执行完成
} catch (err) {
console.error('异步请求失败:', err);
} finally {
loading.value = false;
}
};
</script>
3.3 多Store使用(实战场景)
Pinia无模块嵌套,多个Store独立存在,可在组件中同时引入多个Store,也可在一个Store中引入另一个Store(实现Store间通信)。
3.3.1 组件中引入多个Store
// src/store/userStore.ts(新增用户Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';
export const useUserStore = defineStore(Names.User, {
state: () => ({
token: '',
userInfo: { name: '游客', age: 18 }
}),
actions: {
login(token: string, userInfo: any) {
this.token = token;
this.userInfo = userInfo;
},
logout() {
this.token = '';
this.userInfo = { name: '游客', age: 18 };
}
}
});
// 组件中使用多个Store
<script setup lang="ts">
import { useTestStore } from '@/store';
import { useUserStore } from '@/store/userStore';
const testStore = useTestStore();
const userStore = useUserStore();
// 调用不同Store的方法
const handleLogin = () => {
userStore.login('abc123', { name: '小明', age: 20 });
};
</script>
3.3.2 Store间通信(一个Store调用另一个Store)
在一个Store的actions中,引入另一个Store实例,即可实现Store间的数据交互:
// src/store/cartStore.ts(购物车Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';
import { useUserStore } from './userStore'; // 引入用户Store
export const useCartStore = defineStore(Names.Cart, {
state: () => ({
cartList: [] as { id: number; name: string; price: number }[]
}),
actions: {
// 添加商品到购物车(需判断用户是否登录)
addToCart(goods: { id: number; name: string; price: number }) {
const userStore = useUserStore(); // 实例化用户Store
if (!userStore.token) {
alert('请先登录');
return;
}
this.cartList.push(goods);
}
}
});
四、Vue3+Pinia实战案例(模拟电商场景)
结合前面的核心用法,实现一个简单的电商场景实战案例,包含「用户登录/退出」「购物车添加/删除」「全局状态共享」,整合多Store、异步actions、响应式解构等核心知识点,可直接复制到项目中使用。
4.1 实战准备(创建3个Store)
创建store-name.ts、userStore.ts(用户)、cartStore.ts(购物车)、goodsStore.ts(商品),代码如下:
// 1. store-name.ts(Store名称枚举)
export const enum Names {
User = 'USER',
Cart = 'CART',
Goods = 'GOODS'
}
// 2. userStore.ts(用户Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';
// 定义用户信息类型(TS类型约束)
interface UserInfo {
name: string;
age: number;
avatar: string;
}
export const useUserStore = defineStore(Names.User, {
state: () => ({
token: localStorage.getItem('token') || '', // 持久化存储token
userInfo: {} as UserInfo
}),
actions: {
// 登录(异步,模拟接口请求)
async login(account: string, password: string) {
// 模拟接口请求
const res = await new Promise((resolve) => {
setTimeout(() => {
resolve({
token: 'pinia_demo_token_123',
userInfo: { name: '小明', age: 22, avatar: 'https://picsum.photos/200/200' }
});
}, 1000);
});
const data = res as { token: string; userInfo: UserInfo };
this.token = data.token;
this.userInfo = data.userInfo;
// 本地持久化token(避免页面刷新丢失)
localStorage.setItem('token', data.token);
},
// 退出登录
logout() {
this.token = '';
this.userInfo = {} as UserInfo;
localStorage.removeItem('token');
}
}
});
// 3. goodsStore.ts(商品Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';
// 商品类型约束
interface Goods {
id: number;
name: string;
price: number;
img: string;
stock: number;
}
export const useGoodsStore = defineStore(Names.Goods, {
state: () => ({
goodsList: [] as Goods[] // 商品列表
}),
actions: {
// 异步获取商品列表(模拟接口)
async fetchGoodsList() {
const res = await new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, name: 'Vue3实战教程', price: 99, img: 'https://picsum.photos/200/200', stock: 100 },
{ id: 2, name: 'Pinia入门手册', price: 59, img: 'https://picsum.photos/200/200', stock: 50 },
{ id: 3, name: 'TS入门到精通', price: 79, img: 'https://picsum.photos/200/200', stock: 80 }
]);
}, 800);
});
this.goodsList = res as Goods[];
}
}
});
// 4. cartStore.ts(购物车Store)
import { defineStore } from 'pinia';
import { Names } from './store-name';
import { useUserStore } from './userStore';
import { Goods } from './goodsStore'; // 复用商品类型
export const useCartStore = defineStore(Names.Cart, {
state: () => ({
cartList: [] as { goods: Goods; count: number }[] // 购物车列表(商品+数量)
}),
getters: {
// 计算购物车总价格
cartTotalPrice(state) {
return state.cartList.reduce((total, item) => {
return total + item.goods.price * item.count;
}, 0);
},
// 计算购物车商品总数
cartTotalCount(state) {
return state.cartList.reduce((total, item) => total + item.count, 0);
}
},
actions: {
// 添加商品到购物车
addToCart(goods: Goods, count: number = 1) {
const userStore = useUserStore();
if (!userStore.token) {
alert('请先登录');
return;
}
// 判断商品是否已在购物车中
const existingItem = this.cartList.find(item => item.goods.id === goods.id);
if (existingItem) {
existingItem.count += count;
} else {
this.cartList.push({ goods, count });
}
},
// 从购物车删除商品
removeFromCart(goodsId: number) {
this.cartList = this.cartList.filter(item => item.goods.id !== goodsId);
},
// 修改购物车商品数量
updateCartCount(goodsId: number, count: number) {
const item = this.cartList.find(item => item.goods.id === goodsId);
if (item) {
item.count = count;
}
},
// 清空购物车
clearCart() {
this.cartList = [];
}
}
});
4.2 实战组件开发(3个核心组件)
4.2.1 登录组件(Login.vue)
<template>
<div class="login-container">
<h2>用户登录</h2>
<div class="form-item">
<label>账号:</label>
<input v-model="account" type="text" placeholder="请输入账号" />
</div>
<div class="form-item">
<label>密码:</label>
<input v-model="password" type="password" placeholder="请输入密码" />
</div>
<button @click="handleLogin" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useUserStore } from '@/store/userStore';
import { useRouter } from 'vue-router'; // 路由跳转(需配置路由)
const userStore = useUserStore();
const router = useRouter();
const account = ref('');
const password = ref('');
const loading = ref(false);
const handleLogin = async () => {
if (!account.value || !password.value) {
alert('请输入账号和密码');
return;
}
loading.value = true;
try {
await userStore.login(account.value, password.value);
alert('登录成功');
router.push('/home'); // 登录成功跳转首页
} catch (err) {
alert('登录失败,请重试');
} finally {
loading.value = false;
}
};
</script>
<style scoped>
.login-container {
width: 300px;
margin: 100px auto;
text-align: center;
}
.form-item {
margin: 15px 0;
text-align: left;
}
input {
width: 100%;
padding: 8px;
margin-top: 5px;
}
button {
width: 100%;
padding: 10px;
background: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
4.2.2 首页组件(Home.vue)
<template>
<div class="home-container">
<header class="home-header">
<h1>Pinia电商实战</h1>
<div class="user-info">
<img v-if="userInfo.avatar" :src="userInfo.avatar" alt="用户头像" class="avatar" />
<span v-if="userInfo.name">欢迎您,{{ userInfo.name }}</span>
<button @click="handleLogout" v-if="token">退出登录</button>
<button @click="toLogin" v-else>去登录</button>
<div class="cart-icon" @click="toCart">
购物车({{ cartTotalCount }})
</div>
</header>
<section class="goods-list">
<h2>商品列表</h2>
<div class="goods-item" v-for="goods in goodsList" :key="goods.id">
<img :src="goods.img" alt="商品图片" class="goods-img" />
<div class="goods-info">
<h3>{{ goods.name }}</h3>
<p class="price">¥{{ goods.price }}</p>
<p class="stock">库存:{{ goods.stock }}</p>
<button @click="addToCart(goods)">加入购物车</button>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { useUserStore } from '@/store/userStore';
import { useGoodsStore } from '@/store/goodsStore';
import { useCartStore } from '@/store/cartStore';
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router';
// 实例化Store
const userStore = useUserStore();
const goodsStore = useGoodsStore();
const cartStore = useCartStore();
const router = useRouter();
// 响应式解构状态(避免失去响应性)
const { token, userInfo } = storeToRefs(userStore);
const { goodsList } = storeToRefs(goodsStore);
const { cartTotalCount, addToCart } = cartStore;
// 组件挂载时,获取商品列表
onMounted(() => {
goodsStore.fetchGoodsList();
});
// 退出登录
const handleLogout = () => {
userStore.logout();
alert('退出成功');
router.push('/login');
};
// 跳转登录页
const toLogin = () => {
router.push('/login');
};
// 跳转购物车页
const toCart = () => {
if (!token.value) {
alert('请先登录');
router.push('/login');
return;
}
router.push('/cart');
};
</script>
<style scoped>
.home-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid #eee;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.user-info {
display: flex;
align-items: center;
}
.user-info button {
margin-left: 20px;
padding: 5px 10px;
cursor: pointer;
}
.cart-icon {
margin-left: 20px;
cursor: pointer;
font-weight: bold;
}
.goods-list {
padding: 20px;
}
.goods-item {
display: flex;
margin: 20px 0;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
}
.goods-img {
width: 100px;
height: 100px;
margin-right: 20px;
}
.goods-info {
flex: 1;
}
.price {
color: #ff4400;
font-size: 18px;
font-weight: bold;
}
.stock {
color: #666;
margin: 10px 0;
}
.goods-info button {
padding: 8px 15px;
background: #ff4400;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
4.2.3 购物车组件(Cart.vue)
<template>
<div class="cart-container">
<h2>我的购物车</h2>
<div class="cart-empty" v-if="cartList.length === 0">
购物车为空,快去添加商品吧!
</div>
<div class="cart-list" v-else>
<div class="cart-item" v-for="item in cartList" :key="item.goods.id">
<img :src="item.goods.img" alt="商品图片" class="cart-img" />
<div class="cart-info">
<h3>{{ item.goods.name }}</h3>
<p class="price">¥{{ item.goods.price }}</p>
<div class="count-control">
<button @click="updateCount(item.goods.id, item.count - 1)" :disabled="item.count <= 1">-</button>
<span>{{ item.count }}</span>
<button @click="updateCount(item.goods.id, item.count + 1)" :disabled="item.count >= item.goods.stock">+</button>
</div>
</div>
<button class="delete-btn" @click="removeFromCart(item.goods.id)">删除</button>
</div>
<div class="cart-footer">
<button class="clear-btn" @click="clearCart">清空购物车</button>
<div class="total-info">
合计:<span class="total-price">¥{{ cartTotalPrice.toFixed(2) }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useCartStore } from '@/store/cartStore';
import { useUserStore } from '@/store/userStore';
import { storeToRefs } from 'pinia';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
const cartStore = useCartStore();
const userStore = useUserStore();
const router = useRouter();
// 响应式解构
const { cartList, cartTotalPrice } = storeToRefs(cartStore);
const { token } = storeToRefs(userStore);
const { updateCartCount, removeFromCart, clearCart } = cartStore;
// 组件挂载时,判断是否登录
onMounted(() => {
if (!token.value) {
alert('请先登录');
router.push('/login');
}
});
// 修改商品数量
const updateCount = (goodsId: number, count: number) => {
updateCartCount(goodsId, count);
};
</script>
<style scoped>
.cart-container {
padding: 20px;
}
.cart-empty {
text-align: center;
padding: 50px;
color: #666;
font-size: 18px;
}
.cart-item {
display: flex;
align-items: center;
margin: 20px 0;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
}
.cart-img {
width: 80px;
height: 80px;
margin-right: 20px;
}
.cart-info {
flex: 1;
}
.price {
color: #ff4400;
font-weight: bold;
margin: 10px 0;
}
.count-control {
display: flex;
align-items: center;
}
.count-control button {
width: 30px;
height: 30px;
border: 1px solid #eee;
background: #fff;
cursor: pointer;
}
.count-control button:disabled {
background: #eee;
cursor: not-allowed;
}
.count-control span {
width: 60px;
text-align: center;
}
.delete-btn {
padding: 8px 15px;
background: #ff0000;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.cart-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 30px;
}
.clear-btn {
padding: 8px 15px;
background: #666;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.total-info {
font-size: 18px;
font-weight: bold;
}
.total-price {
color: #ff4400;
margin-left: 10px;
}
</style>
4.3 路由配置(router/index.ts)
配置路由,实现组件跳转,需先安装vue-router:npm install vue-router@4 -S,然后配置路由:
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import Login from '@/views/Login.vue';
import Home from '@/views/Home.vue';
import Cart from '@/views/Cart.vue';
const routes: RouteRecordRaw[] = [
{ path: '/', redirect: '/home' },
{ path: '/login', component: Login },
{ path: '/home', component: Home },
{ path: '/cart', component: Cart }
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
4.4 入口文件配置(main.ts)
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import router from './router' // 引入路由
const app = createApp(App)
app.use(createPinia()) // 挂载Pinia
app.use(router) // 挂载路由
app.mount('#app')
4.5 实战效果说明
-
未登录状态下,点击「加入购物车」「购物车图标」,会提示登录并跳转登录页;
-
登录成功后,跳转首页,显示用户信息,可查看商品列表、添加商品到购物车;
-
购物车页面可修改商品数量、删除商品、清空购物车,实时显示合计价格和商品总数;
-
退出登录后,清空用户状态和token,购物车状态保留(可结合持久化插件优化,见下文)。
五、Pinia进阶技巧(实战必备)
5.1 数据持久化(避免页面刷新丢失)
Pinia默认不持久化数据,页面刷新后state会重置,可使用pinia-plugin-persistedstate插件实现本地存储(localStorage/sessionStorage)。
// 安装插件
npm install pinia-plugin-persistedstate -S
// main.ts 配置插件
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入插件
import router from './router'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 挂载插件
const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')
在Store中配置持久化(以购物车Store为例):
export const useCartStore = defineStore(Names.Cart, {
state: () => ({ /* ... */ }),
getters: { /* ... */ },
actions: { /* ... */ },
// 配置持久化
persist: {
key: 'cartStore', // 存储的key(localStorage中的key)
storage: localStorage, // 存储方式(localStorage/sessionStorage)
paths: ['cartList'] // 需要持久化的state字段(默认全部持久化)
}
});
5.2 调试工具使用(Vue DevTools)
Pinia支持Vue DevTools,可实时查看Store状态、跟踪actions执行,便于调试:
- Vue3中,安装Vue DevTools扩展,打开开发者工具,切换到「Pinia」面板,即可查看所有Store的state、getters;
- 支持跟踪actions执行记录,可查看每次actions调用的参数和状态变化;
- Vue3中暂不支持time-travel功能(时间回溯),Vue2中支持(需配合Vuex接口)。
5.3 模块热更新(HMR)
Pinia支持模块热更新,修改Store代码后,无需重新加载页面,即可生效,且会保留现有状态,提升开发效率,无需额外配置,Vue3项目默认支持。
六、常见问题与解决方案(实战避坑)
- 问题1:组件中解构state后,修改值不生效? 解决方案:使用
storeToRefs解构,修改时需加.value(如current.value++),直接解构会丢失响应性。 - 问题2:actions中使用this指向异常? 解决方案:actions中的方法不能用箭头函数,需用普通函数,否则this无法指向Store实例。
- 问题3:页面刷新后,Pinia状态丢失? 解决方案:使用
pinia-plugin-persistedstate插件,配置持久化存储。 - 问题4:多个Store之间无法通信? 解决方案:在需要通信的Store中,引入目标Store实例,即可访问其state和actions。
- 问题5:TS类型推断失败,提示“this类型为any”? 解决方案:getters中使用this时,需指定返回值类型;actions中修改state时,确保state字段类型与赋值类型一致。
- 问题6:Vue2中使用Pinia报错? 解决方案:Vue2中需额外引入
PiniaVuePlugin,注册方式参考上传文档中的Vue2配置。
七、总结
Pinia是Vue3官方推荐的状态管理工具,相比Vuex,它更简洁、轻量、易上手,原生支持TS,完美适配组合式API,是Vue3项目的首选状态管理方案。
本文从基础认知、环境搭建、核心用法,到完整实战案例,覆盖了Pinia开发的全流程,重点讲解了Vue3+TS下的实战技巧,新手可按步骤搭建环境、编写代码,快速上手;老手可通过实战案例查漏补缺,优化项目中的状态管理逻辑。
核心要点:Store是Pinia的核心,每个Store包含state、getters、actions;修改state推荐使用$patch和actions;解构state需用storeToRefs保持响应性;结合插件可实现数据持久化,提升用户体验。