前言
之前使用京东微前端框架MicroApp集成10个微前端的页面到AngularJs的后台管理系统中,每个微前端做成一个菜单,一共10个,每次打开都是一个新的微前端,但是发现打开的微前端越多,容易造成内存泄露,下面讲解如何解决这个问题。
操作
之前的写法是每个angularjs页面如下所示:
<div class="border-left animated fadeInRight eee-bg-nano" style="border-left: 10px solid #e7eaec;" ng-controller="domainScenarioCtrl"
ng-init="init('fireManage')">
<div style="width: 100%; height: 100%;">
<section style="height: 100%; width: 100%;" class="content" id="fireManage-p">
<div style="height: 100%; width: 100%;" id="fireManage">
<micro-app style="height: 100%; width: 100%;" name='fireManage'
url="http://xxx/iot-front/fireListMagage"></micro-app>
</div>
</section>
</div>
</div>
这样的代码一共有10个文件,这种写法因为是写死的渲染所以容易造成内存泄露,经过询问ChatGpt以及反复验证和尝试,终于得到一个解决办法:
不使用标签,转为使用renderApp()方法动态控制url,每次打开一个tab时只渲染一个微前端,同时卸载其它微前端,也就是不管打开几个微前端页面,有且只有一个是激活状态!

同时需要解决以下几个场景问题:
1、点击菜单添加tab时激活新的微前端,卸载旧的
2、tab来回切换时激活当前新的微前端,卸载旧的
3、删除tab时激活当前新的微前端,卸载旧的
4、刷新页面时激活当前微前端
5、重复点击菜单tab时激活微前端,卸载旧的
针对以上思路和场景问题,我们一个个来看,首先,我们创建一个文件专门来处理微前端的操作:
micro-app-helper.js
(function (window) {
'use strict';
const MicroAppHelper = {
getAppMap: function () {
const context = window.getContext();
return {
'name1': 'url1',
...
};
},
// 处理微应用切换
// app=url, alias = ''
// global= state = null,sessionStorage
handleMicroAppSwitchByUrl: async function (global, app, isRedirect = false) {
if (!app.url) return;
try {
const appMap = this.getAppMap();
let appName = null, appUrl = null;
for (const key in appMap) {
if (app.url.includes(key)) {
appName = key;
appUrl = appMap[key];
break;
}
}
if (!appName || !appUrl) return;
const activeApps = window.microApp.getActiveApps();
const activeApp = activeApps.length > 0 ? activeApps[0] : null;
const obj = {name: appName, url: appUrl, container: "#" + appName};
// 卸载旧应用
if (activeApp && activeApp !== appName) {
console.log(`卸载旧微前端:${activeApp}`);
await window.microApp.unmountApp(activeApp, {clearAliveState: true});
const appContainer = document.querySelector('#' + activeApp);
if (appContainer) {
appContainer.innerHTML = '';
console.log(`已移除 DOM 容器:${activeApp}`);
}
}
// 是否需要跳转
if (isRedirect) {
if (app.alias){
// 渲染新应用
global.state.go(app.alias).then(() => {
this.renderMicroApp(global, obj)
});
}
} else {
this.renderMicroApp(global, obj)
}
} catch (e) {
console.error('微应用切换错误:', e);
}
},
renderMicroApp: function (global, obj) {
try {
const userData = {
currentUser: angular.copy(global.sessionStorage.currentUser),
userSelOrg: '',
selectOrg: angular.copy(global.sessionStorage.checkOrgInfo)
};
window.microApp.setData(obj.name, userData);
window.microApp.renderApp(obj).then(() => {
console.log(`${obj.name} 渲染完成`);
});
} catch (e) {
console.log('renderMicroApp error:', e)
}
},
getAppUrlByName: function (name) {
const appMap = this.getAppMap();
return appMap[name]
}
};
// 暴露到全局
window.MicroAppHelper = MicroAppHelper;
})(window);
上面代码中做了几件事:
1、卸载旧的微前端
2、渲染新的微前端
3、向微前端传值setData,注意对应好name,否则不生效
4、还有一些其它业务逻辑,比如angularjs中的$state.go()跳转页面方法,还有根据name匹配到微前端的url,最终在renderApp中做为参数执行,如果大家不需要的话,可以略过,我也懒的改了!
建好之后,我们在项目的index.html中引入这个js文件,否则不生效,如下所示:
<script type="module">
// 在主应用中初始化
if (!window.microAppInitialized) {
import('./js/bundle.js').then((microApp) => {
window.microApp = microApp.default || microApp;
window.microAppInitialized = true;
window.appList = []
window.microApp.start({
// iframe: true,
destroy: true,
delay: 0,
preFetchApps: [
{ name: 'buildingListManage', url: window.getContext().jiBaoUrl + 'iot-front/buildingManage' }, // 加载资源并解析
],//预加载
lifeCycles: {
created(e, appName) {
// console.log(`子应用${appName}被创建`)
},
mounted(e, appName) {
console.log(`子应用${appName}已经渲染完成`)
},
unmount(e, appName) {
console.log(`子应用${appName}已经卸载`)
},
},
globalAssets: {
js: ['http://xxx/iot-front/static/js/chunk-libs.91a68588.js',
'http://xxx/iot-front/jquery.min.js',
'http://xxx/iot-front/static/js/app.c49e13c0.js',
'http://xxx/iot-front/static/js/chunk-0f0d195a.483813fd.js'
]
}
});
});
}
</script>
<script src="js/lib/microApp/index.js"></script>
bundle.js就是micro-app的源码,因为是angularjs项目,所以我直接这样写了,如果是vue和react的话,应该直接import就好!这里做了预加载以及初始化,同时把刚才的js文件引入!
1、点击菜单添加tab时激活新的微前端,卸载旧的
注意1和4虽然场景不同,但是效果一样,我们怎么处理呢,代码如下所示:
<div class="border-left animated fadeInRight eee-bg-nano" style="border-left: 10px solid #e7eaec;" ng-controller="domainScenarioCtrl"
ng-init="init('fireManage')">
<div style="width: 100%; height: 100%;">
<div style="height: 100%; width: 100%;" id="fireManage">
</div>
</div>
</div>
我们以这个页面做示例,我们调用init方法来渲染微前端页面,sceneName就是id名,要一致,否则不生效!
$scope.init = function(sceneName) {
MicroAppHelper.handleMicroAppSwitchByUrl({
sessionStorage: $sessionStorage,
state: $state,
},{
url: sceneName,
alias: ''
},false);
})
这样不管是点击打开新的微前端还是刷新页面都会调用这个init方法,就能实时渲染微前端页面了!
2、tab来回切换时激活当前新的微前端,卸载旧的
注意:2、3、5我放在一起讲了,因为代码在一起。
因为我使用的是layui的tab组件,所以下面代码只有参考价值,毕竟大家应该都是用的新的UI技术了。
$scope.initContent = function () {
if (IBE.CONFIG.multiTab) {
$scope.showMultiTab = true;
let list = []
//监听tab变化,不管是增加、删除还是点击,一律添加hash值
layui.element.on('tab(contentab)', function (obj) {
const thisUrl = $(this).attr("data-url");
const thisAlias = $(this).attr('id');
const alias = thisAlias.split('_')
// 这个hash一定要加,否则打开新的tab不会被选中!
location.hash = thisUrl;
// 如果是已经打开过的,则直接激活
if ($rootScope.isMenuTrigger){
MicroAppHelper.handleMicroAppSwitchByUrl({
sessionStorage: $sessionStorage,
state: $state,
},{
url: thisUrl,
alias: alias.join('.')
},false);
}
})
// 监听删除tab事件
layui.element.on('tabDelete(contentab)', function (obj) {
try{
//获取删除后激活的tab元素
const $tabs = $(obj.elem).find('li'); // 剩余的 li
let newActiveIndex = obj.index - 1; // 删除前一个,通常就是新激活
if(newActiveIndex < 0) newActiveIndex = 0;
const $newActive = $($tabs[newActiveIndex]);
const thisAlias = $newActive.attr('id');
const thisUrl = $newActive.attr('data-url');
if (!thisAlias || !thisUrl) return
const alias = thisAlias.split('_')
// 解决关闭tab时,url切换不成功问题
location.hash = thisUrl;
MicroAppHelper.handleMicroAppSwitchByUrl({
sessionStorage: $sessionStorage,
state: $state,
},{
url: thisUrl,
alias: alias.join('.')
},true);
}catch(e){
}
})
$(document).off('click', '.layui-tab[lay-filter="contentab"] .layui-tab-title li');
// 使用自定义绑定和解绑click事件目的是为了防止事件被触发多次
$(document).on('click', '.layui-tab[lay-filter="contentab"] .layui-tab-title li', async function () {
const thisUrl = $(this).attr("data-url");
const thisAlias = $(this).attr("id");
const alias = thisAlias.split('_')
MicroAppHelper.handleMicroAppSwitchByUrl({
sessionStorage: $sessionStorage,
state: $state,
},{
url: thisUrl,
alias: alias.join('.')
},true);
});
}
}
上面一共三个事件tab(contentab)、tabDelete(contentab)和click,简单讲解下:
1、tab(contentab)事件:通过设置location.hash = thisUrl,将url改正确,并且通过isMenuTrigger来判断是否已经打开过也就是对应上面的第5条,如果是的话直接重新激活
2、tabDelete(contentab)事件:也一样激活新的,卸载旧的
3、click事件:和上面一样
这样就解决了所有场景的问题。
总结
1、因为我的主应用是angularjs和layui的tab所以需要处理的地方比较多
2、使用renderApp方式来动态加载微前端,不要使用micro-app标签
3、主子应用通过getData和setData来通信,注意name要匹配,否则不生效