Files
Situation-Awareness-Platfor…/docs/Pinia状态管理实现详解.md
cfy666 c1e9a4be83 chore: sync local changes and add documentation
- Update yarn.lock
- Add project implementation docs in docs/
- Add personal internship experience notes in 实习讲解/
2026-06-29 19:47:30 +08:00

346 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pinia 状态管理 — 面试版
> 简历原话:**"使用 Pinia 进行状态管理"**
>
> 这篇文档帮你理解项目中 Pinia 到底管了哪些状态、怎么管的,以及面试时怎么讲。
---
## 一、先搞清楚Pinia 在项目里干什么?
### 什么是状态管理?
```
没有状态管理的问题:
用户登录信息 → 在 A 组件里获取
用户登录信息 → B 组件也要用 → 怎么传?父子组件可以 props兄弟组件呢
用户登录信息 → C 组件也要用 → 再 props 一层?代码变成意大利面...
有了 Pinia
用户登录信息 → 存在 Pinia store 里
A、B、C 组件 → 各自直接从 store 读取,不需要互相传
```
**一句话概括**Pinia 就是"全局数据仓库",存放多个组件都需要共享的数据(登录状态、当前仓库信息、当前组织信息等),避免组件之间层层传递。
### 项目中 Pinia 的定位
```
数据从哪来 → API 请求 / localStorage
存到哪 → Pinia store全局仓库
谁来用 → 32~62 个组件直接从 store 读取
```
---
## 二、项目里有几个 Store各自管什么
项目有 **10 个 Store**,其中 **6 个是真正活跃使用的**
### 核心 Store用得最多
| Store 名 | 管什么 | 被多少文件使用 |
|----------|--------|---------------|
| `useAccountStore` | 当前登录用户的信息ID、头像、昵称、Token | **62 个文件** |
| `orgInfoStore` | 当前组织的信息(名称、头像、权限、成员) | **37 个文件** |
| `repoInfoStore` | 当前仓库的信息名称、Star数、权限、是否私有 | **35 个文件** |
| `useGlobalInfoStore` | 全局 UI 状态(菜单、主题、搜索、语言列表) | **32 个文件** |
### 辅助 Store用得较少
| Store 名 | 管什么 | 被多少文件使用 |
|----------|--------|---------------|
| `otherAccountStore` | 当前正在查看的其他用户的信息 | 10 个文件 |
| `useMrChangeStore` | 代码合并MR的 diff 显示设置 | 6 个文件 |
| `entranceData` | 首页的仓库/组织列表 | 2 个文件 |
### 每个 Store 管理的状态一目了然
```
useAccountStore用户信息
├── isLogin: 是否已登录
└── accountInfo: { id, nickname, avatar, email, token... }
orgInfoStore组织信息
├── orgInfo: { name, avatar, description, visibility... }
├── isFollow: 是否关注了这个组织
├── memList / memCount: 成员列表和数量
└── computed → isAdmin / isDeveloper / isVisitor权限判断
repoInfoStore仓库信息
├── repoInfo: { name, star_count, visibility, archived... }
├── access_level: 权限等级数字
└── computed → isPrivate / isArchived / isAdmin / isDeveloper权限判断
useGlobalInfoStore全局 UI
├── globalMenuInfo: 菜单配置
├── menuType: 当前菜单类型repo/org
├── globalTheme: 主题light/dark
├── headerSearch: 搜索状态
└── languageList: 编程语言列表
```
---
## 三、怎么实现的?(面试核心)
### 3.1 Store 怎么定义的?
项目用的是 Pinia 的 **Setup 语法**(函数式),和 Vue3 Composition API 风格一致:
```typescript
// stores/user.ts — 最核心的用户 Store
import { defineStore } from 'pinia';
import { reactive, ref } from 'vue';
export const useAccountStore = defineStore('accountInfo', () => {
// ========== 状态state==========
const isLogin = ref(Boolean(localStorage.getItem('op_access_token')));
const accountInfo = reactive({
nickname: '',
avatar: '',
email: '',
op_access_token: '',
// ...
});
// ========== 操作actions==========
const saveAccountInfo = (info) => {
if (info) {
Object.assign(accountInfo, info); // 合并用户信息
} else {
Object.keys(accountInfo).forEach(k => accountInfo[k] = ''); // 清空(退出登录)
}
};
const checkIsLogin = async (token, refreshToken) => {
localStorage.setItem('op_access_token', token);
const userRes = await getUserInfo(); // 调 API 验证
if (userRes.data?.code === 200) {
saveAccountInfo(userRes.data.data); // 存到 store
return true;
}
return false;
};
// ========== 返回(暴露给组件用)==========
return { isLogin, accountInfo, saveAccountInfo, checkIsLogin };
});
```
**为什么用 Setup 语法而不是 Options 语法?**
- 和 Vue3 Composition API 风格统一,团队学习成本低
- 可以直接用 `ref``computed``async/await`,更灵活
- TypeScript 类型推导更好
### 3.2 Store 怎么注册的?
```typescript
// main.ts — 一行代码搞定
import { createPinia } from 'pinia';
app.use(createPinia());
```
就这么简单。不需要像 Vuex 那样手动注册每个 module。
### 3.3 组件怎么用 Store
```vue
<script setup>
import { useAccountStore } from '@/stores/user';
import { repoInfoStore } from '@/stores/Repo';
// 获取 store 实例
const accountStore = useAccountStore();
const repoStore = repoInfoStore();
// 直接读状态
console.log(accountStore.isLogin);
console.log(repoStore.repoInfo.name);
// 直接调 action
accountStore.saveAccountInfo({ nickname: '新名字' });
await repoStore.getRepoInfo();
</script>
<template>
<!-- 模板中直接用 -->
<img :src="accountStore.accountInfo.avatar" />
<span v-if="repoStore.isAdmin">管理</span>
</template>
```
**关键点**:不需要 `$store`,不需要 `mapState``mapActions`,直接用就行。
### 3.4 权限判断怎么用 computed 封装?
这是项目中一个很巧妙的设计——把权限判断逻辑封装在 Store 的 computed 里:
```typescript
// stores/Repo/index.ts
const access_level = ref(0);
// 8 个 computed 属性,自动根据 access_level 判断权限
const isAdmin = computed(() => access_level.value >= admin);
const isDeveloper = computed(() => access_level.value >= developer);
const isVisitor = computed(() => access_level.value >= visitor);
const isAdminOperate = computed(() => access_level.value >= admin && !repoInfo.value?.archived);
const isArchived = computed(() => repoInfo.value?.archived);
const isPrivate = computed(() => repoInfo.value?.visibility === 'private');
```
组件中这样用:
```vue
<template>
<!-- 权限按钮只有管理员才能看到 -->
<d-button v-if="repoStore.isAdmin">删除仓库</d-button>
<!-- 归档提示自动根据状态显示 -->
<span v-if="repoStore.isArchived">此仓库已归档</span>
<!-- 私有标识 -->
<d-tag v-if="repoStore.isPrivate">私有</d-tag>
</template>
```
**好处**:权限判断逻辑集中在 Store 里35 个组件共享同一套规则。改规则只改一处。
### 3.5 持久化怎么做的?
项目**没有**用 `pinia-plugin-persistedstate` 插件,而是手动操作 localStorage
```typescript
// 登录时Token 存 localStorage同时存 Store
localStorage.setItem('op_access_token', token);
saveAccountInfo({ op_access_token: token });
// 页面刷新时:从 localStorage 恢复到 Store
const isLogin = ref(Boolean(localStorage.getItem('op_access_token')));
const accountInfo = reactive({
op_access_token: localStorage.getItem('op_access_token') || '',
// ...
});
// 退出时:清 localStorage清 Store
localStorage.removeItem('op_access_token');
saveAccountInfo({}); // 传空 = 清空所有字段
```
**为什么手动而不用插件?** 因为只有用户 Token 需要持久化,其他状态(仓库信息、组织信息)每次进页面都重新从 API 获取。用插件反而多余。
---
## 四、除了 Pinia还有哪些状态管理方式
项目不是"只用 Pinia",而是**三种方式配合使用**
| 方式 | 用在哪 | 为什么 |
|------|--------|--------|
| **Pinia** | 全局共享状态(用户、仓库、组织、菜单) | 多个页面都需要,跨组件共享 |
| **mitt 事件总线** | 跨组件事件通知(登录/登出、错误、微前端通信) | 一次性事件触发,不需要持久化状态 |
| **provide/inject** | 父子组件深层传递移动端的仓库ID、权限标记 | 只在某个组件子树内共享,不需要全局 |
```typescript
// 事件总线示例 — 登录成功后通知全局
import { emitEvent } from '@/utils/eventBus';
emitEvent('login', userData); // 触发
addEventListener('login', handleLogin); // 监听
// provide/inject 示例 — 移动端深层组件获取仓库ID
const repoId = inject('repoId'); // 孙子组件直接拿,不需要层层 props
```
**面试话术**
> "项目中状态管理不是只有 Pinia而是三种方式配合Pinia 管全局持久状态(用户/仓库/组织mitt 事件总线管跨组件事件通知(登录/登出/错误provide/inject 管组件子树内的深层数据传递。根据数据的使用范围和生命周期选择最合适的方式。"
---
## 五、Store 之间的关系
项目中所有 Store **互相独立**,没有任何 Store 引用其他 Store
```
useAccountStore ──┐
orgInfoStore ─────┤ 互不依赖,各自独立
repoInfoStore ────┤
useGlobalInfoStore┘
```
**为什么这样设计?** 因为每个 Store 对应一个业务领域(用户/仓库/组织/全局),它们的数据来源不同(不同的 API使用场景也不同没有必要互相耦合。
---
## 六、整体架构总结
```
Pinia 在项目中的位置:
API 请求 ──→ Pinia Store ──→ Vue 组件32~62 个文件消费)
├── useAccountStore用户62 个文件用)
├── orgInfoStore组织37 个文件用)
├── repoInfoStore仓库35 个文件用)
├── useGlobalInfoStore全局UI32 个文件用)
└── + 6 个辅助 Store
配合使用:
├── mitt 事件总线30+ 个文件用)→ 事件通知
└── provide/inject → 组件子树数据传递
```
---
## 七、面试问答准备
### Q1"使用 Pinia 进行状态管理"具体做了什么?
> 项目用 Pinia 管理了 10 个全局状态 Store其中核心的 4 个分别管理用户登录信息62 个文件使用、仓库信息35 个文件、组织信息37 个文件)、全局 UI 状态32 个文件。比如用户登录后Token 和用户信息存在 Pinia 里,导航栏、个人主页、仓库页面等几十个组件都能直接读取,不需要层层传递 props。
### Q2为什么选 Pinia 而不是 Vuex
> 三个原因第一Pinia 是 Vue3 官方推荐的状态管理方案Vue3 + Composition API 的项目用 Pinia 最自然第二Pinia 的 API 更简洁,不需要 mutationsactions 里可以直接 async/await第三Pinia 的 TypeScript 支持更好,类型推导自动完成,不需要额外写类型声明。
### Q3Pinia 有两种语法,你用的哪种?
> 用的 Setup 语法(函数式),就是 `defineStore('id', () => { ... })` 这种。因为项目用 Vue3 Composition API 开发Setup 语法和 Composition API 风格统一,可以直接用 ref、computed、async/await。整个项目 10 个 Store 有 9 个用 Setup 语法,只有 1 个占位 Store 用了 Options 语法。
### Q4状态持久化怎么做的
> 没有用 pinia-plugin-persistedstate 插件,因为只有用户 Token 需要持久化,其他状态(仓库、组织信息)每次进页面都重新从 API 获取。所以手动在 localStorage 和 Store 之间同步:登录时 Token 存 localStorage 同时存 Store页面刷新时从 localStorage 恢复到 Store退出时两边都清空。
### Q5Store 里的 computed 有什么用?
> 主要用来封装权限判断逻辑。比如 repoInfoStore 里有 8 个 computed 属性isAdmin、isDeveloper、isVisitor 等,根据 access_level 数字自动判断当前用户的角色。35 个组件用到仓库权限的地方,都直接读这些 computed而不是每个组件自己写判断逻辑。这样权限规则改一处就全局生效。
### Q6除了 Pinia 还用了什么状态管理方式?
> 还用了 mitt 事件总线和 provide/inject。三种方式各有分工Pinia 管需要持久化的全局状态mitt 管一次性事件通知(比如登录/登出事件provide/inject 管组件子树内的深层数据传递。不是所有共享数据都适合放 Pinia事件通知用 mitt 更轻量,局部数据用 provide/inject 更合理。
---
## 八、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| Pinia 版本 | 2.1.3 |
| Store 总数 | 10 个 |
| 活跃使用的 Store | 6 个 |
| 核心 Store | 4 个(用户/仓库/组织/全局UI |
| 使用最多的 Store | useAccountStore62 个文件) |
| 语法风格 | Setup 语法9/10 |
| 持久化方式 | 手动 localStorage无插件 |
| 配合的其他方案 | mitt 事件总线30+ 文件、provide/inject |
---
## 九、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| Pinia 注册 | `src/main.ts`app.use(createPinia()) |
| 用户 Store最核心 | `src/stores/user.ts`3 个 Store 共存) |
| 仓库 Store | `src/stores/Repo/index.ts` |
| 组织 Store | `src/stores/Org/index.ts` |
| 全局 UI Store | `src/stores/Global/index.ts` |
| MR diff 设置 Store | `src/stores/merge.ts` |
| 事件总线 | `src/utils/eventBus.ts`mitt |