Zalando 社が開発した Tailor を使って、サンプル Web アプリを Micro Frontends で構築してみました。Tailor はサーバーサイドで統合するアーキテクチャです。クライアントサイドは、Web Components で作られている Lit Element を使って統合しました。どういった内容か、ここに投稿しようと思います。
作ったリポジトリは、下記に残しています。 https://github.com/silverbirder/micro-frontends-sample-code-4
ざっくり説明すると、HTML から Tailor に対してフラグメント(コンポーネント)を取得・返却するようにします。各フラグメントは、LitElement で WebComponents を定義させた Javascript を指します。フラグメントを読み込むだけで、カスタムエレメントを使えるようになります。
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 で管理するようにします。 私は、下記のような packages 分けをしました。
具体的に言うと、次のようなものを用意しました。
| 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 A simple base class for creating fast, lightweight web components
純粋な WebComponents だけを使えばよかったのですが、次のような理由で LitElement を使いました。
まあ、特にこだわりはないです。 書き方は、次のとおりです。
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 でもテストができるようです。
このサンプルでは、カスタムエレメントを使って、ブラウザ側でレンダリングする 所謂 SPA の動きで構築しています。 『SEO ガー!』と SSR しなきゃと思う訳ですが、正直 SSR を考えたくないです。(ハイドレーションなんて無駄なロードをブラウザにさせたくない) 次の記事のように、ボットのアクセスのみに、ダイナミックレンダリングした結果(SPA のレンダリング結果 HTML)を返すようにしたいです。
技術的には、次のようなものを使えば良いです。
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 に返却しているだけです。
フラグメント同士は、CustomEvent を通して連携します。
developer.mozilla.org全て、この CustomEvent と AddEventListener を管理する EventHub(packages 名)を経由するようにします。(理想)
ページ全体のヒストリーは、HistoryNavigation(packages 名)で管理したいと考えています。(理想)
developer.mozilla.orgまた、ルーティングを制御する Web Components 向けライブラリ vaadin/router も便利そうだったので導入してみました。
LitElement のようなどこでも使っているライブラリは、共通化してバンドルサイズを縮めたいです。 Webpack のようなバンドルツールには、External や DLLPlugin、ModuleFederation などの共通化機能があります。
今回は、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 導入しようと考えたもの)
API は、雑に GraphQL を採用しました。特に理由はありません。
Skelton UI も使ってみたいなと思っていました。
material-ui.comReact を使わなくても、CSS の@keyframes を使えば良いでしょう。が、まあ使っていません。(笑)
developer.mozilla.orgtypescript の処理をリアクティブな雰囲気でコーディングしたかったので導入してみました。
(リアクティブに詳しい人には、怒られそうな理由ですね...笑)
これまで、Podium、Ara-Framework, そして Tailor といった Micro Frontends に関わるサーバーサイド統合ライブラリを使ってみました。
これらは、どれも考え方が良いなと思っています。 Podium のフラグメントのインターフェース設計、Ara-Framework の Render とデータ取得の明確な分離、そして Tailor のストリーム統合です。 しかし、これらは良いライブラリではありますが、プロダクションとしてはあんまり採用したくない(依存したくない)と思っています。
むしろ、もっと昔から使われていた Edge Side Include や Server Side Include などを使ったサーバーサイド統合の方が魅力的です。 例えば、Edge Worker とか良さそうです。(HTTP2 や HTTP3 も気になります)
まあ、まだ納得いく Micro Frontends の設計が発見できていないので、これからも検証し続けようと思います。
-
s※ ログイン不要で投稿できます。
※ 同じブラウザから投稿を削除できます。
0
読み込み中...
タグ「フロントエンド」の記事
趣味で作っているWebアプリにて、実装したものを細かくPull Request(以降、PR)を作成します。 PR に対して、機械的にチェックするCIチェックリストがあります。 個人的に気に入っているものを紹介します。 未使用コードの削除 実
Webmention というのを最近知りました。 名前の通り、Web 上でメンションすることができます。 受信側、送信側、どちらも対応したので感想を残そうと思います。 Webmention ざっくりと Web AサイトにBサイトのリンクをつ
ITヲタクな AI くんにピッタリな仕事の一つは、モジュールアップデート。 特に、メジャーアップデートが超助かる。 昔だったら 数ヶ月掛かった移行も、数日たらずに対応できるようになったので、 さすが ITヲタクくん。 対応してよかったフロー
タグ「成果物」の記事
仕事柄、GitHub Busy を設定しています。 この Busy には有効期限を GUI 上で登録することができます。 GitHub Busy 編集画面 困っていること 毎週水曜日に Busy を設定していて、木曜日に解除しています。 こ
2026年06月19日
個人ブログ好きの皆さんに朗報です! "個人ブログを、見つけて、読んで、シェアする" アプリ、こぶりーをリリースしました!🎉 こぶりー アプリのURLは、以下の通りです。 こぶりー - kobliy.vercel.app こちらから、個人ブ
個人サイトリニューアルを機に、記事を書く執筆環境Webアプリを用意しました。 https://silverbirder.github.io/blog/contents/20260128/ どのようなものか簡単に紹介します。 入力とプレビュー
2026年02月20日