gulp
React
babel

聖なる夜の寂しさを紛らわせるために、Reactを使ってライフゲームを作った↑↑

More than 3 years have passed since last update.

kkyouheiさん2日連続の投稿お疲れ様でした!
昨日の投稿は『1行もGo書いたこと無いけどGo Conference Winter 2015に参加したので資料をまとめた』でした。
そして今日、新卒1年目のg_ryotaroがバトンを受け取りました!!

この記事はプログラミング大好きベーシック Advent Calendar 2015の7日目の投稿です。

クリスマスと言えば、キリスト生誕の日ですよね!!
そんな所以もあって、ライフゲームを作ってみようと思いました。
これでひとりのクリスマスも乗り切れ...orz
さあ気持ちを切り替えて作りましょうか

ただ作ってもあれなので、Thinking in Reactの流れにそって、Reactの開発フローを体感してみました。

「Reactってよく聞くから気になるな」と思われている読者の方が、「あ〜なるほど!これなら出来そうだわ。やってみよう↑↑」と思えるようになっていただければうれしいです!

お前の説明なんていらねーからはやくコード見せろや!!という過激派な方は、こちらにgithubのリンクを貼っておきます。(ノД`)シクシク

最終的にこんな感じです
http://tk2-231-25469.vs.sakura.ne.jp/
25日までには最強のライフゲームが完成しているかもしれません
なにが最強なのか全くわからないですが...

1. ライフゲームとは?

今回作成するライフゲームとは、

生命の誕生、進化、淘汰などのプロセスを簡易的なモデルで再現したシミュレーションゲーム by Wikipedia

です。
はい、全然わかりませんね
これだけだとよくわからないので、参考図です。

CellSample.png

ひとつひとつのセルが生命だと仮定して、その誕生や淘汰をシミュレーションします。
ルールはシンプルに4つです。

  • 誕生: 死んでるセルに隣接する生きたセルが3個 => 次の世代で誕生
  • 生存: 生きているセルに隣接する生きたセルが2or3個 => 次の世代でも生存
  • 過疎: 生きているセルに隣接する生きたセルが0or1個 => 次の世代で死ぬ
  • 過密: 生きているセルに隣接する生きたセルが4個以上 => 次の世代で死ぬ

ここでは生きているセルは黒、死んでいるセルは白とします。

2. Reactについて

Reactはfacebookが公開したJavaScriptライブラリです。
ユーザーインターフェース(UI)を作ることに特化していて、コンポーネントをベースにして設計、開発を行うことが特徴です。
コンポーネントというのは、ネジのような部品と同じイメージです。
3章で説明します。

今回のアプリケーションはReact v0.14.3を使って開発していきます。

3. ライフゲームを作る

3.1 モックを作成する

こんな感じっす

mock.png

3.2 コンポーネントごとに分割し階層構造を明確にする

先ほどのモックをそれぞれのコンポーネントに分割し、階層構造を明確にします。

mock_2.png

コンポーネントは5つです。

  • LifeGameコンポーネント(ピンク)
  • Cellsコンポーネント(青)
  • CellsRowコンポーネント(緑)
  • Cellコンポーネント(黄)
  • SettingsAreaコンポーネント(オレンジ)

各コンポーネントの階層は以下です。

- LifeGame
  - Cells
    - CellsRow
      - Cell
  - SettingsArea

このような設計でアプリを作ってみたいと思います。

3.3 stateを使わずに静的なデータを使って、UIを作ってみる

stateってなんやねんというツッコミを少し横へ置いといて
まずは入力とか考えずに、静的なデータを使ってUIを構成してみます。

ベースになるhtmlファイルは以下です。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>LifeGame</title>
</head>
<style>
  #wrapper { width: 1000px; margin: 0 auto; }
  #content { background-color: #eee; }
  table { margin: 0 auto; }
  td { width: 10px; height: 10px; background-color: #fff; border: solid 1px #000; }
  td.dead { background-color: #000; }
  .settings-area { text-align: center; }
</style>
<body>
  <div id="wrapper">
    <div id="content"></div>
  </div>
  <script type="text/javascript" src="./app.min.js"></script>
</body>
</html>

スタイルが当てられていますが、重要なのはtdとtd.livingの部分です。
.livingがついてるtdは生きてるセルになるということです。
あとReactはjsxで書いたソースをjsへ変換して読み込みます。
gulpfile等はgithubより参照してください!!
こちら

最小のReactアプリケーションを作成します。

lifegame01.jsx
var React = require('react');
var ReactDOM = require('react-dom');

var LifeGame = React.createClass({
  render: function() {
    return (
      <div>Hello world</div>
    )
  }
});

ReactDOM.render(
  <LifeGame />,
  document.getElementById('content')
);

お決まりのHello worldです。
ReactではHTMLタグに似たタグ(上の例だとの部分)をコンポーネントと言って、これらを組み合わせてUIを作っていきます。

ではコンポーネントを組み上げていきます。( 内容に変更の無い部分は省略します )

lifegame02.jsx
var LifeGame = React.createClass({
  render: function() {
    return (
      <div className="lifegame">
        <Cells />
        <SettingsArea />
      </div>
    );
  }
});

var SettingsArea = React.createClass({
  render: function() {
    return (
      <div className="settings-area">
        <button>OK</button>
      </div>
    );
  }
});

var Cells = React.createClass({
  render: function() {
    return (
      <table>
        <tbody>
          <div>hello</div>
        </tbody>
      </table>
    );
  }
});

ここまでで

- LifeGame
  - Cells
  - SettingsArea

の階層構造ができました。
ただ、作りたい階層構造は、

- LifeGame
  - Cells
    - CellsRow
      - Cell
  - SettingsArea

なので、さらに組み上げていきます。

lifegame03.jsx
var Cells = React.createClass({
  render: function() {
    return (
      <table>
        <tbody>
          <CellsRow />
        </tbody>
      </table>
    );
  }
});

var CellsRow = React.createClass({
  render: function() {
    return (
      <tr>
        <Cell />
      </tr>
    );
  }
});

var Cell = React.createClass({
  render: function() {
    return (
      <td></td>
    );
  }
});

これで階層構造ができました。
tdにスタイルが当てられているので、1つのセルが表示されると思います。
こんな感じ!!
スクリーンショット 2015-12-06 17.22.21.png

ここからセルを増やしていきます。

親から子へのデータ受け渡し

セルの生死に関する状態を保持する配列を作ります。
8×8のテーブルをつくるのですが、境界の条件を複雑にしないために10×10の2次元配列を作成します。
1が生きている状態
0が死んでいる状態です

それを一番上の親、LifeGameコンポーネントに渡します。

lifegame04.jsx
var data = [
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
  [0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
  [0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
  [0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];

ReactDOM.render(
  <LifeGame data={data} />,
  document.getElementById('content')
);

これでLifeGameコンポーネントの中で、渡されたデータを使うことができます。

lifegame05.jsx
var LifeGame = React.createClass({
  render: function() {
    return (
      <div className="lifeGame">
        <Cells data={this.props.data} />
        <SettingsArea />
      </div>
    )
  }
});

props

によって渡されたオブジェクトを実際に使用する場合、this.props.属性名 によって呼び出すことが出来ます。
上記の例の場合、与えられたdataを呼び出しています。

さらにCellsコンポーネントにLifeGameコンポーネントが持つdataを渡しています。

よくみかけるこんなやつですね
Component01 2.png

与えられたデータに基いて、セルを表示します。

ここまでのコードがこのようになっています。

lifegame06.jsx
var data = [
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
  [0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
  [0, 1, 0, 0, 0, 0, 0, 0, 1, 0],
  [0, 0, 1, 0, 0, 0, 0, 1, 0, 0],
  [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];

var LifeGame = React.createClass({
  render: function() {
    return (
      <div className="lifegame">
        <Cells data={this.props.data} />
        <SettingsArea />
      </div>
    )
  }
});

var SettingsArea = React.createClass({
  render: function() {
    return (
      <div className="settings-area">
        <button>OK</button>
      </div>
    );
  }
});

var Cells = React.createClass({
  render: function() {
    var rows = [];
    var data = this.props.data;
    var rowsNumber = data.length - 2;
    for(var i=0; i < rowsNumber; i++) {
      rows.push(<CellsRow key={i} rowIndex={i} data={data}/>)
    }
    return (
      <table>
        <tbody>
          {rows}
        </tbody>
      </table>
    );
  }
});

var CellsRow = React.createClass({
  render: function() {
    var cells = [];
    var data = this.props.data;
    var columnsNumber = data.length - 2;
    for(var i=0; i < columnsNumber; i++) {
      cells.push(<Cell key={i} rowIndex={this.props.rowIndex} columnIndex={i} data={data}/>)
    }
    return (
      <tr>
        {cells}
      </tr>
    );
  }
});

var Cell = React.createClass({
  render: function() {
    var rowIndex = this.props.rowIndex;
    var columnIndex = this.props.columnIndex;
    if(this.props.data[rowIndex + 1][columnIndex + 1]) {
      return (
        <td className="living"></td>
      );
    } else {
      return (
        <td></td>
      );
    }
  }
});

ReactDOM.render(
  <LifeGame data={data} />,
  document.getElementById('content')
);

表示はこんな感じです。
スクリーンショット 2015-12-06 18.17.13.png

dataを一番下のセルまで運んで、行列の添字もセルに渡すことで、自分が黒なのか白なのか判断しています。

3.4 最小限のstateを考える

先ほど出てきたpropsが親のデータなら、自分のデータを取ってくるものもあります。それがstateです。

stateによってコンポーネントに状態を置いておくことができます。
stateが変更されると、下層のコンポーネントが勝手に更新されるんです!!!すごい\(^o^)/

今回のアプリの場合、LifeGameコンポーネントがセルの生死についてデータを持っておくのがいいかな??と思います。

3.5 stateを持つコンポーネントを決める

LifeGameコンポーネントですね

少しコードを書き換えます。

lifegame07.jsx
var LifeGame = React.createClass({
  getInitialState: function() {
    return {
      data: this.props.data
    }
  },
  render: function() {
    return (
      <div className="lifegame">
        <Cells data={this.state.data} />
        <SettingsArea />
      </div>
    )
  }
});

これでLifeGameのstateが変化すれば、子コンポーネントが更新されるようになります。
getInitialStateはReactが用意してくれているメソッドです。
return {}のなかのハッシュをstateとして読み込んでくれます。
突然出てくるカッコいい名前のメソッドは大体Reactがよしなにやってくれる仕組みなので、上手く活用しましょう。

ここで、与えられた配列から次の世代の配列がどのようになるか計算するヘルパーを作成します。
ライフゲームのルールに沿って、与えられたデータを素直に計算しました。別ファイルに作成します。

CalcHelper.js
var CalcHelper = {
  cellsOfLife: function(data) {
    var pre_generation = data;
    var areaSize = data.length - 2;
    var next_generation = [];
    for(var i=0; i <= areaSize + 1; i++) {
      var row = [];
      for(var j=0; j <= areaSize + 1; j++) {
        row.push(0);
      }
      next_generation.push(row);
    }

    for(var i=1; i <= areaSize; i++) {
      for(var j=1; j <= areaSize; j++) {
        var livingCellsCnt = this.getLivingAroundCellsCnt(i, j, pre_generation);

        if(pre_generation[i][j]) {
          if(livingCellsCnt >= 4) {
            next_generation[i][j] = 0;
          } else if(livingCellsCnt <= 1) {
            next_generation[i][j] = 0;
          } else {
            next_generation[i][j] = 1;
          }
        } else {
          next_generation[i][j] = (livingCellsCnt == 3) ? 1 : 0;
        }
      }
    }

    for(var i=0; i <= areaSize + 1; i++) {
      next_generation[0][i] = 0;
      next_generation[i][0] = 0;
      next_generation[areaSize + 1][i] = 0;
      next_generation[i][areaSize + 1] = 0;
    }

    return next_generation;
  },
  getLivingAroundCellsCnt: function(i, j, ary) {
    // 生きてるセルがわかれば死んでるセルの個数もわかる
    var livingCellsCnt = 
                 ary[i-1][j-1] +
                 ary[i-1][j]   +
                 ary[i-1][j+1] +
                 ary[i][j-1]   +
                 ary[i][j+1]   +
                 ary[i+1][j-1] +
                 ary[i+1][j]   +
                 ary[i+1][j+1];
    return livingCellsCnt;
  }
}

module.exports = CalcHelper;

CalcHelperを読み込みます。
そして、stateを一定の時間で変更する処理をLifeGameコンポーネントに追加します。

lifegame08.jsx
var React = require('react');
var ReactDOM = require('react-dom');
var CalcHelper = require('./CalcHelper');

//...

var LifeGame = React.createClass({
  getInitialState: function() {
    return {
      data: this.props.data
    }
  },
  refleshData: function() {
    var nextGen = CalcHelper.cellsOfLife(this.state.data);
    this.setState({
      data: nextGen
    });
  },
  componentDidMount: function() {
    setInterval(this.refleshData, 3000);
  },
  render: function() {
    return (
      <div className="lifegame">
        <Cells data={this.state.data} />
        <SettingsArea />
      </div>
    )
  }
});

//...

componentDidMountはコンポーネントが作成された時に呼ばれるメソッドです。
自信のrefleshDataメソッドを呼んで、その中でヘルパーを使って計算し、次の世代を算出しています。

ついに動きだしました!!
LifeGame01.gif

3.6 逆のデータフローを考える

これまでは親のコンポーネントから子のコンポーネントへデータを渡すことをやりました。
逆に、子のコンポーネントに対して入力等が合った場合、どのようにすればいいのでしょうか?

stateを変更したい親コンポーネントが持っているメソッドをpropsで渡して子コンポーネントで使うこと

です。

フローはこんな感じです。
1. 親コンポーネントに状態を変更するメソッドを作る(中身は書かない)
2. 他のデータと同様に、子コンポーネントへ渡す
3. 子コンポーネントでイベントを受ける属性を追加(呼び出すのは自分のメソッド)
4. 呼び出したメソッドの中で、親コンポーネントから受け取ったメソッドを呼び出す
5. 1で作ったメソッドにstateを変更する処理を書く

では上のフローに従ってメソッドを追加します。
ボタンをクリックした際に、初期値のステータスを変更したいと思います。

実行する時はコメントアウトを消さないといけないかもです。

lifegame09.jsx
// ボタン押した後に更新するデータ
var data2 = [
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
  [0, 1, 1, 1, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];

var LifeGame = React.createClass({
  getInitialState: function() {
    return {
      data: this.props.data
    }
  },
  refleshData: function() {
    var nextGen = CalcHelper.cellsOfLife(this.state.data);
    this.setState({
      data: nextGen
    });
  },
  handleBtn: function(data) {
    // 1. 親コンポーネントの状態を帰るメソッド

    // 5. 状態を変更する処理を追加
    this.setState({
      data: data
    });
  },
  componentDidMount: function() {
    setInterval(this.refleshData, 1000);
  },
  render: function() {
    return (
      <div className="lifegame">
        <Cells data={this.state.data} />
        // 2. onBtnとして子へ渡している
        <SettingsArea onBtn={this.handleBtn} />
      </div>
    )
  }
});

var SettingsArea = React.createClass({
  handleOKBtn: function(e) {
    // 4. 親のメソッドを使う
    // eってやつにeventが入ってるからe.target.valueとかで渡す(今回はdata2を渡してる)
    this.props.onBtn(data2);
  },
  render: function() {
    return (
      <div className="settings-area">
        // 3. Reactが用意してくれているonClickという属性で関数を渡す
        <button onClick={this.handleOKBtn}>OK</button>
      </div>
    );
  }
});

ちょっと行ったり来たりで大変なのですが、逆のフローはこのようにして作られます。
ここではボタンのonClickイベントを受け取りましたが、もちろん他にもたくさんあります
https://facebook.github.io/react/docs/events.html

ここまでのコードでこのようになります。
LifeGame02.gif

後は創意工夫で色々な機能を追加していくのみです!!!

自分ならこのように実装するよ!!等のアドバイスを頂けるとうれしいです(^o^)
プルリクエストもお待ちしておりますm(_ _)m

まとめ

Thinking in Reactいいですね!
業務でReactで実装されている部分に機能追加をすることがあったのですが、フローに従わずにあっちこっち考えてしまってとても時間を食いました。

  • 子コンポーネントにデータを流してみること
  • どのコンポーネントが状態を持つべきか考えること
  • 逆のフローは最後に考えること

この3つを順を追って考えることが重要ですね。

明日はCTOのzaruさんで〜す。
よろしくお願いしますm(_ _)m

参考リンクとか本まとめ

フロントエンドJSの@hokacchaさんのプレゼン資料:
https://speakerdeck.com/player/05a61bfc6f554d72b493be8d5771ae27?#

HTML5とか勉強会の動画:33分あたりから
https://www.youtube.com/watch?v=eRPFiVMlJlc

一人React、AdventCalendar2014:
http://qiita.com/advent-calendar/2014/reactjs

Thinking in React:
https://facebook.github.io/react/docs/thinking-in-react.html

jsxを変換するときに救ってくれた記事:
http://blog.webcreativepark.net/2015/11/06-181413.html

入門 React:
オライリーの本です。少しだけ読みました。

おまけ(gulpfileとpackage.json)

gulpfile.js
'use strict'

var gulp = require('gulp');
var browserify = require('browserify');
var source = require("vinyl-source-stream");
var babelify = require('babelify');

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

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

babelifyの7系を使う時は、

{
  "presets": ["react"]
}

が必要みたいです。

package.json
{
  "name": "app07",
  "version": "1.0.0",
  "description": "",
  "main": "gulpfile.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "babel-preset-es2015": "^6.3.13",
    "babel-preset-react": "^6.3.13",
    "babelify": "^7.2.0",
    "browserify": "^12.0.1",
    "gulp": "^3.9.0",
    "react": "^0.14.3",
    "react-dom": "^0.14.3",
    "vinyl-source-stream": "^1.1.0"
  }
}

全然関係ないのですが、javascriptのデバッグするときはコードにdebuggerといれると処理が途中で止まってくれるのを初めて知りました