Zalando tailor で Micro Frontends with ( LitElement & etcetera)
Zalando 社が開発した Tailor を使って、サンプル Web アプリを Micro Frontends で構築してみました。Tailor はサーバーサイドで統合するアーキテクチャです。クライアントサイドは、Web Components で作られている Lit Element を使って統合しました。どういった内容か、ここに投稿しようと思います。
作ったリポジトリは、下記に残しています。 https://github.com/silverbirder/micro-frontends-sample-code-4
全体構成
ざっくり説明すると、HTML から Tailor に対してフラグメント(コンポーネント)を取得・返却するようにします。各フラグメントは、LitElement で WebComponents を定義させた Javascript を指します。フラグメントを読み込むだけで、カスタムエレメントを使えるようになります。
Tailor
https://github.com/zalando/tailor
A streaming layout service for front-end microservices
tailor は、ストリーミングレイアウトサービスというだけあって、fragment の load をストリーミングするそうです。(こちらのライブラリは、Facebook のBigPipe に影響されたそう)
まず、tailor.js の HTML テンプレートは次のとおりです。
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>
これらの fragment の取得は、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 は、後ろのフラグメントに URL を引き継ぐためのようです。 そして、フラグメントサーバーは、次のとおりです。
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 は、Response Header に Link ヘッダを追加するようにします。Tailor は、このヘッダの Javascript を読み込むことになります。
さらに、fragments.js は、Link ヘッダで指定されたリクエストを return fs.createReadStream('./public/bundle.js').pipe(res)
でストリームのパイプを返すそうです。
Lerna
それぞれのフラグメントを Lerna で管理するようにします。 私は、下記のような packages 分けをしました。
- common
- 共通する変数・ライブラリ
- fragment
- LitElement のカスタムエレメント定義
- function
- フラグメントと連携する関数 (ヒストリーやイベントなど)
具体的に言うと、次のようなものを用意しました。
directoy 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 |
どの名前も、その時の気分で雑に設定したので、気にしないでください。(笑) 伝えたいのは、@XXX が 1 チームで管理する領域みたいなことをしたかっただけです。
package を使いたい場合は、次のような依存を設定します。
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
純粋な WebComponents だけを使えばよかったのですが、次のような理由で LitElement を使いました。
- Typescript が書ける
- レンダリングパフォーマンスの良い lit-html が使える
- プロパティ変化によるレンダリング更新ができる
まあ、特にこだわりはないです。 書き方は、次のとおりです。
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;
}
}
LitElement + Typescript では、open-testing を使ってテストすることができます。 https://github.com/PolymerLabs/lit-element-starter-ts/blob/master/src/test/my-element_test.ts
また、jest でもテストができるようです。
https://www.ninkovic.dev/blog/2020/testing-web-components-with-jest-and-lit-element
DynamicRendering
このサンプルでは、カスタムエレメントを使って、ブラウザ側でレンダリングする 所謂 SPA の動きで構築しています。 『SEO ガー!』と SSR しなきゃと思う訳ですが、正直 SSR を考えたくないです。(ハイドレーションなんて無駄なロードをブラウザにさせたくない) 次の記事のように、ボットのアクセスのみに、ダイナミックレンダリングした結果(SPA のレンダリング結果 HTML)を返すようにしたいです。
https://developers.google.com/search/docs/guides/dynamic-rendering?hl=ja
技術的には、次のようなものを使えば良いです。
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)
要は、Puppeteer で実際にレンダリングさせた結果を Bot に返却しているだけです。
EventHub
フラグメント同士は、CustomEvent を通して連携します。
https://developer.mozilla.org/ja/docs/Web/Guide/Events/Creating_and_triggering_events
全て、この CustomEvent と AddEventListener を管理する EventHub(packages 名)を経由するようにします。(理想)
History
ページ全体のヒストリーは、HistoryNavigation(packages 名)で管理したいと考えています。(理想)
https://developer.mozilla.org/en-US/docs/Web/API/History_API
また、ルーティングを制御する Web Components 向けライブラリ vaadin/router も便利そうだったので導入してみました。
ShareModule
LitElement のようなどこでも使っているライブラリは、共通化してバンドルサイズを縮めたいです。 Webpack のようなバンドルツールには、External や DLLPlugin、ModuleFederation などの共通化機能があります。
https://webpack.js.org/concepts/module-federation/
今回は、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",
},
};
共通化したライブラリは、次の Tailor の 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>
そうすると、例えば searchBox の webpack では、次のようなことが使えます。
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'
}
その他
その時の気分で導入したものを紹介します。(or 導入しようと考えたもの)
GraphQL
API は、雑に GraphQL を採用しました。特に理由はありません。
SkeltonUI
Skelton UI も使ってみたいなと思っていました。
https://material-ui.com/components/skeleton/
React を使わなくても、CSS の@keyframes を使えば良いでしょう。が、まあ使っていません。(笑)
https://developer.mozilla.org/ja/docs/Web/CSS/@keyframes
Rxjs
typescript の処理をリアクティブな雰囲気でコーディングしたかったので導入してみました。
(リアクティブに詳しい人には、怒られそうな理由ですね...笑)
所感
これまで、Podium、Ara-Framework, そして Tailor といった Micro Frontends に関わるサーバーサイド統合ライブラリを使ってみました。
https://silverbirder.github.io/blog/contents/microfrontends
https://silverbirder.github.io/blog/contents/ara-framework
これらは、どれも考え方が良いなと思っています。 Podium のフラグメントのインターフェース設計、Ara-Framework の Render とデータ取得の明確な分離、そして Tailor のストリーム統合です。 しかし、これらは良いライブラリではありますが、プロダクションとしてはあんまり採用したくない(依存したくない)と思っています。
むしろ、もっと昔から使われていた Edge Side Include や Server Side Include などを使ったサーバーサイド統合の方が魅力的です。 例えば、Edge Worker とか良さそうです。(HTTP2 や HTTP3 も気になります)
まあ、まだ納得いく Micro Frontends の設計が発見できていないので、これからも検証し続けようと思います。
Tags
2022-11-13
DAZN の Luca Mezzalira さんが書かれたマイクロフロントエンドを読みました。簡単な書籍レビューを残しておこうかなと思います。...
2022-05-28
Micro Frontends(以降、MFE)で組成するフラグメントを Web Components で定義して Module Federation で共有する方法を、ざっくり紹介します。...
2021-01-16
2021年、あけましておめでとうございます。本年も宜しくおねがいします。最近、体重が増えてしまったため、有酸素運動を頑張っています。本記事は、昨年の冬あたりから検証していた クライアントサイド統合でのMicro Frontendsについて話そうと思います。検証したソースコードは、次のリポジトリにあります。...
2020-11-19
Micro Frontends とは?皆さん、**Micro Fronends**(以下、MFE)をご存知でしょうか。説明をざっくりしますと、Microservicesの考え方をフロントエンドまで拡張した考え方です。Microservicesは、バックエンド側で適用される事例をよく耳にしますが、フロントエンドでの適用事例は、あまり聞いたことがありません。...
2020-11-15
今回、またMicro Frontendsの構築を試みようと思います。構築パターンの内、サーバーサイド統合パターン、特にエッジサイド統合を試しました。その内容を紹介します。サンプルコードは、下記に残しています。...
2020-10-07
Micro Frontendsに関わる記事を100件以上読みました(参考記事に記載しています)。そこから得たMicro Frontendsについてこの投稿に記録します。また、調査メモについて、次のリポジトリに残しています。...
2020-08-23
みなさん、こんにちは。silverbirder です。、Micro Frontends があります。今、Ara-Frameworkというフレームワークを使った Micro Frontends のアプローチ方法を学んでいます。...
2020-05-04
Micro FrontendsというWebフロントエンドアーキテクチャがあります。このアーキテクチャを知るために、書籍を読み、簡単なサンプルWebアプリを開発しました。そこから学んだことをすべて議事録として残したいと思います。...