Meteor.js

http://www.meteor.com/ で公開された Meteor.js を少し触ってみました。TechCrunch なんかでも話題になっていましたね。

Meteor.js は JavaScript によるウェブアプリケーションフレームワークですが、クライアントサイドでもサーバーサイドでもない、"Isomorphic" なフレームワークです。

コンセプトとしていくつか特徴があるのですが、その最たるものは "Reactive Programming" で、モデルやセッションなどのストレージを更新するとその更新内容がリアルタイムに、そのアプリケーションを開いている全クライアントに伝わるようなアプリケーションを簡単に作ることができます。

この辺は実例を見るのが早いです。

ここにある動画では、あるブラウザで Leaderboard を操作するとその内容がリアルタイムに他のブラウザで開いている画面にも反映されていますが、これが Reactive Programming による効果です。

"Reactive Programming" という概念はあまり聞き慣れない概念でしたが、特に Meteor がとか、このフレームワークのようにモデルの更新を publish / subscribe で伝えるとかそういうのがオリジンというわけではないようです。やさしいFunctional reactive programming(概要編) - maoeのブログ によれば

要するに、時間や外部の入力とともに変化する値や計算を、ユーザ自身がプログラムするのではなく、システム(言語自体やライブラリ)がユーザからは見えないところで反応(reactive)してくれるというものです。

とのこと。

Meteor.js の特徴あれやこれ

Meteor.js では、MongoDB に保存されたデータやセッションなどへの変更を、この Reactive Programming モデルで抽象化して提供していると言うのが、おそらくはそれっぽい説明ということになるでしょう。実際には WebSocket を利用しつつ pub/sub モデルで抽象化したようなかたちになっています。

Reactive Programming のコンセプトは開発環境にも踏襲されていて 「Meteor」は、JavaScript/HTMLで開発するリアルタイムWebアプリケーション基盤。何が起きているのかすぐに分からないほどすごい - Publickey でも解説されているとおり、JavaScript / HTML / CSS などのファイルを編集するとローカルで動かしている開発用サーバが自動でその変更を検知して、変更を反映してくれるようになっていたりもします。ブラウザのリロードは必要ありません。

そのほか、Meteor.js は node や MongoDB ほかが内蔵されていて一切外部のライブラリなどを必要なくアプリケーションが作れるとか、meteor.com がホストしているウェブアプリケーション環境にコマンドライン一発で deploy できるとか、独自のパッケージシステムを持っているとかいろいろ特徴があります。

テンプレートエンジンは Mustache にヘルパーなどが追加されてちょっと使いやすくなった Handlebars (http://handlebarsjs.com/)、モデルのストレージには MongoDB が利用されます。

特に MongoDB 回りの動きに特徴があります。Minimongo という MongoDB を操作するためのライブラリを利用してデータを操作するのですが、この Minimongo もやはり Isomorphic なライブラリになっています。つまり、

Bookmarks = new Meteor.Collection("bookmarks")
Bookmarks.insert url: "http://d.hatena.ne.jp/naoya", title: "naoyaのはてなダイアリー"

というコードを、クライアントサイドでもサーバサイドでも実行できる。サーバサイドで実行した場合は当然 MongoDB にデータが insert されるのですが、クライアントサイドで実行した場合は、クライアントにもっているローカルキャッシュにデータが insert されて、そのキャッシュへの変更は透過的にサーバサイドの MongoDB に反映されるようになっています。これにより、クライアントサイドでモデルを変更した内容が、全く意識しなくても永続化されるようになっていたりします。

という感じで Meteor.js は

  • クライアント、サーバーサイドを曖昧にするようなアーキテクチャ
  • フルスタックで
  • Reactive で
  • Handlebars と MongoDB で
  • コード量はわりと短くて済み
  • 開発は変更が自動で反映される、リロード要らず
  • ・・・な JavaScript によるウェブフレームワーク

であります。

サンプルで作ってみたもの

http://dl.dropbox.com/u/2586384/image/20120423_004515.png

この辺を実際に触ってみようということで、サンプルで以下のアプリケーションを作りました。簡単なソーシャルブックマーク的なものです。

好きなユーザ名を入力して URL そのほかを入力するとブックマークを追加できます。他のユーザーが同じ http://bookmark.meteor.com/ を開いていると、その更新はリアルタイムで反映されます。試しにブラウザで2枚開いてブックマークしてみると、動きがよくわかるでしょう。

ソースコードは以下です。

CoffeeScript と LESS を使って書きました。.coffee や .less なファイルは Rails でいうところの asset pipeline (でしたっけ?) のような仕組みにより、自分でコンパイルしなくても Meteor がよしなに変換してから配信してくれます。

Reactive Programming 的な、モデルを更新するとリアルタイムでビューが更新されるウェブアプリケーションを素で書こうとすると結構なコード量が必要になりそうなところ、https://github.com/naoya/MeteorSample-SBM/blob/master/bookmark.coffee を見ていただくと分かる通り、80行程度です。また、ファイルも CoffeeScript、HTML、LESS の 3ファイルのみです。

ちなみに URL routing 回りは Backbone.js を使って pjax です。

@Bookmarks = new Meteor.Collection("bookmarks")

@Bookmarks.validate = (bookmark) ->
  if not bookmark.url
    return false
  if not bookmark.url.match /https?:\/\//
    return false
  if not bookmark.user
    return false
  return true

if Meteor.is_client
  Template.main.user = () ->
    return Session.get "user"

  Template.main.events =
    'click h1' : () ->
      Router.navigate "", true
    'click span.user' : (e) ->
      Router.navigate "/" + $(e.target).html(), true

  Template.login.events =
    'click button' : () ->
      if user = $("#form-user").val()
        Session.set "user", user

  Template.entry.events =
    'click button' : () ->
      bookmark =
        user: Session.get "user"
        url:     $("#form-url").val()
        title:   $("#form-title").val()
        quote:   $("#form-quote").val()
        comment: $("#form-comment").val()
        posted_at: Date.now()

      if not Bookmarks.validate bookmark
        alert 'failed validation'
        return

      entry = Bookmarks.findOne user: bookmark.user, url: bookmark.url
      if entry
        Bookmarks.update { _id: entry._id }, { $set: title: bookmark.title, comment: bookmark.comment, quote: bookmark.quote }
      else
        Bookmarks.insert bookmark

      for elem in ['url', 'title', 'quote', 'comment']
        $("#form-#{elem}").val("")

  Template.bookmark.host = (url) ->
    return url.split("/")[2]

  Template.bookmarks.bookmarks = () ->
    user_filter = Session.get 'user_filter'
    selector = if user_filter then { user: user_filter } else {}
    return Bookmarks.find selector, { sort: { posted_at: -1 } }

  Template.bookmarks.events =
    'click span.navigate' : () ->
      Router.navigate "", true

  Template.bookmarks.user_filter = () ->
    return Session.get 'user_filter'

  BookmarkRouter = Backbone.Router.extend
    routes:
      "" : "timeline"
      ":user" : "bookmarks"
    timeline : () ->
      Session.set 'user_filter', null
    bookmarks : (user) ->
      Session.set 'user_filter', user

  Router = new BookmarkRouter

  Meteor.startup () ->
    Backbone.history.start pushState: true

if Meteor.is_server
  Meteor.startup () ->

ソースコードを斜め読みしても、特に publish / subscribe や websocket 的な特殊なコードは見当たらないでしょう。MongoDB から値を取り出してテンプレートにセットしているとか、クリックイベントを処理しているとか、その程度。にも関わらず、リアルタイムに動いてデータは永続化されます。この一見普通のプログラミングモデルで、リアルタイムアプリケーションになってしまうところが Meteor.js のコアコンセプトでしょう。

Meteor.js が中でなにをしているか

冒頭でつらつらと書いたような Meteor.js の中で何が行われているか、といったところは id:Jxck さんが先日のNode学園でコードリーディングの成果 (はやい・・・) を公開されています。参考になるでしょう。自分はまだ中身のコードまでは手をつけられていません。

使ってみての感想

・・・ とまあ、できたてほやほやの Meteor.js を適当にいじってみての現時点での感想を列挙してみましょう。まだ 100% 理解しているとは言い切れないのではありますが。

良いところ
  • ほとんど意識しなくてもリアルタイムアプリケーションが簡単に作れるのは楽しい。id:Jxck さんも書いていますが Reactive Programming の概念は、今後のアプリケーション開発において学ぶべきところは大きいでしょう
  • Reactive Programming をうまく発動させるために守らなければいけないコードの制約が、結果うまく MVC に適合するようになっていて、あまり意識しなくてもクライアントサイドMVCなコードになる。この辺、結構考えながら書いてないと MVC が崩れがちな Backbone.js なんかに比較しても、より手数も少なく自然とそのようなコードになるようになっていて、センスを感じる
  • フルスタックとは言え生成されるのは3ファイルのみ。しかもそれを書き換えるとブラウザにリロードなしで反映される。ちょっと JavaScript での動きを確認したいとか、Sandbox 的なアプリケーションを作りたいという時にとても便利です。(一方でファイルを分割したくてもその方法がまだない? ので大規模なものは現時点では難しそう)
  • meteor.com に特にアカウントなど作らなくてもアプリケーションが公開される、というのは使い道がないようで、今回のように試しに作ったものを人に見せたいというときに使える。何にでも手数が少なければ、便利になるところは出てくる良い例。
まだ微妙なところ
  • "できたばかり" という感じは否めない。たとえば Minimongo によるモデルには認証の仕組みが全くない。Minimongo はクライアントサイドで操作もできる上にそれがサーバサイドにも反映される。なのに認証がない = アプリケーションに保存された全データをクライアントでいじり放題、ブラウザの Console からでも操作できる。つまり、まだまともなデータベースアプリケーションを作れるフェーズではない (認証がない点については、ドキュメントにもそう書いている)。 認証だけでなく、そもそも Minimongo の機能がちょっと少ない。
  • Reactive Programming しようとすると、モデルの操作含めクライアントサイドにコードが集中していってサーバサイドではほとんど何もしないということが多い。それはそれで一見良いようで、そうするとせっかくバックエンドに node.js が控えているのに、node_modules をうまく活用できなかったりする。例えばモデルから find() で引っ張ってきたデータの加工に md5 な処理を加えよう・・・サーバサイドだったら md5.js を npm install して require すればいいところ、実はクライアントサイドにコードを書いているのでそういうわけにいかない。モジュール開発が活発な node という一番良いところがスポイルされてしまっている
  • 「しょうがないので md5.js とかインターネットで見つけてきて script タグで読み込ませるか・・・」と思っても、Dirty Hack なしでうまくそれを実現できる方法がない。そういう細かいところの作り込みがまだまだ。この辺は時間が解決するとは思うけれども。
  • Reactive な要件を満たすためにもフルスタックにしているんだろうというのはなんとなく想像できるのだけれど、たとえば npm ではないパッケージ管理システムを自前で持っていたり (その分、フレームワークによりモジュールを簡単に統合できるようになっているのだけど)、Minimongo しかり、さすがにいろいろ作りすぎている。この作り込み具合でまともにプロダクションで利用できるレベルを維持しようとすると、開発コミュニティが相当活発になる必要があってそこは結構リスクだと感じる

総合すると、この手軽さでリアルタイムアプリケーションがプロダクション品質で作れるようになったら、未来は明るい。でも Meteor.js の開発がそこまで順調に進むかどうかはちょっと心配 ─ まだ始まったばかりだけれど、というようなところです。

ま、しばらくは追いかけてみるとしましょう。