学生向けプログラミング入門 | 無料

学生向けにプログラミングを無料で解説。Java、C++、Ruby、PHP、データベース、Ruby on Rails, Python, Django

Rails導入編 | カート機能の実装 | 21 | アクションケーブル(ActionCable)

↓↓クリックして頂けると励みになります。




20 | Helper】 << 【ホーム】 >> 【22 | チェックアウト






アクションケーブル(ActionCable)はWebSocketとRailsのその他の部分をシームレスに統合するためのものです。
アクションケーブルを導入することでRailsアプリケーションの効率の良さを損なわずにリアルタイム機能を実装できます。


これまでWebブラウザーはURLに直接移動するかリンクまたはボタンをクリックしてリクエストしてきました。
しかし直接リクエストすることなく、Railsアプリからユーザーのブラウザーに情報を送信することもできます。
これを可能にする技術はWebSocketと呼ばれます。
アクションケーブルとWebソケットを使用して、商品を閲覧しているユーザーに価格の更新を通知するようにします。


まずは現在の動作を確認してみましょう。
ブラウザウィンドウを2つ起動し、それぞれでURLhttp://localhost:3000/を開きます。
最初のウィンドウで商品一覧を表示し、2番目のウィンドウで商品を編集し、価格を更新します。
そして最初のウィンドウに戻り商品の価格を確認すると、商品には元の価格が表示されています。

商品の価格を更新
商品の価格を更新


価格は自動更新されない
価格は自動更新されない



アクションケーブルはチャンネルの作成、データの送信、データの受信という3つのステップからなります。
まずはチャンネルの作成を行います。
ターミナルで以下のコマンドを入力します。
コマンド
rails generate channel goods

~/Desktop/Rails7_1/SampleCart $ rails generate channel goods

      invoke  test_unit
      create    test/channels/goods_channel_test.rb
   identical  app/channels/application_cable/channel.rb
   identical  app/channels/application_cable/connection.rb
      create  app/channels/goods_channel.rb
      create  app/javascript/channels/index.js
      create  app/javascript/channels/consumer.js
      append  app/javascript/application.js
      append  config/importmap.rb
      create  app/javascript/channels/goods_channel.js
        gsub  app/javascript/channels/goods_channel.js
      append  app/javascript/channels/index.js



作成されたファイルを編集します。
「SampleCart/app/channels」フォルダにある「goods_channel.rb」ファイルを以下のように編集します。


記述編集 【SampleCart/app/channels/goods_channel.rb】3行目

class GoodsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "goods"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end



ここで重要なのはクラスの名前(GoodsChannel)とストリームの名前(goods)です。
チャネルはセキュリティに影響を与える可能性があるため、デフォルトではRailsは開発モードで実行しているときのみローカルホストからのアクセスを許可します。
複数のマシンで開発を行っている場合は、このチェックを無効にする必要があります。
これを行うには「SampleCart/config/environments」フォルダの「development.rb」ファイルに「config.action_cable.disable_request_forgery_protection = true」の行を追加します。
72行目のコメントアウトを外すだけで大丈夫です。


記述編集 【SampleCart/config/environments/development.rb】72行目

require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # In the development environment your application's code is reloaded any time
  # it changes. This slows down response time but is perfect for development
  # since you don't have to restart the web server when you make code changes.
  config.enable_reloading = true

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports.
  config.consider_all_requests_local = true

  # Enable server timing
  config.server_timing = true

  # Enable/disable caching. By default caching is disabled.
  # Run rails dev:cache to toggle caching.
  if Rails.root.join("tmp/caching-dev.txt").exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true

    config.cache_store = :memory_store
    config.public_file_server.headers = {
      "Cache-Control" => "public, max-age=#{2.days.to_i}"
    }
  else
    config.action_controller.perform_caching = false

    config.cache_store = :null_store
  end

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  config.action_mailer.perform_caching = false

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise exceptions for disallowed deprecations.
  config.active_support.disallowed_deprecation = :raise

  # Tell Active Support which deprecation messages to disallow.
  config.active_support.disallowed_deprecation_warnings = []

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Highlight code that triggered database queries in logs.
  config.active_record.verbose_query_logs = true

  # Highlight code that enqueued background job in logs.
  config.active_job.verbose_enqueue_logs = true

  # Suppress logger output for asset requests.
  config.assets.quiet = true

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  # config.action_view.annotate_rendered_view_with_filenames = true

  # Uncomment if you wish to allow Action Cable access from any origin.
  config.action_cable.disable_request_forgery_protection = true

  # Raise error when a before_action's only/except options reference missing actions
  config.action_controller.raise_on_missing_callback_actions = true
end



ルートの設定をします。
「SampleCart/config/routes.rb」ファイルの5行目に「mount ActionCable.server => '/cable'」の記述を追加しています。


記述追加 【SampleCart/config/routes.rb】

Rails.application.routes.draw do
  resources :line_items
  resources :carts

  mount ActionCable.server => '/cable'

  #root
  root 'markets#index', as: 'markets_index'

  # get
  get 'markets/index'
  get 'says/hello'
  get 'says/goodby'

  resources :goods

  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
  # Can be used by load balancers and uptime monitors to verify that the app is live.
  get "up" => "rails/health#show", as: :rails_health_check

  # Defines the root path route ("/")
  # root "posts#index"
end



「SampleCart/config/cable.yml」ファイルの記述を以下のように変更します。
2行目の記述を「adapter: async」にする必要があります。


記述変更 【SampleCart/config/cable.yml】

development:
  adapter: async

test:
  adapter: test

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: sample_cart_production



次にデータ更新が行われるたびにページを更新する処理をさせます。
「SampleCart/app/controllers/goods_controller.rb」ファイルの「update()」メソッドを以下のように編集します。

  def update
    respond_to do |format|
      if @good.update(good_params)
        format.html { redirect_to @good, notice: '商品の情報が更新されました。' }
        format.json { render :show, status: :ok, location: @good }

        @goods = Good.all.order(:title)
        ActionCable.server.broadcast 'goods', {html: render_to_string('markets/index', layout: false)}

      else
        format.html { render :edit }
        format.json { render json: @good.errors, status: :unprocessable_entity }
      end
    end
  end



「order()」メソッドを使用して商品のリストをタイトル順に並べていることに注意してください。
これはビュー全体を更新する時に、商品が同じ順序になるようにしています。
index()メソッドも同じように変更する必要があります。
6行目を「@goods = Good.all.order(:title)」という記述に変更しています。

  def index
    @goods = Good.all.order(:title)
  end



記述編集 【SampleCart/app/controllers/goods_controller.rb】

class GoodsController < ApplicationController
  before_action :set_good, only: %i[ show edit update destroy ]

  # GET /goods or /goods.json
  def index
    @goods = Good.all.order(:title)
  end

  # GET /goods/1 or /goods/1.json
  def show
  end

  # GET /goods/new
  def new
    @good = Good.new
  end

  # GET /goods/1/edit
  def edit
  end

  # POST /goods or /goods.json
  def create
    @good = Good.new(good_params)

    respond_to do |format|
      if @good.save
        format.html { redirect_to good_url(@good), notice: "商品が登録されました。" }
        format.json { render :show, status: :created, location: @good }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @good.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /goods/1 or /goods/1.json
  def update
    respond_to do |format|
      if @good.update(good_params)
        format.html { redirect_to @good, notice: '商品の情報が更新されました。' }
        format.json { render :show, status: :ok, location: @good }

        @goods = Good.all.order(:title)
        ActionCable.server.broadcast 'goods', {html: render_to_string('markets/index', layout: false)}

      else
        format.html { render :edit }
        format.json { render json: @good.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /goods/1 or /goods/1.json
  def destroy
    @good.destroy!

    respond_to do |format|
      format.html { redirect_to goods_url, notice: "商品が削除されました。" }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_good
      @good = Good.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def good_params
      params.require(:good).permit(:goods_id, :title, :description, :image_url, :price, :date, :maker, :category)
    end
end



最後のステップはクライアントでデータを受信することです。
「SampleCart/app/javascript/channels」フォルダにある「goods_channel.js」ファイルを以下のように編集します。


記述編集 【SampleCart/app/javascript/channels/goods_channel.js】

import consumer from "channels/consumer"

consumer.subscriptions.create("GoodsChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

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

  received(data) {
    const marketElement = document.querySelector("main.market")
    if (marketElement) {
	    marketElement.innerHTML = data.html
    }
  }
});



これで商品の情報を更新したらすぐにページに反映されるようになりました。
実際に動作を確認するのは難しいので、Webサーバーのログで確認します。

[ActionCable] Broadcasting to goods: {:html=>"<div class=\"container mt-4\">\n    <div class=\"row\">\n        <!-- 右側(カート)-->\n        <div class=\"col-md-4\">\n            <div id=\"cart\" class=\"carts\">\n                \n            </div>\n        </div>\n        <!-- 左側(商品リスト) -->\n        <div class=\"col-md-8\">\n         ...
Redirected to http://localhost:3000/goods/2
Completed 302 Found in 526ms (ActiveRecord: 9.3ms | Allocations: 11112)




20 | Helper】 << 【ホーム】 >> 【22 | チェックアウト





↓↓クリックして頂けると励みになります。