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

  • 301
    Like
  • 0
    Comment
More than 1 year has passed since last update.

手元でコードを書きながら React.js を学習しようと、タイトルどおりの最小構成を作ってみました。

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

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

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

  • ECMAScript 6 構文のサポートおよび JSX ファイルのコンパイルに Babel を利用
  • 依存モジュールの管理(CommonJS準拠)に Browserify を利用
  • タスクランナーとして gulp を利用

またサンプルコードとして、簡単なデータバインドを React.js で実装してあります。デモは こちら(GitHub Pages) で確認できます。

demo.png

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

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

環境(package.json)

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

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 は Browserify 用の Babel 変換ライブラリです。自分的には ES6 で書く予定はまだ無いのですが、JSX の変換もやってくれるみたいなので採用しました。

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

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

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

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

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

OS:Mac OS X 10.9.5
npm:2.7.5

gulpのタスク(gulpfile.js)

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 での変換を実行しています(ソースマップが不要なら debugfalse にしてください)。シンタックスエラー時の挙動を定義しないとエラー時に gulp のタスクが終了してしまうので、とりあえず上記のように定義しています。ファイル名やパスを変更する場合はそれっぽいところを修正すればよいです。

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

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

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

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

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

学習はこれからなので慣れていないのですが.. 簡単なデータバインドを React.js で実装してみました。デモはこちら(GitHub Pages)

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

app.jsx

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

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

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

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

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

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.◯◯ で参照します。

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 の中に含まれます。

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 を買ってまだ読んでいないので、コードを書いて試しながらひと通り読んでみようと思います。

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

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

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')
);
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 のオブジェクトに階層があると何故か上手くいかなかったので1階層にしてあります。

参考にさせて頂いた記事