webpack環境でredux&react-routerのページをサーバーサイドレンダリングする

(2016-07-10)

このページをGoogleのSearch Consoleからクローラーがちゃんと見ているか確認してみたら、 なぜか真っ白のページが表示されていた・・・。とりあえずサーバーサイドレンダリングしてみることにした。 コードはgithubに上げてある。

サーバーサイドとはいえ、css-loaderでcss moduleを使っているのでwebpackを使う必要があった。 まず、そのままのwebpackの設定で作ったものをserver.jsから呼ぶとエラーが出た。

***/sambaiz-net/web/public/bundle.js:20933
	module.exports = self.fetch.bind(self);
ReferenceError: self is not defined

そこで、targetをnodeにしたサーバーサイド用にwebpackの設定を作成し、実行してみたところ

module.exports = {
	entry: './js/server.js',
	target: 'node',
	output: {
		path: path.join(__dirname, 'dist'),
		filename: 'server.js',
		publicPath: '/'
	},

今度はこんなエラーが出たので

ERROR in ./~/iconv-lite/encodings/tables/gb18030-ranges.json
Module parse failed: ***/sambaiz-net/web/node_modules/iconv-lite/encodings/tables/gb18030-ranges.json Unexpected token (1:9)
You may need an appropriate loader to handle this file type.

loadersに下の設定を追加した。

{ test: /\.json$/, loader: "json-loader"}

webpackには成功したが、serverを起動すると今度は以下のようなエラーが出た。

return /msie [6-9]\b/.test(window.navigator.userAgent.toLowerCase());
ReferenceError: window is not defined

style-loaderのコードだったので、 まず、フロント側のwebpackで extract-text-webpack-pluginを使ってcssを別に出力することにした。

var ExtractTextPlugin = require('extract-text-webpack-plugin');
...
{
  test: /\.css$/,
  loader:  ExtractTextPlugin.extract('style', 'css?modules', 'postcss'),
  include: __dirname
},
...
plugins: [
    new ExtractTextPlugin("styles.css")
]

そして、サーバー側のwebpackではisomorphic-style-loaderでなんとか動かして、 フロント側で出力したcssと対応するようにした。

loaders: ['isomorphic-style', 'css?modules']

これでようやくwebpackまわりの問題は解決できたので、react-routerの方のコードを書いていく。

reduxに関してはドキュメント通りに 描画した後のstoreをこんな感じでフロントに渡す。 そのほかに、同じ処理をサーバーとフロントで二度行わないようにするための値を追加する必要がある。

window.__INITIAL_STATE__ = ${JSON.stringify(state)}

react-routerも以下のようにmatchとRouterContextを組み合わせるだけだ。 ただし、APIリクエスト等の非同期処理が含まれているので、レンダリングが完了したかどうか判断しなくてはならない。 そのため、storeの状態をsubscribeしてレンダリングの終了判定を都度チェックしている。

function render(req, res, isFinishLoading, title, description, fullUrl) {

  match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {

    let store = createStore();

    if (error) {
      res.status(500).send(error.message)
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search)
    } else if (renderProps) {

      const _render = () =>
        renderToString(<Provider store={store}>
          <RouterContext {...renderProps} />
        </Provider>)

      let unscribe = store.subscribe(() => {
        if(isFinishLoading(store.getState()) === true){
          res.status(200).send(
            page(_render(), store.getState(), title, description, fullUrl)
          )
          unscribe();
        }
      })

      _render();

    } else {
      res.status(404).send('Not found')
    }
  })
}