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
完成了,
我們可以在瀏覽器開兩個分頁來試試看效果