なるべく怠惰にWebサービスを作りたい件[サーバ編1]

サーバ編から行きます。npm, Node.jsはインストール済とします。
とりあえずCoffeeScriptをインストールしましょう、

npm install -g coffee-script

フレームワーク

最初に、フレームワークExpress.ioを選定しました。

Node.jsのフレームワークといえば第一にExpressが来るのに加えて、リアルタイムWebをやりたいのでSocket.IOが必須です。
その上で面倒になる点が、セッション情報の共有です。Socket.IO自体はExpressと全く関係ないので、Expressのセッション情報をソケットにも紐づける必要があります。
これについてはJxckさんの解説記事が詳しく、これを正しくまねれば良いのですが、ちょっと大変です。

ですが、ここでExpress.ioを使えばそれだけでまるっと解決します。
Express.ioはExpressのラッパーですので、特に入れるのに苦労することはなく、
セッションの問題が解決します。
使い方はというと、Socket.IOのonメソッドがいい感じにラップされていて、

元はこんな感じ

socket.on 'fooEvent', (bar) ->
  console.log bar

だったのがこんな感じになる

app.io.route 'fooEvent', (req) ->
  # 何もしなくても req.session にセッションが入ってる(http通信のsessionと共通)
  # 送られてきたデータはreq.dataに入ってる
  bar = req.data
  console.log bar

はい、特に努力せずに大変怠惰になりました。ありがとうございます。
Express.ioのコードもCoffeeScriptで書かれてますが、ちょうど良いですね。
偉そうに書いといて、まだクライアントから呼んでみてすらいないので、本当にreq.sessionに入ってるかどうかは確認していませんが、
githubを見る限りスターが550ついてますから、信頼できると思います(?

1点付け加えるとすれば、expressのレスポンス返却はres.send(200, 'おk') や res.json(500, { msg: 'err!' })等色々用意されているのに対し、
socket.ioでクライアントに送るメソッドはemitメソッド1つしかありません(express.ioも基本ラップしてるだけですので特にありません)。
実際に使っていると次のように、エラーをemitするコードを書くことが増えると思うので

app.io.route 'fooEvent', (req) ->
  # 'error'イベントは予約されてるので使えない。ここでは'err'にしておく 
  if not req.data? then req.io.emit 'err', new Error('えらーです')
  anAsyncMethod req.data, (err) ->
    if err? then req.io.emit 'err', err
    # 以下略...

シンタックスシュガー(って言って良いのかな・・?)を用意すると少しだけスッキリするかなと。

# req.io.emit 'err', data のシンタックスシュガーとして
req.io.error data

$.ajax('get', foo)と$.get(foo)が同じように使える、みたいなイメージです。

しかし、express.io本体に手を入れなければいけないので、もし他にも色々手をいれたければforkするなりラップしたフレームワークを自作するなりでしょうが、
そうでなければ正直このためだけにいじるのは面倒です。

ルーティング (コントローラー)

express.ioの利用により、

# httpのルーティング (expressと同じ)
app.post '/url', callback
# socketのイベントハンドラ
app.io.route 'eventName', callback

と書けるようになり、統一感が出てよい感じになりました。
ここの統一感というのが結構大事だと思っていて、
このメソッドajax通信のつもりだったけどやっぱsocketにしようかな・・・みたいなときに、すんなりと変更できますよね。socket通信とajax通信を、同列に近く扱いたいのです。

しかし、ルーティングを全部app.coffeeに書くわけにはいかないので、うまい具合にばらけさせつつ、メンテが楽ですむ方法を探求したいところです。

expressの(http)ルーティングは、デフォルトだと以下のような感じです

# ## app.coffee (プログラムの起点)
routes = require('./routes')  # routes/index.coffeeがrequireされる
# urlマッピングはここで全部行う
app.get '/', routes.index
app.get '/foo', routes.foo.hoge

# ## routes/index.coffee
# routes以下のファイルでは、urlに対応する関数のみを記述する
exports.index = (req, res) ->
  # 略
exports.foo = require('./foo')

# ## routes/foo.coffee
exports.hoge = (req, res) ->
  # 略

これだとちょっと分かりにくいのですが、簡単にまとめると、

  • routes/以下に、コントローラーメソッドを書く
  • urlとコントローラーメソッドのマッピングは、一カ所で行う

という感じです。ルーティングを設定ファイルに切り出してるのに近いと思います。

この方法の良いところは、

  • 1. routes/以下に自由にファイルを作ってコントローラーメソッドを定義できる
  • 2. URLから、どのコントローラーメソッドが叩かれるのかを調べるときに確実に探せる

が上げられると思いますが、一方で、欠点は

  • 3. コントローラーメソッドを追加するときに、別ファイルでルーティングを設定しなければいけない
  • 4. コントローラーメソッドから、それがなんと言うURLで叩かれるのかを調べる(2の逆)のが面倒

というのが挙げられます。

なので、3や4を解決するために、コントローラーメソッドにannotationをくっつけられたりしたら個人的には最高だと思うわけです。

# ## app.coffeeにはurlとのマッピングを書く必要なし
# ## 以下のように、routes配下の各ファイルのみで完結する

# ## routes/foo.coffee

# @method GET
# @uri '/foo'
exports.hoge = (req, res) ->
  # 略

Javaなら言語にアノテーションがありますから、出来そうですね。jsでも試す価値があると思います。
ただ、これをするには構文解析をしなければいけないわけで、
このような言語仕様外のことをやってしまうのはさすがに危険であると判断しました。
CoffeeScriptはjsにコンパイルして実行されるので、コンパイル後のjsの状態から構文解析する、とかはさすがに厳しいので・・・。

というわけで、それに近い方法を考えます。
新しくurlとそれに対応するコントローラメソッドを追加するとき、ルーティングの1ファイルに追記することで完結すればOKです(その上である程度書き方がきれいでラクなのが望ましい)。
自分からルーティングに登録してもらいに行けば良いのではないでしょうか。

ということで、全体を以下のようにしました。

# ## app.coffee
# routing設定のトリガーを引く
require('./routing.coffee').route(app)
# ## routing.coffee

# ルーティングを行う関数 を返す関数 (appを保持する必要があるのでクロージャに)
getRouteFunc = (app) ->
    (method, uri, func) ->
        console.log 'route!', arguments
        switch method
            # ソケットの場合
            when 'socket', 'SOCKET'
                app.io.route uri, func

            # 以下、httpの場合
            when 'get', 'GET'
                app.get uri, func
            when 'post', 'POST'
                app.post uri, func

            else
                throw new Error("サポートしていないメソッドです: #{method}")

# 各routesファイルを全部呼び出す(その際、上で定義したルーティング関数を渡す)
module.exports = (app) ->
    # ルーティング関数を(appを包んで)取得
    routeFunc = getRouteFunc(app)

    # 各ルーティングを行うファイルを呼び出す
    filenames = fs.readdirSync './routes'
    for filename in filenames
        extname = path.extname filename # 拡張子の手前まで
        basename = path.basename filename # 拡張子
        continue if extname isnt '.coffee' or basename is 'index'
        
        require('./routes/' + basename)(routeFunc)
    return

ここまでが基盤。
あとは、新しくコントローラを実装する際、
route関数にHTTPメソッド(または'socket')・URI(またはイベント名)・コントローラーメソッドをセットで渡すだけとなります。

# ## 各ルーティングファイル
# ## routes/foo.coffee
# 引数でルーティング関数をもらう
module.exports = (route) ->
    # getだとこんな感じ
    route 'GET', '/foo', (req, res) ->
        res.send '200', 'おkkkk'

    # socketだと
    route 'socket', 'fooEvent', (req) ->
        req.io.emit 'afterFoo', { msg: 'foooooooooo!' }

※コードの流れが追いにくいのはひとえに僕の非力さによるもので、もっと分かりやすい方法は絶対にあると思うのですが、、

さて、こうした結果どうなったか見てみます。元の状態と比較すると、
まず長所だった次の2点に関して、

  • 1. routes/以下に自由にファイルを作ってコントローラーメソッドを定義できる
  • 2. URLから、どのコントローラーメソッドが叩かれるのかを調べるときに確実に探せる

1は問題ない。2に関しては、URLの定義が各ファイルに散らばるので分かりにくいという意見もあるかと思います。
私個人的には、探す時は

find ./routes -type f |xargs grep "/foo"

とでもすればすぐ見つかるはずなので、大して気になりません。
一方、短所だった

  • 3. コントローラーメソッドを追加するときに、別ファイルでルーティングを設定しなければいけない
  • 4. コントローラーメソッドから、それがなんと言うURLで叩かれるのかを調べる(2の逆)のが面倒

に関しては完全に解決されており、総合として満足という感触を受けます。

まとめ

個人で作るレベルでそこまで大きくないけど、Socket.ioを使ったWebサービスを怠惰に作りたいという希望のもと、
サーバサイドはexpress.ioを選定し、セッションの共有で詰まる必要がなくなりました。
さらにルーティングを少し工夫することで、メンテをらくちんにしました。

怠惰というかはわかりませんが、書きたいときに気分よく書けるのが大事ですから、基盤部分の設計は丁寧にやりたい。あれこれ考えて頭を使うしね。
結局、30分の単純作業を1時間で自動化した例のように、余計に時間がかかってるのは間違いないけど、それはそれなのです!

サーバはまた工夫しどころがあったら書きます。
次回はクライアント編です。明らかにこっちのが難易度高い。
四苦八苦してますが、もうちょっと頑張れば良いとこいけそうです。1週間以内に続き書きたい。