Tailor made Micro Frontends

In recent years micro frontend architectures have become more and more popular. Let’s have a look at why this is so, let’s summarize what Jonas Boner and Cam Jackson have to say about them, and discover how Zalando (yes that Zalando) fits in.

At micro-frontends.org a micro frontend is defined as:

“a composition of features which are owned by independent teams. Each team has a distinct area of business or mission it cares about and specializes in. A team is cross-functional and develops its features end-to-end, from database to user interface.”

This is about the same as the Self-Contained Systems (SCS) concept. See: scs-architecture.org

Why micro frontends?

The argumentation to go ahead with micro frontends is often described in terms of efficiency: development speed, time-to-market and continuous delivery. As well as their ability to improve the design of applications.

Or even better, as Jonas Boner in his book ‘Reactive microservices’ writes about the UX capabilities: “One of the major benefits of microservices-based architecture is that it gives us a set of tools to exploit reality, to create systems that closely mimic how the world works.”

The five essential traits of microservices that Jonas Boner describes include: isolation, autonomy, single responsibility, exclusive state and mobility.

Micro frontends and microservices are not a free lunch, though. Full-stack web developer Cam Jackson gives multiple examples of costs for micro frontends, such as duplication of dependencies, and the fact that an increase in team autonomy can cause fragmentation in the way teams work.

Jonas Boner also points to unnecessary complexity that slow developers down. Which raises the question whether the benefits out way the downsides.

Decomposing the frontend monolith

According to Cam Jackson, a ‘fairly natural architecture’ of micro frontends consists of a micro frontend for each page in the application and a ‘single container application’.

(image credits martinfowler.com/articles/micro-frontends.html)

The single container application:

  • renders common page elements such as headers and footers
  • addresses cross-cutting concerns like authentication and navigation
  • brings the various micro frontends together onto the page, and tells each micro frontend when and where to render itself

Stitching micro frontends

To disassemble something is easy, but putting the parts back again is often more laborious. Which approaches are available to ‘stitch’ the micro frontends together to a larger whole? Cam Jackson presents five techniques:

(all code examples are from: martinfowler.com/articles/micro-frontends.html)

1. Server-side template composition

Rendering HTML on the server: an index.html with server-side includes that plug in content from HTML file fragments.

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

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;
}

Nice to note with old and simple techniques we can make modern micro frontends too!

2. Build-time integration

Publish the micro frontends as packages and include them as library dependencies in the container.

"dependencies": {
    "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"
    }
}

A big disadvantage is that coupling is re-introduced at the release stage. Therefore, micro frontends should be integrated at run-time, rather than at build-time.

3. Run-time integration via iframes

Yuck. Nevertheless, iframes can be used to stitch micro frontends. Cam Jackson points to a big disadvantage: they make routing, history, and deep-linking more complicated, and they present some extra challenges to making your page fully responsive.

4. Run-time integration via JavaScript

Depending on the route, a given function is called that mounts an associated micro frontend within an element on the page.

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

This is a popular approach. It offers much flexibility; for instance, the bundle.js files can be deployed independently.

5. Run-time integration via WebComponents

Another approach is for each micro frontend to define an HTML custom element for the container to instantiate. This looks a bit like the former JavaScript way. If you are a fan of Web Components, then this might be the option for you.

<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);
</script>
</body>
</html>

Tailor

Zalando not only sells clothes but develops JavaScript libraries too. It has created a set of services to support the microservice architecture. See: mosaic9.org. Tailor is one of these services.

It is a layout service and it is an example of the fourth approach described above, namely the run-time integration via JavaScript. Tailor composes a website from multiple fragment services. Fetching of fragments takes place asynchronously.

In a previous project we used Tailor. We had created several micro frontends and one container application, which we aptly named the ‘coat rack’. It was quite tiny! It consisted of a small number of files and only a few lines of code.

Apart from taking care of the cross-cutting concern of authorization, it mainly defined the overall layout of the single page application (the ‘whole’) that was composed.

We made it technology agnostic. In contrast to David den Toom in his article on micro frontends, this was important for us because there were some legacy Angular applications that we wanted to reuse next to our React micro frontends.

In this ‘coat rack’ container application we defined multiple templates. A template composes a page. For each page, multiple fragments (read: micro frontends) were used. The resulting single page application consisted of multiple pages. An example of such a template looks like this:

module.exports = function(context) {
    return `
<!DOCTYPE html>
<html>
  <head>
  ....
  </head>  
  <body>
    <section class="SplitView">
      <section class="SplitViewHeader" id="header"></section>
      <section class="SplitViewMain" id="foo"></section>
      <section class="SplitViewAside" id="bar"></section>
    </section>
    <fragment src="${context.env}/header"></fragment>
    <fragment src="${context.env}/foo" async></fragment>
    <fragment src="${context.env}/bar" async></fragment>
  </body>
</html>
`;
};

In this example template, a page composes of three fragments: header, foo, bar. This means that there are three micro frontends (header, foo and bar) that serve a bundle.js file. Tailor stitches the three bundles to one page as defined in the template.

In order to serve the bundle to Tailor, each fragment micro frontend application has a server defined like this:

const http = require('http');
const url = require('url');
const fs = require('fs');

const host = process.env['host'];

const server = http.createServer((req, res) => {

  const pathname = url.parse(req.url).pathname;
  const jsHeader = { 'Content-Type': 'application/javascript' };

  switch(pathname) {
    case '/header/bundle.js':
      res.writeHead(200, jsHeader);
      return fs.createReadStream('./bundle.js').pipe(res);
    case '/header/ping':
      return res.end('pong');
    default:
      res.writeHead(200, {
        'Content-Type': 'text/html',
        'Link': '<http://' + host + '/header/bundle.js>; rel="fragment-script"'
      });
      return res.end('')
  }
});

server.listen(80, () => {
  // console.log('Header Fragment Server started at 80')
});

The bundle.js contains the header micro frontend. Works like a charm.

Wrap up?

For us, Tailor did an excellent job stitching our micro frontends together. You might want to check it out too, when jumping on the micro frontend train.

Best of luck with it!

Leave a Reply

Your email address will not be published. Required fields are marked *