昨天把專案透過 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 也可以)
成功部署完後,可以看到頁面可以即時接收其他使用者傳遞出的訊息。

接下來就可以進入遊戲邏輯的實作部分了。 