The background,

Since my graduation in 2012, I have been mainly engaged in TOB business and responsible for several products of one business line, all of which serve a large platform.

1.1 Status Analysis

Due to historical reasons, when the system was just handed over, the situation was as follows:

  1. Subsystems are independent, with an HTML entry, and sub-products are connected into a “big platform” through a static (where static is data) aggregation page placed in a sub-product system project; Each subsystem has a menu component for switching subsystems (to meet the user’s convenience to switch the system), that is to say, n identical components are maintained in N projects; And when switching the system, the whole page is refreshed, and the user experience is very poor

  2. “Platform” in the business of each product general function module (such as examination and approval flow, dictionary, permissions) are placed in A separate sub system (Z) in engineering, and now the child as interaction with other child product engineering logic, that is to say, when A user in the use of A product, want to change A product approval, need to switch to the Z system, Switch to the management of A in system Z and click the Approval menu to modify the approval

  3. Existing requirements: product plug – in ability to combine with external products; For example, our products (A, B, C, D, E, F) and external products A2; External products A3, B3, C3; There are requirements for combined products [A, B, C, A2], [A, B, C, D, A3, B3, C3] and so on. Current solution, add a static aggregation page, put different product links, that is to say, maintain a number of aggregation pages; The switching subsystem menu component in the subsystem is not processed, and the switching menu data is always (A, B, C, D, E, F).

  4. The “large platform” performs single sign-on (SSO), that is, if you log in to one product successfully, other products will also log in successfully. Single products run in isolation with no problem, but on a large platform, each product’s login verification performance is inconsistent (sub-products are handled differently)

After some research, we decided to use the current popular Qiankun to complete the transformation

1.2 Solutions

  1. The “big platform” is missingPortal (Parent container)The concept of
    • The Portal project is added. As an HTML entry point, sub-projects (specific service projects) are loaded on demand through the Portal project and are not refreshed when switching between subsystems
    • Aggregation pages and switching subsystem menus should be in the Portal project, not in the subsystem project, and the sub-product system should focus only on its own product business
    • The platform lacks the concept of a workbench. The current situation is just a aggregation of several products with a poor product experience. Workbench can be added in the Portal project
  2. Gm’s business function modules, dictionary, permissions (approval) should belong to a part of the portal ability, historical reasons have been Z engineering, for cost reasons, is still in Z project, but the entry can be apart from the rest of the subsystem of the entrance, the Z system into several portal entry [dictionary] [approval] [access], reuse subsystem communications, Z By default, the subsystem of the last operation is selected. The same example: when users are using product A and want to change the approval of product A, they only need to directly click the approval menu
  3. The lack of productConfiguration changeConcept, aggregation pages and switching subsystem menus should each require only one page or component to be maintained, with sub-product data on the page or component configurable by assigning different roles to different product permissions
  4. The sub-project does not need to affect its own business, just some transformation, connected to the micro front end of this architecture. The login and logout functions of sub-products follow their own processing logic when they are run independently, and the login and logout functions of portal are invoked when they are run on a large platform.
  5. Others: The public parts (such as VUE, VUex, VUE-Router, Element, private NPM package, etc.) originally needed to be loaded by the sub-project are all scheduled by the main project and loaded by external chain in coordination with the externals function of Webpack

Two, technical implementation

Website: Qiankun.umijs.org/

The front-end engineering stack is built by Vue+ Webpack and Vue Li3

Implementation analysis

  • Master application: Build the master application project as a container for the aggregation of sub-applications
  • Sub-application: existing engineering transformation ADAPTS to main application

2.1 the main application

Demo address: github.com/zxyue25/mic…

Step0 create a project

/ / vuecli3 created
vue create micro-portal-demo
Copy the code

Step1 installation qiankun

npm i qiankun -S
Copy the code

Step2 register the micro application in the main application

QiankunAmong them:

  • Name: uniquely identifies the sub-application name
  • Entry: sub-application entry. Note The entry is a local service during development, for example, http://localhost:8083/. In production deployment, the entry is ${unified address}/${name}.
  • Container: Container for neutron applications
  • ActiveRule: active route

Implementation principle of QiankunOnce the url of the browser changes, the matching logic of qiankun will be automatically triggered. All microapps matched by activeRule rules will be inserted into the specified Container and the lifecycle hooks exposed by the microapps will be called in turn

Create a SRC /micro folder and store all documents related to the micro front end in it. The directory structure is as follows

├ SRC ├─ Micro │ ├─ App.js// Development environment subsystem configuration│ │ ├ ─ event. Js// Events provided by the event bus and Portal to the subsystem│ │ ├ ─ index. Js// Register subsystem and start method│ │ ├ ─ micro. Vue// Provide subsystem load containers and related logic│ │ └ ─ route. Js// Subapplication route
Copy the code
2.1.1 Subsystem Management

Implement subsystem integration by configuring the subsystems to be integrated locally

// src/micro/app.js
// Configure the subsystem file, modify the file, need to restart webpack
const subAppList = [
  {
    APP_NAME: 'subApp1'.FE_ADDRESS: 'http://localhost:8084/subApp1'.API_ADDRESS: ' ',},... ]module.exports = subAppList
Copy the code
2.1.2 Subsystem Loading

For subsystem loading, the Portal system needs to obtain static resources of the subsystem and proxy subsystem interfaces, and provide containers for subsystem loading

2.1.2.1 Subsystem Containers

Determine whether the current subsystem is displayed by routing

// src/micro/micro.vue
<template>
  <div v-if="subAppActive" id="micro-sub-app"></div>
</template>

<script>
import store from '@/store'
import subAppList from '@/micro/app'
import { start } from '@/micro'
import { mapState } from 'vuex'
export default {
  data() {
    return {
      subAppList,
    }
  },
  computed: {
    ...mapState(['subAppActive']),},watch: {
    $route(val) {
      this.handleRouteChange(val)
    },
  },
  beforeRouteEnter(to, from, next) {
    next((vm) = > {
      vm.handleRouteChange.apply(vm, [to])
    })
  },
  mounted() {
    if (!window.subappRegister) { // Register once
      window.subappRegister = true
      start()
    }
  },
  methods: {
    // Listen for route changes to determine whether the subsystem is accessed
    handleRouteChange() {
      const bol = this.isMicroSub(this.subAppList, this.$route.path)
      store.commit('TOGGLE_SUBAPPACTIVE', bol)
      if (bol) {
        // Get the subsystem currently accessed
        const microSub = this.getMicroSub(this.subAppList, this.$route.path)
        // Add hash mode in full screen
        if (
          this.$route.path.startsWith(`${microSub.entry}/full`) | | (this.$route.hash && this.$route.hash.startsWith('#/full'))) {// mounted
          setTimeout(() = > {
            window.eventCenter.emit('SYSTEM_FULL_SCREEN')})}else if (window.__IS_FULL_SCREEN) {
          window.eventCenter.emit('SYSTEM_EXIT_FULL_SCREEN')}}else {
        this.$router.replace({
          path: '/ 404',}}}),// Check whether the route is a subapplication
    isMicroSub(list, path) {
      return list.some((item) = > {
        const context = ` /${item.APP_NAME}`
        return path.startsWith(context)
      })
    },
    // Get the active child application
    getMicroSub(list, path) {
      return list.find((item) = > {
        const context = ` /${item.APP_NAME}`
        return path.startsWith(context)
      })
    },
  },
}
</script>
Copy the code
// src/micro/route.js
import Main from '@/components/main';
const Micro = () = > import(/* webpackChunkName: "micro" */ './micro.vue');

export default {
  path: '/'.component: Main,
  children: [{path: The '*'.component: Micro,
    },
  ],
};
Copy the code
import MicroRoute from '@/micro/route';
constroutes = [...]  routes.push(MicroRoute)const router = new Router({
  routes,
  mode: 'history',
})
router.beforeEach((to, from, next) = > {
  const bol = subAppList.some((item) = > {
    const context = ` /${item.APP_NAME}`
    return to.path.startsWith(context)
  })
  store.commit('TOGGLE_SUBAPPACTIVE', bol)
  next()
})
Copy the code
2.1.2.2 Static Resource Proxy/Interface Proxy

Development environment: Achieve static resource proxy and interface proxy through Webpack Proxy

// vue.config.js
const appName = process.env.VUE_APP_NAME
const publicPath = process.env.VUE_APP_ENV === 'production' ? ` /${appName}/ ` : ' '.For details, see the Deployment section
const subAppList = require('./src/micro/app')
let proxyObjs = {}

subAppList.map((item) = > {
  const proxyObj = {
    [` /${item.APP_NAME}/_baseAPI`] : {// Interface proxy
      target: item.API_ADDRESS,
      changeOrigin: true.pathRewrite: {[` ^ /${item.APP_NAME}/_baseAPI`] :' ',}},` /${item.APP_NAME}`] : {// Static resource proxy
      target: item.FE_ADDRESS,
      secure: false.bypass(req) {
        if (req.headers.accept && req.headers.accept.indexOf('html')! = = -1) { // Since portal is a single-page application, the portal system should return the HTML of portal when accessing the subsystem through /APP_NAME
          return '/portal/index.html'}},pathRewrite: {[` ^ /${item.APP_NAME}`] :' ', }, }, } proxyObjs = { ... proxyObjs, ... proxyObj } })module.exports = {
  productionSourceMap: false,
  publicPath,
  devServer: {
    compress: true.// host: 'portal.fe.com',
    port: 8082.hotOnly: false.disableHostCheck: true.headers: { 'Access-Control-Allow-Origin': The '*' },
    proxy: {
      ...proxyObjs,
    },
  },
  configureWebpack: {
    output: {
      libraryTarget: 'umd'.library: appName,
      jsonpFunction: `webpackJsonp_${appName}`,,}}}Copy the code

Production environment: Implement static resource and interface proxy through Nginx

server {
        listen 80;

        server_name portal.demo;
        root /export/fe;

        gzip on;
        gzip_buffers 32 4K;
        gzip_comp_level 6;
        gzip_min_length 100;
        gzip_types application/javascript text/css text/xml;
        gzip_vary on;

        location / {
                try_files $uri /portal/index.html;
        }
        All system entry HTML does not need to be cached
        location ~ index.html$ {
                add_header Cache-Control no-store;
        }

        location /subApp1 {
                Enter the subsystem address directly in the browser, go to portal first
                if ($http_accept ~ html) {
                        rewrite^ / (. *) /portal/index.html last;
                }
                try_files $uri /subApp1/index.html;
        }
        location /subApp1/_baseAPI {
                proxy_passhttp://subApp1/; }}Copy the code

Overall process:

2.1.3 System Communication
Event bus

Create an event bus based on Eventemitter3 and mount it to the window.eventCenter object.

  • Emit the event window.eventCenter.emit(event type, passing parameters)
  • Listen for the event window.eventCenter.on(event type, callback method)
// src/micro/event.js
import { messageBus as eventCenter } from '@/utils/message-bus';
import router from '@/router';
import { logout } from '@/utils/util';
import { Message } from 'element-ui';

const showMessage = ({ showClose = true, message, type = 'error' } = {}) = > {
  Message({
    showClose,
    message,
    type,
  });
};

const SYSTEM_FORBIDDEN = 'SYSTEM_FORBIDDEN';
const SYSTEM_USER_INVALID = 'SYSTEM_USER_INVALID';
const SYSTEM_LOGOUT = 'SYSTEM_LOGOUT';
const SYSTEM_FULL_SCREEN = 'SYSTEM_FULL_SCREEN';
const SYSTEM_EXIT_FULL_SCREEN = 'SYSTEM_EXIT_FULL_SCREEN';

/** * 401 Jump to no permission page *@param router* /
const forbidden = () = > {
  showMessage({
    message: 'I'm so sorry! You do not have permission to access '});setTimeout(() = > {
    router.push('/ 401');
  }, 500);
};

/** * trigger event *@param type* /
const eventEmit = (type) = > {
  if (window.eventCenter) {
    window.eventCenter.emit(type); }};/** * Initializes Event *@param router* /
export const initEvent = () = > {
  if (window.eventCenter) return;

  Declare the event bus
  window.eventCenter = eventCenter;

  // Listen on global system 401 without permission page
  window.eventCenter.on(SYSTEM_FORBIDDEN, () = > forbidden());

  // Listen for global system login failure
  window.eventCenter.on(SYSTEM_USER_INVALID, () = > logout());

  // Listen for global logout operations
  window.eventCenter.on(SYSTEM_LOGOUT, () = > logout());

  / / full screen
  window.eventCenter.on(SYSTEM_FULL_SCREEN, () = > {
    window.__IS_FULL_SCREEN = true;
    const headDom = document.querySelector('.sys-head');
    const asideDom = document.querySelector('.portal-app-aside');
    if (headDom) {
      headDom.style.display = 'none';
    }
    if (asideDom) {
      asideDom.style.display = 'none'; }});window.eventCenter.on(SYSTEM_EXIT_FULL_SCREEN, () = > {
    window.__IS_FULL_SCREEN = false;
    const headDom = document.querySelector('.sys-head');
    const asideDom = document.querySelector('.portal-app-aside');
    if (headDom) {
      headDom.style.display = 'none';
    }
    if (asideDom) {
      asideDom.style.display = 'none'; }}); };/** * the main application calls directly, in case the Event is not registered when the first load
export const baseSystemLogout = () = > logout();
export const baseSystemForbidden = () = > forbidden();

/** * subapplications must Emit notifications and issue */ via the active subapplication
export const systemForbidden = () = > eventEmit(SYSTEM_FORBIDDEN);
export const systemLogout = () = > eventEmit(SYSTEM_LOGOUT);
Copy the code

message-bus.js

// src/utils/message-bus.js
import Event from 'eventemitter3';

export const messageBus = new Event();
Copy the code
The data transfer

The Main application uses the Qiankun API initGlobalState to define the global state and return the communication method, while the micro application obtains the communication method from props

// src/micro/index.js
import {
  registerMicroApps,
  addGlobalUncaughtErrorHandler,
  removeGlobalUncaughtErrorHandler,
  initGlobalState,
  start as s,
} from 'qiankun'
import { initEvent } from './event'
import { Loading, Message } from 'element-ui'
import subAppList from './app'
import store from '@/store'

// The primary application communicates with its child applications
const initialState = {
  preActiveApp: store.state.preActiveApp,
}

// Initialize state
const actions = initGlobalState(initialState)

actions.onGlobalStateChange((state) = > {
  // Listen for public state changes
  store.commit('CHANGE_PREACTIVEAPP', state.preActiveApp)
})

const consoleStyle = 'color:#fff; background:#2c68ff; line-height:28px; padding:0 40px; font-size:16px; '

/** * register - subapplication collection *@param subAppList* /
export const register = () = > {
  let loading
  try {
    const subApps = subAppList.map((subApp) = > {
      const { APP_NAME, FE_ADDRESS } = subApp
      return {
        name: APP_NAME,
        entry: process.env.VUE_APP_ENV === 'production' ? ` /${APP_NAME}` : FE_ADDRESS, // Production environment is inconsistent with development environment
        container: '#micro-sub-app'.// The primary application inherits the child application container ID
        activeRule: (location) = > location.pathname.startsWith(` /${APP_NAME}`),
        props: { preActiveApp: store.state.preActiveApp }, // Pass communication data
      }
    })
    registerMicroApps(subApps, {
      beforeLoad: [
        async (app) => {
          console.log(`%c${app.name} before load`, consoleStyle)
          loading = Loading.service({
            target: '#micro-sub-app'.lock: true.text: ' '.spinner: 'base-loading-type1'.background: 'hsla (% 0, 0, 100%, 8)',}}),],beforeMount: [
        async (app) => {
          console.log(`%c${app.name} before mount`, consoleStyle)
          const body = document.getElementsByTagName('body') [0]
          if (body) {
            body.setAttribute('id', app.name)
            body.setAttribute('class', app.name)
          }
        },
      ],
      afterMount: [
        async (app) => {
          console.log(`%c${app.name} after mount`, consoleStyle)
          setTimeout(() = > {
            loading.close()
          }, 1000)},].beforeUnmount: [
        async (app) => {
          actions.setGlobalState({
            preActiveApp: app.name ? app.name : store.state.preActiveApp, // Record the appName before the last microapplication is uninstalled})},].afterUnmount: [
        async (app) => {
          console.log(`%c${app.name} after unmount`, consoleStyle)
          const body = document.getElementsByTagName('body') [0]
          if (body) {
            body.setAttribute('id'.' ')}},],})}catch (e) {
    throw new Error(e)
  }
}
/** * Initialization event */
const microEvent = () = > {
  // Failed to listen to the child application load
  addGlobalUncaughtErrorHandler((e) = > {
    if (
      e instanceof PromiseRejectionEvent ||
      e.message === 'ResizeObserver loop limit exceeded'
    ) {
      return
    }
    const eMessage = e.message || ' '
    if (
      eMessage.search('died in status LOADING_SOURCE_CODE')! = = -1 ||
      eMessage.search('died in status SKIP_BECAUSE_BROKEN')! = = -1
    ) {
       Message({
          message: 'System registration failed, please try again later'.type: 'error'})},else if (eMessage.search('Failed to fetch')! = = -1) {
      Message({
        message: 'Resource not found, please check to deploy'.type: 'error',
      })
    }
    removeGlobalUncaughtErrorHandler((error) = > console.log('remove', error))
  })
  
  /** * Initializes the global event */
  initEvent()
}

microEvent()

function getPublicPath(entry) {
  if (typeof entry === 'object') {
    return '/'
  }
  try {
    // URL constructors do not support urls with // prefixes
    const { origin, pathname } = new URL(
      entry.startsWith('/ /')?`${location.protocol}${entry}` : entry,
      location.href
    )
    const paths = pathname.split('/')
    // paths.pop();
    const r = `${origin}${paths.join('/')}/ `
    return r
  } catch (e) {
    console.warn(e)
    return ' '}}/** * Start route listening *@param prefetch
 * @param appList
 * @returns {Promise<unknown>}* /
export function start({ prefetch = false } = {}) {
  return new Promise((resolve, reject) = > {
    try {
      / / register
      register()
      / / start
      s({ prefetch, sandbox: false, getPublicPath })
      resolve()
    } catch (e) {
      Message({
        message: 'System registration failed, please try again later'.type: 'error',
      })
      reject(e)
    }
  })
}
Copy the code

2.2 the application

Demo address: Sub-application: github.com/zxyue25/mic… Step0 create a project

/ / vuecli3 created
vue create micro-subapp-demo
Copy the code

Step1 modify the entry file to export the corresponding lifecycle hooks

The microapplication does not require any additional dependencies to be installed to access the Qiankun master application. Export the bootstrap, mount, and unmount lifecycle hooks in main.js for the main application to call when appropriate

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

let instance = null
function render(props = {}) {
  const { container } = props

  instance = new Vue({
    router,
    store,
    render: (h) = > h(App),
  }).$mount(container ? container.querySelector('#app') : '#app')}// Independent runtime
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped')}export async function mount(props) {
  console.log('[vue] props from main framework', props)// Accept data from the master application
  render(props)
}
export async function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ' '
  instance = null
}
Copy the code

Step 2. Configure the microapplication packaging tool

// vue.config.js
const appName = process.env.VUE_APP_NAME
const publicPath = ` /${appName}/ `

module.exports = {
  publicPath,
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': The '*',},// host: 'subapp1.fe.com',
    port: '8083'.proxy: {[`${publicPath}_baseAPI`] : {target: 'http://subapp1.be.com'.changeOrigin: true.pathRewrite: {[` ^${publicPath}_baseAPI`] :' ',},},},},configureWebpack: {
    output: {
      library: appName,
      libraryTarget: 'umd'.// Package microapplications into umD library format
      jsonpFunction: `webpackJsonp_${appName}`,,}}}Copy the code

Step 3. Hide the header when configuring the microapplication to be integrated into the main application

SubApp1 Sub-application Running independently Screenshot:Main application running screenshot:Add subApp1 to SRC /micro/app.js and integrate subApp1 into the main app.You can see that the child app is successfully integrated in the container with the id=micro-sub-app of the main app, but the header is also out

// src/constants.js
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

const isMicro = window.__POWERED_BY_QIANKUN__ || false
const publicPath = __webpack_public_path__ || ' '

export {
  isMicro,
  publicPath
}
Copy the code
// header.vue
import { isMicro } from "@/constants";
Copy the code

If isMicro is true, hide the header. If isMicro is not true, display the header. As shown in the following figure, the preActiveApp transmitted through props is also receivedStep4 the sub-application invokes the login verification failure event of the primary application

// src/utils/https.js
axiosInstance.interceptors.response.use(
  (response) = > {
    const { data } = response
    if (data.code == '20001' || data.error === 'NotLogin') {
      // window.location.href = ''
    } else if (data.code == '20002' || data.error === 'NotPermission') {
      if(! isMicro) { router.replace({path: '/ 401'})}else {
        window.eventCenter && window.eventCenter.emit('SYSTEM_USER_INVALID') // }}... },(error) = >{... })Copy the code

2.3 other

2.3.1 Deployment (nginx)

/${appName}/, the appName of the main application is portal, subApp1, subApp2,… ; Use paths to distinguish between resources to prevent resource 404 from loading

server {
        listen 80;

        server_name portal.demo;
        root /export/fe;

        gzip on;
        gzip_buffers 32 4K;
        gzip_comp_level 6;
        gzip_min_length 100;
        gzip_types application/javascript text/css text/xml;
        gzip_vary on;

        location / {
                try_files $uri /portal/index.html;
        }
        All system entry HTML does not need to be cached
        location ~ index.html$ {
                add_header Cache-Control no-store;
        }

        location /subApp1 {
                Enter the subsystem address directly in the browser, go to portal first
                if ($http_accept ~ html) {
                        rewrite^ / (. *) /portal/index.html last;
                }
                try_files $uri /subApp1/index.html;
        }
        
        location /subApp1/_baseAPI {
                proxy_pass http://subApp1-demo/;
        }
        
        location /subApp2 {
                Enter the subsystem address directly in the browser, go to portal first
                if ($http_accept ~ html) {
                        rewrite^ / (. *) /portal/index.html last;
                }
                try_files $uri /subApp2/index.html;
        }
        
        location /subApp2/_baseAPI {
                proxy_passhttp://subApp2-demo/; }... }Copy the code
2.3.2 Font file 404 in the CSS after package deployment

The reason: Qiankun changed the outer chain style to an inline style, but the loading path of the font file and background image was relative. Once a CSS file is packaged, you can’t change the path of font files and background images by dynamically modifying publicPath

The link tag is referenced in the FORM of CDN, and the microapplication has the ignore attribute to prevent repeated loading of resources

The main application

// public/index.html
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"/>
Copy the code

Micro application

// public/index.html
<link rel="stylesheet" ignore href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"/>
Copy the code
2.3.3 Handling public Resources

The public parts (such as VUE, VUex, VUE-Router, Element, private NPM package, etc.) originally needed to be loaded by the sub-project are all scheduled by the main project and loaded on demand through external chain in conjunction with the externals function of Webpack

Three, think

The essence of micro front-end: SPA runs of different technology stacks follow the same path. Both Single-SPA and Qiankun, including other existing micro front-end solutions, actually use the same idea. It is the same path to attach the life cycle to the global variables of UMD and then execute them at the time of route switching, but they are not reliable

Development:

  1. webpack5 module federation

The Module Federation | webpack, if does not consider the sandbox, just for the sake of different technology stacks coexist, then WMF can do it, but one thing, is to use webpack, this thing is a runtime code splitting, Is attached to a webpack global variable, essentially similar to hanging umD

  1. Realm API

A new proposal, natural micro front end sandbox, though don’t know until monkey years

  1. portals

WICG/ Portals is also a proposed alternative to IFrame

These new proposal, and we Shared by umd module, by Proxy of the sandbox,, once the proposal really fall to the ground, it wouldn’t need the frame completely, so back to the problem, the current market on the front end of the one and only one, in its form, is a kind of solution, no true and false, as for the future, With all the new proposals coming along, sandboxes, dependencies, will become the norm, built in without the need for secondary packaging