前言
之前使用京東微前端框架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官方文檔