Microservices have become very popular over the years, with many organizations using this architecture to avoid the constraints of a large, single backend. While much has been written about how to structure such server-side applications, many companies still struggle with individual front-end codebase.

Maybe you want to create a progressive and responsive Web application, but you can’t find an easy way to integrate this functionality into existing code. Maybe you want to use the new JavaScript syntax, but you can’t apply the build tools you need to your existing build process; Maybe you just want to extend your development so that multiple teams can work on a single product at the same time, but this complex, highly coupled single application makes everyone else’s feet. These issues can have a negative impact on the quality of the experience you can provide to your customers.

Recently, there has been a growing focus on the overall architecture and organizational structure necessary for sophisticated modern Web development. In particular, we’ve seen patterns emerge that break front-end megalizer applications into smaller, simpler chunks that can be developed, tested, and deployed independently, while still being seen by customers as a single cohesive product. We call this technology a micro front end.

The benefits of using the micro front end technology are as follows:

  1. A smaller, more cohesive, more maintainable code base
  2. Decoupled, autonomous and flexible structure
  3. Upgrade separately and do not affect other parts

Of course, there is no free lunch when it comes to software architecture — everything comes at a price. Some micro-front-end schemes can lead to repeated dependencies, increasing the number of bytes the user must download. In addition, the dramatic increase in team autonomy can lead to a split in the way teams work. Nonetheless, we believe these risks are manageable and that the benefits of the micro front tend to outweigh the risks.

benefit

The incremental upgrade

For many teams, incremental upgrades are the beginning of their micro front end journey. Old, large, front-end megalithic applications that are being weighed down by outdated technology stacks or code written under delivery pressures are now ripe for rewriting. To avoid a complete rewrite, we prefer to rewrite old applications bit by bit while continuing to deliver new features to our customers without being overwhelmed by Stonehenge.

Based on the above requirements, this usually results in a micro front-end architecture. Once one team has the experience of bringing new features into production with just a few modifications to an old application, other teams will jump in. Existing code still needs to be maintained, but adding new functionality to the old code base becomes optional.

We gained more freedom to make individual decisions on each part of the product and to make incremental upgrades to the architecture, dependencies, and user experience.

If the product needs to be changed or new features added, we only upgrade the parts that need to be upgraded, rather than being forced to stop the entire app and upgrade everything immediately. If we want to use new technologies, or new models, we can do it in a much more independent way than before.

Simple and decoupled code base

The source code for each individual microapplication is much smaller than the source code for the individual front-end. For developers, these smaller code bases tend to be simpler and easier to use. We avoid coupling between unrelated components

Independent deployment

As with microservices, it is critical that each microapplication be deployed independently. This reduces the scope of impact of a single deployment and reduces risk. No matter where your front-end code is hosted, each microapplication should continuously interact independently. The deployment of each microapplication should not depend on other code bases

Independent team

With an understanding of decoupled code bases and independent release cycles, we are a long way from having fully independent teams who can own a piece of a product from conception to production and beyond. Teams have everything they need to deliver products to customers, which enables them to move quickly and efficiently. To achieve this, our teams need to be built around vertical parts of business functionality, not around technical capabilities. An easy way to do this is to divide the product by what the end user will see, so we can divide microapplications by page. This is more cohesive than the composition of the team around technical or horizontal concerns such as style, forms, or validation.

simple

In short, a micro front end is about taking something big and scary and breaking it up into smaller, more manageable pieces, and then clarifying the dependencies between them. Our technology choices, our code base, our teams, and our release process should all be able to operate and evolve independently of each other without undue coordination.

example

Take the example of a website where customers can order takeaway food. On the surface, this is a fairly simple concept, but if you want to get it right, there’s a ton of detail:

  • It should have a page where customers can browse and search for restaurants. Customers can filter restaurants by certain attributes, such as price, cooking method, or previous orders
  • Every restaurant needs to have its own page to display menu items and allow customers to choose what they want to eat, including discounts, meal offers and special requests
  • Customers can view their order history, track deliveries and customize their payment options from their personal home page

Each page here is complex enough that it can be divided into multiple teams, each developing, testing, publishing, and deploying their own code independently. However, customers can still see a single, seamless website.

integration

Given the loose definition of a micro front, there are a number of scenarios that can be referred to as a micro front. Next, we’ll look at the pros and cons of these approaches. These solutions generally have one thing in common: each interface of the application is a microapplication, and there is a container application. Container applications do the following:

  • Render common page elements, such as headers and footers
  • Addresses horizontal concerns such as authentication and navigation
  • Aggregate different microapps onto a page and tell each one when and where to render itself

Server side template composition

We started with an uninspired approach to front-end development: using multiple templates or fragments to render HTML on the server side. We have an index. HTML file that contains all the public elements, and we use server-side includes to insert specific content into the index. HTML. The content of the index. HTML looks like this:

<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Feed me</title>
  </head>
  <body>
    <h1>Feed me</h1>
    <! --# include file="$PAGE.html" -->
  </body>
</html>
Copy the code

We use nginx as the server and set the $PAGE variable according to the PAGE access path.

server {
    listen 8080;
    server_name localhost;

    root /usr/share/nginx/html;
    index index.html;
    ssi on;

    # Redirect / to /browse
    rewrite ^/$ http://localhost:8080/browse redirect;

    # Decide which HTML fragment to insert based on the URL
    location /browse {
      set $PAGE 'browse';
    }
    location /order {
      set $PAGE 'order';
    }
    location /profile {
      set $PAGE 'profile'
    }

    # All locations should render through index.html
    error_page 404 /index.html;
}
Copy the code

This is a standard server-side combination, and the reason we call it a micro front end is because we can split up the code and each part can be delivered by a separate team.

To achieve greater independence, a single server can render and serve each microapplication, with one server in the front end making requests to the other microfronts. This can be done without affecting latency by caching the response.

This example illustrates that a micro front end doesn’t have to be a new technology, nor does it have to be complex. If we focus on how our design decisions affect our codebase and team autonomy, no matter what our technical level, we can gain a lot of benefits

Build-time integration

Publish each microapplication as a package and have the container application include them as library dependencies. Here is part of the container application’s package.json:

{
  "name": "@feed-me/container"."version": "1.0.0"."description": "A food delivery web app"."dependencies": {
    "@feed-me/browse-restaurants": ^ "1.2.3"."@feed-me/order-food": "^ 4.5.6." "."@feed-me/user-profile": "^ 7.8.9." "}}Copy the code

This approach seems reasonable. However, this approach means that when a microapplication needs to be updated, we must recompile and republish the container application. We should strongly oppose using this approach to implement a micro front end.

Runtime integration through iframe

One of the easiest ways to combine applications in a browser is to use an iframe. Because of this natural advantage, iframe makes it very easy to combine multiple independent child pages into a new page. They also provide good isolation between styles and global variables without interfering with each other.

<html>
  <head>
    <title>Feed me!</title>
  </head>
  <body>
    <h1>Welcome to Feed me!</h1>

    <iframe id="micro-frontend-container"></iframe>

    <script type="text/javascript">
      const microFrontendsByRoute = {
        '/': 'https://browse.example.com/index.html'.'/order-food': 'https://order.example.com/index.html'.'/user-profile': 'https://profile.example.com/index.html'};const iframe = document.getElementById('micro-frontend-container');
      iframe.src = microFrontendsByRoute[window.location.pathname];
    </script>
  </body>
</html>
Copy the code

We often see people who don’t want to choose an iframe. While some people’s reluctance to use an iframe may seem intuitive that iframes are “annoying,” there are some good reasons to avoid them. For example: The complete isolation mentioned above makes them inflexible, and they complicate routing, history, and deep linking.

Runtime integration through JavaScript

This approach is probably the most flexible and the one we see most often adopted by teams. Each microapplication is introduced to the page via

<html>
  <head>
    <title>Feed me!</title>
  </head>
  <body>
    <h1>Welcome to Feed me!</h1>

    <! -- These scripts don't render anything immediately -->
    <! -- Instead they attach entry-point functions to `window` -->
    <script src="https://browse.example.com/bundle.js"></script>
    <script src="https://order.example.com/bundle.js"></script>
    <script src="https://profile.example.com/bundle.js"></script>

    <div id="micro-frontend-root"></div>

    <script type="text/javascript">
      // These global functions are attached to window by the above scripts
      const microFrontendsByRoute = {
        '/': window.renderBrowseRestaurants,
        '/order-food': window.renderOrderFood,
        '/user-profile': window.renderUserProfile,
      };
      const renderFunction = microFrontendsByRoute[window.location.pathname];

      // Having determined the entry-point function, we now call it,
      // giving it the ID of the element where it should render itself
      renderFunction('micro-frontend-root');
    </script>
  </body>
</html>
Copy the code

This is a very simple example, but it demonstrates the basic techniques. Unlike build-time integration, we can deploy each bundle.js file independently. Unlike iframe, we have the flexibility to build integration between our microapplications. We can extend the above code in a number of ways, such as downloading only the required JS code package, or passing in and out data while rendering the microapplication. The next installment will feature a React project as a full example.

Integration at run time via Web Components

The approach is to define the microapplication as HTML custom elements and then use those custom elements in container references

<html>
  <head>
    <title>Feed me!</title>
  </head>
  <body>
    <h1>Welcome to Feed me!</h1>

    <! -- These scripts don't render anything immediately -->
    <script src="https://browse.example.com/bundle.js"></script>
    <script src="https://order.example.com/bundle.js"></script>
    <script src="https://profile.example.com/bundle.js"></script>

    <div id="micro-frontend-root"></div>

    <script type="text/javascript">
      // These element types are defined by the above scripts
      const webComponentsByRoute = {
        '/': 'micro-frontend-browse-restaurants'.'/order-food': 'micro-frontend-order-food'.'/user-profile': 'micro-frontend-user-profile'};const webComponentType = webComponentsByRoute[window.location.pathname];

      // Having determined the right web component custom element type,
      // we now create an instance of it and attach it to the document
      const root = document.getElementById('micro-frontend-root');
      const webComponent = document.createElement(webComponentType);
      root.appendChild(webComponent);
    </script>
  </body>
</html>
Copy the code

The end result of this approach is very similar to the example of runtime integration using JS, with the major difference being that you chose the “Web component approach.” This is a good choice if you like the Web component specification and enjoy using the capabilities provided by the browser. If you want to define your own interface between a container application and a microapplication, you might be better off with runtime integration through JavaScript

Write in the back

Style isolation

CSS, also known as cascading Style sheets, has no module system, namespaces, or encapsulation. In the micro front end, these problems are exacerbated. For example, a team has a style script h2 {color: black; }, another team has a style script h2 {color: blue; }, and if both selectors apply to the same page, a mode conflict occurs. Because the code was written by different teams and hosted in different repositories, it was difficult to find.

Over the years, people have invented many ways to make CSS easier to manage. For example, use strict naming conventions, such as BEM, to ensure that selectors only work where they are expected; Use a precompiler, such as SASS, to nest selectors as a form of namespace; A newer approach is to use CSS Modules or CSs-in-JS to ensure that styles only apply where the developer wants them to; Another option is the Shadow DOM to provide sample isolation.

You don’t have to worry about which way to achieve style isolation, as long as you find a way to ensure that developers can write their styles independently and that their code behaves in a predictable way when combined as a separate application.

Shared component libraries

Visual consistency across microapplications in microfront-end applications is important, and one way to achieve this is to develop a shared, reusable library of UI components. Overall, it’s a good idea, but it’s hard to do well. The main benefits of creating such a library are increased code reuse, reduced effort, and visual consistency. In addition, component libraries can serve as a living style guide, which can be a great entry point for collaboration between developers and designers.

Creating too many common components too early can easily cause problems. It is tempting to create a basic framework that can be used across all applications. However, experience tells us that it’s hard to guess what a component’s API should be until you actually use it, which can lead to a lot of early confusion. It is more recommended to let the team create the components they need, even if this leads to some duplication at first, once the component’s API becomes obvious, you can collect the duplicated code into a shared library.

Before sharing components across multiple microprojects, ensure that the shared components contain only UI logic and no business logic or state. When business logic is put into a shared library, it creates a high degree of coupling between applications and makes change more difficult.

Communication across applications

One of the most common problems with microfronts is getting applications to communicate with each other. In general, it is recommended that they communicate as little as possible, but communication across applications may be necessary at some point. Browser custom events are a great way to achieve cross-application communication; The React model, which passes callbacks and data down, is also a good solution; The third option is to use the browser address for communication.

Whatever method we choose, we want the micro fronts to communicate by sending messages or events to each other and avoid any shared state.

The back-end communication

If we have independent teams developing front-end applications, what about back-end development? We strongly believe in the value of full-stack teams who have their own application development processes, from visual code to API development to database and infrastructure code. The BFF pattern is helpful here, where every front-end application has a corresponding back end that serves only the front-end requirements.

A BFF may contain 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 microapplication and its BFF to have some of them. If a microapplication only calls one API, and that API is fairly stable, there may be little value in building a BFF at all. The guiding principle here is that one team building a microapplication should not wait for another team to build something for them. Therefore, if the addition of new features to the microapplication also requires back-end changes, then this is a good time for BFF to be owned by the same team.

Another common question is, how should users of micro-front-end applications authenticate and authorize through the server? Obviously, our customers should only need to authenticate themselves once, so Auth needs to exist across applications and should be owned by the container application. The container might have some kind of login form through which we can get some kind of token. This token will be owned by the container and can be injected into each microapplication at initialization time.

test

During the testing phase, we didn’t see any difference between the monomer front end and the micro front end. Each microapplication should have its own automated test suite to ensure quality and correctness of the code.

The obvious gap is in testing the integration of various microfronts with container applications. You can use your favorite functional testing or end-to-end testing tools, but pay attention to the test coverage. By that we mean using unit tests to cover the underlying business logic and rendering logic, and then using functional tests to verify that the page is properly assembled.

If you have users doing cross-application operations, then you can use functional test coverage, but functional tests are only used to verify cross-application integration, not the microapplication’s internal business logic, which should be covered by unit tests

In the next installment, we’ll explain in detail how to integrate containers and microapplications using React. You can see the final result first

At the end

Scan the code to follow the public number