问题是什么?
假设你有一个 qiankun 子应用,里面用 Pinia 存「当前选中的业务上下文」,比如当前租户、当前项目、当前订单等。
操作流程很简单:
- 打开子应用,进入上下文 A(例如项目 A)
- 关闭子应用
- 再次打开子应用,进入上下文 B(例如项目 B)
期望:页面展示项目 B 的数据。
实际:页面仍显示项目 A 的数据,像「幽灵数据」一样残留。
更诡异的是:
- 嵌入主应用、反复开关子应用后,必现
- 把
useStore()从模块顶层挪到created里,问题就「神奇地」好了
这篇文章解释:为什么挪个位置就好了,以及怎样写才不容易踩坑。
先排除一个误会:不是 Pinia getter 的「缓存」
很多人第一反应会去看 getter。比如 store 里写着:
js
// stores/useBizStore.js
export const useBizStore = defineStore('biz', {
state: () => ({
currentContext: null, // 当前业务上下文
}),
getters: {
// 注释:类似 computed,有缓存
currentContext: (state) => state.currentContext,
},
})
Pinia getter 的「缓存」指的是:依赖的 state 没变时,不会重复计算 ,和 Vue 的 computed 一样,是框架自带行为。
上面这种 getter 只是 return state.xxx,没有额外缓存逻辑。
所以:如果你看到旧数据,优先怀疑 state 没被更新或没被清空,而不是 getter「记住了上一次的值」。
真正的原因:两个「只执行一次」,撞在一起了
这个问题由两件事叠加造成:
| 行为 | |
|---|---|
| qiankun 子应用 | 每次 mount,都会 createApp + createPinia,是一个全新的 Pinia ;unmount 时旧 Pinia 随 app 一起销毁 |
| ES Module | .vue / .js 文件第一次 import 时 ,模块顶层代码执行一次;之后 unmount 再 mount,模块不会重新 import |
如果你在模块顶层写了:
js
const bizStore = useBizStore() // ⚠️ 只在第一次 import 时执行
那么这行代码拿到的,是当时活跃的 Pinia 实例上的 store。
子应用下次 mount 时,虽然创建了新的 Pinia,但模块顶层那行不会重新执行 ,变量 bizStore 仍然指向上一次、已经销毁的 Pinia 里的 store。
用时间线理解
markdown
【第一次打开子应用 --- 上下文 A】
1. 主应用 mount 子应用
2. 子应用 main.js:createPinia() → 得到 Pinia 实例 A
3. 某组件文件首次被加载(import)
4. 模块顶层执行:const bizStore = useBizStore()
→ bizStore 绑定在 Pinia A 上
5. 业务代码写入上下文 A 的数据
【关闭子应用】
6. 子应用 unmount,Pinia A 被销毁
7. 但 JS 模块仍留在内存,不会重新 import
【再次打开子应用 --- 上下文 B】
8. 主应用再次 mount 子应用
9. 子应用 main.js:createPinia() → 得到 Pinia 实例 B(全新、空的)
10. 模块顶层那行不会重新执行
11. 代码里用的还是步骤 4 的 bizStore
→ 读到的仍是 Pinia A 里的「上下文 A」
一句话:不是 Pinia 主动缓存了业务数据,而是你手里攥着一根指向「旧 Pinia」的 store 引用。
错误写法 vs 为什么改到 created 就好了
❌ 错误:模块顶层获取 store
js
// Header.vue
import { useBizStore } from '@/stores/useBizStore'
// 文件第一次 import 时执行,且只执行一次
const bizStore = useBizStore()
export default {
name: 'AppHeader',
computed: {
title () {
return bizStore.currentContext?.name ?? ''
},
},
}
✅ 临时有效:在 created 里再取一次
js
export default {
data () {
return { bizStore: null }
},
created () {
this.bizStore = useBizStore()
},
computed: {
title () {
return this.bizStore?.currentContext?.name ?? ''
},
},
}
created 每次组件挂载都会执行 。此时子应用已经完成本次的 app.mount(),getActivePinia() 能拿到当前这次的 Pinia 实例,所以读到的是新上下文的数据。
注意:created 不是有什么特殊能力,只是调用时机对了。
子应用侧通常长什么样?
很多 qiankun 子应用会类似这样写 main.js:
js
let app = null
function render (props = {}) {
app = createApp(App)
app.use(createPinia()) // 每次 mount 都是新 Pinia
app.mount(props.container?.querySelector('#app') ?? '#app')
}
export async function mount (props) {
render(props)
}
export async function unmount () {
app?.unmount()
app = null
}
关键点:每次 mount 都是新 app、新 Pinia;但业务组件的模块代码,不会因为 unmount 而重新执行。
哪些写法有同样风险?
只要是在模块顶层缓存 store 引用,在 qiankun 反复 mount / unmount 下都可能串数据:
js
// 1. .vue 的 <script> 顶层
const store = useBizStore()
// 2. 普通 .js 工具文件顶层
import { useBizStore } from '@/stores/useBizStore'
const store = useBizStore()
export function doSomething () {
store.updateContext(...)
}
// 3. 路由守卫文件顶层(如果子应用每次 mount 会重新注册路由,更要小心)
const store = useBizStore()
router.beforeEach(() => { ... })
独立运行子应用时,app 往往只 mount 一次,问题不明显;嵌入主应用、频繁开关子应用后,就会暴露。
推荐写法
方案 A:在生命周期里获取(Options API)
js
export default {
created () {
this.bizStore = useBizStore()
},
}
方案 B:用 computed 动态读取(更推荐)
不长期持有 store 变量,每次通过当前 active Pinia 获取:
js
import { useBizStore } from '@/stores/useBizStore'
export default {
computed: {
currentContext () {
return useBizStore().currentContext
},
},
}
方案 C:Composition API(<script setup>)
vue
<script setup>
import { storeToRefs } from 'pinia'
import { useBizStore } from '@/stores/useBizStore'
const { currentContext } = storeToRefs(useBizStore())
</script>
<script setup> 会在每次组件实例创建时 执行,天然绑到当前 Pinia,一般不要在同文件的模块顶层再写 const store = useXxxStore()。
方案 D:子应用卸载时清空 state(配合使用)
只改「获取 store 的方式」有时不够。如果 state 里仍留着旧上下文,切换时还应主动 reset:
js
// store
actions: {
reset () {
this.currentContext = null
},
}
// main.js
export async function unmount () {
useBizStore().reset()
app?.unmount()
app = null
}
也可以在路由参数变化(如 tenantId、projectId 变了)时先 reset,再拉新数据。
Pinia 官方怎么说?
Pinia 文档提到:在组件外使用 store,必须保证 Pinia 已经 install 到当前 app 上。
在 qiankun 里,「当前 app」会随着 mount / unmount 变化。模块顶层那次 useStore(),很容易绑到上一轮的 app。
官方建议:
- 在
setup()、created、mounted等时机调用 - 需要时动态调用,不要在 module scope 里缓存 store 引用
参考:Pinia - Using a store outside of a component
排查清单
| 你看到的现象 | 建议先查什么 |
|---|---|
| 关闭再打开子应用,仍显示上一次的数据 | 搜索项目里模块顶层的 useXxxStore() |
| 独立运行正常,嵌入主应用才出问题 | 是否每次 mount 都 createPinia() |
| 怀疑 getter 缓存 | 先看 state 是否更新 / reset |
| 换路由参数后仍串数据 | unmount 或参数变化时是否 $reset |
总结
| 常见误解 | 实际情况 |
|---|---|
| getter 把旧数据缓存住了 | getter 只读 state;旧数据在 state 或过期 store 引用里 |
改到 created 是用了某种刷新 API |
只是每次挂载时重新绑定了当前 Pinia |
模块顶层写 useStore() 没问题 |
qiankun 下极易绑到已销毁的 Pinia 实例 |
记住这一句就够了:
qiankun 子应用每次 mount 都会新建 Pinia,但 JS 模块只 import 一次------不要在模块顶层调用
useStore()并长期使用那个引用。