ホーム自己紹介ブログ
NO.51
DATE2020. 10. 04

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

image
image
GitHub - zalando/tailor: A streaming layout service for front-end microservices
A streaming layout service for front-end microservices - zalando/tailor
github.com

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
lerna

それぞれのフラグメントを Lerna で管理するようにします。 私は、下記のような packages 分けをしました。

  • common
    • 共通する変数・ライブラリ
  • fragment
    • LitElement のカスタムエレメント定義
  • function
    • フラグメントと連携する関数 (ヒストリーやイベントなど)

具体的に言うと、次のようなものを用意しました。

directoy 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

どの名前も、その時の気分で雑に設定したので、気にしないでください。(笑) 伝えたいのは、@XXX が 1 チームで管理する領域みたいなことをしたかっただけです。

package を使いたい場合は、次のような依存を設定します。

package.json

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

LitElement

LitElement
LitElement
Lit
Simple. Fast. Web Components.
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 でもテストができるようです。

Testing Web component with Jest and Lit element
We all know that testing is critical for any software product and JS landscape offers a lot of tools to perform this task. One of the most popular ones is Jest which is famous for its simplicity.
www.ninkovic.dev

DynamicRendering

rendertron
rendertron

このサンプルでは、カスタムエレメントを使って、ブラウザ側でレンダリングする 所謂 SPA の動きで構築しています。 『SEO ガー!』と SSR しなきゃと思う訳ですが、正直 SSR を考えたくないです。(ハイドレーションなんて無駄なロードをブラウザにさせたくない) 次の記事のように、ボットのアクセスのみに、ダイナミックレンダリングした結果(SPA のレンダリング結果 HTML)を返すようにしたいです。

回避策としてのダイナミック レンダリング | Google 検索セントラル  |  Documentation  |  Google for Developers
ダイナミック レンダリングは、JavaScript を利用しているコンテンツに問題が発生しているウェブサイトのための回避策です。
developers.google.com

技術的には、次のようなものを使えば良いです。

GitHub - GoogleChrome/rendertron: A Headless Chrome rendering solution
A Headless Chrome rendering solution. Contribute to GoogleChrome/rendertron development by creating an account on GitHub.
github.com

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 を通して連携します。

developer.mozilla.org

全て、この CustomEvent と AddEventListener を管理する EventHub(packages 名)を経由するようにします。(理想)

History

ページ全体のヒストリーは、HistoryNavigation(packages 名)で管理したいと考えています。(理想)

developer.mozilla.org

また、ルーティングを制御する Web Components 向けライブラリ vaadin/router も便利そうだったので導入してみました。

How to use React Router in Hilla applications | Vaadin
Learn how to integrate and utilize React Router with Hilla for seamless routing.
vaadin.com

ShareModule

LitElement のようなどこでも使っているライブラリは、共通化してバンドルサイズを縮めたいです。 Webpack のようなバンドルツールには、External や DLLPlugin、ModuleFederation などの共通化機能があります。

Module Federation | webpack
webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.
webpack.js.org

今回は、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 も使ってみたいなと思っていました。

material-ui.com

React を使わなくても、CSS の@keyframes を使えば良いでしょう。が、まあ使っていません。(笑)

developer.mozilla.org

Rxjs

typescript の処理をリアクティブな雰囲気でコーディングしたかったので導入してみました。

(リアクティブに詳しい人には、怒られそうな理由ですね...笑)

RxJS
rxjs.dev

所感

これまで、Podium、Ara-Framework, そして Tailor といった Micro Frontends に関わるサーバーサイド統合ライブラリを使ってみました。

Micro Frontends を学んだすべて
Micro FrontendsというWebフロントエンドアーキテクチャがあります。このアーキテクチャを知るために、書籍を読み、簡単なサンプルWebアプリを開発しました。そこから学んだことをすべて議事録として残したいと思います。
silverbirder.github.io
Ara-Framework で Micro Frontends with SSR
みなさん、こんにちは。silverbirder です。、Micro Frontends があります。今、Ara-Frameworkというフレームワークを使った Micro Frontends のアプローチ方法を学んでいます。
silverbirder.github.io

これらは、どれも考え方が良いなと思っています。 Podium のフラグメントのインターフェース設計、Ara-Framework の Render とデータ取得の明確な分離、そして Tailor のストリーム統合です。 しかし、これらは良いライブラリではありますが、プロダクションとしてはあんまり採用したくない(依存したくない)と思っています。

むしろ、もっと昔から使われていた Edge Side Include や Server Side Include などを使ったサーバーサイド統合の方が魅力的です。 例えば、Edge Worker とか良さそうです。(HTTP2 や HTTP3 も気になります)

まあ、まだ納得いく Micro Frontends の設計が発見できていないので、これからも検証し続けようと思います。

フロントエンド
成果物

-

シェアする

フォローする

次のページ

Micro Frontends を調べたすべて

前のページ

Ara-Framework で Micro Frontends with SSR

関連する記事

タグ「フロントエンド」の記事

ヒューマンインターフェース ガイドライン という言葉を知りました。

最近、ヒューマンインターフェース ガイドライン(HIG)という言葉を知りました。 「ヒューマンインターフェイスガイドライン」には、どのAppleプラットフォームでも優れた体験を設計できるようにするためのガイドとベストプラクティスが含まれてい

2026-01-24

フロントエンド
AIの利用上限に達した時にすることを残しておく

主にWeb関連の個人開発をしている際に心がけていることを書きます。 月末に近づくにつれ、AIの利用上限に達してしまうことがあります。 その状況になった時、以下のいずれかの選択肢が私の中では残っています。 課金して利用上限を増やす 無料モデル

2026-01-22

フロントエンド
AI
CSSで頑張らなくても、SVGで楽にできるときもある

個人サイトをリニューアルをしています。 ノート風のデザインを目指して、スタイルを調整していました。 ノートの見た目は、現実にあるノートを再現しようとCSSを書いていました。 現在、以下の画像のようなノートになっています。 ノート風デザインの

2026-01-20

フロントエンド

タグ「成果物」の記事

個人サイトリニューアルの振り返り

個人サイト(ジブンノート)をリニューアルしました。本記事では、個人サイトをリニューアルした際にあった出来事などを振り返りたいと思います。ちなみに、個人サイトは以下のページです。ノート風デザインで、ブログ記事が読めるようになりました!🎉

2026-01-29

成果物
振り返り
個人サイトをリニューアルしました!

個人サイトをリニューアルしました!🎉 https://silverbirder.github.io リニューアルは、今回で6回目です。制作期間は、去年の12月27日から今年の1月28日までの約1ヶ月間です。個人的には最速の開発期間でした。AIの力は偉大ですね。本記事では、個人サイトのリニューアルでこだわったポイントについて紹介します。

2026-01-28

成果物
機能リクエスト投稿サービスを作った

個人開発として、機能リクエスト投稿サービスを作成しました。 サービス名は Fequest で、Feature Request の略です。 Fequest は、プロダクトに対して「この機能を追加してほしい」「ここを改善してほしい」といった要望

2025-12-28

成果物
← ブログ一覧へ