Falcor入門 2日目 FalcorのJSON Graphに触れてみる

Falcor入門の第2回目です。
今日は、Falcorのクライアントサイドにおける基本的な触り方を見ていきたいと思います。

ちなみに過去回はこちら:

* [Falcor入門 1日目 Falcorとは何者か](http://qiita.com/Quramy/items/5cf4f112d715c2e2d300)

## Hello world

なにはともあれ、まずはHello worldですよね。
[第1回目](http://qiita.com/Quramy/items/5cf4f112d715c2e2d300)で、Falcorの導入にはクライアント/サーバ双方の実装が必要である、と書きましたが、取りあえず触るだけであればクライアントのみの実装で充分です。

折角なので、環境構築手順も含めて書いていきます(Falcorに興味を持っている時点である程度のスキルが予想できそうなので、もっとサラッと流してもいいかも知れませんが...)

```sh:必要なモジュール
mkdir falcor-hello-world;cd falcor-hello-workd
npm init -y
npm -g install broserify babel
npm install falcor falcor-http-datasource --save
npm install babel-preset-es2015 --save-dev
```

まずは必要なモジュールのインストールから。

今回は(というより、只の僕の好みですが)、[Babel](https://babeljs.io/)と[Browserify](http://browserify.org/)を利用します。

Falcorをブラウザから利用する際は、下記2つのnpm packageがあれば充分です.

* [falcor](https://www.npmjs.com/package/falcor)
* [falcor-http-datasource](https://www.npmjs.com/package/falcor-http-datasource)

続いて、コード本体を書いてみます。

```js:src/index.js
import * as Falcor from 'falcor/browser';

var model = new Falcor.Model({
    cache: {
        greeting: 'Hello, world'
    }
});

model.getValue('greeting').then(val => {
    console.log(val);
});

model.get('greeting').then(res => {
    console.log(JSON.stringify(res.json, null, 2));
});
```

下記のコマンドで上記のコードをbabeってbundleします:

```sh:babeってbundle
babel -d built --presets es2015 src
browserify built/index.js -o built/bundle.js -d
```

bundleを読み込むhtmlファイルを`index.html`として用意します。

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
<script src="built/bundle.js"></script>
</body>
</html>
```

いざ、起動！

```sh:起動!
open index.html
```

開発者ツールを立ち上げると、以下の出力が得られていると思います:

```text:コンソール出力
Hello, world
{
  "greeting": "Hello, world"
}
```

さて、駆け足でここまで来てしまったので、もういちどじっくり `src/index.js` を見てみましょう。

```js:src/index.js
import * as Falcor from 'falcor/browser';

// 1. Falcor.Modelで新しいModelの生成. cacheオプションでModelのcacheの初期値をセットできる
var model = new Falcor.Model({
    cache: {
        greeting: 'Hello, world'
    }
});

// 2. model.getValueで値の取り出し
model.getValue('greeting').then(val => {
    console.log(val);
});

// 3. model.get でオブジェクトの取り出し
model.get('greeting').then(res => {
    console.log(JSON.stringify(res.json, null, 2));
});
```

シンプルなHello worldですが、いくつかポイントがあります。

まず、`new Falcor.Model(...)` の部分から。このClassが、ブラウザからFalcorを利用する際の窓口になります。

Falcorには大きく分けて、DataSourceとModelというコンポーネントがあるのですが、利用者側はModelを意識していれば大丈夫です。

* DataSource: いわゆるData Access Object. 実際のデータ(AjaxやLocalStorage等)からJSON Graphを取得し、自身のCacheをメンテナンスする責務を持つ
* Model: DataSourceをラップしたClass(いわゆるDecoratorパターン). JSON GraphをVirtual JSONとしてアクセスするアダプタや、batch処理などが出来るようになる

本来は、`Falcor.HttpDataSource` などのDataSouce実体を指定して、Modelとリモートデータソースを紐付けるのですが、取りあえずFalcorとJSON Graphに慣れたい目的であれば、`cache` オプションを利用することで、クライアントサイドオンリーで動作させることができます。

次に `model.getValue('greeting')` の部分です。`getValue` はJSON Graphから単一の値を取り出す際に用います。後述の`get`の方が汎用性が高いので、正直僕は殆ど使いません。
`getValue` に限らず、Falcor.Modelのデータ取得/操作系のメソッドは、結果が非同期で返ってきます。上のコードのようにPromiseとして扱うこともできますが、実のところRxJSのObservableでもあるため、`subscribe()` と書くことも出来ます。

`model.get('greeting')` とした場合、一度に複数のデータを取得することができます。次の節で詳しく見ていきます。


## JSON Graph

続いて、もう少しFalcorらしいサンプルを紹介します。[第1回](http://qiita.com/Quramy/items/5cf4f112d715c2e2d300) でも解説しましたがFalocrの主要概念JSON Graphの例です。

<!-- cache初期化 {{{ -->

```js:src/index.js
var model = new Falcor.Model({
    cache: {
        todoByid: {
            '0001': {
                id: '0001',
                text: '牛乳を買う',
                completedAt: '2016-01-01'
            },
            '0002': {
                id: '0002',
                text: 'ATMでお金を引き出す',
                completedAt: null
            }
        },
        todos: [
            {$type: 'ref', value: ['todoByid', '0001']},
            {$type: 'ref', value: ['todoByid', '0002']}
        ],
        favorites: [
            {$type: 'ref', value: ['todoByid', '0002']}
        ]
    }
});

var out = (res) => {
    console.log(res && JSON.stringify(res.json, null, 2));
};

```

<!-- cache初期化 }}} -->

上記の`cache`はいわゆるTODOリストの例を示しています。TODO全体を表す`todos`というリストと、お気に入りのTODO(そんなもんが世の中にあるのかはおいておき)を表す`favorites` というリストの2つを用意しました。
TODOの実体は`todoByid` という連想配列で表現し、それぞれのリストでは`{$type: 'ref'}`という部分で、グラフの参照を確保しています。

### getのパターン色々

さて、次のように`get` してみましょう。

```js
model.get('todos[0].text').then(out);
```

JSON Graph中の`$ref` が解決された上で、以下のような出力が得られるはずです。

`Falcor.Model`からJSON Graphにアクセスする際は、「JSON Graphのどの部分が欲しいか」を[Path](http://netflix.github.io/falcor/doc/global.html#Path)という表現で記載します。偉そうにPathとか書きましたが、普段、JSONから値を取得するときのアクセス表現を考えれば良いだけです。

```json:出力
{
  "todos": {
    "0": {
      "text": "牛乳を買う"
    }
  }
}
```

複数の値を一度に取得することも出来ます。以下の書き方は全て等価です。

```js:複数値の取得
model.get('todos[0]["text", "completedAt"]').then(out);
model.get('todos[0].text', 'todos[0].completedAt').then(out);
model.get(['todos', 0, 'text'], ['todos', 0, 'completedAt']).then(out)
```

```json:出力
{
  "todos": {
    "0": {
      "text": "牛乳を買う",
      "completedAt": "2016-01-01"
    }
  }
}
```

配列から複数の要素を取り出だすことも出来ます。

```js:配列から複数要素を取得
model.get('todos[0..9, "length"]["text", "completedAt"]').then(out)
```

```json:出力
{
  "todos": {
    "0": {
      "text": "牛乳を買う",
      "completedAt": "2016-01-01"
    },
    "1": {
      "text": "ATMでお金を引き出す",
      "completedAt": null
    },
    "length": 2
  }
}
```

`todos[0..9, "length"]` という部分が、0番目から9番目の要素と配列の長さを寄越せ、という意味になります。

なお、得られた出力をみるとあたかも配列のように見えますが、あくまでこの出力はObjectです。Arrayとして`.map`や`.forEach`のメソッドが使いたい場合、自分で変換する必要があるので注意が必要です。

### FalcorとObject, Arrayのポリシー

先述の説明では意図的に書かなかったのですが、`model.get('todos[0]')` や`model.get('todos')` といった呼び出しが上手く動作しないことに気づいたでしょうか？
それぞれ、「最初のTODOについて、内包する全ての値」と「TODOリスト全件」を意図した呼び出しを行なったという意味です。

この挙動について補足しておきます。**Falcorは「オブジェクト全体」や「配列全件」にクライアントがアクセスすることを意図的に禁止**しています。

このポリシーについては、[http://netflix.github.io/falcor/documentation/model.html#working-with-data-using-a-model](http://netflix.github.io/falcor/documentation/model.html#working-with-data-using-a-model)に書いてありますので、訳文を貼っておきます。

> FalcorはViewへのデータ表示に最適化されています. 配列とオブジェクトは総量が制限されていないデータを含めることができます.
> 配列、オブジェクト全体をリクエストすることは、SQLの世界で`WHERE`を付与せずに`SELECT *` を実行するようなものです。
> 今は5つの要素を含む配列が、そのうち10,000個の要素を含むまでに成長します。
> 最初は高速に提供されていたリクエストが、データがストアされるにつれてViewの描画時間が遅くなっていくことを意味します。
> 
> Modelは、開発者にどの値を抽出するべきか明示させることで、serverが安定したパフォーマンスを発揮できるようにします。
> Modelは、オブジェクト全体を取得できるようにさせず、与えられたシナリオに応じて必要な値のみを取得するように強制します。
> 配列の要素を表示する際も、Modelは事前に配列の全データを取得することを禁止しています。
> 代わりに、まず表示すべき1ページ目を要求し、その後にユーザーのスクロールに応じて追加ページのリクエストをしなくてはなりません。
> この制約によって、利用可能なデータの総量が予期せずに増加したとしても、実際にViewで表示するデータ量が制限されることで、クライアントサイドコードで性能をコントロールできるのです。

言っている内容にはもっとも感がありますが、コード書いてると結構イラっとするのも事実です。
イラっとした時はFalcor使えねぇと思わず、この話を思い出して心を落ち着けましょう。

### 値のset

この節の最後の例として、値をsetするコードを貼り付けておきます。`todo[1].completedAt` に日付を設定する例です。

```js:setの例
model.setValue('todos[1].completedAt', '2016-02-29')
    // setは以下も等価.
    // A: pathValueを渡す
    // .set({path: 'todos[1].completedAt', value: '2016-02-29'})
    // B: jsonEnvelopeを渡す
    // .set({json: {
    //     todoByid: {
    //         '0002': {
    //             completedAt: '2016-02-29'
    //         }
    //     }
    // }})
    .then(() => model.get('todos[1].completedAt')).then(out)
    .then(() => model.get('favorites[0]["completedAt", "text"]')).then(out)
;
```

ポイントはJSON Graphにより、`todo[1]`と`favorites[0]` が同じ参照を有しているため、`todo[1].completedAt` を変更するだけで`favorites[0].completedAt`も変更が反映されている部分ですね。

Falcor.Modelには、`get`/`getValue`と同様に`set`/`setValue`が用意されています。`setValue` は単一の値をセットする場合に、`set`は複数の値をセットする場合に利用できます。
上記は`set`の引数に1つの`{path: ..., value: ...}`を渡していますが、複数のペアを渡すことが出来ますし、`{json: ...}`に変更したいJSON Graphの部分集合(JSON Graph Envelope)を記述することもできます。

## まとめ

第2回目の今日は、以下について書きました。

* Falcorをクライアントサイドで利用する場合の環境構築方法
* Falcor.ModelのgetValue/get/setValue/setの使い方

次回はサーバサイド、falcor-routerについて書きたいと思います。