Hatena::ブログ(Diary)

日記

2013-12-07 (土)

[][][] Rendr徒然 22:00

これは「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があるといっても、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の話題が盛り上がるといいですね。

2013-10-19 (土)

[][] Seasar2.4.47 リリース 17:00

しました.

ソースコードリポジトリGitHubに移行しています。

■変更点

Seasar2.4.46 からの変更点は次のとおりです.

  • Bug
    • [CONTAINER-443] - [S2JDBC-Gen] TIME型のデータがdump-dataとload-dataで復元できない問題を解決しました
    • [CONTAINER-444] - [S2DBCP] コネクションプールへの返却処理中に物理コネクションのsetAutoCommitメソッドから例外がスローされると返却処理が適切に行われない問題に対応しました。
    • [CONTAINER-445] - [S2JDBC-Gen] Gen-Ddlタスクが生成するCREATE TABLE文のカラムの並び順がJava 6と7で異なる問題に対応しました
    • [CONTAINER-447] - [S2JDBC] Jenkins 1.500でテストが失敗するのを修正しました。[operation:2694]
    • [< href='https://github.com/seasarorg/seasar2/pull/3’>GH-3] - [S2Container] 正規表現として扱われる文字列内にあるピリオドをエスケープしました。
  • New Feature
    • [CONTAINER-446] - [S2JDBC] AutoSelect で取得するカラムを指定できるようにしました。[Seasar-user:21654]
    • [GH-1] - [S2-Tiger] DBの4桁のカラムに5桁のテストデータを投入している箇所を修正しました
    • [GH-2] - [S2JDBC-Gen] 開発途中で止まってずっと放置されている maven-s2jdbc-gen-plugin プロジェクトを削除しました

■移行の注意点 (今回はありません)


■ダウンロードはこちらからどうぞ.


Maven2からのご利用はこちらを参照ください.

2012-10-29 (月)

[][][] 文字ストリームと StringDecoder 21:00

この記事は「東京 Node 学園祭 2012 アドベントカレンダー」の 15 日目の記事です。

id:jovi0608 によるこのアドカレ 13日目のエントリ「Node API のクラス図を公開しました。」でも明らかなように、Node の重要なコンセプトの一つがストリームです。ストリームについては id:Jxck による東京 Node 学園の発表資料 「Node Academy 7 | ”Stream Stream Stream !!!”」も参考にどぞー。

そのストリームですが、大雑把には入力ストリーム (Readable Stream) と出力ストリーム (Writable Stream) があり、その両方でもあるフィルタストリーム (Filter Stream) や 双方向ストリーム (Duplex Stream) がある。。。 なんて話が上記の id:Jxck による資料には書いてあったりします。それとは別の観点での分類として、ストリームにはバイトストリームと文字ストリームがあると考えることができます。といっても、実装クラスとして分類できるというわけではありません。単に、ストリームを流れるデータがバイト列か文字列か、というだけです (他にも任意の JavaScript オブジェクトを流す、オブジェクトストリームなんかもありますが、このエントリでは扱いません)。

出力ストリームを文字ストリームとして扱うには、write() メソッドstring の値を渡します。同時にエンコーディングを渡すこともできて、デフォルトUTF-8 です。

os.write('hoge hoge', 'utf8');

出力ストリームの場合、write() のたびに Buffer を渡すことも文字列を渡すこともできます。つまり、バイトストリームか文字ストリームかという明確な状態は持たないということになります。

一方、入力ストリームを文字ストリームとして扱うには、setEncoding() メソッドエンコーディング方式を指定します。エンコーディング方式を省略した場合は UTF-8 になります。

is.setEncoding('utf8');

一度 setEncoding() を呼び出して文字ストリームとすると、それをバイトストリームに戻す API は提供されていません。引数無しで setEncoding() を呼び出すことでバイトストリームになるようにしようという提案 (#3643) がされていて、HTTP モジュールAPI ドキュメントには (間違って) そう書かれていたこともあるのですが、今のところマージされていません (つーか、ストリームはたくさんあるのに net.Socket だけ直すってどんだけー)。必要だと思う人は @isaacs の Streams2 が master にマージされる前後が大チャンスなので (たぶん)、プルリクエストするなり #3643 にコメントするなりしませう。

なお、全ての入力ストリームが文字ストリームになれるわけではありません。たとえば zlib モジュールは現在のところ文字入力ストリームに対応していません。ただし、現在開発中の Streams2 では入力ストリームのベースクラスが setEncoding() メソッドを持っているので、いずれは zlib も文字入力ストリームとして使えるようになると思われます。

このエントリではもっぱら文字入力ストリームについて書いていきます。

まず、なぜ文字ストリーム、とりわけ文字入力ストリームが必要なのでしょうか? それは、文字入力ストリームがないと不便で、バグの元になりやすいからです。たとえば、受信したバイト列を文字列に変換してログ出力する例:

is.on('data', function(buf) {
  console.log('受信データ : ' + buf.toString());
});

このコードは受信したデータが ASCII のように、一文字が 1 バイトにエンコードされたバイト列の場合はうまく動きます。しかし、UTF-8 のように複数バイトにエンコードされている場合、そしてそれが複数のチャンクに分割されている場合には、うまく動かない場合があります。たとえば 'あいう' という文字列は、UTF-8 では

E38182E38184E38186

という 9 バイトにエンコードされます。これが先頭から 4 バイトと残り 5 バイトの二つのチャンクに分割された場合、'い' を正しく文字列化することができません。

同じ状況を REPL で簡単に試すと:

> buf = new Buffer('あいう')
<Buffer e3 81 82 e3 81 84 e3 81 86>
> console.log('受信データ : ' + buf.slice(0, 4).toString())
受信データ : あ�
> console.log('受信データ : ' + buf.slice(4, 9).toString())
受信データ : ��う

このように、まるで文字化けしたような出力になってしまいます。Node の初期の実装 (0.1.95 まで) では、入力ストリームは上記のような実装になっていたため、UTF-8エンコーディングされた文字列を正しく扱うことができませんでした。

そこで必要になるのが文字ストリームで、そのために使われるのが string_decoder モジュールStringDecoder クラスというわけです。StringDecoder は、UTF-8 のように複数バイトでエンコーディングされたバイト列を受け取ると、それが文字の境界で分割されていないかチェックし、もし分割されて不完全なバイト列で終わっていると、その手前の (文字として完全な) バイト列までを文字列に変換します。そして残りのバイト列を覚えておいて、次に受信したバイト列と連結することで、正しく文字列に変換します。

REPL で簡単に試すと:

> decoder = new string_decoder.StringDecoder('utf8')
> console.log('受信データ : ' + decoder.write(buf.slice(0, 4)))
受信データ :あ
> console.log('受信データ : ' + decoder.write(buf.slice(4, 9)))
受信データ :いう

このように、最初の呼び出しで 4 バイトを渡しても、先頭の 3 バイトである 'あ' だけが文字列に変換されて、残りの 1 バイトは次の呼び出しで渡した 5 バイトと連結されて 'いう' に変換されたことがわかります。これにより、前述のコード片や

var data = '';
is.setEncoding('utf8');
is.on('data', function(str) {
  data += str;
});

のように受信した文字列を全部連結して、'end' イベントでまとめて扱うようなコードが UTF-8 でも安全に動作するようになりました。

StringDecoder は最初 Utf8Decoder という名前で、後のコアメンバーである Felix Geisendörfer によって実装されて 0.1.96 にマージされました。名前のように UTF-8 専用だったのですが、0.1.99 で現在の StringDecoder に変更されてその他のエンコーディングに対応しました。といっても、当時の Node が UTF-8 以外にサポートしていたエンコーディング方式は、ASCII ('ascii') と現在は deprecated ということになっている (でも無くさないでくれという要望が根強い) バイナリ ('binary') という、1 バイトへのエンコーディング方式だけだったのですが。

その後、v0.4.0 で Node は UCS-2 をサポートするようになりました。これもドイツの方の貢献によるのですが、そのパッチに含まれていたのは Buffer の対応だけで、StringDecoder は変更されませんでした。そのため、setEncoding('ucs2') はうまく動きませんでした。

v0.6.21 の REPL で試すと:

> buf = new Buffer('あいう', 'ucs2')
<Buffer 42 30 44 30 46 30>
> decoder = new string_decoder.StringDecoder('ucs2')
{ encoding: 'ucs2' }
> console.log(decoder.write(buf.slice(0, 3)))
あ
> console.log(decoder.write(buf.slice(3, 6)))
䘰

まともにデコードできません。しかし、少なくともデータ交換で使われるエンコーディング方式として現在は UTF-8デファクトであり、UCS-2 が使われることはほとんどなかったのでしょう。Node の issue でも要望されたことはなかったような気がします。

それから 1 年以上が経過して、v0.8.0 から Node はサロゲートペアを含む UTF-16 に対応するようになりました。現在の Node では、UCS-2 は UTF-16LE のエイリアスという扱いになっています。そのときにようやく、StringDecoderUCS-2/UTF-16LE に対応しました。需要があったのかどうかは不明ですが。。。

v0.8.14 の REPL で試すと:

> decoder = new string_decoder.StringDecoder('ucs2')
> console.log(decoder.write(buf.slice(0, 3)))
あ
> console.log(decoder.write(buf.slice(3, 6)))
いう

もちろん、UTF-16 に対応しているので、サロゲートペアの途中で途切れたチャンクでも大丈夫です。

> buf = new Buffer('お𠮟り', 'utf16le')
<Buffer 4a 30 42 d8 9f df 8a 30>
> decoder = new string_decoder.StringDecoder('utf16le')
> console.log(decoder.write(buf.slice(0, 4)))
お
> console.log(decoder.write(buf.slice(4, 8)))
𠮟り

ところで、UTF-8サロゲートペアはなく、BMP (基本多言語面U+0000U+FFFF) に含まれない文字は 4 バイトでエンコーディングすることになっているのですが、世の中には UTF-8 の文字は 3 バイトまでということに依存していたりするのか、UTF-16 におけるサロゲートペアの上位・下位の各 16bit をそれぞれ 3 バイト、計 6 バイトで表現する CESU-8 と呼ばれるエンコーディング方式もあるらしいです。Oracle とか Oracle とか Oracle で使われていると Wikipedia に書いてありました。UTF-8 としては RFC 違反なわけですが、サロゲートペアに対応していない UCS-2 -> UTF-8 変換ツールを使って UTF-16 を変換しちゃったりすると、CESU-8 なバイト列ができあがっちゃいます。んで、なぜか Node で利用されている JavaScript エンジン、V8 は CESU-8 なバイト列でも (内部形式である) UTF-16LE に変換してくれるので、Node の StringDecoder でも UTF-8 といいながら CESU-8 なバイト列が一文字 (最長 6バイト) の途中で分断されても大丈夫なようになっています。

> buf = new Buffer('eda182edbe9f', 'hex')
<Buffer ed a1 82 ed be 9f>
> decoder = new string_decoder.StringDecoder('utf8')
> console.log(decoder.write(buf.slice(0, 3)))

> console.log(decoder.write(buf.slice(3, 6)))
𠮟

いわゆる文字コードではないけれど、Buffer がサポートしているエンコーディング方式としては他に HEX ('hex') と BASE64 ('base64') があります。HEX は 1 バイト (たとえば 0x30) を 2文字 (たとえば '30') で表現するので、初期からの StringDecoder でも適切に扱うことができました。

しかし、3 バイトごとのデータを 4 文字で表現する BASE64 の場合は、適切にバイト境界を扱わないと正しくエンコード (文字コードの場合は文字をバイト列にエンコードして、それを文字列にデコードするのですが、BASE64 の場合はバイト列から文字列にする方向がエンコードなのだ) することができません。それだけなら UTF-8 などと同じようにすればいいだけなのですが、BASE64 ではさらにパディングというものがあり、一番最後に 3 バイト未満のデータが残った場合も考慮する必要があります。しかし、従来の StringDecoder にはメソッドwrite() しかなく、ストリームの終端で呼び出されるインタフェースがありませんでした。そのため、StringDecoder では BASE64 をサポートしていなかったのですが、最近リリースされたばかりの v0.9.3 からは StringDecoderend() メソッドが追加され (当然、net.Socket などの既存ストリームはそれを呼び出すように変更されています)、BASE64 にも対応しました。

v0.9.3 の REPL で試すと:

> buf = new Buffer('41424344454647', 'hex')
<Buffer 41 42 43 44 45 46 47>
> decoder = new string_decoder.StringDecoder('base64')
> console.log(decoder.write(buf.slice(0, 5)))
QUJD
> console.log(decoder.write(buf.slice(5, 7)))
REVG
> console.log(decoder.end())
Rw==
ここに至って、入力ストリームは Buffer でサポートされる全エンコーディングに対応したことになります。各安定版 + v0.9 (将来の v0.10) ごとの対応をまとめると、以下のようになります。
エンコーディングv0.2v0.4v0.6v0.8v0.9
ASCII ('ascii')
BINARY ('binary')
UTF-8 ('utf8')
CESU-8 ('utf8')---
UCS-2 ('ucs2')-××
UTF-16LE ('utf16le')---
HEX ('hex')--
BASE64 ('base64')××××
一方、出力ストリームについては未だに取扱注意なエンコーディングがあったりするのですが、それについては機会があれば二巡目にでも書くかもしれません。でもね、このアドカレに参加する人が増えて二巡目は回ってこないくらい盛り上がるといいな! 東京 Node 学園祭 2012 も近いしね。みんな参加してちょ!!!!