静的サイトだけどSPAのような感覚でページ遷移したい-ReactでCode Splitting-

June 28, 2019

静的サイトなんですが、一度アクセスした後はSPAのような感覚でページ遷移するようなサイトを作りたくてCode Splittingを実装したのでその作業ログです。

具体的には、S3などの静的サイトホスティング上で配信するサイト上で、hoge.com/page1のようなサブページにアクセスした後もSPAのような感覚でページ遷移できるようにしたいです。

Code Splittingとは

各ページで必要なjsファイルだけとってきてページ遷移するときに次のページで必要なjsファイルだけとってくるような構成を作ってみたかったので、やってみました。最初のアクセスでページのロード時間を短くしたいのです。スマホの回線環境とかだとSPAを開こうとしたときにたくさんページがある場合に初期表示に不要なファイルのロードも走るので時間がかかってしまいます。それを解消するための手法としてCode Splittingがあるので試してみました。

成果物のソースコードはこちらに置きました。

https://github.com/SatoshiKawabata/sandbox/tree/1126d4bd2b8f7845790c5e2b9ae867c79db02a9a/webpack-react-typescript-codesplitting

Code Splittingの仕組み

フローとしては以下のイメージ。

  • ページにアクセス
  • そのページに必要なjsファイルのみロード
  • 他のページに遷移
  • 遷移先で必要になったjsファイルのみロード

↓この画像がよくわかります。(ちゃんと理解するCode Splitting より抜粋)

ReactでのCode Splitting

ベストプラクティスがわからなかったんで、Reactの公式サイトを真似てみました。

https://reactjs.org/docs/code-splitting.html

React.lazyよりもLoadable Componentsを使ったほうがサーバサイドレンダリングができて良いのでそっちを選びました。

↓こんな感じでコンポーネントを読み込めば別々のjsファイルとしてバンドルしてくれます。

import loadable from '@loadable/component';
const Home = loadable(() => import('./pages/Home'));
const Page1 = loadable(() => import('./pages/Page1'));
const Page2 = loadable(() => import('./pages/Page2'));

Routingにはreact-router-domを選びました。3年前にreact-routerを使ってたので浦島太郎状態でした。quick startを見ながら実装しました。

↓こんな感じにしました。実際のコードはこちらです。

ReactDOM.render(
  <>
    <h1>hello</h1>,
    <Router>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/page1">Page1</Link>
          </li>
          <li>
            <Link to="/page2">Page2</Link>
          </li>
        </ul>
      </nav>
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/page1" component={Page1} />
          <Route path="/page2" component={Page2} />
        </Switch>
      </Suspense>
    </Router>
  </>,
  document.querySelector("#app")
);

サブディレクトリ(/page2)に直接アクセスできない問題

localhost:8888/page2とかにアクセスすると404が返ってきてしまう問題に当たりました。

色々調べて、このサンプルにいきつきました。

https://github.com/jeantimex/react-webpack-code-splitting

webpackの設定を自分のプロジェクトと比べてみると一つそれっぽいところをみつけました。devServer.historyApiFallbackdisableDotRuleをtrueにしたらいけました。

https://webpack.js.org/configuration/dev-server/#devserverhistoryapifallback

When using dots in your path (common with Angular), you may need to use the disableDotRule

パスでドット?どういうことだろう…?

ただ、この方法だとawsのS3や静的サイトのホスティングサービスなんかでは使えません。なのでページごとのhtmlを用意してやる必要があります。

HtmlWebpackPluginを使ってサブディレクトリにアクセス出来るようにする

devServer.historyApiFallbackを使わない方法を探してみるとHtmlWebpackPluginを使ってページごとのhtmlを生成する方法を見つけました。

HtmlWebpackPluginはWebpackのビルド時にhtmlも生成するプラグインです。Webpackでバンドルしたjsファイルを読み込むコードを注入してくれます。

これを使えば以下のような感じでページごとにディレクトリを切ってhtmlを出力できます。

index.html
page1/index.html
page2/index.html

インストール

npm i -D html-webpack-plugin

webpack.config.jsには下記のようにします。

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      template: `${__dirname}/src/index.html`, // テンプレートとなるhtml
      filename: "index.html", // 出力ファイル名
      inject: "body" // bodyの末尾にscriptタグを挿入
    }),
    new HtmlWebpackPlugin({
      template: `${__dirname}/src/index.html`,
      filename: "page1/index.html",
      inject: "body"
    }),
    new HtmlWebpackPlugin({
      template: `${__dirname}/src/index.html`,
      filename: "page2/index.html",
      inject: "body"
    })
  ]
}

テンプレートとなるhtmlを用意します。今回は./src/index.htmlをテンプレートとして各ページのhtmlを出力します。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Hello</title>
</head>
<body>
  <div id="app"></div>
</body>
</html>

これが出来上がったhtmlです。bodyの末尾にjsファイルを読み込むコードが注入されています。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Hello</title>
</head>
<body>
  <div id="app"></div>
<script type="text/javascript" src="index.js"></script></body>
</html>

できた

Code SplittingができたSPAができました。

,2019-06-26 16.56.05.gif