Recently, I came into contact with some concepts such as DevOps and microservices, and I wanted to find an article to have a general understanding of them. As I saw a new title of microfront-end, I chose to translate this article. The original https://martinfowler.com/articles/micro-frontends.html


Microservices have exploded in popularity in recent years, with many organizations using this architecture to avoid the limitations of a large, single back end. While much has been written about this style of building server-side software, many companies still struggle with a unified front-end code base.

Perhaps you want to build a progressive or responsive Web application, but can’t find an easy place to integrate these features into existing code. Maybe you want to start using a new JavaScript language feature (or one of the many languages that can be compiled into JavaScript), but you can’t put the necessary build tools into your existing build process. Or, you might just want to scale your development so that multiple teams can work on a single product at the same time, but coupling and complexity in the existing whole means everyone is stepping on each other’s toes. These are real issues, and they all negatively impact your ability to effectively deliver a quality experience to your customers. Recently, we have seen an increasing focus on the overall architecture and organizational architecture required for complex modern Web development. In particular, we see a pattern of breaking up the front end as a whole into smaller, simpler chunks that can be developed, tested, and deployed independently while still appearing to customers as a single cohesive product. We call this technology a microfront-end.

An architectural style where independently deliverable frontend applications are composed into a greater whole

In the November 2016 issue of ThoughtWorks Technology Radar, we listed microfrontier as a technology that organizations should evaluate. We later rolled it out to trial and eventually Adopt, which means we see it as a proven method that you should use when it makes sense.

example

Imagine a customer ordering site, ostensibly one

  • You need a login interface where users can browse and search for restaurants. It can also filter and query prices, dishes and what has been ordered before.
  • Each restaurant needs to have its own page that displays a menu and allows users to choose the dishes they want as well as the price, quantity and special needs.
  • Customer profile page displays personal ordering history, order details and payment options

Each page is complex enough that we could easily assign a dedicated team to each page, and each team should be able to work on its own page independently of all the other teams. They should be able to develop, test, deploy, and maintain their code without fear of conflict or coordination with other teams. However, our customers should still see a single, seamless site.

Integration method

Given the rather loose definition above, there are a number of methods that can reasonably be called microfronds. In this section, we’ll show some examples and discuss the trade-offs between them. There is a fairly natural architecture that appears in all approaches: there is usually a micro front end for each page application, and there is a single container application, in which:

  • Render generic page elements such as header and footer
  • Address crosscutting concerns such as authentication and navigation
  • Integrate the various microfronts onto the page and tell each microfront when and where to present itself

Server side template combination

We started with a decidedly non-novel approach to front-end development, rendering HTML on the server using multiple templates or fragments. We have an index. HTML, which explodes any common page element, and then uses server-side include to insert specific content into the page from the HTML file:

<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 to provide this file and configure the $PAGE variable by matching the requested URL:

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 pretty standard server-side combination. The reason we can reasonably call this microfront is that we split the code in such a way that each part represents a self-contained domain concept that an independent team can pay for. What is not shown here is how these different HTML files end up on the Web server, but assuming they all have their own deployment pipes, this allows us to deploy changes to one page without affecting or considering any other pages. For greater independence, a single server can render and serve each microfront end, with one server sitting on the front end making requests to the other servers. By carefully caching responses, this can be done without affecting latency.

Build-time integration

One approach we sometimes see is to publish each microfront end as a package and have the container application include them all as library dependencies. Here’s what package.json might look like in our application:

{
    "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

At first, this seemed to make sense. As usual, it generates a deployable JavaScript package that allows us to remove duplicate common dependencies from various applications. However, this approach meant that we had to recompile and publish each individual microfront end in order to publish changes to any individual part of the product. As with microservices, we’ve seen the pain of this synchronous publishing process, so we strongly recommend against using this approach with microfrontend. After all the trouble of dividing an application into discrete code bases that can be independently developed and tested, let’s not reintroduce all coupling at the release stage. We should find a way to integrate the microfront end at runtime, not build time.

Runtime integration through IFrame

One of the easiest ways to combine applications in a browser is with iframe. In essence, iframe makes it easy to build pages with separate child pages. They also provide a good degree of isolation from each other in terms of style and global variables.

<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.come/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

Like the server-side integration option, building pages using Iframes is not a new technology, nor does it seem exciting. However, if we review the main advantages of microfrontend listed earlier, iframe generally fits the bill, as long as we carefully consider how to split the application and the build team. We often see that many people are reluctant to choose iframe. While some reluctance seems to be driven by an intuition that iframes are a bit “nasty”, there are some good reasons to avoid them. The simple isolation mentioned above does make them less flexible than the other options. Building integrations between different parts of the application can be difficult, so they become more complex with routing, history, and deep linking, and present some additional challenges in making the page fully responsive.

Integration through Javascript runtime

Next we will describe one of the most flexible and used methods. Each microfront end is included in the page using a tag script and exposes a global function as its entry point at load time. The container application then determines which microfront end should be mounted and calls relevant functions to tell the microfront end when and where to render itself.

<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');   Copy the code

The above example is obviously a primitive one, but it demonstrates the basic technique. Unlike build-time integration, we can deploy each bundle.js file independently. Unlike Iframe, we have the flexibility to build integrations between any microfront end we like. We can extend the above code in a number of ways, such as downloading each JavaScript package only as needed, or passing data when rendering the microfront end.

The flexibility of this approach, coupled with its independent deployability, makes it our default choice and our most common choice in the wild. We’ll look at it in more detail when we see the full example.

Integration via WebComponents runtime

A variation of the former approach is that instead of defining a global function to call the container, each microfront defines an HTML custom element to instantiate the container.

<html> <head> <title>Feed me! </title> </head> <body> <h1>Welcome to Feed me! </h1> <! -- These scripts don't render anything immediately --> <! -- Instead they each define a custom element type --> <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);   Copy the code

The end result here is very similar to the previous example, with the major difference being that you chose to operate in a “Web component fashion.” This is a good choice if you like web component specifications and enjoy using the capabilities provided by browsers. If you prefer to define your own interface between container applications and microfronts, you may prefer the previous example.

style

Shared component library

Cross-application communication

The back-end communication

test