[MERN] 使用 socket.io 實現即時通信功能

[MERN] 使用 socket.io 實現即時通信功能

效果實現如下:

MERN-socket.io 實現即時聊天

Github 項目地址:https://github.com/GoldenaArcher/messenger-mern

項目使用了 MERN(MongoDB, Express, React, Node.js) + socket.io 實現即時通信功能,并且使用了Turborepo進行 monorepo 的后端、前端和 socket 的模塊管理

用到的具體技術棧有:

  • MongoDB

    實現了數據的持久化

  • Express

    實現的后端,負責登陸/注冊/獲取聊天記錄的功能

  • React

    實現 UI 功能

  • Redux

    實現了 UI 部分的狀態管理

    這次也是第一次嘗試使用 RTQK 進行 CRUD 的操作,大體使用感覺是比 RTK 還要簡單很多,loading/error 的狀態全都由 RTKQ 進行管理,確實少寫了很多的代碼

    如果要單獨進行狀態的更新,實現起來也不太難,在 onQueryStarted 里面就可以通過 queryFulfilled 去進行操作。因為 api 是 cache 的,所以可以通過調動 invalidateTags 去自動重新調用 fetch api,使用起來確實方便很多

    唯一稍微有點麻煩的可能是 debug,我在實現信息的 delivered/read 這部分狀態更新的時候卡 bug 卡了一段時間,主要是 RTKQ 在沒有遇到硬核報錯,并且有些情況下出現 cache 里找不到對應的數據這種情況下,會自動放棄(abort)剩下的操作。因為沒有在 devtool 里看到任何的報錯,函數里的 log 也沒有被觸發,所以在 debug 的時候也確實是折騰了挺久的……

  • turborepo

    實現了 monorepo 的管理

    其實這個配置起來還挺簡單的,因為項目比較簡單,所以也沒有辦法進行一個和 yarn workspace 的對比,主要還是為了使用 一個指令 就能啟動所有的項目,減少反復更新 concurrently 去運行對應的模塊

    底層上來說 turborepo 還是使用了 yarn workspace,不過官網上說 turborepo 可以只重新編譯修改過的模塊,而 yarn workspace 是會更新所有的模塊

    從 bash 上更新的情況上來說是這樣的,不過主要問題就是項目體量太小了,確實沒有辦法感受出差別來

  • docker

    這個為了方便一鍵運行整個項目,只要跑一下 docker compose up -d 就行了

MongoDB

上次用 mongo 還是上次的事情了,這次的設計相對而言比較簡單,就用了 2 個 model,一個是 user,一個是 message。這方面主要還是一個復習+學習,常見的 schema 比較簡單,但是比較有趣的東西吧……一個是 aggregate,另一個是用 find+updateMany 比起 updateMany+find 的搭配可以提升一部分的性能——其主要原因是數據庫在 mutation 后會出現一些延遲

但是從另一個方面來說,實用 find+updateMany 的操作也不是絕對的。在實際應用期間,我們為了保證數據的一致性,后端都是用 updateMany+find;后端返回給前端時,如果是 update 的操作,其實并不會返回具體的數據,只會返回一個 null,所以我們其實都是依賴前端的數據進行的修改……

所以,具體怎么操作還是一個 case by case 的操作,對于目前這個 app,二者的差異其實不太大

其實我還是不太會寫 query,后端做的少這個是真沒什么辦法,只能說有空的時候多看看、多寫寫,多少補一點吧

具體到實踐就是,寫的還是簡單了一些,對于 user 的設計其實沒有做好友的部分,所以 demo 里的好友實現還是拉了一下數據庫里所有存在的 users

message 的設計目前來說是可以滿足需求的,雖然之前有在考慮設計一下 conversation 這個 schema 提升獲取最后消息的功能,但是鑒于還沒有做群組聊天這一功能,想想也就算了……

Express

express 這部分沒什么特別大的難點,總體來說使用還是比較簡單的,畢竟 nodejs 作為服務端配置是真的簡單……

目前來說主要做了 3 個路由:

  • auth,這個就是比較簡單的登錄和注冊的驗證
  • friend,這個是獲取好友列表
  • message,這個就是獲取信息相關的路由

middleware 做了兩個:

  • auth,負責 verify/encode/decode jwt,以及將 decode 的 jwt 加到 request 中

  • upload,負責文件上傳的部分

    文件會上傳到 server 的 /upload 文件下,同時用戶要獲取文件的話,就可以獲取這個文件夾下 host 的靜態文件

    上傳的主要實現時通過 Formidable 做的,在 v3 之后 Formidable 會返回一個數組而不是單獨的屬性,所以我新寫了一個 util 文件去負責這部分

demo 的時候沒做注冊,不過功能差不多都在這里了,唯一不太確定的是上傳文件這塊……我試過了上傳圖片并且渲染成功,但是上傳其他類型的文件還真沒試過

總體來說,express 部分做的和我記憶里面沒什么特別大的差別,流程大概就是:

  • 設計并實現對應的 controller,負責相關的 CRUD 操作
  • 根據情況選擇是否需要添加/實現 middleware
  • 設計一個 route,并且決定對應的 CRUD 操作
  • 在 server.js 里面使用對應的 route,使得 express 能夠找到相關的路徑

或者反過來也行,先搞定 route 再實現 controller

React/Redux

React 部分沒有用什么特別先進的東西,基本上就是 16.8 以后用到現在的 hooks,這次主要升級的部分還是 redux——第一次嘗試全局用 RTKQ 操作

早起的項目(2020 年前)用的都是 redux/react-redux+redux thunk 的實現,當時 redux 本身對于異步 api 的支持是比較差的,所以 redux-thunk 和 redux-saga 的應用還是比較廣泛。我們當時的考量是為了減少依賴的實現,所以只考慮了 thunk,沒有使用 saga

后來也簡單地了解了一下 saga 的用法,不過因為馬上就用到了 redux toolkit,后面就直接換到了 RTK 的 createSlice,對于比較簡單的實現不需要重復寫 action,也就一直用了下去。期間的確是發現,使用 asyncThunk 的時候,還是需要寫不少的 reducers,并且同樣需要自己管理 loading 相關的狀態。當然,這方面和之前使用 redux-thunk 時沒什么太大的差別,所以一直以來感覺有點麻煩,但是可以接受

但是使用 RTKQ 是另外一種感覺了,RTKQ 本身就管理了 loading 和 error 的狀態,讓使用和調用都變得簡單很多。并且因為 RTKQ 本身是 cache 了 API 調用的,所以它的調用不需要額外找 state,直接從導出的 useFunction 去找 data 就行了,二者在 React 中的 component 調用對比如下:

// slice
const { userInfo } = useSelector((state) => state.auth);// RTKQ
const { data, isFetching, error } = useFetchUsersQuery();

二者差別好像不是特別大,但是使用 (state) => state.auth 這種操作最大的問題還是在于 typo——雖然 slice 也可以導出 reducerPath ,不過 slice 層面的導入/導出還是需要額外的操作,我們一般都是直接寫對應的狀態

實現方面 RTKQ 就干凈得多,以一個相對比較簡單的 fetch query 為例:

fetchMessages: builder.query({query: ({ sender, receiver }) => ({url: `/messages`,method: "GET",params: {sender,receiver,},}),transformResponse: (res) => res.data,
});

主要是不用管理 error 和 loading 的狀態,所以就不用像傳統寫法那樣寫 pending, fulfilled, rejected 三個 actions

之余在 mutation 中更新 query 的數據,這個實現方法是這樣的:

postMessage: builder.mutation({query: (data) => ({url: "/messages",method: "POST",data,}),transformResponse: (res) => res.data,async onQueryStarted(arg, { dispatch, queryFulfilled, getState }) {await updateMessageCache({queryFulfilled,dispatch,getState,messageApi,friendApi,});},
});

updateMessageCache 的實現:

export const updateMessageCache = async ({queryFulfilled,messageApi,friendApi,getState,dispatch,
}) => {try {const { data } = await queryFulfilled;dispatch(messageApi.util.updateQueryData("fetchMessages",{ sender: data.sender, receiver: data.receiver },(messageList) => {messageList.push(data);}));const friendsState = getState()[friendApi.reducerPath];const friendList =friendsState?.queries?.["fetchFriends(undefined)"]?.data || [];if (friendList.length > 0) {dispatch(messageApi.util.updateQueryData("fetchLastFriendMessages",{ friendList },(lastMessages) => {const existingIndex = lastMessages.findIndex((msg) =>(msg.sender === data.sender &&msg.receiver === data.receiver) ||(msg.sender === data.receiver && msg.receiver === data.sender));if (existingIndex !== -1) {lastMessages[existingIndex] = data;} else {lastMessages.push(data);}}));}} catch (error) {console.error("Error authenticating user:", error);}
};

這里需要注意的是,在 updateQueryData 更新數據時,參數必須要和在 builder 的定義是一樣的。如果是無參函數,那么寫法如下: fetchDemo(undefined),需要傳一個 undefined,也不能留空

還需要注意的一點就是,如果在 updateQueryData 調用的方法里面找不到對應的數據,那么整個 callback 里面方法都不會被調用,而且也不會有任何的報錯

fetchMessages 為例,它的代碼是這樣的:

messageApi.util.updateQueryData("fetchMessages",{ sender: data.sender, receiver: data.receiver },(messageList) => {messageList.push(data);}
);

我當時的情況是,我在不同的好友列表上,也就是說 senderreceiver 和當前的數據不同,因此這段代碼就沒有觸發,callback 也就沒有執行。這個邏輯藏的還是挺深的,后來在這個 stack overflow 上找到的答案:Unable to trigger updateQueryData in redux-toolkit。這里面提到,如果整個 cache entry 不存在,即 fetchMessages 之前沒有被調用過,那么這個 callback 也不會被執行

另外之前本來想著說多研究一下 invalidateTag 怎么用的,結果最后還是沒怎么用上……

socket.io

socket.io 的使用還是比較簡單的,這里沒有用到特別難的地方,主要就是用到了 socket.on(event, callback)socket.emit(event, callback),并且所有的操作都是從 socket 這部分執行的

基本邏輯就是:UI 觸發一個 event,socket 接收到這個 event,在模塊內處理完必須的邏輯后,再 emit 一個新的 event。UI 端監聽到對應的 event 后,也會負責實現對應的邏輯,包括更新 redux 中的數據,或者是觸發另一個 rest api 調用

簡單的說就是,這個項目只用到了比較初級的 socket.io 進行實現——畢竟除了在當前的 socket 上發消息之外,就是通過 to 給指定的用戶發消息。但是 socket.io 本身的能力還是挺強的,更廣的功能目前還沒辦法用到,比如說:

  • 廣播消息

    類似于游戲里的世界廣播那種功能

  • 創建 room 和 namespace

    這個我其實有點想做的,比如說上面提到的創建一個 conversation schema,和做 group chat 就可以做這個功能

    不過想想先維持基本功能的實現吧

  • acknowledge

    這個看了下,大體是如果 socket 的信息完成傳輸后,可以直接調用 callback 去確認信息已經傳輸完成

    在當前項目里的例子大體是:當用戶發送信息后,就可以直接在后端更新消息已經 deliver 的操作,再通過 socket 將消息傳到前端

    對于現在的實現——即用戶 A 發送信息后通過 socket 發送給用戶 B 新消息已經發送,用戶 B 在 UI 接收到這個消息后,發送一個 rest api 調用到后端,再將消息通過 rest api 結果返回到前端,前端再對 UI 進行更新

    比起來,這里少了一個 socket 到 UI,UI 調用 rest api 的交流,因此對于性能的提升也是顯著的

    沒這么做的原因是想做 MFE 的……后面會提到

  • middleware

    和 acknowledge 類似,可以直接在 socket 中驗證用戶,減少客戶端對 API 調用

  • 自動重連

    我知道有這個功能,沒具體研究過

  • MFE

    因為現在是把 socket 單獨拉出來做了一個模塊,之前有在考慮升級到 MFE 的實現,不過這樣就會涉及到多個 socket instance,然后解決方案就需要升級到 redis。目前我對于 redis 的使用是項目里有用,一直想看,一直還沒抽出時間來具體研究是怎么做的

    所以升級到 MFE 會上比較大的強度,因此暫時就擱置了。可能等到之后有空會回來看看吧……

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/73093.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/73093.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/73093.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【菜鳥飛】Conda安裝部署與vscode的結合使用

介紹 Conda 是一個跨平臺的開源工具,用于管理軟件包和環境。最初由 Anaconda 公司開發,它的設計目標是支持數據科學和機器學習領域,但其功能不僅局限于此。 以下是 Conda 的核心特點: 包管理:安裝、更新、卸載各種庫…

《Android應用性能優化全解析:常見問題與解決方案》

目錄 一、UI卡頓/掉幀 二、內存泄漏(Memory Leak) 三、ANR(Application Not Responding) 四、列表滑動卡頓(RecyclerView/ListView) 五、冷啟動耗時過長 六、內存抖動(Memory Churn&#x…

【MySQL是怎么運行的】0、名詞解釋

聚簇索引:聚簇索引和數據在一起,又名主鍵索引,是主鍵id構建的一顆B樹,非葉節點是主鍵id,葉子節點是真實數據。其他索引統稱二級索引,也稱為非聚簇索引。覆蓋索引:查找的數據就在索引樹上&#x…

深入解析 TCP 協議【真題】

傳輸控制協議(TCP)解析與題目解析 題目解析 關于傳輸控制協議(TCP)表述不正確的是? A. 主機尋址 B. 進程尋址 C. 流量控制 D. 差錯控制 TCP(Transmission Control Protocol)是面向連接、可靠傳…

單例模式的五種實現方式

1、餓漢式 ①實現:在類加載的時候就初始化實例 ②優點:線程安全 ③缺點:實例在類加載的時候創建,可能會浪費資源 //餓漢式 public class EagerSingleton{private EagerSingleton(){} //私有構造方法private static EagerSingle…

SwiftUI 讓視圖自適應高度的 6 種方法(四)

概覽 在 SwiftUI 的世界里,我們無數次都夢想著視圖可以自動根據布局上下文“因勢而變”?。大多數情況下,SwiftUI 會將每個視圖尺寸處理的井井有條,不過在某些時候我們還是得親力親為。 如上圖所示,無論頂部 TabView 容器里子視圖…

小程序SSL證書過期怎么辦?

SSL證書就像小程序的“安全鎖”,一旦過期,用戶訪問時會被提示“不安全”,輕則流失客戶,重則數據泄露!作為企業負責人,如何快速解決證書過期問題?又該如何避免再次踩坑?這篇指南給你答…

ClickHouse優化技巧實戰指南:從原理到案例解析

目錄 ?ClickHouse優化核心思想?表結構設計優化?查詢性能優化技巧?數據寫入優化方案?系統配置調優實戰?高可用與集群優化?真實案例解析?總結與建議 1. ClickHouse優化核心思想 ClickHouse作為OLAP領域的明星引擎,其優化需遵循列式存儲特性,把握…

DeepSeek 助力 Vue3 開發:打造絲滑的表格(Table)之添加列寬調整功能,示例Table14_02帶邊框和斑馬紋的固定表頭表格

前言:哈嘍,大家好,今天給大家分享一篇文章!并提供具體代碼幫助大家深入理解,徹底掌握!創作不易,如果能幫助到大家或者給大家一些靈感和啟發,歡迎收藏關注哦 💕 目錄 Deep…

服務自動被kill掉的原因和查看

服務在運行一段時間后被自動kill掉可能是由多種原因引起的,包括系統資源限制、進程管理策略、應用程序錯誤等。以下是一些常見的原因以及定位問題的過程: 常見原因 系統資源限制: 內存不足:如果服務消耗了過多的內存,系統可能會kill掉該進程以釋放內存資源。CPU使用過高:…

基礎算法——順序表

一、詢問學號 題?來源&#xff1a;洛? 題?鏈接&#xff1a;P3156 【深基15.例1】詢問學號 - 洛谷 難度系數&#xff1a;★ 1. 題目描述 2. 算法原理 直接? vector 或者數組模擬即可。 3. 參考代碼 #include <iostream> #include <vector>using namespace st…

Ubuntu用戶安裝cpolar內網穿透

前言 Cpolar作為一款體積小巧卻功能強大的內網穿透軟件&#xff0c;不僅能夠在多種環境和應用場景中發揮巨大作用&#xff0c;還能適應多種操作系統&#xff0c;應用最為廣泛的Windows、Mac OS系統自不必多說&#xff0c;稍顯小眾的Linux、樹莓派、群輝等也在起支持之列&#…

C#實現高性能異步文件下載器(支持進度顯示/斷點續傳)

一、應用場景分析 異步文件下載器用處很大&#xff0c;當我們需要實現以下功能時可以用的上&#xff1a; 大文件下載&#xff08;如4K視頻/安裝包&#xff09; 避免UI線程阻塞&#xff0c;保證界面流暢響應多任務并行下載 支持同時下載多個文件&#xff0c;提升帶寬利用率后臺…

Oracle比較好的幾本書籍

1.《Oracle專家高級編程》 2.《Oracle高效設計》 3.《Oracle9i&10g&11g編程藝術深入數據庫體系結構》 4.《讓Oracle跑的更快》(1/2) ....... n.《Oracle官方文檔的閱讀》下面包括這幾個部分&#xff0c;可以跟進研讀一下&#xff1a; &#xff08;1&#xff09;《…

js和java中方法重載(js本身是不支持方法重載,方便對比學習)

js如果需要實現方法重載 示例 1&#xff1a;根據參數數量實現重載 function overloadExample() {if (arguments.length 1) {console.log(一個參數:, arguments[0]);} else if (arguments.length 2) {console.log(兩個參數:, arguments[0], arguments[1]);} else {console.l…

Android : Camera之CHI API

來自&#xff1a; https://www.cnblogs.com/szsky/articles/10861918.html 一、CAM CHI API功能介紹&#xff1a; CHI API建立在Google HAL3的靈活性基礎之上&#xff0c;目的是將Camera2/HAL3接口分離出來用于使用相機功能&#xff0c;它是一個靈活的圖像處理驅動程序&#…

Netty基礎—2.網絡編程基礎四

大綱 1.網絡編程簡介 2.BIO網絡編程 3.AIO網絡編程 4.NIO網絡編程之Buffer 5.NIO網絡編程之實戰 6.NIO網絡編程之Reactor模式 5.NIO網絡編程之Buffer (1)Buffer的作用 Buffer的作用是方便讀寫通道(Channel)中的數據。首先數據是從通道(Channel)讀入緩沖區&#xff0c;從…

Git前言(版本控制)

1.Git 目前世界上最先進的分布式版本控制系統。 git官網&#xff1a;https://git-scm.com/ 2.版本控制 2.1什么是版本控制 版本控制(Revision control)是一種在開發的過程中用于管理我們對文件、目錄或工程等內容修改歷史&#xff0c;方便查看更改歷史記錄備份以便恢復以前…

調試正常 ≠ 運行正常:Keil5中MicroLIB的“量子態BUG”破解實錄

調試正常 ≠ 運行正常&#xff1a;Keil5中MicroLIB的“量子態BUG”破解實錄——從勾選一個選項到理解半主機模式&#xff0c;嵌入式開發的認知升級 &#x1f4cc; 現象描述&#xff1a;調試與燒錄的詭異差異 在線調試時 程序正常運行 - 獨立運行時 設備無響應 ! 編譯過程 0 Err…

算法每日一練 (9)

&#x1f4a2;歡迎來到張胤塵的技術站 &#x1f4a5;技術如江河&#xff0c;匯聚眾志成。代碼似星辰&#xff0c;照亮行征程。開源精神長&#xff0c;傳承永不忘。攜手共前行&#xff0c;未來更輝煌&#x1f4a5; 文章目錄 算法每日一練 (9)最小路徑和題目描述解題思路解題代碼…