Madogiwa Blog

主に技術系の学習メモに使っていきます。

MarkdownからRspecにリアルタイムで変換するWebエディタを作った + ActionCableの使い方メモ📝

ActionCableと前に作ったMarkdown形式のテキストをRSpec形式のテキストに変換するgemを使ってリアルタイムに変換できるWebエディタを作りました📝

f:id:madogiwa0124:20201031171112g:plain

https://markdown-to-rspec-web.herokuapp.com/

Markdown形式のテキストをRSpec形式のテキストに変換するgemに関してはこちらから

madogiwa0124.hatenablog.com

Action Cableを使ったのが初めてだったので、そのへんを含めて内部実装を少しメモしておきます。

アプリケーションの概要

以下が今回作ったリアルタイムでMarkdownからRspecに変換するWebエディタの内部実装の概要です。

f:id:madogiwa0124:20201031172602p:plain

エディタに入力されたMarkdown形式の文字列をActionCableを使ったWebSocket通信でバックエンドに投げて、バックエンドでMarkdownToRspecを使って変換をかけてフロントエンドに返しています。

WebSocketとは?

そもそも今回使ったWebsocket通信について、あんまり良くわかってなかったので概要をメモしておきます。

WebSocket(ウェブソケット)は、コンピュータネットワーク用の通信規格の1つである。ウェブアプリケーションにおいて、双方向通信を実現するための技術規格である。
サーバとクライアントが一度コネクションを行った後は、必要な通信を全てそのコネクション上で専用のプロトコルを用いて行う。従来の手法に比べると、新たなコネクションを張ることがなくなる・HTTPコネクションとは異なる軽量プロトコルを使うなどの理由により通信ロスが減る、一つのコネクションで全てのデータ送受信が行えるため同一サーバに接続する他のアプリケーションへの影響が少ないなどのメリットがある

ja.wikipedia.org

通常HTTPではリクエスト単位でTCPコネクションの確立/切断を行う必要があると思うのですが、WebSocketではその必要がなく、軽量なプロトコルでやり取りで来るので、効率よく双方向通信が行えるプロトコルといった感じなのでしょうか👀

ActionCableとは?

このWebSocketを用いた双方向通信をRuby on Rails上で簡単に実装できるようにしてくれるのがAction Cableです🚃

Action Cableは、 WebSocketとRailsのその他の部分をシームレスに統合するためのものです。 パブリッシャ側(Publisher)が、サブスクライバ側(Subscriber)の抽象クラスに情報を送信します。 このとき、個別の受信者を指定しません。Action Cableは、サーバーと多数のクライアント間の通信にこのアプローチを採用しています。 Action Cable の概要 - Railsガイド

ActionCableは、以下のような構成になっています。

  • フロントエンド
  • バックエンド
    • ApplicationCable::Connection: WebSocketコネクションの管理
    • ApplicationCable::Channelを継承した具象クラス: リクエストに対して処理を実行しレスポンスを返す。

大体の流れとしては、

  • foo_channelconsumerを使ってWebSocketのコネクションを確立をリクエス
  • ApplicationCable::Connectionがコネクションを承認
  • foo_channelが確立したコネクションを使ってリクエス
  • ApplicationCable::Channelがリクエストを受けて処理を実行し結果をブロードキャスト

上記のような流れになっているのかなと思っています🙇‍♂️(あんまり自信はない。。。)

またActionCableで使うadapterの設定等はconfig/cable.ymlで管理することが出来ます。 ※herokuにサクッとデプロイするときにproductionadapterの設定をredisからインメモリのasyncに変更する等で使用しました。

ActionCableを使った双方向通信の実装

今回は作成したリアルタイム変換エディタの実際のコードをもとに実装イメージをメモしておきます。

consumerを使ったフロントエンド側の実装

以下がconsumerの実装です。consumer.subscriptions.createの引数に紐づくバックエンド側のclass名と、バックエンド側に渡す引数を指定してサブスクリプション(WebSocket通信を行うクライアント側)を生成します。

今回は、変換リクエストを行うconvert内にマークダウン形式のテキストを引数にバックエンド側に変換リクエストを投げる処理、結果を受け取るreceivedに変換結果とともに完了イベントを発行する処理を追加しています。

this.perform("action_name", args)でバックエンド側のDocumentChannel#action_nameargsを引数に呼び出すような仕組みになっています。

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を使って認証するのが推奨されているようです。

greg.molnar.io

以下が今回実装した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ライクに双方向通信が実装できて使いやすいですね🚃

参考

qiita.com