PubSub 是 Phoenix 內建的 Publisher Subscriber 服務,文件
實作起來非常簡單,而且 Phoenix 預設就已經啟動一個了
我們來看看 lib/blog/application.ex
的第 16 行,我們隨時都可以用這個叫 Blog.PubSub
的 PubSub,(當然如果專案複雜也可以自己再開更多)
我們先來列出要做的事情
Blog.PubSub
的某個頻道我們頻道名稱就用 "chat_room" 吧
先來在打開頁面時訂閱頻道
編輯 lib/blog_web/live/chat_room_live.ex
的 mount 方法
def mount(_params, _session, socket) do
# 加入這行,如果 live_view socket 已經連線,就訂閱 Blog.PubSub 的 "chat_room" 頻道
if connected?(socket), do: Phoenix.PubSub.subscribe(Blog.PubSub, "chat_room")
# 暫時懶得做輸入名字的輸入框,我們先產生的隨便的名字,可以區分就好
name = "路人#{Enum.random(100..999)}"
{:ok,
assign(socket, %{
changeset: Messages.change_message(%Messages.Message{}),
messages: Messages.list_messages(),
author: name,
message_sid: 0
})}
end
為什麼要多判斷 socket 是不是已經連線呢?
其實 mount 會被執行兩次喔!
第一次會比較像 controller 那樣,連線要求進來,產生第一個頁面回覆
我們得到第一個頁面,瀏覽器執行 live_view 內建的 javascript 指令,才開始建立 websocket 連線
這時就是第二次。
我們每次發出一則訊息就可以廣播到頻道裡面告訴大家
因為我們有存每一則訊息,所以我們可以在確定儲存之後再送出,
其實如果不在意的話這個瀏覽器也可以做沒有儲存版本,還更簡單。
更改一下 create_message 方法lib/blog/messages.ex
def create_message(attrs \\ %{}) do
%Message{}
|> Message.changeset(attrs)
|> Repo.insert()
|> broadcast()
end
我們在 Repo.insert 這個輸入資料庫方法後面接 broadcast
這代表我們的 broadcast 可能會收到兩種結果{:ok, message}
跟 {:error, changeset}
我們來在這個模組建立 broadcast 方法
如果是儲存成功,我們要廣播訊息
def broadcast({:ok, message}) do
# 廣播到 Blog.PubSub 的 "chat_room" 頻道,內容是剛剛建立的 message
Phoenix.PubSub.broadcast(Blog.PubSub, "chat_room", message)
# 因為別的地方還是依賴這個 {:ok, message} 來判斷東西,所以我們還是要回傳本來傳進來的 {:ok, message}
{:ok, message}
end
如果建立失敗我們就不傳
def broadcast({:error, _reason} = error), do: error
一但在 live_view 裡面 訂閱了一個頻道,我們就可以用 handle_info 來接收訊息
def handle_info(message, socket) do
# 把新收到的訊息加到 messages 的第一個
{:noreply, update(socket, :messages, fn messages -> [message | messages] end)}
end
我們這邊又 update 來更新 socket 的 :messages 欄位
update 的第三個變數收的是一個方法,執行 update 的時候會把原本的值帶進去方法裡面
如果像之前寫 assign 的話則是assign(socket, %{messages: [message | socket.assigns.messages]})
因為我們的是所有人都加入同一個頻道,這代表發訊息的人也有
還記得我們發訊息除了儲存訊息之外,還有重新讀取目前所有的訊息嗎?
如果這邊還讀取所有的訊息就會變成:讀取訊息之後,又收到新訊息的廣播,新訊息會變兩個
我們在這邊簡單解,變成發訊息後就不讀取了,我一樣用廣播來顯示剛剛發出去的訊息即可
def handle_event("new_message", %{"message" => message_attrs}, socket) do
case Messages.create_message(message_attrs) do
{:ok, _message} ->
{:noreply,
assign(socket, %{
# 刪掉下面這行 messages: Messages.list_messages(),
# messages: Messages.list_messages(),
message_sid: socket.assigns.message_sid + 1
})}
{:error, changeset} ->
{:noreply, assign(socket, %{changeset: changeset})}
end
end
完成了,
我們可以在瀏覽器開兩個分頁來試試看效果