クライアントとサーバの両方で使える JS コードの書き方
追記
- 11/12/25
- Bi ってそんなに一般的ではない、 Both-Sides JavaScript の方が、ということでまた変更しました。(side でなく side's')
- 11/12/04
- Both Side JavaScript は変ということで、 BSJS=Bi-Side JavaScript に変更しました。
本文
CSJS と SSJS で両方同じ言語で処理が書けるメリットの 1 つとして、
書いた処理の共有があげられます。
(そこにメリットを感じない人もいるかも知れませんが。)
例えば
- Validater を共有
- クライアントの状態をサーバで再現
などがあります。前者はそのままですね。
受け取った入力のバリデーションはサーバでは必須で、フィードバックを速くするためにクライアントでも同じように行う場合があります。
今まではサーバで書いたバリデーションと同等のものを JS に直していたでしょう、サーバもJSならそのままです。
後者はゲームのチート対策などで、送られてくるメッセージからクライアントの状態をサーバで再現して、
ユーザからあり得ない情報が送られてないか(ズルしてないか)を確認したりできるそうです。
今までは別の言語で同じロジックを組んでいたところ、クライアントのロジックがそのままサーバで動けば少なからず嬉しいと思います。
他にもいくつかの場面で有効かもしれません。
今回はそんな Both-Sides JavaScript の書き方と、テストのコツを紹介します。
本文中のエイリアスは
- CSJS(Client Side JS)
- ブラウザで動くJS
- SSJS(Server Side JS)
- Node.js で動くJS
- BSJS(Both-Sides JS)
- その両方で動くJS
という前提で進めます。
題材としては、google-diff-match-patch - Diff, Match and Patch libraries for Plain Text - Google Project Hosting を使って一方がパッチを作って送り、
他方がパッチを受け取って文字列を更新する、といった用途のためのちょっとしたラッパー関数を作って、それを両サイドでテストします。
そのため、ここで作成する JavaScript は DOM に一切依存しないものとします。
基本方針
SSJS と CSJS では、それぞれスクリプトの読み込み方と変数空間が少し違います。
ブラウザではスクリプトはファイルごとの変数空間を持たないため、
自分より先に読み込まれたファイルの変数にアクセスできます。
一方 Node.js では、スクリプトはファイル毎の変数空間を持ち、
それを export/require を使って公開/読み込みする機能を持ちます。
BSJS を実現するには、この二つの差を吸収してやる必要があります。
export
では実際にモジュールを書いてみましょう。
まず、あるモジュールを以下のように書きます。
/** * diff_launch.js */ var dmp = new diff_match_patch(); function make_patch(old_text, new_text) { /* snip */ } function apply_patch(origin, patch) { /* snip */ }
このモジュールは二つの関数を持ち、これを外部に公開します。
公開する場合は、 node.js であれば module.exports などを用いるのですが、
module オブジェクトはブラウザにはありません。
しかし、そこで node.js では module.exports === exports === this であることを使います。
(実際は exports はグローバルではなく、モジュールごとのローカルです。詳細はドキュメントを参照。)
this['make_patch'] = make_patch; this['apply_patch'] = apply_patch;
こうすると、ブラウザの場合は window オブジェクトに対して関数を加えた形になり、
他のスクリプトファイルからも参照することが可能になります。
名前空間を切りたいなら、公開する前にオブジェクトのメンバにすれば良いでしょう。
require
先ほど作成した diff_launch.js は、 diff_match_patch.js に依存しているため、これを解決する必要があります。
ブラウザの場合は単に依存するスクリプトファイルが、自分よりも先に読み込まれていれば良いだけなので、そのように読み込みます。
<script src="diff_match_patch.js"></script> <script src="diff_launch.js"></script>
一方、 node.js の場合は require() で読み込んでやる必要があります。
しかし、標準では require() はブラウザには有りません。
そこでこれは、「ブラウザではない場合」を想定して以下のような場合分けをする必要があります。
/** * diff_launch.js */ if (typeof window === 'undefined') { var diff_match_patch = require('./diff_match_patch').diff_match_patch; }
見ての通り、 window オブジェクトが無いことを条件にしています。
しかし、これは「ブラウザ or Not」の条件分岐でしか有りませんので、
もしブラウザと Node.js 以外にも選択肢があり、それが Node.js の require と互換性が無い場合は、
適切な条件などを加える必要があります。
もしくは、全ての環境に互換性のある require() を用意するような方法が必要でしょう。
しかし、今の状況では上の方法が一番手をかけずに要件を満たすと思うので、この方針でいきます。
テストの共有
テストは、テスト対象のスクリプトとは別に用意し、
テスト対象のスクリプトを読み込んで、テストを実行するのが基本です。
BSJS なスクリプトをテストするためには、両環境で動くことを確認するため、テストも単一のものが両環境で実行できることが望ましいです。
これを実現するためには、先ほどの方針と同様、実行された環境に応じて、対象のスクリプトの読み込み方法を変えます。
また、テスティングフレームワーク自体も両環境の実行に対応したものが良いでしょう。
最近はこの用途でのテスティングフレームワークを探していました。
現状では、
などの選択肢があります。
これらのテスティングフレームワークの詳細な比較はまた別で書きたいと思うので、今回は省略します。
自分は今のところ、jasmine がこの用途で一番実装が充実し、使いやすいかと思っています。
今回は Jasmine を使い、BSJS のテストという視点で、
テストを書いてみます。
サーバでテストする場合
ターゲットを読み込み、それに対する検証を記述します。
jasmine-node はコマンドを提供するため、 jasmin-node を require する必要はありません。
/** * diff_launch.test.js */ if (typeof window === 'undefined') { // for jasmine-node var make_patch = require('../lib/diff_launch').make_patch; var apply_patch = require('../lib/diff_launch').apply_patch; }
検証は describe(), it(), expect() 等を用いて普通に書きます。
実行は jasmine-node コマンドを使います。
$ jasmine-node diff_launch.test.js Started ...... Finished in 0.007 seconds 3 tests, 6 assertions, 0 failures
クライアントでテストする場合
jasmine は本来はブラウザでのテストが前提(jasmine-node が後発という意味)なので、
専用の html を用意し、中で必要な CSS, JS を読み込みます。
- テスティングフレームワークのJS(jasmine.js, jasmine-html.js)
- テスト対象のテストが依存するJS(diff_match_patch.js)
- テスト対象のJS(diff_launch.js)
describe(), it(), expect() 等はそのまま使えます。
つまり、サーバのテストで使ったコードは全くそのまま使えるということです。
<link rel="stylesheet" type="text/css" href="lib/jasmine-1.0.2/jasmine.css"> <script type="text/javascript" src="lib/jasmine-1.0.2/jasmine.js"></script> <script type="text/javascript" src="lib/jasmine-1.0.2/jasmine-html.js"></script> <!-- include source files here... --> <script type="text/javascript" src="../public/javascripts/diff_match_patch.js"></script> <script type="text/javascript" src="../lib/diff_launch.js"></script> <!-- include spec files here... --> <script type="text/javascript" src="diff_launch.test.js"></script>
そして、html 用のスクリプトを少し足して完了。
<script type="text/javascript"> jasmine.getEnv().addReporter(new jasmine.TrivialReporter()); jasmine.getEnv().execute(); </script>
ブラウザで実行するとこんな感じ。
TODO: スクショ
どちらの環境でも同じようにスクリプトを読み込み、
テストを実行することができました。
isomorphic なコード
BothSideJavaScript であるということを積極的にメリットとしてとらえる動きはあり、
SocketStream でもそうした機能を持っています。
また、Web 全体で見ても従来のサーバサイド MVC のモデルでは、 View がかなり柔軟かつリッチなるため、
クライアントサイドに MVC を持ち込む Backbone.js のようなものもあります。
そして、このふたつをより密接に関連づけた上で、アプリケーション全体のアーキテクチャの再考を計った結果が
下記のエントリに詳しく書かれています。
Scaling Isomorphic Javascript Code — blog.nodejitsu.com
ここでは、BSJS は "isomorphic=同形" なコードと表現されています。
この考えをもとにした実装として Nodejitsu が発表したのが、
Flatiron です。
エントリは
Introducing Flatiron — blog.nodejitsu.com
Node.js がもつ「非同期/ノンブロック」や「シングルプロセス/クラスタ」などの機能や特徴以外に、
こうした BSJS であることをもっと有効に使った考え方も、今後大事になるかなと思います。
そして、 flatiron は玄人向けモジュールの老舗である Nodejitsu の集大成な感じで、面白そうです。