title-logo

Volt を使って10分でリアルタイムチャットアプリケーションを作るチュートリアル

August 03, 2015

これからも 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 を使ったことがあれば親しみやすいと思いますが、ggenerate の略です。

$ 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_loginbefore_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」からユーザーを登録してログインした状態で再度アクセスしてみてください。

ビューの作成

問題なくログインしてトップページにアクセスできたら、次はビューを作成します。

先ほどの MainControllerindex アクションがレンダリングするビューは、こちらも最初から 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

コンソールが起動したら、以下を試してみましょう。

  1. Message モデルのインスタンスを生成
  2. 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._formMessage モデルのインスタンスを設定しています。ビュー側では page._form._body としてバインディングが設定されているため、これが Message モデル body フィールドとのバインディングとして機能します。

もう1つ追加している send_message というメソッドでは、page._formstore._messages に追加しています。store というのはバックエンドの MongoDB を示したコレクションなので、この操作によってデータが永続化されます。そして、この send_message はビュー側で e-submit="send_message" としてイベントにバインディングされているので、このフォームを submit することによってデータが追加される、という機構になっています。

これで、簡単ではあるものの、リアルタイムなチャットアプリケーションが完成しました!複数の Web ブラウザで同時に接続して何か発言してみると、リアルタイムにチャットが更新されていくのが確認できると思います。