Recently, I took charge of a new project in Qiankun. I would like to write an article to share some problems and thoughts I encountered in the actual combat.

Example code: github.com/fengxianqi/… .

Online demo:qiankun.fengxianqi.com/

Access online subapplications separately:

  • 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 independently deployed and written in React. The main technology selection of our project is VUE, so we need to consider the scheme of embedding the page. There are two main roads:

  • iframeplan
  • qiankunMicro front end solution

Both solutions meet our needs and are feasible. Iframe solutions are common but practical and low-cost. Iframe solutions cover most of the micro front-end business requirements, whereas Qiankun is more demanding.

Technical students also have a strong demand for their own growth, so when both of them can meet the business needs, we hope to apply some newer technologies and do something unknown, so we decided to choose Qiankun.

The project architecture

Background systems are generally up and down or left and right layout. The pink is the base, which is only responsible for the head navigation, and the green is the whole mounted child application. Click the head navigation to switch the child application.

According to the official examples code, the base main and other sub-applications sub-vue and Sub-react are in the root directory of the project. The initial directory structure is as follows:

├── main // Base exercises ├── React // React exercises ├── sub-vue // vue exercisesCopy the code

The base is built with VUE, and the sub-applications are React and Vue.

The base configuration

The main base is built with Vue-Cli3. It is only responsible for rendering navigation and delivering login mode. It provides a container div for the child application to mount.

In addition, the main/ SRC /micro-app.js interface is also used as an example. In addition, the main/ SRC /micro-app.js interface is also used as an example.

const microApps = [
  {
    name: 'sub-vue'.entry: '//localhost:7777/'.activeRule: '/sub-vue'.container: '#subapp-viewport'.// Div mounted by the child application
    props: {
      routerBase: '/sub-vue' // Send the route to the sub-application. The sub-application defines the route in The Qiankun environment based on the value}}, {name: 'sub-react'.entry: '//localhost:7788/'.activeRule: '/sub-react'.container: '#subapp-viewport'.// Div mounted by the child application
    props: {
      routerBase: '/sub-react'}}]export default microApps

Copy the code

Then introduce it in SRC /main.js

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();Copy the code

In app.vue, you need to declare the sub-application mount div (note that the ID must be the same) configured with micro-app.js, and the base layout related, like this:

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

Copy the code

In this way, the base is configured.

Sub-application Configuration

1. Vue sub-application

Use vue-cli to create a sub-vue sub-application in the root directory of the project. The name of the sub-vue should be the same as the name of the parent application configured in SRC /micro-app.js (so that you can directly use the package.json name as the output).

  1. newvue.config.js, change the devServer port to the same as that configured for the master application, and add cross-domainheadersandoutputConfiguration.
// The name of package.json must be the same as the name of 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, // In the. Env VUE_APP_PORT=7788, which is the same as the parent application configuration
    headers: {
      'Access-Control-Allow-Origin': The '*' // The master application gets the cross-domain response header for the subapplication}}}Copy the code
  1. newsrc/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__;
  }
})();
Copy the code
  1. src/router/index.jsInstead, expose only routes,new RouterChange it to themain.jsIn a statement.
  2. transformmain.jsLet’s introduce the abovepublic-path.jsRender, add life cycle functions, etc., and finally look like this:
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
}
Copy the code

So far, the vUE sub-application of the basic version is configured. If the Router and VUex do not need to use them, you can delete them.

React app

  1. throughnpx create-react-app sub-reactCreate a new React app.
  2. new.envFile to addPORTThe port number must be the same as that configured by the parent application.
  3. In order not toejectAll webPack configurations that we usereact-app-rewiredA webpack copy of the solution will do.
  • First of all,npm install react-app-rewired --save-dev
  • newsub-react/config-overrides.js
const { name } = require('./package.json');

module.exports = {
  webpack: function override(config, env) {
    / / solve the problem of main application access may hang up after: 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': The '*'};returnconfig; }; }};Copy the code
  1. newsrc/public-path.js.
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
Copy the code
  1. transformindex.js, the introduction ofpublic-path.js, adding life cycle 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 will only be called once when the microapplication is initialized. The next time the microapplication is re-entered, the mount hook will be called directly. Bootstrap will not be triggered again. * This is usually a place to initialize global variables, such as application-level caches that are not destroyed during the unmount phase. * /
export async function bootstrap() {
  console.log('react app bootstraped');
}
/** * Every time an application enters, it calls the mount method, where we normally trigger the application's render method */
export async function mount(props) {
  console.log(props);
  render();
}
/** * the method is called every time the application is switched out/uninstalled. Usually in this case we will uninstall the application instance of the microapplication */
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
/** * Optional lifecycle hooks that only take effect when microapps are loaded using loadMicroApp */
export async function update(props) {
  console.log('update props', props);
}

serviceWorker.unregister();
Copy the code

At this point, the base version of the React app is configured.

The advanced

Global State Management

Qiankun uses initGlobalState, onGlobalStateChange, and setGlobalState to manage the global state of the main application. By default, qiankun uses props to pass communication methods to the child application. Let’s start with the official example usage:

Main application:

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

Son:

// Props have two apis by default: onGlobalStateChange and setGlobalState
export function mount(props) {
  props.onGlobalStateChange((state, prev) = > {
    // state: indicates the state after the change; Prev Indicates the status before the change
    console.log(state, prev);
  });
  props.setGlobalState(state);
}
Copy the code

The parent and child applications communicate through the onGlobalStateChange method, which is a publil-subscribe design pattern.

Ok, the official example usage is very simple and perfectly usable, pure JavaScript syntax, does not involve any Vue or React stuff, developers are free to customize.

If we were to use the official example directly, the data would be loose and the call complex, with all the child applications declaring onGlobalStateChange to listen for state and then updating the data with setGlobalState.

Therefore, it is necessary for us to do further encapsulation design of data state. The author mainly considers the following points:

  • The main application should be kept simple, and the data delivered by the main application is pure to the sub-applicationsobjectTo better support sub-applications of different frameworks, so the main application does not need tovuex.
  • A VUE child application must inherit the data delivered by its parent application and run independently.

During the mount declaration period, the child application can obtain the latest data delivered by the main application and register the data to a VUex Module named global. The child application updates the data through the action of the Global Module and automatically synchronizes the data back to the parent application.

As a result, the child doesn’t have to know whether it’s a qiankun child or a standalone app. It just has a module called Global that updates data with actions and doesn’t need to be synchronized to the parent app (synchronized actions are wrapped in methods and callers don’t need to care). This is also in preparation for later support for the child application to launch development independently.

  • The react application is the same (I don’t use the React deep not to say).

State encapsulation of the primary application

The primary application maintains initial data for an initialState, which is of type Object, and is sent to child applications.

// main/src/store.js

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

// The initial state of the parent application
/ / Vue observables is in order to make initialState can response: https://cn.vuejs.org/v2/api/#Vue-observable.
let initialState = Vue.observable({
  user: {},});const actions = initGlobalState(initialState);

actions.onGlobalStateChange((newState, prev) = > {
  // state: indicates the state after the change; Prev Indicates the status before the 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 state and send it to the child application
actions.getGlobalState = (key) = > {
  // There is a key for fetching a child object under globalState
  // If there is no key, the value is all
  return key ? initialState[key] : initialState
}

export default actions;
Copy the code

Here are two things to note:

  • Vue.observableIf you don’t use the ue. Observable layer, it’s just a pure object that can be accessed by the child application, but it’s not responsive.This means that the page is not updated when the data changes.
  • getGlobalStateMethod, this iscontroversialThere’s a discussion on Github:Github.com/umijs/qiank….

On the one hand, the authors argue that getGlobalState is not necessary and that onGlobalStateChange is sufficient.

On the other hand, I and other pr students feel the need to provide a getGlobalState API, the reason is that the get method is more convenient to use, the child application needs not to constantly listen to stateChange events, It only needs to be initialized once with getGlobalState on the first mount. Here, I’ll stick to the idea of having the parent app issue a getGlobalState method.

Because getGlobalState is not officially supported, you need to explicitly send this method using props when registering the child application:

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-viewport'.// Div mounted by the child application
    props: {
      routerBase: item.activeRule, // Deliver the basic route
      getGlobalState: store.getGlobalState // Issue the getGlobalState method}}})export default microApps
Copy the code

Vue State encapsulation of a subapplication

When a child application is mounted, it registers the state delivered by the parent application as a vuex module called global.

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

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

  // Get the initialized state
  const initState = props.getGlobalState && props.getGlobalState() || {
    menu: [].user: {}};// Store data from the parent application to the child application, with the fixed namespace global
  if(! store.hasModule('global')) {
    const globalModule = {
      namespaced: true.state: initState,
      actions: {
        // The child app changes state and notifies the parent app
        setGlobalState({ commit }, payload) {
          commit('setGlobalState', payload);
          commit('emitGlobalState', payload);
        },
        // Initialize, which is only used to synchronize data from the parent application when mounting
        initGlobalState({ commit }, payload) {
          commit('setGlobalState', payload); }},mutations: {
        setGlobalState(state, payload) {
          // eslint-disable-next-line
          state = Object.assign(state, payload);
        },
        // Notify the parent app
        emitGlobalState(state) {
          if(props.setGlobalState) { props.setGlobalState(state); ,}}}}; store.registerModule('global', globalModule);
  } else {
    // Synchronize the parent application data each time the mount is performed
    store.dispatch('global/initGlobalState', initState); }};export default registerGlobalModule;
Copy the code

Add global-module usage to main.js:

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

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

As you can see, the VUex module calls initGlobalState to initialize the state delivered by the parent application when the child application is mounted. It also provides the setGlobalState method for external invocation, and the internal notification is automatically synchronized to the parent application. The child application is used on the VUE page as follows:

export default {
  computed: {
    ...mapState('global', {
      user: state= > state.user, // Obtain the user information of the parent application})},methods: {
    ...mapActions('global'['setGlobalState']),
    update () {
    	this.setGlobalState('user', { name: 'Joe'})}}};Copy the code

The child doesn’t need to know about Qiankun. It only knows that there is a global module that can store information. The communication between the parent and child is wrapped in the method itself, and it only cares about its own information storage.

Ps: This scheme also has disadvantages, because the child application will synchronize the state delivered by the parent application when the mount. Therefore, it is only suitable for mount one child at a time (not suitable for multiple children); If the data of the parent application 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 dynamic child transmission, the child application still needs to use the onGlobalStateChange API provided by Qiankun to listen to it. Students with better solutions can share and discuss. This scheme just meets the author’s current project needs, so it is enough. Please encapsulate it according to your own business needs.

Child application switchover processing

Loading the child application for the first time is equivalent to loading a new project, which is relatively slow, so loading must be added.

In the official example, loading is done, but import Vue from ‘Vue /dist/vue.esm’ is added, which increases the package size of the main application (by about 100KB). A loading increases by 100K, obviously the cost is a little unacceptable, so we need to consider a better method.

Our main application is built in Vue, and Qiankun provides a loader method to get loading status of the child application, so it is natural to think: ** When the main.js application is loading, send loading state to the vue instance, so that the Vue instance responds with loading. ** Select a loading component:

  • If the main application uses ElementUI or another framework, you can directly use the loading component provided by the UI library.
  • If the main application does not include a UI library to keep things simple, consider writing a loading component yourself or finding a smaller loading library, such as NProgress.
npm install --save nprogress
Copy the code

The next step is to figure out how to transfer loading state to the main App’s app.vue. $children[0] = $children[0] = $children[0] = $children[0] = $children[0] = $children[0]

// Introduce the NProgress CSS
import 'nprogress/nprogress.css'
import microApps from './micro-app';

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

// Define the loader method to assign the variable to isLoading in app.vue's data when loading changes
function loader(loading) {
  if (instance && instance.$children) {
    $children[0] = app.vue; $children[0] = app.vue
    instance.$children[0].isLoading = loading
  }
}

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

start();
Copy the code

PS: Qiankun’s registerMicroApps method also listens to the child application’s beforeLoad, afterMount and other life cycles, so you can also use these methods to record loading state, but it is certainly better to pass it through the loader parameter.

Modify the app. vue of the main application and listen for isLoading through the watch

<template>
  <div id="layout-wrapper">
    <div class="layout-header">The head of 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>
Copy the code

At this point, the loading effect is implemented. $children[0].isloading instance.$children[0].isloading instance.

Extract common code

Inevitably, there are methods or utility classes that all subapplications need, and having a copy of each subapplication is certainly not easy to maintain, so extracting common code into one place is a necessary step.

Create a common folder in the root directory to store common code, such as global-registrie. js, which can be shared by multiple vUE sub-applications, or reusable utility functions such as Request. js and SDK, etc. I don’t want to paste the code here, please watch the demo directly.

After the common code is extracted, how can other applications use it? Common can be published as an NPM private package, which can be organized in the following ways:

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

This demo adopts the first method because the base and sub-applications are all assembled in one Git repository, but the actual application is published to NPM private server, because later we will split the base and sub-applications into independent sub-repositories to support independent development, which will be discussed later.

Note that since common does not go through Babel and pollfy, the reference needs to explicitly specify that the module needs to be compiled when the WebPack is packaged. For example, the vue subapplication vue.config.js needs to add this:

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

Sub-applications can be developed independently

A very important concept in the micro front is the concept of split, the idea of divide and conquer, to break down all the businesses into individual working modules.

From the developer point of view, the system may have N application, if you start the whole system may slow card, while an demand of a product may only involves one of the application, so only when the development start involving a child of the application can be, independent start-up focused on development, therefore it is necessary to support the application of independent development. If you want to support, you will encounter the following problems:

  • How to maintain the login state of the subapplication?
  • When the base is not started, how to obtain the data and capabilities delivered by the base?

While the dock is running, the login mode and user information are stored on the dock, and the dock sends it to the child application via props. But if the dock doesn’t launch, and the child app launches independently, the child app can’t get the user information it needs using props. Therefore, the solution is for both parent and child applications to implement the same set of login logic. For reuse, you can encapsulate the login logic in common and then add login-related logic to the logic that the child application runs independently.

// sub-vue/src/main.js

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

if (!window.__POWERED_BY_QIANKUN__) {
  // This is the environment where the child application runs independently, implementing the login logic of the child application
  
  // When running independently, it also registers a Store Module named Global
  commonStore.globalRegister(store)
  // After mock login, store user information to global Module
  const userInfo = { name: 'I'm running independently when the name is Zhang SAN' } // Assume the user information obtained 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)
}
// ...
Copy the code

! __POWERED_BY_QIANKUN__ indicates that the child application is in a non-qiankun environment, that is, running independently. At this time, we still need to register a Vuex Module named Global. The sub-app can also obtain user information from the Global Module, so as to erase the environment difference between Qiankun and independent running time.

PS: we write in front of the global – register. Js writes very clever, to be able to support both two kinds of environment, so it can be through the commonStore. GlobalRegister direct reference.

Separate repository for sub-applications

As the project grows, there are likely to be more and more sub-applications, and if the sub-applications and the base are all in the same Git repository, it will become more and more bloated.

If the project has CI/CD, only the code of one subapplication is modified, but the code mention triggers the construction of all subapplications at the same time, which is unreasonable.

At the same time, if the development of sub-applications of some businesses is cross-department and cross-team, how to manage the code warehouse is another problem.

Because of these problems, we had to consider moving each application to a separate Git repository. Since we have separate repositories, the project may not be placed in the same directory again, so the previous NPM I file:.. Common installed in /common does not work, so it is best to publish to your company’s NPM server or use git address.

For the sake of better display, qiankun-example still put all the applications in the same Git repository, please do not copy.

The sub-applications are managed by aggregation after being stored independently

After the independent Git repository, the child application can start independent development independently, but at this time, there will be a problem: the development environment is independent, can not see the whole application.

Although development focused on a child when the application is better, but there is always a need of the whole project running, such as when multiple child application need to depend on each other to jump, so will have a whole project for the application of all child git aggregation of warehouse management, a key to the polymerization warehouse required to install all rely on (including the application), Start the whole project with one click.

Three main options are considered here:

  1. usegit submodule.
  2. usegit subtree.
  3. Simply put all subrepositories into an aggregate directory and.gitignoreIt off.
  4. Use LERNA management.

Git SubModules and Git Subtrees are both good sub-repository management solutions, but the disadvantage is that the aggregate repository has to synchronize changes every time the sub-application changes.

Considering that not everyone will use the aggregation repository, sub-warehouses will not actively synchronize to the aggregation repository when they are developed independently. Students who use the aggregation repository often have to do synchronization operations, which is time-consuming and laborious, and is not particularly perfect.

Therefore, the third scheme is more in line with the author’s current team situation. The aggregation repository is an empty directory where all the sub-repositories will be cloned and gitignore code submissions will be performed in the respective repository directory, so that the aggregation repository can avoid synchronization.

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

# Sub-warehouse 1
git clone [email protected]

# Sub-warehouse 2
git clone [email protected]
Copy the code

Then initialize a package.json in the aggregate library as well, adding:

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

In this way, git Clone aggregation library down, and then NPM run Clone :all can do a key clone all sub-warehouse.

As mentioned above, the aggregation library should be able to install and launch the whole project with one click. We refer to the examples in Qiankun and use npm-run-all to do this.

  1. Aggregation library installationnpm i npm-run-all -D.
  2. Added install and start commands to the aggregated library package.json:
  "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"
  },
Copy the code

–serial of npm-run-all means to execute sequentially, and –parallel means to run simultaneously and in parallel.

After the configuration, install NPM I with one click and start NPM Start with one click.

Vscode eslint configuration

If vscode is used and esLint’s plugin is used for auto-fixes, esLint cannot take effect because the project is in a non-root directory, so you also need to specify esLint’s working directory:

// .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}},Copy the code

Sub-applications jump to each other

In addition to clicking the menu at the top of the page to switch sub-apps, our requirements also require the sub-app to skip other sub-apps within the sub-app, which will involve the display of the active state of the top menu: The sub-vue is switched to the sub-React state, at which time the top menu needs to change the Sub-React state to the active state. There are two options:

  • The child app jump action is thrown up to the parent app, and the parent app does the real jump, so the parent app knows to change the activation state, and there are ideas components$emitEvent meaning to parent component.
  • Parent application listeninghistory.pushStateEvent. When a route change is detected, the parent application knows whether to change the active state.

Because Qiankun doesn’t have an API for the child app to throw events to the parent app, such as the postMessage of iframe, the first solution is a bit difficult. However, the child app can put the active state into the state management, and the child app can synchronize the parent app by changing the value in vuex. This is possible but not very good. Maintaining state is a bit more complicated in state management.

PushState (null, ‘/sub-react’, ‘/sub-react’); pushState(null, ‘/sub-react’, ‘/sub-react’); PushState = history.pushState = history.pushState = history.pushState = history.pushState = history.pushState = history.pushState = history.pushState = history.pushState = history.pushState = history.pushState = history.pushState = history.pushState = history.pushState

// 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()
  }
}
Copy the code

Performance optimization

Each subapplication is a complete application, and each VUE subapplication is packaged with a vUE/Vue-Router /vuex. From the point of view of the whole project, it is equivalent to packaging those modules several times, which is wasteful, so here we can further optimize the performance.

The first thing we can think of is using webPack’s externals or main application to deliver common modules for reuse.

However, it is important to note that if all the sub-applications share the same module, in the long run, it is not conducive to the upgrading of sub-applications, and it is difficult to achieve the best of both worlds.

Now, it is better to: the main application can deliver some modules for its own use. Sub-applications can choose the modules delivered by the main application first. If the main application does not have any modules, they can load the modules by themselves. The child application can also use the latest version without the parent application.

This scheme reference from qiankun micro front-end scheme practice and summary – how to share the common plug-in between sub-projects, thought said very complete, you can have a look, this project has not added this function.

The deployment of

Now online qiankun deployment related articles almost can not be found, maybe I think there is nothing simple to say it. But for those who are not familiar with it, what is the best deployment plan for Qiankun? So I think it is necessary to talk about the deployment plan here, for your reference.

The scheme is as follows:

Considering that there may be route conflicts when the master application and the sub-application share the domain name, sub-applications may be added continuously, so we put all sub-applications in the secondary directory xx.com/subapp/, and leave the root path/for the master application.

The steps are as follows:

  1. The main application main and all subapplications are packaged into an HTML, CSS, JS,static file and uploaded to the server in a subdirectorysubappDirectory, such as:
├── main │ ├── index.html ├─ subapp ├── sub-react │ ├─ index.html ├─ sub-vue ├─ index.htmlCopy the code
  1. Configure nginx, expected to bexx.comThe root path points to the main application,xx.com/subappFor the subapplication, you only need to write a subapplication configuration, and the new subapplication does not need to change the NGINX configuration. The following should be the most concise nGINx configuration of the microapplication deployment.
server {
    listen       80;
    server_name qiankun.fengxianqi.com;
    location / {
        root   /data/web/qiankun/main;  # 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; }}Copy the code

Nginx -s reload will do.

This article specially made online demo:

The whole station (main application) : Qiankun.fengxianqi.com/

Separate access to child applications:

  • Subapp/Sub-Vue, pay attention to the change of vuEX data.
  • subapp/sub-react

Problems encountered

After the React app is launched, the main app will die after the first render

I was completely confused when a hot reload of a child app caused the parent app to die. I’m glad I found something relevantissues/340Disable hot reloading when rewriting the React Webpack. Disable hot reloading when rewriting the React Webpack. :

module.exports = { webpack: Function override(config, env) {// Override (config, env) { https://github.com/umijs/qiankun/issues/340 config.entry = config.entry.filter( (e) => ! e.includes('webpackHotDevClient') ); / /... return config; }};Copy the code

Uncaught Error: Application ‘xx’ died in status SKIP_BECAUSE_BROKEN: [qiankun] Target container with #subapp-viewport not existed while xx mounting!

This problem only appears when the page is first opened after deployment. It will be normal again when F5 is refreshed. It will only occur once after the cache is cleared. The bug bothered me for a few days.

The error message is clear: when the main application mounts the xx sub-application, the DOM used to mount the sub-application does not exist. The #subapp-viewport hasn’t been rendered yet, so try to make sure that the main application is mounted before registering the subapplications.

{render: renderMicroApps() => {render: renderMicroApps(); }, }).$mount('#root-app');Copy the code

But that doesn’t work, even if you use setTimeout, so you have to find another way.

In the end, it was found that the project loaded a section of Autonavi JS, which used document.write to copy the entire HTML when it was first loaded, so the error #subapp-viewport did not exist was reported, so finally we need to find a way to remove the JS file.

Interlude: Why does our project load this Gaudy map JS? We didn’t use it in our project. At this time, we fell into a mistake of thinking: Qiankun belongs to Ali, and Autonavi belongs to Ali, too. Qiankun won’t secretly load Autonavi’s JS to do some data collection when rendering, right? It’s a shame to have this idea for an open source project… In fact, our component library template writer forgot to remove the js used in public/index.html debugging, and commented on the issue. When you encounter a bug, check yourself first. Don’t be too quick to question others.

The last

This article fully shares some ideas and practices of the entire architecture from inception to deployment. I hope it can be helpful to you. It is important to note that this example may not necessarily be a best practice, but it is intended as a thinking reference. Architectures are constantly changing as business needs change, and what is appropriate is best.

Example code: github.com/fengxianqi/… .

Online demo:qiankun.fengxianqi.com/

Access online subapplications separately:

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

Finally, if you like this article, please give me a thumbs-up and a little star. Thank you very much for seeing this.

Some reference articles

  • The practice of Micro front end in Mi CRM system
  • Micro front end practice
  • Probably the most complete micro front end solution you’ve ever seen
  • The practice of Micro front end in Meituan Takeout
  • Qiankun micro front-end program practice and summary

Just to catch up with the nugget essay, participate in a wave haha.

🏆 technology project stage 4 | chat micro front end of those things…