paint-brush
Next.js и SignalR: простая интеграция сокетов и устранение неполадокк@chilledcowfan
4,771 чтения
4,771 чтения

Next.js и SignalR: простая интеграция сокетов и устранение неполадок

к Anton Burduzha8m2023/09/05
Read on Terminal Reader
Read this story w/o Javascript

Слишком долго; Читать

Огромное количество проектов требует интеграции веб-сокетов, чтобы обеспечить мгновенную реакцию интерфейса на изменения без повторной загрузки данных. Мы не собираемся говорить о них или сравнивать сторонние библиотеки, предоставляющие API для повышения удобства разработки. Моя цель — показать, как быстро интегрировать @microsoft/signalr с NextJs.
featured image - Next.js и SignalR: простая интеграция сокетов и устранение неполадок
Anton Burduzha HackerNoon profile picture

Меньшинству, но все же огромному числу проектов требуется интеграция веб-сокетов, чтобы обеспечить мгновенную реакцию интерфейса на изменения без повторной загрузки данных.


Это важная вещь, и мы не собираемся говорить о них или сравнивать сторонние библиотеки, предоставляющие API для лучшего опыта разработки.


Моя цель — показать, как быстро интегрировать @microsoft/signalr с NextJs. И как решить проблемы, с которыми мы столкнулись во время разработки.


Я надеюсь, что все уже установили и развернули проект NextJS локально. В моем случае версия 13.2.4 . Добавим еще несколько важных библиотек: swr (версия 2.1.5 ) для выборки данных и дальнейшей работы с локальным кешем и @microsoft/signalr (версия 7.0.5 ) — 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> ); };


Следующий шаг требует подключения нашей будущей страницы к веб-сокетам, перехвата событий 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


Самым быстрым и надежным решением, которое мы нашли, было сохранение копии данных внутри ref React и использование ее внутри 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 вполне положительный, для установки не потребовалось каких-то особых зависимостей или настроек, он отлично работает и покрывает наши потребности.