あしたからがんばる

プログラミングの話と時々サッカーの話

Babelとwebpackを使ってReactとReduxをビルドする方法を一歩一歩調べた話

ふとjsを書こうと思い立ち、少し調べたところ、何やらReduxが流行っているようでした。 にわかな自分としては、とりあえずReduxに乗っかろうと考えたのですが、Babelとかwebpackとかgulpとか色々出てきて、最終的な設定ファイルの構成だけ見ても何が何やらだったので、少しずつ調べた結果のメモを晒します。 あまり深い話はできないので(よくわかってn..)、操作の結果がどうなるか、という内容になってます。

調べ始める前はぼんやりとこんなことを考えてました。つまり、対象読者はこのぐらいの人です。

  • webpackはrequireを解決してファイルを結合してくれるらしい
  • ReactはBabelで変換するのが便利らしい
  • Babel入れるならes2015のシンタックスも使いたい
  • gulp使わなくてもnpm scriptsだけで色々できるという噂を聞いた

目標

Reduxのexamples/counterをwebpackでbundleして実行することを目指します。

大まかな流れとして、以下の順で見ていきます。

  1. Babelを使ってReactを動かす
  2. webpackを使ってmodule分割してみる
  3. webpackとBabelを使ってReactを動かす
  4. webpackとBabelを使ってReduxを動かす

TL;DR

  • Babelとwebpackを組み合わせたjsのビルドを色々試した
  • 目標はBabelとwebpackを使ってReduxのサンプルをビルドすること
  • 色々試したコードはリポジトリにあげたよ

1. Babel

とっつきやすそうなBabelから手をつけます。

babel-es2015

まず最初のステップとして、Babelを使ってes2015のコードを変換してみます。

元となるソースはこちら。arrow functionがes2015の部分です。

// src.js
[1, 2, 3].map(n => n + 1);

Babelを使うためにpackage.jsonに依存ライブラリを追加します。 es2015の変換なので、babel-preset-es2015を使います。

// package.json
{
  "scripts": {
    "build": "babel src.js -o bundle.js"
  },
  "devDependencies": {
    "babel-cli": "^6.7.5",
    "babel-preset-es2015": "^6.6.0"
  }
}

また、Babel用の設定ファイル(.babelrc)が必要なので用意します。 この設定ファイルでes2015を変換することを指定します。

// .babelrc
{
  "presets": ["es2015"]
}

npm installした後に、npm run buildを実行すると変換結果のbundle.jsが生成されます。 npm run buildではソースと出力先を指定して、babel-cliを実行しています。 (以降全てのパターンにおいて、npm run buildでbundle.jsを生成するのは統一してます)

変換結果はこんな感じで、arrow functionが変換されていることがわかります。

// bundle.js
[1, 2, 3].map(function (n) {
  return n + 1;
});

babel-jsx

次に、Reactでよくみるjsxを変換してみます。 jsxについては、js内にhtmlのタグが書けるやつぐらいの認識しかないです。 設定ファイルのes2015との違いは、babel-preset-2015がbabel-preset-reactになっただけです。

// src.jsx
<div>Hello, world!</div>
// package.json
{
  "scripts": {
    "build": "babel src.jsx -o bundle.js"
  },
  "devDependencies": {
    "babel-cli": "^6.7.5",
    "babel-preset-react": "^6.5.0"
  }
}
// .babelrc
{
  "presets": ["react"]
}
// bundle.js
React.createElement(
  "div",
  null,
  "Hello, world!"
);

src.jsx内のdivタグは、bundle.jsではReact.createElementに変換されているのが分かります。 へー、こんなことやってんのか、という印象です。

babel-react

一歩一歩ということで、src.jsxをもう少しReactっぽいコードにしてみます。 package.jsonと.babelrcはbabel-jsxと同じです。

// src.jsx
var Hello = React.createClass({
    render: function () {
        return <div>Hello world</div>;
    }
});
// bundle.js
var Hello = React.createClass({
    displayName: "Hello",

    render: function () {
        return React.createElement(
            "div",
            null,
            "Hello world"
        );
    }
});

babel-jsxの場合と同様に、jsxはcreateElementに変換されてます。 displayNameはデバッグ用らしいです。

babel-react-es2015

ここまでくると、Helloをes2015のclassとして書きたくなります。 babel-preset-reactとbabel-preset-es2015を両方とも使えば実現できます。

//src.jsx
class Hello extends React.Component {
    render() {
        return <div>Hello world</div>
    }
}
ReactDOM.render(<Hello/>, document.body);
// package.json
{
  "scripts": {
    "build": "babel src.jsx -o bundle.js"
  },
  "devDependencies": {
    "babel-cli": "^6.7.5",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0"
  }
}
// .babelrc
{
  "presets": ["es2015","react"]
}

classに関連する部分をBabelが頑張ってbundle.jsが長くなるので、 bundle.jsは目では見ないことにして(見たい場合はリポジトリを見てください)、 代わりに、以下のようなhtmlを使ってbundle.jsを読み込んで動作確認します。

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
<body>
<script src="bundle.js"></script>
</body>

htmlを開くと、Hellow worldと出力されるので、もろもろちゃんと動いてそうです。

Babelまとめ

  • .babelrcで変換の対象を指定できる
  • 変換には対応するpresetが必要
  • jsxはReact.createElementに変換される

2. Webpack

Webpackに関しては、分割されたモジュールを一つのファイルに結合するツール、ぐらいの認識です。 js意外にも使えるらしいけど、とりあえずはjs以外のことは考えないことにします。

また、現時点ではes2015のモジュール(importとか)よりCommonJSのモジュール(requireとか)の方が 安定していると偉い人が言っていたので、無心でCommonJSスタイルを採用します。

webpack

まず最初に単純なモジュール分割を試します。 lib.jsというモジュールを作成し、src.jsからlib.jsを読み込み、webpackでbundle.jsを作成します。 また、そろそろjsファイルが多くなってくるので、jsはjsディレクトリ以下に配置することにします。

// js/lib.js
module.exports = function () {
    return 'Hello world';
};
// js/src.js
var lib = require('./lib');
document.write(lib());
// package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^1.13.0"
  }
}

設定ファイルとしてwebpack.conf.jsが必要なので用意します。

// webpack.conf.js
module.exports = {
    entry: './js/src.js',
    output: {
        path: __dirname + "/js",
        filename: 'bundle.js'
    }
};

entryが起点となる入力ファイルで、出力先はoutputに設定します。 entryは相対パスなのに、outputは__dirnameが付いてるのが気になりますが、 ドキュメントを見る限り output.pathは絶対パス必須らしいので、無心で__dirnameをつけましょう。

Babelの時と同様にnpm run buildを実行すると、bundle.jsが作成されます。 作成したbundle.jsは以下のようなhtmlを作成することで動作確認できます。

<script src="js/bundle.js"></script>

webpack-babel

つぎに、webpackとBabelの一番シンプルなサンプルのes2015の変換と組み合わせてみます。

webpackにBabelの変換を組み込むにはloaderというwebpackの仕組みを使います。 babel-loaderという良い奴が用意されているのでそれを使いましょう。

まずは、babelの依存パッケージや.babelrcをbabel-es2015のサンプルからコピペしてきます。 (.babelrcではなくwebpack.conf.jsに設定を移すこともできますが、それはそれでということで、.babelrcをそのまま使います。)

// package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "babel-core": "^6.7.7",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "webpack": "^1.13.0"
  }
}
//.babelrc
{
  "presets": ["es2015"]
}

次に、webpack.conf.jsにloaderの設定を追加します。

// webpack.conf.js
module.exports = {
    entry: './js/src.js',
    output: {
        path: __dirname + "/js",
        filename: 'bundle.js'
    },
    module: {
        loaders: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader"
            }
        ]
    }
};

最後に、src.jsとlib.jsをes2015を使って書きます。

// js/lib.js
module.exports = () => {
    return 'Hello world';
};
// js/src.js
const lib = require('./lib');
document.write(lib());

先ほどと同様にbundle.jsを作成してindex.htmlから読み込むと動作確認できます。

webpack-react

es2015の変換に加えてReactのjsxの変換を追加してみます。 .babelrcやpackage.jsonはbabel-reactの時と同様にbabel-preset-reactを追加するだけなので省略します。

まずはsrc.jsとlib.jsを見てみます。

// js/lib.js
const React = require('react');

class Hello extends React.Component {
    render() {
        return <div>Hello world!!!!!</div>
    }
}

module.exports = Hello;
// js/src.js
const React = require('react'); // Must require react because <Hello/> is translated to React.createElement(Hello).
const ReactDOM = require('react-dom');
const Hello = require('./lib');

ReactDOM.render(<Hello />, document.body);

src.jsでは一見Reactのrequireが必要ないように見えますが、 Babelで変換した後はReact.createElementを呼んでいるので、require('react')が必要です。 (requireしないとreact is not definedと怒られて涙目になりました。良い回避法既にありそうな気もする。)

index.htmlは若干修正してbodyをちゃんと書きます。 ReactDom.renderにbodyを渡すと、コンソールにお叱りのログがでるのですが、今回は簡単のためそのままいきます。 Hello world!!!!! が出力されてればOKです。

// index.html
<body>
<script src="js/bundle.js"></script>
</body>

webpackまとめ

  • requireでモジュール読み込み
  • module.exportsでモジュール公開
  • webpack.conf.jsに設定を書く
  • babel-loaderでBabelの処理を挟める

3. Redux

なんやかんややっていて忘れてましたが、そういえばReduxを試したかったんだ、 ということで、今までの流れを踏まえてReduxを組み込んでいきます。

Reduxに関してはfluxのstoreをいい感じに管理してくれるライブラリぐらいの認識です。 ということで、Reduxに関して深いお話はできないので、 Redux/example にあるサンプルを組み込む話だけします。

webpack-redux

一歩一歩が基本方針なので、まずはReactなしで、webpackにReduxを組み込みます。

Reduxの一番簡単なサンプルということで examples/counter-vanilla をwebpackを使って実装してみます。

// src.js
var Redux = require('redux');

function counter(state, action) {
    if (typeof state === 'undefined') {
        return 0
    }
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state
    }
}

var store = Redux.createStore(counter);
var valueEl = document.getElementById('value');

function render() {
    valueEl.innerHTML = store.getState().toString()
}
render();
store.subscribe(render);

document.getElementById('increment')
    .addEventListener('click', function () {
        store.dispatch({type: 'INCREMENT'})
    });
document.getElementById('decrement')
    .addEventListener('click', function () {
        store.dispatch({type: 'DECREMENT'})
    });

ロジックの部分は最小限の部分をコピーしてきただけなので、無視でよいです。 大事なのは1行目で、require('redux')として、モジュールを読み込んでいます。

ということで、package.jsonにはReduxの依存を追加する必要があります。

// package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "redux": "^3.5.1",
    "webpack": "^1.13.0"
  }
}

index.htmlにはcounter-vanillaと同様のElementが必要です。

// index.html
<body>
Clicked: <span id="value">0</span> times
<button id="increment">+</button>
<button id="decrement">-</button>

<script src="js/bundle.js"></script>
</body>

webpack-redux-react

最後にwebpack-reactとwebpack-reduxを組み合わせて、Reactを使いつつ、Reduxで状態管理をします。 参考にするコードはReduxのexamples/counterです。

まず、moduleとしてreactのcomponentとreduxのreducerを切り出します。

// js/counter.js
const React = require('react');

class Counter extends React.Component {
    constructor(props) {
        super(props)
    }

    render() {
        const {value, onIncrement, onDecrement} = this.props;
        return (
            <p>
                Clicked: {value} times
                <button onClick={onIncrement}>+</button>
                <button onClick={onDecrement}>-</button>
            </p>
        )
    }
}

module.exports = Counter;
// js/reducer.js
module.exports = function (state = 0, action) {
    switch (action.type) {
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
};

次に、分割したmoduleをsrc.jsxから読み込んで、examples/counterのようにreduxを組み込みます。

// js/src.jsx
const React = require('react');
const ReactDOM = require('react-dom');
const Redux = require('redux');
const Counter = require('./counter.jsx');
const reducer = require('./reducer');

const store = Redux.createStore(reducer);

function render() {
    ReactDOM.render(
        <Counter
            value={store.getState()}
            onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
            onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
        />,
        document.body
    )
}

render();
store.subscribe(render);

最後に、いつも通りビルドして、index.htmlからbundle.jsを読み込んで動作確認をします。

<!-- index.html -->
<body>
<script src="js/bundle.js"></script>
</body>

その他の設定ファイル類は、最終的には以下のようになります。

// package.json
{
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "babel-core": "^6.7.7",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-react": "^6.5.0",
    "react": "^15.0.1",
    "react-dom": "^15.0.1",
    "redux": "^3.5.1",
    "webpack": "^1.13.0"
  }
}
// .babelrc
{
  "presets": ["react", "es2015"]
}
// webpack.conf.js
module.exports = {
    entry: './js/src.jsx',
    output: {
        path: __dirname + "/js",
        filename: 'bundle.js'
    },
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                loader: "babel-loader"
            }
        ]
    }
};

以上で全ておしまいです。長かった。。

まとめ

  • .babelrcでbabelの変換を設定。対応するpresetを読み込む必要あり。
  • requireでモジュール読み込み。module.exportsでモジュール公開。
  • webpackでmoduleを結合する。babel-loaderでBabelの処理を挟める。
  • 全部のパターンをまとめたリポジトリはこちら
    github.com

ザキオカさん先発や!