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

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

为什么要用微前端


  • 业务管理系统多,技术栈分别为 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;

目录
相关文章
|
2月前
|
前端开发 JavaScript 定位技术
一、前端高德地图注册、项目中引入、渲染标记(Marker)and覆盖物(Circle)
文章介绍了如何在前端项目中注册并使用高德地图API,包括注册高德开放平台账号、引入高德地图到项目、以及如何在地图上渲染标记(Marker)和覆盖物(Circle)。
81 1
|
23天前
|
JavaScript 前端开发 Docker
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
在使用 Deno 构建项目时,生成的可执行文件体积较大,通常接近 100 MB,而 Node.js 构建的项目体积则要小得多。这是由于 Deno 包含了完整的 V8 引擎和运行时,使其能够在目标设备上独立运行,无需额外安装依赖。尽管体积较大,但 Deno 提供了更好的安全性和部署便利性。通过裁剪功能、使用压缩工具等方法,可以优化可执行文件的体积。
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
|
12天前
|
前端开发 Unix 测试技术
揭秘!前端大牛们如何高效管理项目,确保按时交付高质量作品!
【10月更文挑战第30天】前端开发项目涉及从需求分析到最终交付的多个环节。本文解答了如何制定合理项目计划、提高团队协作效率、确保代码质量和应对项目风险等问题,帮助你学习前端大牛们的项目管理技巧,确保按时交付高质量的作品。
26 2
|
1月前
|
前端开发 JavaScript 应用服务中间件
linux安装nginx和前端部署vue项目(实际测试react项目也可以)
本文是一篇详细的教程,介绍了如何在Linux系统上安装和配置nginx,以及如何将打包好的前端项目(如Vue或React)上传和部署到服务器上,包括了常见的错误处理方法。
267 0
linux安装nginx和前端部署vue项目(实际测试react项目也可以)
|
24天前
|
缓存 前端开发 JavaScript
前端架构思考:代码复用带来的隐形耦合,可能让大模型造轮子是更好的选择-从 CDN 依赖包被删导致个站打不开到数年前因11 行代码导致上千项目崩溃谈谈npm黑洞 - 统计下你的项目有多少个依赖吧!
最近,我的个人网站因免费CDN上的Vue.js包路径变更导致无法访问,引发了我对前端依赖管理的深刻反思。文章探讨了NPM依赖陷阱、开源库所有权与维护压力、NPM生态问题,并提出减少不必要的依赖、重视模块设计等建议,以提升前端项目的稳定性和可控性。通过“left_pad”事件及个人经历,强调了依赖管理的重要性和让大模型代替人造轮子的潜在收益
|
30天前
|
前端开发 JavaScript 开发工具
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(三)
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(三)
33 0
|
30天前
|
Web App开发 前端开发 JavaScript
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(二)
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(二)
47 0
|
30天前
|
Web App开发 移动开发 前端开发
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(一)
前端代码规范和质量是确保项目可维护性、可读性和可扩展性的关键(一)
45 0
|
1月前
|
前端开发 API 开发者
🥇前端宝藏:多项目掌握技能的冒险之旅🏆
在前端开发的学习旅程中,实践是提升技能的关键。本文介绍了多个前端项目,包括计算器、天气应用、经典游戏等,涵盖了从React到Svelte的各种技术栈。每个项目都附有在线演示和源代码,旨在帮助读者深入理解实现细节,激励更多人参与实际项目开发。通过这些项目,读者可以将理论知识转化为实践,拓展职业机会。
17 0
|
1月前
|
前端开发 JavaScript
轻松上手:基于single-spa构建qiankun微前端项目完整教程
轻松上手:基于single-spa构建qiankun微前端项目完整教程
39 0