React.js + Babel + Browserify + gulp の環境を作ってみた


手元でコードを書きながら [React.js](https://facebook.github.io/react/) を学習しようと、タイトルどおりの最小構成を作ってみました。

> 上記の公式ページの Starter Kit や Tutorial の構成が物足りなかったので..

GitHub に置いたので、よろしければ参考にしてください。
⇒ https://github.com/hkusu/react-babel-browserify-gulp-sample

上記の `README.md` に記載している通り、特徴としては次のとおりです。

- ECMAScript 6 構文のサポートおよび JSX ファイルのコンパイルに [Babel](https://babeljs.io/) を利用
- 依存モジュールの管理(CommonJS準拠)に [Browserify](http://browserify.org/) を利用
- タスクランナーとして [gulp](http://gulpjs.com/) を利用

またサンプルコードとして、簡単なデータバインドを React.js で実装してあります。デモは [こちら(GitHub Pages)](http://hkusu.github.io/react-babel-browserify-gulp-sample/) で確認できます。

![demo.png](https://qiita-image-store.s3.amazonaws.com/0/35733/161867a5-c88d-2411-e036-fbbe42ebb25f.png "demo.png")

環境の使い方については上記 GitHub の `README.md` に書いたので、この投稿では Tips というか補足を書きます。

> ちなみにフロントエンドの経験はそれほど長くないので、間違いや不備があれば指摘ください！

## 環境(package.json)

フロントエンドの周辺環境は移り変わりが激しいのですが、現時点(2015/5/27)での各ライブラリ最新バージョンを利用しています。

```json:package.json(主要な部分を抜粋)
〜
  "devDependencies": {
    "react": "^0.13.3",
    "browserify": "^10.2.1",
    "babelify": "^6.1.1",
    "gulp": "^3.8.11",
    "vinyl-source-stream": "^1.1.0",
    "gulp-webserver": "^0.9.1"
  },
  "scripts": {
    "build": "browserify --debug --transform babelify app.jsx --outfile bundle.js"
  },
〜
```

> 後方互換を期待して各ライブラリのバージョンは固定していないので、もし手元で上手く動かない場合はバージョン番号の前の`^`を外してバージョンを固定して導入してみてください。

React.js は HTML ファイルの `script` タグで読み込むのではなく、Browserify によるビルドの成果物にライブラリのコードを含めるので、ここで定義しています。

[babelify](https://github.com/babel/babelify) は Browserify 用の Babel 変換ライブラリです。自分的には ES6 で書く予定はまだ無いのですが、JSX の変換もやってくれるみたいなので採用しました。

> ES6 で書かないなら、React.js 用の reactify でも良いかも。

`vinyl-source-stream` は gulp で Browserify を扱う際に利用します。`gulp-webserver` はローカルWEBサーバを立ち上げるのに利用しており、他の手段があるなら外しても良いかと思います。

また `package.json` の `scripts` エリアには任意のコマンドを定義できます（ここで定義したものは `$ npm run ◯◯` で実行できる）。この環境では基本は gulp でビルドしているのですが、このようにコマンドでもビルドできるように一応してあります。

> ソースマップが不要なら `--debug` オプションは外してください。

ちなみに今回はあまり関係なさそうですが、手元の環境は次のとおりです。

> OS：`Mac OS X 10.9.5`
> npm：`2.7.5`

## gulpのタスク(gulpfile.js)

```javascript:gulpfile.js
var gulp = require('gulp');
var browserify = require('browserify');
var babelify = require('babelify');
var source = require('vinyl-source-stream');
var webserver = require('gulp-webserver');

gulp.task('browserify', function() {
  browserify('./app.jsx', { debug: true })
    .transform(babelify)
    .bundle()
    .on("error", function (err) { console.log("Error : " + err.message); })
    .pipe(source('bundle.js'))
    .pipe(gulp.dest('./'))
});

gulp.task('watch', function() {
  gulp.watch('./*.jsx', ['browserify'])
});

gulp.task('webserver', function() {
  gulp.src('./')
    .pipe(webserver({
      host: '127.0.0.1',
      livereload: true
    })
  );
});

gulp.task('default', ['browserify', 'watch', 'webserver']);
```

`browserify` タスクで、その名の通り Browserify によるビルドおよび Babel での変換を実行しています（ソースマップが不要なら `debug` を `false` にしてください）。シンタックスエラー時の挙動を定義しないとエラー時に gulp のタスクが終了してしまうので、とりあえず上記のように定義しています。ファイル名やパスを変更する場合はそれっぽいところを修正すればよいです。

> 作成するアプリケーションのファイル(JSXファイル)を複数に分割している場合でも、どこかのJSXファイルで `require` してあれば、芋づる式に Browserify と Babel 変換がされるようです。

`watch` タスクで、プロジェクトのトップ階層の `.jsx` ファイルを監視して、変更があれば `browserify` タスクを実行しています。

`webserver` タスクはローカルWEBサーバです。`livereload` を `true` とすると、ファイルの変更時に自動的にブラウザをリロードしてくれます。

> ちなみに gulp を引数なしでコマンドライン実行した際は `default` に記載したタスクが実行されます。

## サンプルアプリケーション

学習はこれからなので慣れていないのですが.. 簡単なデータバインドを React.js で実装してみました。デモは[こちら(GitHub Pages)](http://hkusu.github.io/react-babel-browserify-gulp-sample/)。

コードは次のような感じです。

### app.jsx

メインとなるクラス（JavaScript でなく React.js のクラス）です。こういう感じでクラスを基本としてコンポーネントを作っていくんですかね。

`getInitialState`(お決まりのメソッド名)でこのクラスの **state**(保持する値？) の初期値を設定しています。state を更新する場合は `setState`(これもお決まりのメソッド名)で更新します。

`render`(お決まりのメソッド名)で画面へ描画する内容を定義します。この記法が JSX と呼ばれるものです。

このクラスの state(保持する値？)は `this.state.◯◯` で参照します。`handleChange` は独自に定義したメソッドです。

`<Message name=◯◯ age=◯◯ />` で後述の Message クラスへ値を渡しています。

```jsx:app.jsx
// React をロード
var React = require('react');
// 外部ファイルへ分割した Message クラスをロード
var Message = require('./message.jsx');

// このアプリケーションのメインとなる App クラス
var App = React.createClass({
  getInitialState: function() {
    return {
      person: {
        name: 'ヤマダ',
        age: 34
      }
    };
  },
  handleChange: function(event) {
    this.setState({
      person: {
        name: event.target.value,
        age: this.state.person.age
      }
    });
  },
  render: function() {
    return (
      <div>
        <input type="text" value={this.state.person.name} onChange={this.handleChange} />
        <Message name={this.state.person.name} age={this.state.person.age} />
      </div>
    );
  }
});

// app クラスを描画
React.render(
  <App />,
  document.getElementById('container')
);
```

Browserify を利用するので `require` が使えています。

> 今回は ES6 で書いていませんが、ES6 で書いても動くはず。

### message.jsx

この規模だとわざわざ分割する必要も無いのですが、例として `Message` クラスを別ファイルへ分割しました。外部のファイルから読み込めるよう `module.exports` しています。

親のクラスから渡された値は `this.props.◯◯` で参照します。

```jsx:message.jsx
// React をロード
var React = require('react');

// Message クラス
var Message = React.createClass({
  render: function() {
    return (
      <p>
        こんにちは。{this.props.name} さん {this.props.age} 歳ですね！
      </p>
    );
  }
});

// エクスポート
module.exports = Message;
```

### index.html

次のように `index.html` では、ビルド済みの `bundle.js` のみ読み込んでいます。React.js のコードも `bundle.js` の中に含まれます。

```html:index.html
<!DOCTYPE html>
<html>
<head lang="ja">
  <meta charset="UTF-8">
</head>
<body>
<div id="container"></div>
<script src="bundle.js"></script>
</body>
</html>
```

## おわりに

[入門 React](http://www.amazon.co.jp/dp/4873117194) を買ってまだ読んでいないので、コードを書いて試しながらひと通り読んでみようと思います。

## 2015/5/29追記：ES6で書いた場合

もし上記のサンプルアプリケーションを ES6 の記法 & `React.Component` を利用して書いた場合は次のようになります。

```jsx:app.jsx
// React をロード
import React from 'react';
// 外部ファイルへ分割した Message クラスをロード
import Message from './message.jsx';

// このアプリケーションのメインとなる App クラス
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '山田'
    }
  }
  handleChange(event) {
    this.setState({
      name: event.target.value
    })
  }
  render() {
    return (
      <div>
        <input type="text" value={this.state.name} onChange={this.handleChange.bind(this)} />
        <Message name={this.state.name} />
      </div>
    );
  }
}

// app クラスを描画
React.render(
  <App />,
  document.getElementById('container')
);
```

```jsx:message.jsx
// React をロード
import React from 'react';

// Message クラス
export default class Message extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <p>
        {this.props.name}
      </p>
    );
  }
}
```

> state のオブジェクトに階層があると何故か上手くいかなかったので１階層にしてあります。

### 参考にさせて頂いた記事

- [React と Babel を組み合わせて使う - スマホ神](http://smart.ataglance.jp/2015-04-27-use-react-with-babel/)
- [React v0.13.0 Beta 1 - React公式](https://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html)