Phoenix 在最初設計時,其中一個重要的目標就是想解決高併發的訊息傳送情境。雖然 Erlang / Elixir 原本就有 message passing 的機制,但由於 message passing 主要應用在一對一發送訊息的情況,而高併發的訊息傳送常有一對多訊息發送的需求,因此這個功能是用 PubSub 的概念實作。
由於 Channel 牽涉到的元件比較多,我們這次試著反過來試試看。在開始扯淡解釋之前,先試著生成一個 Channel。就算每個步驟不太知道在做什麼也沒關係,之後再來跟著解釋看會比較理解。首先在 shell 裡切換到我們的專案目錄下,輸入以下的指令:
$ mix phx.gen.channel room
* creating lib/hello_phx_web/channels/room_channel.ex
* creating test/hello_phx_web/channels/room_channel_test.exs
Add the channel to your `lib/hello_phx_web/channels/user_socket.ex` handler, for example:
channel "room:lobby", HelloPhxWeb.RoomChannel
在生成兩個檔案後,上面有提示要在 lib/hello_phx_web/channels/user_socket.ex
裡加一行程式碼,但打開檔案會發現已經存在一行範圍更寬的條件,所以把該行反註釋就好:
# lib/hello_phx_web/channels/user_socket.ex
defmodule HelloPhxWeb.UserSocket do
use Phoenix.Socket
## Channels
channel "room:*", HelloPhxWeb.RoomChannel # <= 反註釋這行
要讓瀏覽器可以使用 JavaScript 與後端直接用 websocket 交換訊息,我們也需要處理前端的部份。好在大部份的程式也都先預寫好了。打開 assets/js/socket.js
。移到最下面會發現一段已經寫好的 socket.connect()
的程式。在底下把 socket.channel
的第一個參數改成 “room:lobby”:
// assets/js/socket.js
socket.connect()
let channel = socket.channel("room:lobby", {}) // <= 改這行
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
而因為剛才的 socket.js
預設不會被打包進最終的 JavaScript asset 裡,要打開 assets/js/app.js
反註釋最後一行:
// assets/js/app.js
import "phoenix_html"
// Local files can be imported directly using relative
// paths "./socket" or full ones "web/static/js/socket".
import socket from "./socket" // <= 反註釋這行
一樣用 mix phx.server
將網站應用程式跑起來,用瀏覽器打開 http://localhost:4000
。接著我們要把開發者工具打開,如果是 Google Chrome 的話,就按下 [Option] + [Cmd] + [J],你應該會在底下看到一行 ”Joined successfully”,這表示之前的步驟都是對的。如果沒有的話,請重新確認之前的步驟再往下喔。
至此我們已經把前後端的 websocket 通路接起來了,接著要實作讓使用者聊天的功能。
打開 lib/hello_phx_web/templates/page/index.html.eex
,在最下方加入兩行 HTML,其中第一行是用來放顯示的訊息,第二行是用來發送訊息的文字框:
<! ... >
</div>
<div id="messages"></div>
<input id="chat-input" type="text"></input>
打開 assets/js/socket.js
,加入底下的程式碼。這段的功能是在輸入框輸入文字,並按下 [Enter] 後,會發送 websocket 訊息到後端,並清空輸入框。這裡要注意的是它會用 channel.push("shout", 訊息內容)
將訊息發送到後端。
let channel = socket.channel("room:lobby", {})
/** 加入這段:發送新訊息 **/
let chatInput = document.querySelector("#chat-input")
chatInput.addEventListener("keypress", event => {
if(event.keyCode === 13){
channel.push("shout", {body: chatInput.value})
chatInput.value = ""
}
})
/** 結束 **/
channel.join()
//...
一樣在 assets/js/socket.js
中剛剛那段程式碼的下方,再加進這段程式碼,這樣每次從 websocket channel 中接收到 “shout” 訊息時,會把內容加進顯示的區塊裡。
chatInput.value = ""
}
})
/** 加入這段:顯示新訊息 **/
let messagesContainer = document.querySelector("#messages")
channel.on("shout", payload => {
let messageItem = document.createElement("li");
messageItem.innerText = `[${Date()}] ${payload.body}`
messagesContainer.appendChild(messageItem)
})
/** 結束 **/
channel.join()
好的,實作的部份都完成了,可以來試玩一下了。先確認你的 mix phx.server
依然在運作中,接著打開兩個瀏覽器,都連到我們的 http://localhost:4000
。在其中一個瀏覽器的輸入框輸入任何訊息並按下 [Enter]後,在兩個瀏覽器裡都會出現該筆訊息:
mix phx.gen.channel 名稱
可以快速生成 Channel 後端檔案channels/room_channel.ex
channels/user_socket.ex
assets/js/socket.js
assets/js/app.js
templates/page/index.html.eex
下一篇要來解釋這一切究竟是怎麼一回事。
Happy hacking! 明天見。