qiankun によるマイクロフロントエンドの統合メカニズム
はじめに
こんにちは、READYFOR のテックリード兼フロントエンドエンジニアの菅原(@kotarella1110)です!
READYFOR はクラウドファンディングのプラットフォームで、数年前に実行者(プロジェクトを掲載するユーザー)向けの管理画面をリニューアルし、その際にマイクロフロントエンドアーキテクチャを採用しました。管理画面リニューアルのベータ版リリース時は三つの独立したフロントエンドアプリケーションを統合し、一つの統一されたアプリケーションとして提供していましたが、その後の機能追加によりフロントエンドアプリケーションが増え、現在では六つのフロントエンドアプリケーションを統合して提供しています。
この統合には、qiankun(チェンクン)というマイクロフロントエンドフレームワークを使用しています。そこで、本記事では qiankun によるマイクロフロントエンドの統合メカニズムついて説明したいと思います。
マイクロフロントエンドの説明については省略致しますので、マイクロフロントエンドについて知りたい方は、以下のリンク先の記事が詳しいです。
qiankun とは?
qiankun は、Ant Design などで有名な Ant Group によって開発・メンテナンスされているマイクロフロントエンドフレームワークです。異なる技術スタックや独立したサービスとして機能する複数のフロントエンドアプリケーションを統合し、一つの統一されたアプリケーションを簡単かつ手間なく構築できます。マイクロフロントエンドフレームワークとして人気な single-spa のラッパーであり、single-spa の欠点や問題点を解決しています。マイクロフロントエンドを構築する際の組成パターンは多岐に渡りますが、qiankun はクライアントサイド組成パターンのフレームワークです。
qiankun での実装の仕方
qiankun の Getting Started にも実装の仕方が記載されていますが、以下に簡単な実装の仕方を紹介します。また、以前に qiankun の実装例を GitHub に公開していますので、参考にしてください。
メインアプリケーションの実装
qiankunでは、一つのメインアプリケーションの上に複数のサブアプリケーションが配置される構成となります。
qiankun のインストール
メインアプリケーション側では、qiankun をインストールする必要があります。
npm install qiankun
サブアプリケーションの登録
メインアプリケーションのエントリーファイルでは以下のようにサブアプリケーションを登録します。以下のコードでは、ブラウザの URL が /path1
に変更されると http://localhost:5001 で起動しているサブアプリケーションが #container
にマウント(表示)され、/path2
に変更されると http://localhost:5002 で起動しているサブアプリケーションがマウントされます。
import { registerMicroApps, start } from "qiankun";
registerMicroApps([
{
name: "subApp1", // サブアプリケーションの名前
entry: "//localhost:5001", // サブアプリケーションの URL
container: "#container", // サブアプリケーションをマウントする要素
activeRule: "/path1", // サブアプリケーションをマウントするパス
},
{
name: "subApp2",
entry: "//localhost:5002",
container: "#container",
activeRule: "/path2",
},
]);
start();
サブアプリケーションの実装
サブアプリケーション側では、メインアプリケーションに統合するための追加の依存関係をインストールする必要はありません。
qiankun のライフサイクルをエクスポート
サブアプリケーションのエントリーファイルでは、bootstrap
、mount
、unmount
という3つのライフサイクルを必ずエクスポートする必要があります。これは、メインアプリケーション(qiankun)が適切なタイミングでこれらのライフサイクルを呼び出せるようにするためです。メインアプリケーション側でこれらのライフサイクルを呼び出して、サブアプリケーションをメインアプリケーションに統合します。
import { createRoot, Root } from "react-dom/client";
import App from "./App";
// サブアプリケーションに動的にロードされるスクリプト、スタイル、イメージ、その他のアドレスが正しくない問題に対処します。
if (window.__POWERED_BY_QIANKUN__) {
window.__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let root;
const render = (props) => {
// コンテナ要素を見つけます。`props.container` は、メインアプリケーション(qiankun)側での `mount` の呼び出し時に引数として渡されるサブアプリケーションのマウント先です。`props.container` があれば使用し、そうでなければデフォルトの `#root` を使用します。
const container = props?.container
? props.container.querySelector("#root")
: document.getElementById("root");
root = createRoot(container);
root.render(<App {...props} />);
};
// window.__POWERED_BY_QIANKUN__ がない場合は、単独で通常のアプリケーションとしてレンダリングします。
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* メインアプリケーション(qiankun)が呼び出すライフサイクルを定義する
*/
// サブアプリケーションが初期化されるときにのみ呼び出されます。サブアプリケーションがメインアプリケーションから再度アクセスされると、`bootstrap` が繰り返し呼び出されず `mount` が直接呼び出されます。
export async function bootstrap() {
console.log(`subApp1 bootstrap`);
}
// サブアプリケーションがアクセスされる度に呼び出されます。
export async function mount(props) {
console.log(`subApp1 mount`, props);
render(props);
}
// アプリケーションの切り替わったりアンロードされる度に呼び出されます。
export async function unmount(props) {
console.log(`subApp1 unmount`, props);
root.unmount();
}
// オプションのライフサイクル。qiankun の loadMicroApp でのみ呼び出されます。
export async function update(props) {
console.log(`subApp1 update`, props);
}
サブアプリケーションのバンドラー設定
サブアプリケーションのエントリーファイルでライフサイクルをエクスポートしただけでは、メインアプリケーション側からライフサイクルを呼び出すことができません。そのため、以下のようにバンドラーを設定して UMD 形式でビルドする必要があります。
module.exports = {
//...
output: {
//...
library: `subApp1-[name]`,
libraryTarget: "umd",
chunkLoadingGlobal: `webpackJsonp_subApp1`,
},
};
UMD 形式にする理由は、エントリーファイルでエクスポートされた関数や変数がブラウザの window
オブジェクトに保持されるようにするためです。これらが window
オブジェクトに保持される際の名前は、webpack の設定で指定された library
オプションによって決まります。上の設定では、library
オプションに subApp1-[name]
という値が指定されています。そのため、エクスポートされた関数や変数は、 window
オブジェクトに subApp1-モジュール名
という名前で保持されます。ブラウザでエントリーファイルを読み込んだ後、エクスポートされたライフサイクルには、以下のように window["subApp1-モジュール名"]
という名前でアクセスできるようになります。
console.log(window["subApp1-main"]);
/*
{
bootstrap: ƒ (),
mount: ƒ (props),
unmount: ƒ (props),
update: ƒ (),
__esModule: true,
Symbol(Symbol.toStringTag): "Module",
get bootstrap: () => { ... },
get mount: () => { ... },
get unmount: () => { ... },
get update: () => { ... },
[[Prototype]]: Object
}
*/
window["subApp1-main"].bootstrap(); // qiankun 側ではこのようにライフサイクル関数を呼び出す
qiankun による統合メカニズム
先述の実装を通じて、qiankun を使用することで簡単にマイクロフロントエンドを構築できることが分かりました。
続いて qiankun による統合メカニズムについて理解していきましょう。以下は、qiankunによる統合のシーケンス図です。
plantuml
@startuml
skinparam monochrome true
skinparam shadowing false
title qiankun による統合メカニズム
participant "メインアプリケーション" as mainApp
participant "サブアプリケーション" as subApp
autonumber
mainApp -> subApp: HTMLをfetch
subApp -> mainApp
mainApp -> mainApp: HTMLの解析\n(エントリーを含むスクリプト及び外部スタイルの抽出・HTMLの編集を行う)
mainApp -> subApp: 抽出した外部スタイルを fetch
subApp -> mainApp: HTML 内の外部スタイルをインラインスタイルに置き換え
mainApp -> mainApp: サブアプリケーションの編集された HTML を DOM に変換して\nメインアプリケーションにレンダリング
mainApp -> mainApp: 抽出したエントリーを含むスクリプトを実行し、\nエントリーでエキスポートされたライフサイクルを取得
mainApp -> mainApp: ライフサイクルを呼び出してサブアプリケーションを統合
@enduml
このシーケンス図に沿って、qiankun による統合メカニズムについて詳しく説明します。
1〜2. サブアプリケーションの HTML を fetch する
qiankun はメインアプリケーションからサブアプリケーションをロードして統合するために import-html-entry の importEntry
という API を呼び出します。registerMicroApps
に渡された entry
(サブアプリケーションの URL)は、importEntry
の第一引数として渡されます。
続いて、entry
(サブアプリケーションの URL)は string
のため、importHTML
関数を呼び出します。
importHTML
は、サブアプリケーションの URL で fetch して HTML を取得します。
3. HTML の解析
その後、processTpl
関数で、1〜2で取得した HTML の解析を行います。HTML からエントリースクリプト(entry
)や HTML 内の全てのスクリプト(scripts
)、HTML 内の外部スタイル(styles
)を抽出し、HTML を編集します。この編集では、<script>
のコメントアウトや置き換え等が行われます。編集された HTML は template
に格納されます。
4〜5. 抽出した外部スタイルを fetch し、HTML 内の外部スタイルをインラインスタイルに置き換え
次に、getEmbedHTML
関数で、3で抽出した外部スタイルを fetch して取得します。
fetch により取得した外部スタイルの内容を元に、template
(embedHTML
)内で <link>
で指定された外部スタイルを <style>
タグ内のインラインスタイルに変換します。
例えば、サブアプリケーションで http://localhost:5001/example.css という外部スタイルが以下のように定義されているとします。
/* http://localhost:5001/example.css */
.example {
color: red;
}
そして、サブアプリケーションの HTML 内で以下のように指定されている場合、
<link href="http://localhost:5001/example.css" rel="stylesheet" />
この外部スタイルを取得し、その内容を使って、次のようにインラインスタイルに変換します。
<style>
.example {
color: red;
}
</style>
6. サブアプリケーションの編集された HTML を DOM に変換してメインアプリケーションにレンダリング
最終的に、importEntry
が返すオブジェクトには、以下のものが含まれます。
ここでようやく、qiankun 側に戻ってきます。
importEntry
が返すオブジェクトの内、qiankun で特に重要なのが template
と execScripts
です。まず、template
は3〜5で編集されたサブアプリケーションの HTML です。qiankun はこの HTML を DOM に変換し、メインアプリケーションのコンテナにレンダーします。
7. 抽出したエントリーを含むスクリプトを実行し、エントリーでエキスポートされたライフサイクルを取得
続いて、execScripts
は 3で抽出したエントリースクリプトと全てのスクリプトを実行するための関数です。インラインスクリプトであればそのまま実行され、外部スクリプトは fetch して取得し、その内容を実行します。
スクリプトの実行には、以下の通り eval
が使用されます。
例えば、サブアプリケーションで http://localhost:5001/example.js という外部スクリプトが以下のように定義されているとします。
// http://localhost:5001/example.js
function example() {
console.log("example");
}
example();
そして、サブアプリケーションの HTML 内で以下のように指定されている場合、
<script src="http://localhost:5001/example.js"></script>
HTML 内の外部スクリプトの URL を3で抽出し、その URL から fetch して取得し、その内容を eval
で実行します。
eval(`
function example() {
console.log("example");
}
example();
`);
また、execScripts
は、UMD 形式のエントリースクリプトでエキスポートされた値を返します。エントリースクリプトを eval
で実行することで、エキスポートされたライフサイクルを window
オブジェクトに保持し、window
オブジェクトに保持されたライフサイクルを Promise で返します。
qiankun では、メインアプリケーションからこの execScripts
を呼び出して、サブアプリケーションの全てのスクリプトを実行し、エントリースクリプトでエキスポートされたライフサイクルを取得します。
8. ライフサイクルを呼び出してサブアプリケーションを統合
最終的に qiankun が適切なタイミングで7で取得したライフサイクルを呼び出すことで、サブアプリケーションのマウントやアンマウントを制御し統合することができます。
おわりに
本記事では、qiankun を使用したマイクロフロントエンドの統合メカニズムについて詳しく解説しました。qiankun を利用することで、異なる技術スタックや独立したサービスとして機能する複数のフロントエンドアプリケーションを統合し、一つの統一されたアプリケーションを簡単かつ効率的に構築することが可能です。実装方法や統合の仕組みを理解することで、マイクロフロントエンドの開発や管理をスムーズに行うことができます。
また、qiankun の興味深い機能の一つに、サンドボックスと呼ばれるサブアプリケーション間で JavaScript とスタイルを分離する機能があります。この機能を有効にすることで、サブアプリケーション間での JavaScript のグローバル変数やイベントの衝突や競合を防ぎ、スタイルが互いに干渉することを防いでくれます。この実現方法についてもなかなか面白いので、時間があれば別の記事で詳しく紹介したいと考えています。
「みんなの想いを集め、社会を良くするお金の流れをつくる」READYFORのエンジニアブログです。技術情報を中心に様々なテーマで発信していきます。 ( Zenn: zenn.dev/p/readyfor_blog / Hatena: tech.readyfor.jp/ )
Discussion