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

13 KiB
Raw Permalink Blame History

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 风格一致:

// 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 风格统一,团队学习成本低
  • 可以直接用 refcomputedasync/await,更灵活
  • TypeScript 类型推导更好

3.2 Store 怎么注册的?

// main.ts — 一行代码搞定
import { createPinia } from 'pinia';
app.use(createPinia());

就这么简单。不需要像 Vuex 那样手动注册每个 module。

3.3 组件怎么用 Store

<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,不需要 mapStatemapActions,直接用就行。

3.4 权限判断怎么用 computed 封装?

这是项目中一个很巧妙的设计——把权限判断逻辑封装在 Store 的 computed 里:

// 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');

组件中这样用:

<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

// 登录时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、权限标记 只在某个组件子树内共享,不需要全局
// 事件总线示例 — 登录成功后通知全局
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.tsapp.use(createPinia())
用户 Store最核心 src/stores/user.ts3 个 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.tsmitt