新規投稿のお知らせを受信されたい方は、サブスクリプションをご登録ください:

Cloudflare Workersによるマイクロフロントエンドの段階的な採用

2022-11-17

10分で読了
この投稿はEnglish繁體中文FrançaisDeutschPortuguêsEspañol简体中文でも表示されます。

レガシーなWebアプリケーションにマイクロフロントエンドの利点をもたらす

Incremental adoption of micro-frontends with Cloudflare Workers

最近、CloudflareはWebアプリケーション構築のための新しいフラグメントアーキテクチャについて書きました。これは高速で費用対効果が高く、大規模なプロジェクトにも拡張でき、かつ高速な反復サイクルを可能にするものです。このアプローチでは、複数のCloudflare Workersを使用して、従来のクライアントサイドのアプローチよりも高速にインタラクティブなアプリケーションにマイクロフロントエンドをレンダリングおよびストリーミングし、より優れたユーザー体験とSEOスコアを実現します。

この方法は、新しいプロジェクトを始める場合や、現在のアプリケーションをゼロから作り直す余裕がある場合に最適です。しかし、現実にはほとんどのプロジェクトは、ゼロから作り直すには規模が大きすぎ、アーキテクチャの変更を段階的にしか採用できません。

この記事では、レガシーなクライアントサイドでレンダリングされるアプリケーションで選択された部分だけを、サーバーサイドでレンダリングされるフラグメントに置き換える方法を提案します。これにより、レガシーコードベースの大規模な書き換えを回避しながら、最も重要なビューがより早くインタラクティブになります。さらに独立して開発が可能で、マイクロフロントエンドアプローチのすべてのメリットを享受できるアプリケーションが完成します。このアプローチはフレームワークに依存しません。この記事では、React、Qwik、およびSolidJSで構築されたフラグメントについて説明します。

大規模なフロントエンドアプリケーションの問題

現在開発されている多くの大規模なフロントエンドアプリケーションは、良好なユーザー体験を提供できていません。これは、ユーザーがアプリケーションを操作する前に、大量のJavaScriptをダウンロードし、解析し、実行する必要があるアーキテクチャが原因であることが多いためです。レイジーローディングによって重要でないJavaScriptのコードの読み込みを遅延させる努力や、サーバーサイドレンダリングの使用にもかかわらず、これらの大規模なアプリケーションでは、ユーザーの入力に反応してインタラクティブになるまでに時間がかかりすぎています。

さらに、大規模なモノリシックアプリケーションは、構築やデプロイが複雑になる可能性があります。複数のチームが1つのコードベースで共同作業することもあり、プロジェクトのテストとデプロイを調整するための努力により、個々の機能の開発、デプロイ、反復が困難になっています。

以前の記事で説明したように、Cloudflare Workersが提供するマイクロフロントエンドはこれらの問題を解決できますが、アプリケーションのモノリスをマイクロフロントエンドアーキテクチャに変換することは困難で、コストがかかる場合があります。ユーザーや開発者がメリットを感じられるようになるまでに、数か月、あるいは数年の開発時間がかかることもあります。

必要なのは、プロジェクトでアプリケーション全体を一度に書き換えることなく、アプリケーションの最も影響力のある部分に段階的にマイクロフロントエンドを採用できるようなアプローチなのです。

フラグメントでレスキュー

フラグメントベースのアーキテクチャの目標は、アプリケーションをCloudflare Workersで迅速にレンダリング(およびキャッシュ)できるマイクロフロントエンドに分割することにより、大規模なWebアプリケーションのロードおよびインタラクションのレイテンシを大幅に削減(Core Web Vitalsで測定されます)することです。課題は、元のプロジェクトにかかるコストを最小限に抑えつつ、レガシーのクライアントサイドでレンダリングされるアプリケーションにマイクロフロントエンドフラグメントを統合する方法です。

Cloudflareが提案する手法では、レガシーアプリケーションのUIで最も価値のある部分を、アプリケーションの他の部分から切り離して変換できます。

多くのアプリケーションでは、UIの最も価値のある部分は、ヘッダー、フッター、ナビゲーションの要素を提供するアプリケーションの「シェル」の中にネストされていることが多いことが判明しています。例えば、ログインフォーム、電子商取引アプリケーションの商品詳細パネル、メールクライアントの受信トレイなどです。

ログインフォームを例にとって説明します。アプリケーションでログインフォームを表示するのに数秒かかるようでは、ログインするのが手間になり、せっかくのユーザーを逃してしまうかもしれません。ログインフォームをサーバーサイドでレンダリングされるフラグメントに変換すれば、レガシーアプリケーションの残りの部分がバックグラウンドで起動する間に、ログインフォームがすぐに表示され、インタラクティブに動作します。フラグメントはすぐにインタラクティブになるため、レガシーアプリケーションが起動してページの残りをレンダリングする前に、ユーザーが認証情報を送信することも可能です。

メインアプリケーションの前にログインフォームが利用可能になることを示すアニメーション

Animation showing the login form being available before the main application

このアプローチにより、エンジニアリングチームは、ユーザー体験の向上を犠牲にしたり、長時間でリスクの高いアプリケーション全体の書き換えを必要としたりする従来のアプローチと比べて、わずかな時間と開発コストでユーザーに価値ある改善を提供することができます。モノリシックなシングルページのアプリケーションを使用しているチームは、マイクロフロントエンドアーキテクチャを段階的に採用し、アプリケーションの最も価値のある部分に改善を加えることで、投資に対するリターンを前倒しで得られるようになります。

UIの一部をサーバーサイドでレンダリングされるフラグメントに抽出する際の課題に、いったんブラウザに表示されたら、レガシーアプリケーションとフラグメントがひとつのアプリケーションのように感じられるようにすることがあります。フラグメントは、レガシーアプリケーションシェルにきちんと埋め込まれ、DOM階層を正しく形成することでアプリケーションへのアクセスを維持する必要がありますが、同時にサーバーサイドでレンダリングされたフラグメントは、レガシーのクライアントサイドでレンダリングされるアプリケーションが起動する前に、できるだけ早く表示されてインタラクティブになるようにしたいものです。まだ起動していないアプリケーションシェルにUIフラグメントを埋め込むにはどうすればいいでしょうか。この問題を解決したのが、Cloudflareが考案した「フラグメントピアシング」という技術です。

フラグメントピアシング

フラグメントピアシングは、サーバーサイドでレンダリングされるマイクロフロントエンドフラグメントが生成するHTML/DOMと、レガシーのクライアントサイドでレンダリングされるアプリケーションが生成するHTML/DOMを結合させるものです。

マイクロフロントエンドフラグメントは、HTMLレスポンスのトップレベルに直接レンダリングされ、すぐにインタラクティブになるように設計されています。バックグラウンドでは、レガシーアプリケーションが、これらのフラグメントの兄弟としてクライアントサイドでレンダリングされます。準備が整うと、フラグメントがレガシーアプリケーションに「ピアシング」され、各フラグメントのDOMがレガシーアプリケーションのDOMの適切な場所に配置されます。視覚的な副作用や、フォーカス、フォームデータ、テキスト選択などのクライアントサイドの状態が失われることはありません。一度「ピアシング」されると、フラグメントはレガシーアプリケーションとのコミュニケーションを開始し、レガシーアプリケーションに統合されたようになります。

ピアシング前の、DOMのトップレベルにある「login」フラグメントと空のレガシーアプリケーション「root」要素を以下に示します。

<body>
  <div id="root"></div>
  <piercing-fragment-host fragment-id="login">
    <login q:container...>...</login>
  </piercing-fragment-host>
</body>

以下では、レンダリングされたレガシーアプリケーションの「login-page」divにフラグメントがピアシングされたことがわかります。

<body>
  <div id="root">
    <header>...</header>
    <main>
      <div class="login-page">
        <piercing-fragment-outlet fragment-id="login">
          <piercing-fragment-host fragment-id="login">
            <login  q:container...>...</login>
          </piercing-fragment-host>
        </piercing-fragment-outlet>
      </div>
    </main>
    <footer>...</footer>
  </div>
</body>

この移行中にフラグメントが移動して目に見えるレイアウトのずれが生じないように、ピアシングの前後でフラグメントの位置が同じになるようにCSSスタイルを適用しています。

アプリケーションはいつでも任意の数のピアシングされたフラグメントを表示することも、まったく表示しないことも可能です。この手法は、レガシーアプリケーションの初期ロードだけに限定されるものではありません。また、フラグメントはいつでもアプリケーションに追加したり、アプリケーションから削除したりすることができます。これにより、ユーザーのインタラクションやクライアントサイドのルーティングに応じたフラグメントのレンダリングが可能になります。

フラグメントピアシングを使えば、フラグメントを1つずつの形でマイクロフロントエンドを段階的に採用できます。フラグメントの粒度や、アプリケーションのどの部分をフラグメントにするかは、お客様が決められます。フラグメントですべて同じWebフレームワークを使用する必要はなく、スタックを切り替えるときや、複数のアプリケーションを買収後に統合するときに便利です。

「Productivity Suite」のデモ

フラグメントピアシングと段階的な採用のデモとして、「productivity suite」デモアプリケーションを開発し、ユーザーがToDoリストを管理したり、Hacker Newsを読んだりできるようにしました。このアプリケーションのシェルは、クライアントサイドでレンダリングされるReactアプリケーションとして実装されています。これは、当社の「レガシーアプリケーション」です。このアプリケーションには、マイクロフロントエンドフラグメントを使用するために更新された3つのルートがあります。

  • /login - クライアントサイドの検証を行うシンプルなダミーのログインフォームで、ユーザーが認証されていないときに表示されます(実装はQwik)。

  • /todos - 1つまたは複数のToDoリストを管理し、2つの共同作業用フラグメントとして実装されています。

    • ToDoリストセレクタ - ToDoリストの選択/作成/削除を行うコンポーネント(実装はQwik)です。

    • ToDoリストエディタ - TodoMVCアプリのクローン(実装はReact)です。

  • /news - HackerNews デモのクローン(実装はSolidJS)です。

このデモでは、レガシーアプリケーションと各フラグメントの両方に異なる独立した技術を使用できることを示しています。

レガシーアプリケーションにピアシングされたフラグメントの図

アプリケーションはhttps://productivity-suite.web-experiments.workers.dev/ にデプロイされています。

まず、任意のユーザー名でログインします(パスワードは不要です)。ユーザーのデータはCookieに保存されるので、ログアウトして同じユーザー名で再ログインできます。ログイン後、アプリケーションの上部にあるナビゲーションバーを使って、さまざまなページに移動します。特に、「ToDoリスト」と「ニュース」のページでは、実際のピアシングを見ることができます。

ページを再読み込みすると、レガシーアプリケーションがバックグラウンドでゆっくりと読み込まれるのに対して、フラグメントが即座にレンダリングされることを確認できます。レガシーアプリケーションが表示される前に、フラグメントの操作を試してみてください。

ページの一番上には、フラグメントピアシングの効果を実際に見ていただくためのコントロールがあります。

  • 「Legacy app bootstrap delay」スライダーで、レガシーアプリケーションが起動するまでの遅延時間をシミュレーションします。

  • 「Piercing Enabled」を切り替えると、アプリがフラグメントを使用しない場合のユーザー体験を確認することができます。

  • 「Show Seams」を切り替えると、現在のページで各フラグメントがどの位置にあるかを確認できます。

仕組み

アプリケーションは、いくつかのビルディングブロックから構成されています。

連携するWorkerとレガシーアプリケーションホストの概要

デモのレガシーアプリケーションホストは、クライアントサイドのReactアプリケーションを定義するファイル(HTML、JavaScript、スタイルシート)を提供しています。他の技術スタックで構築されたアプリケーションも同様に動作します。Fragment Workersは、以前のフラグメントアーキテクチャの記事で説明したように、マイクロフロントエンドフラグメントをホストしています。Gateway Workerはブラウザからのリクエストを処理し、レガシーアプリケーションとマイクロフロントエンドフラグメントからのレスポンスストリームを選択、フェッチ、結合します。

これらの部品がすべてデプロイされると、それらが連携してブラウザからの各リクエストを処理します。では、「/login」ルートに移動したときに何が起こるかを見てみましょう。

ログインページ閲覧時のリクエストのフロー

ユーザーがアプリケーションにナビゲートすると、ブラウザがGateway Workerにリクエストを行い、最初のHTMLを取得します。Gateway Workerは、ブラウザがログインページをリクエストしていることを識別します。ここでは2つのサブリクエストが作成されます。レガシーアプリケーションのindex.htmlを取得するものと、サーバーサイドでレンダリングされるログインフラグメントをリクエストするものです。そして、この2つのレスポンスを組み合わせて、ブラウザに配信されるHTMLを含む1つのレスポンスストリームにします。

ブラウザは、レガシーアプリケーションの空のroot要素と、サーバーサイドでレンダリングされるログインフラグメントを含むHTMLレスポンスを表示し、ユーザーに対してすぐにインタラクティブとなります。

次に、ブラウザはレガシーアプリケーションのJavaScriptをリクエストします。このリクエストは、Gateway Workerによりレガシーアプリケーションホストにプロキシされます。同様に、レガシーアプリケーションやフラグメントの他のアセットは、Gateway Workerを経由して、レガシーアプリケーションホストや適切なFragment Workerにルーティングされます。

レガシーアプリケーションのJavaScriptがダウンロードおよび実行され、その過程でアプリケーションのシェルがレンダリングされると、フラグメントピアシングが起動し、UIの状態をすべて維持したまま、フラグメントをレガシーアプリケーションの適切な場所に移動させることができます。

フラグメントピアシングを説明するためにloginフラグメントに焦点を当てましたが、同じ考え方が/todos/newsルートで実装されている他のフラグメントにも当てはまります。

ピアシングライブラリ

すべてのフラグメントは、異なるWebフレームワークを使用して実装されているにもかかわらず、「ピアシングライブラリ」のヘルパーを使用して同じ方法でレガシーアプリケーションに統合されています。このライブラリは、レガシーアプリケーションとマイクロフロントエンドフラグメントの統合を処理するためにデモ用に開発したサーバーサイドとクライアントサイドのユーティリティのコレクションです。ライブラリの主な機能はPiercingGatewayクラス、フラグメントホストフラグメントアウトレットのカスタム要素、およびMessageBusクラスです。

PiercingGateway

PiercingGatewayクラスは、アプリケーションのHTML、JavaScriptなどのアセットに対するすべてのリクエストを処理するGateway Workerのインスタンスとして使用できます。「PiercingGateway」はリクエストを適切なFragment Workerやレガシーアプリケーションのホストにルーティングします。また、これらのフラグメントからのHTMLレスポンスストリームとレガシーアプリケーションからのレスポンスを組み合わせて、ブラウザに返される1つのHTMLストリームにします。

Gateway Workerの実装はピアシングライブラリを使えば簡単です。PiercingGatewayの新しいgatewayインスタンスを作成し、レガシーアプリケーションホストへのURLと、リクエストに対してピアシングが有効かどうかを決定する関数を渡します。Workersランタイムがfetch()ハンドラを構築できるように、Workerスクリプトからgatewayをデフォルトとしてエクスポートします。

const gateway = new PiercingGateway<Env>({
  // Configure the origin URL for the legacy application.
  getLegacyAppBaseUrl: (env) => env.APP_BASE_URL,
  shouldPiercingBeEnabled: (request) => ...,
});
...

export default gateway;

フラグメントは、registerFragment()メソッドを呼び出すことで登録できます。これにより、gatewayがフラグメントのHTMLとアセットに対するリクエストを自動的にFragment Workerにルーティングできるようになります。例えば、loginフラグメントを登録する場合は、以下のようになります。

gateway.registerFragment({
  fragmentId: "login",
  prePiercingStyles: "...",
  shouldBeIncluded: async (request) => !(await isUserAuthenticated(request)),
});

フラグメントホストとアウトレット

Gateway Workerでリクエストをルーティングし、HTMLレスポンスを結合することは、ピアシングを可能にするための条件を半分満たしたに過ぎません。残りの半分はブラウザです。先に説明した手法でレガシーアプリケーションにフラグメントをピアシングする必要があります。

ブラウザでのフラグメントピアシングは、カスタム要素のペア、フラグメントホスト(<piercing-fragment-host>)、フラグメントアウトレット(<piercing-fragment-outlet>)によって促進されます。

Gateway Workerは、各フラグメントのHTMLをフラグメントホストにラップします。ブラウザでは、フラグメントホストがフラグメントの寿命を管理し、フラグメントのDOMをレガシーアプリケーションの所定の位置に移動する際に使用します。

<piercing-fragment-host fragment-id="login">
  <login q:container...>...</login>
</piercing-fragment-host>

レガシーアプリケーションでは、開発者がフラグメントアウトレットを追加することで、ピアシングされたときにフラグメントが表示されるべき場所をマークします。デモアプリケーションのLoginルートは以下のようになります。

export function Login() {
  …
  return (
    <div className="login-page" ref={ref}>
      <piercing-fragment-outlet fragment-id="login" />
    </div>
  );
}

フラグメントアウトレットがDOMに追加されると、現在のドキュメントから関連するフラグメントホストを検索します。見つかった場合、フラグメントホストとそのコンテンツはアウトレット内に移動されます。フラグメントホストが見つからない場合、アウトレットはGateway WorkerにリクエストしてフラグメントHTMLを取得し、writable-domライブラリMarkoJSチームが開発した小規模ながらパワフルなライブラリ)を使ってフラグメントアウトレットに直接ストリーミングされます。

このフォールバックメカニズムにより、新しいフラグメントを含むルートへのクライアントサイドナビゲーションが可能になります。フラグメントは、初期(ハード)ナビゲーションとクライアントサイド(ソフト)ナビゲーションの両方によって、ブラウザでレンダリングされるようになります。

Message Bus

アプリケーションのフラグメントが完全にプレゼンテーショナルまたは自己完結的でない限り、レガシーアプリケーションや他のフラグメントと通信を行う必要があります。[MessageBus](https://github.com/cloudflare/workers-web-experiments/blob/df50b60cfff7bc299cf70ecfe8f7826ec9313b84/productivity-suite/piercing-library/src/message-bus/message-bus.ts#L18)は、レガシーアプリケーションと各フラグメントがアクセスできる、単純な非同期、同型、かつフレームワークに依存しない通信バスです。

当社のデモアプリケーションでは、ユーザーが認証されたときに、loginフラグメントがレガシーアプリケーションに通知する必要があります。このメッセージの発信は、Qwik LoginFormコンポーネントで次のように実装されています。

const dispatchLoginEvent = $(() => {
  getBus(ref.value).dispatch("login", {
    username: state.username,
    password: state.password,
  });
  state.loading = true;
});

レガシーアプリケーションは、次のようにこれらのメッセージをリッスンできます。

useEffect(() => {
  return getBus().listen<LoginMessage>("login", async (user) => {
    setUser(user);
    await addUserDataIfMissing(user.username);
    await saveCurrentUser(user.username);
    getBus().dispatch("authentication", user);
    navigate("/", { replace: true, });
  });
}, []);

フレームワークに依存せず、サーバーとクライアントの両方でうまく機能するソリューションを必要としていたことが、このメッセージバスの実装にたどり着きました。

お試しください

フラグメント、フラグメントピアシング、Cloudflare Workersを使用することで、レガシーなクライアントサイドでレンダリングされるアプリケーションの開発サイクルだけでなく、パフォーマンスも向上させることができます。これらの変更は段階的に採用することが可能であり、任意のWebフレームワークでフラグメントを実装しながら行うこともできます。

これらの機能を示す「Productivity Suite」アプリケーションは、 https://productivity-suite.web-experiments.workers.dev/でご覧いただけます。

今回ご紹介したコードはすべてオープンソースで、Github(https://github.com/cloudflare/workers-web-experiments/tree/main/productivity-suite)に公開されています。

リポジトリの複製は自由に行ってください。ローカルで実行するのも簡単ですし、Cloudflareにあなた自身のバージョンを(無料で)デプロイすることも可能です。コードは可能な限り再利用できるようにしています。コアロジックのほとんどはピアシングライブラリにあり、お客様のプロジェクトでお試しいただけます。フィードバックや提案、あるいは使ってみたいアプリケーションについてお聞かせいただければ幸いです。 GitHubディスカッションにご参加いただくか、discordチャンネルで当社にお問い合わせください。

Cloudflare Workersとフレームワークの最新のアイデアを組み合わせることで、Webアプリケーションのユーザーと開発者の両方の体験を向上させるための次の大きな一歩になると確信しています。Webの可能性の限界を押し広げ続けることで、より多くのデモ、ブログ記事、コラボレーションが見られることを期待しています。また、このジャーニーに直接参加されたい場合は、採用情報をご覧ください。

Cloudflareは企業ネットワーク全体を保護し、お客様がインターネット規模のアプリケーションを効率的に構築し、あらゆるWebサイトやインターネットアプリケーションを高速化し、DDoS攻撃を退けハッカーの侵入を防ぎゼロトラスト導入を推進できるようお手伝いしています。

ご使用のデバイスから1.1.1.1 にアクセスし、インターネットを高速化し安全性を高めるCloudflareの無料アプリをご利用ください。

より良いインターネットの構築支援という当社の使命について、詳しくはこちらをご覧ください。新たなキャリアの方向性を模索中の方は、当社の求人情報をご覧ください。
Developer Week開発者Cloudflare WorkersEdgeMicro-frontendsDeveloper Platform

Xでフォロー

Peter Bacon Darwin|@petebd
Igor Minar|@IgorMinar
Cloudflare|@cloudflare

関連ブログ投稿