勉強会JS編＜4＞ yeoman + backbone.model + grunt

# 勉強会JS編＜4＞ yeoman + backbone.model + grunt

## 関連記事

* [勉強会JS編＜1＞ オブジェクト指向言語としてのJavaScriptを理解](http://qiita.com/park-jh/items/1887d87f33474cf7d733)
* [勉強会JS編＜2＞ クライアントサイドMVCフレームワーク](http://qiita.com/park-jh/items/8b0a21e09223e3a8d0bc)
* [勉強会JS編＜3＞ フロントエンド開発環境構築](http://qiita.com/park-jh/items/319cacc28850c9fd4898)
* 勉強会JS編＜4＞ yeoman + backbone.model + grunt
* [勉強会JS編＜5＞ yeoman + backbone.collection + backbone.localStorage](http://qiita.com/park-jh/items/49fefe30cdf57de7b6f0)
* [勉強会JS編＜6＞ yeoman + backbone.view](http://qiita.com/park-jh/items/44fb4faac997c71201a0)
* [勉強会JS編＜7＞ yeoman + backbone.router](http://qiita.com/park-jh/items/8f7d4275279201d5796b)
* [勉強会JS編＜8＞ yeoman + backbone.js 実践 - その１](http://qiita.com/park-jh/items/bad19e1c6b502ef0d0fa)
* [勉強会JS編＜9＞ yeoman + backbone.js 実践 - その２](http://qiita.com/park-jh/items/55d62bc58b94b12835c3)

## Backbone.Modelとは

* アプリケーションで扱うデータを構造化したもの

## 実習

### プロジェクト生成

* sampleという名前のディレクトリを生成し、そのディレクトリ内で「yo backbone」を実行する。
* 今度はTwitter Bootstrap for Sassだけ選択してEnterキーを押す。（CoffeeScript版は別途投稿する）

```sh
  [devnote@hooni:~/documents/study/js] % mkdir sample
  [devnote@hooni:~/documents/study/js] % cd sample
  total 0
  drwxr-xr-x  2 devnote  staff    68B  1 19 10:06 ./
  drwxr-xr-x  6 devnote  staff   204B  1 19 10:06 ../
  [devnote@hooni:~/documents/study/js/sample] % yo backbone
  undefined
  Out of the box I include HTML5 Boilerplate, jQuery and Backbone.js.
  ? What more would you like? (Press <space> to select)
  ❯◉ Twitter Bootstrap for Sass
   ◯ Use CoffeeScript
   ◯ Use RequireJS
   ◯ Use Modernizr
```

### モデルの雛形を生成

* yeomanで新しいモデルを生成する。

```sh
  [devnote@hooni:~/documents/study/js/sample] % yo backbone:model contact
     create app/scripts/models/contact.js
     create test/models/contact.spec.js
```

* 生成されたモデルを下記のように修正する。

```javascript
  [devnote@hooni:~/documents/study/js/sample] % vi app/scripts/models/contact.js
  /*global Sample, Backbone*/

  Sample.Models = Sample.Models || {};

  (function () {
    'use strict';

    Sample.Models.Contact = Backbone.Model.extend({

      url: '',

      initialize: function() {
      },

      defaults: {
        firstname: '',
        lastname: '',
        email: ''
      },

      validate: function(attrs, options) {
      },

      parse: function(response, options)  {
        return response;
      }
    });

  })();
```

* defaultsプロパティに指定した属性値は、初期値として必ず持つようになる。

```javascript
  defaults: {
    firstname: '',
    lastname: '',
    email: ''
  },
```

### モデルのオブジェクトを生成

* init()の中でContactオブジェクトを生成する。

```javascript
  // app/scripts/main.js
  /*global Sample, $*/

  window.Sample = {
    Models: {},
    Collections: {},
    Views: {},
    Routers: {},
    init: function () {
      'use strict';
      console.log('Hello from Backbone!');

      // モデルのオブジェクトを生成
      var contact = new Sample.Models.Contact({
        firstname: '太郎',
        lastname: '山田',
        email: 'yamada@example.com'
      });
      return contact;
    },
    ex01: function(contact) {
      console.log(JSON.stringify(contact, null, 2));
    },
  };

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.ex01(c);
  });
```

* yeomanで生成する場合、sample（先頭が小文字）で生成されるが、モデルでグロバール変数名がSample（先頭が大文字）になっているため、Sampleで修正する。

* データ構造をJSON形式で確認
  - 引数として受け取ったオブジェクト、あるいは引数のオブジェクトがtoJSON()を持っている場合、戻り値をJSON文字列化する。
  - 第２引数は出力される値のフィルタリングや加工のためのオプションで、第３引数はインデント幅を指定するためのオプション

```javascript
  console.log(JSON.stringify(contact, null, 2));
```

* grunt serveを実行すると、デフォルトブラウザが開ける。

```sh
  grunt serve
```

* developer toolsを開き、consoleでcontactオブジェクトの内容が表示されていることを確認する。
* Macの環境でchromeをデフォルトブラウザで使っている場合は、alt + command + i で開ける。

```javascript
  Hello from Backbone!
  {
    "firstname": "太郎",
    "lastname": "山田",
    "email": "yamada@example.com"
  }
```

### 属性値の設定

* set()

```javascript
  // app/scripts/main.js
  ex02: function(contact) {
    'use strict';
    // 属性値の設定
    // firstname属性に'一郎'を設定する。
    contact.set('firstname', '一郎');
    console.log(JSON.stringify(contact, null, 2));

    // オブジェクトで複数の属性を設定する。
    contact.set({
      firstname: '二郎',
      lastname: '高橋'
    });
    console.log(JSON.stringify(contact, null, 2));
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.ex02(c);
  });
```

### 属性値の取得

* get()

```javascript
  // app/scripts/main.js
  ex03: function(contact) {
    'use strict';
    // 属性値の取得
    // firstnameを取得
    console.log(contact.get('firstname'));
    // lastnameを取得
    console.log(contact.get('lastname'));
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.ex03(c);
  });
```

### 属性値の有無を確認

* has()

```javascript
  // app/scripts/main.js
  ex04: function(contact) {
    'use strict';
    // 属性値の有無の確認
    console.log(contact.has('firstname'));
    // => true
    console.log(contact.has('address'));
    // => false
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.ex04(c);
  });
```

  - 属性が存在するかどうか確認する。
  - 指定した属性にnullまたはundefinedでない値が設定されている場合にのみtrue、それ以外はfalseを返す。

### イベント

* on()

```javascript
  // app/scripts/main.js
  ex05: function(contact) {
    'use strict';
    // changeイベントですべての属性の変化を監視する。
    contact.on('change', function(contact) {
      console.log('属性が変更されました。');
    });

    // change:属性名と記述することで
    // 特定の属性値の変化に絞って監視できる。
    contact.on('change:email', function(contact) {
      console.log('email属性が変更されました。');
    });

    // firstnameを変更する。
    // changeイベントが発生して、changeのコールバック関数が実行される。
    contact.set('firstname', '二郎');
    console.log(JSON.stringify(contact, null, 2));

    // emailを変更する。
    // changeイベントが発生して、change:emailとchangeのコールバック関数が実行される。
    contact.set('email', 'takahashi@example.com');
    // emailが変更されたけど、イベントは発生しない。
    contact.attributes.email = 'jirou@example.com';
    console.log(JSON.stringify(contact, null, 2));

    // 引数なし＝すべてのイベント
    contact.off();

    // イベント名を指定
    // contact.off('change');

    // イベント名と属性名を指定
    // contact.off('change:email');

    contact.set('email', 'yamada@example.com');
    console.log(JSON.stringify(contact, null, 2));

    contact.off();
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.ex05(c);
  });
```

* attributes
  - モデルが持つ属性値はattributesプロパティに格納される。
  - attributesに直接属性値の設定や取得ができるが、イベントのしくみが動かなくなる。

### コールバック関数を特定して解除

* off()

```javascript
  // app/scripts/main.js
  ex06: function(contact) {
    'use strict';
    // コールバック関数を特定して解除
    var onChange = function() {
      console.log('属性が変更されました。');
    }
    var onChangeEmail = function() {
      console.log('email属性が変更されました。');
    }

    contact.on('change', onChange);
    contact.on('change:email', onChangeEmail);

    // changeイベントに対してonChange()メソッドを紐付けた監視だけを解除する。
    contact.off('change', onChange);
    // この属性値の変更に反映するのはonChangeEmail()メソッドのみとなる。
    contact.set('email', 'yamadaIchiro@example.com');
    console.log(JSON.stringify(contact, null, 2));

    contact.off();
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.ex06(c);
  });
```

### 独自のイベントの発生

* trigger()

```javascript
  // app/scripts/main.js
  ex07: function(contact) {
    'use strict';
    // 独自のイベントの発生
    // Contactインスタンスを発生してselect()メソッドを呼び出す
    contact.select();
    console.log('contact.selected:', contact.selected);

    contact.off();
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.ex07(c);
  });

  // app/scripts/models/contact.js
  initialize: function() {
    // ex07 - start
    // selectイベントの発生を監視する
    this.on('select', function(selected) {
      console.log('selectイベントが発生しました。');
    });
    // ex07 - end

    // ex09 - start
    // 検証中に発生したエラーを監視する
    this.on('invalid', function(model, err) {
      // invalidイベントに紐付くコールバック関数はvalidate()メソッドが返すエラーメッセージを受け取ることができる
      // あるいはモデルのvalidationErrorプロパティを参照してもよい
      console.log(err);
    });
    // ex09 - end
  },

  select: function() {
    // 選択中フラグを立てる。連絡先データではないので属性ではなく単なるプロパティとして扱う
    this.selected = true;

    // 独自イベントのselectを発生させる
    // trigger()メソッドの第２引数以降の指定はコールバック関数が受け取れるパラメータとなる
    this.trigger('select', this.selected);
  },
```

### 独自処理の実装

* モデルの中に関数を追加

```javascript
  // app/scripts/main.js
  ex08: function(contact) {
    'use strict';
    console.log('contact.fullname:', contact.fullname());
  },

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.ex08(c);
  });

  // app/scripts/models/contact.js
  defaults: {
    firstname: '',
    lastname: '',
    email: ''
  },

  // ex08 - start
  // 独自処理の実装
  fullname: function() {
    return this.get('firstname') + ' ' + this.get('lastname');
  },
  // ex08 - end
```

### 属性値の検証

* validate()

```javascript
  // app/scripts/main.js
  ex09: function(contact) {
    'use strict';
    // validate()メソッドによる検証を通過しない変更を{validate: true}オプションを付けてわざと行う
    contact.set({
      lastname: ''
    },{
      validate: true
    });

    // モデルの属性が変化していないことを確認する
    console.log(JSON.stringify(contact, null, 2));
  }

  ...snip...

  $(document).ready(function () {
    'use strict';
    var c = Sample.init();
    Sample.ex09(c);
  });

  // app/scripts/models/contact.js
  initialize: function() {
    // ex09 - start
    // 検証中に発生したエラーを監視する
    this.on('invalid', function(model, err) {
      // invalidイベントに紐付くコールバック関数はvalidate()メソッドが返すエラーメッセージを受け取ることができる
      // あるいはモデルのvalidationErrorプロパティを参照してもよい
      console.log(err);
    });
    // ex09 - end
  },
  // ex09 - start
  validate: function(attrs, options) {
    if(!attrs.firstname || !attrs.lastname) {
      return 'firstname属性とlastname属性の両方が必須です。';
    }
  },
  // ex09 - end
```

  - モデルの属性値が期待されてる通りになっているか検証を行う。
  - 実行されるタイミング
      + save()メソッドが呼び出されたとき
      + set()メソッドが{validate: true}のオプションを付けて呼び出されたとき

## その他のイベント

* listenTo()
  - 他のインスタンスに対するイベントを監視
  - イベントを監視するという点ではonと同様

* stopListening()
  - インスタンスから別のインスタンスを監視することを解除する。

* onとlistenToの違い
  - on()の場合はモデルがon()を呼び出す主体で、listenTo()の場合はモデルを監視する側がlistenTo()を呼び出す主体となる。
  - インスタンスを破壊する際、イベントの監視も解除しなければならない。
  - そうしないと、イベントに紐付いているコールバック関数の参照が残ってしまい、ガベージコレクタの対象から離れメモリリークの原因になる。
  - listenToでイベントを監視する場合、stopListeningでそのインスタンスが行っているすべてのイベントの監視を解除する。
  - Backbone.Viewのremove()は内部でstopListeningを呼び出しているので、習慣的にlistenToを使ったほうがいい。

* イベント一覧（http://backbonejs.org/#Events-catalog）

## 参考書籍

* JavaScript徹底攻略
* JavaScriptエンジニア養成読本