前回までの記事から引き続き、マイクロフロントエンドの構築でハマった罠についてまとめました。
CSSのコンフリクト
MarketingプロジェクトのPricingページからAuthプロジェクトのSignInページに遷移したとき、SignInのCSSが新しくロードされることで、Pricingに戻ったときにも表示に影響を与えてしまうことがあります。
この問題の対策として、CSSのスコーピングという方法があります。
スコーピングとはCSSの影響範囲を絞ることをいい、CSS-in-JSやBootstrapとかMaterial-UIのようなライブラリを利用することで実現することができます。
しかし、スコーピングを行っても本番環境で表示が崩れる場合があります。
例えば、Material-UIではCSSスタイルシートをコンポーネントに提供するためにmakeStyles()という関数を使います。
constuseStyles=makeStyles((theme)=>({heroContent:{backgroundColor:theme.palette.background.paper,padding:theme.spacing(8,0,6),},}
ビルドする際に、makeStyles-heroContent-2
のようなセレクタが作られ、.makeStyles-heroContent-2: { backgroundColor: ~ }
というCSSと{ heroContent: 'makeStyles-heroContent-2' }
というJSにわけられるのですが、本番環境でのビルドだと、ファイルをできるだけ小さくするために、jss1
jss2
といった短いセレクタ名に変換されてしまいます。その結果、プロジェクト間でセレクタ名がコンフリクトしてしまい、ルーティングした際にページの表示崩れが発生します。
この問題を解消するためには、プロジェクトごとにセレクタにprefix(ma-
co-
など)をつけてあげる必要があります。
Material-UIであれば、createGenerateClassName
を使用することでprefixを指定することができます。
constgenerateClassName=createGenerateClassName({productionPrefix:'ma',});exportdefault({history})=>{return(<div><StylesProvidergenerateClassName={generateClassName}><Routerhistory={history}><Switch><Routeexactpath="/pricing"component={Pricing}/>
<Routepath="/"component={Landing}/>
</Switch>
</Router>
</StylesProvider>
</div>
);};
Routingの設定
マイクロフロントエンドのRoutingでは以下の点に注意する必要があります。
- Containerおよび各子プロジェクトにそれぞれRouting用ライブラリが必要
- ContainerのRoutingではどの子プロジェクトを表示するかを決める
- 子プロジェクトのRoutingではどのページを表示するかを決める(ルートを追加してもContainerの再デプロイの必要なし)
- 同じ画面に2つ以上の子プロジェクトが表示される場合も想定する(サイドバー+メインでプロジェクトを分ける場合など)
- 既存のRoutingライブラリを使用する
- 子プロジェクトのRoutingは開発時の独立した環境でも使えるようにする
- Containerと子プロジェクト間でRoutingに関する情報のやりとりが必要(history更新のため)
React Router
ReactはSPAであるため、ブラウザからは1つのページとしてしか認識されません。そのため、SPAの画面状態とURLを紐づけてあげる必要があり、そこで使われるのがReact RouterというRouting用のライブラリです。React Routerを使うとHistory APIを操作できるようになり、URLを指定して直接特定の画面にいけたり、ブラウザバックを利用できるようにすることができます。
History
Historyはカレントパスの取得と設定を行うためのオブジェクトです。
- Browser History: カレントパスをURLの/以下で追跡する
- Hash History: カレントパスをURLの#以下で追跡する
- Memory History: カレントパスをメモリ内で追跡する
Containerと子プロジェクトでBrowser Historyを使うと両方でアドレスバーのURLを書き換える必要がでてきてしまうので、子プロジェクト側ではMemory Historyを使います。そのために、createMemoryHistory()を導入して、Routerコンポーネントにhistoryとして渡してあげます。
constmount=(el)=>{consthistory=createMemoryHistory();ReactDOM.render(<Apphistory={history}/>, el);
};
exportdefault({history})=>{return(<div><StylesProvidergenerateClassName={generateClassName}><Routerhistory={history}><Switch><Routeexactpath="/pricing"component={Pricing}/>
<Routepath="/"component={Landing}/>
</Switch>
</Router>
</StylesProvider>
</div>
);};
MarketingプロジェクトにMemory Historyを導入したところで、ホーム画面からMarketingプロジェクトのページ(pricing)に遷移してみると、URLが/
のままです...
マイクロフロントエンドにおけるRoutingの罠
このような問題が起こってしまうのは、ページ内のリンクをクリックしたときにそのリンク先を含んでいるプロジェクトのRoutingを参照してしまうためです。例えば、pricingのリンクをクリックしたときは、MarketingプロジェクトのRoutingを参照することになります。ここではMemory Historyが使われているので、リンク先に遷移してもURLは変化しません。
この問題を解消するために、Marketing(子プロジェクト)関連のリンクをクリックしたときにはContainerのBrowser Historyのカレントパスを更新し、Container関連のリンクをクリックしたときはMarketingのMemory Historyのカレントパスを更新する必要があります。
ここでonNavigate
とonParentNavigate
を使います。
onNavigateとonParentNavigate
onNavigate
を使うことで、pricingのリンクをクリックしたときに、ContainerのBrowser Historyにカレントパスの変更を指示することができます。
useHistory
でContainerのBrowser Historyのオブジェクトを呼び、useEffect
内でカレントパスを更新します。
consthistory=useHistory();// ContainerのhistoryオブジェクトuseEffect(()=>{mount(ref.current,{onNavigate:({pathname:nextPathname})=>{// Marketingのカレントパスconst{pathname}=history.location;// Containerのカレントパスif(pathname!==nextPathname){history.push(nextPathname);// Containerのカレントパスの更新}},});});
Container関連のリンクをクリックしたときにMarketingのMemory Historyのカレントパスを更新できるよう、onParentNavigate
を追加します。
consthistory=useHistory();useEffect(()=>{const{onParentNavigate}=mount(ref.current,{onNavigate:({pathname:nextPathname})=>{const{pathname}=history.location;if(pathname!==nextPathname){history.push(nextPathname);}},});history.listen(onParentNavigate);},[]);
onParentNavigate
内でMarketingのカレントパスを更新することで、ContainerのBrowser Historyと整合がとれるようになります。
constmount=(el,{onNavigate})=>{consthistory=createMemoryHistory();// Marketingのhistoryオブジェクトif(onNavigate){// 本番環境(Containerがある)のときだけ実行history.listen(onNavigate);}ReactDOM.render(<Apphistory={history}/>, el);
return{onParentNavigate({pathname:nextPathname}){// Containerのカレントパスconst{pathname}=history.location;// Marketingのカレントパスif(pathname!==nextPathname){history.push(nextPathname);// Marketingのカレントパスの更新}},};};
Memory HistoryとBrowser Historyの切替
子プロジェクトでも開発環境ではBrowser Historyを使用したい場合があります。
そんなときは、開発環境のときだけ、Browser Historyのオブジェクトをmountに渡してあげるようにすれば大丈夫です。
import{createMemoryHistory,createBrowserHistory}from'history';..consthistory=defaultHistory||createMemoryHistory();..if(process.env.NODE_ENV==='development'){constdevRoot=document.querySelector('#_marketing-dev-root');if(devRoot){mount(devRoot,{defaultHistory:createBrowserHistory});}}
おわりに
Historyの概念がうろ覚えだったので勉強になりました。