React | 在 React 中使用 WebSocket - feat. Socket.io 基本教學
前言
這次的標題有點複雜和騙人,文章內主要是在 React
中搭配 Socket.io
做使用,而 Socket.io
是一個現成的 WebSocket
套件,儘管它不是真正的 webSocket
協定,但 socket.io
還是實現了 webSocket
及增加許多方便的功能。
如果還不了解 WebSocket
的讀者,可以先參考「JavaScript | WebSocket 讓前後端沒有距離」文章內解說的原生使用方式,當然也能直接選擇學習 Socket.io
就好,但文中的例子都會以 React
的Function Component
配合 Hooks
撰寫,就不再另外列舉在一般 HTML
的用法。
另外, Server 方面會以 Node.js
建置,還沒碰過 Node.js
的話也能參考「[筆記][node.js]第一次建置node.js開發環境和安裝npm就上手!」建置環境,有任何問題再麻煩留言告訴我,謝謝!
Socket.io
socket.io
分成兩個主要的部分,一個是負責在 Server 端啟動 WebSocket
服務的 socket.io
和在 Client 端做連結處理的 socket.io-client
,因此使用時便不需要再尋找 Server 及 Client 各需要哪些套件,一切都交給 socket.io
就行了。
基本溝通方式解說
下方會先從建立 Server 的 socket.io
開始敘述,再解釋兩邊的互動方式。
Server
以 npm init
建立一個專案後,透過下列指令安裝 express
和 socket.io
:
npm install express
npm install socket.io
安裝完後在專案內建立一個 server.js
,用來做專案的進入點運行 Server:
server.js
內有幾點需要注意的地方:
- 如果是使用
socket.io
套件,因為它不是真正的webSocket
協定,所以還是得使用http
啟動 Server ,再把 Server 物件送給socket.io
,處理過後會得到一個物件io
,可以用它的on
監聽開啟連線後的設定。 - 如果
io
監聽到有訊息從 Client 傳到 Server 時,會將捕捉到的事件內容socket
物件傳至connection
後的function
中。 - 被傳入
connection
中的socket
也可以自訂監聽message
的類型,例如上方的getMessage
就是筆者自行設定的類型名稱,socket.io
只會觸發對應的監聽Function
。 socket.emit
為回覆訊息給 Client 的Function
,它第一個參數為訊息類型,這個和 Server 的監聽名稱一樣,在 Client 端也只有設定監聽類型為getMessage
的Function
才會觸發,而第二個為訊息內容。
如果上方有不了解的部分,可以先大略看過,配合 Client 的運作一起看會更清楚。
完成 Server 的部分後可以先輸入以下指令運行:
node server.js //server.js 為檔名
如果程式沒有問題,那應該會在運行後出現 open server!
:
Client
Client 端另外開一個專案, React
的環境就不再另外講解,可以參考「webpack&React開發環境篇(1)」和「webpack&React開發環境篇(2)」上下兩篇搭建,那除了 React
的部分還需要下載 socket.io-client
:
npm install socket.io-client
下載後,就能開始實做一個連結 webSocket
並能夠發送及接收訊息的 component
:
關於此 component
的架構可以分為幾下幾塊:
- 使用
useState
在component
內建立一個state
ws
,當使用者點擊「連線」按鈕時觸發connectWebSocket
,觸發後透過從socket.io-client
套件中import
的webSocket
連線至剛剛執行server.js
的http://localhost:3000
,最後webSocket
連線成功後會藉由setWs
將WebSocket
物件寫到ws
。 - 連線後
component
的state
ws
就會改變,生命週期useEffect
也會被觸發,觸發時先判斷ws
內是否真的有值,因為useEffect
在組件第一次render
時就會先執行,但那時候ws
還沒有連線所以是null
,這時候還去執行initWebSocket
就會發生錯誤,所以如果ws
還是初始值null
就不執行接下來的initWebSocket
。 - 在
initWebSocket
中替ws
增加了監聽getMessage
的訊息,當 Server 有發送以getMessage
為名稱的訊息,就會在這裡被捕捉到,並在第二個參數中接收訊息,將訊息內容打印到console
中。 - 當我按下「送出訊息」按鈕時,
sendMessage
中會以ws.emit
送出訊息,而送出的除了訊息內容外,還有辨別他的名稱getMessage
。
在第一點中需要注意的是 ws://localhost:3000
是無法正確與 Server 做connection
的,因為 socket.io
本身是 http
協定而不是 WebSocket
,如果要看更多協議可以參考 socket.io
的協議。
看到這裡,就能夠將剛剛 Server 的程式碼拿來與 Client 一起講解兩邊溝通的流程了:
- 由 Client 以
ws.emit('getMessage','訊息內容')
將訊息送到 Server。 - Server 藉由
socket.on('getMessage',message=>{/*執行動作*/})
捕捉到訊息。 - Server 處理完訊息內容後再透過
socket.emit('getMessage','訊息內容')
將訊息傳給 Client 。 - 最後 Client 會從
ws.on('getMessage',message=>{/*執行動作*/})
的監聽取得 Server 傳來的訊息。
上面的過程都是以 getMessage
為該訊息取名字去送出及監聽捕捉,因此就不會像原生的 WebSocket
,如果要為訊息做分類就還得多做判斷,當然用 socket.io
的方便不只有這樣子而已,先看看上方程式碼的執行畫面,再講解更多有趣的地方:
進階用法 - 群發
就上方的例子而言,眼尖的讀者應該有發現,筆者在使用 socket.emit()
將訊息從 Server 傳給 Client 的時候,訊息內容為「只回傳給發送訊息的 client 」,這也代表這段訊息不會同時讓其他連接著該 WebSocket
的 Client 收到,那該怎麼做才能讓所有 Client 都收到訊息呢?
其實 socket.io
針對要發送的對象,提供了其他方式:
每種方式也都會固定帶著一個名稱被 Client 端監聽接收,如果沒有給名稱就不會被監聽給捕捉到,以下實作呈現這三種發送訊息的方式:
Server
在 Server 端要因應不同的名稱來判斷回傳的方式,因此為幾個不同的名稱做加入監聽:
Client
在 Client 中,下方更改了 sendMessage
的內容,在呼叫的時候傳入 name
作為發送訊息的名稱,並在 initWebSocket
中多為幾個名稱設定監聽,以捕獲 Server 發送的訊息,最後在 return
裡添加了幾個按鈕分別發送不同名稱的訊息:
調整完兩邊的程式後,執行結果如下:
進階用法 - 分組 ( room )
分組的意思就是為 Client 設定聊天室,在遊戲裡就像「頻道」、「分流」的概念, socket.io
能讓 Server 可以針對某個 room
傳送訊息,也可以自由地將 Client 加入或移除某個 room
,而在一般沒有設定 room 的狀態下,都會為每個 Client 預設一個 id 作為 room
。
Client
這次從 Client 開始,在程式中加上一個下拉選單讓使用者選擇房間,選擇時會送出房間名稱給 Server ,並且為 webSocket
增加一個監聽,以接收從 Server 傳送過來的新訊息:
Server
當 Server 端接收到訊息時,可以使用 socket.join
將房間 id 加到該 Client 的 room
物件中,並發送訊息給相同房間 id 的 Client ,而發送的訊息又有分成兩種:
以下分別展示兩種傳遞的結果:
發送給在同一個 room
中除了自己外的 Client :
發送給在 room 中所有的 Client :
需要注意的是如上方所說,一開始連線的時候 socket.io
會為每個 Client 預設 id 在 room
裡,所以取出 room
的時候不會只有 socket.join
進去的房間 id ,還會有預設的 id ,因此下方的程式將對 room
物件所有的 key
做迴圈,取出原本 id 以外的值就是另外 socket.join
的房間 id 了:
另外再補充 socket.join
是非同步的事件,因此如果要在確定 join
後再執行某些事得這麼做:
最後,當 Client 更換或離開聊天室時,得使用 socket.leave
移除在 socket.rooms
的 id :
中斷連線
當 Client 要與 Server 中斷連線時,可以在 Client 端使用 .close()
這個函式,中斷後會觸發在 Server 端的 disconnect
這個名稱的事件,但是中斷後便無法再透過此連結送出訊息到其他 Client 通知「某使用者已離開」,因此下方先透過送出一個訊息到 Server ,等通知完其他 Client 後,再送訊息到提出中斷連線的 Client 執行 .close()
,以下實作:
Server
以 disConnection
監聽申請中斷的事件,再以 leaveRoom
通知 room
裡的所有 Client 及 disConnection
向提出中斷的 Client 送出訊息請它做 .close()
:
Client
新增一個「斷線」的按鈕觸發 disConnectWebSocket
,讓它送訊息給 Server 告知要斷線, leaveRoom
及 disConnection
分別用來接收某個 Client 離開的通知及通知完後關閉連線:
執行結果如下:
本文不知不覺就打了有點多,提到了 socket.io
的基本用法、群發、聊天室,這些都是在實務上都滿有機會會碰到的技術,也搭配了 React
做使用,希望這篇文章能夠讓各位對 socket.io
或搭配 React
時的使用方式有些概念。
如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!
參考文章