React x Webpackプロジェクトの雛形を作る

React.jsとWebpackを利用するプロジェクトの雛形を考えます。

ベースの構成を作る

まずはプロジェクトのディレクトリと、必要なnode-moduleをインストールしておきます。

% npm init -y
% npm i -D webpack html-webpack-plugin

html-webpack-pluginはビルドしたJSファイルを読み込むようになっているindex.htmlを生成してくれるプラグインです。

HTML Webpack Plugin

次に、Webpackの設定ファイルと必要に応じて、その他の設定ファイルを作っておきます。

% touch .gitignore .editorconfig webpack.config.js

webpack.config.jsの基本形は以下のようにします。

var path = require('path');  
var HtmlwebpackPlugin = require('html-webpack-plugin');

const PATHS = {  
  app:   path.join(__dirname, 'app'),
  build: path.join(__dirname, 'build')
};

module.exports = {  
  // ビルドに含めるファイルを格納するディレクトリ
  entry: PATHS.app,

  // ビルドしたファイルを格納するディレクトリと、ファイル名
  output: {
    path: PATHS.build,
    filename: 'build.js'
  },

  plugins: [
    // 生成するHTMLの設定
    new HtmlwebpackPlugin({
      title: 'Kanban'
    })
  ]
};

node_modules/.bin/webpackを実行するとビルドができるので、package.jsonscriptsbuildという名前で登録しておきます。

...
"scripts": {
    "build": "webpack"
},
...

buildを実行すると失敗しますが、webpack.config.jsで設定したように、buildディレクトリが生成され、その中にindex.htmlもあります。

% npm run build

webpack-dev-serverを使う

開発中にいちいちbuildを実行するのはしんどいので、ファイルの変更を監視して、ビルドとブラウザの更新をさせるようにします。

% npm i -D webpack-dev-server

これもnode_modules/.bin/webpack-dev-serverで起動できるので、package.jsonscriptsstartという名前で登録しておきます。

...
"scripts": {
    "build": "webpack",
    "start": "webpack-dev-server"
},
...

startnpmで予め用意されているコマンドなので、run無しで実行します。

% npm start
...
...
webpack: bundle is now VALID.  

設定の切り分け

ここまでで最低限のWebpack環境ができていますが、開発中に使う設定と、プロダクション様に使う設定など、目的に応じた設定をそれぞれ切り分けるようにします。

共通の設定 + 目的ごとの設定を作っていくイメージなので、Webpackの設定をマージするためのプラグインを利用します。

% npm i -D webpack-merge

次にwebpack.config.jsでnpmのコマンドごとの設定と、共通設定を分けます。

var path = require('path');  
var HtmlwebpackPlugin = require('html-webpack-plugin');  
var merge = require('webpack-merge');

const TARGET = process.env.npm_lifecycle_event;

const PATHS = {  
  app:   path.join(__dirname, 'app'),
  build: path.join(__dirname, 'build')
};

// 共通設定
const common = {  
  entry: PATHS.app,

  output: {
    path: PATHS.build,
    filename: 'build.js'
  },

  plugins: [
    new HtmlwebpackPlugin({
      title: 'Kanban'
    })
  ]

};

// npm startを実行した時の設定
if(TARGET === 'start' || !TARGET) {  
  module.exports = merge(common, {
  });
}

// npm buildを実行した時の設定
if(TARGET === 'build') {  
  module.exports = merge(common, {
  });
}

process.env.npm_lifecycle_eventはコマンドが実行されているステージがセットされている環境変数です。npm startを実行するとstartnpm run buildならbuildがセットされます。

current lifecycle event

HMR(Hot Module Replacement)

HMRはファイルの変更があったら自動的にビルドして、ブラウザの更新までしてくれる仕組みです。HMRを使うにはdev-serverと連携させます。

var webpack = require('webpack');

...

if(TARGET === 'start' || !TARGET) {  
  module.exports = merge(common, {
    devServer: {
      historyApiFallback: true,
      hot: true,
      inline: true,
      progress: true,
      stats: 'errors-only',
      host: process.env.HOST,
      port: process.env.PORT
    },

    plugins: [
      new webpack.HotModuleReplacementPlugin()
    ]
  });
}

Babel

JSXのtranspiler、ES6構文を使用するためにBabelを利用します。

% npm i -D babel-core babel-loader
const common = {  
  entry: PATHS.app,

  resolve: {
    // import/requireをするときに拡張子を省略できるようにする
    extensions: ['', '.js', '.jsx']
  },

  output: {
    path: PATHS.build,
    filename: 'build.js'
  },

  module: {
    loaders: [
      // Babelローダー
      test: /\.jsx?$/,
      loaders: ['babel'],
      include: PATHS.app
    ]
  },

  plugins: [
    new HtmlwebpackPlugin({
      title: 'Kanban'
    })
  ]
};

Babelの設定のために.babelrcを用意します。Babel 6ではプラグインを利用するようになります。必要なプラグインをそれぞれ用意する代わりにプリセットが用意されているので、それを使います。

% npm i -D babel-preset-es2015 babel-preset-react

.babelrcで利用するプリセットを記述します。

{
  "presets": [
    "es2015",
    "react"
  ]
}

スタイルシート

素のCSSを利用する場合はcss-loaderstyle-loaderを使います。

% npm i -D css-loader style-loader

SassのようなCSSプロセッサを利用するには、適当なloaderも追加しましょう。

% npm i -D sass-loader

SourceMap

開発中はSourceMapを利用したいので、設定を追加します。

...

if(TARGET === 'start' || !TARGET) {  
  module.exports = merge(common, {
    devtool: 'eval-source-map',
...

動かしてみる

Reactをインストールし、動作するか確認してみます。

% npm i -S react react-dom
% mkdir -p app/components
// app/components/App.jsx
import React from 'react';

export default class App extends React.Component {  
  render() {
    return (
      <h1>Hello, world!</h1>
    );
  }
}
// app/index.jsx
import './main.scss';

import React from 'react';  
import ReactDOM from 'react-dom';

import App from './components/App';

let appNode = document.createElement('div');  
document.body.appendChild(appNode);

ReactDOM.render(<App />, appNode);  

localhost:8080へアクセスし、動作していればOKです。コンポーネントやスタイルシートを変更すると、逐次ページに反映されることも確認しておきます。dev-serverのポートはデフォルトで8080ですが、webpack.config.jsdevServer.portの値をいじれば変更することができます。

リロード時の挙動

このままでも良いのですが、例えば、以下の様なコンポーネントが合った場合

import React from 'react';

export default class App extends React.Component {  
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    };

    this._onClick = this._onClick.bind(this);
  }
  render() {
    return (
      <div>
        <h1>{this.state.counter}</h1>
        <button onClick={this._onClick}>Click me!</button>
      </div>
    );
  }

  _onClick() {
    this.setState({ counter: this.state.counter + 1 });
  }
}

ボタンをクリックする度にstatecounterの値を増やして表示していますが、ページがリロードされてしまうのでファイルを編集する度にstateの内容もリセットされてしまいます。

これを回避するためにホットローディングを行えるようにします。

npm i -D babel-plugin-react-transform react-transform-hmr  

babel-plugin-react-transform

// webpack.config.js
process.env.BABEL_ENV = TARGET;  // トップレベルに追加  
// .babelrc
{
  ...
  "env": {
    "start": {
      "plugins": [
        [
          "react-transform", {
            "transforms": [
              {
                "transform": "react-transform-hmr",
                "imports": ["react"],
                "locals": ["module"]
              }
            ]
          }
        ]
      ]
    }
  }
}

npm-startし直すと、コンポーネントを変更してもページのリロードが発生せず、コンポーネントだけ入れ替わるようになります。