微前端-解决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,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要匹配,否则不生效

引用

micro-app官方文档

相关推荐
As331001019 分钟前
Chrome 插件开发实战:打造高效浏览器扩展
前端·chrome
xrkhy24 分钟前
nvm安装详细教程(卸载旧的nodejs,安装nvm、node、npm、cnpm、yarn及环境变量配置)
前端·npm·node.js
德育处主任2 小时前
p5.js 3D盒子的基础用法
前端·数据可视化·canvas
前端的阶梯2 小时前
为何我的figma-developer-mcp不可用?
前端
weixin_456904272 小时前
Vue3入口文件main.js解析
前端·javascript·vue.js
前端领航者2 小时前
重学Vue3《Vue Watch 监听器深度指南:场景、技巧与底层优化原理剖析》
前端·vue.js
布列瑟农的星空2 小时前
34岁老前端的一周学习总结(2025/8/15)
前端·后端
豆苗学前端2 小时前
vue3+TypeScript 实现一个图片占位符生成器
前端·面试·github
neon12042 小时前
Vue 3 父子组件通信核心机制详解:defineProps、defineEmits 与 defineExpose 完全指南
前端·javascript·vue.js·前端框架