Yakstは、海外の役立つブログ記事などを人力で翻訳して公開するプロジェクトです。
5年弱前投稿

マイクロフロントエンド

大規模なフロントエンド開発のエクスペリエンスを向上させる、マイクロフロントエンドアーキテクチャの提案

原文
Micro-Frontends – Paul Sweeney – Medium (English)
原文公開日
2019-05-26
翻訳依頼者
D98ee74ffe0fafbdc83b23907dda3665
翻訳者
A3e9a72544c34898bde469ab10cddc25 meiq
翻訳レビュアー
D98ee74ffe0fafbdc83b23907dda3665 doublemarket
原著者への翻訳報告
1739日前 原文へのコメントで報告済み 編集


あなたの現場には、リビルドに時間がかかって仕方がない、大規模なUIはありませんか? 複数のチームで作業していてしょっちゅうコードがコンフリクトするなど、インテグレーションに問題が発生することはありませんか? ひとつのアプリが、あまりにも多くの機能を抱えすぎていませんか? そんなとき、きっとマイクロフロントエンドが役に立つでしょう。 マイクロフロントエンドは、バックエンドのエンジニアリングにおけるマイクロサービスアーキテクチャの概念を、フロントエンド開発に適用したものです。 しかしUIを複数のフロントエンドへ分割することが、どのようにしてプロジェクトがスケールするのに役立つのでしょうか?

以前、2012年から2016年まで勤めていた職場で、私は複数の国にまたがる会社のUIフレームワークのリード開発者を勤めており、チームでマイクロフロントエンドアーキテクチャを設計・実装しました。 この記事では、マイクロフロントエンドがもたらす恩恵、および私がそこから学んだことを共有していきます。

マイクロフロントエンドとは?

マイクロフロントエンドは、特に決まったフレームワークやAPIがあるわけではなく、あくまでアーキテクチャ上の概念です。 マイクロフロントエンドの前提にあるのは、ひとつのアプリにつきひとつの機能だけに集中できるよう、元のアプリケーションをいくつかの小さなアプリへ分割し、それぞれ用の独立したレポジトリを用意することです。 このアーキテクチャは、様々な方法で適用することができます。 アーキテクチャはいくらでもリベラルにでき、各々のアプリケーションを異なるフレームワークで実装することができる一方、むしろ制限をかける方向性でツールを提供し、設計上の決定事項を強制することもできます。 どちらのアプローチにもメリットとデメリットがあり、組織のニーズに大きく左右されることでしょう。

マイクロフロントエンドで特筆に値することは、アプリを複数のプロジェクトに分けたとしても、最終的にはひとつのシングルページアプリケーション (SPA) に統合できるということです。 エンドユーザーから見れば、複数ではなく、正真正銘のひとつのアプリとして見えます。 たいていは、各アプリのライフサイクルを扱う親玉のランタイムがあるので、シングルページアプリケーションのエクスペリエンスを提供することができます。 ゆえに、このアーキテクチャを実装することによって、ユーザーエクエスペリエンス面で何かを失うことはありません。

アーキテクチャの使いどころは?

個人的な意見を述べると、アプリ分割における最良のアプローチは、固有の画面や機能のセットで分割を行うことです。 ご自分のスマートフォンを思い浮かべてください。異なる機能のためには異なるアプリがありますね。 ダイヤルするためには電話アプリがあり、テキストを送るためにはメッセージングアプリがあり、番号を保存するためにはコンタクトアプリがあります。 これらのアプリはしばしば互いに協調し合いますが、しっかり区別された目的があるので別々のアプリとして実装されているのです。

他の例としては、大学の管理システムの開発を想像してください。 そのようなシステムでは、職員のプロフィール、学生のプロフィール、授業の詳細やタイムテーブル、教材の配布、試験の結果などといったものを管理するページを設けることになるでしょう。 これらの機能は緩やかに互いに依存し合っていますが、大部分はスタンドアロンな機能であるといえます。 マイクロフロントエンドアーキテクチャで実装するには格好の適用対象プロジェクトになるでしょう。

大学の管理システムの例

このアプローチをとる理由は?

マイクロフロントエンドが何かは分かりましたが、わざわざこのような複雑なアーキテクチャを選択する理由はなぜでしょうか? 私がマイクロフロントエンドが大規模な開発で真価を発揮すると思う理由は、以下のようなことです。

高速なビルド

プロジェクトが大きくなればなるほど、プロジェクトのビルドには時間がかかるようになりがちです。 私が思うには、たとえWebpackやParcelのようなバンドラが複数のスレッドやキャッシュを利用し、頑張ってパフォーマンスを大幅に改善したとしても、もっと根本的な問題に比べれば些細な差をもたらすにすぎません。 その手のパフォーマンス改善を行なっても、アプリが成長するについれ、ビルドにかかる時間のは徐々に長くなってしまいます。 覚えていてください。開発者にとってのエクスペリエンスが良くないのに、ユーザにとって良いエクスペリエンスを実現するのは困難だということを。

アプリをいくつかの異なるプロジェクトに分割し、それぞれ毎のビルドパイプラインを整備すれば、どんなにシステムが成長したとしても各プロジェクトのビルドはとても高速に行うことができます。 このようにするとプロジェクトを並列化でき、個別にビルドした上で最後の最後で結合すればよいので、継続的インテグレーション (CI) のシステムにも有利に働きます。

動的なデプロイ

私が思うに、マイクロフロントエンドのいちばん素敵なところは、ランタイム含め、他の部分を一切リコンパイルすることなしに新しい機能を追加できることです。既存のシステムがあるときでも、システム全体を移行して再インストールすることなく、新しい機能だけを出してインストールすればよいわけです。これは驚くほど強力で、配布のしかたに新たな可能性をもたらします。たとえば、システムのうち特定の機能だけをライセンス化したい場合、その機能だけを別々のインストーラーに切り出すことができるのです。

並列化した開発

UIを別々のプロジェクトに分割することで、UIを複数のチームで開発する道が開かれます。 各チームは、システムのひとつの機能だけに責任を持てばよいのです。 たとえば、あるチームが電話アプリに注力するあいだ、他のチームはコンタクトアプリに注力する、といった具合です。 各チームは自分たちそれぞれ用のGitレポジトリを持ち、独自のバージョニングや変更ログと共に、好きなタイミングでデプロイを走らせることができます。

ほとんどの部分で、これらのチームはお互いについて知る必要はありません。 ただ、どこかでこれら二つのアプリを統合するタイミングは必要です。 各チームは、パブリックなAPIを用意して後方互換性を定義し保証する必要があります。 このAPIはたいてい、URLを使用して実装されます。

どうやって実装する?

マイクロフロントエンドの実装方法にはいくつか異なるアプローチがあり、本記事ではアプローチの詳細については触れませんが、このアーキテクチャを実装する上で、考える必要のある大切なことがいくつかあります。

ルーティングとアプリの読み込み

通常のアプリでは、しばしばコード分割 (code splitting) をサポートしたルーターが用いられます。 特定のroute群が定義され、各々がimport文を持つ形です。 ただマイクロフロントエンドアーキテクチャでは、これだとスケーラブルになりません。 システムの中のひとつひとつのアプリや機能の全てにおいて、いちいちrouteを定義しなければならないような状況は避けたいことでしょう。 それでは新しい機能をデプロイするのが難しくなってしまいます。 代わりに私だったら利用するアプローチとしては、URLをlistenすることにより、アプリのインスタンス化を扱うランタイムを持つ方法があります。

onRouteChange (route) {
    // Assuming routes are "/<app_name>/<internal-app-url>".
    let parts = route.split('/');
    let app = parts[1];
    let app_url = parts.slice(2).join('/');

    if (this.isRunningApp()) {
        this.suspendCurrentApp();
    }
    import(`/${app}/main.js`).then(app => {
        this.startApp(app, app_url);
    });
}

上記のコードにおけるランタイムの関心事は、アプリのコードが配置されているフォルダの名前に対応させる想定で設けられた、URLの最初の部分だけです。 すでにアプリを実行しているかもしれないので、まずアプリをサスペンドします。 その後で 予想できるフォルダ構成 からコードをインポートし、ロードされたら残りのURLを渡して再開しましょう。

なおバンドラはこのコードに混乱し、ファイルが見つからない旨のエラーや警告を投げるかもしれません。 import文を無視するようにバンドラの設定をする必要が出てくるでしょう。

アプリのライフサイクル

先のセクションで述べたように、アプリは開始、サスペンド、再開、終了してリソースを開放します。 ですので、モバイル系OSで見られるのと似たようなライフサイクルを実装してもよいでしょう。

class MyApp extends Application {
    constructor (args) {}
    onAppSuspended () {}
    onAppResumed () {}
    onAppQuit () {}
}
アプリ間の通信

開発者からよくされる質問に、どうやって複数のアプリ間でデータを渡したらよいか? というものがあります。 通常のスタンドアロンなアプリだと、データを一連のpropsとしてコンポーネントに渡すことが多いです。 けれども、もはやアプリや機能が互いの実装へ直接アクセスできない状況下では、この方法は取れません。

シンプルなデータの場合は、全くもってURLで十分でしょう。 アプリや機能に責任を持つチームが、後方互換性を保証したpublicなAPIを実装すればよいです。 これは、アプリがAndroid OSと通信する際に、異なるintentでカスタムなURLハンドラを登録するやりかたと似ています。 OSはこれらのURLを傍受して、対応するアプリを読み込んでデータを渡します。 基本的にこのやりかたが、マイクロフロントエンドでも使えます。 ランタイムがURLを傍受し、アプリをロードし、残りのURLのデータをアプリに渡します。

しかし、ものすごく複雑で、到底URLでは扱えないようなデータを渡す必要があるときも出てくるでしょう。 アプリ間通信を行うメカニズムには、他にもよい方法がたくさんあります。 ひとつのアプローチとして、一時的にデータのblobをサーバに保存してから、データのblobを取得するクエリ用の一時的なIDをURL内で利用する方法があります。 これはより複雑で、サーバー上でデータを注意深く管理する必要があるものの、単にデータを渡す以上の恩恵をもたらします。 たとえば意図しない再読み込みをしてしまったりブラウザがクラッシュしてしまったときでも、データに永続性を持たせることができます。

アプリ間の通信

ライブラリの共有

マイクロフロントエンドでよく開発者が心配する課題のひとつに、それぞれのフロントエンドで各自のバージョンのフレームワークをインポートすることによる、リソースの無駄遣いがあります。 たしかに、バンドラをデフォルトの状態のまま使った場合には問題になりますが、そのようにする必要はありません。

私の個人的なおすすめは、共有ライブラリを利用することです。 このようなライブラリはシステムに既にインストールされていることが多く、あらゆるアプリケーションからimportすることができます。 以下は、ライブラリをシステムへデプロイする際に利用することができるフォルダ構成の例です:

libraries/
    preact/
        8/
        10/
    components/
        1/
        2/

ここに出てきている番号はなんでしょうか? ライブラリのメジャーバージョンです。 ライブラリがセマンティックバージョニング (semver) に従っていれば、その定義からして、同じメジャー番号のライブラリには後方互換性があります。 アプリがライブラリを使いたいとき、特定のマイナーバージョンやパッチのバージョンまで指定するよりは、メジャーバージョンを指定するでしょう。

このやりかたの利点は以下の通りです。

キャッシュ面での利点: ブラウザキャッシュがより効果的に活用される可能性が高まります。 というのも、アプリが参照するのは別々にキャッシュされている複数の特定のバージョンのライブラリではなく、同じライブラリのファイルになるからです。

更新にあたっての利点: この方法をとれば、ライブラリにバグ修正や軽いUXの変更があったとしても、ただ新しいバージョンのライブラリをデプロイするだけで済みますし、ライブラリのメジャーバージョンしか指定していないので、全てのアプリがリコンパイルなしで自動的に最新バージョンを利用するようになります。

iframeはどう?

使うのはやめましょう。 もっとも、お試しのサンドボックス的な用途で使うにはよいのですが、iframe利用下でナビゲーションや親フレームを使ったメッセージングをやろうとすると悲惨な目に合いがちです。

代わりに、アプリの全てのロジックを親側のランタイムで実行しましょう。 そのためにはグローバル変数やCSSに注意を払う必要が出てきますが、強力なlintingルールや便利なhelper関数があれば、それほど心配する必要はないでしょう。

多くのアプリでは、グローバル変数はそこまでの問題にはなりません。 綺麗なやり方ではないかもしれないにせよ、そこまでたくさんの副作用はないはずです。

しかしマイクロフロントエンドアーキテクチャでは、グローバルなスコープにあるものは入念にコントロールする必要があります。 ここでグローバルと言っているのは変数や状態の話だけではなく、windowやdocumentのイベントハンドラ、requestAnimationFrameのループ、永続的なネットワーク接続と言った、アプリがDOMからいなくなった後も動き続ける可能性を持つあらゆるものを含んでいます。 こういったものがリークしてしまう恐れがあることはいとも簡単に忘れられがちですし、不要になったらきちんと解体してあげる必要があるのです。

おわりに

マイクロフロントエンドは、全てのプロジェクトにフィットするものではありません。 世間の大半のプロジェクトにおいては、コード分割さえ行えば十分すぎるくらいだと思います。 マイクロフロントエンドアーキテクチャは、膨大な機能を持つ、大きなエンタープライズレベルのアプリケーションに適しています。 プロジェクトを始めるときは、プロジェクトが将来的にどれくらいの大きさに達するかを考えてみてください。 もし、プロジェクトがスケーラビリティの問題にぶつかるのが避けられないような気がするなら、このアーキテクチャを利用して問題解決するのも一案でしょう。

読んでくださってありがとうございます!

次の記事
Python3.8の新機能
前の記事
モノリポによって得られる多くの恩恵

Feed small 記事フィード

新着記事Twitterアカウント