如何在 React 中正確的使用 socket.io 客戶(hù)端?
最近在一個(gè) React 項(xiàng)目中,使用到了 socket.io 處理即時(shí)消息,這里面有幾點(diǎn)容易被忽視的問(wèn)題,例如:在 React 單頁(yè)面應(yīng)用中如何防止出現(xiàn)多個(gè) socket 實(shí)例、在任意的的組件內(nèi)如何方便的取到 socket 實(shí)例、對(duì)于某個(gè)事件不要隨著頁(yè)面切換出現(xiàn)多個(gè)監(jiān)聽(tīng)器。
在本文中,將會(huì)給大家分享下我在 React 中使用 Socket.io 客戶(hù)端的一些經(jīng)驗(yàn),希望對(duì)此有疑惑的朋友給予一些幫助,也許你會(huì)有一些更好的實(shí)現(xiàn)方式,歡迎交流!
創(chuàng)建 Socket Context
本文的實(shí)現(xiàn)方式是使用狀態(tài)管理工具保存 socket 實(shí)例,供子組件使用,如果使用了 React Hooks,可以用其提供的 useContext API,實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單。
// contexts/socket.tsx
import { createContext, ReactNode, useContext } from 'react';
import io, { Socket } from 'socket.io-client';
const SOCKET_URL = 'ws://localhost:8080';
export const socket = io(SOCKET_URL, {
transports: ['websocket'],
});
const SocketContext = createContext<Socket>(socket);
SocketContext.displayName = 'SocketContext';
export const SocketProvider = ({ children }: { children: ReactNode }) => (
<SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
);
export const useSocket = () => {
const context = useContext(SocketContext);
return context;
};
// contexts/index.tsx
import { ReactNode } from 'react';
import { SocketProvider } from './Socket';
const AppContextProviders = ({ children }: { children: ReactNode }) => (
<SocketProvider>{children}</SocketProvider>
);
export default AppContextProviders;
其中 const socket = io(SOCKET_URL)?,有些朋友可能就有疑問(wèn)了,為什么不執(zhí)行下 socket.connect() 呢?
socket.io 客戶(hù)端默認(rèn)是自動(dòng)鏈接的,如果聲明了 autoConnect 屬性為 false,則需要手動(dòng)執(zhí)行下鏈接。
以上,在頁(yè)面第一次加載時(shí)會(huì)初始化 socket,解決了第一個(gè)問(wèn)題:“React 單頁(yè)面應(yīng)用中如何防止出現(xiàn)多個(gè) socket 實(shí)例”。
根組件提供 socket
在項(xiàng)目的 App.js 文件中引入我們自定義的 Providers,將 AppProviders 組件做為根組件放在最頂層,這樣被包裹的組件都可以使用 AppProviders 組件提供的屬性。也解決了第二個(gè)問(wèn)題:“在任意的的組件內(nèi)如何方便的取到 socket 實(shí)例”。
import AppProviders from './contexts';
import './App.css';
const App = () => (
<AppProviders>
...
</AppProviders>
);
export default App;
任意子組件中使用 socket
組件 A,監(jiān)聽(tīng)服務(wù)器發(fā)來(lái)的消息。
useEffect() 是 React 內(nèi)置的一個(gè) Hook,如果第二個(gè)參數(shù)依賴(lài)項(xiàng)數(shù)組為空,那么傳入的第一個(gè)函數(shù)在該組件內(nèi)只會(huì)執(zhí)行一次,依賴(lài)項(xiàng)數(shù)組只要有一個(gè)狀態(tài)被更新,useEffect() 傳入的第一個(gè)函數(shù)也將會(huì)被執(zhí)行。
還需要注意的是 useEffect() 傳入的第一個(gè)函數(shù),它又返回的函數(shù)在函數(shù)組件卸載時(shí)被調(diào)用,通常我們會(huì)用 useEffect() 模擬類(lèi)組件的 componentDidMount、componentWillUnmount 行為。
在組件卸載時(shí),使用 socket.off() 移除事件監(jiān)聽(tīng)器,實(shí)際上這可以預(yù)防內(nèi)存泄漏,同時(shí)也解決了最開(kāi)始提的第三個(gè)問(wèn)題:“對(duì)于某個(gè)事件不要隨著頁(yè)面切換出現(xiàn)多個(gè)監(jiān)聽(tīng)器”。
import { useEffect } from 'react';
import { useSocket } from '../../contexts/Socket';
const ComponentA = () => {
const socket = useSocket();
useEffect(() => {
// componentDidMount
socket.on('message', handleMessage); // 監(jiān)聽(tīng)消息
return () => {
// componentWillUnmount
socket.off('message', handleMessage);
};
}, [socket]);
return ();
};
export default ComponentA;
組件 B,發(fā)送消息到服務(wù)器。
在我們的組件 B 中,也可以使用自定義的 useSocket Hook 獲取最開(kāi)始初始化的 socket 實(shí)例,但這并不會(huì)產(chǎn)生一個(gè)新的 socket 實(shí)例。
import { useEffect } from 'react';
import { useSocket } from '../../contexts/Socket';
const ComponentB = () => {
const socket = useSocket();
const handleSendMessage = () => {
socket.emit('compress', data); // 發(fā)送消息
}
return <div>
// ...
<button onClick={handleSendMessage}>Send message</button>
</div>;
};
export default ComponentB;