In toB front-end development, we often encounter the following dilemma:

  1. Projects get bigger and bigger and packaging gets slower and slower
  2. There are many team members, complex product functions, frequent code conflicts and large impact areas
  3. The heart wants to do SaaS products, but customers always want to customize

Different teams may have different approaches to solving these problems. Today, with the rapid development of front-end development and the booming development of front-end engineering, I would like to introduce to you another attempt — micro front-end.

What is the micro front end

So what is a micro front end? The microfront end borrows the concept of back-end microservices. In short, Monolith’s front-end engineering is broken down into smaller projects. Do not look down upon these small projects, they are also “small sparrow, all the five organs”, fully equipped with independent development, operation ability. The whole system will be coordinated by these small projects to achieve the display and interaction of all pages.

Compare this to microservices:

Micro service Micro front-end
A microservice consists of a set of interfaces, the address of which is usually a URL. When the micro service receives a request from an interface, it will route to find the corresponding logic and output the response content. A micro front end is made up of a set of pages whose address is also a URL. When the micro front end receives a request for a page URL, it will route to find the corresponding component and render the page content.
The back-end microservice will have a gateway that receives all client interface requests as a single entry and routes to the corresponding service according to the matching relationship between the interface URL and the service. The micro front end will have a loader as a single entrance to receive access to all page urls. According to the matching relationship between the page URL and the micro front end, the corresponding micro front end will be selected to load, and the micro front end will respond to the URL routing.

Note the difference with iframe in implementing the page embedding mechanism. The micro front end does not use IFrame. It uses JavaScript, MVVM and other technologies purely for page loading. The technical implementation will be described later.

Why use a micro front end

Before introducing the specific transformation method, I would like to explain to you the problems we faced at that time and the comparison after the transformation, so that you can judge or decide to use it as a comparison. It mainly includes packaging speed, page loading speed, multi-person and multi-place collaboration, SaaS product customization and product splitting.

The first is packaging speed. Six months ago, our B-side project was Monolith. There were already over 20 dependencies, over 60 common components, over 200 pages, and over 700 interfaces. We used Webpack 2 and enabled DLL Plugin and HappyPack 4. Compiling with 4 threads on my personal mainframe takes about 5 minutes. Without the split, we now have nearly 400 pages with over 1,000 interfaces. What does this time mean? Not only does it take time away from our developers, it also affects the productivity of the entire team. When it goes online, the time will be prolonged in Docker, CI and other environments. If there are several bugs that need to be fixed online immediately after deployment, you don’t know what time to stay up. After using the microfront-end transformation, we now have 26 microfront-end projects with an average packaging time of 30-45 seconds (note that DLL + HappyPack is not applied here).

Page loading speed is not a big impact, because after CDN, GZIP, the size of the resource is acceptable. So this is just to give you some intuition of what’s going on. Six months ago, the package generated app.js was 5MB (post-Gzip 1MB), Vendor.js was 2MB (post-Gzip 700KB) and app.css was 1.5MB (post-Gzip 250KB). That’s about 2 MEgabytes of content on the first screen. After the split, the current first screen only needs to transmit about 800KB.

In terms of collaboration, we have three front-end teams in the country. With so many people developing in the same project, the probability of encountering code conflicts is very frequent and the impact of conflicts is relatively large. If something goes wrong in the code that causes the CI to fail, all other code submissions and updates are blocked. With a micro front end, this risk is spread across projects.

Then there is customization. What we do is a toB product, and it is probably the wish of all practitioners to make a SaaS standard version product. However, limited by the overall market environment and product functions, we often face the requirements of localization and customization from some customers. There are code security considerations for localization, and it is best not to give the customer the source code, or at worst to only buy the source code for the function. From easy to difficult, customization can be divided into independent new modules, transformation of existing modules, and replacement of existing modules. With microfront-end technology, we can easily achieve the lower limit of localized code security – giving the customer only the front-end source code for the module he purchased. The simplest standalone new module in customization is also made simple: the delivery team can add a new micro front-end project without having to roll it into the existing development project and take up no team resources. Customization of existing modules can also be done well: for example, if a panel needs to be added to a standard page, a new micro front-end project can also respond to the URL of the page (in a controlled order, of course) by inserting a new DOM node in the appropriate position of the page.

Finally, there is the consideration of product separation. Our product is relatively large and has several independent and distinctive functions. If there is a need to separate into a sub-product in the future, with a micro-front-end split as the groundwork, the combination will become easier to maneuver.

Other goals

For these reasons and appeals, there are a few additional small goals that need to be set before deciding on a micro front end:

  • There should be no major changes to the existing front-end development approach, at least a smooth transition mechanism.
  • Each front-end project is required to run independently, at least when developed locally.
  • In the loading process, the micro front end should realize the preloading, and can adjust the preloading sequence freely, and even realize the intelligent and personalized loading sequence according to the user’s preference.

How to transform existing projects

“Talk is cheap, show me the code”. Now let’s take a look at the specific transformation! Our micro front end projects can be divided into portal projects, business projects, and Common projects.

Portal project

Portal, as the name suggests, is the entrance. This is also known as the micro front-end loader. When a user opens a browser and enters our page for the first time, whatever the URL, the portal is the first thing that loads. The portal configures the addresses of all service projects, matching urls, and resources to be loaded. Such as:

// Business project name
customer: {
    // URL matching pattern
    matchUrlHash: ['^/customer'].// Micro front-end address
    target: 'http://localhost:8101/mfe-customer/index.html'.// Resource matching mode
    resourcePatterns: ['/app.*.css$'.'/vendor.*.css$'.'/manifest.*.js$'.'/vendor.*.js$'.'/app.*.js$'],}Copy the code

The Portal periodically, asynchronously, and concurrently downloads the resources of service projects and registers them. The service projects are not loaded. The reason why the address (target) and resource (resourcePatterns) of the service project are used here is to ensure that the paths of app.js, Vendor. js, app.css and other resources contained in the service project are known during loading. Every time a service project changes, the path of resources such as app.js will contain a new Hash value of file content, resulting in an unpredictable path. Its index. HTML path is fixed. We read the HTML, parse its contents, and use the re to match the path to a resource like app.js.

When portal runs, it listens for URL changes. Currently we only support URL hashes (such as #/customer). When the Hash is changed, it is matched to the business project and then unloaded and loaded. This mechanism is mainly implemented using single-SPA, but the principle is as simple as that.

import { registerApplication } from 'single-spa';
registerApplication('customer'.// Download the micro front-end project and get three function hooks: bootstrap, mount, and unmount() = > {const html = fetch(mfeConfig.target);
        const {cssUrls, jsUrls} = match(html, mfeConfig.resourcePatterns);
        loadCss(cssUrls);
        loadJs(jsUrls);
        return windows['mfe:customer'];
    },
    // Matches the current browser URL Hash, and if it does (returns true), loads the micro-front end (calls mount); Otherwise unmount (call unmount)() = > {return match(window.location.hash, mfeConfig.matchUrlHash);
    },
    mfeConfig.customProps
);
Copy the code

The business project

Business engineering is the common micro front-end engineering, generally a module a project. A business project plays two roles, a standalone front-end project and a portal-controlled runtime. The former is mainly for our local development and the latter is for online integration. When run on its own, it is no different from the original front-end engineering. For the Vue project, use new Vue({el: ‘#app’}) to launch and render the page.

new Vue({
    el: '#app',
    i18n,
    router,
    store,
    template: '<App/>'.components: { App }
});
Copy the code

When run under control, UMD outputs several hook functions, including initialization, loading, and unloading.

if(!window.IS_IN_MFE){ // Independent runtime
    new Vue({...})
} else { // Controlled run time
    module.exports = {
        bootstrap(){ // Execute at registration time
        },
        mount(customProps){ // execute at load time
            return Promise.resolve().then((a)= >{
            instance = newVue({... }) }) }, unmount(){// Execute during uninstallation
            return Promise.resolve().then((a)= >{
            instance.$destroy()
            })
        }
    }
}
Copy the code

Webpack configuration for the online environment:

output: {
    libraryTarget: "umd".library: 'mfe:customer'
}
Copy the code

Whether controlled or not can be distinguished by determining a global variable. For example, window.is_in_mfe, the Portal project sets this to true at runtime.

To support simultaneous development of multiple projects locally, we need to specify a definite, exclusive port number for each microfront-end project. For example, start from 8100 and increase one by one. At the same time, to support online deployment, we also need to specify a certain, exclusive base path (prefix) for each micro-front-end project. In this way, the same domain name can be accessed using different paths. The path starts with /mfe-, for example, /mfe-customer. This is what is shown in the business engineering configuration example in portal above.

Special business Engineering: MFE-NAVS

The page structure of our product is divided into the top bar, the sidebar, and the middle content area. The top bar and the sidebar remain largely unchanged as the page jumps. So we also spun them off as a separate micro front end business project called MFE-NAVS. It will match all urls, which means that whenever you visit any URL, it will be loaded, and it will be loaded first. When it is loaded, it provides an anchor point DOM (#app 🙂 in the middle content area within the page for other business projects to mount when loading.

Common engineering

As you can see, each business project is a separate front-end project, so it has some of the same dependencies, such as Vue, Moment, Lodash, etc. If all of this is packaged into their own vendor.js, the code will be too redundant and the browser memory will be stressed. We put these public dependencies, public components, CSS, Fonts, etc. into a project, which will package them, export the dependencies and components, and inject them into the global by UMD.

The main. Js:

import Vue from 'vue'; // Public dependencies
import VueRouter from 'vue-router';
import VueI18n from 'vue-i18n';
import '@/css/icon-font/iconfont.css';
import ContentSelector from '@/components/ContentSelector'; // Public components

Vue.use(VueI18n); // Let's do it for everyone.

module.exports = {
    'vue': Vue,
    'vue-router': VueRouter,
    'content-selector': ContentSelector,
};
Copy the code

Webpack configuration:

output: {
    libraryTarget: "umd".library: 'mfe:common'
}
Copy the code

Business projects are introduced into the project through Webpack external dependencies. This way the business project is packaged without this common code.

var externalModules = ['vue'.'vue-router'.'content-selector'];

module.exports = { // WebPack configuration item
    // ...
    externals: (context, request, callback) = >{
        if(externalModules.includes(request)){
            callback(null.'root window["mfe:common"]["'+request+'"])}else{ callback(); }}},Copy the code

conclusion

The above is our micro front-end transformation and practice of some experience. There is still a lot of work to be done, such as History mode support, better i18N integration, load order optimization and personalization of various business projects, etc. In addition to these purely technical explorations, with microfront-end and microservice architectures, teams can also consider a vertical split: a team that is independently responsible for a piece of business has its own microfront-end and microservice projects. From technology management to people management, we want to integrate them into a unified consideration, which is also our exploration direction of software engineering. Look forward to these can bring you some thinking and help!