Lesson 02 · 补课
给 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 的做法
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。 它不是什么新范式,就是把「共享状态」和「变更通知」打包好了。
package.json)。
src/main.ts:40 调
setupStore(app), 内部是
app.use(createPinia())(src/store/index.ts:4-8)。
| Pinia | 你已经会的东西 | 差别 |
|---|---|---|
state |
模块里的那个数据对象 | 必须写成函数返回,且是响应式的 |
getters |
派生值的 getter / 纯函数 | 结果会被缓存,依赖没变就不重算 |
actions |
模块导出的那些方法 | this 就是 store,直接改 state,无需 dispatch |
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 那个 GameEvn 的
ENV: {} as any 对照着看 ——同一个项目里,两种质量。
| 文件 | 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() 只命中
editorStore 和 DBStore,没有
jenkinsStore.clean()。
所以这是颗没引爆的雷,不是线上故障。 同项目里
editor.ts:417-420 就写对了——重置成 [] 而不是
null。
规范做法:Options 式 store 自带
store.$reset(),直接用它 (Pinia · 重置 state)。
useXxxStore()
课 01 我说过「组件里用 useXxx(),src/api/**
里用 useXxxWithOut()」。这句话我说得太绝对了,得更正:
src/api/login/index.ts:7 和
src/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 多给了你什么?
B。读时 track、写时 trigger,省掉手动
emit/on。 A 靠的是你自己写
interface(jenkins.ts:4-9 写了,
GameEvn.ts:12 没写);C 是本项目额外加的
watch
(GameEvn.ts:44-86),不是 Pinia 自带;D 前端没有这个问题。
2. src/api/login/index.ts:7 在模块顶层裸调
useJenkinsStore(),为什么没报错?
C。setActivePinia() 设的是模块级全局变量
(node_modules/pinia/dist/pinia.mjs:1732-1733)。
谁先设上,后面裸调的人就白捡一个。这是 import 顺序依赖, 而且真读不到时的报错只在开发模式抛,生产环境是静默失败。
3. 下面哪种数据最该放进 store?
C。store 的成本是「全局可写」——谁都能改,出问题难查。
只有当跨越组件树、无法用 props 传时才值得付这个成本。 A
用 ref,B 用 props,D 用局部变量。 本项目的
ui.ts(911 行) 就是没守住这条线的样子。
亲眼看一次 store 被写入。
npm run dev,在 DevTools 的 Sources 面板找到
src/store/modules/jenkins.ts,在
addBuildName 的第一行 (:39)下断点。
src/api/login/index.ts:19
在登录成功后会调它查一次遗留构建。
this.buildArray 和
this.$id。 确认 this 就是 store 本身、$id
是字符串 'jenkins' ——课 01 说的「actions 里的
this 就是 store」,这下是你亲眼看到的。
sessionStorage.getItem('GameENV') !== null。
想一想:为什么 jenkins 这个 store 刷新一下就没了,而
GameEvn 不会?(答案在课 01 的 GameEvn.ts:44-86——
持久化是本项目额外写的,不是 Pinia 自带的。)