12周年記念パーティ開催! 2024/5/10(金) 19:00

Falcorで実現する効率的なfetch 第1回 Falcorとは

FalcorはWeb APIに対して新しいアプローチを試みるJavaScriptライブラリです。クライアントとサーバー間のデータのやり取りを、これまでよりも少ないリクエスト回数で、そして最低限のデータ量で行えるようになります。それはなぜか? まず仕組みを解説します。

発行

著者 山田 順久 フロントエンド・エンジニア
Falcorで実現する効率的なfetch シリーズの記事一覧

はじめに

FalcorはNetflixによって開発されているJavaScriptライブラリです。このライブラリを導入することで、主にSPA(シングルページアプリケーション)形式のWebアプリケーションにおいて、クライアントとサーバーの間で行うデータのやり取りを、これまでよりも少ないリクエスト回数で、そして最低限のデータ量で行えるようになります。

導入には、クライアントサイドとサーバーサイド両方の対応が必要で、それぞれの環境用のライブラリはGitHubのNetflix Organizationで公開されています。

第1回となる今回は、使い方などの説明の前に、Falcorとは何なのかといった紹介をしたいと思います。Falcorを使ったコードは例として登場しますが、それらの細かい意味や実装パターンなどは後の回で触れていきますので、今回はとりあえず、こう書くとこうなるのだという理解だけで大丈夫です。

リクエストをもっと効率的に

まずはFalcorを使うことで、Web APIを通じたデータの取得がクライアントサイドから見て便利になる一例を紹介したいと思います。

あるニュースサイトに、人気記事の一覧を取得するREST APIがあったとします。これを使って人気記事のタイトルと要約文、著者の名前を一覧にしたいと思いました。このAPIにリクエストしてみたところ、返ってきたのは次のような、記事IDのコレクションでした。

GET /popular_entries

[
  8358179,
  5729496,
  1919208,
  ...
]

それぞれの記事のデータがほしかったので、個別の記事情報を取得するためのリクエストを行います。

GET /entries/8358179
GET /entries/5729496
GET /entries/1919208
GET ...

取得した記事情報は次のようになっていました。

{
  "title": "すごく人気な記事",
  "body": "すごく人気を呼ぶ本文",
  "author": 4542173
}

authorフィールドには著者のIDが入っていて、著者の情報もほしい場合はそちらのAPIに問い合わせる必要があるようです。これをひとつひとつの記事ごとに行います。

GET /authors/4542173

{
  "name": "すごく人気な著者",
  "body": "すごい経歴"
}

こうした流れで必要なデータは手に入りますが、個別の記事それぞれに対するリクエストと、リレーション先の著者情報のリクエストで、リクエスト回数がかさんでしまいます。また、それぞれのレスポンスには、記事のタグやカテゴリ、著者のその他の情報など、その場では必要としていないデータもあるかもしれません。

ですが、こうしたケースでも、Falcorを使えば一度のリクエストで指定したデータすべてを取得できるようにWeb APIを作ることができます。

たとえばFalcorを使って次のようなリクエストをすることで、人気記事のタイトル、要約文、著者名を10件分取得することができます。REST APIだと、その場では必要のないフィールド(今回の例では記事のカテゴリ、著者の誕生日など、サーバーサイドのバックエンドが持っているその他すべての情報)もレスポンスに含まれてしまうことがありますが、Falcorでは明示的に指定したフィールドの値のみを取得できます。

const falcor = require('falcor');
const HttpDataSource = require('falcor-http-datasource');

const model = new falcor.Model({
  source: new HttpDataSource('/model.json')
});

model.get(
  ['popularEntries', {length: 10}, ['title', 'summary']],
  ['popularEntries', {length: 10}, ['author'], ['name']]
).then((res) => {
  console.log(res);
  // {
  //   json: {
  //     popularEntries: [
  //       {
  //         title: "すごく人気な記事",
  //         summary: "すごく人気を呼ぶ説明文",
  //         author: {
  //           name: "すごく人気な著者"
  //         }
  //       },
  //       {
  //         title: "2番目に人気な記事",
  //         summary: "2番目に人気を呼ぶ説明文",
  //         author: {
  //           name: "すごく人気な著者"
  //         }
  //       },
  //       ...
  //     ]
  //   }
  // }
});

Falcorを扱う際、クライアント側ではfalcor.Modelインスタンスの操作を通じて、リモートに存在するデータの取得や更新を行います。RESTとは考え方が変わるので、RESTfulなAPIについて意識する必要はありません。この後で詳しく説明しますが、データは、すべてJSONの中にあるというイメージで操作を行います。

リクエストの際に考えること

もう少し単純な例でFalcorを使ったデータ取得の考え方を紹介していきます。

たとえばCodeGridにFalcorを導入したとして、ユーザーに対して記事のタイトルを数件分だけ表示したいといった場合には、このようなコードを書くと1度のリクエストで要求した値のみが返ってきます。

// Falcorで4件分の記事タイトルだけを取得する例
model.get(['entries', {length: 4}, ['title']]).then(res => {
  // do something...
});

このコードで.get()に渡されていた引数群はentriesというキーの中にある記事リソースを4件、そしてtitleフィールドだけ取ってきてほしい、という要求です。

取得できるのは次のようなデータです。サーバー側でもきちんと対応していれば、実際にこのような値が取得できるわけです。

// Falcorを使って受け取ったレスポンスデータの例
console.log(res);
// {
//   "json": {
//     "entries": {
//       "0": { "title": "どうなる? 今年のライブラリやAPI" },
//       "1": { "title": "縦書き実装の今" },
//       "2": { "title": "DevFest.Asiaとは" },
//       "3": { "title": "MMOゲームを作る" }
//     }
//   }
// }

Netflixでは「クライアントサイドのパフォーマンス追求のためにRESTではない何かを求めた結果、Falcorが作られた」というようなところがあると筆者は理解しており、RESTと比べて優れた、あるいは少し違った思想が反映されている作りがいくつか挙げられます。

たとえば、RESTでは/entriesというWeb上のリソースに対して、その一覧を取得するならGET、新規に作成するならPOSTのHTTPメソッドを投げる、/entries/1というリソースを取得したければこれに対してGETを投げる、更新したければPUTを投げるというふうに、Web上のリソースをどのように操作するのかという考え方をしていると思います。

一方、Falcorでは、無駄な通信を省くために、Web上のリソースの「何件目 OR 何件分の」「どのフィールドを」どのように操作するのか、くらいの明確さを意識させられる作りになっています。とりあえず/entriesが全部ほしい、であったり、/entries/1の全部のフィールドがほしいといった実装は、一応可能ですが、Falcorの設計思想上、それは推奨されていません。ですから、冒頭の例から{length: 4}['title']の指定を省いて、entriesをすべて取得するということはFalcorを使う上では避けたほうがよいこととなります。

次のような4件のentriesの持つフィールドすべてを取得したいというのはNGで、必要なフィールドを明示するべきです。

// Falcorの設計思想にそぐわないリクエストの例1
model.get(['entries', {length: 4}]).then(res => {
  // NG
});

次のようなentriesが全部ほしいというのもNGです。必要な件数を明示するべきでしょう。

// Falcorの設計思想にそぐわないリクエストの例2
model.get(['entries']).then(res => {
  // NG
});

面倒そうな制約ではありますが、いっそ受け入れて従うほうがFalcorの導入と実装を進める上での迷いはなくなりますし、無駄な通信を省くというFalcorのメリットをより多く受けられるのではないかと思います(どうしてもというときの回避策もありますので、後の回で紹介できたらと思っています)。

キャッシュも効率的に

冒頭の例で取得したデータを使って、CodeGridの記事一覧画面をユーザーのために表示したとします。そしてユーザーは1件目の記事を読みたいと思い選択しました。そこで、記事の詳細画面を表示するために、次のようなコードで本文や著者、公開日など他の情報もリクエストするとします(説明を簡単にするため、この例では本文だけ取得するということで進めます)。

// Falcorで1件目の記事タイトルと本文を取得する例
model.get(['entries', 0, ['title', 'body']]).then(res => {
  // do something...
});

実は、このときに実際に飛んでいるリクエストはentries['0'].bodyの分だけです。先のリクエストで取得済みのentries['0'].titleの値は、その時のキャッシュを利用していて、Falcorはローカルのキャッシュに存在しない差分だけを新たにリクエストします。

console.log(res);
// {
//   "json": {
//     "entries": {
//       "0": {
// キャッシュ -> "title": "どうなる? 今年のライブラリやAPI",
// 新規データ -> "body": "2016年のJS/CSS関連の技術は、どうなって……"
//       }
//     }
//   }
// }

今のリクエストでentries['0'].bodyの値もキャッシュできたので、以降のリクエストでもentries['0'].bodyが引っかかるクエリがあれば、今回のキャッシュが効いてくれます。

この次に以下のようなリクエストをした場合を考えてみましょう。

model.get(['entries', {length: 3}, ['title', 'body']])

次の6つの値を取得することになります。

entries['0'].title
entries['0'].body
entries['1'].title
entries['1'].body
entries['2'].title
entries['2'].body

このうちすでに、前の節で取得済みの各titleと、今、リクエストしたentries['0'].bodyはキャッシュされている値を利用するので、実際に飛ばされるリクエストと受け取るレスポンスはentries['1'].bodyentries['2'].bodyの分だけとなります。

entries['0'].title   キャッシュ
entries['0'].body    キャッシュ
entries['1'].title   キャッシュ
entries['1'].body    新規データ
entries['2'].title   キャッシュ
entries['2'].body    新規データ

Virtual JSON

ここまでに紹介した、JavaScriptのオブジェクトを探索するようなクエリが使えることや、取り回しの良いキャッシュが働いていることは、Falcorがクライアントサイドとサーバーサイドの通信を取り持ちながら、Virtual JSONオブジェクトをクライアント側に表現して見せることで実現しています。

次の画像はFalcorが作るVirtual JSONのイメージを図示したものです。

Falcorを使った開発では、バックエンドのリソースはすべて、Falcorが作り出すVirtual JSONの中に入っているというイメージを持ちながら進めます。なぜ「Virtual」かというと、実際にクライアントのメモリ上にそうしたJSONデータが、はじめからあるわけではなく、['entries', 0, ['title']]のような、JavaScriptライクなクエリをバックエンドでさばいて適切な値を返すことで、そのような構造を持つJSONが存在しているように見せかけているからです。

そしてリクエストのたびにクライアント側のVirtual JSONはその内部にキャッシュをためていきます。そして、次回以降のリクエストには、キャッシュに存在する分のデータを取り出して使用しています。

一方、次の図は、REST APIではリクエストのたびに新しいJSONを返していることを示したものです。

REST APIでは、たとえば/entriesで一覧表示用のデータを取得して、/entries/1で単体の詳細データを取得したりしますが、それぞれのリクエストで受け取るJSONは、リクエストのたび、異なる新しいデータとなっているので、重複した部分があっても再利用することはできません。

まとめ

今回はなるべく簡単にFalcorの良い所を理解してもらうために、コードも最小限の例にとどめ、細かな点には触れずに解説を進めました。今後の記事では具体的な実装例も交えた説明もしていきたいと思っています。

実際のところ、Falcorを導入するにあたって課題となりやすいのは、クライアントサイドとサーバーサイド両方の実装が必要な点ではないかと思います。導入に際して理想的な状況のひとつは、クライアント・サーバーサイドの両方をわかっている人員が、その両方を担当できる環境にいることです。また、Netflixが公開しているライブラリはJavaScript実装なので、サーバーサイドもNode.jsで、という条件が加わってきます。そういった点で敷居の高さがあるのは事実です。

また、REST形式のAPIとも比較する場面はありましたが、Falcorは既存の実装を丸ごと置き換えるものではなく、付け足しのできるものです。ですからREST APIとFalcor APIの両方を混在させても実装上においては特に問題ありません。プロジェクトの性質にもよるところだとは思いますが、いきなりFalcorで作り始めるよりは、まずは普通にREST APIが整備されている上で、そこからフロントのパフォーマンスを詰めるという場面で候補に挙がるものなのかなという気はしています。