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.0
Build, which will be added laterReact
The 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
-
Install qiankun
$YARN add Qiankun # or NPM I Qiankun -SCopy the code
-
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.
-
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
-
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
-
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
-
TAB components in main/SRC/views/layout/components/TabBar. Vue, containing function is mainly composed of switch TAB, close the TAB, etc
-
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
-
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:
-
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
-
Added shared/qiankun_actions.js to SRC
-
SRC /router/index.js route file:
- Route configuration array, need to cache the route, need to set
name
Properties, andname
The property value of the page is requiredpath
Be consistent inmeta
Object SettingskeepAlive
fortrue
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 the
qiankun
Routing prefixroutes.forEach(element= > { element.path = `The ${window.__POWERED_BY_QIANKUN__ ? '/micro-1' : ' '}${element.path}` }) Copy the code
- rewrite
router.push
androuter.history.go
Method that tells the main application to process the history stack for the corresponding TAB page and disables browser forward and backwardif (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
beforeEach
To process the pages that need to be cachedrouter.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
- Route configuration array, need to cache the route, need to set
-
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
-
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
-
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
-
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.
-
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
qiankun
Optimization of communication between main micro applications. This will be referred to latericestark
Based 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 guaranteed
path/name
Microapplication 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!