This article is from a learning sharing by @onwork, a team member. I hope to share and discuss with you.

Seek to accumulate silicon step so that thousands of miles, dare to explore the beauty of life.

Main application function analysis

As a base application, the main application must have the following functions:

  1. Load “child application”, application mount initializer;
  2. Provide some necessary authentication information, such as token acquisition;
  3. Provide some field definitions for communication between applications.
  4. Unified processing of static resources;
  5. Static menus (sometimes the left menu bar doesn’t need to be a child app);
  6. Expose “master application” methods to “child application” calls;
  7. Provide global constants;
  8. Distinguish between “sub-applications” that require validation;
  9. 404 page processing (rewrite fetch, vue-Router dynamic route matching)

Based on the above list, we have a rough idea of what the main app does. What does the main app exist to do? (Actual requirements can be dynamically adjusted based on the project type).

Multi-application management under micro front end

After analyzing the functionality of the main application, it’s time to create the “main application”. But before we do that, we need to solve the small problem of application (project) management.

The end result of the micro front end is to break up a huge application into multiple applications.

So the project management from single to multiple: one “master application” + several “sub-applications”, need to consider the divide and conquer and aggregation, and many applications in maintenance frequently switching projects is not very elegant.

Micro-front-end project management: “independent and interdependent”, “sub-applications” remain independent, but can be aggregated together as a whole project to maintain and manage.

To facilitate the management of multiple applications, it is recommended to use Git Submodule for project management.

Git solves this problem with submodules. Submodules allow you to use one Git repository as a subdirectory of another Git repository. It allows you to clone another repository into your own project while keeping the commit independent.

Create base “Master Application” and business “child Application”

The template is created using vue-CLI scaffolding, so there is little difference in template creation, and the “child application” configuration is roughly the same, but the “main application” configuration is different.

Let’s start by creating four applications:

function Referred to as” application
The main application main demo-web-main
Login application login demo-web-login
System application system demo-web-system
Product application order demo-web-order
➜  demo-web vue -V
@vue/cli 4.5.9
➜  demo-web vue create demo-web-main
➜  demo-web vue create demo-web-login
➜  demo-web vue create demo-web-system
➜  demo-web vue create demo-web-order
Copy the code

Add vue.config.js to the root directory of the four applications. We will define some configuration items later.

Build the Main application

Enter the main application and add the vue environment configuration file.env.development

Demo-web-main (master) ✔ touch.env.developmentCopy the code

Add code to.env.development:

NODE_ENV = 'development'
VUE_APP_RUNTIME = ' '
VUE_APP_BASE_API_GW = '/gw'
VUE_APP_NAME = demo-web-main
VUE_APP_BASE_DEVELOPMENT_PORT = 10000
VUE_APP_PROXY_URL = '//web-main.micro.com/'
Copy the code

Modify devServer in vue.config.js:

const {
  VUE_APP_BASE_DEVELOPMENT_PORT: port, // from the.env.development constant
} = process.env;
module.exports = {
    // publicPath: './', // The primary application does not need publicPath
    devServer: {
        hot: true.disableHostCheck: true,
        port,                          // Main application port Settings: 9000
        overlay: {
            warnings: false.errors: true
        },
        headers: {
          	// All applications need this, otherwise the main application will report cross-domain when loading its children
            'Access-Control-Allow-Origin': The '*'.'Access-Control-Allow-Methods': The '*'.'Access-Control-Allow-Headers': The '*'
        },
        proxy: {
            '/gw': {
                target: 'http://www.api.com'.// Proxy destination address
                changeOrigin: true.ws: true.// Rewrite the path, usually with AXIos' baseUrl implementation proxy to solve cross-domain problems
                // We are working on the gateway
                pathRewrite: { '^/gw': ' ' },
                secure: false}}}},Copy the code

Add settings.js to demo-web-main/ SRC:

module.exports = {
    dashboard: {    // Home page can be split into sub-applications (depending on specific requirements)
        icon: ' '.title: 'home'.path: '/dashboard'.name: 'dashboard',},error404: {    / / 404 pages
        icon: ' '.title: '404'.path: '/ 404'.name: '404',},extract: {    // Open the TAB in a new TAB
        'help-center': {    // Jump to the help center
            icon: ' '.title: 'Help Center'.path: '/help-center'.name: 'goHelpCenter',}},
      
SUB_APP_PREFIX: 'subapp-viewport'.MENUS: { order: { devEntry: '//localhost:9010'.depEntry: `//web-order.demo.com`.moduleName: 'order'.// The child uses a unique table to identify this key (easy to debug) }, system: { devEntry: '//localhost:9024'.depEntry: `//web-system.demo.com`.moduleName: 'system',}},noAuthApp: [ // Applications that do not require authentication, such as login { module: 'login'.defaultRegister: true.devEntry: '//localhost:9001'.depEntry: `//web-login.demo.com`.routerBase: '/login',}}]Copy the code

Public /index.html is not an appropriate mount point to call #app, so let’s rename it:

<! -- public/index.html -->
- <div id="app"></div>
+ <div id="main-app"></div>
Copy the code

The early stages of the Main application are now complete, but it’s still a little far from up and running.

We will now access the Qiankun and run it in the demo-web-main root directory: YARN Add -D Qiankun.

In general, the Vue application main.js does the following:

  1. Handle render/mount logic;
  2. Inject third-party library dependencies;

Now we will create a new core folder under Demo-web-main/SRC to store qiankun related items.

Core /render.js pulls out the render logic

Create render. Js under demo-web-main/ SRC /core and move the initializer from main.js:

import Vue from 'vue'
import App from '.. /App.vue'
import router from '.. /router'
import store from '.. /store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h= > h(App)
- }).$mount('#app') // The mount point corresponding to the previous text change
+ }).$mount('#main-app')
Copy the code

What else is inside main.js in the main app now that the rendering logic has been removed? How did we install the third party dependencies we need? Is it all in main.js? So let’s do the separation again.

Core /install.js is extracted and injected into third-party libraries

Create install.js in core to load third-party dependencies:

import Vue from 'vue';
import Viewer from 'v-viewer';
import 'viewerjs/dist/viewer.css';
// eg. Viewer
Vue.use(Viewer, { defaultOptions: { zIndex: 9999,}}); .// Some other loading like elementui
Copy the code

Once the third-party dependencies are loaded, this is where we can launch the Main app and run around.

Core /auth.js user authentication

Create auth.js under demo-web-main/ SRC /core:

// Vuex is written on a project basis
import store from '.. /store';
// Infrastructure setup as discussed in the previous section
import { LocalStorage, } from '@fe-micro/micro-util'; 
// Import the auth.js method used to limit permissions
import qianKunStart from './app-register'; 
import defaultSettings from '@/settings';
const microAppStart = () = > {
    const token = LocalStorage.getToken();
    if (token) { // Login status To obtain the server microapplication registry
      	// Handle token state sharing
        store.dispatch('appstore/setToken', token);
        return;
    }
	  // Load by default (microapplications that do not need to be fetched by the server when not logged in)
    qianKunStart(defaultSettings.noAuthApp);
};
export default microAppStart;
Copy the code

Core /app-register.js child application load registration

Create app-register.js under demo-web-main/ SRC /core:

The subapplication loading method used in the project is based on routing configuration

import { 
  registerMicroApps, 
  setDefaultMountApp, 
  start, 
  initGlobalState, 
} from 'qiankun';
import store from '@/store';
import appStore from '.. /utils/app-store';

let props = {
    data: store.getters,
    parentStore: store,
};
let isDev = process.env.NODE_ENV === 'development';

// For loading login applications (applications that do not require tokens)
const qianKunStart = (list = []) = > {
    let apps = []; 					// Subapplication array box
    let defaultApp = null;  // The application route prefix is registered by default
    list.forEach(i= > {
        apps.push({
            name: i.module,
            entry: isDev ? i.devEntry : i.depEntry,
            container: `#subapp-viewport-${i.module}`.activeRule: i.routerBase, // The base router is written to death
            props: {
                ...props,
                routes: i.data,
                routerBase: i.routerBase,
            },
        });
        if (i.defaultRegister) defaultApp = i.routerBase;
    });
    registerMicroApps(apps);
    // Set the route prefix of the subapplication to be entered by the default subapplication
    setDefaultMountApp(defaultApp + '/');
    // Start the micro front end
    start({
        prefetch: 'all'.singular: false});// Enable the communication mechanism between qiankun applications
    appStore(initGlobalState);
};

export default qianKunStart;

// Used to load applications that require tokens
export const qkAppStart = menus= > {
    let apps = []; // Subapplication array box
    let isProd = ['production'.'prod'].includes(process.env.NODE_ENV);
    menus.forEach(i= > {
        apps.push({
            name: i.module,
            entry: isProd ? i.depEntry : i.devEntry,
            container: `#subapp-viewport-${i.module}`.activeRule: i.routerBase, // The base router other than the login is concatenated according to the URL returned by the back end
            props: {
                ...props,
                routes: i.children,
                routerBase: i.routerBase,
            },
        });
    });
    registerMicroApps(apps);
    appStore(initGlobalState);
    start({
        prefetch: 'all'.singular: false}); };Copy the code

Now we load these three js files from the main.js reference in the main application to make them work:

import './core/render';
import './core/install';
import microAppStart from './core/auth';
microAppStart();
Copy the code

Complete the steps above and the Main application is basically set up. At this time, we go to Yarn Serve to start the main project. Not only can we see that the project starts normally, but also can see that the console throws an error that login application resources cannot be loaded, because Login has not started.

So the next step is to complete the login configuration.

Setting up the Login application

Switch directory to login application:

➜ demo - web - the main ✗ (master)cd. ➜ demo - webcdDemo-web-login ➜ demo-web-login (master) loginCopy the code

Json (package.json);

{-"name": "login",
+  "name": "demo-web-login". }Copy the code

Add vue environment configuration file.env.development:

Demo-web-login (master) ✔ touch.env. Development demo-web-login (master) ✔Copy the code

Add code to.env.development:

NODE_ENV = 'development'
VUE_APP_RUNTIME = ' '
VUE_APP_BASE_API_GW = '/gw'
VUE_APP_NAME = demo-web-login
VUE_APP_BASE_DEVELOPMENT_PORT = 10001
Copy the code

Open the vue. Config. Js:

const {
  VUE_APP_NAME: name,
  VUE_APP_BASE_DEVELOPMENT_PORT: port,
} = process.env;
const dev = process.env.NODE_ENV === 'development';
module.exports = {
    publicPath: dev ? `//localhost:${port}` : '/'.outputDir: 'dist'.assetsDir: 'static'.filenameHashing: true.devServer: {
    hot: true.disableHostCheck: true,
    port,
    overlay: {
      warnings: false.errors: true,},headers: {
      'Access-Control-Allow-Origin': The '*'.// Each application requires configuration}},configureWebpack: {
    resolve: {
      alias: {
        The '@': resolve('src'),}},output: {
      // Package the child application into the UMD library format
      // library: `${name}-[name]`,
      library: 'demo-web-login'.libraryTarget: 'umd'.jsonpFunction: `webpackJsonp_${name}`,}}}Copy the code

We’ll change the div id of the public/index.html file in the root directory of the login application to app-login.

This id is related to vue initializing $mount in Core/Render. For each child application, refer to login for configuration.

We also create the core directory under the SRC directory and create install.js and life-cycle.js files.

Install.js is the repository for third-party loading, and has the same functionality as the main application.

Now let’s focus on the life-Cycle.js life cycle, which involves inter-app communication and sub-app rendering.

core/life-cycle.js

import { renderSubchild, } from '@fe-micro/micro-core';
import { routeMatch, } from '@fe-micro/micro-router';
import App from '@/App.vue';
import store from '@/store';              // login applies its own vuex
import selfRoutes from '@/router/routes'; // login applies its own routing configuration
import messages from '@/lang';

// Official communication method
import appStore from '@/utils/app-store';
const __qiankun__ = window.__POWERED_BY_QIANKUN__;
let instance = null;
let parentStore = null;
// Export the lifecycle function
const lifeCycle = () = > ({
    // It is called only once when the microapplication is initialized
    async bootstrap() {},
    // The mount method is called on each entry
    async mount(props) {
        props.parentStore.dispatch('appstore/setAppName'.'login');
        appStore(props); // Interapp communication
        parentStore = props.parentStore;
        render(props);   // Render the child application
    },
    // Switch and uninstall applications
    async unmount() {
        // Sub-application - application level keep-alive in fact, the login application does not need this other applications can copy the following
        // In-app - Each application needs to implement its own keep-alive
        const cachedInstance = instance.cachedInstance || instance;
        window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ = cachedInstance;
        const cachedNode = cachedInstance._vnode;
        if(! cachedNode.data.keepAlive) cachedNode.data.keepAlive =true;
        cachedInstance.catchRoute = {
            apps: [...instance.$router.apps]
        };
        if (instance.cachedInstance || instance) {
            instance.$destroy();
            instance = null; }},// There are also some lifecycle methods, please refer to the official website for configuration
});

// Subapplication render
const render = ({ routes, routerBase, container, i18n, }) = > {
    const macthRoutes = routeMatch(routes, routerBase); / / page for details
    const fullMacth = [...macthRoutes, ...selfRoutes];
    const fullSelf = [...selfRoutes];
    const routeBase = __qiankun__ ? routerBase : '/'; // Currently not running independently all/can be ignored
    const __routes = __qiankun__ ? fullMacth : fullSelf;
    Object.keys(messages).forEach(key= > {
        i18n && i18n.mergeLocaleMessage(key, messages[key]);
    });
    const { originInstance, } = renderSubchild({
        routes: __routes,
        routerBase: routeBase,
        store,
        parentStore,
        container,
        i18n,
        subappKey: '#app-login'.mountPoint: App,
    });
    instance = originInstance;
});
export { lifeCycle, render }; // export to quankun call
Copy the code

Let’s change the main.js of the login application:

import { loadPublicPath, } from '@fe-micro/micro-core';
import { lifeCycle, render, } from './core/life-cycle';
loadPublicPath(); // Encapsulates public-path.js provided by Qiankun
import './core/install';
const { bootstrap, mount, unmount, } = lifeCycle();
export { bootstrap, mount, unmount };

// Run independently, but not currently. I still rely on Main to run projects
const __qiankun__ = window.__POWERED_BY_QIANKUN__;
if(! __qiankun__) { render(); }Copy the code

We now run the login application: yarn Serve. If we complete the above steps we will have two Node processes:

  1. 10000The port ofmainApplication;
  2. 10001The port ofloginapplication

Since the login application of port 10001 is not independently deployed, we can only open the main application of port 10000 and rely on the main application to obtain the resources of login application for display.

Open themainWe get an error:

Yes, this error tells us that we need something like
to display the contents of our login application.

Main Load The login page is displayed

Enter the main App to modify its app.vue and add the relevant code:

HTML part:

<template>
    <div id="root" class="root-container">
        <! Enter child app view with Token -->
        <template v-if="hasToken">
            <! -- Left menu area -->
            <sidebar id="sidebar-container" class="sidebar-container"/>
            <! -- Right view -->
            <div class="container-panel">
                <div class="main-container-content">
                    <! -- Upper navigation -->
                    <Nav />
                    <! -- Subapp Render area -->
                    <div class="main-container-view">
                        <el-scrollbar class="main-scroll">
                            <div v-for="subapp in subappList" :id="subapp.key" :key="subapp.key" v-show="$route.path.startsWith(`/${subapp.pathPrefix}`)"></div>
                        </el-scrollbar>
                    </div>
                </div>
            </div>
        </template>
        <! -- Enter login view without Token -->
        <div
            v-else
            id="subapp-viewport-login"
            v-show="$route.path.startsWith('/login/')"
        ></div>
    </div>
</template>
Copy the code

Javascript part:

<script>
import { Sidebar, } from '@/components/sidebar';
import Nav from '@/components/nav/Nav.vue';
import defaultSettings from '@/settings';

export default {
    name: 'mainView'.components: {
        Sidebar,
        Nav,
    },
    computed: {
        hasToken() {
            return!!!!!this.$store.getters.token;
        },
        subappList() {
            return Object.keys(defaultSettings.MENUS).map(key= > ({
                key: `${defaultSettings.SUB_APP_PREFIX}-${key}`.pathPrefix: key, })); ,}}}; </script>Copy the code

Style section:

<style lang="scss">
@import '@/styles/mixin.scss';
html.body {
    margin: 0;
    padding: 0;
    height: 100%;
}
.root-container {
    display: flex;
    width: 100%;
    height: 100%;
}
.container-panel {
    position: relative;
    flex: 1;
    height: 100vh;
    display: flex;
    flex-flow: column;
    overflow-x: overlay;
    overflow-y: hidden;
    .main-container-content {
        flex: 1;
        display: flex;
        flex-flow: column;
        height: 100%;
        overflow-x: overlay;
        overflow-y: hidden;
        .main-container-view {
            background: $white;
            box-sizing: border-box;
            overflow-y: overlay;
            height: 100%;
            .main-scroll {
                width: 100%;
                height: 100%;
                background: #fff;
                border-radius: 4px;
                .el-scrollbar__wrap {
                    overflow-x: hidden;
                }
            }
        }
    }
}
[id^='subapp-viewport'] {
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    [id^='__qiankun_microapp_wrapper_'] {
        height: 100%;
    }
}
</style>
Copy the code

After completing the above steps, let’s run the Login application again. It looks like this:

Now that the main application and login application are both up, we can make the login page a little more beautiful, do a simple login, in fact, to set the token.

After we have the token, the MENUS we configure in the Settings make sense.

Because menu rendering is business related, I will not put the code in this article, but the key logic is nothing more than loop or recursive implementation, interested students can see the small demo at the end of my article.

We need to call the qkAppStart method in app-Register to load the “sub-application” and render the menu immediately after we get the menu data from the background.

It can be seen from the above image that the refresh is loaded:

  1. systemApplication:http://localhost:10002/
  2. orderApplication:http://localhost:10003/

Because the configuration of system and order has not been improved and not started, these two applications are marked red with error.

We can initialize system and order by referring to the configuration of main and login. Once one of the subapplications is completed, we can copy and paste the other subapplications (assuming they are based on the Vue stack) and then modify them slightly:

  1. Mount point ID: public/index.htmldivid;
  2. Port configuration: env.development correspondsmainPort inside Settings
  3. Core/Life-Cycle in the sub appsubappKeyThe mount point corresponding to public/index.html changesid
  4. Child application vue. Config. Js configureWebpack. In the output. The library

Results show

Let’s take a look at the final result:

Your browser’s address bar enter http://localhost:10000 will pass the main application of the router redirects to http://localhost:10000/login/,

DOM structure problem.

Matters needing attention

1. Router of the subapplication

The SRC /router folder of the child application should export the Routes array instead of the vue-Router instance.

2. SyntaxError: entry should not be empty

In actual business scenarios, SyntaxError: Entry should not be empty! This error is caused by incorrect entry configuration. You are advised to use IP address or domain name.

3. Path of activeRule in qkAppStart

QkAppStart: activeRule /${subapplication name}

4. Load all sub-applications in full

If all child applications are loaded and DOM is displayed upon entry, it is because there is no default child application mounted by setDefaultMountApp in app-registry.

5. Primary application Sub-applications cannot share a Vuex

We have tried various schemes for Vuex. Currently, we use Vuex to expose the “main application” to the “sub-application”. If you have a better scheme, please feel free to share.

The Vuex of the master application has been delivered to the child application through qkAppStart in app-registry. In the encapsulation method of renderSubchild in the life-cycle of the child application, it is automatically bound to the Vue.

The “sub-app” uses Vuex:

import { mapParentGetters, } from '@fe-micro/micro-core'; 
// or
this.$parentStore
Copy the code

Keep-alive between applications

The core code of keep-Alive between applications is as follows:

export const renderSubchild = ({ routes, routerBase, container, subappKey, store, parentStore, mountPoint, i18n, } = {}) = > {
    // Step pit: be sure to get this in the method, in conjunction with the life-cycle setting in the child application.
    const { __CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__, } = window;
    let instance = null;
    const router = new VueRouter({
        mode: 'history'.base: routerBase,
        routes: routes,
    })
    router.onError(err= > {
        console.log(err, 35);
        window.history.pushState(null.null.'/ 404')});/* set subapplication's routes will need when add tag */
    LocalStorage.setSubapplicationRoutes(routes.map(i= > {
        const prefix = i.path.slice(0, routerBase.length + 1);
        let path = i.path;
        if(prefix ! = =`${routerBase}/ `) {
            path = `${routerBase}${path}`;
        }
        return {
            ...i,
            path,
        }
    }));
    Vue.prototype.$parentStore = parentStore;
    if (__POWERED_BY_QIANKUN__ && __CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__) {
        const cachedInstance = __CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;

        // Get _vnode from the original Vue instance
        const cachedNode = cachedInstance._vnode;

        // Make the current route available on the original Vue instancerouter.apps.push(... cachedInstance? .catchRoute? .apps); instance =new Vue({
            router,
            store,
            i18n,
            render: () = > cachedNode
        });
        // Cache the initial Vue instance
        instance.cachedInstance = cachedInstance;

        router.onReady(() = > {
            const { path } = router.currentRoute;
            const { path: oldPath } = cachedInstance.$router.currentRoute;
            // If the current route is inconsistent with the last uninstallation, switch to the new route
            if (path !== oldPath) {
                cachedInstance.$router.push(path);
            }
        });
        instance.$mount(container ? container.querySelector(subappKey) : subappKey);
    } else {
        instance = new Vue({
            router,
            store,
            i18n,
            render: h= > h(mountPoint)
        }).$mount(container ? container.querySelector(subappKey) : subappKey)
    }
    return { originInstance: instance, originRouter: router };
};
Copy the code

Then there is the entire unmount code in life-cycle.js. If you want to implement on-demand keep-alive, just set window.**CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE** to false before uninstalling instance.$destroy( CachedInstance.

The code is also from Issues.

After that, let’s demonstrate keep-alive between apps:

Finally, a demonstration of our locally developed project:

The Demo address

Attached is the Demo address, interested partners can refer to:

function Referred to as” application
main The main application demo-web-main
login Login application demo-web-login
system System application demo-web-system
order Product application demo-web-order
server Background Fetch Menu (EGGJS) demo-server

Reference article:

  • vue-element-admin
  • wl-mfe

That is all the content of this sharing. I hope it will help you

Like the words don’t forget to move your fingers, like, collect, pay attention to three even a wave away.


About us

We are the front end team, left hand component library, right hand tool library, all kinds of technology savage growth.

A man can’t run as fast as a crowd can run. Welcome to join our team, the year of the Ox is booming forward.