⚠️ 这篇已失效,仅作历史记录保留

本文写于 2026-08-10,依据的是 gameboxclient旧代码基线。 当前代码库已被整体替换:文中反复出现的 /workspace(游戏资源库)、 /option(游戏发布栈)、src/views/Workspace/src/views/UI/ 在新代码中均已不存在, 所有 file:line 引用都无法再定位。

请改读重建后的课程首页, 或直接看新的架构图接手文档

Lesson 02 · 补课

Pinia 是什么

给 Node 人的五分钟版。读课 01 卡在这个词上的话,先读这篇。

一句话

Pinia 是 Vue 的状态管理库:一个能被界面「订阅」的全局单例。 你在 Node 里写惯的模块级单例,加上「谁读过我,我变了就自动通知谁」这一条,就是它。

先看它在解决什么问题

发布栈里有个真实需求:「现在有哪些 Jenkins 构建正在跑」这份列表,六个互不相邻的地方都要看。

谁要读它 拿去干什么
src/views/Option/Option.vue 已有构建时禁止重复启动
src/views/Option/Components/StatuList.vue 任务卡上显示状态
src/api/progress/index.ts 轮询进度、结束时移除
src/api/login/index.ts 登录时查一次有没有遗留构建
src/App.vue 顶层进度条
src/views/Database/Components/DatabaseTable.vue 策划工作台也要知道

它们在组件树上离得很远,有两个甚至根本不是组件(src/api/** 下的普通 TS 模块)。 靠 props 一层层传是不可能的。需要一个所有人都能直接拿到的地方——这就是 store。

你在 Node 里会怎么做,差在哪

你大概会写一个模块级单例:

// Node 的做法
export const buildState = { buildArray: [] as string[] }
export function addBuild(name: string) { buildState.buildArray.push(name) }

这在 Node 里够用了。但界面多一个要求:数据变了,画面得跟着变。 上面这种写法做不到——没人知道 buildArray 什么时候被 push 了。

你会怎么补?大概是 EventEmitter:改完手动 emit,关心的人 on 一下。 问题是这得手动维护——漏了一次 emit,界面就静静地停在旧数据上。

Vue 响应式 = 自动化的 emit / on

Vue 把这件事做成了自动的:读的时候记账(track),写的时候通知(trigger)。 只要你读过某个响应式数据,Vue 就记下「这段逻辑依赖它」;等它被改,Vue 自动重跑那段逻辑。 你不用写 emit,也不用写 on。 (Vue · 深入响应式系统

所以:Pinia ≈ 你熟悉的模块级单例 + 自动的 track/trigger。 它不是什么新范式,就是把「共享状态」和「变更通知」打包好了。

它在生态里的位置

三个部件

Pinia 你已经会的东西 差别
state 模块里的那个数据对象 必须写成函数返回,且是响应式的
getters 派生值的 getter / 纯函数 结果会被缓存,依赖没变就不重算
actions 模块导出的那些方法 this 就是 store,直接改 state,无需 dispatch

看一个真实的、写得对的 store

src/store/modules/jenkins.ts 只有 79 行,三个部件齐全,而且 ——难得地有类型

export interface JenkinsState {
  buildArray: string[]
  buildName: string
  nextName: string
  buildNativeWebArr: any
}

export const useJenkinsStore = defineStore('jenkins', {
  state: (): JenkinsState => ({
    buildArray: [], buildName: '', nextName: '', buildNativeWebArr: []
  }),
  getters: {
    getBuildName(): string { return this.buildName }
  },
  actions: {
    addBuildName(buildName: string) {
      if (this.buildArray.indexOf(buildName) >= 0) return
      this.buildName = buildName
      this.buildArray.push(buildName)
    }
  }
})
src/store/modules/jenkins.ts:4-44(节选)

把它和课 01 那个 GameEvnENV: {} as any 对照着看 ——同一个项目里,两种质量。

本项目有哪些 store

文件 defineStore id 行数 管什么
GameEvn.ts game 86 ★ 运行时配置(课 01)
jenkins.ts jenkins 79 ★ 正在跑的构建(发布栈)
progress.ts progress 33 ★ 进度面板
permission.ts permission 69 路由表(零权限过滤)
tagsView.ts tagsView 175 标签页与缓存
app.ts app 284 布局与全局开关
ui.ts ui 911 UI 编辑器
database.ts database 554 策划工作台
editor.ts editor 422 动画编辑器
uiInfoManager.ts manager 121 UI 编辑器辅助
locale.ts(68) · stepCache.ts(45) · counter.ts(37) · dict.ts(34)——小工具与脚手架残留

带 ★ 的三个在你 mission 的主链路上。ui.ts 911 行、 database.ts 554 行——store 长成这样已经不是「状态」而是「另一个业务层」了, 属于读懂即可、别扩散的范畴。

这个实现值不值得模仿

✅ 照做 —— jenkins.ts 的类型写法

export interface JenkinsState + state: (): JenkinsState => ({...}):4-17)。Pinia 会顺着这个接口推导出整个 store 的类型, 写错属性名当场报错。新建 store 一律照这个来。 (Pinia · State · TypeScript

❌ 别学 —— 手写 clean() 把字段置 null
clean() {
  for (const key in this) {
    if (Object.prototype.hasOwnProperty.call(this, key)) this[key] = null
  }
}

src/store/modules/jenkins.ts:67-73。它把 buildArray[] 变成 null,下次 addBuildName()this.buildArray.indexOf(...) 就会抛 TypeError。

目前没有找到调用点——搜 .clean() 只命中 editorStoreDBStore,没有 jenkinsStore.clean()。 所以这是颗没引爆的雷,不是线上故障。 同项目里 editor.ts:417-420 就写对了——重置成 [] 而不是 null

规范做法:Options 式 store 自带 store.$reset(),直接用它 (Pinia · 重置 state)。

⚠️ 隔离 —— 非组件模块里裸调 useXxxStore()

课 01 我说过「组件里用 useXxx()src/api/** 里用 useXxxWithOut()」。这句话我说得太绝对了,得更正: src/api/login/index.ts:7src/api/progress/index.ts:14 都是在模块顶层裸调 useJenkinsStore() 的。

能跑,但靠的是运气。查装在项目里的 Pinia 源码 (node_modules/pinia/dist/pinia.mjs:1725-1740):传实例进去会 setActivePinia(pinia) 设置一个模块级全局变量;裸调时则读这个全局变量, 读不到就抛 "getActivePinia()" was called but there was no active Pinia ——而且这个报错只在开发模式抛,源码注释写着 This will fail in production

所以这两处生效的前提是:在它们被 import 之前,已经有别的模块调用过 useXxxWithOut()app.use(pinia) 把全局变量设上了。 这是隐式的 import 顺序依赖,调整 import 就可能炸,而且炸在生产环境。

规范做法:非组件模块一律用 useXxxWithOut() 显式传实例 (Pinia · 在组件外使用 store)。 已有的两处别动,新代码不要沿用。

检索练习

1. 比起「导出一个普通对象的模块」,Pinia 多给了你什么?

2. src/api/login/index.ts:7 在模块顶层裸调 useJenkinsStore(),为什么没报错?

3. 下面哪种数据最该放进 store?

去真机上验证(约 5 分钟)

亲眼看一次 store 被写入。