Eine Minderheit, aber immer noch eine große Anzahl von Projekten erfordert die Integration von Web-Sockets, um eine sofortige Reaktion einer Schnittstelle auf Änderungen zu ermöglichen, ohne dass Daten erneut abgerufen werden müssen.
Es ist eine wesentliche Sache, und wir werden nicht darüber sprechen oder einen Vergleich zwischen Bibliotheken von Drittanbietern anstellen, die APIs für ein besseres Entwicklungserlebnis bereitstellen.
Mein Ziel ist es zu zeigen, wie man @microsoft/signalr
schnell in NextJs integrieren kann. Und wie wir die Probleme lösen können, mit denen wir während der Entwicklung konfrontiert waren.
Ich hoffe, dass jeder das NextJS-Projekt bereits lokal installiert und bereitgestellt hat. In meinem Fall ist die Version 13.2.4
. Fügen wir einige weitere wichtige Bibliotheken hinzu: swr
(Version 2.1.5
) für den Datenabruf und die weitere Arbeit mit dem lokalen Cache und @microsoft/signalr
(Version 7.0.5
) – API für Web-Sockets.
npm install --save @microsoft/signalr swr
Beginnen wir mit der Erstellung einer einfachen fetcher
und eines neuen Hooks namens useChatData
, um Anfangsdaten von unserer REST-API abzurufen. Es gibt eine Liste der Nachrichten für den Chat, Felder zur Erkennung von Fehlern und des Ladestatus sowie die Methode mutate
zurück, mit der zwischengespeicherte Daten geändert werden können.
// 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, }; };
Um zu testen, ob es wie vorgesehen funktioniert, aktualisieren wir unsere Seitenkomponente. Importieren Sie unseren Hook oben und extrahieren Sie Daten daraus, wie im Snippet unten. Wenn es funktioniert, werden die gerenderten Daten angezeigt. Wie Sie sehen, ist es ganz einfach.
// 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> ); };
Im nächsten Schritt müssen wir unsere zukünftige Seite mit Web-Sockets verbinden, NewMessage
Ereignisse abfangen und einen Cache mit einer neuen Nachricht aktualisieren. Ich schlage vor, mit dem Aufbau des Socket-Dienstes in einer separaten Datei zu beginnen.
Gemäß den Beispielen in den SignalR-Dokumenten müssen wir eine Verbindungsinstanz für das weitere Abhören von Ereignissen erstellen. Ich habe außerdem ein Verbindungsobjekt zum Verhindern von Duplikaten und zwei Helfer zum Starten/Stoppen der Verbindungen hinzugefügt.
// 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, };
Nun könnte unsere Seite also wie im Codeausschnitt aussehen. Wir holen und extrahieren data
mit der Liste der Nachrichten und rendern sie. Außerdem registriert useEffect
oben das NewMessage
Ereignis, erstellt eine Verbindung und lauscht auf das Backend.
Wenn das Ereignis ausgelöst wird, aktualisiert die mutate
Methode des Hooks die vorhandene Liste mit einem neuen Objekt.
// 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> ); };
Für mich sieht es gut aus, es funktioniert und wir sehen, wie neue Nachrichten im Feed erscheinen. Ich habe das einfache Beispiel mit Chat gewählt, weil es klar und leicht verständlich ist. Und natürlich wenden Sie es auf Ihre eigene Logik an.
Bei Verwendung einer der Versionen ( @microsoft/signalr
) hatten wir ein Problem mit Duplikaten. Es war mit useEffect
, dem Abhängigkeitsarray, verbunden. Jedes Mal, wenn die Abhängigkeit geändert wurde, connection.on(event, callback);
Rückruf zwischengespeichert und immer wieder ausgelöst.
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
Die schnellste und zuverlässigste Lösung, die wir gefunden haben, bestand darin, eine Kopie der Daten in der React- ref
zu behalten und sie in useEffect
für zukünftige Updates zu verwenden.
// 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> ); };
Derzeit verwenden wir eine neue Version von @microsoft/signalr
, die offenbar bereits notwendige Korrekturen enthält. Aber wenn jemand diese Lösung nützlich findet und diese Problemumgehung nutzt, freue ich mich trotzdem. Abschließend möchte ich sagen, dass meine Erfahrungen mit SignalR recht positiv sind, die Installation keine besonderen Abhängigkeiten oder Einstellungen erforderte und es gut funktioniert und unsere Anforderungen erfüllt.