利用 Socket.io 建立聊天室及通知功能

Ruby Lai
7 min readDec 20, 2020

--

Socket.io 是使用 emit 來將 client 或 server 端的資料發送給彼此,並使用 on 來接收資料,第一個參數代入觸發的事件名稱,第二個參數代入傳送的資料,其中 Socket.io 很貼心的在官方文件中整理出在各種情況下的使用,以下以傳送訊息為例:

首先,在 server 端建立連線,監聽 connection (連線)及 disconnect (斷線)事件。之後監聽從 client 端發送的 chatMessage 事件,寫入收到別人訊息時做的動作,例如: 將訊息存入資料庫,之後再發送 chatMessage 事件,將資料傳送至 client 端。

當按下傳送訊息按鈕時,client 端會發送 chatMessage 事件,將資料傳送給 server 端,之後會再監聽 server 端發送的 chatMessage 事件,將資料渲染至畫面上。

將以上的步驟整理如下:

1. 建立連線
2. 當使用者按下傳送訊息按鈕時,client 端會發送事件,將資料(例如: 訊息內容、使用   者資料等)傳送給 server 端
3. server 端接聽 client 端傳送的事件,並將接收的資料進行儲存等動作,之後再發送   事件至 client 端,將資料傳送給 client 端
4. client 端監聽 server 端傳送的事件,並將資料整理後渲染至頁面上
5. 若使用者離開則斷線

如此便完成利用 Socket.io 製作最基本的即時傳送訊息功能,而以下將一一介紹我在製作進階功能時遇到的問題及解決方法。

製作進階功能遇到的問題

  1. 顯示上線使用者

在公開聊天室會有一個欄位顯示上線使用者,讓使用者知道目前誰在線上;起初的想法是,在建立連線的當下,從 server 端傳送目前使用者的資訊給 client 端,讓 client 端去渲染畫面。然而此方法會造成以下兩個問題:

  • User 1 先上線,User 2 再上線,此時 User 1 的畫面會顯示自己及 User 2,但 User 2 的畫面只會顯示自己。
  • 當不斷刷新 User 1 頁面時,User 1 的頁面只會剩下自己,而 User 2 的頁面會出現自己及不斷重複出現 User 1。

深入思考過後,發現問題的原因可以歸納為以下兩點:

  • 在對方的頁面重複出現自己
    刷新頁面的當下,會先觸發 disconnect,再觸發 connection,connection 建立的同時,會將使用者渲染至畫面上,此時若沒有在 disconnect 的當下將使用者從畫面上清除,就會重複出現使用者;因此在 client 端加上以下程式碼,從 server 端收到斷線事件時,則將此使用者從畫面上清除。
  • 自己的頁面只會顯示自己
    由於在登入或是刷新頁面的當下觸發 connection,此時傳入至 client 端的資料只有正在操作此動作的使用者資料,因此畫面上只會顯示自己;於是在 server 端建立一 onlineUsers 的陣列來記錄目前上線的使用者,並且將重複出現的移除。另外,在 client 端每次收到上線事件時,將畫面上的使用者資料清除,以免資料累積在畫面上。

Server 端

Client 端

2. 傳送私人訊息

在先前的文章中提過利用傳送者的 UserId 及接收訊息者的 UserId 來建立私人聊天室,以及在發送訊息的當下讓使用這 join room 來接收私人訊息。然而此方法會造成以下兩個問題:

  • 傳送私人訊息時,不僅與接收訊息者的對話框會收到訊息,傳送者與自己的對話框也會收到此訊息。
  • 對方一開始傳送的訊息,接收訊息者不會立即看到,要在接收訊息者按下傳送訊息按鈕後才能看到。

深入思考過後發現問題是由以下兩點所導致:

  • 傳送者與接收訊息者對話框的 room number 是來自接收訊息者的 UserId,而傳送者與自己對話框的 room number 是來自自己的 UserId,因此在傳送時會同時傳入此兩間 room;於是將接收訊息者的 UserId 及傳送者的 UserId 以字串方式合併,便可阻隔訊息傳入至傳送者與自己的對話框。
    例如: User 2 傳給 User 1,將使用者加入 room number 21 及 12,而此時User 2 與自己對話框的 room number 為 22。
  • 由於 join room 的時機點設在監聽"按下傳送訊息按鈕"之後,接收訊息者在登入的當下並沒有 join room 中,因此自然接收不到對方傳送的訊息;於是將 join room 的時機點移至最外層,在登入的同時即 join room。

3. 計算未讀訊息數量

起初的想法是與私人訊息相同,利用 socket.to('room number').emit 的方法將通知傳送至各別的 room,但此方法會造成若使用者在私人聊天室以外的頁面時並不會接收到通知。

深入思考過後,發現問題的原因是由於 room number 是由接收訊息者的 UserId 及 傳送者的 UserId 所組成,所以接受訊息者在按下與對方的對話框時才有辦法 join 至此 room number。於是將 server 端改為傳送通知給所有人,最後在 client 端做阻擋 if(data.receiveId === userId)

Server 端

Client 端

4. 通知功能

當訂閱者推文、被人追蹤、推文被人按讚或留言時發送通知給使用者,實作此功能的方式其實與發送訊息大同小異,唯一多出來的功能是點選通知後,會導到對方的留言、推文或使用者頁面。而實作的方式即是 server 端在 Notice 資料庫建立資料的同時,將資料傳至 client 端來渲染頁面。

後記

在製作聊天室功能時遇到許多問題,而我也從中得到一些解問題的小撇步。

  1. 統整並釐清問題發生的原因。
    例如:第一點提及的上線使用者問題,我是在多次刷新頁面研究後,才將問題發生的原因歸納為兩種,並依此解決問題。
  2. 將問題轉化為文字,程式碼不是我們熟悉的語言,中文才是,將複雜的程式碼轉化為淺顯易懂的文字有助於思考。
    例如:第一點提及的上線使用者會累積在畫面上的問題,我想了很久以及嘗試各種方法(例如: 在前端做篩選等)都沒辦法成功解決,最後我想說留到明天來解,於是便在筆記本上記下,"原本的使用者沒清除又將資料 append 進去"這幾個文字時,我突然茅塞頓開,發現其實問題很簡單,只要加上 userList.innerHTML = “ “ 此程式碼,先將使用者從畫面上清除,即能解掉此問題。

Github: https://github.com/Rubyrubylai/twitter

網站: https://secure-sierra-32583.herokuapp.com/

--

--