昨天把專案透過 zeabur 部署上線。
https://ironman2023-hanabi.zeabur.app/
今天來替專案導入 Action Cable
https://guides.rubyonrails.org/action_cable_overview.html
Action Cable 是 Rails 對 WebSocket 的包裝,透過他來達成伺服器與瀏覽器的雙向即時通訊。
相關概念後面有時間可能會分出一篇來介紹,這裡先建立一個範例來驗證『雙向即時通訊』的可行性。
TBD Again...
--- updated at 04:00
rails g channel ChatRoom
定義如何識別不同的連線(websocket connection)
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
+ identified_by :uuid
def connect
+ self.uuid = SecureRandom.urlsafe_base64
end
end
end
定義訂閱的頻道
# app/channels/chat_room_channel.rb
class ChatRoomChannel < ApplicationCable::Channel
def subscribed
# stream_from "some_channel"
+ stream_from 'public_channel'
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
前端部分使用 Stimulus 在元件 connect 時訂閱頻道,並將 connected, disconnected, received 等方法注入進去。
// app/javascript/channels/chat_room_channel.js
import consumer from "channels/consumer";
export default function createRoom({ connected, disconnected, received }) {
return consumer.subscriptions.create("ChatRoomChannel", {
connected() {
if (connected) {
connected();
} else {
// Called when the subscription is ready for use on the server
console.log("connected");
}
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
if (received) {
received(JSON.stringify(data));
} else {
// Called when there's incoming data on the websocket for this channel
console.warn("received data");
}
},
sendMessage(messageBody) {
this.perform("foobar", messageBody);
},
});
}
// app/javascript/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus";
import chat_room_channel from "channels/chat_room_channel";
import {
receivedFromUser,
receivedFromWorld,
} from "controllers/receive_wrapper";
export default class extends Controller {
static targets = ["title"];
connect() {
this.titleTarget.textContent = "hello controller loaded";
this.channel = chat_room_channel({
connected: this.connected.bind(this),
received: this.received.bind(this),
});
}
connected() {}
received(data) {
const { message, uuid, role, nickname } = JSON.parse(data);
if (role === null || role === undefined) {
console.warn("role is missing");
return;
}
let insertElement;
switch (role) {
case "player":
insertElement = receivedFromUser({ message, uuid, nickname });
break;
case "lobby":
insertElement = receivedFromWorld(message);
break;
default:
break;
}
this.element.insertAdjacentElement("beforeend", insertElement);
}
sendMessage(messageBody) {
this.channel.sendMessage(messageBody);
}
ping_the_room() {
this.sendMessage({
nickname: "Paul",
body: "This is a cool chat app.",
});
}
}
// app/javascript/controllers/receive_wrapper.js
const receivedFromWorld = (textContent) => {
const html = document.createElement("p");
html.textContent = "Lobby: " + textContent;
return html;
};
const receivedFromUser = ({ message: textContent, uuid: user, nickname }) => {
const html = document.createElement("p");
html.textContent =
(nickname ? `${nickname}(${user})` : user) + ": " + textContent;
return html;
};
export { receivedFromUser, receivedFromWorld };
找個頁面把 Stimulus Controller 掛上去
// app/views/game_rooms/index.html.erb
+ <div class="min-w-full" data-controller="hello">
+ <p class="font-bold text-2xl" data-hello-target="title"></p>
+ <button type="button" data-action="hello#ping_the_room" class="rounded-lg py-3 px-5 bg-emerald-600 text-white">ping to the room</button>
+ </div>
定義 channel 可接受的 action
# app/channels/chat_room_channel.rb
class ChatRoomChannel < ApplicationCable::Channel
...
+ def foobar(data)
+ chat_to_public(data['body'], { nickname: data['nickname'] })
+ end
+
+ private
+
+ def broadcast(data, speaking: 'Lobby')
+ ActionCable.server.broadcast 'public_channel', { message: data, role: :lobby }
+ end
action cable 需要 redis 當作資料庫,在 zeabur 專案頁面 > 建立服務 > Marketplace > Redis
建立後到 Rails app 設定環境變數 REDIS_URL=${REDIS_URI} (或是改 cable.yml 也可以)
成功部署完後,可以看到頁面可以即時接收其他使用者傳遞出的訊息。
接下來就可以進入遊戲邏輯的實作部分了。