Several technical selections and detailed introductions of the micro front-end architecture

background

With the large-scale application of SPA, a new problem arises: a large-scale application needs to be split.

On the one hand, the rapid increase in functions leads to a proportional increase in packaging time, but the requirement for urgent releases is that the shorter the better, which is contradictory. On the other hand, when a code base integrates all functions, day-to-day collaboration is definitely very difficult.

Moreover, in the past ten years or so, front-end technology has developed very rapidly. Every two years is an era, which requires comrades to upgrade the project or even change the framework. But if you want to do this well in one version of a large-scale application, it is basically impossible.

The earliest solution was to use the iframe method, split large-scale applications according to main functional modules, and use jumps between sub-applications. But the biggest problem with this solution is that it causes page reloading and a white screen.

So what is a good solution? Cross-application solutions like micro frontends emerged in this context!

The concept of micro frontend

What is micro front-end: Micro front-end is an architecture similar to microservices. It is an architectural style that consists of multiple independently delivered front-end applications. It decomposes front-end applications into smaller and simpler ones that can be developed independently. The application is tested, deployed, and still appears to users as a cohesive single product. There is a base application (main application) that manages the loading and unloading of various sub-applications .

 

 

Therefore, micro front-end does not refer to a specific library, a specific framework, or a specific tool, but an ideal and architectural model.

The three core principles of micro front-end are: independent operation, independent deployment, and independent development

Advantages of micro frontend

The advantage of adopting a micro-front-end architecture is that integrating these small applications into a complete application, or integrating several unrelated applications that have been running for a long time into one application can integrate multiple projects into one and reduce the number of projects. The coupling between them improves project scalability.

Several ways to implement micro frontends

  • From single-spa to qiankun

  • Micro-app based on WebComponent

  • Module Federation implemented by webpack5

Classification of micro front-end frameworks

Single-spa

single-spaIt is a very good micro front-end basic framework, and qiankunthe framework is single-spaimplemented based on . single-spaIt has a layer of encapsulation based on , and also solves single-spasome defects of .

First, let’s understand how to use it single-spato complete the construction of micro front-end.

Single-spa implementation principle

First, register the routes of all Apps in the base application, single-spasave the route mapping relationship of each sub-application, and act as a micro front-end controller Controller . When the URL responds, the sub-application route is matched and the sub-application is loaded and rendered. The picture above is single-spaa complete description.

With the theoretical foundation in place, let's take a look at how to use it at the code level.

The following uses the Vue project as an example to build a single-spa base, and complete the configuration of the base in the Vue project entry file main.js.

Base configuration


//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'

Vue.config.productionTip = false

const mountApp = (url) => {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = url

    script.onload = resolve
    script.onerror = reject

    // 通过插入script标签的方式挂载子应用
    const firstScript = document.getElementsByTagName('script')[0]
    // 挂载子应用
    firstScript.parentNode.insertBefore(script, firstScript)
  })
}

const loadApp = (appRouter, appName) => {

  // 远程加载子应用
  return async () => {
    //手动挂载子应用
    await mountApp(appRouter + '/js/chunk-vendors.js')
    await mountApp(appRouter + '/js/app.js')
    // 获取子应用生命周期函数
    return window[appName]
  }
}

// 子应用列表
const appList = [
  {
    // 子应用名称
    name: 'app1',
    // 挂载子应用
    app: loadApp('http://localhost:8083', 'app1'),
    // 匹配该子路由的条件
    activeWhen: location => location.pathname.startsWith('/app1'),
    // 传递给子应用的对象
    customProps: {}
  },
  {
    name: 'app2',
    app: loadApp('http://localhost:8082', 'app2'),
    activeWhen: location => location.pathname.startsWith('/app2'),
    customProps: {}
  }
]

// 注册子应用
appList.map(item => {
  registerApplication(item)
})
 
// 注册路由并启动基座
new Vue({
  router,
  mounted() {
    start()
  },
  render: h => h(App)
}).$mount('#app')

 

The core of building a base is: configuring sub-application information, registering the sub-application through registerApplication , and starting the base during the mounting stage of the base project.

Sub-application configuration

 

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'

Vue.config.productionTip = false

const appOptions = {
  el: '#microApp',
  router,
  render: h => h(App)
}

// 支持应用独立运行、部署,不依赖于基座应用
// 如果不是微应用环境,即启动自身挂载的方式
if (!process.env.isMicro) {
  delete appOptions.el
  new Vue(appOptions).$mount('#app')
}
// 基于基座应用,导出生命周期函数
const appLifecycle = singleSpaVue({
  Vue,
  appOptions
})

// 抛出子应用生命周期
// 启动生命周期函数
export const bootstrap = (props)  => {
  console.log('app2 bootstrap')
  return appLifecycle.bootstrap(() => { })
}
// 挂载生命周期函数
export const mount = (props) => {
  console.log('app2 mount')
  return appLifecycle.mount(() => { })
}
// 卸载生命周期函数
export const unmount = (props) => {
  console.log('app2 unmount')
  return appLifecycle.unmount(() => { })
}

 

 Configure the sub-application as the umd packaging method

//vue.config.js
const package = require('./package.json')
module.exports = {
  // 告诉子应用在这个地址加载静态资源,否则会去基座应用的域名下加载
  publicPath: '//localhost:8082',
  // 开发服务器
  devServer: {
    port: 8082
  },
  configureWebpack: {
    // 导出umd格式的包,在全局对象上挂载属性package.name,基座应用需要通过这个
    // 全局对象获取一些信息,比如子应用导出的生命周期函数
    output: {
      // library的值在所有子应用中需要唯一
      library: package.name,
      libraryTarget: 'umd'
    }
  }

Configure sub-application environment variables

// .env.micro 
NODE_ENV=development
VUE_APP_BASE_URL=/app2
isMicro=true

The core of sub-application configuration is that after using singleSpaVue to generate sub-routing configuration, its life cycle function must be thrown .

Using the above method, you can easily implement a simple micro front-end application.

So we have single-spathis micro frontend solution, why do we need qiankunit?

In contrast single-spa, qiankunhe solves the JS sandbox environment and does not need us to deal with it ourselves. During single-spathe development process, we need to manually write the method to call the sub-application JS (such as the createScript method above). qiankunNo Qiankun only needs you to pass in the configuration of the corresponding apps, which will help us load it. .

Qiankun

Advantages of Qiankun

  • Based on the single-spa[1]  package, a more out-of-the-box API is provided.

  • It has nothing to do with the technology stack . Applications from any technology stack can be used/accessed, whether it is React/Vue/Angular/JQuery or other frameworks.

  • The HTML Entry access method allows you to access micro-apps as easily as using an iframe.

  • Style isolation ensures that styles between micro-applications do not interfere with each other.

  • JS sandbox ensures that global variables/events between micro-applications do not conflict.

  • Resource preloading , preloading unopened micro-app resources during browser idle time, speeding up the opening speed of micro-apps.

Base configuration

import { registerMicroApps, start } from 'qiankun';


registerMicroApps([
  {
    name: 'reactApp',
    entry: '//localhost:3000',
    container: '#container',
    activeRule: '/app-react',
  },
  {
    name: 'vueApp',
    entry: '//localhost:8080',
    container: '#container',
    activeRule: '/app-vue',
  },
  {
    name: 'angularApp',
    entry: '//localhost:4200',
    container: '#container',
    activeRule: '/app-angular',
  },
]);
// 启动 qiankun
start();

Sub-application configuration

Take  the create react app generated  react 16 project as an example, with  react-router-dom 5.x.

src 1. Added  in  the directory public-path.jsto solve the conflict of accessing static resources when sub-applications are mounted

  if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }

2. Set  history the mode routing  base:

  <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>

3. To modify the entry file  index.js , in order to avoid conflicts between the root id  #root and other DOMs, the search scope needs to be limited.

  import './public-path';
  import React from 'react';
  import ReactDOM from 'react-dom';
  import App from './App';


  function render(props) {
    const { container } = props;
    ReactDOM.render(<App />, container ? container.querySelector('#root') : 
    document.querySelector('#root'));
  }


  if (!window.__POWERED_BY_QIANKUN__) {
    render({});
  }


  export async function bootstrap() {
    console.log('[react16] react app bootstraped');
  }


  export async function mount(props) {
    console.log('[react16] props from main framework', props);
    render(props);
  }


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

4. Modify  webpack configuration

Install plugins  @rescripts/cli, of course you can also choose other plugins, eg  react-app-rewired.

npm i -D @rescripts/cli

Added to the root directory  .rescriptsrc.js:

const { name } = require('./package');


module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';


    return config;
  },


  devServer: (_) => {
    const config = _;


    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;


    return config;
  },
};

It can be seen from the above use of Qiankun that the process is very similar to the use of single-spa. The difference is that the use process of Qiankun is simpler. Some built-in operations are implemented internally by Qiankun. This is an implementation of IOC thinking . We only focus on container development, and other operations are managed by the Qiankun framework.

Micro-app

micro-appInstead of following single-spathe same idea, we borrowed the idea of ​​​​WebComponent and used CustomElement combined with the customized ShadowDom to encapsulate the micro front end into a WebComponent-like component to achieve componentized rendering of the micro front end.

And due to the isolation characteristics of custom ShadowDom, micro-appthere is no need to require sub-applications to modify rendering logic and expose methods like and , nor does it need to modify webpack configuration. It is currently the lowest-cost solution for accessing micro-frontends on the market single-spa.qiankun

The concept of WebComponent

WebComponent[2] is a set of interfaces for custom elements provided by HTML5. WebComponent is a set of different technologies that allow you to create reusable custom elements (their functionality is encapsulated outside your code) and use it in your web application. use them in. The above is the MDN community's explanation of WebComponent.

  • Custom elements:  A set of JavaScript APIs that allow you to define custom elements and their behavior, and then use them as needed in your user interface.

  • Shadow DOM  : A set of JavaScript APIs for attaching an encapsulated "shadow" DOM ​​tree to an element (rendered separately from the main document DOM) and controlling its associated functionality. This way you can keep the functionality of your elements private so that they can be scripted and styled without fear of conflicting with other parts of the document.

  • HTML templates:<template> The  [3] and <slot> [4] elements allow you to write markup templates that are not displayed in the rendered page. They can then be reused multiple times as the basis for custom element structures.

Next, use a small example to understand the concept of WebComponent faster.

A WebComponent with intra-component interaction

// 基于HTMLElement自定义组件元素
class CounterElement extends HTMLElement {

  // 在构造器中生成shadow节点
  constructor() {
    super();

    this.counter = 0;

    // 打开影子节点
    // 影子节点是为了隔离外部元素的影响
    const shadowRoot = this.attachShadow({ mode: 'open' });

    // 定义组件内嵌样式
    const styles = `
          #counter-increment {
              width: 60px;
              height: 30px;
              margin: 20px;
              background: none;
              border: 1px solid black;
          }
      `;

    // 定义组件HTMl结构
    shadowRoot.innerHTML = `
          <style>${styles}</style>
          <h3>Counter</h3>
          <slot name='counter-content'>Button</slot>
          <span id='counter-value'>; 0 </span>;
          <button id='counter-increment'> + </button>
      `;

    // 获取+号按钮及数值内容
    this.incrementButton = this.shadowRoot.querySelector('#counter-increment');
    this.counterValue = this.shadowRoot.querySelector('#counter-value');

    // 实现点击组件内事件驱动
    this.incrementButton.addEventListener("click", this.decrement.bind(this));

  }

  increment() {
    this.counter++
    this.updateValue();
  }

  // 替换counter节点内容,达到更新数值的效果
  updateValue() {
    this.counterValue.innerHTML = this.counter;
  }
}

// 在真实dom上,生成自定义组件元素
customElements.define('counter-element', CounterElement);

With an understanding of WebComponent, next, we understand the advantages of Micro-app better.

Advantages of micro-app

 

  • Simple to use

    We encapsulate all functions into a class WebComponent component, so that a single line of code can be embedded in the base application to render a micro front-end application.

    At the same time, micro-appit also provides a series of complete functions such as js沙箱, 样式隔离, 元素隔离, 预加载, 数据通信, and so on.静态资源补全

  • Zero dependencies

    micro-appThere are no dependencies, which gives it a small size and higher scalability.

  • Compatible with all frameworks

    In order to ensure the independent development and independent deployment capabilities of various businesses, micro-appmany compatibility measures have been made, and they can run normally in any technical framework.

Easy configuration of the base

Dock has preloaded sub-apps, parent-child app communication, public file sharing, and more

// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from './App'
import microApp from '@micro-zoe/micro-app'

const appName = 'my-app'

// 预加载
microApp.preFetch([
  { name: appName, url: 'xxx' }
])

// 基座向子应用数据通信
microApp.setData(appName, { type: '新的数据' })
// 获取指定子应用数据
const childData = microApp.getData(appName)

microApp.start({
  // 公共文件共享
  globalAssets: {
    js: ['js地址1', 'js地址2', ...], // js地址
    css: ['css地址1', 'css地址2', ...], // css地址
  }
})

Assign a route to the child application

// router.js
import { BrowserRouter, Switch, Route } from 'react-router-dom'

export default function AppRoute () {
  return (
    <BrowserRouter>
      <Switch>
        <Route path='/'>
          <micro-app name='app1' url='http://localhost:3000/' baseroute='/'></micro-app>
        </Route>
      </Switch>
    </BrowserRouter>
  )
}

Easy configuration of sub-applications

// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from './App'
import microApp from '@micro-zoe/micro-app'

const appName = 'my-app'

// 子应用运行时,切换静态资源访问路径
if (window.__MICRO_APP_ENVIRONMENT__) {
  __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}

// 基子应用向基座发送数据
// dispatch只接受对象作为参数
window.microApp.dispatch({ type: '子应用发送的数据' })
// 获取基座数据
const data = window.microApp.getData() // 返回基座下发的data数据

//性能优化,umd模式
// 如果子应用渲染和卸载不频繁,那么使用默认模式即可,如果子应用渲染和卸载非常频繁建议使用umd模式
// 将渲染操作放入 mount 函数 -- 必填
export function mount() {
  ReactDOM.render(<App />, document.getElementById("root"))
}

// 将卸载操作放入 unmount 函数 -- 必填
export function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById("root"))
}

// 微前端环境下,注册mount和unmount方法
if (window.__MICRO_APP_ENVIRONMENT__) {
  window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
  // 非微前端环境直接渲染
  mount()
}

Set sub-application routing

import { BrowserRouter, Switch, Route } from 'react-router-dom'

export default function AppRoute () {
  return (
    // 设置基础路由,子应用可以通过window.__MICRO_APP_BASE_ROUTE__获取基座下发的baseroute,
    // 如果没有设置baseroute属性,则此值默认为空字符串
    <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}>
      ...
    </BrowserRouter>
  )
}

The above is the usage of Micro-app

Module Federation

Module Federation is a concept proposed by Webpack5. Module federation is used to solve the problem of code sharing between multiple applications, allowing us to achieve cross-application code sharing more elegantly .

What MF wants to do is similar to the problem that micro frontend wants to solve. Split an application into multiple applications. Each application can be developed and deployed independently. One application can dynamically load and run the code of another application . And achieve dependency sharing between applications .

In order to realize such a function, MF proposes these core concepts in the design.

Container

A module packaged by ModuleFederationPlugin is called  a Container . In layman's terms, if one of our applications is built using ModuleFederationPlugin, then it becomes a  Container , which can load other  Containers and can be  loaded by other  Containers .

Host&Remote

Looking at Container from the perspective of consumers and producers  , Container  can also be called  Host  or  Remote .

Host: Consumer, which dynamically loads and runs the code of other Containers.

Remote: Provider, which exposes properties (such as components, methods, etc.) for   use by the Host

You can know that Host  and  Remote here   are relative, because a  Container can be used  as either  Host or  Remote .

Shared

Container  can  share  its dependencies (such as react, react-dom) for other  Containers  to use, that is, shared dependencies.

 

 

The above is a comparison chart of module management between webpack5 and previous versions.

Microapp configuration

Achieve the effect of micro-application through the configuration of webpack5

// 配置webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
new ModuleFederationPlugin({
  name: "appA",
 //出口文件
  filename: "remoteEntry.js",
 //暴露可访问的组件
  exposes: {
    "./input": "./src/input",
  },
 //或者其他模块的组件
 //如果把这一模块当作基座模块的话,
 //这里应该配置其他子应用模块的入口文件
  remotes: {
    appB: "appB@http://localhost:3002/remoteEntry.js",
  },
 //共享依赖,其他模块不需要再次下载,便可使用
  shared: ['react', 'react-dom'],
})

The above is my understanding of micro-application architecture and the evolution of micro-application architecture technology. It is not difficult to see that the evolution of these technologies is evolving in the direction of ease of use and scalability. Technology also has its limitations of the times, but ideas and technology are always improving. These types of technology selections have their own advantages and disadvantages, and each has its own merits. We can choose different technologies to build applications according to different needs.

The following are references at the time of writing this article:

single-spa:
https://zh-hans.single-spa.js.org/docs/getting-started-overview

qiankun:
https://qiankun.umijs.org/zh/guide

WebComponent:
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components

micro-app:
http://cangdu.org/micro-app/docs.html#/

References

[1]

single-spa:  https://github.com/CanopyTax/single-spa

[2]

WebComponent : https://developer.mozilla.org/zh-CN/docs/Web/Web_Components#example

[3]

<template>:  https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/template

[4]

<slot>: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/slot

Guess you like

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