• Originally written: Micro Frontends
  • Originally written by Cam Jackson
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: xilihuasi
  • Proofreader: Stevens1995, LGH757079506

Microfronds: New Trends in future front-end development – Part 3

It’s not easy to do front-end development well, but it’s even harder to scale it up so that multiple teams can work on a large, complex product. This series will describe the trend of breaking up large front-end projects into many small, manageable parts, and discuss how this architecture can improve the effectiveness and efficiency of front-end code teams. In addition to discussing the various benefits and costs, we’ll look at some of the implementation options available and delve into a complete sample application that applies the technology.

It is recommended to read this series in order:

  • The Micro Front End: Future trends in Front-end development – Part 1
  • Microfronds: New Trends in future front-end development – Part 2
  • Microfronds: New Trends in future front-end development – Part 3
  • Microfronds: New Trends in future front-end development – Part 4

Cross-application communication

The most common problem with microfronts is how to get them to talk to each other. In general, we recommend communicating as little as possible, as this often reintroduces the kind of inappropriate coupling that we want to avoid in the first place.

That said, some level of communication is usually required. Custom events allow microfronts to communicate directly, which is a good way to minimize direct coupling, although it does make it more difficult to identify and enforce the conventions that exist between microfronts. In addition, the React model of passing callbacks and data down (in this case from the container application down to the micro front end) is a good way to make the convention more explicit. The third way is to use the address bar as a communication mechanism, which we’ll talk about in more detail later.

If you’re using Redux, the common approach is to create a single, global, shared store for the entire application. However, if the microfront end is supposed to be its own standalone application, then it makes sense for each of them to have their own Redux store. The Redux documentation even mentions “isolating the Redux application as a component in a larger application” as a justification for having multiple stores.

Either way, we want the microfronts to communicate with each other by sending messages or events, avoiding any state sharing. Just like sharing databases across microservices, as long as we share data structures and domain models, there is a lot of coupling that becomes very difficult to maintain.

As with styles, there are several different approaches that can work well in this regard. The most important thing is to think deeply about the coupling you are introducing and how you will maintain the convention. As with integration between microservices, you can’t make significant changes to integration without a coordinated upgrade process across different applications and teams.

You should also consider how to automatically verify that the integration is not dead. Functional tests are one way to do this, but we prefer to limit the number of functional tests written to control the cost of implementing and maintaining them. Or you could implement some form of consumer-driven contract so that each microfront can specify its dependencies on other microfronts without actually integrating and running them in the browser.


The back-end communication

If we had separate teams working independently on front-end applications, what about back-end development? We strongly believe in the value of the full-stack team, which is responsible for the entire application development, from visual code to API development, database and infrastructure code. The BFF pattern comes into play here, where each front-end application has a corresponding back-end to meet the needs of the front-end alone. While the BFF pattern may initially mean a dedicated back end for each front-end channel (Web, mobile, etc.), it can easily be extended to the back end for each micro-front end.

There are many factors to consider here. The BFF may be self-contained, with its own business logic and database, or it may simply be an aggregator of downstream services. If there are downstream services, it may or may not make sense for the team that owns the micro front end and its BFF to own some of these services. If the microfront has only one API to communicate with, and it’s fairly stable, then there’s not much value in building a BFF at all. As a guiding principle, teams building a particular micro front end should not have to wait for other teams to build things for them. So a BFF owned by the same team is a good case in point if you require backend changes every time you add new functionality to the micro front end.

Figure 7: There are many different ways to build your front/back end relationship

Other common questions are, how should the server authenticate and authorize users of micro front-end applications? It is clear that our users should only need to authenticate once, so authentication is generally a matter of widespread concern for container applications. The container might have some form of login through which we get some token. Tokens are held by the container and can be injected into each microfront at initialization. Finally, the micro front end can carry a token in any request sent to the server, which can then perform any required validation.


test

In terms of testing, we don’t see much difference between a clunky front end and a micro front end. In general, any strategy you use to test a clunky front end can be applied to every micro front end. That said, each microfront should have its own comprehensive suite of automated tests to ensure the quality and correctness of the code.

The obvious obstacle is the integration testing of various micro-front-end and container applications. This can be done using your favorite feature/end-to-end testing tool (such as Selenium or Cypress), but don’t overdo it. Functional testing should only cover areas that cannot be tested at lower levels of the test pyramid. What we mean is that using unit tests covers both low-level business logic and rendering logic, and functional tests only verify that the page is rendered correctly. For example, you can load a fully integrated application at a specific URL and assert that the hard-coded title of the corresponding micro-front end appears on the page.

If user usage spans the microfront end, you can test these with functional testing, but make sure that functional testing focuses on verifying the integration of the front end, not the internal business logic of each microfront end, which should already be covered by unit testing. As mentioned earlier, user-driven conventions help to directly specify the interactions that take place between the micro-front ends without the pitfalls of integration environments and functional testing.


Case,

Much of the rest of this article explains in detail one way our sample application is implemented. We’ll focus on how container applications and microfronts integrate using JavaScript, which is probably the most interesting and complex part. You can see the final results of live deployment at Demo.microfrontends.com, and all the source code is available on Github.

Figure 8: Home page “overview” of the entire microfront-end sample application

This example was developed entirely with React, and it’s important to note that React doesn’t have a monopoly on this architecture. A number of different tools or frameworks can be used to implement a microfront end. We use React here because of its popularity and how familiar it is.

The container

We’ll start with the container because it’s the entry point for our users. Let’s see what we can find in its package.json:

{
  "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": "react-app-rewired test"
  },
  "dependencies": {
    "react": "^ 16.4.0"."react-dom": "^ 16.4.0"."react-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." "."react-app-rewire-micro-frontends": "^ 0.0.1." "."react-app-rewired": "^ 2.1.1"
  },
  "config-overrides-path": "node_modules/react-app-rewire-micro-frontends"
}
Copy the code

React and react-scripts rely on create-react-app to create a React application. What’s even more interesting is what’s not there: any mention of the microfront end we’ll compose to form our final application. If we specify them as library dependencies here, then we will be heading down the path of build-time integration, which, as mentioned earlier, will lead to problematic coupling in our release cycle.

The React-Scripts 1.x version can have multiple applications on a single page without conflict, but the 2.x version uses some Webpack features that cause errors when more than two applications are rendered on a single page. For this reason we use react-app-rewired to override some of the React-scripts configuration within Webpack. It fixes these bugs and lets us continue to rely on React-Scripts to manage our build tools.

To see how we select and demonstrate a microfront, let’s take a look at app.js. We use the React Router to match the current URL with the predefined route list 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 less interesting — it simply redirects to a page corresponding to a randomly selected restaurant URL. Browse and Restaurant 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. In addition to the History object (which becomes important later), we specify a unique name for the application and the address of the host from which the bundle was downloaded. When running locally, this configuration driven URL similar to http://localhost:3001, the production environment is similar to https://browse.demo.microfrontends.com.

We chose a MicroFrontend in app.js, and now we’ll render it in MicroFrontend. Js, which is just another React component:

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

This is not a complete class, and we’ll see more methods of it soon.

When rendering, all we need to do is place the container element with the unique ID of the micro front end on the page. This is where we tell the micro front end to render itself. We use React componentDidMount as a trigger for the download and render microfront-end:

ComponentDidMount is a life cycle function for the React component, which is only called by the framework when a component instance is first “rendered” in the DOM.

MicroFrontend class…

  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 have to get the URL of the script from the static manifest file because the compiled JavaScript file name printed by React-scripts contains a hash value for caching.

First we check to see if the relevant script with the unique ID has been downloaded, and if so, we can render it immediately. If not, we get the asset-manifest.json file from the appropriate host to look up the full URL of the main script asset. Once we’ve set up the script’S URL, all that’s left is to attach it to the document and render the microfront end using the onload handler:

MicroFrontend class

  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 called the window.renderBrowse global function, which was placed there by the script we just downloaded. We assign an ID and history object to the


element that the micro front end should render, as we’ll explain shortly. The signature of this global function is the key contract between the container application and the microfront end. This is where any communication or integration should take place, so keep it fairly lightweight to make it easy to maintain, and add new microfronts in the future. Whenever we want to do something that requires changing this code, we should think carefully about what it means for coupling our code base and maintaining conventions.

The last thing is to deal with the clean-up. When our MicroFrontend component is unloaded (removed from the DOM), we want to unload the corresponding MicroFrontend as well. To do this, each microfront defines a corresponding global function, which we call in the appropriate React lifecycle method:

MicroFrontend class…

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

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

In terms of its own content, everything the container renders directly is the top headers and navigation bars of the site, because these are constant across all pages. The CSS for these elements has been carefully written to ensure that it styles only the elements in the title, so it should not conflict with any styling code within the microfront.

That’s the end of the container application! It was fairly rudimentary, but this gave us a shell that dynamically downloaded our microfronts at run time and glued them together to form content on a single page. These microfronts can be deployed independently in production without changing any other microfronts or the container itself.

It is recommended to read this series in order:

  • The Micro Front End: Future trends in Front-end development – Part 1
  • Microfronds: New Trends in future front-end development – Part 2
  • Microfronds: New Trends in future front-end development – Part 3
  • Microfronds: New Trends in future front-end development – Part 4

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.