preface

  • This chapter mainly share micro front-end source code, only a simple description of the theory, such as what is not understand the micro front-end, or understand the basic components of the micro front-end, etc., can be moved to the author: juejin.cn/post/702545…

  • The actual combat case, suitable for Taiwan management system, want to upgrade vuE2 to VUE3 partners.

1) Project Overview

This project case mainly uses the micro-front-end Qiankun framework to get through VUe2.6 + VUe3.0 + Vue3.2 (Vite). Contains child and parent communication,

The application name Application level Using a framework port
main The main vue2.6 8080
crm The child vue3.2 8081
sale The child vue3.0 8082

2) Basic application construction

  • Vue2: vue create main

    {" name ":" main ", "version" : "0.1.0 from", "private" : true, "scripts" : {" serve ":" vue - cli - service serve ", "build" : "Vue-cli-service build", "lint": "vue-cli-service lint", "dependencies": {"ant-design-vue": "^1.7.8", "core-js": "^ 3.6.5 qiankun", ""," ^ 2.5.1 ", "register - service - worker" : "^ 1.7.2", "vue" : "^ 2.6.11", "vue - the router" : "^ 3.2.0 js -", "cookie", "^ 2.2.1", "vuex" : "^ 3.4.0"}, "devDependencies" : {" @ vue/cli - plugin - Babel ": "~ 4.5.0 @", "vue/cli - plugin - eslint" : "~ 4.5.0", "@ vue/cli - plugin - the router" : "~ 4.5.0", "@ vue/cli - plugin - vuex" : "~ 4.5.0 @", "vue/cli - service" : "~ 4.5.0", "Babel - eslint" : "^ 10.1.0", "eslint" : "^ 6.7.2", "eslint - plugin - vue" : "^ 6.2.2 vue - the template", "- the compiler" : "^ 2.6.11"}, "eslintConfig" : {" root ": true," env ": {" node" : true}, "extends" : [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "babel-eslint" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ] }Copy the code

package.json

  • vue3: create-vite-app crm

package.json

{" name ":" CRM ", "version" : "0.0.0", "scripts" : {" serve ":" vite ", "build" : "vite build"}, "dependencies" : {" path ": "^ 0.12.7 sass", ""," ^ 1.43.2 ", "vite - plugin - style - import" : "^ 1.4.0", "vue" : "^ 3.2.16", "vue - the router" : "^ 4.0.12", "vuex" : "^ 4.0.0 0" and "vite plugin - qiankun" : "1.0.10", "vuex - persistedstate" : "^ 4.1.0"}, "devDependencies" : {@ types/js - "cookie", "^ 3.0.0", "@ types/node" : "^ 16.11.1", "@ vitejs/plugin - vue" : "^ 1.9.3", "ant - design - vue" : "^ 2.2.8 typescript", ""," ^ 4.4.3 ", "vite" : "^" 2.6.4 - beta ", "vue - TSC" : "^ 0.3.0"}}Copy the code

3) Reset the subapp mode

After the primary application and sub-application are registered separately, data communication can be completed. In this case, change the startup mode of the child application:

import { setupAntd } from "@/plugins/antd" import { routes } from "@/router" import { setupStore } from "@/store" import  { qiankunWindow, renderWithQiankun } from "vite-plugin-qiankun/dist/helper" import { createApp } from "vue" import { createRouter, createWebHistory } from "vue-router" import registerMainStore from '.. /.. /main/src/globalStore/register' import App from "./App.vue" import store from "./store" let instance: any = null const history: any = null function render(props: Any = {}) {const {container} = props instance = createApp(App) setupAntd(instance) // Import antd setupStore(instance) // Store const history = createWebHistory(qiankunWindow.__powered_by_qiankun__? "/crm" : "/") const router = createRouter({ history, routes }) instance.use(router) instance.mount(container ? container.querySelector("#app") : Document.getelementbyid ("app")) if (qiankunwindow.__powered_by_qiankun__) {console.log(" CRM is running as a child app")}} function storeMonitor(props: any) { if (props.onGlobalStateChange) { props.onGlobalStateChange((value: any, prev: Any) => {console.log(' [CRM receives data successfully]: ', value) store.dispatch("syncMainProject", value)}, }} renderWithQiankun({bootstrap() {console.log(" CRM,vue3 startup successfully ")}, mount(props) { store.dispatch("initMainProject", props) storeMonitor(props) render(props) registerMainStore(store, props) }, Unmount (props) {console.log(" CRM uninstalled ") instance.unmount() instance._container.innerhtml = "" history.destroy() // Do not uninstall Router causes other application routes to fail instance = null}}) if (! Console. log(' main app ') render()}Copy the code

4) Apply the route configuration

Qiankun uses a micro front end with two ideas:

  • 1) registerMicroApps
  • 2) loadMicroApp

In my opinion, registerMicroApps is more suitable for mall page connection, while mid-stage management system, because there is a need to share the menu bar, header, etc., loadMicroApp is more suitable.

Let’s start with the simplest mount:

Const app = loadMicroApp({name: 'CRM ', entry: 'http://localhost:8081', // corresponding routing address container: '# crm_Container', / / mount id activeRule: '/ CRM', / / forwarding address props: {/ /... With arguments}}); start();Copy the code

At this point, you also need to consider the difference between the main application and its children, the switching between different applications (using the scheme to mount multiple colleagues, not the current subproject display: None hidden), etc. You can directly see the debugged code:

export default { data() { return { loadedApp: {}, microApps: [ { name: 'crm', entry: 'http://localhost:8081', container: '#crm_Container', activeRule: '/crm', }, { name: 'sale', entry: 'http://localhost:8082', container: '#appChild2', activeRule: '/sale', }, ], }; }, computed: { ... mapGetters(['getToken']), }, methods: { isQianKun( routePath = this.$route.path ){ const microApp = this.microApps.find(item => routePath.includes(item.activeRule)); return microApp; }, goQiankun( routePath = this.$route.path ) { const loadedApp = this.loadedApp; const microApp = this.microApps.find(item => routePath.includes(item.activeRule)); If (microApp) {const childRoutePath = RoutePath.replace (Microapp.activerule, "); // If the current child application is not loaded LoadedApp [microapp.name]) {// Start loading const app = loadMicroApp({... MicroApp, props: {token: this.getToken, getGlobalState: actions.getGlobalState}}); // Start app.loadPromise.then(() => {}); LoadedApp [microapp. name] = {// Save the current subRoutes to the loadedApp cache app, subRoutes: [childRoutePath],}; Const subRoutes = loadedApp[microapp.name]. SubRoutes; const subRoutes = loadedApp[microapp.name]. if (! subRoutes.includes(childRoutePath)) { subRoutes.push(childRoutePath); } // Add keep-alive include actions.setGlobalState(loadedApp); } this.loadedApp = loadedApp; start(); ,}}};Copy the code

In this way, different routes can be controlled to access different applications. The corresponding pages are displayed by different applications.

Reference link: qiankun.umijs.org/zh/api#regi…

5) Start the common configuration

When multiple projects exist at the same time, each run will be one NPM, NPM Run serve, and so on. It would be much easier if all projects could be launched directly from outside. So, the arrangement.

The new package. Json:

{"name": "index. Js ", "devDependencies": {NPM - run - "all", "^ 4.1.5"}, "scripts" : {" install ":" NPM - run - all - serial install: * ", "install: main" : "cd main && npm install", "install:crm": "cd platform && npm install", "install:sale": "cd platform && npm install", "serve": "npm-run-all --parallel serve:*", "serve:main": "cd main && npm run serve", "serve:crm": "cd crm && npm run serve", "serve:sale": "CD sale && NPM run serve"}, "keywords": ["main", "platform"], "author":" step forward ", "license": "MIT", "__npminstall_done": false }Copy the code

6) Apply style isolation

If different applications are displayed in the same browser window, they will affect each other if not handled.

Here are a few quick tips:

  • Different project internal component libraries can be directly distinguished using BEM. You can guarantee that there is no overlap.

  • If the same page displays both Ant-Design-Vue version 1.0 and Ant-Design-Vue version 2.0, you can use the rename component library thinking.

We can change the prefix of Ant-design-vue 2.0 to ANT2 – so that it does not conflict with the original Ant -.

export default defineConfig( ... , css: { preprocessorOptions: { less: { modifyVars: { "ant-prefix": "ant2" }, javascriptEnabled: true } } } }Copy the code

APP.vue

 <div class="app">
    <a-config-provider :locale="locale" prefix-cls="ant2">
        <router-view />
    </a-config-provider>
  </div>
Copy the code

Remember to replace the corresponding Ant style file, ant- with ant2-

7) Application status sharing

At this time, communication problems of different projects need to be considered. We first introduced initGlobalState of Qiankun. Just look at the code.

Registered instance of primary application:

import { initGlobalState } from 'qiankun'; import Vue from 'vue'; import utils from ".. /utils/utils"; // parent app's initialState const initialState = vue.observable ({type: "",}); const actions = initGlobalState(initialState); Actions. OnGlobalStateChange ((state, prev) = > {the console. The log (' main application to monitor changes in the state, prev); const newState = JSON.parse( JSON.stringify(state)); console.log('newState', newState); for (const key in newState) { initialState[key] = newState[key] } }); GetGlobalState = key => {getGlobalState = key => {getGlobalState = key => {getGlobalState = key; Console. log(' master app listens to store fetch ', key); return key ? initialState[key] : initialState; }; export default actions;Copy the code

Sub-application acceptance examples:

/** * @props (props, props = {}) {if (! store || ! Store. HasModule) {return} / / obtain the initialization state const initState = props. GetGlobalState && props. GetGlobalState () | | {} / / Store the parent application's data in the child application's namespace with the fixed global if (! Store.hasmodule ('global')) {// Store const globalModule = {namespaced: true, state: initState, actions: SetGlobalState ({commit}, payload) {commit('setGlobalState', payload) commit('emitGlobalState', Payload)}, // InitGlobalState ({commit}, payload) {commit('setGlobalState', payload)},}, mutations: { setGlobalState(state, payload) { // eslint-disable-next-line state = Object.assign(state, payload) }, // Notify parent emitGlobalState(state) {console.log(' notify parent successful, parameter: ', state); if (props.setGlobalState) { props.setGlobalState(state) } }, }, } store.registerModule('global', GlobalModule)} else {// Each time mount, Store. Dispatch ('global/initGlobalState', initState)}} export default registerMainStoreCopy the code

In this case, we only need to add a listener in the child application mount life cycle to receive real-time communication from the main application:

function storeMonitor(props: any) { if (props.onGlobalStateChange) { props.onGlobalStateChange((value: any, prev: Any) => {console.log(' [child CRM received data successfully]: ', value) store.dispatch("syncMainProject", value)}, true)}}Copy the code

8) User rights are cleared

User information and authority, the author’s design is unified maintenance by the main application. If sub-applications need it, we can use the scheme of “sharing application state” above to synchronize to all sub-applications:

When we log in to the main application, we apply the same steps:

// Assume that setToken is the login method and redirectToken should be applied step by step. setToken(state , token){ state.token = token; Cookies.set('token', token); setTimeout(() => { globalStore.setGlobalState({ type: 'redirectToken', token }); }, 100); }Copy the code

Child applications receive messages:

function storeMonitor(props: any) { if (props.onGlobalStateChange) { props.onGlobalStateChange((value: any, prev: Any) => {console.log(' [CRM receives data successfully]: ', value) store.dispatch("syncMainProject", value)}, Async syncMainProject({commit, dispatch, getters}: ActionContext<IQianKunState, IStore>, obj: any) { switch (obj.type) { case "redirectToken": commit("setToken", obj? .token) break; }}Copy the code

At this point, the child application can synchronize the user status in real time. As for the user status after obtaining, how to display, that belongs to each application autonomy.

9) TAB switching

TAB switching involves two pain points, one of which is caching (discussed separately below). Another is inter-project control:

You need to go through the main application logic each time, and also check to see if the child application is invoked.

<template> <div> <a-tabs v-model="tabActive" type="editable-card" @change="onChange" @edit="onDel"> <a-tab-pane v-for="(item, index) in tabList" :key="index" :tab="item.name" :closable="true" > </a-tab-pane> </a-tabs> </div> </template> <script> import { mapGetters } from "vuex"; import qiankun from ".. /views/qiankun.js"; export default { mixins: [qiankun], data(){ return{ tabActive: Number(this.$store.getters.getActiveTabs ) } }, computed: { tabList(){ return this.$store.getters.getTabItems; }, }, watch:{ '$store.getters.getActiveTabs': function(val){ this.tabActive = val; } }, methods: { onDel(targetKey, action) { this.$store.dispatch("delTabs", targetKey); }, onChange(targetKey, action) { this.tabActive = targetKey; this.$router.push({ path: this.tabList[targetKey].path }); this.$store.commit("setActiveTabs", targetKey); this.isQianKun() && this.goQiankun(); },},}; </script>Copy the code

10) Get through to keep-alive

First, keep-Alive is autonomous for each project. We just have to maintain what we want to cache and what we don’t want to cache. These should be maintained in the main project:

Main application:

<div v-show="$route.path.startsWith('/main')">
    <keep-alive :include="getCacheTabs>
        <router-view></router-view>
    </keep-alive>
 </div>

 <div v-for="o in microApps" v-show="$route.path.startsWith(o.activeRule)" :key="o.name">
     <KeepAlive>
         <div :id="o.container.slice(1)"></div>
     </KeepAlive>
 </div>
 
Copy the code

All page redirects are processed by the main application. If a child app needs a page jump, it also goes through the main app.

DelTab: indicates whether to delete the current TAB. Name: indicates the adjusted TAB name. Obj. isRouterName: indicates the configured name. */ redirectPage(url, delTab = false, name, obj = {}) { const mainRoutes = vue.$router.options.routes[1].children; console.log(`mainRoutes`, mainRoutes); const path = url.indexOf("?" )? url.split("?" )[0] : url; const nowIndex = mainRoutes.findIndex(item => { return item.path === path }) const isExistMain = nowIndex ! == -1 if(isExistMain){const meta = obj.meta? obj.meta : mainRoutes[nowIndex].meta; const routerName = name || meta.title; vue.$store.dispatch("setTabs", { path: url, name: routerName, delTab }) vue.$router.push({ path: url }); $store. Dispatch ("setTabs", {path: url, name, delTab}) route. push({path: url}); }}Copy the code

11) Common extraction

The case demo is too small, so this case is temporarily provided for extraction.

But to do that, it’s easier to create a new Common project. If the login page needs to be reused, it can be applied to the Common project.

12) Qiankun deployment

Configure nginx directly,

server { listen 80; listen 443 ssl; server_name ****; include conf.d/ssl/ssl.conf; include conf.d/oss/oss.conf; location /crmMicro/ { proxy_pass http://**.**.**.**:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /saleMicro { proxy_pass http://**.**.**.**:8082/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { proxy_pass http://**.**.**.**:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}Copy the code

conclusion

Hurried articles and cases, do not understand the place, welcome to leave a message!

Github address: github.com/zhuangweizh…