Falcor入門の第2回目です。
今日は、Falcorのクライアントサイドにおける基本的な触り方を見ていきたいと思います。
ちなみに過去回はこちら:
Hello world
なにはともあれ、まずはHello worldですよね。
第1回目で、Falcorの導入にはクライアント/サーバ双方の実装が必要である、と書きましたが、取りあえず触るだけであればクライアントのみの実装で充分です。
折角なので、環境構築手順も含めて書いていきます(Falcorに興味を持っている時点である程度のスキルが予想できそうなので、もっとサラッと流してもいいかも知れませんが...)
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とBrowserifyを利用します。
Falcorをブラウザから利用する際は、下記2つのnpm packageがあれば充分です.
続いて、コード本体を書いてみます。
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します:
babel -d built --presets es2015 src
browserify built/index.js -o built/bundle.js -d
bundleを読み込むhtmlファイルをindex.html
として用意します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="built/bundle.js"></script>
</body>
</html>
いざ、起動!
open index.html
開発者ツールを立ち上げると、以下の出力が得られていると思います:
Hello, world
{
"greeting": "Hello, world"
}
さて、駆け足でここまで来てしまったので、もういちどじっくり 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回 でも解説しましたがFalocrの主要概念JSON Graphの例です。
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
はいわゆるTODOリストの例を示しています。TODO全体を表すtodos
というリストと、お気に入りのTODO(そんなもんが世の中にあるのかはおいておき)を表すfavorites
というリストの2つを用意しました。
TODOの実体はtodoByid
という連想配列で表現し、それぞれのリストでは{$type: 'ref'}
という部分で、グラフの参照を確保しています。
getのパターン色々
さて、次のようにget
してみましょう。
model.get('todos[0].text').then(out);
JSON Graph中の$ref
が解決された上で、以下のような出力が得られるはずです。
Falcor.Model
からJSON Graphにアクセスする際は、「JSON Graphのどの部分が欲しいか」をPathという表現で記載します。偉そうにPathとか書きましたが、普段、JSONから値を取得するときのアクセス表現を考えれば良いだけです。
{
"todos": {
"0": {
"text": "牛乳を買う"
}
}
}
複数の値を一度に取得することも出来ます。以下の書き方は全て等価です。
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)
{
"todos": {
"0": {
"text": "牛乳を買う",
"completedAt": "2016-01-01"
}
}
}
配列から複数の要素を取り出だすことも出来ます。
model.get('todos[0..9, "length"]["text", "completedAt"]').then(out)
{
"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に書いてありますので、訳文を貼っておきます。
FalcorはViewへのデータ表示に最適化されています. 配列とオブジェクトは総量が制限されていないデータを含めることができます.
配列、オブジェクト全体をリクエストすることは、SQLの世界でWHERE
を付与せずにSELECT *
を実行するようなものです。
今は5つの要素を含む配列が、そのうち10,000個の要素を含むまでに成長します。
最初は高速に提供されていたリクエストが、データがストアされるにつれてViewの描画時間が遅くなっていくことを意味します。Modelは、開発者にどの値を抽出するべきか明示させることで、serverが安定したパフォーマンスを発揮できるようにします。
Modelは、オブジェクト全体を取得できるようにさせず、与えられたシナリオに応じて必要な値のみを取得するように強制します。
配列の要素を表示する際も、Modelは事前に配列の全データを取得することを禁止しています。
代わりに、まず表示すべき1ページ目を要求し、その後にユーザーのスクロールに応じて追加ページのリクエストをしなくてはなりません。
この制約によって、利用可能なデータの総量が予期せずに増加したとしても、実際にViewで表示するデータ量が制限されることで、クライアントサイドコードで性能をコントロールできるのです。
言っている内容にはもっとも感がありますが、コード書いてると結構イラっとするのも事実です。
イラっとした時はFalcor使えねぇと思わず、この話を思い出して心を落ち着けましょう。
値のset
この節の最後の例として、値をsetするコードを貼り付けておきます。todo[1].completedAt
に日付を設定する例です。
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について書きたいと思います。