これからも Volt 推しでいきます。
「Volt を試してみたいけど、例やチュートリアルが少ない」という意見をしばしば聞きます。確かに、公式ドキュメントのチュートリアルはホントに触りだけで、モデルすらほとんど出てこない内容です。
そこで、モデル側の実装やユーザー機構にも少し触れることができる「Volt を使って10分でリアルタイムチャットアプリケーションを作る」というチュートリアルを公開します。「10分」というのはとりあえず何かこういうフレーズがあった方がキャッチーかな、と思って適当に付けただけなので、本当に10分でできる保証はどこにもないですすいません。
それと、「公開します」って言っても、実はこのチュートリアルは以下のスクリーンキャストの内容をほぼそのまま作っていますので、英語で全然 OK ってことなら、そっちを見てもらった方が動画なのでわかりやすいかもしれません。 Build a Realtime Chat App with Ruby and Volt
事前準備
このチュートリアルでは、事前に Volt と MongoDB をインストールしておく必要があります。Volt のバージョンは 0.9.4
を使用しています。
$ gem install volt
MongoDB のインストールに関しては割愛するので、もしインストールされていなければ、それぞれ自分の環境に合わせてインストールしてください。
プロジェクトの作成
まずは volt new
を実行して Volt アプリケーションのためのプロジェクトの雛形を作成します。名前は「volt-chat-example」とします。
$ volt new volt-chat-example
これを実行すると、まずプロジェクトの土台 (scaffold) が作られ、それから Bundler によって必要なライブラリがインストールされます。
インストールが完了したら、ログに出力されている内容にしたがって、一度サーバーを起動してみましょう。
$ cd volt-chat-example
$ bundle exec volt server
※ volt server
は省略して volt s
とすることもできます。
Web ブラウザーで http://localhost:3000/ にアクセスしてみると、すでにアプリケーションが起動できていることが確認できます。ただ、まだ中身はほとんど空っぽなので、これからチャットのための機能を追加していきます。
Message モデルの作成
まずは、Message
というデータモデルを作ります。これは、1つのチャットログのデータを表すもので、バックエンドの MongoDB にデータを永続化することにします。したがって、例えば Web ブラウザーで更新などの操作を行っても、保存されたデータはそのまま残ります。
モデルを作るには、volt g model モデル名
を実行してください。Rails を使ったことがあれば親しみやすいと思いますが、g
は generate
の略です。
$ bundle exec volt g model message
これで、Message
モデルの scaffold ファイルが生成されます。生成されたファイルを見てみます。
app/main/models/message.rb
class Message < Volt::Model
end
MongoDB はスキーマレスなデータベースなので、必ずしも事前にフィールドを定義しておく必要はありません。しかし、フィールドを定義することでバリデーションなどの機能を活用することができる利点があるため、ここではフィールドを明示的に定義することにします (ただ、今回のチュートリアルではバリデーションなどには触れません) 。
チャットのメッセージの本文を示すための、String
型の body
というフィールドを定義しましょう。
class Message < Volt::Model
field :body, String
end
それから、それぞれのメッセージのオーナー (発言者) に関する設定を行います。Volt には、特に設定をしなくても最初からユーザー機構が組み込まれていますので、それを利用しましょう。オーナーの設定には own_by_user
メソッドを使います。
class Message < Volt::Model
own_by_user
field :body, String
end
own_by_user
メソッドによって、オーナーのためのフィールドがモデルに追加されます。そして、ログインしたユーザーが Web ブラウザーからモデルを作成する操作を行ったとき、モデルにはそのユーザーが自動的に設定されます。
コントローラーの設定
次に、コントローラーの設定を行います。Scaffold によってデフォルトで生成されている MainController
があるので、それを編集します。
app/main/controllers/main_controller.rb
...
def index
# Add code for when the index view is loaded
end
...
デフォルトでは、この index
アクションが、利用者が Web サイトを訪問したときに最初に表示されるトップページのためのアクションです。その画面でチャット機能を利用できるように作っていきます。そのため、チャットを利用するためにはログインが必要となるように、require_login
の before_action
メソッドを追加します。
module Main
class MainController < Volt::ModelController
before_action :require_login, only: :index # <= 追加
def index
# Add code for when the index view is loaded
end
...
これは、index
アクションの実行にログインを要求するという意味の記述です。この宣言的な記法も、Rails を使ったことがあれば親しみのあるものだと思います。
では、試しにトップページにアクセスしてみましょう。期待通りに動いていれば、ログインページにリダイレクトされたと思います。当然そのままではログインできないので、「Signup here」からユーザーを登録してログインした状態で再度アクセスしてみてください。
ビューの作成
問題なくログインしてトップページにアクセスできたら、次はビューを作成します。
先ほどの MainController
の index
アクションがレンダリングするビューは、こちらも最初から Scaffolding で自動的に作られている app/main/views/main/index.html
です。これまでトップページには何度もアクセスしてちゃんとページが表示されているので、何もしていなくても存在しているのは当然といえば当然ですね。また、すでにここまでで気づいているかもしれませんが、ファイルの更新は自動的に検知されるため、サーバーの再起動や Web ブラウザーのリロードは不要です。便利。
<:Title>
Home
<:Body>
<h1>Home</h1>
デフォルトでは上記の状態であるこのビューファイルを、<:Body>
を差し変える形で以下のように編集します。
<:Title>
Home
<:Body>
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">Chat</div>
<div class="panel-body">
<form class="form-inline center" e-submit="send_message">
<div class="form-group">
<input type="text" class="form-control" placeholder="Enter a message" value="{{ page._form._body }}">
</div>
<input type="submit" class="btn btn-default"/>
</form>
<hr/>
<div>
{{ store._messages.each do |message| }}
<p>
<strong>{{ message.user.then(&:_name) }}</strong>
<span>{{ message._body }}</span>
</p>
{{ end }}
</div>
</div>
</div>
</div>
</div>
何やら一気に色々登場してきましたね。ちょっとだけ眺めてみます。
イベントバインディング
form
の属性に e-submit="send_message"
というものがあります。こういった e-*
の属性は Volt の「イベントバインディング」と呼ばれ、イベントに対してコントローラー側で定義したアクションを実行することができるようにするものです。この場合は、submit
イベントで send_message
メソッドが実行されるようになっています。ただし、まだこの send_message
メソッドは実装していないので、この時点では動きません。
Mustache テンプレート
Volt のビューでは {{ }}
の Mustache テンプレート形式で囲まれた場所のコードは、Ruby コードとして実行されます。その最も一般的な用途は「コンテンツバインディング」というもので、データをリアクティブにレンダリングするために利用されます。
もう少し詳しく
上記の1箇所を例として、もう少し詳しく見ていきましょう。
{{ page._form._body }}
というのが input
の値に指定されています。これは「双方向データバインディング」のために設定されています。これによって、input に対する変更がモデルに反映され、逆に、モデルへの変更が input に反映される、ということが可能になっています。ただ、このバインディングに関する実装もまだ行っていないので、現時点では動きません。
また、この page
というオブジェクトについても少し説明します。これは、Volt で定義されている「コレクション」の1つです。Volt には page
以外にも、データベースへの永続化をする store
や、ローカルストレージへ永続化する local_store
などの色々なコレクションがあり、用途に応じて利用できます。その中で page
コレクションというのは、一時的なデータの保持に使うためのもので、Web ブラウザーのリロードなどの操作を行うとデータが失われます。
volt console でモデルに触ってみる
Volt にはコンソールも用意されています。それを使って、少しモデル操作を行ってみましょう。
$ bundle exec volt console
コンソールが起動したら、以下を試してみましょう。
- Message モデルのインスタンスを生成
- body フィールドへのデータの設定
[1] volt(main)> message = Message.new
=> #<Message id: "5afd..5905">
[2] volt(main)> message.body
=> nil
[3] volt(main)> message.body = "Hello, world!"
=> "Hello, world!"
[4] volt(main)> message
=> #<Message id: "5afd..5905", body: "Hello, world!">
このように、ドット記法によって、フィールドとして指定していた body
へアクセスできることが確認できました。ところで、実際には Volt のモデルでは、前述した通り、事前にフィールドを定義していなくても任意のフィールド (属性) にアクセスすることが可能です。ただし、その場合には、「アンダースコア・アトリビュート」といって、必ず _
を前置した形でアクセスしないとエラーになってしまいます。
[5] volt(main)> message._something
=> nil
[6] volt(main)> message._something = "Something!"
=> "Something!"
[7] volt(main)> message
=> #<Message id: "5afd..5905", body: "Hello, world!", something: "Something!">
[8] volt(main)> message.something
NoMethodError: undefined method `something' for #<Message:0x007fd2ec0faf70>
チャット機能の作成
もう一度、{{ page._form._body }}
を見てみましょう。先ほど見た通り、これは page
コレクションの _form
という属性にアクセスしています。また、属性はネストして設定することが可能なので、更にその _body
という属性をバインディングしていることがわかります。
では、この _form
という属性に、用意した Message
モデルのインスタンスをアサインするようにコントローラーにコードを追加します。
app/main/controllers/main_controller.rb
# By default Volt generates this controller for your Main component
module Main
class MainController < Volt::ModelController
before_action :require_login, only: :index
def index
# Add code for when the index view is loaded
reset_message # << 追加
end
def about
# Add code for when the about view is loaded
end
private
# << 追加
def send_message
store._messages << page._form
reset_message
end
# << 追加
def reset_message
page._form = Message.new
end
# The main template contains a #template binding that shows another
# template. This is the path to that template. It may change based
# on the params._component, params._controller, and params._action values.
def main_path
"#{params._component || 'main'}/#{params._controller || 'main'}/#{params._action || 'index'}"
end
# Determine if the current nav component is the active one by looking
# at the first part of the url against the href attribute.
def active_tab?
url.path.split('/')[1] == attrs.href.split('/')[1]
end
end
end
まず、index
アクションの中で reset_message
を実行するように修正しています。この reset_message
も新たに追加したメソッドで、page._form
に Message
モデルのインスタンスを設定しています。ビュー側では page._form._body
としてバインディングが設定されているため、これが Message
モデル body
フィールドとのバインディングとして機能します。
もう1つ追加している send_message
というメソッドでは、page._form
を store._messages
に追加しています。store
というのはバックエンドの MongoDB を示したコレクションなので、この操作によってデータが永続化されます。そして、この send_message
はビュー側で e-submit="send_message"
としてイベントにバインディングされているので、このフォームを submit することによってデータが追加される、という機構になっています。
これで、簡単ではあるものの、リアルタイムなチャットアプリケーションが完成しました!複数の Web ブラウザで同時に接続して何か発言してみると、リアルタイムにチャットが更新されていくのが確認できると思います。