Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

JavaScriptMVCフレームワーク「Spine.js」を使ってみる

むらたです。
若手ではないのですが、こちらに出張投稿します。

AcroquestではHTML5+CSS3+JavaScriptによるグラフィカルなWebUIを開発するためのOSSであるWGPを開発しています。このWGPを利用する上で、JavaScriptMVCフレームワークを活用したいと考えており、私はJavaScriptMVCフレームワークを調査しています。そんなわけで「ステートフルJavaScript」を読んでおり、Spine.jsを調査しています。

Spine.jsはbackbone.jsにインスパイアされて開発されたCoffeeScriptと親和性が高いフレームワークであり、CoffeeScript好きな私としてはbackbone.jsの前にこちらを調べています。今回はSpine.jsのTodoアプリケーションを作ってみたので、その内容をまとめました。

Spine.jsのTodoアプリケーションを作る

Spine.jsではExampleのページでこのTodoアプリケーションが紹介されています。画面イメージはこちら。

Spine.jsにはSpine.appという生成機能があるのですが、本家のサイトで紹介されている例では、Spine.appを使わない方法(一番シンプルな方法)で説明されているので、ここではSpine.jsの売りでもあるSpine.appを使った開発方法を紹介します。

動作する本家のサンプルはこちらです。
LiveDemo

開発環境の準備

まずは開発環境を作ります。Spine.jsにて手順が説明されていますので、簡略版です。開発には、node.js、Spine.app、Hemが必要になります。node.jsのインストールについては省略しますw

npm install spine.app hem

gオプションでグローバルインストールするのが一般的ですが、私は嫌いなので、ローカルにインストールしてます。

spine.app、hemをインスト−ルしたら、アプリケーションのひな形を作成します。

./node_modules/spine.app/bin/spin app todos
	create	 todos/.npmignore
	create	 todos/app
	create	 todos/app/controllers
	create	 todos/app/controllers/.gitkeep
	create	 todos/app/index.coffee
	create	 todos/app/lib
	create	 todos/app/lib/setup.coffee
	create	 todos/app/models
	create	 todos/app/models/.gitkeep
	create	 todos/app/views
	create	 todos/app/views/.gitkeep
	create	 todos/css
	create	 todos/css/index.styl
	create	 todos/css/mixin.styl
	create	 todos/package.json
	create	 todos/Procfile
	create	 todos/public
	create	 todos/public/favicon.ico
	create	 todos/public/index.html
	create	 todos/slug.json
	create	 todos/test
	create	 todos/test/public
	create	 todos/test/public/index.html
	create	 todos/test/public/lib
	create	 todos/test/public/lib/jasmine.css
	create	 todos/test/public/lib/jasmine.html.js
	create	 todos/test/public/lib/jasmine.js
	create	 todos/test/specs
	create	 todos/test/specs/.gitkeep

cd todos
npm install .

npm installで必要なライブラリがインストールされます。

npm install jquery.tmpl --save

今回はjquery.tmplを使うので、こちらもインストールします。これで開発環境の準備は完了です。

Modelの作成

それでは、Spine.appを使ってModelを作成し、そのコードを書きます。

../node_modules/spine.app/bin/spine model task

これでModelのひな形が app/models/task.coffee として生成されます。このひな形にコードを書いていきます。

app/models/task.coffee
Spine = require('spine')

class Task extends Spine.Model
  @configure 'Task', "name", "done"

  @extend @Local

  @active: ->
    @select ( (item) -> !item.done )

  @done: ->
    @select ( (item) -> !!item.done )

  @destroyDone: ->
    rec.destroy() for rec in @done()

module.exports = Task

Spine.jsでは何も指定しないとデータはメモリ上に保存されますが、ここでは@Localを使ってHTML5のローカルストレージを使います。Spine.Modelのselect関数はコールバックを引数に取るのですが、本家のサンプルの書き方では一目では意味が分からなかったので、分かり易さのためにカッコをつけてます。(CoffeeScript的にはなくてもよい)

Controllerの作成

次にControllerを作成します。Modelと同じくSpine.appを使ってひな形を生成します。

../node_modules/spin.app/bin/spin controller tasks

Controllerは app/controllers/tasks.coffee として生成されます。このひな形にまたコードを書いて行きます。

app/controllers/tasks.coffee
Spine = require('spine')
Task = require('models/task')
$ = Spine.$

class Tasks extends Spine.Controller

  constructor: ->
    super
    @item.bind("update", @render)
    @item.bind("destroy", @release)

  events:
    "change input[type=checkbox]": "toggle"
    "click .destroy": "remove"
    "dblclick .view": "edit"
    "keypress input[type=text]": "blurOnEnter"
    "blur input[type=text]": "close"

  elements:
    "input[type=text]": "input"

  render: =>
    @replace($("#taskTemplate").tmpl(@item))
    @

  toggle: ->
    @item.done = !@item.done
    @item.save()

  remove: ->
    @item.destroy()

  edit: ->
    @el.addClass("editing")
    @input.focus()

  blurOnEnter: (e) ->
    if e.keyCode is 13 then e.target.blur()

  close: ->
    @el.removeClass("editing")
    @item.updateAttributes({name: @input.val()})

module.exports = Tasks

このControllerはTodoリストのViewに当たる部分をコントロールしています。Todoリストの一行を表現するテンプレートはjquery.tmplを使っているのですが、テンプレート部は後述するindex.htmlに記述しています。

次に、Todoアプリケーション全体の中で、Todoリスト部分以外のViewをコントロールするもう一つのControllerを作成します。これはアプリケーション全体も含めてTaskAppクラスとして作成します。このコードはSpine.appがアプリケーションのひな形を生成したときに作成される app/index.coffee に記述します。

app/index.coffee
require('lib/setup')

Spine = require('spine')
Task = require('models/task')
Tasks = require('controllers/tasks')

class TaskApp extends Spine.Controller
  constructor: ->
    super
    Task.bind("create", @addOne)
    Task.bind("refresh", @addAll)
    Task.bind("refresh change", @renderCount)
    Task.fetch()

  events:
    "submit form": "create"
    "click .clear": "clear"

  elements:
    ".items": "items"
    ".countVal": "count"
    ".clear": "clearlink"
    "form input": "input"

  addOne: (task) =>
    view = new Tasks(item: task)
    @items.append(view.render().el)

  addAll: =>
    Task.each(@addOne)

  create: (e) ->
    e.preventDefault()
    Task.create(name: @input.val())
    @input.val("")

  clear: ->
    Task.destroyDone()

  renderCount: =>
    active = Task.active().length
    @count.text(active)

    inactive = Task.done().length
    if inactive
      @clearlink.show()
    else
      @clearlink.hide()

module.exports = TaskApp

参考にしている本家のサンプルに実はバグがあり、上記はそれを修正しています。(どこか分かりますか?)このバグを見つけるのに時間がかかってしまいましたよ(TT

Viewを作成する

今回のアプリケーションは非常にシンプルなものなので、Viewは最初に表示する public/index.html だけです。先ほど書いたように、この中にTodoリストのView部分となるテンプレートをscriptタグで書いています。

public/index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset=utf-8>
  <title>Todos</title>
  <link rel="stylesheet" href="/application.css" type="text/css" charset="utf-8">
  <script src="/application.js" type="text/javascript" charset="utf-8"></script>
  <script type="text/x-jquery-tmpl" id="taskTemplate">
    <div class="item {{if done}}done{{/if}}">
      <div class="view" title="Double click to edit...">
        <input type="checkbox" {{if done}}checked="checked"{{/if}}>
        <span>${name}</span> <a class="destroy"></a>
      </div>

      <div class="edit">
        <input type="text" value="${name}">
      </div>
    </div>
  </script>

  <script type="text/javascript" charset="utf-8">
    var jQuery  = require("jqueryify");
    var exports = this;
    jQuery(function(){
      var TaskApp = require("index");
      exports.app = new TaskApp({el: $("#tasks")});
    });
  </script>
</head>
<body>
  <div id="views">
    <div id="tasks">
      <h1>Todos</h1>

      <form>
        <input type="text" placeholder="What needs to be done?">
      </form>

      <div class="items"></div>

      <footer>
        <a class="clear">Clear completed</a>
        <div class="count"><span class="countVal"></span> left</div>
      </footer>
    </div>
  </div>
</body>
</html>

サンプルではHTML5の機能である chache.manifest を指定して、アプリケーションをキャッシュ化していますが、デバッグ時に面倒なので削除しています。

作成するものは以上です。

Build & 動作確認

Spine.appで生成したアプリケーションはHemを使っています。このHemはライブラリマネージャとしての機能を持っており、ライブラリの依存関係を解決してくれます。index.htmlを見ると分かるように、本家のサンプルに記載している数々のscriptタグによるライブラリの指定は、ここでは記載していません。これはHemによってビルドを行うと、今回開発したjavascript(CoffeeScript)と依存関係のあるjavascriptを全てapplication.jsという一つのjavascriptファイルに結合します。なので、index.htmlにはapplication.jsのscriptタグしかありません。

実はcssについても同様のことができるのですが、今回は本家のサンプルからapplication.cssをコピーしてきてそれを利用します。(これはHemによるビルドの後にやらないとapplication.cssがビルドの度に生成されるのでご注意を)

おっと、忘れてました。jquery.tmplを個別にinstallしているので、Hemの定義ファイルである slug.json に jquery.tmpl を追加する必要があります。

{
  "dependencies": [
    "es5-shimify",
    "json2ify",
    "jqueryify",
    "jquery.tmpl",
    "spine",
    "spine/lib/local",
    "spine/lib/ajax",
    "spine/lib/route",
    "spine/lib/manager"
  ],
  "libs": []
}

では、ビルドをしましょう。

./node_modules/hem/bin/hem build

これで全ての準備が完了です。public/application.js、application.cssが生成されます。動作の確認はhemに組み込まれているサーバ機能を使って試します。

./node_modules/hem/bin/hem server

これで localhost:9294 にアクセスすれば試すことができます。
実際にStep by Stepで試してみてください。
それでは!

補足編を書きました。
JavaScriptMVCフレームワーク「Spine.js」を使ってみる(補足編) - Taste of Tech Topics