Practice of Qiankun from building to deployment of micro front end

Time:2020-11-19

Recently, I was responsible for a new projectqiankunWrite an article to share some problems and thoughts in the actual combat.

Example code: https://github.com/fengxianqi/qiankun-example 。

Online demo: http://qiankun.fengxianqi.com/

Separate access to online sub applications:

  • subapp/sub-vue
  • subapp/sub-react

Why use Qiankun

A functional requirement of the project is to embed an existing tool within the company, which is deployed independently and can be used withReactWhat is the main technology selection of our projectvueTherefore, we need to consider the scheme of embedding pages. There are two main routes:

  • iframeprogramme
  • qiankunMicro front end solution

Both solutions can meet our needs and are feasible. Have to say,iframeAlthough the scheme is ordinary, it is practical and low cost,iframeThe solution can cover most of the micro front-end business requirements, andqiankunThe technical requirements are higher.

Technical students also have a strong demand for their own growth, so when both can meet the business needs, we hope to apply some newer technologies and toss some unknown things, so we decided to chooseqiankun

Project architecture

Background system is generally up and down or left and right layout. In the figure below, pink is the base, which is only responsible for head navigation, and green is the whole sub application attached. Click the head navigation to switch the sub application.
Practice of Qiankun from building to deployment of micro front end

Referring to the official examples code, there is a base under the root directory of the projectmainAnd other sub applicationssub-vuesub-reactThe initial directory structure is as follows:

├ - common // common module
├ - Main // base
├ -- sub react // react
└ - sub Vue // Vue subapplication

The base isvueBuild, sub applications arereactandvue

Base configuration

The main base is built by vue-cli3. It is only responsible for rendering the navigation and Issuing the login state. It provides a container div for the sub application to mount. The base should be kept simple (Qiankun’s official demo is even built directly with native HTML). It should not do business related operations.

This library only needs to be introduced in the base, and themain.jsIn order to facilitate the management, we put the configuration of sub applications in:main/src/micro-app.jsNext.

const microApps = [
  {
    name: 'sub-vue',
    entry: '//localhost:7777/',
    activeRule: '/sub-vue',
    Container: '# subapp viewer', // div mounted by sub application
    props: {
      Routerbase: '/ sub Vue' // issue the route to the sub application, and the sub application defines the route in the Qiankun environment according to the value
    }
  },
  {
    name: 'sub-react',
    entry: '//localhost:7788/',
    activeRule: '/sub-react',
    Container: '# subapp viewer', // div mounted by sub application
    props: {
      routerBase: '/sub-react'
    }
  }
]

export default microApps

And then in thesrc/main.jsIntroduction in

import Vue from 'vue';
import App from './App.vue';
import { registerMicroApps, start } from 'qiankun';
import microApps from './micro-app';

Vue.config.productionTip = false;

new Vue({
  render: h => h(App),
}).$mount('#app');


registerMicroApps(microApps, {
  beforeLoad: app => {
    console.log('before load app.name====>>>>>', app.name)
  },
  beforeMount: [
    app => {
      console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name);
    },
  ],
  afterMount: [
    app => {
      console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name);
    }
  ],
  afterUnmount: [
    app => {
      console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name);
    },
  ],
});

start();

stayApp.vue, need to declaremicro-app.jsThe configuration of the sub application mount div (note that the ID must be consistent), and the base layout is related, roughly as follows:

<template>
  <div id="layout-wrapper">
    < div class = "layout header" > head navigation < / div >
    <div id="subapp-viewport"></div>
  </div>
</template>

In this way, the base is configured. After the project starts, the child application will be mounted to<div id="subapp-viewport"></div>Medium.

Sub application configuration

1、 Vue subapplication

Create a new one at the root of the project with Vue clisub-vueThe name of the child application is better than that of the parent applicationsrc/micro-app.jsThe name of the configuration in is consistent (this can be used directlypackage.jsonMediumnameAs output).

  1. newly addedvue.config.jsThe port of devserver is changed to be consistent with the configuration of the main application, and cross domain is addedheadersandoutputto configure.
// package.json The name of should be consistent with the main application
const { name } = require('../package.json')

module.exports = {
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    }
  },
  devServer: {
    port:  process.env.VUE_ APP_ Port, // Vue in. Env_ APP_ Port = 7788, consistent with the configuration of the parent application
    headers: {
      'access control allow origin': '*' // cross domain response header when master app gets child application
    }
  }
}
  1. newly addedsrc/public-path.js
(function() {
  if (window.__POWERED_BY_QIANKUN__) {
    if (process.env.NODE_ENV === 'development') {
      // eslint-disable-next-line no-undef
      __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}/`;
      return;
    }
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }
})();
  1. src/router/index.jsOnly routes are exposed,new RouterChange tomain.jsIt is stated in.
  2. reformmain.js, introduce thepublic-path.js, rewrite render, add lifecycle function, etc., and finally as follows:
Import '. / public path' // note that public path needs to be introduced
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import store from './store'
import VueRouter from 'vue-router'

Vue.config.productionTip = false
let instance = null

function render (props = {}) {
  const { container, routerBase } = props
  const router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? routerBase : process.env.BASE_URL,
    mode: 'history',
    routes
  })
  instance = new Vue({
    router,
    store,
    render: (h) => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app')
}

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)

  render(props)
}

export async function unmount () {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
}

At this point, the Vue sub application of the basic version is configuredrouterandvuexNo need, can be removed.

2、 React subapplication

  1. adoptnpx create-react-app sub-reactCreate a new react application.
  2. newly added.envFile additionPORTThe variable and port number should be consistent with those configured by the parent application.
  3. Why notejectFor all webpack configurations, we can use the react app rewired scheme to copy the webpack.
  • firstnpm install react-app-rewired --save-dev
  • newly buildsub-react/config-overrides.js
const { name } = require('./package.json');

module.exports = {
  webpack: function override(config, env) {
    //To solve the problem that the main application will hang up after access: https://github.com/umijs/qiankun/issues/340
    config.entry = config.entry.filter(
      (e) => !e.includes('webpackHotDevClient')
    );
    
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    return config;
  },
  devServer: (configFunction) => {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      config.open = false;
      config.hot = false;
      config.headers = {
        'Access-Control-Allow-Origin': '*',
      };
      return config;
    };
  },
};
  1. newly addedsrc/public-path.js
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  1. reformindex.js, introducepublic-path.js, add lifecycle functions, etc.
import './public-path'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

function render() {
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
}

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

/**
 *Bootstrap is called only once when the micro application is initialized. The mount hook will be called directly when the micro application re enters the next time. Bootstrap will not be triggered repeatedly.
 *Generally, we can initialize some global variables here, such as the application level cache that will not be destroyed in the unmount phase.
 */
export async function bootstrap() {
  console.log('react app bootstraped');
}
/**
 *The mount method is called every time the application enters. Usually, we trigger the application's rendering method here
 */
export async function mount(props) {
  console.log(props);
  render();
}
/**
 *The method that will be called every time the application is cut out / unloaded. Usually, we will unload the application instance of the micro application here
 */
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
/**
 *Optional life cycle hook, which takes effect only when the micro application is loaded in the loadmicroapp mode
 */
export async function update(props) {
  console.log('update props', props);
}

serviceWorker.unregister();

So far, the react sub application of the basic version is configured.

Advanced

Global state management

qiankunThrough initglobalstate, onglobalstate change and setglobalstate, the global state management of the master application is realized, and then, by default, the global state management of the master application is implementedpropsPass the communication method to the child application. Let’s take a look at the official example usage:

Main application:

// main/src/main.js
import { initGlobalState } from 'qiankun';
//Initialize state
const initialState = {
  User: {} // user information
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
  //State: state after change; prev state before change
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

Sub application:

//To obtain the communication method from the lifecycle mount, props has two APIs, onglobalstatechange and setglobalstate, by default
export function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    //State: state after change; prev state before change
    console.log(state, prev);
  });
  props.setGlobalState(state);
}

These two pieces of code are not difficult to understand, the parent-child application throughonGlobalStateChangeThis method is used to communicate, which is actually a publish subscribe design pattern.

OK, the official example usage is very simple and sufficient. The syntax of pure JavaScript does not involve any Vue or react. Developers can customize it freely.

If we use the official example directly, the data will be loose and the call will be complex, and all sub applications will have to declareonGlobalStateChangeMonitor the status, and then pass thesetGlobalStateUpdate the data.

Therefore, it is necessary for usMake further encapsulation design for data state。 The author mainly considers the following points:

  • The main application should be concise and simple. For the sub application, the data sent by the main application is a very pure oneobjectIn order to better support sub applications of different frameworks, the main application does not needvuex
  • Vue child applications should be able to inherit the data from the parent application and support independent operation.

Sub application inmountThe declaration cycle can obtain the latest data issued by the main application, and then register the data to a file namedglobalIn the vuex module of the global module, the child application updates the data through the action action of the global module, and automatically synchronizes back to the parent application at the same time.

Therefore, for sub applications,It doesn’t have to know whether it is a Qiankun sub application or a stand-alone application. It just has a name calledglobalIt can update data through action, and no longer need to care whether to synchronize to the parent application(synchronized actions are encapsulated inside the method, and the caller doesn’t care)Support sub application to start development independently

  • The application of react is the same.

Practice of Qiankun from building to deployment of micro front end

State encapsulation of main application

The main application maintains oneinitialStateIt is aobjectType, which will be distributed to sub applications.

// main/src/store.js

import { initGlobalState } from 'qiankun';
import Vue from 'vue'

//Initial state of the parent application
// Vue.observable To make initialstate responsive: https://cn.vuejs.org/v2/api/#Vue -observable。
let initialState = Vue.observable({
  user: {},
});

const actions = initGlobalState(initialState);

actions.onGlobalStateChange((newState, prev) => {
  //State: state after change; prev state before change
  console.log('main change', JSON.stringify(newState), JSON.stringify(prev));

  for (let key in newState) {
    initialState[key] = newState[key]
  }
});

//Define a method to get the state and send it to the sub application
actions.getGlobalState = (key) => {
  //If there is a key, it means to take a child object under the globalstate
  //No key means all
  return key ? initialState[key] : initialState
}

export default actions;

Here are two things to note:

  • Vue.observableThis is to make the state of the parent application responsive if it is not used Vue.observable Package level, it is just a pure object, which can be obtained by sub applications, but will lose its responsiveness,This means that the page will not be updated after the data changes
  • getGlobalStateMethod, this isControversialWe have a discussion on GitHub: https://github.com/umijs/qiankun/pull/729 。

On the one hand, the author thinks thatgetGlobalStateIt’s not necessary,onGlobalStateChangeIt’s enough.

On the other hand, the author and other students who mentioned PR feel it necessary to provide onegetGlobalStateThe reason is that the get method is more convenient to use. The sub application does not need to listen to StateChange events all the time. It only needs to be initialized once through getglobalstate at the first mount. Here, the author insists that the parent application issue a getglobalstate method.

Since getglobalstate is not officially supported, it is necessary to issue the method through props when registering child applications

import store from './store';
const microApps = [
  {
    name: 'sub-vue',
    entry: '//localhost:7777/',
    activeRule: '/sub-vue',
  },
  {
    name: 'sub-react',
    entry: '//localhost:7788/',
    activeRule: '/sub-react',
  }
]

const apps = microApps.map(item => {
  return {
    ...item,
    Container: '# subapp viewer', // div mounted by sub application
    props: {
      routerBase:  item.activeRule , // issue basic route
      getGlobalState:  store.getGlobalState  //Issue getglobalstate method
    },
  }
})

export default microApps

State encapsulation of Vue subapplication

As mentioned above, the child application will register the state issued by the parent application as aglobalIn order to facilitate reuse, we encapsulate the following:

// sub-vue/src/store/global-register.js

/**
 * 
 *@ param {vuex instance} store 
 *@ param {props issued by Qiankun} props 
 */
function registerGlobalModule(store, props = {}) {
  if (!store || !store.hasModule) {
    return;
  }

  //Gets the initialized state
  const initState = props.getGlobalState && props.getGlobalState() || {
    menu: [],
    user: {}
  };

  //The data of the parent application is stored in the child application, and the namespace is fixed to global
  if (!store.hasModule('global')) {
    const globalModule = {
      namespaced: true,
      state: initState,
      actions: {
        //The child application changes the state and notifies the parent application
        setGlobalState({ commit }, payload) {
          commit('setGlobalState', payload);
          commit('emitGlobalState', payload);
        },
        //Initialization, which is only used to synchronize the data of the parent application when mount
        initGlobalState({ commit }, payload) {
          commit('setGlobalState', payload);
        },
      },
      mutations: {
        setGlobalState(state, payload) {
          // eslint-disable-next-line
          state = Object.assign(state, payload);
        },
        //Notify parent app
        emitGlobalState(state) {
          if (props.setGlobalState) {
            props.setGlobalState(state);
          }
        },
      },
    };
    store.registerModule('global', globalModule);
  } else {
    //The parent application data is synchronized once every mount
    store.dispatch('global/initGlobalState', initState);
  }
};

export default registerGlobalModule;

main.jsAdd the use of global module in

import globalRegister from './store/global-register'

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

As you can see, the vuex module will call theinitGlobalStateInitialize the state issued by the parent application, and provide thesetGlobalStateMethod for external call, internal automatic notification synchronization to the parent application. The sub application is used in Vue page as follows:

export default {
  computed: {
    ...mapState('global', {
      user: state =>  state.user , // get the user information of the parent application
    }),
  },
  methods: {
    ...mapActions('global', ['setGlobalState']),
    update () {
        this.setGlobalState ('user ', {Name:' Zhang San '})
    }
  },
};

In this way, an effect is achieved: the child application does not need to know the existence of Qiankun, it only knows that there is such a global module to store information, the communication between father and son is encapsulated in the method itself, it only cares about its own information storage.

PS: this scheme also has disadvantages, because the child application will synchronize the state issued by the parent application only when it is mounted. Therefore, it is only suitable for the architecture where only one child application is mounted at a time (not suitable for the coexistence of multiple child applications); if the parent application data changes and the child application does not trigger mount, the latest data of the parent application cannot be synchronized back to the child application. If you want to achieve the coexistence of multiple child applications and the parent dynamically transfers the child, the child application still needs to use the one provided by QiankunonGlobalStateChangeAPI monitoring line, there is a better plan for students to share a discussion. The program just meets the current project requirements of the author, so it is enough. Please encapsulate it according to your own business needs.

Sub application switching loading processing

When the sub application is loaded for the first time, it is equivalent to loading a new project, which is still relatively slow. Therefore, loading has to be added.

In the official example, loading processing is done, but it needs to be introduced additionallyimport Vue from 'vue/dist/vue.esm'This will increase the package size of the main application (about 100kb increased by comparison). A loading increases 100k, obviously the cost is a little unacceptable, so we need to consider a better way.

Our main application is built with Vue, and Qiankun provides a loader method to obtain the loading status of sub applications. Therefore, it is natural to think of:staymain.jsWhen the instance is loaded, the instance will be displayed in response.Next, select a loading component

  • If the main application uses element UI or other frameworks, it can directly use the loading component provided by UI library.
  • If the main application does not introduce a UI Library in order to keep it simple, you can consider writing a loading component by yourself, or find a small loading library, such as nprogress, which I will use here.
npm install --save nprogress

The next step is to figure out how to transfer the loading status to the main applicationApp.vue。 Through the author’s experiments, it is found that,new VueThe Vue instance returned by theinstance.$children[0]To changeApp.vueData, so change itmain.js

//CSS with nprogress
import 'nprogress/nprogress.css'
import microApps from './micro-app';

//Get instance
const instance = new Vue({
  render: h => h(App),
}).$mount('#app');

//Define the loader method. When loading changes, the variable is assigned to App.vue Isloading in data of
function loader(loading) {
  if (instance && instance.$children) {
    //Instance. $children [0] is App.vue In this case, change it directly App.vue Isloading of
    instance.$children[0].isLoading = loading
  }
}

//Add loader method to sub application configuration
let apps = microApps.map(item => {
  return {
    ...item,
    loader
  }
})
registerMicroApps(apps);

start();

PS: Qiankun’s registermorapps method also monitors the life cycle of the child application, such as beforeload and aftermount. Therefore, these methods can also be used to record the loading status, but the better usage is to pass it through the loader parameter.

Transformation of main application App.vue , monitoring through WatchisLoading

<template>
  <div id="layout-wrapper">
    < div class = "layout header" > head navigation < / div >
    <div id="subapp-viewport"></div>
  </div>
</template>

<script>
import NProgress from 'nprogress'
export default {
  name: 'App',
  data () {
    return {
      isLoading: true
    }
  },
  watch: {
    isLoading (val) {
      if (val) {
        NProgress.start()
      } else {
        this.$nextTick(() => {
          NProgress.done()
        })
      }
    }
  },
  components: {},
  created () {
    NProgress.start()
  }
}
</script>

So far, the effect of loading is realized. althoughinstance.$children[0].isLoadingIt seems that the operation is more coquettish, but it does cost much less than the official examples (the volume increase is almost 0). If there is a better way, please share it in the comments section.

Extract public code

Inevitably, some methods or tool classes are needed by all sub applications. It is certainly not easy to maintain if each sub application copies a copy, so it is necessary to extract public code to one place.

Create a new one in the root directorycommonThe folder is used to store public code, such as shared by multiple Vue sub applicationsglobal-register.jsOr reusablerequest.jsandsdkAnd so on. The code is not posted here. Please look at the demo directly.

After public code extraction, how to use other applications? Common can be published as an NPM private package. The NPM private package has the following organizational forms:

  • NPM points to the local file address:npm install file:../common。 Create a common directory directly in the root directory, and then NPM directly depends on the file path.
  • NPM points to private git warehouse:npm install git+ssh://xxx-common.git
  • Publish to NPM private server.

This demo uses the first method because the base and sub applications are all assembled in a git repository. However, in actual application, it is released to NPM private server, because we will split the base and sub application into independent sub warehouses to support independent development, as will be discussed later.

It should be noted that since common does not pass through Babel and Polly, the referent should explicitly specify that the module needs to be compiled when the webpack is packaged, such as that of Vue sub application vue.config.js It is necessary to add the following sentence:

module.exports = {
  transpileDependencies: ['common'],
}

Independent application development support

A very important concept of micro front end is splitting, which is the idea of divide and conquer. It divides all business into independent and operational modules.

Is too laggy and stuck for the whole system. A product may only involve one of its sub applications, so the development of the system can only start with the sub application involved, and the independent development of the N is needed. Therefore, it is necessary to support the independent development of the sub application. If you want to support it, you will encounter the following problems:

  • How to maintain the login status of sub applications?
  • When the base is not started, how to get the data and capability of the base?

When the base is running, the login status and user information are stored on the base, and then the base is sent to the sub application through props. However, if the base does not start and only the sub application starts independently, the sub application can not get the required user information through props. Therefore, the solution can only be that both parent and child applications must implement the same set of login logic. In order to be reusable, the login logic can be encapsulated in common, and then the login related logic can be added to the logic that the sub application runs independently.

// sub-vue/src/main.js

import { store as commonStore } from 'common'
import store from './store'

if (!window.__POWERED_BY_QIANKUN__) {
  //Here is the environment for the sub application to run independently to realize the login logic of the sub application
  
  //When running independently, it also registers a store module named Global
  commonStore.globalRegister(store)
  //After the simulation login, the user information is stored in the global module
  Const userinfo = {Name: 'I am a stand-alone runtime, and my name is Zhang San'} // assume the user information retrieved after login
  store.commit('global/setGlobalState', { user: userInfo })
  
  render()
}
// ...
export async function mount (props) {
  console.log('[vue] props from main framework', props)

  commonStore.globalRegister(store, props)

  render(props)
}
// ...

!window.__POWERED_BY_QIANKUN__Indicates that the child application is not inqiankunThe independent runtime. At this time, we still need to register aglobalThe sub application can also get the user’s information from the global module, so as to smooth the environment difference between Qiankun and independent runtime.

PS: we wrote about it earlierglobal-register.jsIt is very clever and can support two environments at the same time, so the above can be accessed throughcommonStore.globalRegisterDirect reference.

Sub application independent warehouse

With the development of the project, there may be more and more sub applications. If the sub application and the base are assembled in the same git warehouse, it will become more and more bloated.

If the project has CI / CD, only the code of a certain sub application is modified, but the code submission will trigger the construction of all sub applications at the same time, which is unreasonable.

At the same time, if the development of some business sub applications is cross departmental and cross team, how to divide the authority management of code warehouse is a problem.

Based on the above problems, we have to consider migrating each application to a separate git repository. Since we have independent warehouse, the project may not be placed in the same directory, so the previousnpm i file:../commonIt is not applicable to install the common by using the method, so it is better to publish it to the company’s NPM private server or use git address form.

qiankun-exampleIn order to better display, we still put all the applications in the same git warehouse. Please don’t copy them.

Post aggregation management of sub application independent warehouse

After the sub application is independent of GIT warehouse, it can start and develop independently. At this time, there will be problems:The development environment is independent, can not see the whole picture of the application

Although it is better to focus on a certain sub application during development, there is always a time when the whole project needs to run. For example, when multiple sub applications need to rely on each other, it is necessary to have an aggregate management of GIT repository for all sub applications. The aggregate warehouse requires that all dependencies (including sub applications) can be installed with one click and the whole project can be started with one click.

Three schemes are considered here

  1. usegit submodule
  2. usegit subtree
  3. Simply put all the sub warehouses in the aggregate directory and.gitignoreDrop it.
  4. Use lerna management.

git submoduleandgit subtreeBoth of them are good sub warehouse management schemes, but the disadvantage is that the aggregation library has to synchronize the changes every time the sub application changes.

Considering that not everyone will use the aggregate warehouse, when the sub warehouse is independently developed, it will not be actively synchronized to the aggregate library. Students who use the aggregate library will have to do synchronization frequently, which is time-consuming and labor-consuming, which is not particularly perfect.

So the third scheme is more in line with the current situation of the author’s team. The aggregate library is equivalent to an empty directory. Under this directory, clone all sub warehouses, andgitignoreIn this way, the aggregate library can avoid synchronous operation.

Since all sub warehouses have been ignored, the aggregate library clone will still be an empty directory. At this time, we can write a scriptscripts/clone-all.shWrite the clone command of all sub warehouses:

#Zicangku I
git clone [email protected]

#Zicangku II
git clone [email protected]

Then initialize one in the aggregate librarypackage.json, scripts plus:

  "scripts": {
    "clone:all": "bash ./scripts/clone-all.sh",
  },

In this way, after the GIT clone aggregate library is down, thenpm run clone:allYou can click one key to clone all the sub warehouses.

As mentioned above, the aggregation library should be able to install and start the whole project with one click. We refer to Qiankun’s examples and use NPM run all to do this.

  1. Aggregation library installationnpm i npm-run-all -D
  2. Aggregate Library’s package.json Add install and start commands:
  "scripts": {
    ...
    "install": "npm-run-all --serial install:*",
    "install:main": "cd main && npm i",
    "install:sub-vue": "cd sub-vue && npm i",
    "install:sub-react": "cd sub-react && npm i",
    "start": "npm-run-all --parallel start:*",
    "start:sub-react": "cd sub-react && npm start",
    "start:sub-vue": "cd sub-vue && npm start",
    "start:main": "cd main && npm start"
  },

npm-run-allOf--serialIt means to execute one by one in sequence,--parallelMeans to run in parallel at the same time.

One click installationnpm i, one click Startnpm start

Vscode eslint configuration

If vscode is used and the eslint plug-in is used for automatic repair, because the project is in a non root directory, eslint cannot take effect. Therefore, you need to specify the working directory of eslint:

// .vscode/settings.json
{
  "eslint.workingDirectories": [
    "./main",
    "./sub-vue",
    "./sub-react",
    "./common"
  ],
  "eslint.enable": true,
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "search.useIgnoreFiles": false,
  "search.exclude": {
    "**/dist": true
  },
}

Sub applications jump to each other

In addition to clicking on the menu at the top of the page to switch to sub applications, we also need to ask sub applications to jump in other sub applications. This will involve the display of the active status of the top menusub-vueSwitch tosub-reactIn this case, the top menu needs to set thesub-reactChange to active state. There are two options:

  • The jump action of the child application is thrown up to the parent application, and the parent application does the real jump, so that the parent application knows to change the activation state and has sub components$emitThe meaning of the event to the parent component.
  • Parent app monitorhistory.pushStateEvent. When the route is found to be changed, the parent application knows whether to change the activation state.

becauseqiankunFor the time being, there is no API to encapsulate the event thrown by the child application to the parent application, such as iframepostMessageTherefore, scheme 1 is a little difficult, but the activation state can be put into the state management. The child application can synchronize the parent application by changing the value in vuex. The practice is feasible but not very good, and the maintenance of state is a little complicated in state management.

So we choose scheme two. The sub application jumps throughhistory.pushState(null, '/sub-react', '/sub-react')Therefore, the parent application tries to listen when mountedhistory.pushStateThat’s fine. becausehistory.popstateCan only listenback/forward/goBut you can’t listenhistory.pushState, so you need to do an extra global copyhistory.pushStateevent.

// main/src/App.vue
export default {
  methods: {
    bindCurrent () {
      const path = window.location.pathname
      if (this.microApps.findIndex(item => item.activeRule === path) >= 0) {
        this.current = path
      }
    },
    listenRouterChange () {
      const _wr = function (type) {
        const orig = history[type]
        return function () {
          const rv = orig.apply(this, arguments)
          const e = new Event(type)
          e.arguments = arguments
          window.dispatchEvent(e)
          return rv
        }
      }
      history.pushState = _wr('pushState')

      window.addEventListener('pushState', this.bindCurrent)
      window.addEventListener('popstate', this.bindCurrent)

      this.$once('hook:beforeDestroy', () => {
        window.removeEventListener('pushState', this.bindCurrent)
        window.removeEventListener('popstate', this.bindCurrent)
      })
    }
  },
  mounted () {
    this.listenRouterChange()
  }
}

performance optimization

Each sub application is a complete application, and each Vue sub application is packagedvue/vue-router/vuex。 From the perspective of the whole project, it is equivalent to packaging those modules many times, which will be very wasteful, so we can further optimize the performance here.

The first thing we can think of is through webpackexternalsOr the main application sends out the common module for reuse.

However, it should be noted that if all sub applications share the same module, in the long run, it is not conducive to the upgrade of sub applications, and it is difficult to achieve the best of both worlds.

Now I think the better way is: the main application can distribute some modules for its own use, and the sub application can give priority to the modules distributed by the main application. When the main application is not found, it will load it by itself; the child application can also directly use the latest version instead of the one distributed by the parent application.

This scheme refers to the practice and summary of Qiankun micro front-end solution – how to share public plug-ins among subprojects. The idea is very complete. You can have a look. This function has not been added to this project for the time being.

deploy

Now almost no articles related to the deployment of Qiankun on the Internet can be found. It may be that there is nothing easy to say. But for those who are not familiar with it, what is the best deployment scheme for Qiankun deployment? So it is necessary to talk about the deployment scheme of the author here for your reference.

The scheme is as follows:

Considering that there may be routing conflicts when the primary application and the sub application share the domain name, the sub applications may be added continuously. Therefore, we put all the sub applications in thexx.com/subapp/In this secondary directory, the root path/Leave it to the main application.

The steps are as follows:

  1. The main application and all the sub applications are packaged with a HTML, CSS, JS, static, which is uploaded to the server by directory, and the sub applications are put in a unified waysubappUnder the table of contents, finally as follows:
├── main
│   └── index.html
└── subapp
    ├── sub-react
    │   └── index.html
    └── sub-vue
        └── index.html
  1. Configure nginx. The expected value isxx.comThe root path points to the main application,xx.com/subappFor sub applications, you only need to write a copy of the configuration of sub applications, and you don’t need to change the nginx configuration when adding new sub applications. The following should be the simplest nginx configuration for micro application deployment.
server {
    listen       80;
    server_name qiankun.fengxianqi.com;
    location / {
        Root / data / Web / Qiankun / main; # the directory where the main application is located
        index index.html;
        try_files $uri $uri/ /index.html;
    }
    location /subapp {
        alias /data/web/qiankun/subapp;
        try_files $uri $uri/ /index.html;
    }

}

nginx -s reloadThen it’s OK.

In this paper, an online demo is presented

Whole station (main application) http://qiankun.fengxianqi.com/

Separate access to sub applications:

  • Subapp / sub Vue, observe the change of vuex data.
  • subapp/sub-react

Problems encountered

1、 After the react sub application is started, the main application will hang up after rendering for the first time

Practice of Qiankun from building to deployment of micro front end
The hot overload of the child application actually caused the parent application to hang up directly. At that time, I was completely confused. Fortunately, we found the relevant issues / 340, that is, disable hot overloading when copying the webpack of react (adding the following configuration to disable the hot overload will result in no hot overload. The react application has to be manually refreshed during development, isn’t it a bit uncomfortable…) :

module.exports = {
  webpack: function override(config, env) {
    //To solve the problem that the main application will hang up after access: https://github.com/umijs/qiankun/issues/340
    config.entry = config.entry.filter(
      (e) => !e.includes('webpackHotDevClient')
    );
    // ...
    return config;
  }
};

2、 Uncaught error: application ‘XX’ died in status skip_ BECAUSE_ BROKEN: [qiankun] Target container with #subapp-viewport not existed while xx mounting!

It is completely normal during local dev development. This problem will only appear when the page is opened for the first time after deployment. It will be normal after F5 refresh. It can only be repeated once after clearing the cache. This bug has been bothering for days.

The error message is very clear, that is, the DOM used to load the sub application does not exist when the main application is mounting the XX sub application. So when we first thought that Vue was the master application,#subapp-viewportYou haven’t had time to render, so try to make sure the main app is workingmountThen register the sub application.

//Main application main.js
new Vue({
  render: h => h(App),
  mounted: () => {
    //After mounted, register the sub application
    renderMicroApps();
  },
}).$mount('#root-app');

But this method can’t work, even if setTimeout is used. We need to find another way.

Finally, through debugging step by step, it is found that the project loaded a section of JS of Gaud map, which will be used when loading for the first timedocument.writeTo copy the entire HTML, it results in an error that the ා subapp viewer does not exist. Therefore, it is necessary to find a way to remove the JS file.

Episode: why does our project load the Gaud map JS? We didn’t use it in our project. At this time, we fell into a misunderstanding: Qiankun belongs to Ali, and Gaode belongs to Ali. Qiankun won’t load Gao De’s JS dynamically during rendering to do some data collection? I’m very ashamed to have this idea for an open source project… In fact, it is because our partners who write component library templates forget to remove debuggingpublic/index.htmlThis JS was used. At that time, I also went to comment on issue (covering my face and crying). I want to tell you that when you encounter a bug, you should check yourself first, and don’t easily question others.

last

This article from the beginning of the construction to the deployment of a very complete sharing of the entire architecture of some ideas and practices, I hope to help you. As an example, the best practice is not the best one, but the best one.

Example code: https://github.com/fengxianqi/qiankun-example 。

Online demo: http://qiankun.fengxianqi.com/

Separate access to online sub applications:

  • subapp/sub-vue
  • subapp/sub-react

Finally, like the students of this article, please also give a praise and little star encouragement, thank you very much for seeing here.

Some reference articles

  • Practice of micro front end in Xiaomi CRM system
  • Practice of micro front end
  • Maybe the most perfect micro front end solution you’ve ever seen
  • The practice of micro delivery group in the front end
  • Practice and summary of Qiankun micro front end scheme