微前端-解决MicroApp微前端内存泄露问题
前言
之前使用京东微前端框架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,sessionStoragehandleMicroAppSwitchByUrl: 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'); // 剩余的 lilet 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) returnconst 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要匹配,否则不生效
引用
micro-app官方文档