memorandums

日々の生活で問題解決したこと、知ってよかったことなどを自分が思い出すために記録しています。

Action Cableで特定のクライアントにプッシュ通知するテストプログラム

前置き

ついさっき書いた以下の続きです。

memorandums.hatenablog.com

Railsでプッシュ通知するにはやはりAction Cableを学ばないといかん。。。と諦めがつきました。

で、とりあえずググってみて実装してみたのが以下の記事でした。

【Rails6.0】ActionCableを使用したライブチャットアプリを実装する手順を解説|TechTechMedia

Action Cableといえば必ず出てくるのがDHHによるチャットアプリ実装です。その動画自体もかなり古いのですが。。。それくらい枯れた技術と思われます。

上記の記事はその動画を見なくても手順だけみれば作れるようにして、さらに解説を加えてくれている記事になります。とても貴重な記事と思うのですが。。。実はこれは手順自体が長くて。。。なんというか1つ1つが何のために実装しているのかわからなくなるように感じました。それでも1度は頑張って入力して動くところまで行ったんですが。。。さっぱりわからない。著者も3回実装してわかったと仰っているくらいですが、3回もやる若さは僕にはありませんでした。ごめんなさい。

Railsアプリの作り方の基本はわかっているという前提で、Action Cableを超シンプルにしたときにどこをどうすればいいのか?そこが(だけが)知りたいわけで、そういうことを解説したり試作している記事が見つけられませんでした。

朝の出勤電車で試しながら実装してみました。とりあえず、所望のアプリができそうなところまではわかりましたので、ここにまとめておこうと思います。ただし、何となく使えるだけで、本番系などではもっと別のスマートな実装(もしくはgem)があると思いますので、プロの方は無視してください。僕の欲しい機能のための僕の実装ですので。

作り方

とりあえず試しただけなので内部仕様の説明はできないのでとりあえず手順だけ示します。railsは5.2.8.1を使用します。

Railsプロジェクトを新規作成し、テスト用のコントローラとビューを自動生成します。

rails new push_notification --skip-coffee
cd push_notification
rails g controller rooms a b

app/controllers/rooms_controller.rb を編集します。用途はあとでまとめて説明します。

class RoomsController < ApplicationController
  def s
    ActionCable.server.broadcast 'room_channel', message: params['message']
    redirect_to root_path
  end
end

app/views/rooms/a.html.erb を編集します。

<input type="text" id="id">

app/views/rooms/b.html.erb を編集します。

<%= form_tag rooms_s_path, method: :post do %>
   <%= text_field_tag :message %>
   <%= submit_tag '送信' %>
<% end %>

以上のRailsファイルにアクセスできるようにルーティング(config/routes.rb )を定義します。

Rails.application.routes.draw do
  root to: 'rooms#a'
  post 'rooms/s'
  get 'rooms/b'
end

Action Cableを生成します。WebSocketでやりとりする通信路をチャネルと読んでいるらしく、上記の参考記事と同様にチャネル名をroomとします。speakはメソッド名ですが今回は特に使いません。

rails g channel room speak

app/channels/room_channel.rb を編集します。WebSocket通信のサーバ側になるようです。

class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    ActionCable.server.broadcast 'room_channel', message: data['message']
  end
end

app/assets/javascripts/channels/room.js を編集します。WebSocket通信のクライアント側になるようです。

App.room = App.cable.subscriptions.create("RoomChannel", {
  connected: function() {
    // Called when the subscription is ready for use on the server
  },

  disconnected: function() {
    // Called when the subscription has been terminated by the server
  },

  received: function(data) {
    let e = document.getElementById('id')
    if (data['message'].split(',')[0] == e.value) {
      return alert(data['message']);
    }
  },

  speak: function(message) {
    return this.perform('speak', {message: message});
  }
});

以上でとりあえず動作するはずです。説明の前に動作しているところをどうぞ。上のウィンドウがサーバ側のイメージです。サーバ側で何かイベントが発生したというイメージです。下の2つのウィンドウがクライアントで、それぞれにいれた数字がクライアントIDとしています。そのIDをもったウィンドウでサーバから送信されたメッセージが表示される感じです。これがやりたかった。。。ことです。


www.youtube.com

さて、上記の流れの説明になります。説明をダラダラ書いても僕自身面倒だし、できれば1枚に全体が詰まっているのが好きなので絵にしてみました。僕はこれの方がわかりやすいので。ご容赦を。①〜④の流れに沿って読んでもらえばRails知っている人ならわかるかと思います。WebSocketの利用経験もあったほうがいいですね。

以下、ダラダラ書いてしまいそうなので箇条書きで書いてみます。

  • 左右でわけました。左側はいわゆるRailsのモジュールです(Action CableももちろんRailsの一部ですが)。routes.rb、コントローラ、ビューです。これらは既知なので別に説明の必要はないかと思います。

  • 右側がAction Cableの関係者です。この部分が知りたかったことです。うん。雛形はrails g channelコマンドで生成できます。サーバとクライアントでファイルがわかれています。 WebSocketでは通信は双方向ですから、クライアント→サーバ、サーバ→クライアント、が記述できます。これが面白いですね。

  • クライアント→サーバは、この例ではroom.jsのspeakメソッド内のthis.performでspeakと書いているので、これを呼べば、サーバ側のroom_channel.rbのspeakメソッドが呼び出されるんだと思います。今回はこの機能は不要なので実装していません。チャットアプリではもちろん必要なのでここを利用しているようです。

  • サーバ→クライアントは、この例では(なんでもよかったんですが)b.html.erbのformでメッセージを入力して送信ボタンを押すと、RoomsControllerのsアクションが呼び出され、そこで大事なActionCable.server.broadcastを実行します。ブロードキャストする以外に特定のクライアントだけに送信する機能がもしかしてあるんじゃないかと思いますが、とりあえず今回は探すのが面倒なのでブロードキャストしてクライアント側で表示するかを選り分けるようにしました。このbroadcastを実行すると、どういう仕組になっているかわかりませんが、恐らくクライアントに送信され、room.jsのreceivedで定義した関数が呼び出されるんだと思います。そこであとはJSの世界で適当に処理しています。

やっぱりダラダラ書いてしまいましたが、知りたかったことをシュッとさせると以下になります。

  • Action Cableでサーバからクライアントに送信するには、まずrails g channelで雛形ファイルを生成し、

  • クライント側のjsファイル(今回はroom.js)のreceivedの関数をいじればよい。

  • サーバ側からクライアントに送信するときには、ActionCable.server.broadcast 'room_channel', message: data['message'] の一行を実行するだけでよく、そのときに、送信データはmessage: のあとに付ける感じでよい。

です。

あーすっきりした。わかってしまえばシンプルな実装でできそうな気がしてきました。はい。

では、こんなところで。おしまい。

あ、そうそう。以下、ソースです。

github.com