Techioz Blog

Rails 6 で Importmaps を使用して ActionCable を設定するにはどうすればよいですか? (MRIとJruby)

概要

Actioncable - 概要。

私はjruby 9.2.16(したがってruby 2.5.7)とrails 6.1.6.1を使用しています。

Actioncable が開発中のみなのか、SSL (wss) なしでのみ使用できるのか、単純なクライアント側で使用できるかどうかはわかりません。

var ws = new WebSocket('ws://0.0.0.0:3000/channels');
ws.onmessage = function(e){ console.log(e.data); }

しかし、少なくとも実稼働環境で wss を使用して「チャネルからのストリーミング」を実行することはできませんでした。これはローカルで動作するためです(ターミナルで「redis-cli」が開始され、次に「monitor」が表示されます)。

そこで、actioncable クライアント スクリプトを実装しようとしたため、8 日が無駄になってしまいました。

しかし結局のところ、importmap が行うことはすべて、JavaScript ファイルをロードするという、車輪の再発明のように私には見えます。何か、手動で簡単に実行できます。 モジュールのインポートも簡単なJavaScriptで可能です。それでは、最終的にブラウザが動作する JavaScript ファイルとは何でしょうか?

そのファイルやその生成場所の説明が見つかりません。あるいは、それが単なるリンクであるのか、あるいは何らかの方法でコンパイルする必要があるのかもわかりません。しかし、私には、importmap が完全に冗長で、物事を非常に複雑にするだけのように見えます。文字列「xyz」を書くメリットがわかりません。文字列「xyz」は、別の文字列「xyz_2」に設定するために面倒な方法で変換されますが、これも見つからない可能性があります。変数がある場合は、action_cable_meta_tag のような同じ考え方を使用して直接ロードできます。

技術的には、Actioncable は Faye が以前に行ったことを行っているだけです。では、なぜ車輪の再発明としか思えない、いわゆる「現代的」な方法が必要なのでしょうか?

そこで、アクションケーブルの取り付け方法を、不要なツールを使わずに簡単にわかりやすく説明したいと思います。

しかし、まずは自分で動作させる必要があります。したがって、問題は次のとおりです。

GET http://0.0.0.0:3000/actioncable.esm.js net::ERR_ABORTED 404 (Not Found)

みなさん、アイデアをありがとう!

解決策

まず、importmap-rails から:

勝負を受けて立つ。今のところは mri を使用します (後で、何か奇妙なことが起こるかどうかを確認するために、あなたのバージョンで試してみます)。

$ rails _6.1.6.1_ new cable --skip-javascript
$ cd cable

# https://github.com/rails/importmap-rails#installation

$ bin/bundle add importmap-rails
$ bin/rails importmap:install

# ActionCable guide seems rather crusty. Until this section:
# https://guides.rubyonrails.org/v6.1/action_cable_overview.html#connect-consumer
# A generator for the client side js is mentioned in the code comment.

$ bin/rails g channel chat

# Oops, generated server side rb as well.
# This should really be at the start of the guide.
# app/channels/chat_channel.rb

class ChatChannel < ApplicationCable::Channel
  def subscribed
    # NOTE: just keep it simple
    stream_from "some_channel"
  end
end

app/javascript ディレクトリは一般的であるように見えます。これは、shakapacker、jsbundling-rails、importmap-rails などで使用されるすべての Javascript のものに当てはまります。ここで少し説明しました: https://stackoverflow.com/a/73174481/207090

// app/javascript/channels/chat_channel.js

import consumer from "./consumer"

consumer.subscriptions.create("ChatChannel", {
  connected() {
    // NOTE: We have to check if our set up is online first,
    //       before chatting and authenticating or anything else.
    console.log("ChatChannel connected")
  },

  disconnected() {},
  received(data) {}
});

メッセージをブロードキャストするには、アプリ内のどこかでこれを呼び出します[原文ママ]。

ActionCable.server.broadcast("some_channel", "some message")

さて、とにかくコントローラーを作成する必要があります。

$ bin/rails g scaffold Message content
$ bin/rails db:migrate
$ open http://localhost:3000/messages
$ bin/rails s

また、ページに読み込むには、チャンネルをどこかにインポートする必要があります。レイアウト内の javascript_importmap_tags はアプリケーションのみをインポートします。 https://translate.google.com/translate?hl=ja&sl=en&tl=ja&u=https://github.com/rails/importmap-rails#usage

<script type="module">import "application"</script>
<!--                          ^            -->
<!-- this imports the pinned `application` -->

application.js にチャネルをインポートするのは理にかなっています。 require があるため、./channels/index をインポートできません。ノードを機能させるにはノードを使用するか、すべてのチャンネルをインポートするために何か他のことをする必要があります。手動の方法が最も簡単です。

// app/javascript/channels/index.js
// NOTE: it works a little differently with importmaps that I haven't mentioned yet.
//       skip this index file for now, and import channels in application.js

// app/javascript/application.js
import "./channels/chat_channel"

ブラウザコンソールに @rails/actioncable が欠落していると表示されます。まだ誰もインストールするように言っていません。 pin コマンドを使用して追加します。 https://translate.google.com/translate?hl=ja&sl=en&tl=ja&u=https://github.com/rails/importmap-rails#using-npm-packages-via-javascript-cdns

$ bin/importmap pin @rails/actioncable

ブラウザを更新します。

ChatChannel connected                                          chat_channel:5

ページに JavaScript が追加されました。ブロードキャストしてみましょう:

# app/controllers/messages_controller.rb

# POST /messages
def create
  @message = Message.create(message_params)
  ActionCable.server.broadcast("some_channel", @message.content)
end
<!-- app/views/messages/index.html.erb -->

<div id="chat"></div> <!-- output for broadcasted messages -->

<!-- since I have no rails ujs, for my purposes: bushcraft { remote: true } -->
<!--                                                 v                      -->
<%= form_with model: Message.new, html: { onsubmit: "remote(event, this)" } do |f| %>
  <%= f.text_field :content %>
  <%= f.submit %>
<% end %>

<script type="text/javascript">
  // you can skip this, I assume you have `rails_ujs` installed or `turbo`.
  // { remote: true } or { local: false } is all you need on the form.
  function remote(e, form) {
    e.preventDefault();
    fetch(form.action, {method: form.method, body: new FormData(form)})
    form["message[content]"].value = ""
  }
</script>

私たちはつながっていることを知っています。フォームは、「some_channel」にブロードキャストしている場所を更新せずに、MessagesController#create に送信されます。残っているのは、ページにデータを出力することだけです。 https://translate.google.com/translate?hl=ja&sl=en&tl=ja&u=https://guides.rubyonrails.org/v6.1/action_cable_overview.html#client-server-interactions-subscriptions

// app/javascript/channels/chat_channel.js

// update received() function
received(data) {
  document.querySelector("#chat")
    .insertAdjacentHTML("beforeend", `<p>${data}</p>`)
}

アクションケーブルが完成しました。次に、importmaps を修正しましょう。

これまで言及しなかった点ですが、理解することが非常に重要です。

すべてが機能しますが、開発段階でのみ、その理由をここで説明しました。 https://translate.google.com/translate?hl=ja&sl=en&tl=ja&u=https://stackoverflow.com/a/73136675/207090

URL と相対パスまたは絶対パスはマッピングされず、さらに重要なことに、アセット パイプライン スプロケットをバイパスします。実際に importmaps を使用するには、app/javascript/channels 内のすべてのファイルをマップ (別名固定) し、インポート時に固定された名前のみで参照する必要があります。

# config/importmap.rb

# NOTE: luckily there is a command to help with bulk pins
pin_all_from "app/javascript/channels", under: "channels"

pin "application", preload: true
pin "@rails/actioncable", to: "https://ga.jspm.io/npm:@rails/[email protected]/app/assets/javascripts/actioncable.esm.js"
# NOTE: the big reveal -> follow me ->------------------------------------------------------------------^^^^^^^^^^^^^^^^^^

# NOTE: this only works in rails 7+
# pin "@rails/actioncable", to: "actioncable.esm.js"
# `actioncable.esm.js` is in the asset pipeline so to speak and can be found here:
# https://github.com/rails/rails/tree/v7.0.3.1/actioncable/app/assets/javascripts

pin と pin_all_from については、以下を参照してください。 https://translate.google.com/translate?hl=ja&sl=en&tl=ja&u=https://stackoverflow.com/a/72855705/207090

これにより作成されるインポートマップはブラウザまたはターミナルで確認できます。

$ bin/importmap json
{
  "imports": {
     "application":           "/assets/application-3ac17ae8a9bbfcdc9571d7ffac88746f5a76b18c149fdaf02fa7ed721b3e7c49.js",
     "@rails/actioncable":    "https://ga.jspm.io/npm:@rails/[email protected]/app/assets/javascripts/actioncable.esm.js",
     "channels":              "/assets/channels/index-78e712d4a980790be34a2e859a2bd9a1121f9f3b508bd3f7de89889ff75828a0.js",
     "channels/chat_channel": "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js",
     "channels/consumer":     "/assets/channels/consumer-b0ce945e7ae055dba9cceb062a47080dd9c7794a600762c19d38dbde3ba8ff0d.js"
  }#    ^                        ^
}  #    |                        |
   #  names you use             urls browser uses
   #    |   to import            ^   to actually get it
   #    |                        |
   #    `---> importmaped to ----'

importmaps 情報 (importmap-rails gem ではありません): https://translate.google.com/translate?hl=ja&sl=en&tl=ja&u=https://github.com/WICG/import-maps

Importmaps は何もインポートせず、名前を URL にマップします。 /name、./name、../name、http://js.cdn/name の URL のように名前を作成すると、マップするものは何もありません。

import "channels/chat_channel"

// stays unchanged and is now the same as

import "/assets/channels/chat_channel-0a2f983da2629a4d7edef5b7f05a494670df3f99ec6a22a2e2fee91a5d1c1d05.js"

// because we have an importmap for "channels/chat_channel"

ファイルの更新時にダイジェスト ハッシュが変更され、ブラウザーのキャッシュが無効になるため (これはスプロケットによって処理されます)、js ファイル内で絶対パスを含む 2 番目の形式を使用することは望ましくありません。

すべてのインポートを変換します。

import consumer from "./consumer"
import "./channels/chat_channel"

ピン留めされた名前と一致させるには:

import consumer from "channels/consumer"
import "channels/chat_channel"

// import "channels" // is mapped to `channels/index`
// TODO: want to auto import channels in index file?
//       just get all the pins named *_channel and import them,
//       like stumulus-loading does for controllers:
//       https://github.com/hotwired/stimulus-rails/blob/v1.1.0/app/assets/javascripts/stimulus-loading.js#L8

jrubyでも同じ設定です。それをインストールして、Gemfile を更新しました。

# Gemfile
ruby "2.5.7", engine: "jruby", engine_version: "9.2.16.0"
gem "activerecord-jdbcsqlite3-adapter"
gem "importmap-rails", "< 0.8" # after version 0.8.0 ruby >= 2.7 is required

サーバー起動時の最初のエラー:

NoMethodError: private method `importmap=' called for #<Cable::Application:0x496a31da>

importmap= メソッドはここで定義されています。 https://translate.google.com/translate?hl=ja&sl=en&tl=ja&u=https://github.com/rails/importmap-rails/blob/v0.7.6/lib/importmap/engine.rb#L4

Rails::Application.send(:attr_accessor, :importmap)

jruby では、次のように使用する場合にプライベート メソッドを定義します。

>> A = Class.new
>> A.attr_accessor(:m)
>> A.new.m
NoMethodError (private method 'm' called for #<A:0x5f2f577>)

修正するには、アプリの定義をオーバーライドするか、これらのメソッドをパブリックにすることです。

# config/application.rb

module Cable
  class Application < Rails::Application
    # make them public
    public :importmap, :importmap=

    config.load_defaults 6.1
  end
end

Ruby 2.5 は 2021 年 4 月 5 日に廃止されました。最新の gem が古い Ruby バージョンと適切に動作することは期待できません。それでおしまい。他に問題はありません。 mri よりかなり遅れている jruby を使用しているため、いずれにせよ、ある程度の後退は予想されるはずです。