Most of this article will be devoted to explaining how to integrate multiple microapplications at run time using JS. You can see the final results here and the source code on GitHub

React is used in this example, but you can use any other framework

The container

Let’s start with the container, which has the following package.json contents

{ "name": "@micro-frontends-demo/container", "description": "Entry point and container for a micro frontends demo", "scripts": { "start": "PORT=3000 react-app-rewired start", "build": "react-app-rewired build", "test": "The react - the app - rewired test"}, "dependencies" : {" react ":" ^ 16.4.0 ", "the react - dom" : "^ 16.4.0", "the react - the router - dom" : "^ 4.2.2 react -", "scripts" : "^ 2.1.8"}, "devDependencies" : {" enzyme ":" ^ 3.3.0 ", "enzyme - adapter - react - 16" : "^ 1.1.1 jest - enzyme", "" :" ^ 6.0.2 ", "the react - app - rewire - micro - frontends" : "^ 0.0.1", "the react - app - rewired" : "^2.1.1"}, "config-overrides path": "node_modules/react-app-rewire-micro-frontends"}Copy the code

Json is a react application created using creation-react-app. In package.json we don’t see any microapp-related configuration, as we mentioned in the previous article. Build-time integration results in release cycle coupling.

To see how the micro application is displayed on the interface, let’s first look at the content in app.js. We use the React Router to match the URL to a predefined list of routes and render the corresponding component

<Switch>
  <Route exact path="/" component={Browse} />
  <Route exact path="/restaurant/:id" component={Restaurant} />
  <Route exact path="/random" render={Random} />
</Switch>
Copy the code

The Random component is only used to redirect the page to a Random restaurant page. The Browse and Restaurant components look like this

const Browse = ({ history }) => (
  <MicroFrontend history={history} name="Browse" host={browseHost} />
);
const Restaurant = ({ history }) => (
  <MicroFrontend history={history} name="Restaurant" host={restaurantHost} />
);
Copy the code

In both cases, we render the MicroFrontend component on the page. In addition to the History object, we also pass the MicroFrontend component the microapplication name and the microapplication’s host address. In the value of the local runtime host form, such as http://localhost:3001 in a production environment the value of the host such as https://browse.demo.microfrontends.com.

In app.js, the React Router matches the micro application to be rendered. In microfrontend.js, the micro application is rendered.

/ / class MicroFrontend... class MicroFrontend extends React.Component { render() { return <main id={`${this.props.name}-container`} />; }}Copy the code

At render time, all we need to do is place a container element on the page. This container element has an ID named after the microapplication’s name, and the microapplication will render itself at that location. We downloaded and installed the micro front end in the React componentDidMount hook as follows:

/ / class MicroFrontend... componentDidMount() { const { name, host } = this.props; const scriptId = `micro-frontend-script-${name}`; if (document.getElementById(scriptId)) { this.renderMicroFrontend(); return; } fetch(`${host}/asset-manifest.json`) .then(res => res.json()) .then(manifest => { const script = document.createElement('script'); script.id = scriptId; script.src = `${host}${manifest['main.js']}`; script.onload = this.renderMicroFrontend; document.head.appendChild(script); }); }Copy the code

We first check whether the microapplication related script has been downloaded. If so, we immediately call the method to render the page. If not, we request asset-manifest. Get the script path of the microapplication to be rendered from asset-manifest.json and render the page after the script is downloaded. The code for this. RenderMicroFrontend is as follows:

/ / class MicroFrontend... renderMicroFrontend = () => { const { name, history } = this.props; window[`render${name}`](`${name}-container`, history); // E.g.: window.renderBrowse('Browse-container', history); };Copy the code

In the above code we call the global method named window.renderBrowse, which is defined in the microapplication’s script, and pass the


ID and history object to it. This global approach is the key to establishing a relationship between container applications and microapplications. We should make it simple, easy to maintain, and if we need to change it, we should think carefully about whether the change will bring about coupling in the code base and communication.

We’ve shown you what to do with component loading, and now we’ll show you what to do with component unloading. When the MicroFrontend component is uninstalled, we expect the related microapplications to be uninstalled as well. Each microapplication defines a global method associated with unmount. This global method is called in the MicroFrontend component’s componentWillUnmount hook function as follows:

componentWillUnmount() {
    const { name } = this.props;

    window[`unmount${name}`](`${name}-container`);
  }
Copy the code

The site header and navigation bar are constant across all pages, so they are located directly in the container application. We want to make sure that their CSS styles only apply to specific addresses and don’t conflict with the CSS in the microapplication.

In this case, the container application is very rudimentary; it just provides a shell for our microapplications, dynamically downloads our microapplications at runtime, and integrates them into a single page. These microapplications can be independently deployed into the production environment without any changes to any other microapplications or to the container itself.

Micro application

As we know from the previous chapter, the global rendering approach that microapplications expose is critical. Global methods are defined in microapplications as follows:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

window.renderBrowse = (containerId, history) => {
  ReactDOM.render(<App history={history} />, document.getElementById(containerId));
  registerServiceWorker();
};

window.unmountBrowse = containerId => {
  ReactDOM.unmountComponentAtNode(document.getElementById(containerId));
};
Copy the code

In React applications, it’s common to call reactdom.render at the top-level scope, which means that DOM elements are rendered as soon as the script is loaded. In this example we need to control where and when the DOM element is rendered, so we wrap ReactDOM. Render in a function and pass the ID of the DOM element into the function. Then bind the function to the Window object.

While we already know how to call this function when integrating microapplications into an entire container application, we also need to be able to develop and run microapplications independently, so each microapplication should also have its own index.html, as shown below:

<html lang="en">
  <head>
    <title>Restaurant order</title>
  </head>
  <body>
    <main id="container"></main>
    <script type="text/javascript">
      window.onload = () => {
        window.renderRestaurant('container');
      };
    </script>
  </body>
</html>
Copy the code

The micro application in this demo is just a regular React application. The Browse application gets a list of restaurants from the server, provides an input box to search for a specific restaurant and gives each restaurant a link that can jump to the details of the restaurant. The Order app displays restaurant details

We use the CSS-in-JS approach to styling components in our microapplication, which ensures that the styles in the microapplication do not affect the container application and other microapplications

Communication across applications through routing

As mentioned in previous articles, we should make communicating across applications as simple as possible. In this case, our only requirement is that the Browse application needs to tell the Order application which restaurant to load, and in this case we use the browser routing to solve this problem.

In this case, all three applications use React Router to declare routes, but they do so in different ways. In the container application we use a

, which will instantiate a history object internally. This is the history object we mentioned earlier. We use this object to manipulate client history, and we can also use it to link multiple React routes together. In the microapplication, we initialize the route as follows:

<Router history={this.props.history}>
Copy the code

In this case, instead of instantiating a new History object with the React Router in the micro application, we pass the history object from the container application to the micro application. All

instances are connected together, so routing changes triggered in any one instance are reflected in all Router instances. We can pass parameters from one microapplication to another via urls. For example, in the Browse application, we have a link like this:

<Link to={`/restaurant/${restaurant.id}`}>
Copy the code

When the link is clicked, the path in the container application will be updated, and the container will determine whether the restaurant app should be installed and rendered based on the new URL. The restaurant microapplication’s routing logic then extracts the restaurant ID from the URL and renders the correct information.

Common content

While we want each of our microapps to be as separate as possible, some things should be common. I’ve written before about how shared component libraries can help achieve consistency in microapplications, but for this example, one component library is too much. As a result, we have a small repository of common content, including images, JSON data, and CSS, that is supplied via web requests to all microapplications.

Common dependencies can be shared across microapplications. As we will soon describe, repeated dependencies are a common drawback of micro frontend. Although sharing these dependencies across applications has its own difficulties, it is worth discussing how to do it in this demo.

The first step is to determine which dependencies to share. A quick analysis of the compiled code reveals that approximately 50% of the packages are provided by React and React-dom. These two dependencies are core dependencies, and if we extract them from the code we can significantly reduce the size of the code package.

To extract react and react-dom, we need to modify the WebPack configuration as follows:

module.exports = (config, env) => {
  config.externals = {
    react: 'React',
    'react-dom': 'ReactDOM'
  }
  return config;
};
Copy the code

We then add two script tags to each index. HTML file to get the libraries from the shared content server.

<body> <noscript> You need to enable JavaScript to run this app. </noscript> <div id="root"></div> <script SRC = "% REACT_APP_CONTENT_HOST % / react. Prod - 16.8.6. Min. Js" > < / script > < script SRC = "% REACT_APP_CONTENT_HOST % / react - dom. Prod - 16.8.6. Min. Js" > < / script > < / body >Copy the code

Sharing code across teams is always a tricky business. We need to make sure we only share what we really want to share. Only by being careful about what we share and what we don’t can we truly benefit.

disadvantages

In my last article, I mentioned that micro fronts, like any architecture, need to weigh the benefits we can reap against the costs, and here I’ll discuss the drawbacks of micro fronts.

Duplicate downloads

Independently built JavaScript packages lead to duplication of common dependencies, increasing the number of bytes we have to send to the end user over the network. For example, if each microapp included its own React pack, we would force customers to download React n times. It is not easy to solve the problem. There is a conflict between our desire to let teams write their applications independently so that they can work autonomously, and our desire to build our applications in such a way that we can share common dependencies.

One approach is to extract public dependencies as external dependencies, as we demonstrated above. But to do so, we had to use the exact versions of those dependencies, which allowed us to reintroduce some build time coupling into the micro front end.

Repetitive dependency is a tricky problem, but it’s not all bad. First, if we don’t do anything about duplicate dependencies, it’s still possible for each individual page to load faster than if we built a single front end. This is because we have effectively decoupled the code by compiling each page separately. In a traditional Boulder application, when any page in the application is loaded, we usually download the source code and dependencies for the other pages at the same time. By building independently, any single page load in this demo will only download the source code and dependencies for that page. This may cause the initial page to load faster, but subsequent navigation jumps to be slower as the user is forced to re-download the same dependencies on each page.

There are many “maybes” and “maybes” in the previous paragraphs, which highlight the fact that each application always has its own unique performance characteristics. If you want to know exactly what impact a particular change will have on performance, there is no substitute for actual measurement, preferably in a production environment. So while it’s important to consider the performance impact of each architectural decision, it’s important to know where the real bottlenecks lie.

Environmental differences

We should be able to develop a single microapplication independently, without having to consider microapplications being developed by other teams. We can even run our micro front end in standalone mode on a blank page instead of running it in a container application in a production environment. However, there are risks associated with developing in a completely different environment than production. We may encounter situations where the microapplication works as expected when it is run during the development phase, but does not work as expected when it is run in the build environment. We need to pay particular attention to possible style conflicts from containers or other microapplications.

If we develop microapplications locally in a different environment than production, we need to make sure that the environment in which we integrate and deploy microfronts is consistent with production. We also need to test in these environments so that we can find and fix integration problems early, but it’s still not going to solve the problem completely. We need to make a trade-off: is the productivity gain from simplifying the development environment worth the risk of integration problems? The answer will depend on the project.

Operational and governance complexity

As a distributed architecture, a micro front-end architecture will inevitably lead to more things to manage, more warehouses, more tools, more deployment channels, more services, more domain names, etc. Here are some questions to consider before adopting a micro front-end architecture:

  1. Do you have enough automated processes in place to actually provide and manage the additional infrastructure required?
  2. Can your front-end development, test, and release process scale across multiple applications?
  3. Wouldn’t you be comfortable with tools and development decisions becoming more decentralized and out of control?
  4. How do you ensure the highest level of decoupling in multiple independent code bases?

By definition when you adopt a micro front-end architecture it means that you are creating many small components rather than directly creating a large result. You have to think hard about whether you have the technology and the mature organization to make sure that micro-front-end architecture doesn’t bring chaos to you.

conclusion

As front-end codebase has become more complex in recent years, our need for scalable architectures has grown. We need to be able to draw the line and establish the right level of coupling and cohesion between technology entities and domain entities. We should be able to measure software delivery across independent, autonomous teams.

Micro front-end architecture benefits many teams, and it may or may not be a good choice for you, depending on the circumstances. I just want front-end engineering and architecture to be taken seriously, and it’s worth it.

Write in the back

The next installment will cover dynamic page visualization

Scan the code to follow the official wechat account