Remember as an intern to the first day of the company was what monorepo, micro front end and other terms were confused, after a period of learning finally got to know a bit of the way, so today to talk to you about my views and understanding of micro front end design.

1. Introduction: What are microservices?

Microservice is a very 🔥 word in the Internet industry in recent years, in our university boiling point studio Java group has Spring Cloud microservice practice precedent, then we as the front end of the Angle, how to understand the microservice?

Check it out at 🌰 :

A system has PC Web end, mobile phone H5, and background management system, then the structure of the whole system is roughly like this:

What problems would that cause?

  • Single server project is too large, not conducive to quick start and package compilation;
  • Different systems will have the same function points, resulting in a lot of repetitive meaningless interfaces;
  • Database design is complex.

So what’s the solution to microservices?

The core is to divide the system into different services, which are controlled and invoked simply by gateways and controllers, and the services are divided and governed without affecting each other.

Now let’s look at the new project structure:

Is our system clearer by splitting the services 😁, then the question comes?

Does this have anything to do with our topic, the micro front

This is where the idea of a micro front end comes from: decoupling the logic by splitting the services.

2. Front-end microservice design

2.1 Why do front-end microservices need to be used?

When we create a new project, you must have the following experience:

Day 1 of the writing project: Pack 20s

One week of writing the project: 1 minute of packing

One month of writing the project: 5 minutes of packaging

I have experienced the old project of the company before, the code volume is very large, the readability is not high, and the packaging takes 10+ minutes.

As the volume of the project increases, a large single application is difficult to maintain, resulting in a series of problems such as low development efficiency and difficulty in getting online.

2.2 Application Scenarios of the Micro front End

For a management system, the page usually looks like this:

For every TAB in the sidebar, there may be several secondary or even tertiary nodes below it. Over time, such a management system will eventually be as difficult to maintain as the server mentioned earlier.

How do we design it with a micro front end?

Each TAB is a child app with its own state; Its own scope; And it’s packaged and distributed separately. At the global level, you only need a master application to manage and control.

In a word: Apply route distribution -> Route distribution application.

2.3 Early micro front end idea — iFrame

Why not iframe ?

When you click on different tabs, the view area displays the iFrame component, dynamically changing the SRC attribute of the iFrame depending on the route.

What are the benefits?

  • Bring their own styles
  • Sandbox mechanism (environmental isolation)
  • The front ends can operate independently of each other

So why didn’t we use iFrame for the micro front end?

  • CSS problem (window size out of sync)
  • Subapplication communication (using postMessage is not friendly)
  • Components cannot be shared
  • Using an iframe may have an impact on performance or memory

Design idea of micro front end: not only can inherit the advantages of iframe, but also can solve its shortcomings.

3. Micro front end core logic

3.1 Child Application Loading (Loader)

Let’s start with the flow of the micro front end:

The consensus is that you need to load the master first and then give the choice to the master. The master app will decide who to load based on the registered child apps. When the child app loads successfully, the Vue-Router or React-Router will render the component based on the route.

3.1.1 registered

If you simplify the code logic, there are really only three things you need to do in the base:

// Suppose our micro front frame is called Hailuo

import Hailuo from './lib/index';



// 1. Declare the sub-application

const routers = [

    {

        path: 'http://localhost:8081'.activeWhen: '/subapp1'

    },

    {

        path: 'http://localhost:8082'.activeWhen: '/subapp2'}];// 2. Register the sub-application

Hailuo.registerApps(routers);



// 3. Run micro front end

Hailuo.run();
Copy the code

Registration is easy to understand. An array is used to maintain all registered child applications:

    registerApps(routers: Router[]) {

        (routers || []).forEach((r) = > {

            this.Apps.push({

                entry: r.path,

                activeRule: (location) = >(location.href.indexOf(r.activeWhen) ! = = -1)}); }); }Copy the code

3.1.2 intercept

We need to intercept registered routing events to ensure logical processing timing for the master/child application.

import Hailuo from ".";



// Practices that require interception

const EVENTS_NAME = ['hashchange'.'popstate'];

// Practice collection

const EVENTS_STACKS = {

    hashchange: [].popstate: []};// Base switch routing logic

const handleUrlRoute = (. args) = > {

    // Load the corresponding subapplication

    Hailuo.loadApp();

    // Execute the subapplication routing methodcallAllEventListeners(... args); };export const patch = () = > {

    // 1. Ensure that the event listening route of the base changes

    window.addEventListener('hashchange', handleUrlRoute);

    window.addEventListener('popstate', handleUrlRoute);



    // 2. Override addEventListener and removeEventListener

    // When routing events are encountered: collect them in stack

    // For other events: execute the original event listener method

    const originalAddEventListener = window.addEventListener;

    const originalRemoveEventListener = window.removeEventListener;



    window.addEventListener = (name, handler) = > {

        if(name && EVENTS_NAME.includes(name) && typeof handler === "function") {

            EVENTS_STACKS[name].indexOf(handler) === -1 && EVENTS_STACKS[name].push(handler);

            return;

        }

        return originalAddEventListener.call(this, name, handler);

    };



    window.removeEventListener = (name, handler) = > {

        if(name && EVENTS_NAME.includes(name) && typeof handler === "function") {

            EVENTS_STACKS[name].indexOf(handler) === -1 && 

            (EVENTS_STACKS[name] = EVENTS_STACKS[name].filter((fn) = >(fn ! == handler)));return;

        } 

        return originalRemoveEventListener.call(this, name, handler);

    };



    // Manually add the ability to listen for route changes to pushState and replaceState

    // This is similar to the array mutation method in VUe2

    const createPopStateEvent = (state: any, name: string) = > {

        const evt = new PopStateEvent("popstate", { state });

        evt['trigger'] = name;

        return evt;

    };



    const patchUpdateState = (updateState: (data: any, title: string, url? : string)=>void, name: string) = > {

        return function() {

            const before = window.location.href;

            updateState.apply(this.arguments);

            const after = window.location.href;

            if(before ! == after) { handleUrlRoute(createPopStateEvent(window.history.state, name)); }}; }window.history.pushState = patchUpdateState(

        window.history.pushState,

        "pushState"

    );

    window.history.replaceState = patchUpdateState(

        window.history.replaceState,

        "replaceState"

    );

}
Copy the code

3.1.3 load

Once you have a matching subapplication, how do you load it into the page?

We know that THE HTML file of SPA is just an empty template, the essence is to render the page through JS driven, so we cut the JS file of A page, all into the

    async loadApp() {

        // Load the corresponding subapplication

        const shouldMountApp = this.Apps.filter(this.isActive);

        const app = shouldMountApp[0];

        const subapp = document.getElementById('submodule');

        await fetchUrl(app.entry)

        // Render the HTML to the main application

        .then((text) = > {

            subapp.innerHTML = text;

        });

        // Execute the js fetched to

        const res = await fetchScripts(subapp, app.entry);

        if(res.length) {

            execScript(res.reduce((t, c) = > (t+c), ' ')); }}Copy the code

Better Practice — [html-entry]

It is a library that loads and processes HTML, JS, and CSS.

Instead of loading individual JS and CSS resources, it loads the micro application’s entry HTML.

  • Step 1: Send a request to get the child application entry HTML.
  • Step 2: Process the HTML document, remove the HTML and head tags, and process the static resources.
  • Step 3: Process sourceMap; Handle JS sandbox; Find the entry js.
  • Step 4: Obtain the content of the child application Provider

At the same time, the child application is constrained to provide load and destroy functions (if this structure looks familiar) :

export function provider({ dom, basename, globalData }) {



    return {

        render() {

            ReactDOM.render(

                <App basename={basename} globalData={globalData} />,

                dom ? dom.querySelector('#root') : document.querySelector('#root')); },destroy({ dom }) {

            if(dom) { ReactDOM.unmountComponentAtNode(dom); }}}; }Copy the code

3.2 Sandbox

What a sandbox is: You can think of it as a metaphor for scope, a sandbox where anything I do has no impact on the outside world.

Why do we need sandboxes?

When we integrate many sub-applications together, there will inevitably be conflicts, such as global variable conflicts, style conflicts, these conflicts may lead to the application style exception, or even function unavailable. So for the micro front end to be production-usable, a sandbox mechanism with some degree of isolation between each sub-application is essential.

To realize the sandbox, the most important thing is to control the opening and closing of the sandbox.

3.2.1 Snapshot Sandbox

When we switch back from environment B, we can use this snapshot to immediately restore the situation in environment A. For example:

// Switch to environment A

window.a = 2;



// Switch to environment B

window.a = 3;



// Switch to environment A

console.log(a);    / / 2
Copy the code

Let’s assume we have the Sandbox class:

class Sandbox {

    private original;

    private mutated;

    sandBoxActive: () = > void;

    sandBoxDeactivate: () = > void;

}
Copy the code
const sandbox = new Sandbox();

const code = "...";

sandbox.activate();

execScript(code);

sandbox.sandBoxDeactivate();
Copy the code

Here’s the logic:

  1. When sandBoxActive, save variables to original;
  2. At sandBoxDeactivate, compare the current variable to Original, save the different variables to mutated (save the snapshot), and then restore the state of the variable to original;
  3. When the sandbox fires sandBoxActive again, the mutated variable can be restored to the window, enabling the sandbox switch.

3.2.2 VM sandbox

Similar to the VM module in Node (compiling and running code in the V8 VIRTUAL machine context) : nodejs.cn/api/vm.html…

The disadvantage of snapshot sandboxes is that they cannot support multiple instances at once. However, THE VM sandbox uses proxy to solve this problem.

class SandBox {

    execScript(code: string) {

        const varBox = {};

        const fakeWindow = new Proxy(window, {

            get(target, key) {

                return varBox[key] || window[key];

            },

            set(target, key, value) {

                varBox[key] = value;

                return true; }})const fn = new Function('window', code); fn(fakeWindow); }}export default SandBox;



// Isolation is implemented

const sandbox = newSandbox(); The sandbox. ExecScript (code);const sandbox2 = newSandbox(); Sandbox2. ExecScript (code2);// map

varBox = {

    'aWindow': '... '.'bWindow': '... '

}
Copy the code

We put the Windows of each child application into the map, and proxy through proxy. When accessing, the window object of each child application is directly accessed. If not, use window.addeventListener, for example, to look for the actual window.

3.2.3 CSS sandbox

  • Premise: WebPack is built to eventually add style tags to HTML with appendChild

Solution: Hijack appendChild and add namespace.

❤️ Thanks for your support

That’s all for this share. Hope it helps you

If you like it, don’t forget to share, like and collect it.

Welcome to pay attention to the public number ELab team receiving good articles ~

We are from Bytedance, and we are the front end department of education under bytedance. We are responsible for the front end development of bytedance education products.

We focus on product quality improvement, development efficiency, creativity and cutting-edge technology and other directions to precipitation and dissemination of professional knowledge and cases, to contribute experience value to the industry. Including but not limited to performance monitoring, component library, multi-terminal technology, Serverless, visual construction, audio and video, artificial intelligence, product design and marketing, etc.

Interested students are welcome to click in the comments section or use the push-code to push to the author’s department 🤪

Bytes to beat school/club recruit pushed the yards, 9 uqj2j2 delivery links: jobs.toutiao.com/s/eX9dge4