Vue3+Pinia实战完整版|从入门到精通,替代Vuex的状态管理首选

15 阅读6分钟

本文专为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的替代方案,两者核心区别如下:

对比维度VuexPinia
核心结构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.tscartStore.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.tsuserStore.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 实战效果说明

  1. 未登录状态下,点击「加入购物车」「购物车图标」,会提示登录并跳转登录页;

  2. 登录成功后,跳转首页,显示用户信息,可查看商品列表、添加商品到购物车;

  3. 购物车页面可修改商品数量、删除商品、清空购物车,实时显示合计价格和商品总数;

  4. 退出登录后,清空用户状态和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保持响应性;结合插件可实现数据持久化,提升用户体验。