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

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

Ruby on Rails 6.0によるWebアプリケーション開発28 アクションケーブル(ActionCable)を利用して更新する

<<前  [TOP]  次>>


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


これまでWebブラウザーはURLに直接移動するかリンクまたはボタンをクリックしてRailsアプリから情報を要求してきました。
しかし直接リクエストすることなく、Railsアプリからユーザーのブラウザーに情報を送信することもできます。
これを可能にする技術はWebSocketと呼ばれます。
アクションケーブルとWebソケットを使用してカタログを閲覧しているユーザーに価格の更新を通知できます。
確認するにはまず2つのブラウザウィンドウまたはタブでアプリケーションを起動します。
最初のウィンドウでカタログを表示し、2番目のウィンドウでアイテムの価格を更新します。
そして最初のウィンドウに戻り、そのアイテムをカートに追加します。
この時点ではカートには更新された価格が表示されますが、カタログには元の価格が表示されます。


価格の更新
価格の更新


価格の更新後にカートに入れる
価格の更新後にカートに入れる


アクションケーブルはチャンネルの作成、データの送信、データの受信という3つのステップからなります。
まずはチャンネルの作成を行います。
コマンドプロンプトで「bin」フォルダに移動して「rails generate channel goods」と入力します。
チャンネルの作成
チャンネルの作成


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


【C:\Rails6\work\shop\app\channels\goods_channel.rb】

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

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



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


【C:\Rails6\work\shop\config\environments\development.rb】

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 on
  # every request. 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.cache_classes = false

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

  # Show full error reports.
  config.consider_all_requests_local = 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 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

  # Debug mode disables concatenation and preprocessing of assets.
  # This option may cause significant delays in view rendering with a large
  # number of complex assets.
  config.assets.debug = true

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

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

  # Use an evented file watcher to asynchronously detect changes in source code,
  # routes, locales, etc. This feature depends on the listen gem.
  # config.file_watcher = ActiveSupport::EventedFileUpdateChecker

  config.action_cable.disable_request_forgery_protection = true

end



このチャネルを介してデータのみを送信してコマンド処理しないので、安全です。
次に更新が行われるたびにカタログ全体に送信する処理をさせます。
「C:\Rails6\work\shop\app\controllers」フォルダにある「goods_controller.rb」ファイルの「update()」メソッドを以下のように編集します。


【C:\Rails6\work\shop\app\controllers\goods_controller.rb】

  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('market/index', layout: false)

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



アクティブレコードの「order()」を使用して商品のリストをタイトル順に並べていることに注意してください。
これはビュー全体を更新する場合、カタログが同じ順序になり切り替えがそれほど不快にならないようにするためです。
同じ順序を使用するようにGoodsControllerのindex()メソッドも変更する必要があります。
「index()」メソッドを変更した「goods_controller.rb」ファイルは以下のようになります。
「@goods = Good.all.order(:title)」というように記述を変更しています。


【C:\Rails6\work\shop\app\controllers\goods_controller.rb】

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

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

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

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

  # GET /goods/1/edit
  def edit
  end

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

    respond_to do |format|
      if @good.save
        format.html { redirect_to @good, notice: '商品情報を新規登録しました。' }
        format.json { render :show, status: :created, location: @good }
      else
        format.html { render :new }
        format.json { render json: @good.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /goods/1
  # PATCH/PUT /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('market/index', layout: false)

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

  # DELETE /goods/1
  # DELETE /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



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


【C:\Rails6\work\shop\app\javascript\channels\goods_channel.js】

import consumer from "./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
    }

  }
});



これで商品の情報を更新したらすぐに商品カタログの価格表示に反映されるようになりました。
確認してみてください。


<<前  [TOP]  次>>