微前端项目难点解决(一)

简介: 微前端项目难点解决

为什么要用微前端


  • 业务管理系统多,技术栈分别为 vue3/vue2/react16/react hook
  • 管理人员需要同时使用多系统,但是又不想切换系统重新登陆,页面会刷新,需要新开浏览器tab
  • 部分子应用需要支持子公司的业务,需要独立部署运行。
  • 对于开发者来说,如果需要在应用 A 实现应用B的某些功能,例如在应用A的页面弹出应用B的弹窗,如果是react、vue两种不同的框架的话,重新写一遍业务逻辑代码很明显是不理智的。

所以从技术角度来看,我们需要用一个父架构来集成这些子应用,把它们整合到统一平台上,同时子应用也可以脱离父架构独立部署运行。

微前端架构图

e8cfe2023341d5bdc2eb443e19e384de_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpg


为什么放弃iframe


浏览记录无法自动被记录,浏览器刷新,状态丢失、后退前进按钮无法使用。

嵌套子应用弹窗蒙层无法覆盖全屏 页面通信比较麻烦,只能采用postMessage方式。

每次子应用进入都需要重新请求资源,页面加载速度慢。


强调一下,目规模小、数量少的场景其实不建议使用微前端。


罗列一下碰到的问题


  • 多tab切换操作久了会越来越卡
  • 双应用切换数据缓存
  • 同一个基座如何同时并行加载两个应用
  • 子应用部署后,如何提示业务人员更新系统
  • 性能优化:父应用如何实现预加载和按需加载


qiankun 实现原理


微前端方案中我们最终选择了 qiankunqiankun是基于single-spa开发,它主要采用HTML Entry模式,直接将子应用打出来 HTML作为入口,通过 fetch html 的方式,解析子应用的html文件,然后获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。

应用切出/卸载后,同时卸载掉其样式表即可,浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样即能保证,在一个时间点里,只有一个应用的样式表是生效的。

HTML Entry 方案则天生具备样式隔离的特性,因为应用卸载后会直接移除去 HTML 结构,从而自动移除了其样式表。

子应用挂载时,会自动做一些特殊处理,可以确保子应用所有的资源dom(包括js添加的style标签等)都集中在子应用根节点dom下。子应用卸载时,对应的整个dom都移除了,这样也就避免了样式冲突。

提供了js沙箱,子应用挂载时,会对全局window对象代理、对全局事件监听进行劫持等,确保微应用之间 全局变量/事件 不冲突。

通过阅读qiankun源码。熟悉一下qiankun代码的执行流程

17cb1bce9baa476cf79ddf1566b3373f_640_wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1.jpg


业务中碰到的难点解决


双应用切换数据缓存


不同系统之间切换数据缓存问题,同一个应用可以使用 keep-alive 去缓存页面,但是不同子应用之间切换的时候,会导致子应用被销毁,缓存失效


多开tab缓存方案


代码实现


通过display:none;控制不同子应用dom的显示隐藏

<template>
  <div id="app">
  <header>
    <router-link to="/app1/">app1</router-link>
    <router-link to="/app2/">app2</router-link>
  </header>
  <div id="appContainer1" v-show="$route.path.startsWith('/app1/')"></div>
  <div id="appContainer2" v-show="$route.path.startsWith('/app2/')"></div>
  <router-view></router-view>
</div>
</template>


解决方案


思考, 如何优化渲染性能:

每一个微应用实例都是运行在一个基座里,那我们如何尽可能多的复用沙箱,子系统切换时候不卸载,这样切换路由就快了

  1. 方案一

方案优势:直接调用官网api loadMicroApp,方便快捷 切换的时候不卸载子应用,tab切换速度比较快。方案不足:超级管理员应用太多,子应用切换时不销毁DOM,会导致DOM节点和事件监听过多,造成页面卡顿;子应用切换时未卸载,路由事件监听也未卸载,需要对路由变化的监听做特殊的处理。                   2. 方案二

start({
    prefetch: 'all',
    singular: false,
})

有点:代码量少,通过registerMicroApps注册子应用,通过start的prefetch预加载, 但是有个问题就是子应用在切换的时候会unmount,导致数据丢失,导致之前填写的表单数据丢失&重新打开速度也慢

看了一下 基于微前端qiankun的多页签缓存方案实践:https://zhuanlan.zhihu.com/p/548520855 3.1章节的实现方法,我感觉太复杂了,而且还需要同时实现react和vue两种方案,代码量也比较大。

当时就想着要是微应用切换的时候不卸载dom就好了。


方案二优化


调用了start方法后,子应用切换怎么才能不卸载dom呢 通过查阅文献以及阅读qiankun生命周期钩子函数的源码,最终找到了解决方案

首先修改子项目的render()和unmount()方法

子项目修改

let instance
export async function render() {
  if(!instance){
     instance = ReactDOM.render(
        app,
        container
            ? container.querySelector("#root")
            : document.querySelector("#root")
    );ount('#app1History');
  }
}
export async function unmount(props) { 
    //     const { container } = props;
    //     ReactDOM.unmountComponentAtNode(
    //         container
    //             ? container.querySelector("#root")
    //             : document.querySelector("#root")
    //     );
}

vue项目同理

然后,主应用调用

start({
    prefetch: 'all',
    singular: false,
})

然后借助patch-package修改qiankun源码

patch-package的使用方法这里就不赘述了,网上有很多,很容易搜到

总共修改五处地方,基于qiankun2.9.1

diff --git a/node_modules/qiankun/es/loader.js b/node_modules/qiankun/es/loader.js
index 6f48575..285af0e 100644
--- a/node_modules/qiankun/es/loader.js
+++ b/node_modules/qiankun/es/loader.js
@@ -286,11 +286,14 @@ function _loadApp() {
           legacyRender = 'render' in app ? app.render : undefined;
           render = getRender(appInstanceId, appContent, legacyRender); // 第一次加载设置应用可见区域 dom 结构
           // 确保每次应用加载前容器 dom 结构已经设置完毕
-          render({
-            element: initialAppWrapperElement,
-            loading: true,
-            container: initialContainer
-          }, 'loading');
+          console.log("qiankun-loader--loading", getContainer(initialContainer).firstChild)
+          if (!getContainer(initialContainer).firstChild) {
+            render({
+              element: initialAppWrapperElement,
+              loading: true,
+              container: initialContainer
+            }, 'loading');
+          }
           initialAppWrapperGetter = getAppWrapperGetter(appInstanceId, !!legacyRender, strictStyleIsolation, scopedCSS, function () {
             return initialAppWrapperElement;
           });
@@ -305,8 +308,8 @@ function _loadApp() {
           speedySandbox = _typeof(sandbox) === 'object' ? sandbox.speedy !== false : true;
           if (sandbox) {
             sandboxContainer = createSandboxContainer(appInstanceId,
-            // FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
-            initialAppWrapperGetter, scopedCSS, useLooseSandbox, excludeAssetFilter, global, speedySandbox);
+              // FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
+              initialAppWrapperGetter, scopedCSS, useLooseSandbox, excludeAssetFilter, global, speedySandbox);
             // 用沙箱的代理对象作为接下来使用的全局对象
             global = sandboxContainer.instance.proxy;
             mountSandbox = sandboxContainer.mount;
@@ -409,11 +412,18 @@ function _loadApp() {
                         appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);
                         syncAppWrapperElement2Sandbox(appWrapperElement);
                       }
-                      render({
-                        element: appWrapperElement,
-                        loading: true,
-                        container: remountContainer
-                      }, 'mounting');
+                      //修改2
+                      if (!getContainer(remountContainer).firstChild) {
+                        render({
+                          element: appWrapperElement,
+                          loading: true,
+                          container: remountContainer
+                        }, 'mounting');
+                      }
                     case 3:
                     case "end":
                       return _context5.stop();
@@ -458,11 +468,18 @@ function _loadApp() {
                 return _regeneratorRuntime.wrap(function _callee8$(_context8) {
                   while (1) switch (_context8.prev = _context8.next) {
                     case 0:
-                      return _context8.abrupt("return", render({
-                        element: appWrapperElement,
-                        loading: false,
-                        container: remountContainer
-                      }, 'mounted'));
+                      return _context8.abrupt("return", () => {
+                        console.log(initialContainer, remountContainer)
+                        //修改3
+                        console.log("qiankun-loader-mounted", getContainer(initialContainer).firstChild)
+                        if (!getContainer(remountContainer).firstChild) {
+                          render({
+                            element: appWrapperElement,
+                            loading: false,
+                            container: remountContainer
+                          }, 'mounted')
+                        }
+                      });
                     case 1:
                     case "end":
                       return _context8.stop();
@@ -554,15 +571,17 @@ function _loadApp() {
                 return _regeneratorRuntime.wrap(function _callee15$(_context15) {
                   while (1) switch (_context15.prev = _context15.next) {
                     case 0:
-                      render({
-                        element: null,
-                        loading: false,
-                        container: remountContainer
-                      }, 'unmounted');
-                      offGlobalStateChange(appInstanceId);
-                      // for gc
-                      appWrapperElement = null;
-                      syncAppWrapperElement2Sandbox(appWrapperElement);
+                      //修改4
+                      console.log('qiankun-loader-unmounted')
+                    // render({
+                    //   element: null,
+                    //   loading: false,
+                    //   container: remountContainer
+                    // }, 'unmounted');
+                    // offGlobalStateChange(appInstanceId);
+                    // // for gc
+                    // appWrapperElement = null;
+                    // syncAppWrapperElement2Sandbox(appWrapperElement);
                     case 4:
                     case "end":
                       return _context15.stop();
diff --git a/node_modules/qiankun/es/sandbox/patchers/dynamicAppend/forStrictSandbox.js b/node_modules/qiankun/es/sandbox/patchers/dynamicAppend/forStrictSandbox.js
index 724a276..1dd3da1 100644
--- a/node_modules/qiankun/es/sandbox/patchers/dynamicAppend/forStrictSandbox.js
+++ b/node_modules/qiankun/es/sandbox/patchers/dynamicAppend/forStrictSandbox.js
@@ -91,8 +91,9 @@ export function patchStrictSandbox(appName, appWrapperGetter, proxy) {
       rebuildCSSRules(dynamicStyleSheetElements, function (stylesheetElement) {
         var appWrapper = appWrapperGetter();
         if (!appWrapper.contains(stylesheetElement)) {
-          var mountDom = stylesheetElement[styleElementTargetSymbol] === 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
-          rawHeadAppendChild.call(mountDom, stylesheetElement);
+          console.log("qiankun-forStrictSandbox")
+          // var mountDom = stylesheetElement[styleElementTargetSymbol] === 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
+          // rawHeadAppendChild.call(mountDom, stylesheetElement);
           return true;
         }
         return false;

目录
相关文章
|
5天前
|
前端开发 Java 编译器
【前端学java】java基础练习缺少项目?看这篇文章就够了!(完结)
【8月更文挑战第11天】java基础练习缺少项目?看这篇文章就够了!(完结)
14 0
|
4天前
|
前端开发 JavaScript
在 Vue3 + ElementPlus 项目中使用 computed 实现前端静态分页
本文介绍了在Vue3 + ElementPlus项目中使用`computed`属性实现前端静态分页的方法,并提供了详细的示例代码和运行效果。
19 1
在 Vue3 + ElementPlus 项目中使用 computed 实现前端静态分页
|
4天前
|
前端开发 数据库
SpringBoot+Vue+token实现(表单+图片)上传、图片地址保存到数据库。上传图片保存位置到项目中的静态资源下、图片可以在前端回显(二))
这篇文章是关于如何在SpringBoot+Vue+token的环境下实现表单和图片上传的优化篇,主要改进是将图片保存位置从磁盘指定位置改为项目中的静态资源目录,使得图片资源可以跨环境访问,并在前端正确回显。
|
5天前
|
前端开发 Java 编译器
【前端学java】java基础练习缺少项目?看这篇文章就够了!(17)
【8月更文挑战第11天】java基础练习缺少项目?看这篇文章就够了!
11 0
【前端学java】java基础练习缺少项目?看这篇文章就够了!(17)
|
16天前
|
数据采集 资源调度 JavaScript
Node.js 适合做高并发、I/O密集型项目、轻量级实时应用、前端构建工具、命令行工具以及网络爬虫和数据处理等项目
【8月更文挑战第4天】Node.js 适合做高并发、I/O密集型项目、轻量级实时应用、前端构建工具、命令行工具以及网络爬虫和数据处理等项目
30 5
|
19天前
|
Web App开发 开发框架 编解码
在基于ABP框架的前端项目Vue&Element项目中采用电子签章处理文件和打印处理
在基于ABP框架的前端项目Vue&Element项目中采用电子签章处理文件和打印处理
|
19天前
|
开发框架 前端开发 JavaScript
在基于ABP框架的前端项目Vue&Element项目中采用日期格式处理,对比Moment.js和day.js的处理
在基于ABP框架的前端项目Vue&Element项目中采用日期格式处理,对比Moment.js和day.js的处理
|
19天前
|
开发框架 前端开发 JavaScript
在基于ABP框架的前端项目Vue&Element项目中采用电子签名的处理
在基于ABP框架的前端项目Vue&Element项目中采用电子签名的处理
|
1月前
|
前端开发 JavaScript 安全
前端技术栈都有那些,需要学会啥才可以上手写项目?
【7月更文挑战第9天】 前端技术栈包括HTML/CSS/JS基础,熟悉Vue.js/React/Angular等框架,掌握Git、Webpack等工具,理解HTTP协议及安全概念。使用Node.js和编辑器提升效率,从基础到框架层层深入,实践项目以巩固知识,持续学习应对技术更新。
34 0
|
1月前
|
前端开发 测试技术 API
前端必备的【项目知识】
前端必备的【项目知识】
25 0