Micro front-end qiankun usage + pitfalls

background

Background of project transformation using qiankun:

Project A, Project B, Project C;

Project A and Project B have clear service boundaries and can be divided into two projects from the perspective of service type.

In the context of company project integration, all projects should be one project.

When the research and development of project B is started
: 1. Due to the tight development time;
2. Project B needs to share the "project module" and "personnel management" module in project A;
3. The functional modules in project B are activated and loaded according to the routing of project A. ;

Based on the above situation, we added modules to project A to develop project B.

Since project B is included in project A, when project A and project B start requirements iteration at the same time, it is a disaster when two developers start to merge the code. It takes a lot of time to do this work carefully in order not to use it
. Project A becomes a monolith application, and project A needs to be deconstructed.
The micro-front-end architecture aims to solve the problem of a single application evolving from an ordinary application to a frontend monolith application over a relatively long time span due to the increase and changes in the number of people and teams involved. Unmaintainable problem.

Why doesn't our project use iframe?

iframe is a sub-application system embedded in the main application. iframe provides a perfect isolation environment, perfect style isolation and js isolation for the sub-application.

Problems caused by iframe:
1. iframe has an independent window. Independent browser cache (single sign-in cannot be conveniently implemented. For example: the main application stores the token in sessionStorage after logging in, and the sub-application cannot directly obtain the token. )
2. The iframe url status will be lost after refreshing .
3. iframe will block the loading of the page and affect the loading speed of the web page . For example, the window's onload event will be executed immediately after the page or image is loaded. However, if an iframe is used in the current page, it will not be executed until all the elements in the iframe are loaded. This will make the user feel that the webpage is loading. The speed is extremely slow, which affects the experience.

webpack5 module federation

Different from qiankun's base application, module federation is deneutral and two projects can refer to each other.
Configure the ModuleFederationPlugin
plug-in in webpack.config.js. Module federation can actually be regarded as webpack5 packaging the components that need to be exported into a runtime file , and then dynamically loading other projects at runtime . The loading process is asynchronous. Yes, it is synchronous when executed. Based on this feature, we can implement a centralized component module center and then distribute the modules externally.

Why choose qiankun

  1. The technology stack has nothing to do with it.

  2. The main application and micro-applications can run independently and be developed independently.

  3. The micro-applications and the main application are isolated where they should be, and what should not be isolated are shared. (Browser cache can be shared)

When encountering a project situation like ours:

    主应用与微应用基地相同,意味着,两个项目的缓存操作方法相同,vuex store方法相同时,应该首选采取qiankun 接入微应用的方式。

    由于qinkun 没有隔离浏览器缓存,因此,可以不用考虑子应用的登录问题,菜单栏tab 的显示问题。

Simply silkier than Dove! !

What is qiankun?

First of all, qiankun is not a single framework. It adds more functions based on single-spa . The following are the features provided by qiankun:

Realizes the loading of sub-applications, and provides HTML Entry
style and JS isolation based on the original single-spa JS Entry
. More life cycles: beforeMount, afterMount, beforeUnmount, afterUnmount. Sub-
application preloading,
global state management,
and global error handling.
Insert image description here

How to use qiankun

First of all, you need a base to host various cross-technology stack sub-applications. Here we take vue as an example.

Main application configuration

First write a registerApps.js under src

import {
    
     registerMicroApps, start } from "qiankun"; // 底层是基于single-spa

const loader = (loading) => {
    
    
  console.log(loading);
};
registerMicroApps(
  [
    {
    
    
      name: "m-vue",//package.json的name
      entry: "//localhost:20000",//项目起的端口号
      container: "#container",
      activeRule: "/vue",
      loader,
    },
    {
    
    
      name: "reactApp",
      entry: "//localhost:30000",
      container: "#container",
      activeRule: "/react",
      loader,
    },
  ],
  {
    
    
    beforeLoad: () => {
    
    
      console.log("加载前");
    },
    beforeMount: () => {
    
    
      console.log("挂在前");
    },
    afterMount: () => {
    
    
      console.log("挂载后");
    },
    beforeUnmount: () => {
    
    
      console.log("销毁前");
    },
    afterUnmount: () => {
    
    
      console.log("销毁后");
    },
  }
);
start({
    
    
  sandbox: {
    
    
    // experimentalStyleIsolation:true
    strictStyleIsolation: true,
  },
});


Introduce import './registerApps' in main.js

Sub-application configuration

vue micro application
vue.config.js

module.exports = {
    
    
    publicPath: '//localhost:20000', //保证子应用静态资源都是像20000端口上发送的
    devServer: {
    
    
        port: 20000, // fetch
        headers:{
    
    
            'Access-Control-Allow-Origin': '*'
        }
    },
    configureWebpack: {
    
     // 需要获取我打包的内容  systemjs=》 umd格式
        output: {
    
    
            libraryTarget: 'umd',
            library: 'm-vue'// window['m-vue']
        }
    }
}

// 3000 -> 20000 基座回去找20000端口中的资源,  publicPath  /

The router's index.js
exports the routing table, not the router.

const routes = [
  {
    
    
    path: '/',
    name: 'Home',
    component: () => import( '../views/Home.vue')
  },
  {
    
    
    path: '/about',
    name: 'About',
    component: () => import( '../views/About.vue')
  }
]

export default routes

main.js

import {
    
     createApp } from 'vue'
import {
    
     createRouter, createWebHistory } from 'vue-router';
import App from './App.vue'
import routes from './router'

// 不能直接挂载 需要切换的时候 调用mount方法时候再去挂载
let history;
let router;
let app;
function render(props = {
     
     }){
    
    
    history =  createWebHistory('/vue');//加上路由前缀  
    router = createRouter({
    
    
        history,
        routes
    });
    app = createApp(App);
    let {
    
    container} = props; // 默认他会拿20000端口的html插入到容器中,
    //没传container,就是自己跑起来的,传了代表是在基座中跑的
    app.use(router).mount(container ? container.querySelector('#app'):'#app')
}

// 乾坤在渲染前 给我提供了一个变量 window.__POWERED_BY_QIANKUN__

if(!window.__POWERED_BY_QIANKUN__){
    
     // 独立运行自己
render();
console.log(window.__POWERED_BY_QIANKUN__)
}

// 需要暴露接入协议,返回需要是promise,所以加上async
export async function bootstrap(){
    
    
    console.log('vue3 app bootstraped');
}

export async function mount(props){
    
    
    console.log('vue3 app mount',);
     render(props)
}
export async function unmount(){
    
    
    console.log('vue3 app unmount');
    history = null;
    app = null;
    router = null;
}

The scripts of react micro-application
.rescriptsrc.js
in package.json must also be modified because of the use of .rescriptsrc

module.exports = {
    
    
    webpack:(config)=>{
    
    
      config.output.library = 'm-react';  
      config.output.libraryTarget = 'umd';
      config.output.publicPath = '//localhost:30000/';
      return config
    },
    devServer:(config)=>{
    
    
        config.headers = {
    
    
            'Access-Control-Allow-Origin':'*'
        };

        return config
    }
}

.env
PORT=30000
WDS_SOCKET_PORT=30000
index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

reportWebVitals();

function render(props = {
     
     }) {
    
    
  let {
    
     container } = props;
  ReactDOM.render(<App />,
    container ? container.querySelector('#root') : document.getElementById('root')
  );
}
if (!window.__POWERED_BY_QIANKUN__) {
    
    
  render();
}

export async function bootstrap() {
    
    

}

export async function mount(props) {
    
    
  render(props)
}

export async function unmount(props) {
    
    
  const {
    
     container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.getElementById('root'))
}

How to implement style isolation in qinakun?

How to handle styles in qiankun?
Sub-applications and sub-applications will use dynamic style sheets to add styles when loading and uninstall styles when deleting (style isolation between sub-applications) How to
isolate the main application and sub-applications (we can use BEM specifications) -> (css-modules) Dynamically generate a prefix (not completely isolated)
because there is a sandbox in the start() function. Shadow dom is used to resolve style conflicts. Shadow dom is an isolated environment. It will put all the content of the sub-application inside shadow dom. The styles in shadow dom will not affect external styles.

What is shadowdom?

Through shadow DOM, a complete DOM tree can be added as a node to the parent DOM tree.
That is, DOM encapsulation can be achieved, which means that CSS styles and CSS selectors can be restricted to the shadow DOM subtree, rather than acting on the entire top-level DOM tree.

Create shadow DOM

The shadow DOM is created and added to a valid HTML element via the attachShadow() method:

Shadow host** (shadow host)**, that is, the element that accommodates the shadow DOM.
Shadow root ( shadow root) , that is, the root node of the shadow DOM.
The attachShadow() method requires a shadowRootInit object, that is, this object must contain a mode attribute with a value of "open" or "closed"
mode The reference to the shadow DOM with the attribute value "open" can be obtained on the HTML element through the shadowRoot attribute. The reference to the shadow DOM with the attribute value "closed" cannot be obtained.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      div {
    
    
        color: blue !important;
      }
    </style>
  </head>

  <body>
    <div>hello world</div>
    <script>
      const appContent = `<div id="qiankun">
           <div>hello world</div>
           <style>div{color:red}</style>
           </div>`; // 好比qiankun中获取的html,我们拿到后包裹了一层
      const containerElement = document.createElement("div");
      console.log(containerElement);
      containerElement.innerHTML = appContent;
      const appElement = containerElement.firstChild; // 拿出第一个儿子,中的内 容
      const {
    
     innerHTML } = appElement;
      appElement.innerHTML = "";
      let shadow = appElement.attachShadow({
    
     mode: "open" }); // 将父容器变为 shadowDOM
      shadow.innerHTML = innerHTML; // 将内容插入到shadowDOM中
      document.body.appendChild(appElement);
    </script>
  </body>
</html>

[Shadow DOM has the highest priority]

Under normal circumstances, as soon as the shadow DOM is added to an element, the browser will give it the highest priority and render its content first instead of the original DOM content, such as the following example:

document.body.innerHTML = `
    <div id="foo">
      <h1>I'm foo's child</h1>
    </div>
`;
const foo = document.querySelector('#foo');
const openShadowDOM = foo.attachShadow({
    
    
  mode: 'open'
});
// 为影子 DOM 添加内容
openShadowDOM.innerHTML = `
  <p>this is openShadowDOM content</p>
`

Insert image description here

qiankun’s js isolation solution?

There are three main types of js sandbox isolation, snapshot sandbox (snapshot sandbox), Proxy sandbox (proxy sandbox), and lagacySandBox (legacy sandbox).

sanpshotsandbox snapshot sandbox

The principle is to implement the sandbox by recording/restoring the status in the form of snapshots when the sub-application is activated/uninstalled. To sum up, make a diff of the current window and shallow copy snapshot to implement the sandbox.
However, the obvious disadvantage of the snapshot sandbox is that it needs to traverse the window every time it is switched, which is very time-consuming.

legacysandbox legacy sandbox

Directly record the diff when the micro-application modifies window.xxx and use it for environment recovery

Proxy sandbox proxy sandbox

In order to prevent the real window from being polluted, qiankun implements proxysandbox. The idea is
to copy some of the native attributes of the current window (such as document, location, etc.) and place them on a separate object. This object is also called fakewindow. Then, each micro-application is assigned
a fakewindow
when the micro-application modifies global variables. When:
If it is a native attribute, modify the global window.
If it is not a native attribute, modify the content in the fake window.
When the micro application obtains the global variable:
If it is a native attribute, it is taken from the window.
If it is not a native attribute, it is taken from the fake window first. In
this way, you don't even need to restore the environment, because each micro-application has its own environment. When it is active, a fake window is assigned to the micro-application, and when it is inactive, the fake window is saved for later use. .

What are some ideas for implementing a sandbox?

Method:
with and eval
proxy+with
iframe

qiankun-zi uses loading

The main steps of the Html Entry method are as follows: first obtain the entire Html file through the url, parse the html, js and css addresses from the html, create a container in the base, update the html to the container, and then dynamically create the style and script tag, assign the css and js of the child application to it, and finally place the container in the base.

JS Entry

The concept of JS Entry is used when loading micro-applications. When using single-spa to load micro-applications, what we load is not the micro-application itself, but the JS file exported by the micro-application, and an entry file will be exported. Object. This object has bootstrap, mount, and unmount, which are three life cycle methods that must be provided by the single-spa framework. The mount method specifies how the micro application should be mounted to the container node provided by the main application. Of course, you have to Accessing a micro application requires a series of modifications to the micro application. However, the problem with JS Entry lies here. The modification is too intrusive to the micro application, and the coupling with the main application is too strong.
Disadvantage 2: When the entire micro-application is packaged into a JS file, common packaging optimizations are basically gone, such as on-demand loading, first-screen resource loading optimization, CSS independent packaging and other optimization measures.

Html Entry principle

HTML Entry is implemented by the import-html-entry library. It loads the first screen content of the specified address, that is, the html page, through http requests, and then parses the html template to obtain template, scripts, entry, styles.

{
    
    
  template: 经过处理的脚本,link、script 标签都被注释掉了,
  scripts: [脚本的http地址 或者 {
    
     async: true, src: xx } 或者 代码块],
  styles: [样式的http地址],
    entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最后一个 script 标签的 src
}

Then remotely load the style content in styles, and replace the commented out link tags in the template with the corresponding style elements.
Then expose a Promise object to the outside.
From the above reading, we know that HTML Entry will eventually return a Promise object. Qiankun uses the template, assetPublicPath and execScripts in this object to add the template to the main application through DOM operations and execute The execScripts method obtains the life cycle methods exported by the micro-application, and also solves the problem of global pollution of JS, because the execution context of JS can be specified through the proxy parameter when executing the execScripts method.

{
    
    
  // template 是 link 替换为 style 后的 template
    template: embedHTML,
    // 静态资源地址
    assetPublicPath,
    // 获取外部脚本,最终得到所有脚本的代码内容
    getExternalScripts: () => getExternalScripts(scripts, fetch),
    // 获取外部样式文件的内容
    getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
    // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
    execScripts: (proxy, strictGlobal) => {
    
    
        if (!scripts.length) {
    
    
            return Promise.resolve();
        }
        return execScripts(entry, scripts, proxy, {
    
     fetch, strictGlobal });
    }
}

qiankun​​ Common errors

1. The sub-project does not export the required life cycle function
Insert image description here
2. When the sub-project is loaded, the container is not rendered.
Insert image description here
 Check whether the container div is written in a route. The route does not match it, so it is not loaded. . If you only load sub-projects on a certain routing page, you can register the sub-project in the mounted cycle of the page and start it.
 3. The request in the sub-application is 200, but in the base it is 400.
Because when you send a request to the sub-application in the base, the proxy of the sub-application will fail. The solution is to give the base a proxy.

devServer: {
    
    
    proxy: {
    
    
      '/proxyApi': {
    
    
        changeOrigin: true,
        target: 'http://redcloud.devops.sit.xiaohongshu.com',
        pathRewrite: {
    
    
          '^/proxyApi': '',
        },
      },
    },
  },

4. How do the base and micro applications communicate before?
1. The base can pass in props when registering a sub-application, and the sub-application can receive the
base sub
Please add image description
-application in the prop parameter in mount.
Please add image description

The core of qiankun lies in the loading of sub-applications (Html ​​Entry) and the isolation of styles and js

Guess you like

Origin blog.csdn.net/wyh666csdn/article/details/125812028