前々回のAngularJS、前回のEmber.jsときて、ラストはBackbone.jsを試してみたい。作成するリアルタイム掲示板は今までと同じ仕様。
目次
環境
- Ruby on Rails 4.1.6
- Backbone.js 1.1.2
- jquery 2.1.1
セットアップ
いつも通りGemfileから。またもやRails Assetsのお世話になる。
📄Gemfile
source 'https://rubygems.org' source "https://rails-assets.org" gem 'rails', '4.1.6' gem 'sqlite3' gem 'sass-rails', '~> 4.0.3' gem 'uglifier', '>= 1.3.0' gem 'coffee-rails', '~> 4.0.0' gem 'jbuilder', '~> 2.0' gem 'rails-assets-jquery' gem 'rails-assets-backbone' group :development do gem 'spring' end
Backbone.jsはjqueryに依存しているので、必ず指定すること。編集が終わったら言うまでもなくbundle install
を叩き込む。それからrails new
を実行し終わったら、application.js
を編集する。
📄app/assets/javascripts/application.js
//= require jquery //= require underscore //= require backbone //= require_tree ./backbonejs //= require board_sample
backboneはjquery, underscoreに依存しているので、上記の順で指定する必要がある。require_treeで指定しているbackbonejs
ディレクトリ、それからapp/assets/javascripts直下にboard_sample.js.coffee
というファイルを作成しておこう(中身は空で構わない)。
とりあえず何か画面を表示しよう。PagesControllerを生成する。
📄app/config/routes.rb
$ rails g controller pages home
続いて、ルーティングを設定をしてあげる。
Rails.application.routes.draw do root to: 'pages#home' end
さらには、レイアウトapplication.html.erb
も。turbolinksとか削除しておかねばならないし。
📄app/views/layouts/application.html.erb
<!DOCTYPE html> <html> <head> <title>Board sample</title> <%= stylesheet_link_tag 'application', media: 'all' %> <%= csrf_meta_tags %> </head> <body> <%= yield %> <%= javascript_include_tag 'application' %> </body> </html>
これでhttp://localhost:3000にアクセスすると、殺風景なページが表示されるはずだ。
コンソールでBackbone.VERSION
と入力してみよう。バージョン番号が返ってくれば正しくセットアップできている。
step1. 投稿を表示する
最初のステップとして、既存の投稿を画面に表示することから始めよう。兎にも角にもテーブルとモデルがないと始まらない。さっそく生成しよう。
📄db/seeds.rb
$ rails g model Post author body $ rake db:migrate
新規に投稿する機能を作成するまでは、データはdb/seeds.rb
であつらえることにする。
Post.create!(author: '名無しさん', body: 'これは本文です') Post.create!(author: '豊田サリー', body: 'テクマクマヤコンテクマクマヤコン') Post.create!(author: 'ガンジー', body: "非暴力\n非服従\n言葉は要らない")
もうちょっとカッコいいデータを投入してみるのも自由だ。気に入ったデータを定義したら、rake db:seed
で投入とシャレ込もう。
投稿データの取得はREST APIにてリクエストを送り、JSONで受け取ることになる。というわけで、さっそくAPIを作成するため ルーティングを設定する。
📄config/routes.rb
Rails.application.routes.draw do root to: 'pages#home' resources :posts, defaults: { format: 'json' }, only: %i(index show create) end
JSONを返すためにPostsControllerを作成する。
📄app/controllers/posts_controller.rb
class PostsController < ApplicationController respond_to :json def index respond_with Post.all end end
ここまででJSONが取得できるようになっているはずなので、http://localhost:3000/postsにアクセスしてみよう。
いい感じにJSONが取得できるようになったので、いよいよBackbone.jsで画面に表示できるようにしていこう。
まずはモデルの作成から。app/assets/javascripts/backbonejs/models
というディレクトリを作成して、その中にpost.js.coffeeというファイルを作成する。
📄app/assets/javascripts/backbonejs/models/post.js.coffee
@Post = Backbone.Model.extend urlRoot: '/posts' @Posts = Backbone.Collection.extend model: Post url: '/posts'
とりあえず、最低限の記述を施した。Backbone.Model
のextend関数でBackboneモデルを作成することができる。これはレコード1件に相当する。urlRootで指定しているのはRESTリソースのルートパスである点に注意。平たく言えばindexに相当するURLを指定すればよいということ。Backbone.Collection
はこのモデルを複数レコード抱えるコレクションで、model
オプションでモデルを指定する。
「上手く記述できていればいいんだけど」と心配性のあなたは、ブラウザのコンソールでnew Posts().fetch()
と叩いてみればよい。わらわらとデータが取得できることが分かるだろう。まだWebrickのログでも、/postsへのリクエストが来ていることが確認できるはずだ。
モデルのお次はビューに進もう。backboneとrailsを組み合わせる上で若干の設定が必要なのでまずはそれから。app/assets/javascripts/backbonejs/setting.js.coffeeファイルを作成する。
📄app/assets/javascripts/backbonejs/setting.js.coffee
# erbとの衝突を避けるため _.templateSettings = { interpolate : /\{\{=(.+?)\}\}/g, escape : /\{\{-(.+?)\}\}/g, evaluate: /\{\{(.+?)\}\}/g, }; # Ajax送信時にトークンを送信する(トークンがないとRails側で認証エラーになる) $ -> token = $('meta[name="csrf-token"]').attr('content') $.ajaxPrefilter (options, originalOptions, xhr) -> xhr.setRequestHeader('X-CSRF-Token', token)
erbの識別子<%%>がUnderscore.jsとかぶっているので、これを避けるために{{= }}, {{- }}, {{ }}に置き換える。また、AngularJS・Ember.jsのときと同様にトークンを送信するロジックを仕込んでおこう。
では、投稿を表示するビューhome.html
を作成していく。
📄app/views/pages/home.html
<div id="posts"> </div> <script type="text/template" id="post-template"> <span>{{- index }}</span> <span class="author">{{- post.author }}</span> <span class="time">{{- post.created_at }}</span> <div class="body">{{- post.body }}</div> </script>
実際に投稿を表示するのはdiv#postsで、その中にレンダリングするのが#post-templateの中身だ。これが投稿1件に相当する。scriptタグで囲んでいるので、このまま開いてもも画面には何も表示されない。Backbone.Viewを作成する必要がある。
📄app/assets/javascripts/backbonejs/views/post.js.coffee
# 投稿一覧 @PostsView = Backbone.View.extend el: '#posts' #DOM要素を割り当てる initialize: -> @render() #描画する @listenTo(@collection, 'add', @addNew) #コレクションに追加された場合のリスナー登録 addNew: (post) -> postView = new PostView(model: post) @$el.append(postView.render().el) render: -> @collection.each (post, i) => postView = new PostView(model: post, index: i) @$el.append(postView.render().el) @ # 投稿 @PostView = Backbone.View.extend tagName: 'div' #ラップするタグ名 className: 'post' template: _.template($('#post-template').html()) initialize: (@options) -> render: -> template = @template(post: @model.toJSON(), index: @options.index) @$el.html(template) @
えぇ〜、こんなに記述しないといけないのか・・・と思う方もいるだろう。Backboneが提供してくれるのは本当に骨格部分だけというイメージで、データとDOMのバインド→自動反映なんて気の利いたことは一切してくれない。自力で実装する必要がある。手間がかかるなと思うかもしれないが、ある意味漢らしいといえるかも。暗黙的な規約がないという点ではEmber.jsとは対照的で、処理の流れが可視化されている方が安心という方にはこの方がよいのかもしれない。
ソースが少し長いので、部分毎にポイントを説明しておく。PostsView
は投稿の一覧を表示するためのビュー。initialize
というインスタンスが生成されるときに呼び出される関数内で、自分がどのDOMに相当するのかを指定している(el: '#posts'
)。listenTo
はイベントリスナーを登録する関数で、’add’というイベントが発生した場合にaddNew
関数を呼び出すように設定している。@collectionってなんぞや?とお思いかもしれないが、これはnew PostsView(…)するときにパラメータで渡す予定のPostモデルの配列だ。addNewで投稿1件に相当するPostViewをnewしている。
PostViewは生成するごとに<div class="post"...</div>
というタグでラップしてほしいので、tagName
className
でそれぞれ値を指定している。template
には先程のhome.htmlで記述したテンプレートの中身を取得するようにしている。これも自力で書く必要があるのか・・・。
それではいよいよ画面を描画してやろうと思うので、board_sample.js.coffee
にコードを記述しよう。
📄app/assets/javascripts/board_sample.js.coffee
jQuery -> posts = new Posts() posts.fetch() new PostsView(collection: posts)
Postsコレクションモデルを生成し、fetch()でREST経由でデータを取得している。その後、一覧ビューを表示するためにコレクションを引数にビューをnewしちゃう。
うまく表示出来きた。少し体裁をよくしておこう。
📄app/assets/stylesheets/posts.css.scss
.post { margin: 10px 0; border-bottom: 1px solid; .author { background: lightyellow; padding: 3px; } .time { color: gray; margin-left: 10px; } .body { white-space: pre; margin: 5px 0; } }
とりあえず、全投稿を表示することができた。
step2. ヘルパーを作成する
表示されている投稿日時が人類に優しくない書式なので変更しよう。RailsならI18nとロケールで一撃だが、ここではそうはいかない。まずは日付周りの処理を便利にしてくれるmoment.jsを導入する。
📄Gemfile
gem 'rails-assets-moment'
📄app/assets/javascripts/application.js
//= require jquery //= require underscore //= require backbone //= require moment //= require_tree ./backbonejs //= require board_sample
日付の書式を変換するのはPostViewがよいだろう。Rubyで変換するほうが楽だといって、サーバ側でやるのはよろしくない。「データをどう見せるのか」はプレゼン側(=ビュー)の仕事なので。
📄app/assets/javascripts/backbonejs/views/post.js.coffee
# 投稿 @PostView = Backbone.View.extend ... render: -> template = @template author: @model.get('author') created_at: moment(@model.get('created_at')).format('YYYY-MM-DD HH:mm:ss') body: @model.get('body') index: @options.index @$el.html(template) @
テンプレートに渡す引数を変更した。というわけでhtmlも変更する必要がある。
📄app/views/pages/home.html
<div id="posts"> </div> <script type="text/template" id="post-template"> <span>{{- index }}</span> <span class="author">{{- author }}</span> <span class="time">{{- created_at }}</span> <div class="body">{{- body }}</div> </script>
やっとこさ、日付表示が人肌に温まった。
step3. 投稿できるようにする
いよいよ新しい投稿をポストできるようにしよう。まずは投稿フォームを作成する。
📄app/views/pages/home.html
<div id="posts"> </div> <form id="add-post"> <div> お名前: <input type="text" id="author" required /> </div> <div> 本文: <textarea id="body" required></textarea> </div> <input type="submit" value="投稿する" /> </form> <script type="text/template" id="post-template"> ...
なんの変哲もないformだ。こいつ対するBackboneビューを実装しよう。
📄app/assets/javascripts/backbonejs/views/post.js.coffee
... # 新規投稿 @AddPostView = Backbone.View.extend el: '#add-post' events: submit: 'submit' initialize: -> @initForm(new Post()) # 投稿フォームの初期化 initForm: (post) -> $('#author').val(post.get('author')) $('#body').val(null) # 登録 submit: (e) -> e.preventDefault() post = new Post(author: $('#author').val(), body: $('#body').val()) post.save() #DBに保存 @collection.add(post) @initForm(post)
events
オプションで、submitとかclickのようなDOMイベントに対する関数を割り当てることができる。submit関数内でPostモデルを生成し、save()でDBに保存する。jqueryで自力で値を取ってくる点がBackboneらしさ(?)。@collection
にaddした時点で以前リスナー登録したPostsView#addNewが走るようになっている。
投稿フォームの初期化の際に、Postモデルの初期値を使用するようにしているので、Postモデルを修正しよう。
📄app/assets/javascripts/backbonejs/models/post.js.coffee
@Post = Backbone.Model.extend urlRoot: '/posts' defaults: author: '名無しさん' ...
defaultsオプションでモデルの初期値を設定することができる。モデルの初期化からバリデートまではクライアント側で行い、Rails側では永続化のみを担当させるのがよさげだ。その永続化のためのPostsControllerに保存ロジックを入れてあげよう。
📄app/controllers/posts_controller.rb
class PostsController < ApplicationController ... def create respond_with Post.create!(post_params) end private def post_params params.require(:post).permit(:author, :body) end end
さあこれで完成と思いきや、先程作成したAddPostViewを生成する処理が必要だということを思い出した。
📄app/assets/javascripts/board_sample.js.coffee
jQuery -> posts = new Posts() posts.fetch() new PostsView(collection: posts) new AddPostView(collection: posts)
これで新しい投稿ができるはずだ。
データが登録できるようになると、いまだにテンションが上がる!
step4. 未読投稿を自動的に取得する
画面を開いているだけで自動的に投稿を追記していくように修正しよう。データ取得はポーリング方式で行うので、Postsコレクションに処理を追加することにする。
📄app/assets/javascripts/backbonejs/models/post.js.coffee
@Posts = Backbone.Collection.extend model: Post url: '/posts' startPolling: (@interval) -> @fetchInterval() fetchInterval: -> @fetch() setTimeout(_.bind(@fetchInterval, @), @interval)
fetch()を定期的に呼び出すようにしている。_.bind(@fetchInterval, @)
としているのは、普通にsetTimeoutを呼び出すとトップレベルコンテキストで関数が呼び出されてしまうため。
投稿した場合も未読分を取得しておかなければならない。自分が投稿文を書いている間に、他の誰かが投稿しているかもしれないので。というわけで、AddNewPostを修正する。
📄app/assets/javascripts/backbonejs/views/post.js.coffee
# 新規投稿 @AddPostView = Backbone.View.extend ... # 登録 submit: (e) -> e.preventDefault() post = new Post(author: $('#author').val(), body: $('#body').val()) @collection.fetch().then => post.save().then => @collection.add(post) @initForm(post)
@collection.fetch()の結果はPromiseなので、取得が完了してから自分の投稿を保存、追記するようにする。最後に、ポーリングを開始するのはもちろんboard_sample.js.coffeeだ。
📄app/assets/javascripts/board_sample.js.coffee
jQuery -> posts = new Posts() posts.startPolling(5 * 1000) new PostsView(collection: posts) new AddPostView(collection: posts)
5秒毎にデータを取得するようにした。ブラウザを2つ開いて互いに投稿してみよう。自動的に投稿が反映されるはずだ。
step5. アニメーションさせる
アニメーションというほど派手なことはしないが、投稿が追加されたことを知らせる最低限のハイライトを実装しよう。Jquery UIを使用するので、Gemfileを修正しよう。
📄Gemfile
gem 'rails-assets-jquery.ui'
もちろんapplication.jsも修正する。必ずjqueryより後ろに宣言すること。
📄app/assets/javascripts/application.js
//= require jquery //= require jquery.ui/ui/core //= require jquery.ui/ui/effect //= require jquery.ui/ui/effect-highlight //= require underscore ...
アニメーションをさせるのは、投稿ビューを追加するとき。すなわち、PostsView#addNewが相応しい。
📄app/assets/javascripts/backbonejs/views/post.js.coffee
# 投稿一覧 @PostsView = Backbone.View.extend ... addNew: (post) -> postView = new PostView(model: post) postView.render().$el .css("opacity", 0) .appendTo(@$el) .fadeTo(200, 1) .effect('highlight', {}, 500) ...
ブラウザをリロードして、最終確認しよう。
見事なピカリ! お疲れさま。
所感
- AngularJS, Ember.jsと違い、自分で実装しなければいけない部分が多い
- 暗黙の規約がキモいと感じる人にはうってつけ
- 往年のVisual Basic 6っぽさがある(html=デザインビュー、Backbone.View=フォーム.bas + イベントドリブン)
- 自由度が高いので、カオスなコードになりやすいかも?
- 始めるにあったてのハードルは低い
- Jquery, Underscore.jsの知識必須
AngularJSやEmber.jsに慣れている人にとっては「Backbone.js何にもしてくれね〜」って思うかもしれないが、その名の通り『背骨』なのでそれはそれで正しいのかもしれない。軽量な骨格を求めているならば、ベストチョイスなのかも。
AngularJS, Ember.js, Backbone.jsと試してきたが、個人でWebサービスを作るという観点から見ると、バランス、そして情報量の多さからいって、やはりAngularJSが無難なチョイスだと思った。Ember.jsはちょっと大げさな気がするし、暗黙的な決まりが多いのがちょっと。。Backbone.jsはシンプルだし依存関係も少ないので結構気に入ったけど、やはり自分で色々と実装するのが面倒か。
参考リンク
関連する記事
- Rails+JSフレームワークでリアルタイム掲示板を作成してみる(Ember.js編)
- Rails+JSフレームワークでリアルタイム掲示板を作成してみる(AngularJS編)
- 【Marionette】CompositeViewをCollectionViewで描画するサンプル
- パーシャルをrenderする際のパフォーマンスに関する注意点
- 【tubetop】YouTubeの国別人気動画を自動集約するサイトを作ってみた