ActionCableと前に作ったMarkdown形式のテキストをRSpec形式のテキストに変換するgemを使ってリアルタイムに変換できるWebエディタを作りました📝
https://markdown-to-rspec-web.herokuapp.com/
↓Markdown形式のテキストをRSpec形式のテキストに変換するgemに関してはこちらから
Action Cableを使ったのが初めてだったので、そのへんを含めて内部実装を少しメモしておきます。
アプリケーションの概要
以下が今回作ったリアルタイムでMarkdownからRspecに変換するWebエディタの内部実装の概要です。
エディタに入力されたMarkdown形式の文字列をActionCableを使ったWebSocket通信でバックエンドに投げて、バックエンドでMarkdownToRspec
を使って変換をかけてフロントエンドに返しています。
WebSocketとは?
そもそも今回使ったWebsocket通信について、あんまり良くわかってなかったので概要をメモしておきます。
WebSocket(ウェブソケット)は、コンピュータネットワーク用の通信規格の1つである。ウェブアプリケーションにおいて、双方向通信を実現するための技術規格である。
サーバとクライアントが一度コネクションを行った後は、必要な通信を全てそのコネクション上で専用のプロトコルを用いて行う。従来の手法に比べると、新たなコネクションを張ることがなくなる・HTTPコネクションとは異なる軽量プロトコルを使うなどの理由により通信ロスが減る、一つのコネクションで全てのデータ送受信が行えるため同一サーバに接続する他のアプリケーションへの影響が少ないなどのメリットがある
通常HTTPではリクエスト単位でTCPコネクションの確立/切断を行う必要があると思うのですが、WebSocketではその必要がなく、軽量なプロトコルでやり取りで来るので、効率よく双方向通信が行えるプロトコルといった感じなのでしょうか👀
ActionCableとは?
このWebSocketを用いた双方向通信をRuby on Rails上で簡単に実装できるようにしてくれるのがAction Cable
です🚃
Action Cableは、 WebSocketとRailsのその他の部分をシームレスに統合するためのものです。 パブリッシャ側(Publisher)が、サブスクライバ側(Subscriber)の抽象クラスに情報を送信します。 このとき、個別の受信者を指定しません。Action Cableは、サーバーと多数のクライアント間の通信にこのアプローチを採用しています。 Action Cable の概要 - Railsガイド
ActionCableは、以下のような構成になっています。
- フロントエンド
consumer
: WebSocketコネクションのクライアントfoo_channel
:consumer
に対してサブスクリプションを作成
- バックエンド
ApplicationCable::Connection
: WebSocketコネクションの管理ApplicationCable::Channel
を継承した具象クラス: リクエストに対して処理を実行しレスポンスを返す。
大体の流れとしては、
foo_channel
がconsumer
を使ってWebSocketのコネクションを確立をリクエストApplicationCable::Connection
がコネクションを承認foo_channel
が確立したコネクションを使ってリクエストApplicationCable::Channel
がリクエストを受けて処理を実行し結果をブロードキャスト
上記のような流れになっているのかなと思っています🙇♂️(あんまり自信はない。。。)
またActionCableで使うadapter
の設定等はconfig/cable.yml
で管理することが出来ます。
※herokuにサクッとデプロイするときにproduction
のadapter
の設定をredis
からインメモリのasync
に変更する等で使用しました。
ActionCableを使った双方向通信の実装
今回は作成したリアルタイム変換エディタの実際のコードをもとに実装イメージをメモしておきます。
consumer
を使ったフロントエンド側の実装
以下がconsumer
の実装です。consumer.subscriptions.create
の引数に紐づくバックエンド側のclass名と、バックエンド側に渡す引数を指定してサブスクリプション(WebSocket通信を行うクライアント側)を生成します。
今回は、変換リクエストを行うconvert
内にマークダウン形式のテキストを引数にバックエンド側に変換リクエストを投げる処理、結果を受け取るreceived
に変換結果とともに完了イベントを発行する処理を追加しています。
this.perform("action_name", args)
でバックエンド側のDocumentChannel#action_name
をargs
を引数に呼び出すような仕組みになっています。
import consumer from "@js/channels/consumer.ts"; // 複数画面開いた時に同一のChannelを参照して変更を共有しないように乱数で分けてる const documentId = window.crypto.getRandomValues(new Uint16Array(1))[0]; export const documentChannel = consumer.subscriptions.create( { channel: "DocumentChannel", documentId: documentId }, { connected: function () {}, disconnected: function () {}, // バックエンド側の変換処理の呼び出し convert: function (data: object) { return this.perform("convert", data); }, // バックエンド側の変換結果の受け取り received: function (data: object) { const event = new CustomEvent("converted-document", { detail: data }); document.dispatchEvent(event); }, } );
エディタの入力と変換完了のイベントを検知して、上記の実装をそれぞれ実行するようなJSの実装も合わせて用意します。
const markdownEditor = document.querySelector('#markdown-editor') const convertedArea = document.querySelector('#converterd-area) // Editor変更時にdocumentChannelの変換リクエスト送信を実行 markdownEditor.addEventListener('input', (markdown: string) => { documentChannel.convert({ to: "rspec", markdown: markdownEditor.value }); // documentChannelのレスポンス受信時に発行される変換完了イベントを検知して結果を反映 document.addEventListener("converted-document", ((event: CustomEvent) => { convertedArea.value = event.detail.convertedDocument; }) as EventListener);
これでフロントエンドからコネクション確立のリクエスト、MarkdownからRspec形式への変換リクエストの発行/レスポンスの受領ができるようになりました🙌
ApplicationCable::Channel
を使ったバックエンド側の実装
今回は認証等を行わないので、ApplicationCable::Connection
は使用せず、
ApplicationCable::Channel
のみを使用します。
ちなみに、WebSocketではセッションを参照できないのでログイン中のuserのidを署名付きcookieを使って認証するのが推奨されているようです。
以下が今回実装したApplicationCable::Channel
を継承した具象クラスです。
フロントエンド側からのコネクション確立リクエストに対してsubscribed
が実行され、双方向通信のストリームを"document_channel_#{params[:documentId]}"
という名前で作成します。
※このストリームを指定することによって、ストリームにつながっているクライアントに対してブロードキャストすることが出来ます。
フロントエンド側からの変換リクエストに対してconvert
が実行され、MarkdownからRspecへの変換をかけて結果を返却しています。
class DocumentChannel < ApplicationCable::Channel def subscribed stream_from "document_channel_#{params[:documentId]}" end def unsubscribed; end def convert(data) converted = converted_document(to: data['to'], markdown: data['markdown']) ActionCable.server.broadcast "document_channel_#{params[:documentId]}", convertedDocument: converted end private def converted_document(to:, markdown:) converters = { rspec: -> { ::MarkdownToRspec.to_rspec(markdown) } } converters[to.to_sym]&.call end end
これでWebSocketを使って、エディタに入力されたMarkdown形式の文字列をRspec形式に変換する処理をリアルタイム呼び出して結果を反映できるようになりました📝
おわりに
今回はAction Cableの概要とそれを使って実際に作ってみたエディタに関するメモを書いてみました📝
ActionCableは今まで使ったことがなかったのですが、使ってみるとサクッとControllerライクに双方向通信が実装できて使いやすいですね🚃