動機
- Rails で動的に要素を更新するページを作りたい
- 反 JavaScript 勢としては、JavaScript のコードをできるだけ書かずにすませたい
今回の解決策のメリット
- サーバサイドでレンダリングするので、View のテンプレートを利用できる
今回の解決策のデメリット
- データの転送量が多い
よいですよね、 Phoenix LiveView 。 同じようなことを Rails でもやってみたいと思い立ち、試行錯誤してみました。
app を用意する
サンプルとして、サーバサイドで時刻を更新しブラウザ上で刻々と表示を更新する clock app を作ります。
Rails のバージョンとしては 6.0 、標準で WebPacker を利用していることを前提にしています。
$ rails new clock $ cd clock
コントローラを追加する
ページを表示するためにコントローラを用意します。 更新は動的に行うのでコントローラのアクションは show のみ用意しています。
$ bin/rails g controlle clocks show
チャネルを追加する
更新する要素を輸送するチャネルを用意します。 クライアントからの update の要求を受け取れるようにイベントハンドラを用意しています。
$ bin/rails g channel clock update
JavaScript を編集する
本音としてはいろいろあっても、やはり JavaScript のコードをまったく書かずにすますわけにはいきませんでした。
と、いうわけで。 自動生成された JavaScript のコードを編集します。
app/javascript/packs/application.js
ページごとに .js ファイルの読み込みを制御したいので、チャネルの .js ファイルを一括で読み込んでいるコードをコメントアウトします。
require("@rails/ujs").start() require("turbolinks").start() // require("channels") コメントアウトする
app/javascript/channels/clock_channel.js
consumer.subscriptions.create
の行の先頭に export default
を追加してチャネルのオブジェクトを export します。
このファイルで定義されている関数を編集してカスタマイズするのが行儀のよい方法なのだと思います。 今回は、サーバから受信したデータをハンドリングする関数をこのオブジェクトの received にあとから代入するという方法をとります。
import consumer from "./consumer" export default consumer.subscriptions.create("ClockChannel", { connected() { // Called when the subscription is ready for use on the server }, disconnected() { // Called when the subscription has been terminated by the server }, received(data) { // Called when there's incoming data on the websocket for this channel }, update: function() { return this.perform('update'); } });
app/javascript/packs/clock.js
export されたチャネルのオブジェクトを import し、各イベントをハンドリングする .js ファイルを用意します。
import clockChannel from '../channels/clock_channel' window.addEventListener('DOMContentLoaded', () => { // ブラウザで update がクリックされたら、チャネルの update を呼び出す const buttonUpdate = document.getElementById('update') buttonUpdate.addEventListener('click', () => { clockChannel.update() }) // チャネルからデータを受け取ったら container に格納Gする const container = document.getElementById('container') clockChannel.received = (data) => { container.innerHTML = data.html } })
view を編集する
app/views/clocks/show.html.erb
update のイベントを発するボタン、動的に生成された要素を格納するコンテナを配置します。 また先に用意したチャネルの .js ファイルを読み込むタグを追加します。
<button id="update">UPDATE</button> <div id="container"></div> <%= javascript_pack_tag 'clock' %>
channel を編集する
app/channels/clock_channel.rb
update
メソッドの内容に tramsmit
メソッドでデータを送信するコードを追加します。
ここでは現在時刻を文字列に変換し div
で囲ったものを送っています。
class ClockChannel < ApplicationCable::Channel periodically :update, every: 1.second def subscribed # stream_from "some_channel" end def unsubscribed # Any cleanup needed when channel is unsubscribed end def update transmit(html: "<div>#{Time.current}</div>") end end
表示を確認する
Rails サーバを起動して、追加したページをブラウザで開きます。
ページを開くとボタンが一つ表示されています。 それをクリックすると、現在時刻が表示されるはずです。
更新する要素をテンプレートで書く
更新する要素を update メソッドに直書きするのではサーバサイドで更新するメリットがありません。 view と同じようにテンプレートを使ってレンダリングするようにしてみます。
テンプレートを用意する
app/views/clocks/clock.html.erb
まず一般の view と同じようにテンプレートを用意します。
<div><%= current_time %></div>
app/views/layouts/plain.html.erb
テンプレートのレンダリングではレイアウトが利用されます。 標準で用意されているレイアウトは HTML のヘッダなどが含まれています。 ページ内の要素をレンダリングするにはそれらは不要ですので、何も装飾しないレイアウトを用意します。
<%= yield %>
channel を編集する
app/channels/clock_channel.rb
ApplicationController.render
を使って用意したテンプレートでレンダリングします。
update メソッドを次のように変更します。
def update html = ApplicationController.render('clocks/clock.html', layout: 'plain.html', locals: {current_time: Time.current.to_s}) transmit(html: html) end
表示を確認する
再び Rails サーバを起動してページをブラウザで開きます。 編集前と変わらない表示ができています。
view と同じテンプレートを利用できるので Slim や Haml を利用することもできます。
定期的に表示を更新する
Action Cable には Periodically というメソッドが用意されています。 これを利用するとクライアントから update のイベントを送ることなく、サーバが自ら定期的に更新を実行できるようになります。
app/channels/clock_channel.rb
ClockChannel クラスに periodically :update, every: 1.second
の一行を追加します。
class ClockChannel < ApplicationCable::Channel periodically :update, every: 1.second # 以下略
表示を確認する
三度 Rails サーバを起動してページをブラウザで開きます。 今度は update ボタンを押さなくても秒ごとに表示が更新されることがわかります。
periodically を設定すると常に update メソッドが呼ばれていないかと気になりますが、チャネルは接続の状態( subscribed / unsubscribed )を把握しているので接続中のみメソッドを呼ぶようになっています。
いつか読むはずっと読まない:Life on Earth
博士は間違いなくわたしの科学好きに影響を与えたお一人です。

- 作者:デイヴィッド アッテンボロー
- 発売日: 2019/09/19
- メディア: 大型本