環境: Rails 6.0、Vue.js 2.6、ソース: https://github.com/kazubon/cable60
ActionCable、ActiveJob、およびVue.jsを使って、次のような非同期処理を行う画面を作ります。
必要なライブラリ
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 だと効かないからです。
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: cable60_development
コントローラでは、ActionCable用の接続の識別子となるランダム文字列をクッキーに入れます。このサンプルでは、「1ユーザー - 1識別子 - 1ストリーム」とします。1対多の送信は行いません。
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 を取り出します。
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 にそのまま渡します。
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.active_job.queue_adapter = :sidekiq
コントローラでは、「開始」ボタンで呼び出す update アクションを書いておきます。SampleJobというジョブにクッキーの識別子を渡して非同期処理をさせます。
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: 処理中かどうか }
。
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とかプロパティを増やすことを想定しています。
import Vue from 'vue';
export default Vue.observable({
sample: { },
// foo: { },
// bar: { },
})
UserChannelに対応するuser_channel.jsを修正します。receivedでデータを受け取ったら、ActionCable用のオブジェクトcableDataのプロパティにそのまま入れます。
オブジェクトのtypeプロパティの値がcableDataの各プロパティの名前に対応していることにします。
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を使っています。
<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")
ががあることを確認しましょう。
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のデータを扱えます。
export default {
sample: { }
}
この場合は、Vueコンポーネントでdataを使ってオブジェクトを渡せば、sampleプロパティの変更が反映されます(リアクティブになります)。
<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>