エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

ActionCable のクライアントも Ruby で書きたい(前篇)〜Opal覚え書き

Action Cable を使いたいときがあります。 Ruby on Rails の app を書いているのですから、サーバもクライアントも Ruby で書くのが自然です。

というわけで。Opal を導入してみました。

Rails app を用意する

rails new コマンドで適当な Rails app を作成します。 Action Cable は有効にしておきます。

webpacker を利用する方法も調べたのですが、いまひとつうまくいかなかったので今回は Sprockets を利用する方法を書きます。

Rails に Opal を追加する

opal-rails gem を使います。

詳細はドキュメントにありますのでそちらを確認してください。

Gemfile に gem 'opal-rails' を追加してインストールします。

config/initializers/assets.rb を編集してオプションを指定します。ひとまず README にある通りの内容を追加します。

Rails.application.config.opal.method_missing           = true
Rails.application.config.opal.optimized_operators      = true
Rails.application.config.opal.arity_check              = !Rails.env.production?
Rails.application.config.opal.const_missing            = true
Rails.application.config.opal.dynamic_require_severity = :ignore

app/assets/javascripts/application.jsapp/assets/javascripts/application.js.rb に rename して内容を編集します…というか、JS から Ruby への書き直しなので前者を削除して新しくファイルを作成すると考えた方がよいです。

  • Ruby の文法で書く
  • rails-ujsopal_ujs に変更する
  • 先頭に require 'opal' を追加する。
require 'opal'
require 'opal_ujs'
require 'turbolinks'
require_tree '.'

動作を確認します。 app/assets/javascripts/ に次のような .rb のファイルを追加して Rails app のサーバを起動します。

# app/assets/javascripts/hello.rb

puts 'Hello, Opal!'

正しく動作すればブラウザのコンソールに puts した文字列が出力されます。

Action Cable を使う

対比のために、まず Opal を利用しない版を書きます。

Action Cable の channel を追加する

rails g channel コマンドを利用して channel を追加します。

$ bin/rails g channel mumbler

サーバ側の app/channels/mumbler_channel.rb とクライアント側の app/assets/javascripts/channels/mumbler.js が追加されます。

それぞれ次のように編集します。

class MumblerChannel < ApplicationCable::Channel
  def subscribed
    stream_for 'any_channel'
  end

  def unsubscribed
  end

  def mumble(data)
    MumblerChannel.broadcast_to 'any_channel', {monologue: data['monologue']}
  end
end
function connected() {
}

function disconnected() {
}

function received(data) {
  const monologue = document.createElement('li')
  monologue.appendChild(document.createTextNode(data.monologue))
  const monologues = document.getElementById('monologues')
  monologues.prepend(monologue)
}

const handlers = { connected, disconnected, received }
App.mumbler = App.cable.subscriptions.create('MumblerChannel', handlers)

window.addEventListener('load', () => {
  const munbleButton = document.getElementById('mumbleButton')
  mumbleButton.addEventListener('click', () => {
    App.mumbler.perform('mumble', {monologue: document.getElementById('monologue').value})
  })
})

Action Cable を利用するページを追加する

上で書いた Action Cable を利用する monologues というページを追加します。

config/routes.rbmonologues のページのルーティングを追加します

  get 'monologues', to: 'monologues#index'

コントローラ app/controllers/monologues_controller.rb を追加します。

class MonologuesController < ApplicationController
  def index
  end
end

ビュー app/views/monologues/index.html.erb を追加します。

<h1>Monologues</h1>

<input type="text" id="monologue"></input>
<button type="submit" id="mumbleButton">MUMBULE</button>
<ul id="monologues"></ul>

Rails app サーバを起動します。 正しく動作すれば入力欄とボタンが表示され、テキストを入力してボタンを押すとそのテキストがリストに追加されます。

複数のウィンドウでこのページを開くとボタンを押すたびに同時にテキストが追加されることが確認できます。

f:id:E_Mattsan:20190330114206p:plain

Action Cable のクライアントを Ruby(Opal) で書き直す

ファイル名を app/assets/javascripts/channels/mumbler.js から app/assets/javascripts/channels/mumbler.js.rb に変更して、内容を次のように書き換えます。

Window = Native(`window`)
Document = Native(`document`)

handlers = {
  connected: -> () { },
  disconnected: -> () { },
  received: -> (data) {
    monologue = Document.createElement('li')
    Native(`monologue`).appendChild(Document.createTextNode(Native(`data`).monologue))
    monologues = Document.getElementById('monologues')
    monologues.prepend(monologue)
  }
}

mumbler = Native(`App`).cable.subscriptions.create('MumblerChannel', handlers)

Window.addEventListener('load', -> (_) {
  mumbleButton = Document.getElementById('mumbleButton')
  mumbleButton.addEventListener('click', ->(_) {
    mumbler.perform('mumble', {monologue: Document.getElementById('monologue').value})
  })
})

ページを読み込み直すと同じように動作することが確認できます。

Ruby でクライアントが書けました。

…。

これだけだとあまり嬉しくないかもしれませんが、これで Ruby(Opal) で書かれたフレームワークを利用することができるようになりました。

続く。

いつか読むはずっと読まない:暗黒通信団

PhoenixChannel のクライアントを Elixir で書いてみたい、という衝動もあります。

まぁそれは、 Elm を使おうかな…。

Erlangで言語処理系作成

Erlangで言語処理系作成