Micro front-end (QianKun) Summary of landing implementation and final deployment online

It lasted less than two months. After I came to the new company, I received a new demand: “We need to split the ERP system, including PMS, OMS, WNS and so on”.

The first idea was the micro front end, so the next step was to land the solution and implement the transformation.

Graph TD consult the data --> the first implementation of the plan did not pass --> the second implementation of the plan passed --> ground transformation

background

Before the transformation, the project technology stack is Vue family bucket (vue2.6.10+element2.12.0+ WebPack4.40.2 + VUE-cli4.5.7), using dynamic menu, menu permissions, routing using history mode. Therefore, this paper is about Vue access QianKun.

Microfront-end concept

  • type<iframe></iframe>Same, except that the micro front end uses fetch to request JS and render in the specified DOM container.
  • Regardless of the technology stack, any front-end technology stack can be accessed.
  • Multiple applications can be combined and run together or separately.
  • A complex and huge project is divided into a number of micro-applications, which are developed, deployed and tested separately without mutual influence.
  • The principle is to import each child’s entry file (main.js) in the main app, parse it, and specify the render container (DOM), followed by each child setting the packaged file asUMD, and then exposed in main.js (export) Lifecycle approach (bootstrap,mount.unmount), and thenmountRender, which isnew Vue(...)And, inunmountperformdestory.

When do you need a micro front end

  • Similar to an ERP system.
  • When a large system needs to be split up between different teams.
  • There are many modules in the system, and many sub-modules in the module.

QiankunThe API used is introduced

  • registerMicroApps(apps, lifeCycles?)Automatic transmission loading module, write a good configuration, direct input, and then callstart().qiankunAutomatically listens for URL changes and calls the corresponding application leak lifecycle function.
  • start(opts?)Cooperate withregisterMicroAppsUsed when calledregisterMicroAppsAfter the run starts.
  • loadMicroApp(app, configuration?)To manually load the module, you need to listen for the Url and manually load the module yourself.
  • addGlobalUncaughtErrorHandler(handler)/removeGlobalUncaughtErrorHandler(handler)Add/remove listening application loading errors.
  • initGlobalState(state)Initialize global shared state, similar to vuex, and return three methods, respectivelysetGlobalState(state)andonGlobalStateChange((newState, oldState) => {})
    • setGlobalState(state)Setting global Status
    • onGlobalStateChange((newState, oldState) => {})Listen for global state changes

App parameter description:

parameter instructions type Whether or not the only The default value
name The application name string Y
entry Apply the access address, which is distinguished by environment variables string Y
container Apply render node string
activeRule Apply the URL prefix triggered by the last /nameSame, because it is easy to determine which application belongs to the route string Y
loader Application loading (loading) => {}
props Parameters passed to the child application string | number | array | array
// apps Application information
// name Application name (unique)
// Entry Application access address (unique)
// Container applies the render node
// activeRule app triggered URL prefix (unique)
// props is the parameter passed to the child application[{name: 'pms'.entry: 'http://localhost:7083/'.container: '#subView'.activeRule: '/module/pms'.loader: (loading) = > console.log(loading),
        props: {
            routerBase: '/module/pms'.// The route prefix of the subapplication (router base)
            routerList: [...]. .// The routing list of the subapplication. }},... ]Copy the code

Start of implementation

The project structure

| -- erp
     | -- .git
     | -- common // Public template
     | -- main / / the main application
          | -- package.json
     | -- pms / / PMS applications
          | -- package.json
     | -- oms / / oms application
          | -- package.json
     | -- tns / / TNS application
          | -- package.json
     | -- wns / / being applied
          | -- package.json
     | -- package.json
Copy the code

Routing design

First of all, the project has a login page, but the login page does not load the child application, only after the successful login, jump to the first page, to load the child application.

First unified terms: login page, start page

There’s a distinction between running together and running alone, so let’s talk about running together

Run together

In this case, you can log in to the main application and switch to the corresponding sub-page after successful login.

/login -> Login page

/module/ -> After successful login, the default startup page will be displayed. The global routing guard will judge the route and jump to the first route in the routing table according to the obtained routing table data. If the routing table does not have data, it means that the user does not have a menu, that does not have permission, directly jump back to the login page, and the prompt is OK, but still depends on your company product how to decide.

After the active application logs in successfully, the route is saved to the global state. Besides the active application addRoute, there are two ways to process the dynamic menu of sub-applications

  1. After the route guard obtains all the menus, it then determines the prefix to pass the corresponding sub-application routesappsConfiguration of thepropsPass it in.
  2. When each sub-application runs for the first time, the global routing guard determines that it runs together, directly obtains the routing table in the global state, and circulates whether it belongs to the route of the current sub-applicationaddRouteTo go in.

The component of the startup page points to Layout, and the dynamic loading route is loaded into the sub-routes of Layout to ensure that it will not be triggered when the micro-application is started for the first time and the route is jumped.

Since /module/ is the startup page, how about splicing the child pages? To give you a few examples,

/module/pms/A // PMS application A page
/module/pms/B // PMS application B page
/module/oms/A // Oms application PAGE A
Copy the code

You may wonder if the route prefixes of sub-applications are basically the same. Do you need to write them every time? Base: ‘/module/ PMS ‘for PMS applications.

new Router({
    base: '/module/pms',
    routes,
    mode: 'history'
})
Copy the code

Independent operation

Independent running means that sub-applications run independently. After running, the login page and Layout basic modules, including menus and logout, can be developed and used normally.

At this time, it is necessary to migrate the login page, Layout, App three modules to the common module by introducing; Then according to window.__POWERED_BY_QIANKUN__ determine whether the current operating environment is running independently to do the corresponding logical processing.

  • window.__POWERED_BY_QIANKUN__True, run together
  • window.__POWERED_BY_QIANKUN__False, run independently
// PMS applications run independently
/module/ PMS /login -> login page /module/pms/ -> Layout
/module/ PMS /A -> A page /module/ PMS /B -> B pageCopy the code

Code transformation

Preparation materials:

  1. The application ofHere, let’s callpms
  2. Port number to avoid conflicts with existing applications, such as 7083
  3. Fixed a prefixThis has something to do with your routing design/module/

Common Package configuration

Public packages are designed to integrate public modules such as AXIos, Element UI, dayJS, styles, Store, utils, which can be directly introduced by sub-applications.

If a plug-in is installed for the public package, it can be imported directly instead of being installed again in the child application. Here’s an example of element-UI

cd common
npm i element-ui -S
Copy the code
// The PMS child application main.js
import { Message } from 'common/node_modules/element-ui'
Message('Prompt content')
Copy the code
| -- common
     | -- src
          | -- api
          | -- components // Public components
          | -- pageg
               | -- layout
               | -- App.vue
          | -- plugins // Element, dayjs, V-viewer
          | -- sdk
               | -- fetch.js / / axios encapsulation
          | -- store
               | -- commonRegister.js // Dynamic vuex module, combined with onGlobalStateChange
          | -- styles
          | -- utils
          | -- index.js
     | -- package.json
Copy the code
  1. CD into the common
    • And in the implementationnpm init -y, will be generatedpackage.jsonFile.
    • Modify the entry file path,mainProperties forsrc/index.js."main": "src/index.js"
  2. Modify themain.jsThe content of the document, what is specific, depends on your project situation.
import store from './store'
import plugins from './plugins'
import sdk from './sdk'
import * as utils from './utils'
import globalComponents from './components/global'
import components from './components'
import * as decorator from './utils/decorator'

export { store, plugins, sdk, utils, decorator, globalComponents, components }
Copy the code
  1. commonRegister.jsGlobal state

Commonregister.js reference microfront-end qiankun state encapsulation of master applications from setup to deployment practice.

// commonRegister.js

/ * * * *@param {} vuex instance store
 * @param {props under Qiankun} props
 * @param {vue instance - the router} router
 * @param {Function} resetRouter- Reset route method */
function registerCommonModule(store, props = {}, router, resetRouter) {
  if(! store || ! store.hasModule) {return
  }

  // Get the initialization state
  // eslint-disable-next-line no-mixed-operators
  const initState = (props.getGlobalState && props.getGlobalState()) || {
    menu: null./ / the menu
    user: {}, / / user
    auth: {}, / / token permissions
    app: 'main' // Enable the application name. By default, main(main application) is used to distinguish between applications. If the application is running PMS, PMS is used to determine routes
  }

  // Store the data of the parent application in the child application. The namespace is fixed as common
  if(! store.hasModule('common')) {
    const commonModule = {
      namespaced: true.state: initState,
      actions: {
        // The child changes state and notifies the parent
        setGlobalState({ commit }, payload = {}) {
          commit('setGlobalState', payload)
          commit('emitGlobalState', payload)
        },
        // Initialization, only used to synchronize parent application data during mount
        initGlobalState({ commit }, payload = {}) {
          commit('setGlobalState', payload)
        },
        / / login
        async login({ commit, dispatch }, params) {
          // ...
          dispatch('setGlobalState')},/ / refresh token
        async refreshToken({ commit, dispatch }) {
          // ...
          dispatch('setGlobalState')},// Get user information
        async getUserInfo({ commit, dispatch }) {
          // ...
          dispatch('setGlobalState')},/ / logout
        logOut({ commit, dispatch }) {
          to(api.logout())
          commit('setUser')
          commit('setMenu')
          commit('setAuth')
          dispatch('setGlobalState')
          if (router) {
            router && router.replace && router.replace({ name: 'Login'})}else {
            window.history.replaceState(null.' '.'/login')
          }
          resetRouter && resetRouter() // Reset the route
        },
        // Get the menu
        async getMenu({ commit, dispatch, state }) {
          // ...
          dispatch('setGlobalState')},setApp({ commit, dispatch }, appName) {
          commit('setApp', appName)
          dispatch('setGlobalState')}},mutations: {
        setGlobalState(state, payload) {
          // eslint-disable-next-line
          state = Object.assign(state, payload)
        },
        // Notify the parent application
        emitGlobalState(state) {
          if (props.setGlobalState) {
            props.setGlobalState(state)
          }
        },
        setAuth(state, data) {
          state.auth = data || {}
          if (data) {
            setToken(data)
          } else {
            removeToken()
          }
        },
        setUser(state, data) {
          state.user = data || {}
        },
        setMenu(state, data) {
          state.menu = data || null
        },
        setApp(state, appName) {
          state.app = appName
        }
      },
      getters: {
          // ...
      }
    store.registerModule('common', commonModule)
  } else {
    // The parent application data is synchronized each time the mount is performed
    store.dispatch('common/initGlobalState', initState)
  }
}
Copy the code

Sub-application Configuration

  1. Modify thepackage.json:
    • nameProperties forThe application of.
    • dependenciesProperty to add a"common": ".. /common"In order to introduce public packages.
    • Modify thevue.config.jsthepublicPathattributeFixed a prefix+The application of./module/pms.
    • Set up theheaderAllow cross-domain requests.
    • The introduction ofpackage.jsonTo set uppublicPathforFixed a prefix+The application of,configureWebpack.outputSet the packaged format toUMDconvenientQiankunImporting and setting up common packagescommonParticipate in compilation.
      // vue.config.js
      const { name } = require('./package.json')
      module.exports = {
          publicPath: `/module/${name}`.// /module/pms
          devServer: {
              // The port number is configured in the environment variable
              port: process. Env. VUE_APP_PORT,headers: {
                'Access-Control-Allow-Origin': The '*'.'Cache-Control': 'no-cache'.Pragma: 'no-cache'.Expires: 0}},...configureWebpack: {
              output: {
                  // Package the child application into the UMD library format
                  library: `${name}-[name]`.libraryTarget: 'umd'.jsonpFunction: `webpackJsonp_${name}`}},// Set common to participate in compilation packaging (ES6 -> ES5)
          transpileDependencies: ['common']}Copy the code
  2. Env, it doesn’t say it has to be set here, you can set it somewhere else, depending on your project design, but it has to be unique and doesn’t conflict with an existing application
// .env
VUE_APP_PORT=7083
Copy the code
  1. insrcLet’s go ahead and create a new onepublic-path.jsfile
; (function () {
  if (window.__POWERED_BY_QIANKUN__) {
    if (process.env.NODE_ENV === 'development') {
      // eslint-disable-next-line
      __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}${process.env.BASE_URL}`
      return
    }
    // eslint-disable-next-line
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
    // __webpack_public_path__ = `${process.env.BASE_URL}/`
  }
})()

Copy the code
  1. transformmain.jsfile
// main.js
import './public-path'
import Vue from 'vue'
import Router from 'vue-router'
import store from './store'
import common from 'common'
import App from 'common/src/pages/App'

Use (common.plugins.base, isNotQiankun) // Install common plugins
Use (common.globalcomponents) // globalComponents
Vue.use(Router)

const { name: packName } = require('.. /package.json')
require('@styles/index.scss')

const _import = require('@router/_import_' + process.env.NODE_ENV)
// true: run together. False: run independently
const isNotQiankun = !window.__POWERED_BY_QIANKUN__


Vue.config.productionTip = false
let instance = null

/** * Subprojects are initialized by default@param {Object} props- Parameters passed by the main application */
function render(props) {
    const { container, routerBase, routerList, name } = props || {}
    // Initialize the route
    const router = new Router({
        base: isNotQiankun ? process.env.BASE_URL : routerBase,
        routes: routerList || [],
        mode: 'history'
    })

    instance = new Vue({
        name,
        router,
        store,
        provide: {
          name: packName,
          isNotQiankun
        },
        render: (h) = > h(App) / / utility APP. Vue
    }).$mount(container ? container.querySelector('#app') : '#app')}// If run independently, this will be executed
if (isNotQiankun) {
  // What should I do when running independently

  render()
}

** * Three life cycles of sub-application * bootstrap initialization * Mount render time * unmount */

export async function bootstrap(props) {
    // Vue.prototype.$mainBus = props.bus
}

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

export async function unmount() {
    instance.$destroy()
    instance.$el.innerHTML = ' '
    instance = null
}
Copy the code
  1. Set the global route guard
// router/config.js
import NProgress from 'common/node_modules/nprogress' // Progress bar
import store from '@store'
import { utils } from 'common'
import Layout from 'common/src/pages/layout' // Introduce the layout of cmMOM
const _import = require('@router/_import_' + process.env.NODE_ENV)
const { name } = require('.. /.. /package.json')

const isNotQiankun = !window.__POWERED_BY_QIANKUN__

// Route whitelist
const whitelist = ['/login'.'/ 404'.'/ 401'.'/']

export default {
    install(router) {
        router.beforeEach(async (to, from, next) => {
            // The main application props is passed to the child application
            // When running together, route interception is left to the master, and child applications do nothing to avoid collisions
            if(! isNotQiankun)return next()

            // When running independently, perform the open progress bar and get menu
            NProgress.start()

            // Set to launch the app. You can also set it directly in main.js.
            store.dispatch('common/setApp', name)

            // To enter the whitelist of routes, proceed to next
            if (whitelist.includes(to.path)) return next()

            // Redirect to login page without permission (token
            if(! store.getters['common/token']) return next({ path: '/login'.replace: true })
            
            // If there is a menu, determine whether to start the page (/layout/), if so, redirect to the first routing table
            if (store.getters['common/menu']) { 
                const match = utils.findFirstRoute(store.getters['common/menu'])
                if(! (to.path ==='/layout/' && match)) return next()
                const { base } = router.options
                return next({ path: match.path.replace(base, ' ')})}else {
                // If there is no route
                const [err, routes] = await utils.to(store.dispatch('common/getMenu'))
                if (err) return next('/login')
                const routerList = utils.filterRouter(routes ? [routes] : [], _import, Layout, 0)
                const { children } = routerList[0]
                children.forEach((e) = >{ router.addRoute({ ... e,path: e.path.startsWith('/')? e.path :` /${e.path}`}) }) next({ ... to,replace: true })
                return next()
            }
        })

        router.afterEach(() = > {
            isNotQiankun && NProgress.done() / / end of Progress}}})Copy the code

Active Application Configuration

  1. In the SRC createmicroDirectory, create three files in it,apps.js,store.jsandindex.js.
// micro/apps.js
import store from './store'
import Vue from 'vue'
import vuexStore from '@store'
import { OPEN_LOADING, CLOSE_LOADING } from '@store/types'
import { utils } from 'common'

// Global route prefix
export const MODULE_NAME = 'module'

/** * Get the menu by application name, such as PMS *@param {string} name- Application name *@returns {array} Application route list */
function getRoute(name) {
  const routerList = vuexStore.getters['common/menu'[]] | |const childPath = ` /${MODULE_NAME}/${name}`
  const match = routerList.find((e) = > e.path === childPath)
  if(! match)return []
  return Array.isArray(match.children) ? match.children : []
}

// Whether the production environment
const isProduction = process.env.NODE_ENV === 'production'

/** * name: sub application name Unique * Entry: sub application path unique * container: sub application rendering container fixed * activeRule: sub application triggering path unique */
const apps = [
  {
    name: 'pms'.entry: 'http://localhost:7083/'.container: '#subView'
  },
  {
    name: 'oms'.entry: 'http://localhost:8823/'.container: '#subView'}]/ / {
// name: 'childTemplate',
// entry: 'http://localhost:8082/module/childTemplate/',
// container: '#subView',
// activeRule: '/module/childTemplate',
// props: {
// routerBase: '/module/childTemplate',
// getGlobalState: store.getGlobalState,
// components: [MainComponent],
// utils: {
// mainFn
/ /}
/ /}
// }
export default (routerList) =>
  apps.map((e) = > ({
    ...e,
    entry: `${isProduction ? '/' : e.entry}${MODULE_NAME}/${e.name}/? t=${utils.rndNum(6)}`.activeRule: ` /${MODULE_NAME}/${e.name}`.// container: `${e.container}-${e.name}`, // KeepAlive
    loader: (loading) = > {
      if (loading) {
        vuexStore.commit(`load/${OPEN_LOADING}`)}else {
        vuexStore.commit(`load/${CLOSE_LOADING}`)}},props: {
      routerBase: ` /${MODULE_NAME}/${e.name}`.// Base of the subapplication route
      getGlobalState: store.getGlobalState, // Provide child applications to get public data
      routerList: getRoute(e.name, routerList), // A list of routes to be provided to the child application
      bus: Vue.prototype.$bus // Main application Bus communication}}))Copy the code
// micro/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.
export const initialState = Vue.observable({
    menu: null.user: {},
    auth: {},
    tags: [].app: 'main'
})

const actions = initGlobalState(initialState)

actions.onGlobalStateChange((newState, prev) = > {
    // console.log(' parent app changes data ', newState, prev)
    for (const 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, which represents a child object under globalState
    // If there is no key, all data is obtained
    return key ? initialState[key] : initialState
}

export default actions
Copy the code
// micro/index.js
import {
  registerMicroApps,
  // setDefaultMountApp,
  start,
  addGlobalUncaughtErrorHandler
} from 'qiankun'
import apps from './apps'
import { Message } from 'common/node_modules/element-ui'
import NProgress from 'common/node_modules/nprogress'
import router from '@router'
import { utils } from 'common'
export default function (routerList) {
  registerMicroApps(apps(routerList), {
    beforeLoad: (app) = > {
      // console.log('--------beforeLoad', app)
      NProgress.start()
    },
    beforeMount: (app) = > {
      // console.log('--------beforeMount', app)
      // console.log('[LifeCycle] before beforeMount %c%s', 'color: green; ', app.name)
    },
    afterMount: (app) = > {
      NProgress.done()
      // console.log('-------afterMount', app)
      // console.log('[LifeCycle] before afterMount %c%s', 'color: green; ', app.name)
    },
    beforeUnmount: (app) = > {
      // console.log('-------beforeUnmount', app)
      // console.log('[LifeCycle] before beforeUnmount %c%s', 'color: green; ', app.name)
    },
    afterUnmount: (app) = > {
      // console.log('-------afterUnmount', app)
      // console.log('[LifeCycle] after afterUnmount %c%s', 'color: green; ', app.name)}})// Listening error
  addGlobalUncaughtErrorHandler(
    utils.debounce((event) = > {
      const { error } = event
      if(error && ~error.message? .indexOf('LOADING_SOURCE_CODE')) {
        Message.error(`${error.appOrParcelName}Application load failed)
        router.push({ name: 'Child404'})}},200))// The application is loaded by default
  // setDefaultMountApp('/module/childTemplate/')

  start()
}
Copy the code

When using, just introduce micro.

<template> <! <div id="subView" v-loading="loading" elemental-loading-text ="loading a child app..." /> </template> <script> import micro from '@/micro' import { GET_LOADING } from '@store/types' export defalt { computed: {loading() {return this.$store. Getters [' load/${GET_LOADING} ']}}, mounted() {// load/${GET_LOADING} '}} </script>Copy the code

FAQ & pay attention to the point

  1. When loading a child application, you must write the container node of the app first, and wait until the container node is loaded before running the micro-application, that is, in the Mounted life cycle.

  2. The app name, Entry, and activeRule must be unique.

  3. App Entry suggests judging assignment by environment variables, because there can be three modes of deployment:

    1. Multiple applications correspond to multiple ports, so requests of micro-applications should be allowed to cross domains. Since the master application obtains the static resources of sub-applications through FETCH, and then resolves the static resource information of sub-applications through regex, then fetch comes down, these static resources must be required to support cross-domain.
    2. Multiple applications have one port, and the subapplication path is dynamically matched by regular expression. This is requiredThe application ofwithApply the/to the last character of the triggered URL prefixSame thing, justappthenamewithactiveRuleField.
    const isProduction = process.env.NODE_ENV === 'production'
    const apps = [
        {
            name: 'pms'
            entry: isProduction ? '/' : 'http://localhost:7083/'.activeRule: '/module/pms'. },... ]Copy the code
  4. Global state communication, there are several methods

    1. vue.observable+initGlobalState(state)+getGlobalState()+ setGlobalState()+onGlobalStateChange(handle)Method combination. throughobservableInitialize the data, make it responsible, and pass it ininitGlobalStateReturn an object and pass it throughappthepropsPass to the child application call whenstateWhen changes occur,onGlobalStateChangeWill respond to change and make changes, like thatwatch.
    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.
    export const initialState = Vue.observable({
        name: 'xxx'
    })
    
    const actions = initGlobalState(initialState)
    
    actions.onGlobalStateChange((newState, prev) = > {
      // console.log(' parent app changes data ', newState, prev)
      for (const key in newState) {
        initialState[key] = newState[key]
      }
    })
    
    // Define a method to get state to pass to the child application
    actions.getGlobalState = (key) = > {
      return key ? initialState[key] : initialState
    }
    
    // When used by child applications, it is similar to setData
    // const state = actions.getGlobalState() // get
    // state.name = '4'
    // actions.setGlobalState(state) // set
    
    export default actions
    Copy the code
    1. I’m going to go up on the example of 1, plusvuex+registerModuleDynamic module, you can expand the user module (login, get token, get menu, get application, log off) into, so that each application does not have to write the user module again,See the examplecommonRegister.jsconfiguration
  5. Route interception design, when run together, is handed over to the main application; When running independently, it is handled by the child applications running together or independently. The value of window.__powered_by_QIANkun__ can be used to determine whether to run together or independently.

  6. The routing table provides a way to check the attribution. You can set the application name to be the same as the content after the last/of the matching URL prefix, and then check whether the prefix is the same.

    ActiveRule: '/module/ PMS '}Copy the code
  7. Render error caused by multiple applications setting mount node (#app) with the same name. You can use the container node in props passed by the parent application to look for the #app below.

    // main.js
    function render(props) {
        const { container, routerBase, routerList, name } = props || {}
        new Vue({
            ...
        }).$mount(container ? container.querySelector('#app') : '#app')}Copy the code
  8. The initState of commonregister. js must have the same initial content as the initialState of the main application SRC /micro/store.js. Otherwise, the global states run together and separately will not be consistent.

  9. Vue-devtools does not display the node of the child application and cannot be debugged. This is because the child application does not have a parent node to inherit it, so you can set it manually.

// main.js
const isNotQiankun = !window.__POWERED_BY_QIANKUN__

/** * Subprojects are initialized by default@param {Object} props- Parameters passed by the main application */
function render(props) {... omit// Resolve the issue that Vue-DevTools cannot be used in Qiankun
    if(! isNotQiankun && process.env.NODE_ENV ==='development') {
        // vue-devtools
        const instanceDiv = document.createElement('div')
        instanceDiv.__vue__ = instance
        document.body.appendChild(instanceDiv)
    }
}
Copy the code

  1. To quickly create a child application, you can create a template child application childTemplate in advance, and then use node.js script to generate, which only need to change the application name, port number, but some routes, script scripts need to manually add.

KeepAlivetransform

Bread cutting switch, manage page cache.

Here is a solution that has been implemented and deployed online, manually loading child application implementations using loadMicroApp and not using registerMicroApps to prevent becoming Mediterranean.

After running together, after switching from PMS application to OMS application, if the PMS application uses multi-level routing and the Layout component is wrapped with
as cache, only the outermost App component node is left at this time. The cache for the Layout component will also disappear.

Because the routing address is used by the OMS, the PMS application cannot find a component that matches the current route. As a result, the Layout component disappears and the cache disappears.

Route changes before and after the switchover: Before: Module/PMS /A After: Module /oms/B

Components before and after the switchover: Before: app-layout (KeepAlive) After: App

It is obvious that the component does not match when the route changes.

Run independently using Layout component mode, use
inside

Look at the effect of transformation first

So we have the following transformation ideas:

Design ideas

  1. All microapplications refer to the sameAppComponent and the sameLayoutComponent, so you can putAppandLayoutPut it in the common.
  2. appthecontainerSet it to be unique and loop it out on the main application and render it to the child application.
    • Run together: for the main applicationLayoutAll subapplications are loaded, and all subapplication routes are converted to level-1 routes, and then sent to the master applicationLayoutThe routingchildren; The child applicationAppThe component is enabledKeepAlive.LayoutComponents are only used by the main application.
    // Primary application route
    const mainRoutes = [
        {
            path: '/module'.component: Layout,
            children: []}]const childRoutesFlag = [...] // All subapplication routes have been converted to level-1 routes
    mainRoutes.[0].children.push(... childRoutesFlag)Copy the code
    • Run independently: Start the applicationAppComponent not enabledKeepAlive, the use ofLayoutComponent, which acts as a container and is enabled in itKeepAlive.

Public package/SRC /pages/App component

<template> <div id="app" class="WH"> <template v-if="! isQiankun"> <RouterView class="WH app__container" /> </template> <template v-else> <Transition name="slide-left" mode="out-in" appear> <KeepAlive :include="tags"> <RouterView class="WH app__container" /> </KeepAlive> </Transition> </template> </div> </template> <script> // App.vue export default { name: 'APP', computed: { isQiankun() { return window.__POWERED_BY_QIANKUN__ }, tags() { if (! this.isQiankun) return [] const tags = this.$store.getters['common/tags'] const { base } = this.$router.options return tags .filter((e) => e.path.startsWith(base) && (e.meta || {}).keepAlive === 1) .map((e) => { const pathSplit = e.path.replace(base, '').split('/').pop() || '' return pathSplit .replace(/-(\w)/g, ($0, $1) => $1.toUpperCase()) .replace(/^([a-z]{1})/, ($0) => $0.toUpperCase()) }) } } } </script>Copy the code

Common package/SRC/Pages /Layout components

<template> <div class="layout WH"> <! -- <LayoutSide class="layout__left" :isCollapse="isCollapse" /> --> <div class="layout__right"> <! -- <LayoutHeader v-model="isCollapse" /> --> <template v-if="route.meta.isNotChild || isNotQiankun"> <ElScrollbar :vertical="false" class="scroll-container"> <div class="layout__main__container"> <Transition name="slide-left" mode="out-in" appear> <KeepAlive :include="tags"> <RouterView :key="key" class="WH layout__main__view" /> </KeepAlive> </Transition> </div> </ElScrollbar> </template> <Component :is="container" v-show="container && ! isNotQiankun" class="layout__container WH" ></Component> </div> </div> </template> <script> // Layout export default { name: 'Layout', props: {// Render the component of the child application, Only when used in the main application of the incoming / / main/router/index/js/import ChildContainer from '@ components/ChildContainer / / {/ / path: '/module', // component: Layout, // props: { // container: ChildContainer, // isNotQiankun: false // }, // children: [] // } container: { type: Object, default: null }, isNotQiankun: { type: Boolean, default: true } }, inject: { isNotQiankun: { default: false } }, computed: { route() { return this.$route }, key() { return this.$route.fullPath }, tags() { const tags = this.$store.getters['common/tags'] const { base } = this.$router.options return tags .filter( (e) => (e.path.startsWith(base) || this.isNotQiankun) && (e.meta || {}).keepAlive === 1 ) .map((e) => { const pathSplit = e.path.replace(base, '').split('/').pop() || '' return pathSplit .replace(/-(\w)/g, ($0, $1) => $1.toUpperCase()) .replace(/^([a-z]{1})/, ($0) => $0.toUpperCase()) }) } } } </script>Copy the code

The main application/SRC/components/ChildContainer components, apply colours to a drawing application

<template> <div v-loading="loading" : elemental-loading-text =" 'loading ${childName}... `" class="childContainer WH" > <ElScrollbar ref="scrollContainer" :vertical="false" class="scroll-container"> <template>  <div v-for="(item, index) in childList" v-show="activation.startsWith(item.activeRule)" :id="item.container.replace('#', ")" :key="index" class="sub-content-wrap WH" /> </template> </ElScrollbar> </div> </template> <script> // subcontainer import apps from '@micro/apps' import { GET_LOADING, OPEN_LOADING, CLOSE_LOADING } from '@store/types' import { loadMicroApp } from 'qiankun' export default { name: 'ChildContainer', data() { return { microList: new Map() } }, computed: { loading() { return this.$store.getters[`load/${GET_LOADING}`] }, childList() { return apps() }, activation() { return this.$route.path || '' }, childName({ activation, childList }) { return childList.find((item) => activation.startsWith(item.activeRule))? .name || '' } }, watch: { activation: { immediate: true, handler: 'activationHandleChange' } }, methods: {// Listen for route changes, Add/modify/delete cache Async activationHandleChange(path, oldPath) { this.$store.commit(`load/${OPEN_LOADING}`) await this.$nextTick() const { childList, microList } = this const conf = childList.find((item) => path.startsWith(item.activeRule)) if (! Conf) return this.store.com MIT (' load/${CLOSE_LOADING} ') Use this. Codestore.com MIT (' load/${CLOSE_LOADING} '); // Cache current child application const micro = loadMicroApp({... conf, router: this.$router }) microList.set(conf.activeRule, micro) micro.mountPromise.finally(() => { this.$store.commit(`load/${CLOSE_LOADING}`) }) } } } </script>Copy the code

Nginx deployment

There are three Nginx deployment solutions, and I recommend the third one if there are no special requirements

  1. Multiple applications have multiple ports. The primary application is configured with multiple subapplication paths to be forwarded to the corresponding subapplication ports.
    • Advantages: Child applications can be accessed separately
    • Disadvantages: Each time a subapplication is added, a subapplication port and forwarding must be added
    http{ # main server { listen 80; location / { try_files $uri $uri/ /index.html; root /usr/share/nginx/main; index index.html index.htm; } location /module/pms { try_files $uri $uri/ /index.html; Proxy_pass http://127.0.0.1:8081; } location /module/oms { try_files $uri $uri/ /index.html; Proxy_pass http://127.0.0.1:8082; } } # pms server { listen 8081; location / { try_files $uri $uri/ /index.html; root /usr/share/nginx/pms; index index.html index.htm; } } # oms server { listen 8082; location / { try_files $uri $uri/ /index.html; root /usr/share/nginx/oms; index index.html index.htm; }}}Copy the code
  2. Multiple applications have one port. Sub-applications need a secondary directory and only one sub-application can be configuredlocationOk, but the directory name must be the same as the main applicationLayoutThe routingpathThe application name must be the same as that of the deployed directory. For example, if there is a main application and subapplications are PMS and OMS, the directory structure is as follows:
    | -- main
         | -- index.html
    | -- module
         | -- pms
              | -- index.html
         | -- oms
              | -- index.html
    Copy the code
    • Advantages: Only one port,locationJust two, one master application, one subapplication
    • Disadvantages: All sub-applications need to be in a specified directory. After packaging, sh command is needed to change the dist directory name and location, increasing the complexity. Some O&M deployment software cannot be rolled back. The child application cannot be accessed separately
    server {
        listen       80;
        location / { # main application
            root   /data/web/qiankun/main;
            index index.html;
            try_files $uri $uri/ /index.html;
        }
        
        # ^~ matches any query starting with /module/ and stops the search. Any regular expressions will not be tested.
        The # Module must be routed to the Layout path of the main application
        location ^~ /module/ { # All subapps
            alias /data/web/qiankun/module;
            try_files $uri $uri/ /index.html; }}Copy the code
    1. Multiple applications of a port, through the regular expression to match the suffix name, withaliasorrewriteOverride request that the application name be the same as the directory in which it is deployedvue.config.jstheoutputDirProperty to change the dist directory name.

    Start by analyzing the request rule

    Request - >/module/ // The startup page of the main applicationRequest - >/module/pms/A // PMS application A pageRequest - >/module/pms/B // PMS application B pageRequest - >/module/oms/C // Oms application C pageRequest - >/module/oms/D // Oms application D page
    Copy the code

    According to the rule above, we can know the first/secondmoduleIs fixed, the second/following isThe application of, the third/is followed by the specific routing address. So you can use regular expressions to match and rewrite the request according to the above rules.

    • Advantages: One port,locationJust two, one master application, one child application; The application of the childlocationDynamic match with regular expression and userewriteDynamic url rewriting; The path after the server is packaged is the final path without rewriting the directory.
    • Cons: NginxlocationRegex match performance costs more?
    server {
        listen       80;
        location / { # main application
            root   /data/web/qiankun/main;
            index index.html;
            try_files $uri $uri/ /index.html;
        }
        
        location ^~ /module/(.*) { # All subapps
            try_files $uri $uri/ /index.html;
            if (The $1! ="") { # if there is a value, jump to the corresponding child application
                alias /data/web/qiankun/The $1
                # rewrite "/module/(.*)" "/data/web/qiankun/$1" last;
            } else { # If there is no value, jump to the main application
                alias /data/web/qiankun/main
                #rewrite "/module/(.*)" "/data/web/qiankun/main" last;}}}Copy the code

Refer to the link

(2) Thinking on communication between applications Based on the deployment of qiankun landing micro front end climb “pit” remember

Finally, recruit talent

Base: Guangzhou – Haizhu District – Modisha Metro

Guangzhou Bomi Technology is hiring

position salary content
Middle and senior front-end engineer 15-35k Vue family bucket + Nuxt + Flutter + element-UI +vant, mainly supply chain and mall
Medium senior JAVA engineer 15-35k
Middle and senior PHP engineer 15-35k
Middle and senior test engineer 15-30k
E-commerce product manager 20-40k
Supply Chain Product Manager 20-40k
Director of the test 30-50

Send me your resume, I’ll push it in. [email protected], email subject: RESUME – Name – with your resume at the front