paint-brush
Next.js 和 SignalR:轻松的套接字集成和故障排除经过@chilledcowfan
5,339 讀數
5,339 讀數

Next.js 和 SignalR:轻松的套接字集成和故障排除

经过 Anton Burduzha8m2023/09/05
Read on Terminal Reader

太長; 讀書

大量项目需要 Web 套接字集成,以提供界面对更改的即时反应,而无需重新获取数据。我们不会讨论它们或对提供 API 以获得更好开发体验的第三方库进行比较。我的目标是展示如何将“@microsoft/signalr”与 NextJs 快速集成。
featured image - Next.js 和 SignalR:轻松的套接字集成和故障排除
Anton Burduzha HackerNoon profile picture

少数(但仍然是大量)项目需要 Web 套接字集成,以提供界面对更改的即时反应,而无需重新获取数据。


这是一件很重要的事情,我们不会讨论它们或对提供 API 以获得更好的开发体验的 3rd 方库进行比较。


我的目标是展示如何将@microsoft/signalr与 NextJs 快速集成。以及我们在开发过程中遇到的问题如何解决。


我希望大家已经在本地安装并部署了 NextJS 项目。就我而言,版本是13.2.4 。让我们添加一些更重要的库: swr (版本2.1.5 )用于数据获取并进一步使用本地缓存和@microsoft/signalr (版本7.0.5 ) - 用于 Web 套接字的 API。


 npm install --save @microsoft/signalr swr


让我们首先创建一个简单的fetcher函数和一个名为useChatData的新挂钩,以从 REST API 获取初始数据。它返回聊天消息的列表、检测错误和加载状态的字段以及允许更改缓存数据的方法mutate


 // hooks/useChatData.ts import useSWR from 'swr'; type Message = { content: string; createdAt: Date; id: string; }; async function fetcher<TResponse>(url: string, config: RequestInit): Promise<TResponse> { const response = await fetch(url, config); if (!response.ok) { throw response; } return await response.json(); } export const useChatData = () => { const { data, error, isLoading, mutate } = useSWR<Message[]>('OUR_API_URL', fetcher); return { data: data || [], isLoading, isError: error, mutate, }; };


为了测试它是否按预期工作,让我们更新我们的页面组件。在顶部导入我们的钩子,并从中提取数据,如下面的代码片段所示。如果有效,您将看到渲染的数据。如您所见,这非常简单。


 // pages/chat.ts import { useChatData } from 'hooks/useChatData'; const Chat: NextPage = () => { const { data } = useChatData(); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };


下一步需要将我们未来的页面连接到 Web 套接字,捕获NewMessage事件,并使用新消息更新缓存。我建议从在单独的文件中构建套接字服务开始。


根据 SignalR 文档中的示例,我们必须创建一个连接实例以进一步监听事件。我还添加了一个用于防止重复的连接对象和两个用于启动/停止连接的帮助程序。


 // api/socket.ts import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; let connections = {} as { [key: string]: { type: string; connection: HubConnection; started: boolean } }; function createConnection(messageType: string) { const connectionObj = connections[messageType]; if (!connectionObj) { console.log('SOCKET: Registering on server events ', messageType); const connection = new HubConnectionBuilder() .withUrl('API_URL', { logger: LogLevel.Information, withCredentials: false, }) .withAutomaticReconnect() .build(); connections[messageType] = { type: messageType, connection: connection, started: false, }; return connection; } else { return connections[messageType].connection; } } function startConnection(messageType: string) { const connectionObj = connections[messageType]; if (!connectionObj.started) { connectionObj.connection.start().catch(err => console.error('SOCKET: ', err.toString())); connectionObj.started = true; } } function stopConnection(messageType: string) { const connectionObj = connections[messageType]; if (connectionObj) { console.log('SOCKET: Stoping connection ', messageType); connectionObj.connection.stop(); connectionObj.started = false; } } function registerOnServerEvents( messageType: string, callback: (payload: Message) => void, ) { try { const connection = createConnection(messageType); connection.on('NewIncomingMessage', (payload: Message) => { callback(payload); }); connection.onclose(() => stopConnection(messageType)); startConnection(messageType); } catch (error) { console.error('SOCKET: ', error); } } export const socketService = { registerOnServerEvents, stopConnection, };


现在,我们的页面可能看起来像代码片段中的那样。我们使用消息列表获取和提取data并渲染它们。另外,上面的useEffect注册了NewMessage事件,创建连接,并监听后端。


当事件触发时,挂钩中的mutate方法会使用新对象更新现有列表。


 // pages/chat.ts import { useChatData } from 'hooks/useChatData'; import { socketService } from 'api/socket'; const Chat: NextPage = () => { const { data } = useChatData(); useEffect(() => { socketService.registerOnServerEvents( 'NewMessage', (payload: Message) => { mutate(() => [...data, payload], { revalidate: false }); } ); }, [data]); useEffect(() => { return () => { socketService.stopConnection('NewMessage'); }; }, []); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };


对我来说看起来不错,它有效,我们可以看到新消息如何出现在提要中。我选择了聊天的基本示例,因为它清晰易懂。当然,您可以按照自己的逻辑应用它。

小额奖金

使用其中一个版本( @microsoft/signalr ),我们遇到了重复问题。它连接到依赖数组useEffect 。每次改变依赖关系时, connection.on(event, callback);缓存回调并一次又一次触发它。


 useEffect(() => { // data equals [] by default (registerOnServerEvents 1 run), // but after initial data fetching it changes (registerOnServerEvents 2 run) // each event changes data and triggers runnning of registerOnServerEvents socketService.registerOnServerEvents( 'NewMessage', // callback cached (payload: Message) => { // mutate called multiple times on each data change mutate(() => [...data, payload], { revalidate: false }); } ); }, [data]); // after getting 3 messages events, we had got 4 messages rendered lol


我们发现的最快、最可靠的解决方案是在 React ref中保留一份数据副本,并在useEffect中使用它以供将来更新。


 // pages/chat.ts import { useChatData } from 'hooks/useChatData'; import { socketService } from 'api/socket'; const Chat: NextPage = () => { const { data } = useChatData(); const messagesRef = useRef<Message[]>([]); useEffect(() => { messagesRef.current = chatData; }, [chatData]); useEffect(() => { socketService.registerOnServerEvents( 'NewMessage', (payload: Message) => { const messagesCopy = messagesRef.current.slice(); mutate(() => [...messagesCopy, payload], { revalidate: false }); } ); }, [data]); useEffect(() => { return () => { socketService.stopConnection('NewMessage'); }; }, []); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };


目前,我们使用新版本的@microsoft/signalr ,它似乎已经进行了必要的修复。但无论如何,如果有人发现这个解决方案有用并使用这个解决方法,我会很高兴。总而言之,我想说我对 SignalR 的体验非常积极,安装不需要任何特定的依赖项或设置,并且它运行良好并满足我们的需求。