マイクロフロントエンド③ CSSとRoutingにおける罠

前回までの記事から引き続き、マイクロフロントエンドの構築でハマった罠についてまとめました。

https://qiita.com/suzuki0430/items/b1b7c2d63e1eaf4d2c66

https://qiita.com/suzuki0430/items/77e885bf9a157b9c840a

#CSSのコンフリクト
MarketingプロジェクトのPricingページからAuthプロジェクトのSignInページに遷移したとき、SignInのCSSが新しくロードされることで、Pricingに戻ったときにも表示に影響を与えてしまうことがあります。

この問題の対策として、CSSのスコーピングという方法があります。
スコーピングとはCSSの影響範囲を絞ることをいい、CSS-in-JSやBootstrapとかMaterial-UIのようなライブラリを利用することで実現することができます。

しかし、**スコーピングを行っても本番環境で表示が崩れる場合があります。**

例えば、Material-UIではCSSスタイルシートをコンポーネントに提供するためにmakeStyles()という関数を使います。

```js
const useStyles = 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を指定することができます。

```App.js
const generateClassName = createGenerateClassName({
  productionPrefix: 'ma',
});

export default ({ history }) => {
  return (
    <div>
      <StylesProvider generateClassName={generateClassName}>
        <Router history={history}>
          <Switch>
            <Route exact path="/pricing" component={Pricing} />
            <Route path="/" 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として渡してあげます。

```bootstrap.js
const mount = (el) => {
  const history = createMemoryHistory();
  ReactDOM.render(<App history={history} />, el);
};
```

```App.js
export default ({ history }) => {
  return (
    <div>
      <StylesProvider generateClassName={generateClassName}>
        <Router history={history}>
          <Switch>
            <Route exact path="/pricing" component={Pricing} />
            <Route path="/" component={Landing} />
          </Switch>
        </Router>
      </StylesProvider>
    </div>
  );
};
```
MarketingプロジェクトにMemory Historyを導入したところで、ホーム画面からMarketingプロジェクトのページ(pricing)に遷移してみると、URLが`/`のままです...
![スクリーンショット 2021-03-16 22.40.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/bfb0135d-5fd3-63b8-53aa-59af7f571fae.png)


##マイクロフロントエンドにおける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にカレントパスの変更を指示することができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/8369a16a-44d1-af93-710d-7a7a2ef51e01.png" width="700">

`useHistory`でContainerのBrowser Historyのオブジェクトを呼び、`useEffect`内でカレントパスを更新します。

```MarketingApp.js
  const history = 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`を追加します。

```MarketingApp.js
  const history = 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と整合がとれるようになります。

```bootstrap.js
const mount = (el, { onNavigate }) => {
  const history = createMemoryHistory(); // Marketingのhistoryオブジェクト

  if (onNavigate) { // 本番環境(Containerがある)のときだけ実行
    history.listen(onNavigate);
  }

  ReactDOM.render(<App history={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に渡してあげるようにすれば大丈夫です。

```bootstrap.js
import { createMemoryHistory, createBrowserHistory } from 'history';
.
.
const history = defaultHistory || createMemoryHistory();
.
.
if (process.env.NODE_ENV === 'development') {
  const devRoot = document.querySelector('#_marketing-dev-root');

  if (devRoot) {
    mount(devRoot, { defaultHistory: createBrowserHistory });
  }
}
```

#おわりに
Historyの概念がうろ覚えだったので勉強になりました。

#参考資料
https://www.udemy.com/share/103KXSCEEZd11VRno=/

https://zenn.dev/h_yoshikawa0724/articles/2020-09-22-react-router

https://reactrouter.com/web/api/history