Some thorny problems encountered in micro-frontend projects

Why Use Micro Frontends

  • There are many business management systems, and the technology stacks are vue3/vue2/react16/react hook

  • Administrators need to use multiple systems at the same time, but do not want to switch systems and log in again, the page will be refreshed, and a new browser tab needs to be opened

  • Some sub-applications need to support the subsidiary's business and need to be independently deployed and run.

  • For developers, if you need to implement some functions of application B in application A, such as popping up the pop-up window of application B on the page of application A, if there are two different frameworks of react and vue, rewrite the business logic code Obviously irrational.

So from a technical point of view, we need to use a parent architecture to integrate these sub-applications and integrate them into a unified platform. At the same time, sub-applications can also be deployed independently from the parent architecture.

Micro frontend architecture diagram

why ditch iframes

Browsing history cannot be automatically recorded, the browser refreshes, the state is lost, and the back and forward buttons cannot be used.

The nested sub-application pop-up window mask cannot cover the full-screen page communication, which is troublesome, so the postMessage method can only be used. 

Every time a sub-application enters, resources need to be requested again, and the page loading speed is slow.

To emphasize, it is not recommended to use micro-frontends for small-scale and small-scale scenarios.

List the problems encountered

  • The multi-tab switching operation will become more and more stuck after a long time

  • Dual application switching data cache

  • How can the same dock load two applications in parallel at the same time

  • After the sub-application is deployed, how to prompt business personnel to update the system

  • Performance optimization: how the parent application implements preloading and on-demand loading

The principle of qiankun

We finally chose the micro front-end solution  qiankun, which qiankunis based on single-spadevelopment. It mainly adopts HTML Entrythe pattern, directly prints out the HTML of the sub-application as the entry point, parses the html file of the sub-application through fetch html, and then obtains the static resources of the sub-application, and at the same time Insert the HTML document as a child node into the container of the main frame.

After the application is switched out/uninstalled, its style sheet can be uninstalled at the same time, and the browser will restructure the entire CSSOM for the insertion and removal of all style sheets, so as to achieve the purpose of inserting and uninstalling styles. In this way, it can be guaranteed that at a point in time, only one applied style sheet is in effect.

HTML Entry The solution is inherently style-isolated, because the HTML structure will be removed directly after the application is uninstalled, thereby automatically removing its style sheet.

When the sub-application is mounted, it will automatically do some special processing to ensure that all resources dom of the sub-application (including style tags added by js, etc.) are concentrated under the sub-application root node dom. When the sub-application is uninstalled, the corresponding entire dom is removed, thus avoiding style conflicts.

A js sandbox is provided. When a sub-app is mounted, it will proxy the global window object and hijack global event monitoring to ensure that global variables/events between micro-apps do not conflict.

By reading qiankunthe source code. Familiarize yourself with the execution flowqiankun of the code

Difficulties encountered in business solutions

Dual application switching data cache

Data cache switching between different systems, the same application can use keep-alive to cache pages, but when switching between different sub-applications, the sub-applications will be destroyed and the cache will become invalid

Multi-tab caching scheme

Code

Control the display and hiding of different sub-application dom through display:none;

 <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>

solution

Think, how to optimize rendering performance:

Each micro-application instance runs in a base, so how can we reuse as many sandboxes as possible, without unloading when the subsystem is switched, so that switching routes is fast

  1. Option One

Advantages of the solution: directly call the official website api  loadMicroApp, which is convenient and quick to switch without uninstalling sub-applications, and the tab switching speed is relatively fast. Insufficient solution: too many super administrator applications, and the DOM is not destroyed when sub-applications are switched, which will cause too many DOM nodes and event monitoring, resulting in page freezes; sub-applications are not uninstalled when switching, and routing event monitoring is not uninstalled. Change monitoring is handled specially. 2. Option 2

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

A bit: the amount of code is small, the sub-app is registered through registerMicroApps, and pre-loaded through the prefetch of start, but there is a problem that the sub-app will unmount when switching, resulting in data loss, resulting in the loss of previously filled form data & slow reopening speed

I took a look at the practice of the multi-page caching solution based on the micro-frontend qiankun: https://zhuanlan.zhihu.com/p/548520855 The implementation method of chapter 3.1, I feel that it is too complicated, and both react and vue need to be implemented at the same time program, the amount of code is relatively large.

At that time, I thought that it would be fine if the dom was not uninstalled when the micro-app was switched.

Option two optimization

After calling the start method, how can the sub-application switch without unloading the dom? After consulting the literature and reading the source code of the qiankun life cycle hook function, I finally found a solution

First modify the render() and unmount() methods of the subproject

Subproject modification

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")
    //     );
}

The same goes for the vue project

Then, the main application calls

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

Then patch-packagemodify qiankunthe source code with

patch-packageI won’t go into details about how to use it here, there are many on the Internet, and it’s easy to find

A total of five places have been modified, based on 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;

Multiple sub-applications are loaded in parallel, and sub-applications are nested

  1. Load two or more sub-applications in parallel on the same base

Multiple sub-apps can be loaded using loadMicroApp 

       2. How to solve the conflict/preemption problem caused by the coexistence of multiple routing systems?

let historyPath=window.location.pathname.startWith('/vue1/')?process.env.BASE_URL+'/vue1/':process.env.BASE_URL
const router = createRouter({
    history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? historyPath : process.env.BASE_URL),
    // history: createWebHashHistory(),
    routes: constantRoutes,
})

Calling startand at the same time will cause the sub-application to render twice. Although it has no effect on the page structure and style, the interface will be called twice, so you must uninstall the unnecessary sub-application loadMicroAppwhen jumping out of the sub-applicationloadMicroApp.unmount()

Encapsulation of qiankun code entry function

import type { MicroAppStateActions } from 'qiankun';
import QiankunBridge from '@/qiankun/qiankun-bridge'
import {
    initGlobalState,
    registerMicroApps,
    start,
    loadMicroApp,
    addGlobalUncaughtErrorHandler,
    runAfterFirstMounted
} from 'qiankun';
import { getAppStatus, unloadApplication } from 'single-spa';

export default class Qiankun {
    private actions: MicroAppStateActions | null = null
    private appMap: any = {}
    private prefetchAppMap: any = {}
    init() {
        this.registerApps()
        this.initialState()
        this.prefetchApp();
        this.errorHandler();
    }
    registerApps(){
        const parentRoute = useRouter();
        registerMicroApps([{
            name: 'demo-vue',
            entry: `${publicPath}/demo-vue`,
            container: `#demo-vue`,
            activeRule: `${publicPath}${'demo-vue'}`,
            props: {
                parentRoute,
                QiankunBridge:new QiankunBridge()
            }
        }, ...]);
    }
    initialState(){  
        const initialState = {};
        // 初始化 state
        this.actions = initGlobalState(initialState);
        this.actions.onGlobalStateChange((state, prev) => {
            // state: 变更后的状态; prev 变更前的状态
            console.log(state, prev);
        });
    }
    setState(state: any) {
        this.actions?.setGlobalState(state);
    }
    //预加载
    prefetchApp() {
        start({
            prefetch: 'all',
            singular: false,
        });
    }
    //按需加载
    demandLoading(apps){
       let installAppMap = {
          ...store.getters["tabs/installAppMap"],
       };
       if (!installAppMap[config.name]) {
          installAppMap[config.name] = loadMicroApp({
            ...config,
            configuration: {
                // singular: true
                sandbox: { experimentalStyleIsolation: true },
            },
            props: {
                getGlobalState: actions.getGlobalState,
                fn: {
                    parentRoute: useRouter(),
                    qiankunBridge: qiankunBridge,
                },
            },
        });
       }
    }
    /**
     * @description: 卸载app
     * @param {Object} app 卸载微应用name, entry
     * @returns false
     */
    async unloadApp(app) {
        // await clearCatchByUrl(getPrefetchAppList(addVisitedRoute, router)[0])
        const appStatus = getAppStatus('utcus');
        if (appStatus !== 'NOT_LOADED') {
            unloadApplication(app.name);
            // 调用unloadApplication时,Single-spa将执行以下步骤。

            // 在要卸载的注册应用程序上调用卸载生命周期。
            // 将应用程序状态设置为NOT_LOADED
            // 触发重新路由,在此期间,单spa可能会挂载刚刚卸载的应用程序。
            // 由于unloadApplication调用时可能会挂载已注册的应用程序,因此您可以指定是要立即卸载还是要等待直到不再挂载该应用程序。这是通过该waitForUnmount选项完成的。
        }
    }
    //重新加载微应用
    reloadApp(app) {
        this.unloadApp(app).then(() => {
            loadMicroApp(app);
        });
    }
    //加载单个app
    loadSingleApp(name) {
        if (!this.appMap[name]) {
            this.appMap[name] = loadMicroApp(this.prefetchAppMap[name]);
        }
    }
    // 切出单个app,和unloadApp用法不同unloadApp 是卸载start方法生成的应用,unmountSingleApp是卸载loadMicroApp方法生成的应用
    async unmountSingleApp(name) {
        if (this.appMap[name]) {
            await this.appMap[name].unmount();
            this.appMap[name] = null;
        }
    }
    //错误处理
    errorHandler() {
        addGlobalUncaughtErrorHandler((event: any) => {
            console.log('addGlobalUncaughtErrorHandler', event);
            if (
                event?.message &&
                event?.message.includes('died in status LOADING_SOURCE_CODE')
            ) {
                Message('子应用加载失败,请检查应用是否运行', 'error', false);
            }
            //子应用发版更新后,原有的js会找不到,所以会报错
            if (event?.message && event?.message.includes("Unexpected token '<'")) {
                Message('检测到项目更新,请刷新页面', 'error', false);
            }
        });
    }
}

Application Event Communication

Application scenario: sub-application a calls the event of sub-application b

const isDuplicate = function isDuplicate(keys: string[], key: string) {
    return keys.includes(key);
};
export default class QiankunBridge {
    private handlerMap: any = {}
    // 单例判断
    static hasInstance = () => !!(window as any).$qiankunBridge
    constructor() {
        if (!QiankunBridge.hasInstance()) {
            ; (window as any).$qiankunBridge = this;
        } else {
            return (window as any).$qiankunBridge;
        }
    }
    //注册
    registerHandlers(handlers: any) {
        const registeredHandlerKeys = Object.keys(this.handlerMap);
        Object.keys(handlers).forEach((key) => {
            const handler = handlers[key];
            if (isDuplicate(registeredHandlerKeys, key)) {
                console.warn(`注册失败,事件 '${key}' 注册已注册`);
            } else {
                this.handlerMap = {
                    ...this.handlerMap,
                    [key]: {
                        key,
                        handler,
                    },
                };
                console.log(`事件 '${key}' 注册成功`);
            }
        });
        return true;
    }
    removeHandlers(handlerKeys: string[]) {
        handlerKeys.forEach((key) => {
            delete this.handlerMap[key];
        });
        return true;
    }
    // 获取某个事件
    getHandler(key: string) {
        const target = this.handlerMap[key];
        const errMsg = `事件 '${key}' 没注册过`;
        if (!target) {
            console.error(errMsg);
        }
        return (
            (target && target.handler) ||
            (() => {
                console.error(errMsg);
            })
        );
    }
}

sub-application a registration

import React from "react";
export async function mount(props) {
    if (!instance) {
        React.$qiankunBridge = props.qiankunBridge;
        render(props);
    }
}

React.$qiankunBridge &&
React.$qiankunBridge.registerHandlers({
    event1: event1Fn
});

Sub-application b calls

Vue.$qiankunBridge.getHandler('event1')

Project deployment, prompting users to update the system

 The state of  the plug-in serviceWorker is judged when  it is registered   , so as to determine   whether there is a new version, and then perform the corresponding update operation, that is, pop-up window prompts; one disadvantage is that the project may need to frequently release versions to correct some bugs, resulting in frequent update pop-up windows. So it can only be discarded.registrationwaitingserviceWorker

The simpler solution for the main project is to write the version number data into the cookie, generate a json file through webpack and deploy it to the server, and the front-end code requests the version number of the json file for comparison. If it is not the latest version, the pop-up window will pop up. .

If the sub-project is updated, the js and css resources will be recompiled to generate a new link, so the request cannot be found, and if the  addGlobalUncaughtErrorHandlerresource request error is detected, just prompt the update pop-up window directly

addGlobalUncaughtErrorHandler((event: any) => {
    //子应用发版更新后,原有的文件会找不到,所以会报错
    if (event?.message && event?.message.includes("Unexpected token '<'")) {
        Message('检测到项目更新,请刷新页面', 'error', false);
    }
});

The re-rendering of the main application component causes the dom of the sub-application to disappear

The scene where the problem occurs: the ipad mobile terminal switches to widescreen, the layout changes, and the re-rendering of the vue component causes the dom in the microservice to disappear.

Solution: Use single-spathe unloadApplicationmethod to unload the sub-application, and use qiankunthe loadMicroAppmethod to reload the sub-application.

reloadAppSee the method above for details 

After the application is switched, the original sub-application route change monitoring becomes invalid

function render(props = {}) {
  const { container } = props
  router=Router() //添加此行代码,react子应用清空下dom重新调用下ReactDOM.render即可
  instance = createApp(App)
  instance
    .use(router)
    .mount(
      container
        ? container.querySelector('#app-vue')
        : '#app-vue'
    )
}

 

Guess you like

Origin blog.csdn.net/hyupeng1006/article/details/129560371