在微前端架构里,Vue 3 子应用被主应用动态加载和卸载已是常见模式。当业务切换或权限变更导致子应用下线时,仅依赖框架默认的卸载逻辑往往不够,必须针对实例销毁与各类资源清理做完整处理,否则容易引发内存泄漏和状态错乱。

一、Vue 3 子应用实例的挂载与卸载机制
Vue 3 使用 createApp 工厂函数生成应用实例,子应用通常会在导出的 bootstrap、mount、unmount 生命周期中控制自身行为。主应用通过微前端框架(如 qiankun 或 wujie)调用 mount 时将子应用渲染到指定容器,调用 unmount 时则应销毁该实例。
在 Vue 3 中,应用实例的 unmount 方法会递归卸载组件树并触发各组件的 onUnmounted 钩子。但需要注意,unmount 仅负责框架托管的渲染层与响应式绑定,对于在业务代码中手动绑定到 window、document 的监听器,或第三方库生成的独立实例,框架并不会自动追踪。
// 子应用入口常见结构
let app = null;
export async function mount(props) {
app = createApp(RootComponent);
app.use(router);
app.mount(props.container);
}
export async function unmount() {
if (app) {
app.unmount();
app = null;
}
}
上述代码虽然调用了 app.unmount(),但若 RootComponent 内部或业务模块中注册了全局事件,这些资源在 unmount 后依旧存活。因此,实例销毁只是第一步,清理动作必须配合业务代码主动完成。
二、子应用需清理的典型资源类别
1. 全局事件与定时器
许多子应用会在挂载时通过 window.addEventListener 监听 resize、message 等事件,或使用 setInterval 做轮询。若 unmount 时不移除,这些函数会持续引用组件作用域,导致内存无法回收。
推荐在根组件 onUnmounted 中统一清理,或由子应用暴露的 unmount 方法执行。这样无论微前端框架以何种方式卸载,清理逻辑都能生效。
import { onUnmounted } from 'vue';
export default {
setup() {
const handler = () => console.log('resize');
window.addEventListener('resize', handler);
const timer = setInterval(() => {}, 1000);
onUnmounted(() => {
window.removeEventListener('resize', handler);
clearInterval(timer);
});
}
}
2. 路由与状态管理实例
子应用独立创建的 router 与 pinia/vuex 实例,在卸载后若未被置空,可能和再次加载的新实例产生冲突。尤其是使用了同一个 history 模式的路由,容易造成主应用路由栈污染。
应在 unmount 阶段调用路由的 destroy 类方法(如有)并解除引用,状态库则可调用 pinia.dispose() 或重置 store。下方示例展示在 unmount 中清理:
export async function unmount() {
if (app) {
app.unmount();
router.destroy && router.destroy();
pinia.dispose && pinia.dispose();
app = null;
}
}
3. 第三方库与 DOM 残留
图表库(如 ECharts)、地图 SDK 常生成自身 canvas 或 WebGL 上下文。仅移除 Vue 节点不会释放这些上下文,必须显式调用其 dispose 方法。此外,微前端若未开启严格沙箱,子应用插入到 body 的弹窗节点也会残留。
建议在根组件卸载钩子里遍历并销毁第三方实例,同时检查挂载容器内是否还存在游离节点。对沙箱型微前端,还应确认 js 沙箱在卸载后回收了全局变量。
// 清理 ECharts 示例
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
三、主应用与微前端框架的协同清理
1. 卸载生命周期的对接
以 qiankun 为例,主应用通过 loadMicroApp 加载子应用,返回的 microApp 对象拥有 unmount 方法。主应用在路由离开或条件不满足时应主动调用,而非直接移除 DOM 容器。
如果主应用仅用 v-if 删除子应用挂载点,而子应用 unmount 未执行,那么前面提到的所有清理逻辑都不会触发。因此主应用需保证微前端生命周期完整走完。
// 主应用控制卸载
const micro = loadMicroApp({ name: 'sub-vue3', entry: '//ipipp.com/sub' });
// 条件触发卸载
micro.unmount().then(() => {
console.log('子应用已卸载');
});
2. 沙箱与样式隔离的回收
部分微前端方案通过快照或 Proxy 实现 JS 沙箱,CSS 则采用 scoped 或 Shadow DOM。子应用卸载时,框架应自动恢复全局状态并移除样式标签。但在自定义集成中,开发者需手动移除动态插入的 <style> 与 <link> 标签。
可在子应用 unmount 末尾通知主应用,由主应用统一清理容器外的资源。这种双向协作能显著降低耦合,也方便后期排查泄漏点。
| 清理对象 | 负责方 | 常用方法 |
|---|---|---|
| Vue 组件树 | 子应用 | app.unmount() |
| 全局事件监听 | 子应用 | removeEventListener |
| 第三方实例 | 子应用 | dispose / destroy |
| 样式与沙箱 | 主应用或框架 | 移除标签 / 恢复快照 |
四、完整卸载示例与最佳实践
综合上述要点,一个健壮的子应用入口应当把实例销毁和资源清理全部封装在 unmount 中,并在根组件内配合 onUnmounted 兜底。下方给出一个较完整的参考实现。
import { createApp, onUnmounted } from 'vue';
import RootComponent from './Root.vue';
import router from './router';
import { createPinia } from 'pinia';
let app = null;
let pinia = null;
export async function mount(props) {
app = createApp(RootComponent);
pinia = createPinia();
app.use(pinia);
app.use(router);
app.mount(props.container);
// 根组件内统一清理
app._context.components.RootComponent.setup = function() {
onUnmounted(() => {
window.removeEventListener('resize', onResize);
});
};
}
export async function unmount() {
if (app) {
app.unmount();
router.destroy && router.destroy();
pinia.dispose && pinia.dispose();
app = null;
pinia = null;
}
}
function onResize() {}
实践中建议为子应用编写卸载自测:连续加载卸载多次,使用浏览器性能面板观察内存与 DOM 节点数是否回到基线。若发现监听器数量持续增长,说明清理存在遗漏。通过规范生命周期与资源登记机制,Vue 3 微前端子应用完全能做到干净卸载,不影响主应用长期运行的稳定性。