<

Micro Frontends with Zalando tailor (LitElement & etcetera)

I tried to build a sample web application with Micro Frontends using Tailor, developed by Zalando. Tailor is an architecture that integrates on the server side. On the client side, I integrated it using Lit Element, which is made with Web Components. I thought I would post what it's all about here.

The repository I created is left below. https://github.com/silverbirder/micro-frontends-sample-code-4

Overall Structure

Application configuration
Application configuration

To explain roughly, you get and return fragments (components) to Tailor from HTML. Each fragment refers to Javascript defined by LitElement for WebComponents. Just by loading the fragment, you can use custom elements.

Tailor

image
image

https://github.com/zalando/tailor

A streaming layout service for front-end microservices

Tailor, as a streaming layout service, seems to stream the load of fragments. (This library was influenced by Facebook's BigPipe)

First, the HTML template of tailor.js is as follows.

templates/index.html

<body>
  <div id="outlet"></div>
  <fragment src="http://localhost:7000" defer></fragment>
  <fragment src="http://localhost:8000" defer></fragment>
  <fragment src="http://localhost:9000" defer></fragment>
</body>

The acquisition of these fragments is done through tailor.js.

tailor.js

const http = require("http");
const Tailor = require("node-tailor");
const tailor = new Tailor({
  templatesPath: __dirname + "/templates",
});

http
  .createServer((req, res) => {
    req.headers["x-request-uri"] = req.url;
    req.url = "/index";
    tailor.requestHandler(req, res);
  })
  .listen(8080);

x-request-uri seems to be for inheriting the URL to the following fragment. And the fragment server is as follows.

fragments.js

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

const server = http.createServer((req, res) => {
  const pathname = url.parse(req.url).pathname;
  const jsHeader = { "Content-Type": "application/javascript" };
  switch (pathname) {
    case "/public/bundle.js":
      res.writeHead(200, jsHeader);
      return fs.createReadStream("./public/bundle.js").pipe(res);
    default:
      res.writeHead(200, {
        "Content-Type": "text/html",
        Link: '<http://localhost:8000/public/bundle.js>; rel="fragment-script"',
      });
      return res.end("");
  }
});

server.listen(8000);

fragments.js will add a Link header to the Response Header. Tailor will load the Javascript of this header. Furthermore, fragments.js seems to return a stream pipe with return fs.createReadStream('./public/bundle.js').pipe(res) for the request specified in the Link header.

Lerna

lerna
lerna

Manage each fragment with Lerna. I divided the packages as follows.

  • common
    • Common variables and libraries
  • fragment
    • Definition of LitElement custom elements
  • function
    • Functions that cooperate with fragments (history, events, etc.)

Specifically, I prepared the following.

directory namepackage name
packages/common-module@type/common-module
packages/common-variable@type/common-variable
packages/fragment-auth-components@auth/fragment-auth-components
packages/fragment-product-item@product/fragment-product-item
packages/fragment-search-box@search/fragment-search-box
packages/function-event-hub@controller/function-event-hub
packages/function-history-navigation@controller/function-history-navigation
packages/function-renderer-proxy@controller/function-renderer-proxy
packages/function-search-api@search/function-search-api
packages/function-service-worker@type/function-service-worker

Don't mind the names, I just set them casually at the time. (laughs) What I wanted to say is that I just wanted to do something like @XXX is a domain managed by one team.

If you want to use a package, set the dependency as follows.

package.json

{
  "dependencies": {
    "@controller/function-event-hub": "^0.0.0",
    "@type/common-variable": "^0.0.0"
  }
}

LitElement

LitElement
LitElement

https://lit-element.polymer-project.org/

LitElement A simple base class for creating fast, lightweight web components

Although it would have been sufficient to use pure WebComponents, I used LitElement for the following reasons:

  • You can write in Typescript
  • You can use lit-html, which has good rendering performance
  • Rendering updates due to property changes are possible

Well, I'm not particularly picky about it. The way to write it is as follows:

import { LitElement, html, customElement, css, property } from "lit-element";

@customElement("product-item")
export class ProductItem extends LitElement {
  static styles = css`
    :host {
      display: block;
      border: solid 1px gray;
      padding: 16px;
      max-width: 800px;
    }
  `;
  @property({ type: String })
  name = ``;

  render() {
    return html`<div>${this.name}</div>`;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "product-item": ProductItem;
  }
}

With LitElement + Typescript, you can test using open-testing. https://github.com/PolymerLabs/lit-element-starter-ts/blob/master/src/test/my-element_test.ts

Also, it seems that you can test with jest.

https://www.ninkovic.dev/blog/2020/testing-web-components-with-jest-and-lit-element

DynamicRendering

rendertron
rendertron

In this sample, we are building with the so-called SPA movement, rendering on the browser side using custom elements. You might think you have to do SSR for "SEO!", but honestly, I don't want to think about SSR. (I don't want to make the browser load unnecessary things like hydration) As in the following article, I want to return the result of dynamic rendering (the rendered HTML of the SPA) only for bot access.

https://developers.google.com/search/docs/guides/dynamic-rendering?hl=ja

Technically, you can use something like the following:

https://github.com/GoogleChrome/rendertron

function-renderer-proxy/src/renderer.ts

...
const page = await this.browser.newPage(); // browser: Puppeteer.Browser
...
const result = await page.content() as string;  // Puppeteerのレンダリング結果コンテンツ(HTML)

In essence, it's just returning the result of actual rendering with Puppeteer to the bot.

EventHub

Fragments interact with each other through CustomEvent.

https://developer.mozilla.org/ja/docs/Web/Guide/Events/Creating_and_triggering_events

Everything will go through this EventHub (package name) that manages CustomEvent and AddEventListener. (Ideal)

History

I want to manage the entire page history with HistoryNavigation (package name). (Ideal)

https://developer.mozilla.org/en-US/docs/Web/API/History_API

Also, I introduced the vaadin/router, a library for controlling routing for Web Components, because it seemed convenient.

https://vaadin.com/router

ShareModule

Libraries like LitElement that are used everywhere, I want to commonize and reduce the bundle size. Bundle tools like Webpack have commonization features such as External, DLLPlugin, and ModuleFederation.

https://webpack.js.org/concepts/module-federation/

This time, we are using external.

common-module/common.js

exports["rxjs"] = require("rxjs");
exports["lit-element"] = require("lit-element");
exports["graphql-tag"] = require("graphql-tag");
exports["graphql"] = require("graphql");
exports["apollo-client"] = require("apollo-client");
exports["apollo-cache-inmemory"] = require("apollo-cache-inmemory");
exports["apollo-link-http"] = require("apollo-link-http");

common-module/webpack.config.js

module.exports = {
  entry: "./common.js",
  output: {
    path: __dirname + "/public",
    publicPath: "http://localhost:6006/public/",
    filename: "bundle.js",
    libraryTarget: "amd",
  },
};

The commonized library is loaded in the following Tailor's index.html.

templates/index.html

<script>
  (function (d) {
    require(d);
    var arr = [
      "lit-element",
      "rxjs",
      "graphql-tag",
      "apollo-client",
      "apollo-cache-inmemory",
      "apollo-link-http",
      "graphql",
    ];
    while ((i = arr.pop()))
      (function (dep) {
        define(dep, d, function (b) {
          return b[dep];
        });
      })(i);
  })(["http://localhost:6006/public/bundle.js"]);
</script>

Then, for example, in the webpack of searchBox, you can use the following.

fragment-search-box/webpack.config.js

externals: {
    'lit-element': 'lit-element',
    'graphql-tag': 'graphql-tag',
    'apollo-client': 'apollo-client',
    'apollo-cache-inmemory': 'apollo-cache-inmemory',
    'apollo-link-http': 'apollo-link-http',
    'graphql': 'graphql'
}

Others

I will introduce what I introduced depending on my mood at the time. (or what I was considering introducing)

GraphQL

For the API, I adopted GraphQL casually. There is no particular reason.

SkeltonUI

I also wanted to try using Skelton UI.

https://material-ui.com/components/skeleton/

Even if you don't use React, you should be able to use CSS's @keyframes. But, well, I'm not using it. (laughs)

https://developer.mozilla.org/ja/docs/Web/CSS/@keyframes

Rxjs

I introduced it because I wanted to code the processing of typescript in a reactive atmosphere.

(It's a reason that might make people who are familiar with reactive angry...laughs)

https://rxjs.dev/

Impressions

So far, I have tried using server-side integration libraries related to Micro Frontends such as Podium, Ara-Framework, and Tailor.

https://silverbirder.github.io/blog/contents/microfrontends

https://silverbirder.github.io/blog/contents/ara-framework

I think all of these have good concepts. The interface design of Podium's fragments, the clear separation of Ara-Framework's Render and data acquisition, and Tailor's stream integration. However, while these are good libraries, I don't really want to adopt them (depend on them) as a production.

Rather, I find server-side integration using things like Edge Side Include or Server Side Include, which have been used for a long time, more attractive. For example, Edge Worker seems good. (I'm also interested in HTTP2 and HTTP3)

Well, I haven't found a Micro Frontends design that I'm satisfied with yet, so I think I'll continue to verify it in the future.

If it was helpful, support me with a ☕!

Share

Related tags