Reactを開発するときに、babelやwebpackの設定をしますが、
すでに古い情報が多かったり、結局何のためにどの指定が必要なのかがわかりづらいため、改めて何を何のためにやっているのか整理します。
本記事は基本的にはReactの利用有無にかかわらず参考にできます。
また、セットアップ済みのプロジェクトはgithubに上げてるので試してみたい場合はそちらを。
https://github.com/haradakunihiko/devserver-boilerplate
前提
ES2015ベースで、webアプリ開発をしたいので開発環境を準備します。
ここでは以下のバージョンを使用します。
- node@4
- npm@2
- babel@6
npm@2を利用している場合は特定のライブラリをinstallすると、それが依存しているライブラリは併せてインストールされますが、npm@3の場合は明示的に入れる必要があります。おそらくインストール時にワーニングなど出ると思うので適宜入れて下さい。
js コンパイル環境の準備
babelの導入
ES6、JSXで実装するためbabelを導入します。babel@5では、babelモジュール自体全ての機能が搭載されており、ES7などを利用する場合はオプションで指定するという形式でした。babel@6では細かくパッケージが分離され、必要なpluginを別々にインストールする形式になっています。
# ES6
npm install babel-preset-es2015@6 --save-dev
# ES7
npm install babel-preset-stage-0@6 --save-dev
# JSX (Reactを利用する場合)
npm install babel-preset-react@6 --save-dev
これらを利用するため、.babelrcを作成します。
{
"presets": ["es2015", "react", "stage-0"]
}
ES6、JSXで実装したソースをコマンドラインでコンパイルする
コマンドラインでbabelを利用するために、babel-cliをインストールします。
npm install babel-cli@6 --save-dev
簡単なES6、JSXを利用したファイルを作り、コンパイルします。
export const hello = () => 'hello';
import { hello } from './es6sample';
document.write(hello());
const MyDiv = ({ children }) => <div className='my-div'>{children}</div>;
export default {
MyDiv
};
babelを実行します。いくつかの出力方式がありますが、フォルダを対象にコンパイルします。
# 標準出力
./node_modules/.bin/babel src/es6sample.js
# ファイルへ出力
./node_modules/.bin/babel src/es6sample.js -o es6sample-compiled.js
# watchする
./node_modules/.bin/babel src/es6sample.js -w -o es6sample-compiled.js
# ソースマップを一緒に出力する
./node_modules/.bin/babel src/es6sample.js -o es6sample-compiled.js -s
# フォルダを対象にする <- これ
./node_modules/.bin/babel src -d lib
reactをまだ入れていないためjsxsample.jsはまだ使えませんが、ES6、JSXがES5形式にコンパイルされます。
依存性を解決する
コンパイルされたソースコードでは、require
を利用して、moduleの依存性を定義しています。
require
の解決にはbrowserifyというものもありますが、browserify自体非常にシンプルなので、
- 複数のファイルを生成したい
- jsxを利用したい
- ES6で書きたい
など要件が増えると、少し面倒でした。lessのコンパイルなどもするためgulpで定義してもよいのですが、より汎用的にソースコードのコンパイル全般を担ってくれるwebpackを利用します。
# webpackをインストール
npm install webpack@1 --save-dev
# babelでコンパイル後のファイルを指定して実行
./node_modules/.bin/webpack lib/index.js dist/index_bundle.js
webpackで、JSX・ES6のコンパイルもする。
webpackは依存性を解決してくれるだけではなく、loaderという仕組みでファイルの変換も行うことができます。babelコマンドで行ったJSX、ES6の変換を、webpackにbabel-loaderを導入して行います。
# babel-loderのインストール
npm install babel-core@6 --save-dev
npm install babel-loader@6 --save-dev
.js、.jsxファイルをbabel-loaderで読み込むための設定をwebpack.config.jsに記載します。
'use strict';
module.exports = {
module: {
loaders: [
{
// .jsxと.jsを対象にする
test: /\.jsx?$/,
// node_modulesを除く
exclude: /node_modules/,
loaders: ['babel-loader'],
}
]
}
};
元のソースを指定して実行します。
# エントリーファイルを指定してコンパイル
./node_modules/.bin/webpack src/index.js dist/index_bundle.js
# watchする
./node_modules/.bin/webpack src/index.js dist/index_bundle.js -w
これで、ES6、JSX、を利用し、かつrequireでmoduleを利用して開発できる最低限の環境が整いました。
Webpack Dev Server
Webpack Dev Serverを導入し、ソースの更新が動的にクライアントのリソースが更新される環境を構築します。
サーバーから公開されたソースコードが読み込まれている事を明確にするため、ファイルのパスはdist/...でなくpublic/...にしています。
<!DOCTYPE html>
<html>
<head>
<title>react-boilerplate</title>
</head>
<body>
<script src="public/index_bundle.js"></script>
</body>
</html>
また、index.jsをHMR(画面の再描画なしに修正後のjavascriptを適用させる)に対応させるため、実装を変更します。(この詳細は別のエントリで説明しています)
このような実装にしない場合はHMRはできませんが、自動で再描画されるため殆どの場合は十分かもしれません。また、reactアプリを利用する場合は後述のbabelプラグインを入れることで同様の効果を得ることができます。
import { hello } from './es6sample';
var $div = document.createElement('div');
$div.innerHTML = hello();
document.body.appendChild($div);
if (module.hot) {
module.hot.accept(function(err) {
if (err) {
console.error(err);
}
});
module.hot.dispose(function() {
$div.parentNode.removeChild($div);
});
}
npm install --save-dev webpack-dev-server@1
Webpack Dev Serverを起動するには3つの方法があります。
- Webpack Dev Server CLI (コマンドラインよる実行)
- Webpack Dev Server API (node.jsスクリプトによる実行)
- Webpack middleware (サーバーを別に立てる)
Webpack Dev Server CLI
コマンドラインでWebpack Dev Serverを起動することができます。できることは多くありませんが、特別な実装はほとんどすることなく利用することができます。
iframe mode
./node_modules/.bin/webpack-dev-server src/index.js --output-filename index_bundle.js --output-public-path public
指定するのは、元になるソースファイルとコンパイル後のファイル名、コンパイルされたファイルを公開するパスです。コンパイルされたファイルは直接書き込まれずにサーバーから公開されます。
http://localhost:8080/webpack-dev-server/index.html へのアクセスしてsrc/index.jsなどを修正すると、自動的にリロードされます。
inline mode
./node_modules/.bin/webpack-dev-server src/index.js --output-filename index_bundle.js --output-public-path public --inline
URLはhttp://localhost:8080/index.html です。
HOT MODULE REPLACEMENT(HMR)
HMRを利用すれば、ファイルの変更の度にリロードするのではなく、変更されたmoduleのみ更新することができます。CLIで行う場合は、引数に-hotをつけるだけです。
./node_modules/.bin/webpack-dev-server src/index.js --output-filename index_bundle.js --output-public-path public --inline --hot
ブラウザのconsoleに、以下のログが出力されます。
[HMR] Waiting for update signal from WDS...
[WDS] Hot Module Replacement enabled.
webpack.config.js
最後に、CLIに引数で渡してきた値をwebpack.config.jsにまとめます。
'use strict';
var path = require('path');
module.exports = {
entry: {
app: [
'./src/index.js'
],
},
output: {
path: path.join(__dirname, 'dist'),
filename: 'index_bundle.js',
publicPath: '/public/'
},
module: {
loaders: [
{
// .jsxと.jsを対象にする
test: /\.jsx?$/,
// node_modulesを除く
exclude: /node_modules/,
loaders: ['babel-loader'],
}
]
}
};
// コンパイルする
./node_modules/.bin/webpack
// webpack-dev-serverを起動する
./node_modules/.bin/webpack-dev-server
Webpack Dev Server(node API版)
node.jsのスクリプトとしてwebpack-dev-serverを起動します。CLIがやってくれている内容を実装する必要がありますが、より細かい制御をすることができます。
簡単にwebpack-dev-serverの仕事を整理すると、
- webpackのコンパイル(と監視)の実行
- コンパイルされたファイルの公開
- HMRの際のwebsocket通信
まずは、--inline
、--hot
を指定せずにCLIを実行した時と同等の設定をします。
iframeモードを利用できます。
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var config = require("./webpack.config.js");
var compiler = webpack(config);
var server = new WebpackDevServer(compiler, {
publicPath: config.output.publicPath,
});
server.listen(8080);
node devserver
次に、inlineモードで動作させます。(CLIで--inline
指定と同等)
サーバーとソケット通信を確立し、サーバーから変更の通知が来るとブラウザをリロードするスクリプト(webpack-dev-server/client.index.js)をwebpackで埋め込みます。
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var config = require("./webpack.config.js");
// webpackが生成するjsの全てのentry pointに、webpack-dev-server/client/index.jsを含める。
// http://localhost:8080とsocket通信することを指定(CLIの時は何も指定しなかったが、デフォルトのlocalhost:8080が利用された。)
// webpack.config.jsに直接書いてもよいが、全てのentry pointに必要なため、動的に追加している
Object.keys(config.entry).forEach(function (key) {
config.entry[key].unshift(
'webpack-dev-server/client?http://localhost:8080' // 変更を検知してリロードする
);
});
var compiler = webpack(config);
var server = new WebpackDevServer(compiler, {
publicPath: '/public',
});
server.listen(8080);
node devserver
最後に、HMRを利用します。(CLIで--hotを指定したのと同等)
画面をリロードするのではなく、変更のあったモジュールのみ取得して更新します。
変更されたモジュールからの呼び出し元をたどり、HMRに対応しているモジュールが無ければリロードします。
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var config = require("./webpack.config.js");
Object.keys(config.entry).forEach(function (key) {
config.entry[key].unshift(
'webpack-dev-server/client?http://localhost:8080', // 変更を検知した後、webpack/hot/dev-serverに処理を委譲する
'webpack/hot/dev-server' // HotModuleReplacementPluginにモジュールの更新を行わせる。
);
});
config.plugins = [
// 1. 変更されたモジュールのみ含まれるファイルを生成する(Webpackのコンパイル時の挙動)
// 2. 変更されたmoduleがHMR可能かどうかを調べ、可能であれば置き換えるためのコードをjsソースに含める
new webpack.HotModuleReplacementPlugin(),
];
var compiler = webpack(config);
var server = new WebpackDevServer(compiler, {
publicPath: '/public',
hot: true, // hotモードを有効にする。
});
server.listen(8080);
node devserver
Proxy
Webpack Dev Server(API) ではproxyを設定することができます。
基本的にWebpack Dev Serverはjsなどの静的ファイルの公開が責務なので、アプリケーションのバックエンドサーバーなどと、同一のエンドポイントを利用したい場合はproxyを設定する事で実現できます。
まずはバックエンドサーバーを用意します。
npm install --save-dev express@4
var express = require('express');
var app = new express();
var port = 3000
app.get('/', function (req, res) {
res.send('Hello, World!<script src="public/index_bundle.js"></script>');
});
app.listen(port, function(error) {
if (error) {
console.error(error)
} else {
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
}
});
次に、devServerからproxyさせる設定をします。
var WebpackDevServer = require("webpack-dev-server");
var webpack = require("webpack");
var config = require("./webpack.config.js");
Object.keys(config.entry).forEach(function (key) {
config.entry[key].unshift(
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/dev-server'
);
});
config.plugins = [
new webpack.HotModuleReplacementPlugin(),
];
var compiler = webpack(config);
var server = new WebpackDevServer(compiler, {
publicPath: '/public',
hot: true,
proxy: { '*': 'http://localhost:3000' } // proxyの設定
});
server.listen(8080);
proxyへの*を指定することで、devserverが受け付けるURL(具体的には、localhost:8080/webpack-dev-server、localhost:8080/{publicPath})以外を全て指定したurlにproxyさせることができます。
node devserver.js
node appserver.js
http://localhost:3000 で確認できます。
Webpack middleware
expressサーバーを自分で用意し、HMRのためのサーバーの設定をミドルウェアを利用して行います。開発用のサーバーがある場合に適用させることができます。また、アプリケーションでWebSocketを利用する場合もこちらを利用したほうが良いでしょう。
webpack-dev-middlwareとwebpack-hot-middlewareの2つのミドルウェアを使います。
webpack-dev-middlewareは上述のwebpack-dev-server内でも利用されていますが、webpack-hot-middlwareは使われていません。
webpack-hot-middlewareの役割は、ファイルの変更を通知をクライアントに通知することですが、webpack-dev-serverとは結構違う実装になっているようです。
通知の方式も、webpack-dev-serverはSockJSを利用するのに対し、こちらはEventSource。
そのため、jsに埋めるソースも、パラメータも全く違うので要注意です。
npm install --save-dev webpack-dev-middleware@1
npm install --save-dev webpack-hot-middleware@2
npm install --save-dev express@4
var webpackDevMiddleware = require("webpack-dev-middleware");
var webpackHotMiddleware = require("webpack-hot-middleware");
var webpack = require("webpack");
var config = require("./webpack.config.js");
var express = require('express');
var port = 8080;
var app = express();
Object.keys(config.entry).forEach(function (key) {
config.entry[key].unshift(
'webpack-hot-middleware/client' // webpack-dev-serverで指定したスクリプトとは全く別物
);
});
config.plugins = [
new webpack.HotModuleReplacementPlugin(),
];
var compiler = webpack(config);
// webpackの実行(監視)と、生成されたファイルを公開するためのルーティング
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
// 変更があった場合にclientへ変更を通知する
app.use(webpackHotMiddleware(compiler));
// index.htmlが使われているわけでないことを念のため明確にするため
app.get('/', function (req, res) {
res.send('I am using middleware!<script src="public/index_bundle.js"></script>');
});
app.listen(port, function(error) {
if (error) {
console.error(error)
} else {
console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
}
});
で、EventSourceってなんだっけと思ってググると、
「きれいなComent」または「妥協したWebSocket」
との事。。(http://0-9.sakura.ne.jp/pub/HTML5han/start.html)
HMRに対応させる (React)
前述のとおりHMRに対応するにはアプリケーションにいくつかの実装が必要です。
Reactでは、Componentがstateなどを保持したままそれを可能にするプラグインがあるため、その設定を最後に説明します。
ReactにかかわらずHMRへ対応する実装の詳細は別のエントリに書いたので見てください。
babelのプラグインと、トランスフォーマーから構成されています。
トランスフォーマーはいくつかあり、自分で実装することもできます。
npm install --save-dev babel-plugin-react-transform@2
# hmrに対応させる
npm install --save-dev react-transform-hmr@1
# エラーが発生した際にブラウザに表示する
npm install --save-dev react-transform-catch-errors@1
# react-transform-catch-errorsで利用するモジュール
npm install --save-dev redbox-react@1
.babelrcはこんな感じ。それぞれのtransforms
に指定しているimports
、locals
はそれぞれのトランスフォーマーのドキュメントを参照すること。
{
"presets": ["es2015", "react", "stage-0"],
"env": {
// 環境変数のNODE_ENVかBABEL_ENVが設定されていないか、"development"の時のみ
"development": {
"plugins": [
["react-transform", {
"transforms": [{
"transform": "react-transform-hmr", // https://github.com/gaearon/react-transform-hmr
"imports": ["react"],
"locals": ["module"]
}, {
"transform": "react-transform-catch-errors", // https://github.com/gaearon/react-transform-catch-errors
"imports": ["react", "redbox-react"]
}]
}]
]
}
}
}
これで起動すると、コンポーネントの修正をしても、リロードやstateが消える事無く無くコンポーネントが最新のものに置き換える事ができます。
参考
https://github.com/gaearon/babel-plugin-react-transform
https://webpack.github.io/docs/configuration.html
http://jamesknelson.com/using-es6-in-the-browser-with-babel-6-and-webpack/
https://babeljs.io/docs/usage/cli/
https://webpack.github.io/docs/webpack-dev-server.html