qiankun 子应用重开后仍显示旧数据?问题出在模块顶层的 useStore()

问题是什么?

假设你有一个 qiankun 子应用,里面用 Pinia 存「当前选中的业务上下文」,比如当前租户、当前项目、当前订单等。

操作流程很简单:

  1. 打开子应用,进入上下文 A(例如项目 A)
  2. 关闭子应用
  3. 再次打开子应用,进入上下文 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,是一个全新的 Piniaunmount 时旧 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
}

也可以在路由参数变化(如 tenantIdprojectId 变了)时先 reset,再拉新数据。


Pinia 官方怎么说?

Pinia 文档提到:在组件外使用 store,必须保证 Pinia 已经 install 到当前 app 上

在 qiankun 里,「当前 app」会随着 mount / unmount 变化。模块顶层那次 useStore(),很容易绑到上一轮的 app

官方建议:

  • setup()createdmounted 等时机调用
  • 需要时动态调用,不要在 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() 并长期使用那个引用


参考资料

相关推荐
货拉拉技术1 小时前
面向 Agent Skill 的 CLI/SSO 鉴权体系:安全、无感、可追溯
前端·agent
ssshooter2 小时前
为什么父元素的高度不会包含子元素的 margin?
前端·javascript·面试
静Yu2 小时前
从一个九宫格素材小程序,看轻量工具产品该如何优化体验
前端·微信小程序
程序员黑豆2 小时前
AI全栈开发之Java:第一个Java程序
前端·后端·ai编程
小Q的编程笔记2 小时前
Pump.fun 的核心是什么?用 300 行 Solidity 实现 Bonding Curve 与自动 LP 销毁
前端·后端·智能合约
卷帘依旧2 小时前
React Fiber机制
前端
卷帘依旧2 小时前
JavaScript 判断页面加载完成的多种场景
前端
光影少年3 小时前
React 项目常见优化方案
前端·react.js·前端框架
lichenyang4533 小时前
把 demo 里的 console.log 全换成 HiLog:从 %{private} 没脱敏的困惑说起
前端