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
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
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
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 name | package 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
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
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.
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)
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.
Share
Related tags
- I read 'Micro Frontends'
- Defining Fragments Composed in Micro Frontends as Web Components and Sharing them with Module Federation
- Micro Frontends on the Client Side (ES Module)
- Memo Micro Frontends
- Building Micro Frontends with Cloudflare Workers (Edge Worker)
- Everything you need to know about Micro Frontends
- Micro Frontends with SSR in Ara-Framework
- Everything I Learned About Micro Frontends
- Created an App to Consistently Record and Visualize Data in a Free Format
- Developing "Bochi-Bochi", an App to Easily Find Cheap Ingredients
- What I Learned from Refreshing My Blog Page with Qwik
- Introducing AI Ghostwriter - A Tool to Improve Writing Efficiency
- Development of Stable Diffusion API
- Defining Fragments Composed in Micro Frontends as Web Components and Sharing them with Module Federation
- Created OEmbed and OGP WebComponents for use on my blog site
- Things I Learned from Developing Chrome Extensions (Manifest V3)
- If you're writing in Markdown, Rocket, an SSG that uses WebComponents, is recommended!
- Refreshing Silverbirder's Portfolio Page (v2)
- I Made an API That Only Returns Google Account Images
- Building a TikTok Scraping Infrastructure on GCP and the Challenges Faced
- Micro Frontends on the Client Side (ES Module)
- Micro Frontends with SSR in Ara-Framework
- Created a GAS Library, zoom-meeting-creator, to Automatically Generate Zoom Meetings
- Introducing a Tool for Bulk Updating Account Images and What I Learned
- Cotlin is a Tool for Collecting Links on Twitter, Discover Presentations from Around the World
- I tried creating rMinc, a service that registers GMail to GCalendar
- I Tried Making a One-Frame Comic Search Service Tiqav2 (Algolia + Cloudinary + Google Cloud Vision API)