Action CableでWebSocketを実現するときにconfig.action_cable.urlは必ずしも設定しなくて良い #Rails - Qiita

Rails で Action Cable の config.action_cable.url を設定しなくても動いたのだけども…

Rails8 から Solid Cable が標準搭載されるようになりましたね。

それによって、Redisを使用しなくてもリアルタイム機能の実装がグッと楽になりました! これによって WebSocket を使ったリアルタイム性のあるユーザー体験を少し楽に提供できます。

ところで、Action Cable の設定を見ていると config/environments/*.rb ファイルに config.action_cable.url という項目があります。これは WebSocket サーバーの URL を設定するもののようですが…

「あれ?これ設定しなくても Action Cable 動いてるぞ?」
「仮で入れていたURLなのに Action Cable 動いてるぞ?」

なんて思ったことはありませんか?(あまりないかもしれません)

実は、クライアント側(Vue, React など)で WebSocket の接続先 URL をちゃんと指定していれば、config.action_cable.url は多くの場合、設定不要です。

この記事では、その理由を Rails の情報も交えながら、少し掘り下げて解説していきます。

そもそも config.action_cable.url さんのお仕事は?

まず、この設定項目が本来何のために用意されているのかを見てみましょう。Railsガイドによると、モノリシックな実装を行う際にフロントエンドへaction_cableの値を伝えるためのaction_cable_meta_tag で使われることが説明されています。

action_cable_meta_tag は、クライアントサイド JavaScript が接続先を知るための タグを HTML に出力するビューヘルパーです。

 name="action-cable-url" content="ws://localhost:3000/cable">

では、このヘルパーは config.action_cable.url をどのように使うのでしょうか?

実際の Rails のソースコード (ActionCable::Helpers::ActionCableHelper) を確認すると、このヘルパーは以下のように動作します。

  1. まずアプリケーション設定の config.action_cable.url を参照します
  2. もし config.action_cable.url が設定されていなければ、config.action_cable.mount_path (デフォルトは /cable) に基づいて URL を自動生成します

rails/actioncable/lib/action_cable/helpers/action_cable_helper.rb

def action_cable_meta_tag
  tag "meta", name: "action-cable-url", content: (
    ActionCable.server.config.url ||
    ActionCable.server.config.mount_path ||
    raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
   )
end

このように、config.action_cable.url は、主に action_cable_meta_tag ヘルパーが URL を決定する際に(優先的に)参照する設定、という位置づけになります。

Frontend も Rails で完結させるアプリケーション構成(いわゆるモノリス構成)では、ページに埋め込まれた JavaScript がこのメタタグを読み取って、「あ、WebSocket サーバーはここね!」と接続先を知る、という使い方をしていました。このメタタグの contentconfig.action_cable.url の値が使われるわけです(設定されていなければ、デフォルトの /cable パスが使われます)。

この時点で既にあれ?API modeの時はじゃあ使わなくていいのでは?となると思いますが、その通りです。

なぜ設定なしでも動くのか? API mode × モダンフロントエンドのような場合

では、なぜ config.action_cable.url を設定しなくても、Vue や React を使ったアプリケーションで Action Cable が動くのでしょうか? 答えはクライアント側の実装にあります。

  1. クライアントライブラリ @rails/actioncable

Vue や React などのフロントエンドフレームワークから Action Cable を使う場合、通常 npmyarn@rails/actioncable という JavaScript ライブラリをインストールします。

npm install @rails/actioncable
# or
yarn add @rails/actioncable
# etc...
  1. createConsumer() で接続先を直接指定

このライブラリの中心的な機能の一つが createConsumer() 関数です。これを使って WebSocket サーバーへの接続(Consumer)を作成するのですが、この関数には接続先の WebSocket URL を直接引数として渡せます

Rails ガイドにもしっかり書かれています。

4.1.1. Connect Consumer

// app/javascript/channels/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `bin/rails >generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()

This will ready a consumer that’ll connect against /cable on your server by default. >The connection won’t be established until you’ve also specified at least one >subscription you’re interested in having.

The consumer can optionally take an argument that specifies the URL to connect to. >This can be a string or a function that returns a string that will be called when the >WebSocket is opened.

// Specify a different URL to connect to
createConsumer('wss://example.com/cable')
// Or when using websockets over HTTP
createConsumer('https://ws.example.com/cable')

// Use a function to dynamically generate the URL
createConsumer(getWebSocketURL)

function getWebSocketURL() {
 const token = localStorage.get('auth-token')
 return `wss://example.com/cable?token=${token}`
}

つまり、Vue や React のコードの中で、

import { createConsumer } from "@rails/actioncable"

// .env ファイルなどから URL を取得するのがベスト
const wsUrl = 'ws://localhost:3000/cable'
const consumer = createConsumer(wsUrl)

// あとは consumer を使って Channel をsubscribe...

のように、接続先の URL を JavaScript 側で明示的に指定してしまえば、Rails 側の config.action_cable.url 設定や action_cable_meta_tag ヘルパーは全く必要なくなるわけです。クライアント自身が接続先を知っているので、サーバーに教えてもらう必要がないんですね。

サーバー内部のブロードキャストは URL を見ていない

「でも、サーバー側で ActionCable.server.broadcast するときに URL が必要じゃないの?」という疑問も湧くかもしれません。

答えは「通常は不要」です。

 
Rails アプリケーション内部(モデルのコールバック、コントローラーのアクション、ジョブなど)からブロードキャストを行う場合、Action Cable は config/cable.yml で設定された Pub/Sub アダプタ (async, redis, postgresql, solid_cable など) を介してメッセージを配信します。

今回は私がSolid Cableを使用しているので、solid_cableです

# config/cable.yml (例)
development:
  adapter:  solid_cable

production:
  adapter: solid_cable
  polling_interval: 0.1.seconds
  message_retention: 1.day

asyncsolid_cable なら同じプロセスやDB内で、redispostgresql ならRedisサーバーや、LISTEN / NOTIFY という非同期通知メカニズムを利用して、「このチャンネルを購読している人たちにこのデータを送って!」という指示が伝わります。この仕組みにおいて、外部からアクセス可能な WebSocket の URL (config.action_cable.url で設定するようなもの) は直接的には使われません。

まぁ、railsの中のことなので絶対パスがなくても大丈夫みたいな感じでしょうか

じゃあ、config.action_cable.url はいつ使うの? (補足)

この設定が完全に無意味かというと、そうでもありません。以下のような少し特殊なケースでは設定が必要になることがあります。

  • Rails のビューで action_cable_meta_tag を使って URL をクライアントに伝えたい場合(最もベーシックな使い方)
  • Rails アプリケーション外部のプロセス(例: 別のマイクロサービス、独立した Rake タスクなど)から Action Cable サーバーにブロードキャストしたい場合に、その接続先としてこの URL が必要になることがあります(ただし、このケースはあまり想像がつかないので無いかもしれません)
  • Action Cable のテストで URL を明示的に設定する必要がある場合(テストは大事ですが、何かしらのAPIを叩いたら発火や、DBの登録で発火などの実装が多いと思いますので、そのAPIを叩いてテストすればいいと思います)

まとめ

以下の条件が揃っていれば、config.action_cable.url を設定しなくても Action Cable は問題なく動作します。

  • クライアント側 (Vue, React など) で @rails/actioncable を使い、createConsumer() で WebSocket の URL を直接指定している
  • サーバーからのブロードキャストは Rails アプリケーション内部から行われている
  • Rails のビューヘルパー action_cable_meta_tag を使っていない

調べてみると、これはどうやらバグや偶然ではなく、Rails の設計通りの挙動だったようです。

モダンな Rails API + SPA (Single Page Application) 構成では、この設定は省略できることが多いので、config/environments/*.rb が少しスッキリするかもしれません。

ただし、あったらエラーになるというわけでもないので、将来的に上記の「必要になるケース」が発生する可能性も考慮して、明示的に設定しておく、という判断ももちろんアリだと思います!


参考:




フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link