初心者
入門
webpack
React
babel

Webpack4 + React スタートアップ(create-react-app を使いたくない)

概要

  • create-react-app が自動的に何をやってくれているのか理解したい人(初学者)向け。

※ 2018/03/25 Webpack4 に対応するため加筆修正しました。
※ 2018/05/08 改題の上、内容を大幅に改定しました。
※ 2018/05/09 誤字脱字、文章表現を修正しました。

前提
$ node -v
v8.11.1

$ npm -v
6.0.0

参考資料

全体図

プレゼンテーション1.png

プロジェクトフォルダの作成

npm init -y でプロジェクトフォルダ直下に package.json が作成されます。

shell
$ mkdir react-startup
$ cd react-startup/
$ npm init -y

Webpack のインストール

shell
$ npm install -D webpack webpack-cli

webpack とは JS、CSS、画像などのファイルをまとめるモジュールバンドラです。

webpack4 のデフォルトでは src/index.js がエントリーファイル、dist/main.js がアウトプットファイルとなっています(そのままで良いのであれば webpack.config.js に記述する必要がなくなりました)。

とりあえず、エントリーファイルJSを作成。

src/index.js
console.log('React Startup!');

package.json のスクリプトに追加。

package.json
"scripts": {
    "build": "webpack",
    "test": "echo \"Error: no test specified\" && exit 1"
},

さっそく実行してみると、

shell
$ npm run build
- 中略 -
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.

mode オプションがセットされてない」と怒られます。
npm スクリプトを修正し、npm run develnpm run build でそれぞれ development モードと production モードを使い分けることにします。

package.json
"scripts": {
    "devel": "webpack --mode development",
    "build": "webpack --mode production",
    "test": "echo \"Error: no test specified\" && exit 1"
},

npm run develnpm run build では dist/main.js として出力されるファイルのサイズが大きく異なることが確認できます。

Babel のインストールと設定

babel は ES6 (es2015~) で書かれた JS ファイルを ES5 の形式へ変換してくれるトランスパイラです。
webpack 単体ではこの変換を行うことができないので、webpack から babel を利用するためのローダーもインストールします。

shell
$ npm install -D babel-core babel-loader babel-preset-env

babel の設定ファイル .babelrc をプロジェクトフォルダ直下に作成します。

.babelrc
{
  "presets": [
    // 推奨されているプリセット(最新の ES6 -> ES5 へ変換)を使う
    "env"
  ]
}

プロジェクトフォルダ直下に webpack.config.js を作成し、webpack から babel を利用するように設定します。

webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        // 拡張子が js または jsx のファイル(正規表現)を
        test: /\.(js|jsx)$/,
        // babel-loader を使って babel に ES5 へ変換させる
        use: {
          loader: 'babel-loader',
        },
        // ただし node_modules 以下のファイルは除外する
        exclude: /node_modules/,
      },
    ]
  },
};

React のインストールと設定

やっと React を利用する下地ができました。
babel に React で書かれたコードを扱わせるためのプリセットもインストールします。

shell
$ npm install -D react react-dom
$ npm install -D babel-preset-react

.babelrc に追記

.babelrc
{
  "presets": [
    "env",
    "react"
  ]
}

エントリーファイル src/index.js を適当な React コードに修正。
index.js からインポートしている App コンポーネントも作成します。

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';

ReactDOM.render(<App />, document.getElementById('app'));

src/App.js
import React from 'react';

class App extends React.Component {
  render() {
    return (
      <div className="container">
        <h1>Hello React!</h1>
      </div>
    );
  }
}

export default App;

コードの呼び出し側である index.htmldist フォルダに作成します。

dist/index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8" />
  <title>React Startup</title>
</head>

<body>
  <div id="app"></div>

  <!-- webpack が dist (このHTMLのあるフォルダ)に吐き出す JS を読み込む -->
  <script src="main.js"></script>
</body>

</html>

(最初の)実行

shell
$ npm run devel
または
$ npm run build

これで dist/main.js が更新されるので、dist フォルダの index.html をブラウザで開けば結果が確認できます。
でも毎回 npm run *** するのは面倒です。
また、このままでは React Devtoolこれとかこれ)も使えません。
ということで、開発用サーバをインストールします。

webpack-dev-server のインストールと設定

インストール

shell
$ npm install -D webpack-dev-server

webpack.config.js へ追加

webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: {
          loader: 'babel-loader',
        },
        exclude: /node_modules/,
      },
    ]
  },
  // 開発用サーバを使う
  devServer: {
    // dist 以下を serve
    contentBase: 'dist',
    // ポート番号は 3000
    port: 3000,
  },
};

npm スクリプトも追加

package.json
"scripts": {
    "start": "webpack-dev-server --mode development --open",
    "devel": "webpack --mode development",
    "build": "webpack --mode production",
    "test": "echo \"Error: no test specified\" && exit 1"
},

再び実行

shell
$ npm start

- 中略 -

[./src/App.js] 2.27 KiB {main} [built]
[./src/index.js] 466 bytes {main} [built]
    + 33 hidden modules
i 「wdm」: Compiled successfully.

start スクリプトだけは途中の run を省略することができます。
npm start でブラウザが開き、ポート番号 3000 で serve します。
webpack-dev-server は原則として dist へは出力しません(development モードで利用することが前提とされています)。
この wds を起動している間は src フォルダ以下のファイルへの変更は再読込みすることなくブラウザに反映されます。

CSS もバンドルする

以下のような CSS ファイルを作成し、React 内でインポートしたとします。

src/style.css
.container {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

src/App.js
import React from 'react';

// CSS ファイルのインポート
import './style.css';

class App extends React.Component {
  render() {
    return (
      <div className="container">
        <h1>Hello React!</h1>
      </div>
    );
  }
}

export default App;

webpack に CSS も dist/main.js にまとめさせるには以下の2つのローダーが必要です。

shell
$ npm install -D style-loader css-loader

webpack.config.js に追加

webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: {
          loader: 'babel-loader',
        },
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [
          // <link> タグに CSS を展開
          'style-loader',
          // CSS を JS にバンドル
          'css-loader'
        ]
      }
    ]
  },
  devServer: {
    contentBase: 'dist',
    port: 3000,
  },
};

use 配列に指定したローダーが最後尾のものから適用されます。
npm start してみると CSS が適用されていることが確認できます。

画像もバンドルする

src フォルダに適当な画像(ここでは 2.60KB の svg ファイル)をひとつ追加し、React へインポート・JSX に追加します。

src/App.js
import React from 'react';
import './style.css';

// svg ファイルのインポート
import logo from './logo.svg';

class App extends React.Component {
  render() {
    return (
      <div className="container">
        {// 画像を追加
        }
        <img src={logo} width="256" height="256" />
        <h1>Hello React!</h1>
      </div>
    );
  }
}

export default App;

画像を JS へバンドルするだけなら url-loader を追加するだけです。

shell
$ npm install -D url-loader

webpack.config.js
{
  test: /\.(gif|png|jpe?g|svg)$/,
  loader: 'url-loader',
}

JS へバンドルすることによってサーバーリクエスト数を減らすことができます(ただし JS ファイル自体の容量は増えます)。

SourceMap をつける・ビルド方法を使い分ける

SourceMap を付けることによってトランスパイル前後のコード位置を照らし合わせることができ、デバッグが容易になります。
ただし webpack が出力する SourceMap 付き JS はファイル容量が大きくなります。
つまり、本番環境(production モード)では不要なものであるため、ビルド方法を使い分ける必要ができてきます。

webpack でのビルド方法を使い分ける方法は色々ありますが、ここではもっとも分かりやすい(かつ手間がかかる)、2つの webpack.config.js を用意するやり方をとります。

※ 参考
【webpack】環境によってビルドする内容を分ける方法 (Qiita)

shell
// development モード用
$ mv webpack.config.js webpack.config.dev.js

// production モード用
$ cp webpack.config.dev.js webpack.config.prod.js

2つのコンフィグファイルを使い分けるように npm スクリプトを変更します。

package.json
"scripts": {
  "start": "webpack-dev-server --config webpack.config.dev.js --open",
  "devel": "webpack --config webpack.config.dev.js",
  "build": "webpack --config webpack.config.prod.js",
  "test": "echo \"Error: no test specified\" && exit 1"
},

development モード用コンフィグファイルの編集

webpack.config.devel.js
module.exports = {
  // development モードを使う
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: {
          loader: 'babel-loader',
        },
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              // css での url() メソッドを無効に
              url: false,
              // 圧縮
              minimize: true,
              // CSS 用ソースマップを付ける
              sourceMap: true,
            },
          }
        ]
      },
      {
        test: /\.(gif|png|jpe?g|svg)$/,
        loader: 'url-loader',
      }
    ]
  },
  // ソースマップを付ける
  devtool: 'inline-source-map',
  // 開発用サーバを使う
  devServer: {
    contentBase: 'dist',
    port: 3000,
  },
};

productionモード用コンフィグファイルの編集

webpack.config.prod.js
module.exports = {
  // production モードを使う
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: {
          loader: 'babel-loader',
        },
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              url: false,
              minimize: true,
              // CSS 用ソースマップは作らない
              sourceMap: false,
            },
          }
        ]
      },
      {
        test: /\.(gif|png|jpe?g|svg)$/,
        loader: 'url-loader',
      }
    ]
  },
};

開発時には npm start(または npm run devel)、本番向けビルドには npm run build を使い分けます。

画像ファイルの容量に応じてビルド方法を変更する

先に見たように画像ファイルを JS にバンドルすればサーバーリクエストは減少しますが、JS ファイル自体の容量は肥大化してロード時間は延びてしまいます。

そこで file-loader を使って、ある一定容量以上の画像ファイルはそのまま(バンドルせずに) dist フォルダ内のしかるべき場所へ配置することもできます。

file-loader のインストール

shell
$ npm install -D file-loader

webpack.config.*.js の編集

webpack.config.devel.js
{
  test: /\.(gif|png|jpe?g|svg)$/,
  use: [
    {
      // url-loader で画像を Base64 化し、JS へバンドル
      loader: 'url-loader',
      // しきい値を超える画像は file-loader にわたす
      options: {
        // 100KB がしきい値
        limit: 100 * 1024,
        // dist/images/ へ 'ファイル名.拡張子' として配置
        name: './images/[name].[ext]',
      },
    }
  ],
}

TODOリスト

  • ESLint の導入・設定
  • PWA 化(serviceWorker + マニフェスト)
  • GitHub Pages へのデプロイ
  • node の環境変数 NODE_ENV を使ったビルド方法の使い分け(Windows では cross-env が必要)
  • SaSS にも対応する
  • prop-types による最低限の型チェック
  • Flow または TypeScript の導入による厳格な型チェック

その他の資料