LoginSignup
76
83

More than 5 years have passed since last update.

ES6記法とモジュール管理をするためにbabelやwebpackの設定を0から行うためのハンズオン

Last updated at Posted at 2016-02-19

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を作成します。

.babelrc
{
  "presets": ["es2015", "react", "stage-0"]
}

ES6、JSXで実装したソースをコマンドラインでコンパイルする

コマンドラインでbabelを利用するために、babel-cliをインストールします。

npm install babel-cli@6 --save-dev

簡単なES6、JSXを利用したファイルを作り、コンパイルします。

src/es6sample.js
export const hello = () => 'hello';
src/index.js
import { hello } from './es6sample';

document.write(hello());
src/jsxsample.js
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に記載します。

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/...にしています。

index.html
<!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プラグインを入れることで同様の効果を得ることができます。

src/index.js
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つの方法があります。

  1. Webpack Dev Server CLI (コマンドラインよる実行)
  2. Webpack Dev Server API (node.jsスクリプトによる実行)
  3. 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にまとめます。

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の仕事を整理すると、
1. webpackのコンパイル(と監視)の実行
2. コンパイルされたファイルの公開
3. HMRの際のwebsocket通信

まずは、--inline--hotを指定せずにCLIを実行した時と同等の設定をします。
iframeモードを利用できます。

devserver.js
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で埋め込みます。

devserver.js
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に対応しているモジュールが無ければリロードします。

devserver.js
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
appserver.js
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させる設定をします。

devserver.js
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
devserver.js

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に指定しているimportslocalsはそれぞれのトランスフォーマーのドキュメントを参照すること。

.babelrc
{
  "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

76
83
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76
83