Rendr徒然

これは「Node.js Advent Calendar」7日目の記事です。

去年のアドカレ (東京Node学園祭用で12月じゃなかったけど) 以来、リリースの告知エントリ一つしか書いてなかったよ。ブログだけじゃなくTwitterもたいしてつぶやいてないし、ネットからのフェードアウトが進んでおりまする。あー、なにも残さず消えるように死ねたらいいのに。実際には今死ぬと膨大なゴミ (大半は書籍と雑誌だけど) を残すので早く処分しないと死ぬに死ねない。


ともあれ (JW)、今回はRendrについてダラダラと書きます。まとまりなくてごめんなさいです。
自分がRendrを知ったのは、@mshkさんによるQiitaの記事からでした。

同じく@mshkさんによる東京Node学園祭のRendr紹介セッションの資料はこちら。

Qiitaの最初の記事は結構注目を集めたようで、自分もそれを見て実際にRendrを触ってみて、これはいいと思い実戦投入するなどしたのですが、その後あまりRendrの情報が増えなくて残念な感じです。フェードアウト中のお前が言うなって感じですが。そんなわけで (どんなわけで?)、始めます。

RendrのMV*的な構造

前述のリンク先等で紹介されているように、RendrはBackboneをクライアントのみならずサーバでも動かすためのライブラリです。BackboneはいわゆるMVCフレームワークではあるものの、古典的なMVCそのものではなく、DOMイベントのハンドリングをViewで行う (ViewがControllerを兼務する) 一方、Controllerそのものに該当するものはありません。また、ViewはViewでどう作るかはアプリケーションの開発者に丸投げ感があります。Backboneは文字通り背骨だけ、骨格の一部だけを提供する薄いフレームワークなのですね。

Rendrはその薄いBackboneをそのままサーバでも動かすというわけではなく、いくつか追加の構造を与えます。具体的には、

  • Controllerがある。
  • レンダリングにはテンプレートエンジン (デフォルトはHandlebars) を使う。

Controllerがあるといっても、BackboneのViewで行うDOMイベントのハンドリングを担うものではありません。そうではなく、URLの変化、今時のブラウザだと popstate イベントに反応して、次に表示するためのModelを用意するのがRendrのControllerです。

図にするとこんな感じ (スマホとか画面の幅が狭いとまともに見えないかも、ごめんちゃい)。

+-----+  popstate  +------------+  create  +-------+
| BOM |----------->| Controller |--------->|       |
+-----+            +------------+          |       |
                                           |       |
+-----+   event    +------------+  update  |       |
|     |----------->|    View    |--------->| Model |
|     |            +------------+          |       |
| DOM |                                    |       |
|     |   render   +------------+   read   |       |
|     |<-----------|  Template  |<---------|       |
+-----+            +------------+          +-------+

Backboneでpopstateイベントを扱っているのはRouterですが、それは昔 (v0.5より前) Controllerという名前だったとか。RendrのControllerはRouterの先で呼び出される (というか、クライアントサイドではControllerの this はRouterですw) ので、先祖返りといった趣ですね。

URLにどのControllerが対応するかは、app/routes.jsで定義します。たとえばサンプルの場合はこんな感じ。

module.exports = function(match) {
  match('',                   'home#index');
  match('repos',              'repos#index');
  match('repos/:owner/:name', 'repos#show');
  match('users'       ,       'users#index');
  match('users/:login',       'users#show');
  match('users_lazy/:login',  'users#show_lazy');
};

まるでサーバサイドのWebフレームワークでコントローラ/アクションを定義しているみたいですね。実際、RendrのControllerはまさにサーバサイドのWebフレームワークにおけるControllerそのものです。なお、現在のRendrはモジュールシステムとしてCommonJSを使います (RequireJS対応のプルリクも進行中)。そして、その配置や命名に少しばかり規則があります。たとえば 'users#show' の場合は、app/controllers/users.js というモジュールで定義されているControllerの、show() メソッドが呼び出されます。また、それに対応するViewはapp/views/users/show.js、テンプレートはapp/templates/users/show.hbsのようになります。

サーバサイドで動く時のRendrはこんな感じになります。

+-----+   GET    +------------+  create  +-------+
|     |=========>| Controller |--------->|       |
|  B  |          +------------+          |       |
|  r  |                                  |       |
|  o  |          +------------+          |       |
|  w  |          |    View    |          | Model |
|  s  |          +------------+          |       |
|  e  |                                  |       |
|  r  |   HTML   +------------+   read   |       |
|     |<=========|  Template  |<---------|       |
+-----+          +------------+          +-------+

ControllerがサーバサイドWebフレームワークのControllerそのままだということが一目瞭然ですよね。この書き方だとViewが宙ぶらりんな感じですが、実際は使用するテンプレートを決めたり、テンプレートに渡すデータ (Model) を整えたりします。Viewのメソッドはそれらのようにクラサバ両方で実行されるもの (DOMに依存しない) と、クライアントでのみ実行されるもの (DOMに依存できる) があるので、@mashkさんの「Rendr入門(1)」やGitHubのドキュメントというかreadmeを見ておくといいでしょう。

テンプレートがサーバサイドで実行される場合は、その時のModelやViewをクライアントサイドで復元するための情報も一緒にレンダリングします。これによって、ブラウザではあたかも最初からそこで動いていたかのようにBackboneアプリが動作することができるのです。ただし、この部分 (と、Modelのキャッシュ) はシンプルなRendrの中ではちょっとばかりトリッキーなところで、トラブルの元になりやすい印象です。アプリ内で画面遷移 (Router.navigate()) した後はうまく動くのに、リロードするとうまく動かない、なんて場合はそこら辺ではまってる可能性があります。

なお、サーバサイドでBackboneが動く時というのは、(ブックマーク等から) URLが直接叩かれた場合や (F5等で) リロードされた場合などで、HTTPメソッドとしてはGETのみとなります。なので、app/routes.js で定義するのもGETメソッドでアクセスされるURLだけです。

RendrはUI層

RendrはBackboneをクライアントでもサーバでも実行するためのライブラリですが、ここでのサーバはサーバといっても、基本的にUI層です。つまり、Backboneを使ったシングルページアプリケーションの典型的なシステム構成がこんな感じだとして、

  client               server
==========         ==============

+---------+  HTTP  +------------+
| Browser |<======>| API server |
+---------+        +------------+

Rendrを導入するとこうなるということです。

  client                           server
==========         ======================================

                UI層                         サービス層
===================================        ==============

+---------+  HTTP  +--------------+  HTTP  +------------+
| Browser |<======>| Rendr server |<======>| API server |
+---------+        +--------------+        +------------+

Rendrは基本的に、バックエンドのサービス層でBackboneを使う (Backbone.Modelにビジネスロジックを実装するなど) ことを意図したライブラリではないということです。これは、Rendr開発元のAirbnbが元々Railsで実装されたサーバを持っていて、モバイル用のフロントエンドを構築する際、シングルページアプリケーションの初期表示を改善するためにRendrを開発したという経緯があるためでしょう。

Rendrを使うわけではなくても、このようにサービス層を小さく分割してAPI化し、UI層でマッシュアップする的なアーキテクチャは、特にユーザ数の多いサービスで頻繁に見かけますね。そのUI層として、複数のAPIを並行に呼び出すのが簡単といった理由でNode.jsが選ばれるケースも多く見かけるようになってきて、最近もWalmartとかGrouponとかがそれっぽい感じです。そんなアーキテクチャでシングルページアプリもクライアントの一つとして提供するなら、Rendrはピッタリとマッチするでしょう。

Rendrとバックエンドサービス

Rendrがバックエンドのサービスとどのようにコミュニケートするか、Backboneがサーバサイドで実行される場合とクライアントサイドで実行される場合、それぞれで見てみましょう。まずはサーバサイドの場合。

Browser                                 Rendr server                             Backend
=======          ======================================================        ===========

+-----+   GET    +------------+    +-------+    +------+    +---------+        +---------+
|     |=========>| Controller |--->|       |    |      |    |         |        |         |
|  B  |          +------------+    |       |    |      |    |         |        |         |
|  r  |                            |       |    |      |    |         |        |         |
|  o  |          +------------+    |       |    |      |    |         |  HTTP  |         |
|  w  |          |    View    |    | Model |<-->| Sync |<-->| Data    |<======>| Backend |
|  s  |          +------------+    |       |    |      |    | Adapter |        |         |
|  e  |                            |       |    |      |    |         |        |         |
|  r  |   HTML   +------------+    |       |    |      |    |         |        |         |
|     |<=========|  Template  |<---|       |    |      |    |         |        |         |
+-----+          +------------+    +-------+    +------+    +---------+        +---------+

BackboneのModelは fetch()save() が呼ばれると、Syncというオブジェクトを経由してバックエンドとModelの内容をやり取りします。Backbone標準ではXHR経由でRestfulなWeb APIとやり取りしやすいSyncの実装が提供されています。

RendrはこのSyncを置き換えます。サーバサイドでは、RendrのSyncはDataAdapterと呼ばれるオブジェクトを直接呼び出します。DataAdapterはサーバサイド版XHRといった感じのモジュールで、標準ではRequest (mikeal/request)を使ってRestfulなWeb APIとやり取りするためのRestAdapterという実装が用意されています。

Backboneがクライアントサイドで実行される場合はもう少し複雑になりますが、このようになります。

                     Browser                                     Rendr server                  Backend
==================================================        ===========================        ===========

+-----+    +------------+    +-------+    +------+        +----------+    +---------+        +---------+
| BOM |--->| Controller |--->|       |    |      |        |          |    |         |        |         |
+-----+    +------------+    |       |    |      |        |          |    |         |        |         |
                             |       |    |      |        |          |    |         |        |         |
+-----+    +------------+    |       |    |      |  HTTP  |          |    |         |  HTTP  |         |
|     |    |    View    |<-->| Model |<-->| Sync |<======>| ApiProxy |<-->| Data    |<======>| Backend |
|     |    +------------+    |       |    |      |        |          |    | Adapter |        |         |
| DOM |                      |       |    |      |        |          |    |         |        |         |
|     |    +------------+    |       |    |      |        |          |    |         |        |         |
|     |<---|  Template  |<---|       |    |      |        |          |    |         |        |         |
+-----+    +------------+    +-------+    +------+        +----------+    +---------+        +---------+

クライアントサイドでは、RendrのSyncは標準のBackbone.Sync経由でXHRを使ってサーバとやり取りします。その際、たとえばModelが要求したURLが /users だとすると、/api/-/users のように変形したURLをリクエストします。このようなURLは、Rendrサーバ (これはExpressアプリです) ではApiProxyというミドルウェアにルーティングされます。これは名前の通りAPI呼び出しのためのプロキシで、単純にDataAdapterに委譲するだけです。そしてDataAdapterがバックエンドのサービスとやり取りします。

重要なポイントとして、

  • ブラウザから直接バックエンドのサービスとやり取りすることはありません。Rendrサーバを介します。
  • Backboneアプリがブラウザ上で動作している場合、サーバ側ではBackboneアプリは呼び出されません。

前者に関しては好都合なことが多く、Rendrサーバ上でHTTPセッションを維持して認証などに使うことが出来たり、APIサーバとの通信に必要なAPIキーをクライアントサイドに露出することなくDataAdapterのところで設定出来たりします。

後者に関してですが、RendrはBackboneをクライアントサイドでもサーバサイドでも動かすためのライブラリなのですが、どちらかというと「クライアントサーバで」動かすと認識しておいた方が実際の動作に近いです。ブラウザ側でBackboneが動いてる時のHTTPリクエストではサーバ側でBackboneは動かず、サーバ側でBackboneが動く時はURL直叩きなのでブラウザ側ではBackboneは動いてないということなのです。

サーバサイドJavaScriptのメリットとしてクライアントとサーバでコードを共有できるという期待が語られたり、実際にはバリデーションくらいしか共有出来ないという現実が語られたりします。RendrだとあたかもBackboneのModel (通常バリデーションはここで行う) も含めてクラサバでの共有がより進みそうな印象を持つ人もいるかも知れません。実際、ModelもViewもControllerも (テンプレートも) クラサバで共有されるのですが、それはあくまでもレンダリングのためです。

特にバリデーションについては、サーバサイドでは基本的に実行されません。サーバサイドでBackboneが実行される時というのは、前述のようにURL直叩きやリロード時であり、GETメソッドによるリクエストの時だけです。その場合、通常のBackboneアプリでは Model.fetch() が呼ばれることはあっても Model.save() が呼ばれることはなく、従ってバリデーションが実行されることもありません (変わった実装をしていない限り)。

ちなみに、RendrではクライアントサイドのSyncはリクエストにemulateJSONを使います。つまり、ブラウザからRendrサーバへのリクエストではコンテントタイプは application/json ではなく、application/x-www-form-urlencoded になります。理由は不明ですが、Aribnbがサポートしているデバイスの都合でしょうか?

ふたたびRendrはUI層?

このように、Rendrというのは名前の通りサーバサイドでもレンダリングを行うためのライブラリです。そのため、Rendrの標準的な構成ではDataAdapter (RestAdapter) を通じてバックエンドのサービスを利用することになります。

しかし、冒頭でも紹介した「Rendr入門」の @mshk さんは、DataAdapterのところでMySQLへのアクセスを含む、いわゆるビジネスロジックを実装しているようです。AirbnbのようにRendr以前からバックエンドのサーバが存在している場合はともかく、全部新規のサービスだとわざわざHTTPアクセスを挟んでレイテンシを増やしてまでUI層とサービス層をわける理由がないということでしょう。しかし少なくとも現時点では、DataAdapterの先でデータベースアクセスを含むビジネスロジックを実装するのはRendrのユースケースから外れていて苦労しているようでした。たとえばDataAdapterでURLを見て自前でルーティング的なことをしないといけないなどなど。

また、RendrのGitHubリポジトリに早い時期からプルリクエストなどしていたアーリーアダプターの一人 @tak0303 (GitHubでは @takashi) さんも、同じように困っている模様。また、DataAdapterのところからデータベース (MongoDB) へアクセスしてもいるようです。MongoDBのようにHTTPでアクセス可能なDBはDataAdapter経由でアクセスすることも容易ですが、その場合はModelにビジネスロジックを実装することになるのでしょうか。通常のBackboneアプリ (ブラウザのみで実行される) から直接MongoDBにアクセスするような感じ? クライアントサイドはいたずらしやすいので認証・認可とかしっかりしていないと怖い気がしますが、DynamoDBなんかもブラウザから直接アクセスできるようになってきてるし (これとかこれ)、この方向もありなのかもしれませんね。

サーバサイドでこの先生きのこるには?

思い返せば、Node.jsは当初「サーバサイドJavaScript」として注目を集めたのでした。それから数年が経過し、今のNode.jsはサーバサイドではなく専らコマンドラインの世界で使われています。特にGruntの普及がめざましく、そのせいかnpmレジストリの負荷がこの一年で劇的に増加しているそうです。一方サーバサイドでは、主な用途として期待されたWebSocketの使い道が今ひとつ見つからず、かといって典型的な (RDBにアクセスしてHTMLを返す) Webアプリでは大きなメリットも見いだせず、といった状況でした。

そんな中、RendrはサーバサイドでNode.jsを使うメリットを鮮やかに見せてくれました。現在のRendrはBackboneべったりな作りですが、重要なのはBackboneかどうかではなく、同じフレームワークを使ってクライアントとサーバの両方でレンダリングするというアーキテクチャの方です。実際、Rendrの作者Spike Brehmのプレゼン資料「General Assembly Workshop: Advanced JavaScript」では、Backboneを使わずにRendr的「Isomorphic」なアーキテクチャを解説しています (チュートリアルもあり)。つまり、AngularJS版やEmber.js版のRendrがあってもいいわけですね。実現性は怪しいですが、Rendr自体にもBackbone依存を無くしてAngularJSなどへ対応しやすくするというIssueがあったりします。

Rendrや同じようにIsomorphicなアーキテクチャのクライアントサイドMV*なフレームワークが普及すれば、大規模なサービスにおけるサーバサイドのUI層として、あるいはより小規模な環境でのthinサーバとして、Node.jsが使われる機会が増えるかなと期待しています。が、シングルページアプリ自体が話題の割に今ひとつ広まらない (スマホもネイティブアプリ一辺倒になってるし) 印象もあり、果たしてサーバサイドJavaScriptとしてのNode.jsがこの先生きのこることができるのか、心配で夜も眠れない日々を過ごしています(日中はよく眠れてしまうので出勤時間的に非常にまずいです)。


明日はid:mizchiさんです。


P.S.

  • はてな記法を忘れてて書くのが大変だった。
  • Cacooで図を書き始めたけど必要以上に凝りそうになって今日中に終わらなくなりそうだったのでやめた。


P.P.S

他のアドカレをまねてRendrに送ったプルリクをまとめてみるよ。といっても4つしかないけど。

#111はこの中で一番役に立ったプルリクだと思う。当時のRendrはサーバサイドのControllerで複数のModel/Collectionを用意しても、レンダリング後にクライアント側で復元されるModel/Collectionは一つだけでした。#111はそれを修正して複数のModel/Collectionを復元出来るようにしたもの。マージされた際にはRendrの公式アカウントでツイートされて嬉しかったです。



#115はバックエンドからのHTTPステータスがブラウザに返されなくて (ステータス200になってしまう)、ステータス204などレスポンスボディがない場合にjQueryのXhrでエラー扱いされてしまうのを修正したもの。Rendrを実戦投入した案件ではステータス204を使うことになっていたので、マージしてもらえてよかったです。

#142はModel.fetch()でエラーとなった場合の詳細情報をContollerから取得しやすいようにしたもの。そこで別の (エラー用の) 画面に遷移するなどのハンドリングがしやすくなりました。

最後の#143はどうでもいい些細なものですね。Rendrの中の人 (Spike) は割とプルリクをマージしてくれるので (受け入れ過ぎじゃないかと思うくらい)、みんなもどんどんプルリクしたらいいと思うよ!!


最初にプルリクした当時はRendrのGitHub issueは静かなものだったのですが、最近は連日たくさんのIssueコメントが付いて、スターしてると飛んでくるメールも結構な量になります。といっても1日10通前後かな? Node.jsみたいにそれだけで毎日数十通ということはないけれど、結構な人気ぶりです。国内でももっとRendrの話題が盛り上がるといいですね。