node.jsで Isomorphic フレームワーク作ってみようとしたら超辛かった
現状どんな実装があるかはここみてください Isomorphic JavaScript - The future of web app development
主張
- Node.jsは現状フロントエンドユーティリティに最も適している
- Node.jsは一般にサーバーサイドでの運用は難しいし、自分もそう思う
- どうせやるなら、Node.jsにしかできないことをやるべきだ
- Node.jsのキラーアプリはRailsクローンではなく単体ソースからクライアント・サーバーのコードを同時に生成するIsomorphicフレームワークであるべきだ。
というわけで Isomorphicなフレームワークを自分で作ってみたいと思っていた。 今ならせめてConceptualな実装ぐらいならいけるのではないか、という着想があった。
着想
- React、renderComponent以外はサーバーでも動くし、データドリブンな設計すればギリギリまでサーバーで動いてそのレイヤーでテスタブルにできるんじゃないだろうか
- renderComponentの部分を専用のラッパで呼んでやれば完璧なラッパーでnodeで走らせてテスト出来て最高なのでは?
- クライアントではできないStorageが抽象化APIがあると嬉しいよな
- Storage抽象、Meteorぐらいしかみたことないし作ってみるか
API案
まずはDBを扱うStorageクラスだけ実装しようとしてみた。というかここがキモになる(と、最初は思っていたら最難関だった)。
class Todo extends ism.Storage endpoint: '/todos' schema: title: String user_id: String validate: ({title, user_id}) -> title.length > 1 and user_id
todo = new Todo title: 'hoge' todo.save() Todo.fetch().then (data) -> console.log data
クライアント・サーバーで両方共こんなAPIが動いてほしい。クライアントでバリデーションできるし、 サーバーでもバリデーションできると嬉しい。サーバーに投げて404の返事が来る前に弾かれるんならクライアントで叩きたい。
実装
- インターフェースだけ定義したクラス作って、クラサバで別実装を差しなおせばいいのでは
- クライアント側はcommon.jsでビルドして意識しないようにさせよう
- ライブラリはとりあえずKoa, Mongoose(mongo)で決め打ちする
大雑把に共通インターフェースを用意する。(以下適当にコード書くけどすごい雑なので真に受けないでください)
class ism.Storage endpoint: null schema: {} @extend: (obj) -> class extends ism.Storage _.extend @::, obj @fetch: -> throw 'should implement fetch' save: -> throw 'should implement save'
サーバー側(抜粋)
- ルーティング適当に決める
- DB操作の実装を用意する
ism.Storage.fetch = (query = {}) -> new Promise (done) => @model.find query, (err, content) -> done(content) ism.Storage::save = -> Promise (done) => params = {} for own k, v of @ then params[k] = v model = new @constructor.model params model.save (err, data) -> done(data) # Storage定義からAPIを定義する(koaのコード) ism.Storage.mount = (Storage) -> Model = mongoose.model Storage.name, Storage::schema Storage.model = Model # fetch route.get Storage::endpoint, -> query = @request.query ? {} @body = yield (cb) -> Storage.fetch(query).then (content) -> cb null, content # save route.post Storage::endpoint, -> params = @request.body @body = yield (cb) -> model = new Model params model.save (err, data) -> cb(null, {error: err})
クライアント側(抜粋)
- サーバーで用意したAPIを殴ってPromiseで待つ
ism.Storage.fetch = (params) -> new Promise (done, reject) => $.get(host+'/api'+@::endpoint).done (data) -> done data ism.Storage::save = -> new Promise (done, reject) => params = {} for own k, v of @ then params[k] = v $.post(host+'/api'+@endpoint, params).done (data) -> done(data)
これでサンプルコードがだましだまし動いた…んだけど
結果
考えが全然甘かった
- とにかくAPIデザインが難しい
- 下手なクラサバ抽象化は結果として使いにくくなる
- 一行書くにつれてもう片方で出来ないことが増えて、泥沼に入り込んでいく感じがある
- 同じ名前のAPIで別物、っていうのが思った以上に気を使うし実装する過程で発狂しそうになる
- セキュリティサンドボックスを厳密にするために、クライアントとサーバーの細かい部分をどう折り合いをつけるか難しい
- 大雑把なAPI提供してクライアントからやり放題になるの辛い
- とはいえ、なんか適当に作ったデフォルト実装で実際の使用時に満足するわけない
- 結局ブラウザのcommonjsとサーバー側でrequireできるライブラリが違うので意識しないのは無理
あとkoaだとyield必須なんだけどクライアントで動かないのでそこを気をつけたりしないとならなかった(これはライブラリ選択の問題で自分のせいだけど)
とにかく考えることが多くて、実際のユースケースを消化した上でよっぽどAPIセンスが良くないと設計できない。Meteorちょっと触った程度の自分はちょっと限界を感じて、諦めた。
仮コードはここにある https://github.com/mizchi-sandbox/ism
example以下がこうやりたい的なAPIなんだけどここまでたどり着ける気がしないので一旦pending
meteorの苦労がわかりました。