Leon's Blogging

Coding blogging for hackers.

跟著 DHH 練習 ActionCable

| Comments

ActionCable 已經有一陣子了,剛好有時間來玩一下

主要跟著 DHH 的影片來練習一下,了解整個 ActionCable 的流程

介紹

ActionCable 主要是一個 Pub/Sub 模型 + WebSocket 的 Ruby 框架,可以讓 Rails 實現即時通訊的方式

WebSocket

  • 是一種建立在單一 TCP 連線上的全雙工(full-duplex)通訊管道,可以讓網頁應用程式與伺服器之間做即時性、雙向的資料傳遞。
  • 瀏覽器與伺服器之間若要建立一條 WebSocket 連線,在一開始的交握(handshake)階段中,要先從 HTTP 協定升級為 WebSocket 協定

Polling 輪詢

  • 瀏覽器每隔一段時間就自動送出一個 HTTP 請求給 server ,獲取最新的網頁資料
  • 在 server 沒有新資料時,瀏覽器也是會自動送出請求,造成網路資源浪費

Long-Polling 長時間輪詢

  • server 在接收到瀏覽器所送出的 HTTP 請求後, server 會等待一段時間,若在這段時間裡 server 有新的資料,它就會把最新的資料傳回給瀏覽器
  • 如果等待的時間到了之後也沒有新資料的話,就會送一個回應給瀏覽器,告知瀏覽器資料沒有更新
  • 如果在資料更新很頻繁的狀況下,長時間輪詢並不會比傳統的輪詢有效率,而且有時候資料量很大時,會造成連續的 polls 不斷產生,反而會更糟糕。

Streaming

  • 讓 server 在接收到瀏覽器所送出 HTTP 請求後,立即產生一個回應瀏覽器的連線,並且讓這個連線持續一段時間不要中斷,而 server 在這段時間內如果有新的資料,就可以透過這個連線將資料馬上傳送給瀏覽器。
  • 由於是建立在 HTTP 協定上的一種傳輸機制,所以有可能會因為代理 server(proxy)或防火牆(firewall)將其中的資料存放在緩衝區中,造成資料回應上的延遲,因此許多使用串流的 Comet 實作會在偵測到有代理 server 的狀況時,改用 Long-Polling 的方式處理。

Pub/Sub 發佈/訂閱模式 (Publish/Subscribe Pattern)

  • 發佈/訂閱設計模式是一種在即時通訊上很常用的架構,可以將通訊拆成發佈方和訂閱方,發佈方非同步地將訊息傳送給不定數量的訂閱方。
  • Pub/Sub 部分可以從你的主應用 Process 外,獨立出來成為一個單獨的運作元件。

Actioncable 實作

  • 建立新的專案
rails new campfire
  • 建立 rooms controller
# show 可以再產生 controller 自動產生 show action & router
rails g controller rooms show
  • 修改 routes
# config/routes.rb
Rails.application.routes.draw do
  root to: 'rooms#show'
end
  • 建立 message model
# content:text 可自動在 migration 產生相對應的欄位
rails g model message content:text
rails db:migrate
  • 修改 rooms controller
# controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def show
    @messages = Message.all
  end
end
# rails console 建立一筆來測試
Message.create!(content: "Hello World!")
  • 修改 rooms/show.html.erb 讓 messages 顯示
# views/rooms/show.html.erb
<h1>Chat room</h1>
<div id="messages">
  <%= render @messages %>
</div>

此時就可以 rails s 看一下首頁有沒有東西

  • 建立 channel

speak 會自動產生 channel 的 action

rails g channel room speak

  • Server 端 Mount ActionCable
# config/routes.rb
Rails.application.routes.draw do
  root to: 'rooms#show'
  mount ActionCable.server => '/cable'
end
  • 修改 room.coffee

請注意這邊是 coffee js 因此空格會有差別

# assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
  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) ->
    # Called when there's incoming data on the websocket for this channel
    # 接到 data 後要做什麼處理,目前設定是將 message alert 彈出
    alert data['message']

  speak: (message)->
  # 傳 message 到 RoomChannel 的 speak action
    @perform 'speak', message: message
  • 修改 room channel 的 speak action
# channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    # 訂閱的頻道名稱
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    # 將 message 廣播到 room_channel 的 received function
    ActionCable.server.broadcast("room_channel", message: data['message'])
  end
end

重新啟動 server,在瀏覽器的 console 執行

# 這個指令會 call client side 的 speak function,也就是 @perform 'speak', message: message
App.room.speak("Hello, World!") 

就會看到彈出視窗,表示伺服器端已經接受到訊息。

  • 新增 text input,讓 client 可以直接輸入 data
# views/rooms/show.html.erb
<h1>Chat room</h1>
<div id="messages">
 <%= render @messages %>
</div>
<form>
  <label>Say something:</label>
  <input type="text" data-behavior="room_speaker">
</form>
  • 設定輸入後執行的動作

記得先安裝 jquery,因為後面會利用 jquery 來操作 domevent

# Gemfile
gem 'jquery-rails'
# assets/javascripts/application.js
//= require jquery

請注意這邊是 coffee js 因此空格會有差別

# assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
  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) ->
    # Called when there's incoming data on the websocket for this channel
    # 接到 data 後要做什麼處理,目前設定是將 message alert 彈出
    alert data['message']

  speak: (message)->
    @perform 'speak', message: message

# 設定在按下鍵盤 Enter 按鍵後,要執行 App.room.speak,就不用透過 console 去輸入
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  if event.keyCode is 13 #return
    App.room.speak event.target.value
    event.target.value = ''
    event.preventDefault()

基本上現在就可以透過輸入框,讓 alert 跳出來了

透過 job 來執行 ActionCable

  • 實際存取到 db
# channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    # 改成直接 create record 在 db 裡面
    Message.create(content: data['message'])
  end
end
  • create 後 callback 執行 Job
# models/message.rb
class Message < ApplicationRecord
  after_create_commit { MessageBroadcastJob.perform_later self }
end
  • 建立 job
rails g job MessageBroadcast
# jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
     # perform_later 後會執行的動作
    ActionCable.server.broadcast("room_channel", message: render_message(message))
  end

  private

  def render_message(message)
     # 透過 ApplicationController 將 html code render 回來當一個參數做傳遞
    ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
  end
end

此時 rails s 測試是否正常,因該會有 alert 跳出一段 html code

  • 修改 received function
# assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
  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) ->
    # Called when there's incoming data on the websocket for this channel
    # alert 改成透過 jquery 找到 id='messages' 的 dom,並把回傳來的 html code append 上去
    $('#messages').append data['message']

  speak: (message)->
    @perform 'speak', message: message

$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  if event.keyCode is 13 #return
    App.room.speak event.target.value
    event.target.value = ''
    event.preventDefault()

再重啟一次,這時輸入資料後,畫面會立即新增,可以開雙畫面去做測試,另一邊的畫面是否也是立即新增

參考文件:

Comments