React.jsでプロダクション環境を見据える

reactjsでアプリケーション構築した際、
production環境へのデプロイを見据えてwebpack周りの設定を見直したので
それをまとめました。

# webpack.config.jsのリネーム

webpack.configでbabelなコードが書きたかったのでリネーム
あわせて、constじゃなくて、importでライブラリを読み込むように変更

## リネーム & 書き換え

```bash:
% mv webpack.config.js webpack.config.babel.js
```

```js
// before
const webpack = require('webpack');

// after
import webpack from 'webpack';
```

# webpack-merge
[webpack-merge](https://www.npmjs.com/package/webpack-merge "webpack-merge")

webpack.configをマージするためのモジュール。
共通設定ファイルと環境別の設定ファイルをマージするみたいな用途で利用する。
 
環境別でwebpack.configをかき分けたい。
手動でオブジェクトのdeep mergeするのはいやだ！みたいな人は使おう。

## インストール
`npm i -D webpack-merge`

## 設定

- 共通設定: `webpack.config.babel.js`
- `NODE_ENV=production`: `webpack.config.prod.babel.js`
- `NODE_ENV=development`: `webpack.config.dev.babel.js`

```js:webpack.config.babel.js
import merge from 'webpack-merge';

// NODE_ENVで分岐
const config = process.env.NODE_ENV === 'production' ?
    require('./webpack.config.prod.babel.js') :
    require('./webpack.config.dev.babel.js');

const common = {
  context: __dirname,
  entry: './src/index.jsx',
  output: {
    path: `${__dirname}/public`,
    publicPath: '/',
    filename: 'bundle.js',
  },
  module: {
    preLoaders: [
      { test: /\.jsx?$/, exclude: /node_modules/, loader: 'eslint' },
    ],
    loaders: [
      { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['react-hot-loader/webpack', 'babel'] },
      { test: /\.css$/, loaders: ['style', 'css'] },
      { test: /\.json$/, loader: 'json' },
      { test: /\.(jpe?g|png|gif)$/, loader: 'url?limit=10000' },
    ],
  },
  resolve: {
    extensions: ['', '.js', '.jsx', '.json'],
  },
  eslint: {
    configFile: './.eslintrc',
  },
};

module.exports = merge(common, config);
```

npm runコマンドでwebpack呼ぶときは適宜NODE_ENVをつける。

```package.json
  scripts: {
    start: webpack-dev-server --history-api-fallback --hot --inline --progress --colors,
    build-debug: webpack --display-error-details --progress --colors,
    build: NODE_ENV=production webpack --progress --colors,
  },
```

# UglifyJsPlugin などでjsを最適化・圧縮
UglifyJsPluginで圧縮して、DedupePlugin, AggressiveMergingPluginで最適化する。

詳細が知りたければこちら
[list of plugins](http://webpack.github.io/docs/list-of-plugins.html "list of plugins")

## 設定

```js:webpack.config.prod.babel.js
import webpack from 'webpack';

module.exports = {
  debug: false,
  devtool: false,
  plugins: [
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false,
      },
      comments: false,
    }),
    new webpack.optimize.AggressiveMergingPlugin(),
  ],
};
```

# html-webpack-plugin
[html-webpack-plugin](https://www.npmjs.com/package/html-webpack-plugin "html-webpack-plugin")

index.htmlに直接、bundle.jsを書き込んでいたところに、
ハッシュコード付きのscriptタグを挿入したかったのでこのプラグインを導入した。

ハッシュコードってのはこういうやつね
`<script src="/bundle.js?4398c4ce46af21776b47"></script>`

## インストール
`npm i -D html-webpack-plugin`

## テンプレートファイルの用意
index.htmlをejsファイルにリネームし、`<script src="/bundle.js" />`なコードを除去する。

`mv src/index.html src/index.template.ejs`

```diff:src/index.template.ejs
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <title>title</title>
  </head>
  <body>
    <div id="root"></div>
-  <script type="text/javascript" src="/bundle.js"></script></body>
  </body>
</html>
```


## 設定
pluginsに下記のように設定すると、`npm run build`した時にいい感じでindex.htmlを吐いてくれる。
他にもscriptとかmetaタグを`<head>`に入れたくなったらマニュアル見てね。

```js:webpack.config.babel.js
import HtmlWebpackPlugin from 'html-webpack-plugin';

  plugins: [
    new HtmlWebpackPlugin({
      hash: true,
      filename: 'index.html',
      favicon: './src/favicon.jpg',
      template: './src/index.template.ejs',
      inject: 'body',
    }),
  ],
```

### 出力サンプル

``` html:public/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <title>title</title>
  <link rel="shortcut icon" href="/favicon.jpg"></head>
  <body>
    <div id="root"></div>
  <script type="text/javascript" src="/bundle.js?4398c4ce46af21776b47"></script></body>
</html>
```

# webpack-manifest-plugin
[webpack-manifest-plugin](https://www.npmjs.com/package/webpack-manifest-plugin "webpack-manifest-plugin")

bundle.jsって名前だとCDNに上げた時、世代管理できない。
自動生成されたファイル名にしたいってなったらこれ。

## インストール
`npm i -D webpack-manifest-plugin chunk-manifest-webpack-plugin`

## 設定
下記設定だと、entryのキーが出力後のファイル名になるので注意。
hot loadしたいのでproduction環境以外は`[chunkhash]`つけない。

```diff:webpack.config.js
import ManifestPlugin from 'webpack-manifest-plugin';
import ChunkManifestPlugin from 'chunk-manifest-webpack-plugin';

module.exports = {
  context: __dirname,
  entry: {
+    bundle: './src/index.jsx',
  },
   output: {
     path: `${__dirname}/public`,
     publicPath: '/',
-    filename: 'bundle.js',
+    filename: '[name].js',
   },

  plugins: [
+    new ManifestPlugin(),
+    new ChunkManifestPlugin({
+      filename: 'chunk-manifest.json',
+      manifestVariable: 'webpackManifest',
+    }),
  ]
```

```diff:webpack.config.prod.js
module.exports = {
  context: __dirname,
  entry: {
+    bundle: './src/index.jsx',
  },
   output: {
     path: `${__dirname}/public`,
     publicPath: '/',
-    filename: 'bundle.js',
+    filename: '[name].[chunkhash].js',
   },
  ]
```

`npm run build`したらこういうのが出力される。
manifest.jsonも吐き出されるので、なにかと連携したければそれを読み込むといいかも。

``` html:src/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <title>title</title>
  </head>
  <body>
    <div id="root"></div>
  <script type="text/javascript" src="/bundle.b00a9f3a29b791781cc5.js?7490b7329f7ad1443141"></script></body>
</html>
```

# copy-webpack-plugin
[copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin "copy-webpack-plugin")

本番環境にデプロイして、いざ動作確認してみたら、
react-router使ってるせいで、404出ちゃったとかありませんか。

そしたら、apacheの設定ファイル置きたくなったり、sass用のリダイレクトファイルを
public配下に設置したくなりますよね。 そいういう時にこれ使いました。
例 `.htaccess`, `_redirects`



```js:webpack.config.babel.js
import CopyWebpackPlugin from 'copy-webpack-plugin';
// 〜略〜
  plugins: [
    new CopyWebpackPlugin([
      { from: { glob: './src/static/**', dot: true }, to: '[name].[ext]' },
    ]),
```

# Google Analytics
[react-ga](https://www.npmjs.com/package/react-ga "react-ga")

react-router使ってたら、画面遷移時にアナリティクスに取れなくなるのでこれを導入

## インストール方法
`npm i -S react-ga`

## 設定

```js:src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import ReactGA from 'react-ga';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import App from '../containers/App';

window.React = React;
ReactGA.initialize('UA-123456-7');

const logPageView = () => {
  ReactGA.set({ page: window.location.pathname });
  ReactGA.pageview(window.location.pathname);
};

ReactDOM.render((
  <Router history={browserHistory} onUpdate={logPageView}>
    <Route path="/">
      <IndexRoute component={App} />
      <Route path="/:userName" component={App} />
    </Route>
  </Router>
), document.getElementById('root'));
```

# 所管
あとやり残してることあるかな。気が付いたら追記する。