Pinia 知识笔记
1. Pinia 简介
Pinia 是 Vue 的专属状态管理库,是 Vuex 的替代品,由 Vue 核心团队推荐在 Vue 3 项目中使用。Pinia 最初于 2019 年 11 月左右开始设计,目的是创建一个支持组合式 API 的 Vue 状态管理库。
Pinia 的主要特点:
- 同时支持 Vue 2 和 Vue 3
- 支持 TypeScript
- 支持 Vue DevTools
- 支持服务端渲染 (SSR)
- 支持热更新 (HMR)
- 简化的 API,去掉了 Vuex 中 mutations 和 actions 的区分
- 更好的组合式 API 支持
为什么选择 Pinia 而非 Vuex?
- 更简单的 API,更少的样板代码
- 更好的 TypeScript 支持
- 不需要嵌套模块,扁平化的 Store 结构
- 支持多个 Store,不需要命名空间
2. 安装和配置
安装 Pinia
npm install pinia
# 或
yarn add pinia
在 Vue 3 项目中配置 Pinia
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
3. Pinia 核心概念
3.1 Store
Store 是一个保存状态和业务逻辑的实体,它不与组件树绑定,可以在整个应用中访问。Store 有三个核心概念:
- state: 存储数据的地方(相当于组件中的
ref或reactive) - getters: 计算属性(相当于组件中的 computed)
- actions: 可执行的方法(相当于组件中的函数)
3.2 使用组合式 API 定义一个用户状态的Store
import { getCurrentUser } from "@/api/user";
import { defineStore } from "pinia";
import { ref } from "vue";
export const useLoginUserStore = defineStore("loginUser", () => {
const loginUser = ref({
id: "",
userRole: 0,
userName: "未登录",
userAccount: "",
avatarUrl: "",
});
const initialized = ref(false);
//远程获取登录用户信息
async function fetchLoginUser() {
try {
const res = await getCurrentUser();
if (res.data.code === 0) {
loginUser.value = res.data.data;
}
initialized.value = true;
} catch (error) {
initialized.value = true;
throw error;
}
}
//单独设置登录用户信息
function setLoginUser(newLoginUser: any) {
loginUser.value = newLoginUser;
}
return { loginUser, fetchLoginUser, setLoginUser, initialized };
});
3.2.1 代码要点解释
Store定义
export const useLoginUserStore = defineStore("loginUser", () => { // Store 内容 });defineStore是 Pinia 的核心函数,用于创建一个 Store第一个参数
loginUser是 Store 的唯一 ID第二个参数是一个函数,使用组合式 API 风格定义 Store
State定义
const loginUser = ref({ id: "", userRole: 0, userName: "未登录", userAccount: "", avatarUrl: "", }); const initialized = ref(false);- 使用 Vue 的
ref()函数定义响应式状态 - 每个 ref 变量都会成为 Store 的一个状态属性
- 初始值设置为默认值
- initialized 用于跟踪用户信息是否已初始化
- 使用 Vue 的
Actions定义
//远程获取登录用户信息 async function fetchLoginUser() { try { const res = await getCurrentUser(); if (res.data.code === 0) { loginUser.value = res.data.data; } initialized.value = true; } catch (error) { initialized.value = true; throw error; } } //单独设置登录用户信息 function setLoginUser(newLoginUser: any) { loginUser.value = newLoginUser; }- 定义了两个actions:
fetchLoginUser和setLoginUser fetchLoginUser是一个异步函数,用于从 API 获取当前登录用户信息setLoginUser是一个同步函数,用于手动设置用户信息- 两个函数都直接修改 state(
loginUser.value),这是Pinia 的特点,不需要像 Vuex 那样通过 mutations
- 定义了两个actions:
返回
return { loginUser, fetchLoginUser, setLoginUser, initialized };- 返回所有需要暴露的状态和方法
- 这些属性和方法将可以在组件中访问
3.3 Store ID
每个 Store 都需要一个唯一的 ID,这个 ID 用于区分不同的 Store,也用于持久化等功能。
4. 在组件中使用 Store
<script setup>
import { useLoginUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
import { onMounted } from 'vue';
// 获取 store 实例
const loginUserStore = useLoginUserStore();
// 解构 store 的状态 (按照业务需求是否需要解构)
const { loginUser, initialized } = storeToRefs(loginUserStore);
// 直接解构 actions (不需要 storeToRefs)
const { fetchLoginUser, setLoginUser } = loginUserStore;
// 组件挂载时获取用户信息
onMounted(async () => {
if (!initialized.value) {
try {
await fetchLoginUser();
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
});
</script>
<template>
<div>
<p v-if="!initialized">加载中...</p>
<div v-else>
<p>用户名: {{ loginUser.userName }}</p>
<p>账号: {{ loginUser.userAccount }}</p>
<img v-if="loginUser.avatarUrl" :src="loginUser.avatarUrl" alt="头像" />
</div>
</div>
</template>
4.1 修改用户信息
<script setup>
import { useLoginUserStore } from '@/stores/user';
const loginUserStore = useLoginUserStore();
function updateUsername(newName) {
// 方法 1: 使用 setLoginUser 更新整个用户对象
const updatedUser = { ...loginUserStore.loginUser, userName: newName };
loginUserStore.setLoginUser(updatedUser);
// 方法 2: 直接修改 state
// loginUserStore.loginUser.userName = newName;
// 方法 3: 使用 $patch 方法
// loginUserStore.$patch({
// loginUser: { ...loginUserStore.loginUser, userName: newName }
// });
}
</script>
<template>
<div>
<input
v-model="loginUserStore.loginUser.userName"
placeholder="修改用户名"
/>
<button @click="updateUsername('新用户名')">更新用户名</button>
</div>
</template>
5. Getters
5.1 定义 Getters
import { getCurrentUser } from "@/api/user";
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useLoginUserStore = defineStore("loginUser", () => {
const loginUser = ref({
id: "",
userRole: 0,
userName: "未登录",
userAccount: "",
avatarUrl: "",
});
const initialized = ref(false);
// Getters
const isLoggedIn = computed(() => !!loginUser.value.id);
const isAdmin = computed(() => loginUser.value.userRole === 1);
const displayName = computed(() =>
loginUser.value.userName || loginUser.value.userAccount || "未登录用户"
);
//远程获取登录用户信息
async function fetchLoginUser() {
try {
const res = await getCurrentUser();
if (res.data.code === 0) {
loginUser.value = res.data.data;
}
initialized.value = true;
} catch (error) {
initialized.value = true;
throw error;
}
}
//单独设置登录用户信息
function setLoginUser(newLoginUser: any) {
loginUser.value = newLoginUser;
}
// 登出
function logout() {
loginUser.value = {
id: "",
userRole: 0,
userName: "未登录",
userAccount: "",
avatarUrl: "",
};
}
return {
loginUser,
initialized,
isLoggedIn,
isAdmin,
displayName,
fetchLoginUser,
setLoginUser,
logout
};
});
5.2 持久化
import { getCurrentUser } from "@/api/user";
import { defineStore } from "pinia";
import { ref, computed, watch } from "vue";
export const useLoginUserStore = defineStore("loginUser", () => {
// 从本地存储加载初始状态
const savedUser = localStorage.getItem('loginUser');
const loginUser = ref(savedUser ? JSON.parse(savedUser) : {
id: "",
userRole: 0,
userName: "未登录",
userAccount: "",
avatarUrl: "",
});
const initialized = ref(!!savedUser);
// 监听用户信息变化并保存到本地存储
watch(
loginUser,
(newValue) => {
localStorage.setItem('loginUser', JSON.stringify(newValue));
},
{ deep: true }
);
// Getters
const isLoggedIn = computed(() => !!loginUser.value.id);
// 其他代码保持不变...
return {
// 返回值保持不变...
};
});
6. 插件系统
Pinia 支持插件系统,可以扩展 Store 的功能。
6.1 创建插件
// plugins/persistPlugin.js
export function persistPlugin({ store }) {
// 从 localStorage 恢复状态
const storedState = localStorage.getItem(`pinia-${store.$id}`)
if (storedState) {
store.$patch(JSON.parse(storedState))
}
// 监听状态变化并保存到 localStorage
store.$subscribe((mutation, state) => {
localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
})
}
6.2 使用插件
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { persistPlugin } from './plugins/persistPlugin'
const pinia = createPinia()
pinia.use(persistPlugin)
const app = createApp(App)
app.use(pinia)
app.mount('#app')
7. 订阅 Store 变化(进阶技巧)
7.1 订阅 State 变化
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// 订阅状态变化
const unsubscribe = counterStore.$subscribe((mutation, state) => {
// mutation 包含:
// - type: 'direct' | 'patch object' | 'patch function'
// - storeId: store 的 id
// - payload: 传递给 patch 的参数
console.log('State changed:', mutation, state)
// 可以在这里执行任何操作,如持久化到本地存储
localStorage.setItem('counter', JSON.stringify(state))
})
// 停止订阅
// unsubscribe()
7.2 订阅 Actions
const unsubscribe = counterStore.$onAction({
before: (actionName, store, args) => {
// action 执行前调用
console.log(`${actionName} 即将被调用,参数:`, args)
},
after: (actionName, store, args, result) => {
// action 成功执行后调用
console.log(`${actionName} 被调用,结果:`, result)
},
error: (actionName, store, args, error) => {
// action 抛出错误时调用
console.error(`${actionName} 出错:`, error)
},
})
// 停止订阅
// unsubscribe()
8. 实际应用示例
8.1 Store 目录结构
src/
stores/
index.ts # 导出所有 store
user.ts # 用户相关 store
settings.ts # 应用设置 store
notification.ts # 通知 store
8.2 在 index.ts 中统一导出
// src/stores/index.ts
import { useLoginUserStore } from './user';
import { useSettingsStore } from './settings';
import { useNotificationStore } from './notification';
export {
useLoginUserStore,
useSettingsStore,
useNotificationStore
};
8.3 Store 之间的交互
// src/stores/notification.ts
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { useLoginUserStore } from './user';
export const useNotificationStore = defineStore('notification', () => {
const notifications = ref([]);
async function fetchUserNotifications() {
const userStore = useLoginUserStore();
if (!userStore.isLoggedIn) {
return [];
}
// 获取用户通知的逻辑
// ...
}
return {
notifications,
fetchUserNotifications
};
});
9. 标准用法
- 按功能模块拆分 Store:每个功能模块创建独立的 Store 文件
- 使用组合式 API 风格:更符合 Vue 3 的设计理念
- 使用 TypeScript:获得更好的类型提示和错误检查
- 避免过度使用 Store:不是所有状态都需要放在全局 Store 中
- 合理使用 getters:对于需要计算的派生状态,使用 getters 而非直接在组件中计算
- 在 actions 中集中处理业务逻辑:保持组件的简洁性
- 使用 storeToRefs 解构:保持响应性
- 使用 $reset() 重置状态:需要恢复初始状态时使用
10. 调试技巧
- 查看和修改 Store 状态
- 使用
store.$state查看完整状态 - 使用
store.$id获取 Store 的唯一标识符 - 使用
store.$reset()重置状态
- 使用
欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com