昔、Dojo Toolkitを使ってFlashなUIをJavaScriptに書き換えた時以来、仕事でWeb UIを触ることはなかったんだけど、最近になってWeb UIを書かなければいけなくなるような気がして再学習を始めた。

題材はReact (とRedux)。 今一番人気のフロントエンドフレームワークで、昔触ったこともあるので。

前回の記事でReactが生まれた経緯を学んだので、今回から実習に入る。

(2018/11/21更新)

プロジェクト作成

ちょっとCreate React Appを触ってみたけど使わないことにした。 すぐ開発始められるのはよかったんだけど、裏でなにが起こっているかわからな過ぎて肌に合わないし、使うライブラリが結構固定されちゃいそうだったし、トラブルシュート(特にライブラリのバグを踏んだ時)が大変そうだったので。

代わりに、公式で紹介されているブログ記事であるCreating a React App… From Scratch.を見ながら、スクラッチからプロジェクトを作ることにした。

環境はWindows 10 Home。

最終的な成果はGitHubに置いた。

Node.jsインストール

なにはともあれNode.js

Node.jsのバージョン管理には以前はnodist使っていたんだけど、こいつは2年ほど前に開発が止まっているので、代わりにnvm for Windowsを入れた。

nvm installで任意のバージョンのNode.jsをインストール出来て、nvm useで使うNode.jsのバージョンを切り替えられる。

今回使うNode.jsのバージョンは、現時点でLTS版の最新である8.11.4にする。

C:\>nvm install 8.11.4
Downloading node.js version 8.11.4 (64-bit)...
Complete
Creating C:\Users\kaitoy\AppData\Roaming\nvm\temp

Downloading npm version 5.6.0... Complete
Installing npm v5.6.0...

Installation complete. If you want to use this version, type

nvm use 8.11.4

C:\>nvm use 8.11.4
Now using node v8.11.4 (64-bit)

Yarnインストール

パッケージマネージャにはYarnを使う。

Yarnちょっとバギーだとか、npm 5がlockファイルをサポートしてYarnの優位性が減ったとか、Yarnからnpmに戻るためのツールが出てきたりしてるけど、現時点では深く考えずにYarnでいいと思う。

YarnはWindows環境ではMSIファイルをダウンロードして実行すればインストールできる。

(npmでもインストールできるけど邪道。)

Yarnはv1.7.0を使う。

package.json生成

プロジェクトの構成情報を記述するファイルであるpackage.jsonをYarnで生成する。

C:\>mkdir react-redux-scaffold

C:\>cd react-redux-scaffold

C:\react-redux-scaffold>yarn init
yarn init v1.7.0
question name (react-redux-scaffold):
question version (1.0.0):
question description: React Redux Scaffold
question entry point (index.js): src/index.jsx
question repository url: https://github.com/kaitoy/react-redux-scaffold.git
question author: kaitoy
question license (MIT):
question private:
success Saved package.json
Done in 40.38s.

できたのがこれ。

package.json:

{
  "name": "react-redux-scaffold",
  "version": "1.0.0",
  "description": "React Redux Scaffold",
  "main": "src/index.jsx",
  "repository": "https://github.com/kaitoy/react-redux-scaffold.git",
  "author": "kaitoy",
  "license": "MIT"
}


以降、カレントディレクトリはC:\react-redux-scaffoldとして、プロンプト表示は省略する。

ビルド環境セットアップ

ビルド環境としてトランスパイラとかモジュールバンドラとかをセットアップする。

Babel

トランスパイラはデファクトスタンダードのBabelを使う。 2018年8月に出たv7。

Babelのプラグインはとりあえず最低限入れるとして、以下のnpmパッケージをプロジェクトにインストールする。

これらのパッケージは実行時には要らないのでyarn add -Dコマンドで開発時依存としてインストールする。

yarn add -D @babel/core @babel/preset-react @babel/preset-env

Babelはv7.1.6が入った。


で、Babelの設定ファイルを書いてプロジェクトルートに置いておく。

.babelrc:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

Polyfill

BabelはES 2015+で追加された構文の変換はしてくれるけど、追加されたグローバルオブジェクト(e.g. Promise)とかメソッド(e.g. Object.assignとかArray.prototype.includes)とかを補完してくれるわけではない。 そこを補完してくれるのがPolyfill

少なくとも後で導入するredux-sagaが使うジェネレータがPolyfillを必要とする(ないとReferenceError: regeneratorRuntime is not definedというエラーが出る)ので、今の時点で入れておくことにする。

Polyfillの実装はいくつかあるけど、定番っぽい@babel/polyfillを使う。 こちらは実行時依存としてインストールする。

yarn add @babel/polyfill


@babel/polyfillのアプリへのロード方法はいくつかあるけど、今回使うwebpack(後述)の場合、useBuiltIns: 'usage'というオプションを使うのがよさそう。 これを使うと、ソースに@babel/polyfillのimportを書かなくても、必要に応じて必要なPolifillをロードしてくれる。

.babelrc:

 {
-  "presets": ["@babel/preset-env", "@babel/preset-react"]
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+        "useBuiltIns": "usage"
+      }
+    ],
+    "@babel/preset-react"
+  ]
 }

webpack

モジュールバンドラは現時点で一番人気のwebpackを使う。 (Parcelの方がナウいはナウいけど。)

webpackは、タスクランナーの機能も備えたモジュールバンドラみたいな感じで、バンドルしたいファイルの形式とか実行したいタスクに応じたローダーを設定することでプロジェクトのビルドを定義できる。

ちょっと古いけどこの記事を読むとwebpackの理解が深まる。

こちらもとりあえず最低限のローダーをセットアップするとして、以下のnpmパッケージをプロジェクトにインストールする。

  • webpack: webpack本体。
  • webpack-cli: webpackのコマンドラインインターフェース。
  • webpack-dev-server: webpackから起動できる開発用 HTTP サーバ。ライブリロードしてくれる。(webpack-serveの方がモダンではある。)
  • babel-loader: Babelを実行してくれるやつ。Babel 7で使うにはv8以降である必要がある。


yarn add -D webpack webpack-cli webpack-dev-server babel-loader

webpackはv4.26.0が入った。

webpack設定ファイル

webpackの設定は設定ファイルを書いてプロジェクトルートに置けばいい。 設定は結構複雑だけど、v1の時よりかは若干書きやすくなったし、公式のマニュアルとかローダーのマニュアル見てれば書くのは難しくない。 設定ファイルを生成してくれるサイトもある。

とりあえず適当に書くとこんな感じ。

webpack.config.js:

const path = require('path');
const packageJson = require('./package.json');

module.exports = {
  mode: 'development',
  entry: [`./${packageJson.main}`],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: [path.resolve(__dirname, 'src')],
        loader: 'babel-loader',
      },
    ],
  },
  resolve: {
    extensions: ['*', '.js', '.jsx'],
    modules: ['node_modules'],
  },
};

この設定の意味は、entryに指定された./src/index.jsxを読んで、.js.jsxを拡張子としたモジュールファイルやノードモジュールをロードするコードがあったら、babel-loaderでBabelを呼んでトランスパイルして、バンドルした結果は<プロジェクトルート>/dist/bundle.jsに吐き出す。 というだけ。 (__dirnameはNode.jsが値を入れてくれる変数で、webpack.config.jsのあるディレクトリの絶対パスが入ってる。)

モジュールファイルをロードするコードというのは、import App from './components/App';みたいなやつ。 webpackはこのコードを読んだら、./componentsディレクトリのなかを見て、AppApp.jsApp.jsxというファイルを探してロードする。 また、ノードモジュールをロードするコードというのはimport React from 'react';みたいなやつで、webpackはこのコードを読んだら、プロジェクトのnode_modules/react/package.jsonmainプロパティの値に書いてあるファイルをロードする。 という挙動が上記webpack.config.jsのresolveに定義してある。 (モジュールロードの詳細は公式のドキュメントのModule Resolutionに書いてある。)

.babelrcuseBuiltIns: 'usage'を付けたので、webpack.config.jsに@babel/preset-envを書く必要はない。

modeについては後述。

webpack-dev-server設定

webpack-dev-serverの設定もwebpack.config.jsに書く。

以下をresolveの次辺りに書き足せばいい。

  devServer: {
    contentBase: path.join(__dirname, 'public'),
    compress: true,
    hot: true,
    port: 3000,
    publicPath: 'http://localhost:3000/',
  },


この設定でwebpack-dev-serverを実行すると、http://localhost:3000/へのアクセスにpublic/index.htmlを返すWebサーバを起動できる。 Webサーバが起動するときにプロジェクトがインメモリでビルドされ、メモリからbundle.jsがサーブされる。

hotをtrueにしておくとHot Module Replacementが有効になる。 これによって、webpack-dev-serverの起動中にソースを編集すると、自動で再ビルドし、動的にモジュール単位でロードし、ブラウザをリロードしてくれるようになる。 Hot Module Replacementを有効にするときはpublicPathをフルURLで書かないといけない。

webpack-dev-serverの他の設定については公式のマニュアルのDevServerを見るべし。

webpackのmode

webpackにはビルドのmodeという概念があり、modeを切り替えることで適切な最適化を適用してくれる。

modeにはdevelopmentとproduction(とnone)があり、productionにしておくと、UglifyJsPluginとかを適用して、出力するバンドルファイルのサイズを小さくしてくれたりする。 (v1のころはUglifyJsPluginとかは全部自分でwebpack.config.jsに指定していた記憶があるので、楽になった。)

webpack.config.jsの分割

modeを切り替えるのにwebpack.config.jsを書き換えるのはイケてないので、developmentとproductionでファイルを分割して使い分けるようにする。

developmentとproductionはほとんどが共通の設定なので、共通部分をwebpack.common.jsに書いて、developmentとproductionに固有な設定だけをそれぞれwebpack.dev.jsとwebpack.prod.jsに書く。 webpack.common.jsは、webpack-mergeでwebpack.dev.jsとwebpack.prod.jsにマージする。 というのが公式で紹介されているプラクティス。

まずwebpack-mergeをプロジェクトにインストール。

yarn add -D webpack-merge

分割したファイルは以下の感じ。全部プロジェクトルートに置いておく。

webpack.common.js

const path = require('path');
const packageJson = require('./package.json');

module.exports = {
  entry: [`./${packageJson.main}`],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: [path.resolve(__dirname, 'src')],
        loader: 'babel-loader',
      },
    ],
  },
  resolve: {
    extensions: ['*', '.js', '.jsx'],
    modules: ['node_modules'],
  },
};

webpack.dev.js

const path = require('path');
const webpackMerge = require('webpack-merge');
const webpackCommon = require('./webpack.common.js');

module.exports = webpackMerge(webpackCommon, {
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'public'),
    compress: true,
    hot: true,
    port: 3000,
    publicPath: 'http://localhost:3000/',
  },
});

webpack.prod.js

const webpackMerge = require('webpack-merge');
const webpackCommon = require('./webpack.common.js');

module.exports = webpackMerge(webpackCommon, {
  mode: 'production',
});

npmスクリプト

webpackによるビルドは次のコマンドで実行できる。

node_modules\.bin\webpack --config webpack.prod.js

また、webpack-dev-serverは次のコマンドで起動できる。

node_modules\.bin\webpack-dev-server --hot --config webpack.dev.js

--hotはHot Module Replacementに必要なオプション。

コマンドが長くて面倒なのは、npmスクリプトで楽にできる。 package.jsonのmainの次辺りに以下を書き足せばいい。 (npmスクリプトは実行時にnode_modules\.binにPATHを通してくれるので、それを省略できる。)

  "scripts": {
    "build": "webpack --config webpack.prod.js",
    "start": "webpack-dev-server --hot --config webpack.dev.js"
  },

こうしておくと、yarn buildでビルド、yarn startでwebpack-dev-server起動できる。


以上でビルド環境セットアップはいったん完了とする。 次回はReactが動くところらへんまで。