React + Reduxアプリケーションプロジェクトのテンプレートを作る ― その1: Node.jsとYarnとBabelとwebpack
Sun, Aug 19, 2018
react frontend webpack babelTable of Contents
昔、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パッケージをプロジェクトにインストールする。
- @babel/core: Babel本体。
- @babel/preset-react: ReactのJSXとかFlowとかを処理するプラグイン集。
- @babel/preset-env: ES 2015+をES 5にトランスパイルするプラグイン集。
これらのパッケージは実行時には要らないので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
ディレクトリのなかを見て、App
かApp.js
かApp.jsx
というファイルを探してロードする。
また、ノードモジュールをロードするコードというのはimport React from 'react';
みたいなやつで、webpackはこのコードを読んだら、プロジェクトのnode_modules/react/package.json
のmain
プロパティの値に書いてあるファイルをロードする。
という挙動が上記webpack.config.jsのresolve
に定義してある。
(モジュールロードの詳細は公式のドキュメントのModule Resolutionに書いてある。)
.babelrc
にuseBuiltIns: '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が動くところらへんまで。