Build a micro front-end architecture suitable for your company from 0 to 1

1. Why should it be transformed into a micro front-end development model?

use background

  • Because my team is currently mainly working on a set of constructive projects serving government informatization, which is what we often call toG . This project was jointly completed by the product R&D teams of nearly a dozen bid-winning companies. Each company is responsible for a sub-business system, and the complexity of each system is unmatched by the projects I have done before. It took a year and a half for our company's subsystem from project approval to launch. Many members of the team were working on such a huge project for the first time. Therefore, there are huge challenges in terms of project experience and completing the docking with various contractors repeatedly. Of course, there is pressure to grow;
  • The front-end architecture of the project is that one of the packages assumes the role of a unified portal, and then the sub-business systems of other packages are embedded into the portal system in the form of iframe , and the technology stack of each package is unified into a single-page Vue + ElementUI .
  • Since the company is also working on a set of SAAS- based products, it is also necessary to completely separate the existing set of government-serving projects from the dependence of other contractors to achieve seamless connection with the company's SAAS- based products.
  • All subsystems of this SAAS- based product share a set of user systems, and then use tenants to land in different provinces and business parties. All our subsystems share a set of login system , the same menu structure , the same header , the same tab switching , but the business logic of each subsystem is different . As far as the front end is concerned, if we still use the traditional single-page model to develop, if there are 20 provinces to land, then we need to develop 20 sets of single-page projects, so that some of our public menus, user systems , Header, etc. have to be copied repeatedly, both the front-end development cost and the later maintenance cost are huge. Therefore, considering all aspects, it may be a good choice to access the micro front end.

Here is a simple demo example diagram of the company's SAAS products

Log in

insert image description here

System entry page
insert image description here

subsystem
insert image description here

  • The front-end architecture of the company's SAAS- based products is based on the micro front-end SingleSpa , and another front-end team took the lead in exploring and completing it. Therefore, if our project serving the government wants to be integrated into the company's SAAS products, we need to complete the connection with the company's micro front-end architecture. Before connecting, I need to figure out how the company's micro-frontend architecture is built from beginning to end. Not much to say, we will build our own micro-frontend architecture from scratch.

2. How to build a micro front-end project suitable for your own company system?

market

At present, many major manufacturers are also using the packaged framework of qiankun , which can be used out of the box. But , packaged by others is not necessarily applicable to us; So , if you want to really understand the core concept of the micro-frontend, you still need to do it yourself. Before doing it, let’s take a look at the micro-frontend The difference from the traditional single page;

  • How are micro frontends different from traditional single page applications?
traditional single page

insert image description here

micro frontend

insert image description here

  • How to build a set of micro-frontends suitable for your company from 0 to 1?
  1. From the figure above, we can clearly see that the core of the micro-frontend is actually composed of a module loader ( enter ),
    a Base module (navbar) (login + menu + header and other common parts) , and different subsystem modules . consists of parts. I also created 4 front-end projects by hand
    , namely: entry (enter), navbar (base), subsystem 1, and subsystem 2 .
    insert image description here

  2. The entry application ( enter ) loads the packaged app.js file on demand through systemjs according to the different routing identifiers of different subsystems .

Transformation of enter application

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="importmap-type" content="systemjs-importmap">
    <link rel="stylesheet" href="/style/common.css">
    <title>微前端入口</title>
    <script type="systemjs-importmap">
        {
     
     
          "imports": <%= JSON.stringify(htmlWebpackPlugin.options.meta.all) %>
        }
    </script>
    <!-- 引入样式 -->
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <link rel="preload" href="/js/single-spa.min.js" as="script" crossorigin="anonymous" />
    <link rel="preload" href="/js/vue.min.js" as="script" crossorigin="anonymous" />
    <script src='/js/minified.js'></script>
    <script src="/js/import-map-overrides.js"></script>
    <script src="/js/system.min.js"></script>
    <script src="/js/amd.min.js"></script>
    <script src="/js/named-exports.js"></script>
    <script src="/js/named-register.min.js"></script>
    <script src="/js/use-default.min.js"></script>
</head>
<body>
    <script>
        (function() {
     
     
            Promise.all([
                System.import('single-spa'),
                System.import('vue'),
                System.import('vue-router'),
                System.import('element-ui')]).then(function (modules) {
     
     
                    
            var singleSpa = modules[0];
            var Vue = modules[1];
            var VueRouter = modules[2];
            var ElementUi = modules[3];
            Vue.use(VueRouter)
            Vue.use(ElementUi)

            <% for (let app in htmlWebpackPlugin.options.meta.route) {
     
      %>
                singleSpa.registerApplication(
                    '<%= app %>',
                    function () {
     
     
                        return System.import('<%= htmlWebpackPlugin.options.meta.route[app] %>')
                    },
                    function(location) {
     
     
                        <% if (app !== 'navbar') {
     
      %>
                            return location.pathname.split('/')[1] === '<%= app %>'
                        <% } else {
     
      %>
                            return true
                        <% } %>
                    })
            <% } %>
            
            singleSpa.start();
        })
      })()
    </script>
    <import-map-overrides-full show-when-local-storage="overrides-ui"></import-map-overrides-full>
</body>
</html>
  • First, import the plug-ins that the micro-frontend depends on through script .
  • Then, we first introduce some public plug-ins needed in the project through system.js , such as vue, vue-router, ElementUi , etc. globally.
  • Finally, with the help of the meta attribute in the htmlWebpackPlugin plug-in of webpack , the packaged app.js of each sub-business system is traversed and loaded. Of course, we can also directly import asynchronously through promises without using traversal . Here, we use traversal to import mainly for better later maintenance and better scalability.

webpack configuration

new HtmlWebpackPlugin({
    filename: 'index.html',
    template: resolve(__dirname, '../index.ejs'),
    inject: false,
    title: 'title',
    minify: {
        collapseWhitespace: false
    },
    meta: {
        all: Object.assign(config[0], config[1]),
        route: config[1],
        outputTime: new Date().getTime()
    }
})
  • The app.js of the subsystem loads app.js in different environments through config , that is, the Node environment variable is used to distinguish the three environments of development , testing , and production .

development environment

module.exports = {
    "navbar": '//localhost:8002/navbar/app.js',
    "children1": '//localhost:8003/children1/app.js',
    "children2": '//localhost:8004/children2/app.js',
};

test environment

const host = process.env.HOST;
module.exports = {
    "navbar": host + '/navbar/app.js',
    "children1": host + '/children1/app.js',
    "children2": host + '/children2/app.js',
};

Production Environment

const host = process.env.HOST;
module.exports = {
    "navbar": host + '/navbar/app.js',
    "children1": host + '/children1/app.js',
    "children2": host + '/children2/app.js',
};

The host here is the access domain name of the test and production environment that we packaged and built in the entry.

"build": "rimraf dist && cross-env NODE_ENV=production HOST=//spa.caoyuanpeng.com:9001 webpack --config build/webpack.prod.config.js"

The above are the core configuration steps of the entry application ( enter ). Of course, we need to pay attention to the packaging of enter here. Our libraryTarget uniformly adopts the UMD packaging mode.

output: {
    path: resolve(__dirname, '../dist'),
    publicPath: '/',
    filename: '[name].js',
    chunkFilename: 'js/[name]-[chunkhash:6].js',
    library: 'app',
    libraryTarget: 'umd'
}
  1. About the transformation of navbar application

Package
webpack.base.config.js

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HappyPack = require('happypack');
const BasePlugins = require('./plugins');

const { resolve } = path;
const isDevMode = process.env.NODE_ENV === 'development';

module.exports = {
    devtool: process.env.NODE_ENV !== 'production' ? 'source-map' : 'none',
    // 入口
    entry: {
        app: ['webpack-hot-middleware/client', resolve(__dirname, main)]
    },
    // 出口
    output: {
        filename: 'app.js',
        path: resolve(__dirname, '../dist'),
        chunkFilename: 'js/[name]-[chunkhash:6].js',
        publicPath: isDevMode ? '/' : '/navbar',
        // library: 'navbar',
        libraryTarget: 'umd'
    },
    externals: isDevMode ? {} : ['vue', 'vue-router', 'element-ui'],
    plugins: [
        ...BasePlugins,
        new MiniCssExtractPlugin({
            filename: 'css/[name].[hash:6].css',
            chunkFilename: 'css/[id].[hash:6].css'
        }),
        new HappyPack({
            /*
             * 必须配置
             */
            // id 标识符,要和 rules 中指定的 id 对应起来
            id: 'babel',
            // 需要使用的 loader,用法和 rules 中 Loader 配置一样
            // 可以直接是字符串,也可以是对象形式
            loaders: ['babel-loader?cacheDirectory']
        })
    ],
    module: {
        rules: [
            {
                test:/\.css$/,
                use:[
                    {
                        loader: isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader 
                    },
                    'css-loader', {
                    loader: 'postcss-loader',
                    options: {
                        plugins: [require('autoprefixer')]
                    }
                    }
                ]
              },
              {
                test: /\.less$/,
                use:[
                    {
                        loader: isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader
                    },
                    'css-loader',
                    'less-loader',
                    {
                        loader: 'postcss-loader',
                        options: {
                            plugins: [require('autoprefixer')]
                        }
                    }
                ]
              },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: ['happypack/loader?id=babel'],
            },
            {
                test: /\.(jpg|jpeg|png|gif)$/,
                loaders: 'url-loader',
                exclude: /node_modules/,
                options: {
                    limit: 8192,
                    outputPath: 'img/',
                    name: '[name]-[hash:6].[ext]'
                }
            },
            {
                test: /\.(woff|woff2|svg|eot|ttf)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            outputPath: 'fonts/',
                            name: '[name].[ext]'
                        }
                    }
                ]
            },
            {
                test: /\.vue$/,
                use: ['vue-loader']
            }
        ]
    },
    resolve: {
        extensions: ['.js', 'json', '.less', '.css', '.vue'],
        alias: {
            vue$: 'vue/dist/vue.common.js',
            '@': resolve(__dirname, '../src'),
            'pages': resolve(__dirname, '../src/pages'),
        }
    }
};
  • We use externals to remove vue, vue-router, and element-ui when packaging the production environment and the test environment , because these plug-ins have been introduced in our enter , so only in the development environment in the navbar system It will be used, and the test and production environments do not need to be introduced repeatedly.
  • publicPath: '/navbar' defines the packaged virtual path, which must be specified here, and the name cannot be the same as that of each sub-business system;
  • libraryTarget: 'umd' , still use the packaging mode of UMD

Entry transformation of navbar system

base.js

import '@babel/polyfill';
import { setPublicPath } from 'systemjs-webpack-interop';
import Vue from 'vue';
import VueRouter from 'vue-router';
import Element from 'element-ui';
import singleSpaVue from 'single-spa-vue';
import routes from '../router';

const baseFn = () => {
    // 默认控制台不输出vue官方打印日志
    Vue.config.productionTip = false;
    // 使用devtools调试
    Vue.config.devtools = true;
    // 注册navbar
    setPublicPath('navbar');
    // 生成vue-router实例
    const router = new VueRouter({
        mode: 'history',
        routes
    });
    
    Vue.use(VueRouter);
    Vue.use(Element);
    // appOptions抽离
    const appOptions = {
        render: h => <div id="navbar">
            <router-view></router-view>
        </div>,
        router
    };
    // 注册single-spa-vue实例
    const vueLifecycles = singleSpaVue({
        Vue,
        appOptions
    });

    return vueLifecycles;
}

export default baseFn;

Here, register the navbar through setPublicPath in systemjs-webpack-interop , and then mount the vue instance, router and vuex to singleSpaVue .

main.dev.js

import BaseFn from './base';

BaseFn();

main.prod.js

import BaseFn from './base';

const vueLifecycles = BaseFn();

export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

In the production environment, mount vueLifecycles to the hooks of the three single-spa cycles of bootstrap, mount, and unmount .

Retrofit of page routing

import View from '@/pages/components/view';
const Template = () => import(/* webpackChunkName: "index" */ '@/pages/index');

const routes = [
    {
        path: '/navbar',
        component: Template,
        meta: {
            title: '菜单'
        },
        children: [
            {
                path: '*',
                component: View,
                meta: {
                    title: ''
                }
            }
        ]
    }
];

export default routes;

We need to add a unified access prefix navbar in front of the route , so that different app.js can be loaded on demand through different route access prefixes in the entry enter .One thing to pay special attention to here is that our purpose is to always let the navbar application load without destroying it. Here we need to set the path under children to *

  1. About the modification of the subsystem application

The transformation of the subsystem is actually similar to the transformation of the above navbar, nothing more than some differences in the configuration of webpack and the routing of the page.

output: {
    filename: 'app.js',
    path: resolve(__dirname, '../dist'),
    chunkFilename: 'js/[name]-[chunkhash:6].js',
    publicPath: isDevMode ? '/' : '/children2',
    library: 'children2',
    libraryTarget: 'umd'
}

publicPath should be set to children2 , and library should be set to children2 .

Then there is the routing here, you need to set the unified access prefix of the subsystem's page to children2

import View from '@/pages/components/view';
const Template = () => import(/* webpackChunkName: "index" */ '@/pages/index');
const Detail = () => import(/* webpackChunkName: "detail" */ '@/pages/test');

const routes = [
    {
        path: '/children2',
        component: View,
        meta: {
            title: '子应用'
        },
        children: [
            {
                path: 'index',
                component: Template,
                meta: {
                    title: '首页'
                }
            },
            {
                path: 'detail',
                component: Detail,
                meta: {
                    title: '详情'
                }
            }
        ]
    }
];

export default routes;
  1. After loading app.js of the subsystem, single-spa-vue generates different vue instances according to different routes.

Here is just a demo demonstration, link: http://spa.caoyuanpeng.com:9001/ , interested friends can click this link to experience it.

insert image description here

insert image description here

insert image description here

  • First of all, through the demo above, we can clearly see that when we access the domain name for the first time to load the navbar, an instance of the navbar will be generated first (here two button subsystems 1 and 2 are used to display the content of the navbar) , when we click the Subsystem 1 button, an instance of children1 will be automatically generated, the page will render the content of Subsystem 1, and the instance of navbar will remain undestroyed.
  • Then, when we click the Subsystem 2 button, a children2 instance will be automatically generated, and the content of Subsystem 2 will be rendered on the page. At this point, the children1 instance will be destroyed, while the navbar instance will remain undestroyed.
  • At this point, we can build a micro-frontend application from beginning to end.

3. What is the difference between the deployment of micro-frontends in the production environment and the traditional single-page deployment?

  • For traditional single-page applications, we just need to package our static files through jenkins or docker , then copy the packaged static packages to the static server through scripts, and finally use Nginx to proxy and start them.
  • The deployment of the micro-frontend is somewhat different from our traditional single page, because we essentially have a main application to load different sub-applications, that is, the main application and the base application, and the main application and the sub-applications are interdependent. of. Therefore, when deploying, we need to find a way to handle these correspondences clearly through the build script so that the micro frontend can be started smoothly.
  • If the jenkins pipeline is used to build, what we need to pay attention to is to delete the old build directory before each build of the main application, and then copy the new main application directory. We can link the file directory of the main application to the same level as the sub-application directory by creating a soft link .

Here it is packaged and copied to the static file directory we specified by jenkins .
insert image description here

Proxy main application on nginx

server {
    listen       9001;
    server_name  spa.caoyuanpeng.com;

    location / {
        root /home/single-spa-vue;
        try_files       $uri $uri/ /index.html;
        index index.html;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

  1. Our nginx here only needs to locate the index.html file in the entry application.
  2. The file path of the subsystem on the server must be consistent with the route identification name of our subsystem, otherwise the file cannot be loaded into the system.
  3. The subsystem does not need to configure nginx separately , but only needs to configure an entry application.

4. What are the advantages and disadvantages of transforming into a micro-frontend?

Advantage

  • To reduce the packaging volume of the system, the average bundle of the subsystem is less than a few hundred k, and all public js files and css files only need to be loaded once.
  • Compatible with various technology stacks, we can use multiple technical frameworks (React, Vue, AngularJS) on the same page without refreshing the page.
  • If there is a subsystem that wants to access the micro frontend, the access cost is low and there is no need to refactor the code.
  • The code of each subsystem can be loaded on demand without wasting additional resources.
  • Each subsystem can be a separate git project and can be deployed independently.
  • It is possible to assemble arbitrary modules between different git projects based on each independent page route.
  • The user experience is better, and users can load multiple subsystems at the same time without awareness.

insufficient

  • Since multiple Vue instances will be generated during the loading process, it is necessary to formulate detailed specifications on the global style, otherwise it will cause various style pollution.
  • The subsystem uses external to extract some public plug-ins, such as vue, vue-router, element-ui, etc. We also need to avoid the pollution caused by the constructor.
  • The routing guards of the subsystems should avoid being affected by each other.
  • Not all scenarios are suitable for the micro-frontend architecture. When you have enough projects, and they are all similar single-page projects, and you need to combine different subsystem functions into a large system, these scenarios are suitable. Retrofit of micro frontends.

5. What better breakthroughs can be made on the basis of micro-frontends in the future?

  • Let the back-end partners output in the form of micro-services, and the front-end output the subsystems according to the functional modules according to the routing, so that different subsystems can be assembled according to the functions of the modules, and the micro-front end can be fully realized. Of course, this is relatively difficult, and the data interconnection and intercommunication between various subsystems is a big problem.
  • The front-end encapsulates the output according to the module, and the data format returned by the back-end is consistent among the various subsystems.

6. Summary

  • With the continuous evolution of front-end componentization and engineering, the micro front-end architecture may become more and more popular.
  • If you have the same business scenario as above, from the perspective of technological innovation, I think you can try to modify it, and you may have unexpected surprises and gains.

Guess you like

Origin blog.csdn.net/qq_34888327/article/details/112755297