preface

The recent project I participated in used the Qiankun micro front-end architecture system, so I took it out as a basic demo example, and wrote this article to summarize some problems and solutions encountered in the development process.

Specifically, I only remember how to mount different pages of different micro-applications under multiple tabs, and realize the loading and destruction of the corresponding page cache of micro-applications. The introduction of qiankun micro-front-end will not be described here, please refer to the official documents of Qiankun.

Example code: github.com/luckyfrogg/…

The project architecture

As shown above, the Demo project consists of one main application and two micro applications. At present, the main micro applications are usedVue2.0Build, which will be added laterReactThe application.

The directory structure

├ ─ ─ main / / main application ├ ─ ─ micro - app1 / / micro application 1 └ ─ ─ micro - app2 / / micro application 2Copy the code

start

Install all the main micro-application dependencies

npm install
Copy the code

Start all applications

npm start
Copy the code

Active Application Configuration

  1. Install qiankun

    $YARN add Qiankun # or NPM I Qiankun -SCopy the code
  2. Qiankun configuration or method is on the main/SRC/config/microAppConfig js

     import store from '@/store'
     import { loadMicroApp } from 'qiankun'
     export const microAppList = [
         {
             id: 'micro1'.name: 'micro-1'.entry: 'https://127.0.0.1:5201'.container: '#micro1'.activeRule: '#/micro-1'
         },
         {
             id: 'micro2'.name: 'micro-2'.entry: 'https://127.0.0.1:5202'.container: '#micro2'.activeRule: '#/micro-2'}]/ * * *@description Check whether the current TAB is a page under the micro-application */
     export function isMicroApp(path) {
         return!!!!! microAppList.some(item= > {
             return path.startsWith(item.activeRule.substring(1))})}/ * * *@description Check whether the current TAB page belongs to the micro-application page and return the corresponding micro-application configuration item */
     export function findMicroAppByPath(path) {
         return microAppList.find(item= > {
             let activeRule = item.activeRule.substring(1)
             return path.startsWith(activeRule)
         })
     }
     / * * *@description Create a microapplication */
     export function createMicroApp(path) {
         return new Promise((resolve, reject) = > {
             constloadedMicroApps = { ... store.state.loadedMicroApps }// Manually mounted microapplication objects
             if(! isMicroApp(path)) {// Direct jump for non-micro applications
                 resolve()
                 return
             }
    
             // microapply jump processing
             / * * *@description 1. Check whether the file is manually loaded. If yes, the system directly jumps to */
             const microAppResult = findMicroAppByPath(path) // Whether the micro application jump
             if (Object.prototype.hasOwnProperty.call(loadedMicroApps, microAppResult.name)) {
                 resolve()
                 return
             }
             try {
                 loadedMicroApps[microAppResult.name] = loadMicroApp(microAppResult) // Load the microapplication
                 store.dispatch('setLoadedMicroApps', loadedMicroApps)
                 . / / the console log (' mount mounted after micro application = = > ', store. State. LoadedMicroApps)
                 resolve()
             } catch (err) {
                 reject(err)
                 console.log(err)
             }
         })
     }
    Copy the code

    CreateMicroApp method in the main application of the main/SRC/start page views/index/index. The vue jump when the next click the module, used to judge whether the application page and manually loading applications.

  3. The configuration of VUEX is listed in main/ SRC /store/, in which mutations. Js is as follows

     import Vue from 'vue'
     import { randomString } from '@/utils/tools'
     export default {
         setTabs(state, val) {
             if(! val || ! val.length) {let defaultTabs=[{
                     id: randomString(8),
                     title: 'home'.originRoute: {
                         path: '/'.query: {},
                         params: {}},realRoute: {
                         path: '/'.query: {},
                         params: {}},active: true.closeAble: false.cachePaths: [].history: [].isIframe: false
                 }]
                 localStorage.setItem('tabs'.JSON.stringify(defaultTabs))
                 state.tabs = [...defaultTabs]
                 return
             }
             localStorage.setItem('tabs'.JSON.stringify(val))
             state.tabs = val
         },
         setIframes(state, val) {
             state.iframes = val
         },
         setLoadedMicroApps(state, val) {
             state.loadedMicroApps = val
         },
     }
    Copy the code

    TabsBar. Vue watch is used to listen for changes in TAB data. LocalStorage Tabs are used to refresh the page and keep the tabs

  4. The handling of tabs is placed in main/ SRC /utils/tabs.js and mounted to the Vue prototype in main.js

     import _ from 'lodash'
     import store from '@/store'
     import router from '@/router'
     import { randomString, isIframe } from '@/utils/tools'
     import { createMicroApp, findMicroAppByPath } from '@/config/microAppConfig.js'
     import actions from '@/shared/qiankun_actions'
     class Tabs {
         constructor(){!this.getLocalTabs() && this.setLocalTabs([])
             this.initTabs()
         }
         /** * Initializes Tabs */
         initTabs() {
             this.compareTabs = this.getLocalTabs()
             this.tabs = this.getLocalTabs()
         }
         /** * Save the latest Tabs to vuex */
         setLocalTabs(tabs = this.tabs) {
             store.dispatch('setTabs', tabs)
             this.initTabs()
         }
         getLocalTabs() {
             return _.cloneDeep(JSON.parse(localStorage.getItem('tabs')))}/** * is called when a new TAB needs to be opened@param  el* /
         async openTab(el) {
             let realRoute = el
             // console.log('openTab===>', el)
             let openedTab = this.tabs.find(item= > {
                 return item.originRoute.path === el.path
             }) // Find the current Tabs that are already open
             if (openedTab && openedTab.realRoute) {
                 // If there are already open tabs, take the path of realRoute to jump
                 realRoute = openedTab.realRoute
             }
             let isExist = false
             this.tabs.forEach(item= > {
                 if (item.originRoute.path === el.path) {
                     item.active = true
                     isExist = true // It already exists
                 } else {
                     item.active = false}})let currentId = randomString(8)
             let tab = {
                 id: currentId,
                 title: el.title,
                 originRoute: {
                     path: el.path,
                     query: el.query || {},
                     params: el.params || {}
                 },
                 active: true.closeAble: true.history: [].cachePaths: [realRoute.path],
                 isIframe: isIframe(el.path)
             }
             if(! isExist) {this.tabs.push(tab)
             }
             // if (! isIframe(el.path)) {
             router
                 .replace({ path: realRoute.path, query: realRoute.query || {}, params: realRoute.params || {} })
                 .then(to= > {
                     // The TAB page will be added only after it has been successfully added
                     this.tabs.forEach(item= > {
                         if (item.originRoute.path === el.path) {
                             item.realRoute = {
                                 path: to.path,
                                 title: to.title,
                                 query: to.query,
                                 params: to.params
                             }
                         }
                     })
                     this.setLocalTabs(this.tabs)
                 })
                 .catch(() = > {
                     this.initTabs()
                 })
             // } else {
             // router.replace({ path: '/iframe', query: { id: currentId } })
             // this.setLocalTabs(this.tabs)
             // }
         }
         /** ** call */ when switching Tab
         async switchTab(el) {
             let { path, query = {}, params = {} } = el.realRoute
             this.tabs.forEach(item= > {
                 item.active = el.id === item.id
             })
             this.setLocalTabs(this.tabs)
             // if (! isIframe(path)) {
             createMicroApp(path).then(res= > {
                 router.replace({ path, query, params })
             })
             // } else {
             // router.replace({ path: '/iframe', query: { id: this.activeTab.id } })
             // }
         }
         /** * close the TAB *@description * If the active value of the current TAB is true, remove the tabs and set the active value to True. * Set active to true if none of the latter is present; * If the active value of the current TAB is not true, remove it directly */
         closeTab(el) {
             /** * The logic here needs to be changed: * There is likely to be more than one open microapp in the current TAB (LLDB: open flow Designer in the flow center TAB), so go through all the microapps, unmount or update each microapp */
             let tabLength = this.tabs.length
             let currentIndex = this.tabs.findIndex(item= > {
                 return item.id === el.id
             })
             if (el.active) {
                 let selectIndex = 0
                 if (currentIndex + 1 === tabLength) {
                     // Specify the last item, remove the previous item active set to true
                     this.tabs[currentIndex - 1].active = true
                     selectIndex = currentIndex - 1
                 } else {
                     this.tabs[currentIndex + 1].active = true
                     selectIndex = currentIndex + 1
                 }
                 let realRoute = this.tabs[selectIndex].realRoute
                 router.replace({ path: realRoute.path, query: realRoute.query || {}, params: realRoute.params || {} })
             }
             let loadedMicroApps = store.state.loadedMicroApps
             this.tabs.splice(currentIndex, 1)
             let microApp = findMicroAppByPath(el.realRoute.path)
             // console.log('this.tabs=>', this.tabs)
             try {
                 if (microApp) {
                     let currentMircoApp = loadedMicroApps[microApp.name]
                     let currentMicroAppHasLeftTab = this.tabs.some(item= > {
                         // Determine whether there are other open pages in the current micro-app. If not, destroy the micro-app directly. If yes, notify the microapplication to clear the keepAlive cache for the current TAB
                         return item.realRoute && item.realRoute.path.match(microApp.activeRule.substring(1))})if(! currentMicroAppHasLeftTab) {// Destroy the microapplication directly
                         currentMircoApp.unmount()
                         delete loadedMicroApps[microApp.name]
                         store.dispatch('setLoadedMicroApps', loadedMicroApps)
                     } else {
                         let routeNameList = [...new Set([el.realRoute.path, ...el.cachePaths])]
                         routeNameList = routeNameList.map(item= > {
                             item = item.split('/') [2]
                             return item
                         })
                         currentMircoApp.update({ props: { type: 'closeTab'.tabNameList: routeNameList } }) // When clicking close TAB, you need to notify the micro-app to destroy the current page keep-alive}}this.setLocalTabs(this.tabs)
    
                 // debugger
             } catch (error) {
                 this.setLocalTabs(this.tabs)
             }
         }
         /** * Close all non-fixed tabs and destroy the microapp or cached pages in the microapp */
         async closeAllTabs(el) {
             // Close all non-fixed tabs and destroy the microapp or cached pages in the microapp
             let firstFixedTabIndex = this.tabs.findIndex(item= > {
                 return! item.closeAble })/ / home page
             if(el && el.id ! = =this.tabs[firstFixedTabIndex].id) {
                 let realRoute = this.tabs[firstFixedTabIndex].realRoute
                 router.replace({ path: realRoute.path, query: realRoute.query || {}, params: realRoute.params || {} })
             }
             let needCloseTabs = this.tabs.filter(item= > {
                 return item.closeAble
             })
             for (let i = 0; i < needCloseTabs.length; i++) {
                 try {
                     let items = needCloseTabs[i]
                     let currentIndex = this.tabs.findIndex(citem= > {
                         return citem.id === items.id
                     })
                     this.tabs.splice(currentIndex, 1)
                     let loadedMicroApps = store.state.loadedMicroApps
                     let microApp = findMicroAppByPath(items.realRoute.path)
                     if (microApp) {
                         let currentMircoApp = loadedMicroApps[microApp.name]
                         let currentMicroAppHasLeftTab = this.tabs.some(item= > {
                             return item.realRoute && item.realRoute.path.match(microApp.activeRule.substring(1))})if (currentMircoApp) {
                             if(! currentMicroAppHasLeftTab) {// Destroy the microapplication directly
                                 await currentMircoApp.unmount()
                                 delete loadedMicroApps[microApp.name]
                                 store.dispatch('setLoadedMicroApps', loadedMicroApps)
                             } else {
                                 let routeNameList = [...new Set([items.realRoute.path, ...items.cachePaths])]
                                 routeNameList = routeNameList.map(item= > {
                                     item = item.split('/') [2]
                                     return item
                                 })
                                 await currentMircoApp.update({ props: { type: 'closeTab'.tabNameList: routeNameList } }) // When clicking close TAB, you need to notify the micro-app to destroy the current page keep-alive}}}}catch (error) {
                     console.log(error)
                 }
             }
             this.setLocalTabs([])
         }
         /** ** call */ when logging in
         async resetTabs() {
             /*this.$store.state.showApp=false this.$nextTick(()=>{ this.$store.state.showApp=true })*/
             let loadedMicroApps = store.state.loadedMicroApps
             console.log('Whether there are mounted microapplications at login =>', loadedMicroApps && Object.values(loadedMicroApps).length)
             if (loadedMicroApps && Object.values(loadedMicroApps).length) {
                 // If there are mounted microapplications, delete all of them
                 let hasRegisterMicroApps = Object.values(loadedMicroApps)
                 for (let item of hasRegisterMicroApps) {
                     try {
                         await item.unmount()
                     } catch (error) {
                         console.log(error)
                     }
                 }
             }
             store.dispatch('setLoadedMicroApps', {})
             console.log('Mounted microapplications at login =>', store.state.loadedMicroApps)
             this.setLocalTabs([])
         }
         /** * Jumps to the home page of the current TAB *@param el* /
         turnToFirstPage(el) {
             this.tabs.forEach(item= > {
                 if (el.path === item.originRoute.path) {
                     item.cachePaths = []
                     item.realRoute.name = item.name
                     item.realRoute.path = item.path
                     item.realRoute.query = item.query
                     item.realRoute.params = item.params
                 }
             })
             this.setLocalTabs(this.tabs)
    
             router.replace({ path: el.path, query: el.query || {}, params: el.params || {} })
         }
         /** * update the latest realRoute * when switching pages in the TAB@param {*} to* /
         setRealRoute(to) {
             if (!this.tabs || !this.tabs.length) return
             if (!this.currentTabHasChanged()) {
                 // Route modification, but still in the current TAB, set the new route to active is true
                 this.activeTab.cachePaths = [...new Set([...this.activeTab.cachePaths, to.path])]
                 this.activeTab.realRoute = {
                     path: to.path,
                     title: to.title,
                     query: to.query || {},
                     params: to.params || {}
                 }
             }
             this.setLocalTabs(this.tabs)
         }
         /** * Check whether the tabs have changed (add, delete, switch tabs, etc.) */
         currentTabHasChanged() {
             let r1 = this.tabs.find(item= > {
                 return item.active
             })
             let r2 = this.compareTabs.find(item= > {
                 return item.active
             })
             // debugger
             returnr1.id ! == r2.id }/** * The currently selected TAB */
         get activeTab() {
             return _.find(this.tabs, ['active'.true])}activeTabHistoryPush() {
             actions.setGlobalState({ historyAction: null }) // set it to null
             this.activeTab.history.push(this.activeTab.realRoute) // push the currently selected TAB history stack
         }
         activeTabHistoryPop() {
             actions.setGlobalState({ historyAction: null }) // set it to null
             let lastRoute = this.activeTab.history.pop() // pop The currently selected TAB history stack
             if(! lastRoute)return
             router.replace({ path: lastRoute.path, query: lastRoute.query || {}, params: lastRoute.params || {} })
         }
         get activeTabHistoryAction() {
             return {
                 null: () = > {
                     return
                 },
                 push: this.activeTabHistoryPush.bind(this),
                 pop: this.activeTabHistoryPop.bind(this)}}}let tabs = new Tabs()
     export default tabs
    Copy the code
  5. The layout of the main application in the main/SRC/views/layout/layout vue, the application of the container DOM is also stored. Note that you need to use v-show instead of V-if to determine whether the container is loaded.

     <template>
     <div class="layout-container">
         <Header></Header>
         <tab-bar></tab-bar>
         <div class="layout-main">
         <template v-show=! "" isMicroApp">
             <keep-alive>
             <router-view></router-view>
             </keep-alive>
         </template>
    
         <template v-show="isMicroApp">
             <div
             :id="item.id"
             v-for="item in microAppList"
             :key="item.id"
             v-show="isMicroApp"
             ></div>
         </template>
         </div>
     </div>
     </template>
     <script>
     import Header from "./components/Header";
     import TabBar from "./components/TabBar";
     import { microAppList, isMicroApp } from "@/config/microAppConfig.js";
     export default {
     name: "Layout".components: {
         Header,
         TabBar,
     },
     data() {
         return {
             microAppList
         };
     },
     methods: {
         isMicroApp() {
         return isMicroApp(this.$route.path); ,}}};</script>
    
     <style lang="less" scoped>
     .layout-container{
         .layout-main{
             padding: 12px; }}</style>
    Copy the code
  6. TAB components in main/SRC/views/layout/components/TabBar. Vue, containing function is mainly composed of switch TAB, close the TAB, etc

  7. Start page in the main entrance/SRC/views/index/index. The vue, click on the link to trigger openLink method

    openLink(page) {
       let { path, query, title } = page
       createMicroApp(path).then(res= > {
           this.$tabs.openTab({
               title,
               path,
               query
           })
       })
     }
    Copy the code
  8. Main/SRC /shared/qiankun_actions.js was introduced for communication between main micro applications (transferring token, user information, notifying main application to update history stack after page jump within micro applications, etc.)

Microapplication configuration

As the updating and destruction of the corresponding page cache can be controlled when the tabs are switched or closed, the main idea here is to store the pages that need to be cached in VUex during route redirecting, and introduce the array that needs to be cached in VUex through the include supported by keep-alive component in app. vue. Closing a TAB triggers the UPDATE hook function to manually remove the corresponding item from the page array.

Refer to the Vue official documentation for the use of include.

The configuration procedure is as follows:

  1. Add public-path.js in the SRC directory

     if (window.__POWERED_BY_QIANKUN__) {
     __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
     }
    Copy the code
  2. Added shared/qiankun_actions.js to SRC

  3. SRC /router/index.js route file:

    • Route configuration array, need to cache the route, need to setnameProperties, andnameThe property value of the page is requiredpathBe consistent inmetaObject SettingskeepAlivefortrue
      const routes = [
          {
              path: '/p2'.name: 'p2'.// The page (usually a list page) for which keepAlive needs to be set. Vue files and route configurations need to have a name attribute with the same value
              component: () = > import(/* webpackChunkName: "Index" */ '@/views/p2/p2.vue'),
              meta: {
                  keepAlive: true}}, {path: '/p3'.component: () = > import(/* webpackChunkName: "Index" */ '@/views/p3/p3.vue')}]Copy the code
    • Set up theqiankunRouting prefix
      routes.forEach(element= > {
          element.path = `The ${window.__POWERED_BY_QIANKUN__ ? '/micro-1' : ' '}${element.path}`
      })
      Copy the code
    • rewriterouter.pushandrouter.history.goMethod that tells the main application to process the history stack for the corresponding TAB page and disables browser forward and backward
      if (window.__POWERED_BY_QIANKUN__) {
          /** * Overwrites the router.push method to tell the main application to push the page into the history stack of the currently highlighted TAB */
      
          VueRouter.prototype.push = function push(location) {
              actions.setGlobalState({ historyAction: 'push' })
              return VueRouter.prototype.replace.call(this, location) // The reason to switch to router.replace is to avoid creating a browser history.
          }
          /** * Overwrites the router.history.go method to tell the main application to fetch the last page in the history stack of the currently highlighted TAB */
          const originalRouterHistoryGo = router.history.__proto__.go
          router.history.__proto__.go = function go(val) {
              if (val === -1) {
                  // Only go(-1) is processed
                  return actions.setGlobalState({ historyAction: 'pop'})}return originalRouterHistoryGo.call(this, val)
          }
      }
      Copy the code
    • beforeEachTo process the pages that need to be cached
      router.beforeEach((to, from, next) = > {
          let keepAlive = store.state.keepAlive
          if (to.meta.keepAlive) {
              if(! keepAlive.includes(to.name)) { keepAlive.push(to.name) store.dispatch('SET_KEEP_ALIVE', keepAlive)
              }
          }
          next()
      })
      Copy the code
  4. The main code in app.vue is as follows

     <div id="micro1">
         <keep-alive :include="keepAlive" v-if="isRouterAlive">
             <router-view></router-view>
         </keep-alive>
     </div>
    Copy the code
  5. Rewrite main.js, introduce the newly added public-path, rewrite the render function, add the life cycle of Qiankun, initialize component communication and other operations

     import "./public-path";
     import Vue from "vue";
     import App from "./App.vue";
     import router from "./router";
     import store from "./store";
     import actions, { updateMicroAppStrategy } from './shared/qiankun_actions'
     Vue.config.productionTip = false;
     let instance = null;
     function render(props = {}) {
         const { container } = props;
         instance = new Vue({
             router,
             store,
             render: (h) = > h(App),
         }).$mount(container ? container.querySelector("#micro1") : "#micro1");
     }
    
     // Independent runtime
     if (!window.__POWERED_BY_QIANKUN__) {
         render();
     }
     // Lifecycle
     export async function bootstrap(props) {
         console.log('Micro-app1 Micro-app bootstrap', props)
         // console.log('[vue] vue app bootstraped')
     }
     export function mount(props) {
         console.log('Micro-app1 Micro Application mount', props)
         actions.setActions(props)
         render(props)
     }
     export async function unmount() {
         console.log('Micro-app1 micro-app unmount')
         instance.$destroy()
         instance.$el.innerHTML = ' '
         instance = null
         store.dispatch('SET_KEEP_ALIVE'[])},export async function update(payload) {
         console.log('Micro-app1 Micro App Update')
         let { props } = payload  
         updateMicroAppStrategy[props.type](props)
     }
    Copy the code
  6. Package configuration changes (vue.config.js)

     const { name } = require('./package');
     module.exports = {
         devServer: {
             headers: {
             'Access-Control-Allow-Origin': The '*',}},configureWebpack: {
             output: {
             library: `${name}-[name]`.libraryTarget: 'umd'.// Package microapplications into umD library format
             jsonpFunction: `webpackJsonp_${name}`,}}};Copy the code

    At this point, the configuration of the main micro application is basically complete.

Problems encountered

  1. Switch from the micro application 1 TAB to the main application or micro application 2 TAB, and then switch back to the micro application 1 TAB. The page cache is invalid under the micro application 1 TAB, and the form data is lost?

    A: do not use registerMicroApps to globally registerMicroApps, which will be destroyed when the route is switched to another microapp page. The master app manually mount the micro-app using the loadMicroApp method when it needs to load the micro-app. The micro-app stores the page in vuEX in beforeEach. Keep-alive introduces the array of pages in vuEX through the include attribute. Closing a TAB triggers the UPDATE hook function to manually remove the corresponding item from the page array.

  2. Click the back button to jump to a link that is not on the current TAB?

    A: This is because forward return invokes the forward and backward stack of the browser. When clicking back, it may appear that the corresponding TAB has been closed and the TAB that needs to be highlighted cannot be found. Implement the history history stack for each TAB, save the history page data after switching off the current TAB to the corresponding TAB history stack, rewrite router.push and router.go methods in the micro application, and notify the main application to process the history stack when switching routes

preview

The late optimization

  • qiankunOptimization of communication between main micro applications. This will be referred to latericestarkBased on publish and subscribe mode, data between applications is shared through the global Window.
  • Cache optimization of microapplications. Currently, the routing of pages that need to be cached by microapplications needs to be guaranteedpath/nameMicroapplication configuration lacks flexibility. Developers of microapplication projects need to strictly follow this rule to configure routes, which increases the mental burden of developers.
  • The method of closing the TAB is optimized. At present, closing the TAB only destroys the microapplications of the currently opened page, but the actual situation is that more than one microapplication may have been mounted to a TAB, and all the microapplications or their pages in the history stack of the current TAB should be destroyed.

The last

The demo project only realized loading and jumping between multi-tab microapplications. For other Issues related to style isolation, please refer to the official documents of Qiankun or Qiankun Github Issues

This article is intended to sort out and summarize the actual development process. This project is not a best practice and is only for reference. If there are any problems or deficiencies in the article, please point out and exchange them, thank you!