【qiankun 踩坑】路由切换回来,子应用 Vuex Store 数据居然还在

【qiankun 踩坑】路由切换回来,子应用 Vuex 数据居然还在?一次把原理和根治方案讲透

关键词:qiankun、微前端、Vuex、模块缓存、沙箱、keep-alive


1. 问题现象

在基于 qiankun 的微前端项目里,很多团队都会遇到这样诡异的一幕:

  1. 从主应用切进子应用,页面正常渲染,Vuex 初始 state 正常。
  2. 从主应用切走子应用 (路由跳到别的页面),qiankun 的 unmount 生命周期也执行了,DOM 被移除,JS 沙箱看起来也销毁了。
  3. 再次切回子应用 ------页面重新挂载,结果:
    • Vue Devtools 里 state 还是上一次的数据;
    • 全局的定时器、websocket 连接、缓存的表单值全在;
    • 看起来就像"子应用根本没有被卸载"!

于是大家开始怀疑:

  • qiankun 的 keep-alive 是不是没关?
  • 沙箱是不是没销毁?
  • 代码里是不是内存泄漏?

其实都不是。真正元凶是:浏览器 ES Module 的静态缓存


2. 为什么沙箱被销毁,store 还在?

2.1 qiankun JS 沙箱做了什么?

qiankun 的 JS 沙箱(无论是 SnapshotSandbox 还是 ProxySandbox)的核心逻辑:

  • 为每个子应用创建一份"代理 window"。
  • 子应用代码在 with(proxyWindow){ ... } 里执行。
  • 卸载时,把代理 window 上的新增属性全部还原或删除。

但它不会:

  • 重新下载子应用的入口文件;
  • 清空浏览器的 ES Module 缓存
  • 清空 CommonJS 的 require.cache

2.2 ES Module 的静态缓存机制

浏览器在第一次加载 main.js 时,会把所有被 import 的模块放到 模块缓存表

只要 URL 不变,后续再执行同一份文件,直接返回缓存里的导出对象

js 复制代码
// store/index.js
const store = new Vuex.Store({ ... })
export default store   // 同一个对象引用被永久缓存

当 qiankun 再次激活子应用时,模块系统认为 store.js 已经解析过 ,于是把旧 store 实例 直接给你。

沙箱隔离的是运行时全局变量,隔离不了模块缓存。这就是"store 没被清空"的根本原因。


3. 复现 Demo(最小可运行示例)

主应用:

js 复制代码
import { registerMicroApps, start } from 'qiankun'

registerMicroApps([
  {
    name: 'child',
    entry: '//localhost:8080',
    container: '#subapp',
    activeRule: '/child'
  }
])

start()

子应用 src/store/index.js

js 复制代码
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: { count: 0 },
  mutations: {
    inc: state => state.count++
  }
})

export default store

子应用 src/main.js

js 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

let instance = null
export async function mount () {
  instance = new Vue({ router, store, render: h => h(App) }).$mount('#app')
}
export async function unmount () {
  instance.$destroy()
  instance.$el.innerHTML = ''
}

运行步骤:

  1. 进入 /childcount = 0
  2. 在页面里 commit('inc')count = 1
  3. 切到主应用别的路由,再切回 /child
  4. count 仍然是 1,而不是 0。

4. 根治方案

方案 A:每次重新创建 store(推荐)

把 store 的创建逻辑从模块顶层移到 mount 钩子里,确保每次进入子应用都拿到全新的实例。

js 复制代码
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 导出工厂函数,而不是单例
export default function createStore () {
  return new Vuex.Store({
    state: { count: 0 },
    mutations: { inc: s => s.count++ }
  })
}
js 复制代码
// main.js
import createStore from './store'

let instance = null
export async function mount () {
  const store = createStore()
  instance = new Vue({ store, render: h => h(App) }).$mount('#app')
}

优点:

  • 100% 干净状态;
  • 不依赖 qiankun 版本;
  • 与 keep-alive 无关。

缺点:

  • 需要改造 store 的导出方式(一次即可)。

方案 B:强制清空模块缓存(hack)

如果你不想改代码,可以在 unmount 里暴力清空缓存:

js 复制代码
// main.js
export async function unmount () {
  // 1. 销毁 Vue 实例
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null

  // 2. 清空 webpack 模块缓存
  const storeKey = require.resolve('./store')
  delete __webpack_require__.c[storeKey]   // webpack
  // 或 ES Module:import.meta.hot.invalidate()(vite)
}

注意:不同打包器写法不同,且属于非公开 API,升级风险高。

方案 C:qiankun sandbox: { singleton: false }

js 复制代码
start({ sandbox: { singleton: false } })

每次激活子应用都会重新下载并执行入口文件,自然也能得到全新 store。

缺点:资源重新加载,白屏时间变长。


5. 最佳实践小结

场景 建议
子应用需要彻底隔离、无状态 方案 A(工厂函数)
子应用想保持缓存,仅局部刷新 在组件级做缓存,而不是 store 级
旧项目大量文件难以改动 方案 C(singleton: false)过渡

6. 结语

"沙箱销毁 ≠ 模块重载" 是 qiankun 用户最容易忽视的细节。

把 store 做成工厂函数 是官方推荐、最稳妥的做法。

希望本文能帮你少走 3 小时弯路,也把这篇文章转给正在踩坑的同事吧!

相关推荐
dy17172 小时前
element-plus表格默认展开有子的数据
前端·javascript·vue.js
2501_915918416 小时前
Web 前端可视化开发工具对比 低代码平台、可视化搭建工具、前端可视化编辑器与在线可视化开发环境的实战分析
前端·低代码·ios·小程序·uni-app·编辑器·iphone
程序员的世界你不懂7 小时前
【Flask】测试平台开发,新增说明书编写和展示功能 第二十三篇
java·前端·数据库
索迪迈科技7 小时前
网络请求库——Axios库深度解析
前端·网络·vue.js·北京百思可瑞教育·百思可瑞教育
gnip7 小时前
JavaScript二叉树相关概念
前端
attitude.x8 小时前
PyTorch 动态图的灵活性与实用技巧
前端·人工智能·深度学习
β添砖java8 小时前
CSS3核心技术
前端·css·css3
空山新雨(大队长)8 小时前
HTML第八课:HTML4和HTML5的区别
前端·html·html5
猫头虎-前端技术8 小时前
浏览器兼容性问题全解:CSS 前缀、Grid/Flex 布局兼容方案与跨浏览器调试技巧
前端·css·node.js·bootstrap·ecmascript·css3·媒体