3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rails 6: ActionCableとVue.jsで非同期処理を行うサンプル

Last updated at Posted at 2020-12-08

環境: Rails 6.0、Vue.js 2.6、ソース: https://github.com/kazubon/cable60

ActionCable、ActiveJob、およびVue.jsを使って、次のような非同期処理を行う画面を作ります。
image.png

必要なライブラリ

GemfileにRedisとSidekiqを追加して、bundle install してください。

gem 'redis', '~> 4.0'
gem 'sidekiq'

Redisをインストールしていない場合は、インストールして起動しておきます。

% brew install redis
% brew services start redis

ActionCableの準備

cable.ymlで、development環境の設定を async から redis に変えます。サンプルでは、ActiveJobのジョブの中でActionCableのブロードキャストを行いますが、async だと効かないからです。

config/cable.yml
development:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: cable60_development

コントローラでは、ActionCable用の接続の識別子となるランダム文字列をクッキーに入れます。このサンプルでは、「1ユーザー - 1識別子 - 1ストリーム」とします。1対多の送信は行いません。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_cable_code

  private
  # Action Cable用ユーザー識別
  def set_cable_code
    cookies.signed[:cable_code] ||= SecureRandom.hex
  end
end

connection.rbではクッキーから識別子 cable_code を取り出します。

app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :cable_code

    def connect
      if cookies.signed[:cable_code]
        self.cable_code = cookies.signed[:cable_code]
      end
    end
  end
end

bin/rails g channel user で user_channel.rb を作っておき、識別子 cable_code を stream_from にそのまま渡します。

app/channels/user_channel.rb
class UserChannel < ApplicationCable::Channel
  def subscribed
    stream_from cable_code if cable_code
  end

  def unsubscribed
  end
end

ActiveJobの準備

config/environments下のdevelopment.rbとproduction.rbを修正し、ActiveJobではSidekiqを使うことを指定します。

config/environments/development.rb
  config.active_job.queue_adapter     = :sidekiq

コントローラでは、「開始」ボタンで呼び出す update アクションを書いておきます。SampleJobというジョブにクッキーの識別子を渡して非同期処理をさせます。

app/controllers/samples_controller.rb
class SamplesController < ApplicationController
  def show
  end

  def update
    SampleJob.perform_later(cookies.signed[:cable_code])
    render json: {}
  end
end

bin/rails g job sample でSampleJobを作っておいて、performメソッドにサンプル用の処理を書きます。20回スリープしながら現在のパーセンテージを進めるだけのものです。

ActionCableのブロードキャストを使い、識別子 cable_code に対してハッシュ(JavaScriptのオブジェクト)を送信します: { type: 処理の種類に付けた名前, progress: パーセンテージ, processing: 処理中かどうか }

app/jobs/sample_job.rb
class SampleJob < ApplicationJob
  queue_as :default

  def perform(cable_code)
    20.times do |idx|
      sleep 0.2
      ActionCable.server.broadcast(cable_code,
        type: 'sample', progress: (idx + 1) * (100 / 20), processing: true)
    end
    ActionCable.server.broadcast(cable_code,
      type: 'sample', progress: 100, processing: false)
  end
end

JavaScriptでデータを受け取り、進行状況を表示する

JavaScript側では、ActionCableからデータを受け取るためのオブジェクトを作ります。Vue.observable を使うことで、sampleプロパティを変更したらVueのテンプレートに反映するようにします。

ほかに非同期処理を扱う画面が増えたら、fooとかbarとかプロパティを増やすことを想定しています。

app/javascript/channels/cable_data.js
import Vue from 'vue';

export default Vue.observable({
  sample: { },
  // foo: { },
  // bar: { },
})

UserChannelに対応するuser_channel.jsを修正します。receivedでデータを受け取ったら、ActionCable用のオブジェクトcableDataのプロパティにそのまま入れます。

オブジェクトのtypeプロパティの値がcableDataの各プロパティの名前に対応していることにします。

app/javascript/channels/user_channel.js
import consumer from "./consumer"
import cableData from "./cable_data";

consumer.subscriptions.create("UserChannel", {
  connected() {
  },

  disconnected() {
  },

  received(data) {
    switch(data.type) {
      case 'sample':
        cableData.sample = data;
        break;
      // case 'foo':
      //   cableData.foo = data;
      //   break;
      // case 'bar':
      //   cableData.bar = data;
      //   break;
    }
  }
});

非同期処理の進行状況を表示するVueコンポーネントです。ActionCable用のオブジェクト cableData.sampleのprogressとprocessingの値を画面に反映させます。

なお、ここではBootstrapのProgressを使っています。

app/javascript/sample.vue
<template>
  <div>
    <div class="form-group row">
      <div class="progress">
        <div class="progress-bar" role="progressbar" :aria-valuenow="progress"
          aria-valuemin="0" aria-valuemax="100" :style="`width: ${progress}%`"></div>
     </div>
    </div>
    <div class="form-group row">
      <button type="button" class="btn btn-primary" @click="startProcess"
        :disabled="processing">開始</button>
    </div>
  </div>
</template>

<script>
import Axios from 'axios'
import cableData from "./channels/cable_data"

export default {
  data() {
    return {
    };
  },
  computed: {
    progress() {
      return cableData.sample.progress || 0;
    },
    processing() {
      return cableData.sample.processing;
    }
  },
  methods: {
    startProcess() {
      cableData.sample = { progress: 0, processing: true };
      Axios.patch('/sample');
    }
  }
}
</script>

<style scoped>
.progress {
  width: 100%;
}
</style>

サンプルのVueコンポーネントをマウントするpacks下のJavaScriptです。require("channels") ががあることを確認しましょう。

app/javascript/packs/application.js
import 'bootstrap';
import '../stylesheets/application';

require("@rails/ujs").start()
require("turbolinks").start()
// require("@rails/activestorage").start()
require("channels")

import Vue from 'vue';
import TurbolinksAdapter from 'vue-turbolinks';

import Sample from '../sample.vue';
import '../axios_config';

Vue.use(TurbolinksAdapter);

document.addEventListener('turbolinks:load', () => {
  if(document.getElementById('sample')) {
    new Vue(Sample).$mount('#sample');
  }
});

bin/rails s でサーバーを起動し、別のターミナルで bundle exec sidekiq を起動すれば、非同期処理の動作を確認できます。

Vue.observable を使わない場合

上記のcable_data.jsで、Vue.observableを使わずにJavaScriptのオブジェクトをそのままエクスポートしても、ActionCableのデータを扱えます。

app/javascript/channels/cable_data.js
export default {
  sample: { }
}

この場合は、Vueコンポーネントでdataを使ってオブジェクトを渡せば、sampleプロパティの変更が反映されます(リアクティブになります)。

app/javascript/sample.vue
<script>
import Axios from 'axios'
import cableData from "./channels/cable_data"

export default {
  data() {
    return {
      cableData: cableData
    };
  },
  computed: {
    progress() {
      return this.cableData.sample.progress || 0;
    },
    processing() {
      return this.cableData.sample.processing;
    }
  },
  methods: {
    startProcess() {
      this.cableData.sample = { progress: 0, processing: true };
      Axios.patch('/sample');
    }
  }
}
</script>
3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?