Vue3中Pinia状态管理实战详解
目录
- 前言
- Pinia 简介
- 3.1 Vue3 项目初始化
- 3.2 安装 Pinia
- 4.1 定义 Store 文件结构
- 4.2
defineStore
详解 - 4.3 ASCII 图解:响应式状态流动
- 5.1 根应用挂载 Pinia
- 5.2 组件内调用 Store
- 5.3 响应式更新示例
- 6.1 Getters(计算属性)的使用场景
- 6.2 Actions(方法)的使用场景
- 6.3 异步 Action 与 API 请求
- 7.1 多个 Store 的组织方式
- 7.2 互相调用与组合 Store
- 8.1 Pinia 插件机制简介
- 8.2 使用
pinia-plugin-persistedstate
实现持久化 - 8.3 自定义简单持久化方案示例
- 9.1 安装与启用
- 9.2 调试示意图
- 10.1 项目目录与功能描述
- 10.2 编写
useTodoStore
- 10.3 组件实现:添加、删除、标记完成
- 10.4 整体数据流动图解
- 11.1 组合式 Store:
useCounter
调用useTodo
- 11.2 自定义插件示例:日志打印插件
- 11.1 组合式 Store:
- 总结
前言
在 Vue3 中,Pinia 已经正式取代了 Vuex,成为官方推荐的状态管理工具。Pinia 以“轻量、直观、类型安全”为目标,通过 Composition API 的方式定义和使用 Store,不仅上手更快,还能借助 TypeScript 获得良好体验。本文将从安装与配置入手,结合代码示例与图解,深入讲解 Pinia 各项核心功能,帮助你在实际项目中快速掌握状态管理全流程。
Pinia 简介
- 什么是 Pinia:Pinia 是 Vue3 的状态管理库,类似于 Vuex,但接口更简洁,使用 Composition API 定义 Store,无需繁重的模块结构。
核心特点:
- 基于 Composition API:使用
defineStore
定义,返回函数式 API,易于逻辑复用; - 响应式状态:Store 内部状态用
ref
/reactive
管理,组件通过直接引用或解构获取响应式值; - 轻量快速:打包后体积小,无复杂插件系统;
- 类型安全:与 TypeScript 一起使用时,可自动推导 state、getters、actions 的类型;
- 插件机制:支持持久化、订阅、日志等插件扩展。
- 基于 Composition API:使用
环境准备与安装
3.1 Vue3 项目初始化
可依据个人偏好选用 Vite 或 Vue CLI,此处以 Vite 为例:
# 初始化 Vue3 + Vite 项目
npm create vite@latest vue3-pinia-demo -- --template vue
cd vue3-pinia-demo
npm install
此时项目目录类似:
vue3-pinia-demo/
├─ index.html
├─ package.json
├─ src/
│ ├─ main.js
│ ├─ App.vue
│ └─ assets/
└─ vite.config.js
3.2 安装 Pinia
在项目根目录运行:
npm install pinia
安装完成后,即可在 Vue 应用中引入并使用。
创建第一个 Store
4.1 定义 Store 文件结构
建议在 src
下新建 stores
或 store
目录,用于集中存放所有 Store 文件。例如:
src/
├─ stores/
│ └─ counterStore.js
├─ main.js
├─ App.vue
...
4.2 defineStore
详解
在 src/stores/counterStore.js
编写第一个简单计数 Store:
// src/stores/counterStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// 1. state:使用 ref 定义响应式变量
const count = ref(0);
// 2. getters:定义计算属性
const doubleCount = computed(() => count.value * 2);
// 3. actions:定义方法,可同步或异步
function increment() {
count.value++;
}
function incrementBy(amount) {
count.value += amount;
}
return {
count,
doubleCount,
increment,
incrementBy
};
});
defineStore('counter', () => { ... })
:第一个参数为 Store 唯一 id(counter
),第二个参数是一个“setup 函数”,返回需要暴露的状态、计算属性和方法。- 状态
count
:使用ref
定义,组件读取时可直接响应。 - 计算属性
doubleCount
:使用computed
,自动根据count
更新。 - 方法
increment
、incrementBy
:对状态进行更改。
4.3 ASCII 图解:响应式状态流动
┌───────────────────────────┐
│ useCounterStore() │
│ ┌────────┐ ┌───────────┐ │
│ │ count │→ │ increment │ │
│ │ ref │ └───────────┘ │
│ └────────┘ ┌──────────┐ │
│ │ double │ │
│ │ Count │ │
│ └──────────┘ │
└───────────────────────────┘
组件 ←─── 读取 count / doubleCount ───→ 自动更新
组件 ── 调用 increment() ──▶ count.value++
- 组件挂载时,调用
useCounterStore()
拿到同一个 Store 实例,读取count
或doubleCount
时会自动收集依赖; - 当调用
increment()
修改count.value
,Vue 的响应式系统会通知所有依赖该值的组件重新渲染。
在组件中使用 Pinia
5.1 根应用挂载 Pinia
在 src/main.js
(或 main.ts
)中引入并挂载 Pinia:
// src/main.js
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');
这一步让整个应用具备了 Pinia 的能力,后续组件调用 useCounterStore
时,都能拿到相同的 Store 实例。
5.2 组件内调用 Store
在任意组件里,使用如下方式获取并操作 Store:
<!-- src/components/CounterDisplay.vue -->
<template>
<div>
<p>当前计数:{{ count }}</p>
<p>双倍计数:{{ doubleCount }}</p>
<button @click="increment">+1</button>
<button @click="incrementBy(5)">+5</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counterStore';
// 1. 取得 Store 实例
const counterStore = useCounterStore();
// 2. 从 Store 解构需要的部分
const { count, doubleCount, increment, incrementBy } = counterStore;
</script>
<style scoped>
button {
margin-right: 8px;
}
</style>
- 组件渲染时,
count
、doubleCount
自动读取 Store 中的响应式值; - 点击按钮时,调用
increment()
或incrementBy(5)
修改状态,UI 自动更新。
5.3 响应式更新示例
当另一个组件也引用同一 Store:
<!-- src/components/CounterLogger.vue -->
<template>
<div>最新 count:{{ count }}</div>
</template>
<script setup>
import { watch } from 'vue';
import { useCounterStore } from '@/stores/counterStore';
const counterStore = useCounterStore();
const { count } = counterStore;
// 监听 count 变化,输出日志
watch(count, (newVal) => {
console.log('count 变为:', newVal);
});
</script>
- 无论是在
CounterDisplay
还是其他组件里调用increment()
,CounterLogger
中的count
都会随着变化而自动触发watch
。
Getters 与 Actions 深度解析
6.1 Getters(计算属性)的使用场景
- 用途:将复杂的计算逻辑从组件中抽离,放在 Store 中统一管理,并保持惟一数据源。
- 示例:假设我们有一个待办列表,需要根据状态计算未完成数量:
// src/stores/todoStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useTodoStore = defineStore('todo', () => {
const todos = ref([
{ id: 1, text: '学习 Pinia', done: false },
{ id: 2, text: '写单元测试', done: true }
]);
// 计算属性:未完成条目
const incompleteCount = computed(() =>
todos.value.filter((t) => !t.done).length
);
return { todos, incompleteCount };
});
- 组件中直接读取
incompleteCount
即可,且当todos
更新时会自动重新计算。
6.2 Actions(方法)的使用场景
- 用途:封装修改 state 或执行异步逻辑的函数。
- 同步 Action 示例:添加/删除待办项
// src/stores/todoStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useTodoStore = defineStore('todo', () => {
const todos = ref([]);
function addTodo(text) {
todos.value.push({ id: Date.now(), text, done: false });
}
function removeTodo(id) {
todos.value = todos.value.filter((t) => t.id !== id);
}
function toggleTodo(id) {
const item = todos.value.find((t) => t.id === id);
if (item) item.done = !item.done;
}
const incompleteCount = computed(() =>
todos.value.filter((t) => !t.done).length
);
return { todos, incompleteCount, addTodo, removeTodo, toggleTodo };
});
- 异步 Action 示例:从服务器拉取初始列表
// src/stores/todoStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import axios from 'axios';
export const useTodoStore = defineStore('todo', () => {
const todos = ref([]);
const loading = ref(false);
const error = ref('');
async function fetchTodos() {
loading.value = true;
error.value = '';
try {
const res = await axios.get('/api/todos');
todos.value = res.data;
} catch (e) {
error.value = '加载失败';
} finally {
loading.value = false;
}
}
const incompleteCount = computed(() =>
todos.value.filter((t) => !t.done).length
);
return { todos, incompleteCount, loading, error, fetchTodos };
});
- 组件中调用
await todoStore.fetchTodos()
即可触发异步加载,并通过loading
/error
跟踪状态。
6.3 异步 Action 与 API 请求
组件中使用示例:
<!-- src/components/TodoList.vue -->
<template>
<div>
<button @click="load">加载待办</button>
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="item in todos" :key="item.id">
<span :class="{ done: item.done }">{{ item.text }}</span>
<button @click="toggle(item.id)">切换</button>
<button @click="remove(item.id)">删除</button>
</li>
</ul>
<p>未完成:{{ incompleteCount }}</p>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import { useTodoStore } from '@/stores/todoStore';
const todoStore = useTodoStore();
const { todos, loading, error, incompleteCount, fetchTodos, toggleTodo, removeTodo } = todoStore;
function load() {
fetchTodos();
}
function toggle(id) {
toggleTodo(id);
}
function remove(id) {
removeTodo(id);
}
// 组件挂载时自动加载
onMounted(() => {
fetchTodos();
});
</script>
<style scoped>
.done {
text-decoration: line-through;
}
</style>
- 组件以
onMounted
调用异步 ActionfetchTodos()
,并通过解构获取loading
、error
、todos
、incompleteCount
。 - 按钮点击调用同步 Action
toggleTodo(id)
、removeTodo(id)
。
模块化与多 Store 管理
7.1 多个 Store 的组织方式
对于大型项目,需要将状态拆分成多个子模块,各司其职。例如:
src/
├─ stores/
│ ├─ todoStore.js
│ ├─ userStore.js
│ └─ cartStore.js
userStore.js
管理用户信息:登录、登出、权限等cartStore.js
管理购物车:添加/删除商品、计算总价
示例:userStore.js
// src/stores/userStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
const userInfo = ref({ name: '', token: '' });
const isLoggedIn = computed(() => !!userInfo.value.token);
function login(credentials) {
// 模拟登录
userInfo.value = { name: credentials.username, token: 'abc123' };
}
function logout() {
userInfo.value = { name: '', token: '' };
}
return { userInfo, isLoggedIn, login, logout };
});
7.2 互相调用与组合 Store
有时一个 Store 需要调用另一个 Store 的方法或读取状态,可以直接在内部通过 useXXXStore()
获取相应实例。例如在 cartStore.js
中,获取 userStore
中的登录信息来确定能否结账:
// src/stores/cartStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useUserStore } from './userStore';
export const useCartStore = defineStore('cart', () => {
const items = ref([]);
const userStore = useUserStore();
function addToCart(product) {
items.value.push(product);
}
// 只有登录用户才能结账
function checkout() {
if (!userStore.isLoggedIn) {
throw new Error('请先登录');
}
// 结账逻辑...
items.value = [];
}
const totalPrice = computed(() => items.value.reduce((sum, p) => sum + p.price, 0));
return { items, totalPrice, addToCart, checkout };
});
- 注意:在任意 Store 内以 function 调用
useUserStore()
,Pinia 会确保返回相同实例。
插件与持久化策略
8.1 Pinia 插件机制简介
Pinia 支持插件,可以在创建 Store 时注入额外功能,例如:日志记录、状态持久化、订阅等。插件形式为一个接收上下文的函数,示例:
// src/plugins/logger.js
export function logger({ store }) {
// 在每次 action 执行前后输出日志
store.$onAction(({ name, args, after, onError }) => {
console.log(`⏩ Action ${name} 开始,参数:`, args);
after((result) => {
console.log(`✅ Action ${name} 结束,返回:`, result);
});
onError((error) => {
console.error(`❌ Action ${name} 报错:`, error);
});
});
}
在主入口注册插件:
// src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import { logger } from './plugins/logger';
const app = createApp(App);
const pinia = createPinia();
// 使用 logger 插件
pinia.use(logger);
app.use(pinia);
app.mount('#app');
- 这样所有 Store 在调用 Action 时,都会执行插件中的日志逻辑。
8.2 使用 pinia-plugin-persistedstate
实现持久化
依赖:pinia-plugin-persistedstate
npm install pinia-plugin-persistedstate
在入口文件中配置:
// src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import piniaPersist from 'pinia-plugin-persistedstate';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
// 注册持久化插件
pinia.use(piniaPersist);
app.use(pinia);
app.mount('#app');
在需要持久化的 Store 中添加 persist: true
配置:
// src/stores/userStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: { name: '', token: '' }
}),
getters: {
isLoggedIn: (state) => !!state.userInfo.token
},
actions: {
login(credentials) {
this.userInfo = { name: credentials.username, token: 'abc123' };
},
logout() {
this.userInfo = { name: '', token: '' };
}
},
persist: {
enabled: true,
storage: localStorage, // 默认就是 localStorage
paths: ['userInfo'] // 只持久化 userInfo 字段
}
});
- 之后刷新页面
userInfo
会从localStorage
中恢复,无需再次登录。
8.3 自定义简单持久化方案示例
如果不想引入插件,也可以在 Store 内手动读写 LocalStorage:
// src/stores/cartStore.js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useCartStore = defineStore('cart', () => {
const items = ref(JSON.parse(localStorage.getItem('cartItems') || '[]'));
function addToCart(product) {
items.value.push(product);
localStorage.setItem('cartItems', JSON.stringify(items.value));
}
function clearCart() {
items.value = [];
localStorage.removeItem('cartItems');
}
return { items, addToCart, clearCart };
});
- 在每次更新
items
时,将新值写入 LocalStorage;组件挂载时从 LocalStorage 初始化状态。
Pinia Devtools 调试
9.1 安装与启用
- Chrome/Firefox 扩展:在浏览器扩展商店搜索 “Pinia Devtools” 并安装;
- 在代码中启用(Vue3 + Vite 默认自动启用 Devtools,不需额外配置);
启动应用后打开浏览器开发者工具,你会看到一个 “Pinia” 选项卡,列出所有 Store、state、getter、action 调用记录。
9.2 调试示意图
┌────────────────────────────────────────────────┐
│ Pinia Devtools │
│ ┌───────────┐ ┌─────────────┐ ┌───────────┐ │
│ │ Stores │ → │ State Tree │→│ Actions │ │
│ └───────────┘ └─────────────┘ └───────────┘ │
│ ↓ ↓ ↓ │
│ 点击查看 查看当前 state 查看执行 │
│ 及 getters 更新 过的 actions │
└────────────────────────────────────────────────┘
- Stores 面板:列出所有已注册的 Store 及其 id;
- State Tree 面板:查看某个 Store 的当前 state 和 getters;
- Actions 面板:记录每次调用 Action 的时间、传入参数与返回结果,方便回溯和调试;
实战示例:Todo List 应用
下面用一个 Todo List 应用将上述知识串联起来,完整演示 Pinia 在实际业务中的用法。
10.1 项目目录与功能描述
src/
├─ components/
│ ├─ TodoApp.vue
│ ├─ TodoInput.vue
│ └─ TodoList.vue
├─ stores/
│ └─ todoStore.js
└─ main.js
功能:
- 输入框添加待办
- 列表展示待办,可切换完成状态、删除
- 顶部显示未完成条目数
- 保存到 LocalStorage 持久化
10.2 编写 useTodoStore
// src/stores/todoStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useTodoStore = defineStore('todo', () => {
// 1. 初始化 state,从本地存储恢复
const todos = ref(
JSON.parse(localStorage.getItem('todos') || '[]')
);
// 2. getters
const incompleteCount = computed(() =>
todos.value.filter((t) => !t.done).length
);
// 3. actions
function addTodo(text) {
todos.value.push({ id: Date.now(), text, done: false });
persist();
}
function removeTodo(id) {
todos.value = todos.value.filter((t) => t.id !== id);
persist();
}
function toggleTodo(id) {
const item = todos.value.find((t) => t.id === id);
if (item) item.done = !item.done;
persist();
}
function persist() {
localStorage.setItem('todos', JSON.stringify(todos.value));
}
return { todos, incompleteCount, addTodo, removeTodo, toggleTodo };
});
- 每次增删改都调用
persist()
将最新todos
写入 LocalStorage,保证刷新不丢失。
10.3 组件实现:添加、删除、标记完成
10.3.1 TodoInput.vue
<template>
<div class="todo-input">
<input
v-model="text"
@keydown.enter.prevent="submit"
placeholder="输入待办后按回车"
/>
<button @click="submit">添加</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useTodoStore } from '@/stores/todoStore';
const text = ref('');
const todoStore = useTodoStore();
function submit() {
if (!text.value.trim()) return;
todoStore.addTodo(text.value.trim());
text.value = '';
}
</script>
<style scoped>
.todo-input {
display: flex;
margin-bottom: 16px;
}
.todo-input input {
flex: 1;
padding: 6px;
}
.todo-input button {
margin-left: 8px;
padding: 6px 12px;
}
</style>
useTodoStore()
:拿到同一个 Store 实例,调用addTodo
将新待办加入。
10.3.2 TodoList.vue
<template>
<ul class="todo-list">
<li v-for="item in todos" :key="item.id" class="todo-item">
<input
type="checkbox"
:checked="item.done"
@change="toggle(item.id)"
/>
<span :class="{ done: item.done }">{{ item.text }}</span>
<button @click="remove(item.id)">删除</button>
</li>
</ul>
</template>
<script setup>
import { useTodoStore } from '@/stores/todoStore';
const todoStore = useTodoStore();
const { todos, toggleTodo, removeTodo } = todoStore;
// 包装一层方法,方便模板调用
function toggle(id) {
toggleTodo(id);
}
function remove(id) {
removeTodo(id);
}
</script>
<style scoped>
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.done {
text-decoration: line-through;
}
button {
margin-left: auto;
padding: 2px 8px;
}
</style>
- 直接引用
todoStore.todos
渲染列表,toggleTodo
与removeTodo
修改状态并持久化。
10.3.3 TodoApp.vue
<template>
<div class="todo-app">
<h2>Vue3 + Pinia Todo 应用</h2>
<TodoInput />
<TodoList />
<p>未完成:{{ incompleteCount }}</p>
</div>
</template>
<script setup>
import TodoInput from '@/components/TodoInput.vue';
import TodoList from '@/components/TodoList.vue';
import { useTodoStore } from '@/stores/todoStore';
const todoStore = useTodoStore();
const { incompleteCount } = todoStore;
</script>
<style scoped>
.todo-app {
max-width: 400px;
margin: 20px auto;
padding: 16px;
border: 1px solid #ccc;
}
</style>
- 组件只需引入子组件,并从 Store 中读取
incompleteCount
,实现整体展示。
10.4 整体数据流动图解
┌─────────────────────────────────────────────────────────┐
│ TodoApp │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ TodoInput│ │ TodoList │ │ incomplete │ │
│ └──────────┘ └──────────┘ └────────────┘ │
│ ↓ ↓ ↑ │
│ user 输入 → addTodo() → toggle/removeTodo() │ │
│ ↓ ↓ │ │
│ todoStore.todos ←─────────┘ │ │
│ ↓ │ │
│ localStorage ← persist() │ │
└─────────────────────────────────────────────────────────┘
- 用户在
TodoInput
里调用addTodo(text)
,Store 更新todos
,子组件TodoList
自动响应渲染新条目。 - 点击复选框或删除按钮调用
toggleTodo(id)
或removeTodo(id)
, Store 更新并同步到 LocalStorage。 incompleteCount
getter 根据todos
实时计算并展示。
高级用法:组合 Store 与插件扩展
11.1 组合式 Store:useCounter
调用 useTodo
有时想在一个 Store 内重用另一个 Store 的逻辑,可在 setup 中直接调用。示例:实现一个同时维护“计数”与“待办”的综合 Store:
// src/stores/appStore.js
import { defineStore } from 'pinia';
import { useCounterStore } from './counterStore';
import { useTodoStore } from './todoStore';
import { computed } from 'vue';
export const useAppStore = defineStore('app', () => {
const counterStore = useCounterStore();
const todoStore = useTodoStore();
// 复用两个 Store 的状态与方法
const totalItems = computed(() => todoStore.todos.length);
function incrementAndAddTodo(text) {
counterStore.increment();
todoStore.addTodo(text);
}
return {
count: counterStore.count,
increment: counterStore.increment,
todos: todoStore.todos,
addTodo: todoStore.addTodo,
totalItems,
incrementAndAddTodo
};
});
useAppStore
自动依赖counterStore
和todoStore
的状态与方法,方便在组件中一次性引入。
11.2 自定义插件示例:日志打印插件
前面在 8.1 中演示了一个简单的 logger
插件,下面给出更完整示例:
// src/plugins/logger.js
export function logger({ options, store }) {
// store.$id 为当前 Store 的 id
console.log(`🔰 插件初始化:Store ID = ${store.$id}`, options);
// 监听 state 更改
store.$subscribe((mutation, state) => {
console.log(`📦 Store(${store.$id}) Mutation: `, mutation);
console.log(`📦 New state: `, state);
});
// 监听 action 调用
store.$onAction(({ name, args, after, onError }) => {
console.log(`▶ Action(${store.$id}/${name}) 调用开始,参数:`, args);
after((result) => {
console.log(`✔ Action(${store.$id}/${name}) 调用结束,结果:`, result);
});
onError((error) => {
console.error(`✖ Action(${store.$id}/${name}) 调用出错:`, error);
});
});
}
在 main.js
中注册:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import { logger } from './plugins/logger';
const app = createApp(App);
const pinia = createPinia();
pinia.use(logger);
app.use(pinia);
app.mount('#app');
- 每当某个 Store 的 state 变更,或调用 Action,都在控制台打印日志,方便调试。
总结
本文从Pinia 简介、安装与配置、创建第一个 Store、组件内使用、Getters 与 Actions、模块化管理、插件与持久化、Devtools 调试,到实战 Todo List 应用、组合 Store、自定义插件等方面,对 Vue3 中 Pinia 的状态管理进行了全方位、实战详解。
- Pinia 上手极其简单:基于 Composition API,直接用
defineStore
定义即可; - 响应式与类型安全:无论是 JavaScript 还是 TypeScript 项目,都能享受自动推导和类型提示;
- 多 Store 划分与组合:可灵活拆分业务逻辑,又可在需要时将多个 Store 组合引用;
- 插件与持久化:Pinia 内置插件机制,让持久化、本地存储、日志、订阅等功能扩展便捷;
- Devtools 支持:通过浏览器插件即可可视化查看所有 Store、state、getters 和 action 日志。
掌握本文内容,相信你能轻松在 Vue3 项目中使用 Pinia 管理全局或跨组件状态,构建更清晰、更易维护的前端应用。
评论已关闭