Вступление
В современной веб-разработке существует множество способов обновления данных в реальном времени. Среди наиболее распространённых методов выделяются Polling и WebSockets. Оба подхода имеют свои уникальные особенности и применения, что делает их подходящими для различных сценариев. В этой статье мы рассмотрим основные различия между Polling и WebSockets, их преимущества и недостатки, а также приведем примеры использования каждого из них в React приложениях на хуках.
Polling
Polling – это метод, при котором клиент периодически отправляет запросы на сервер для получения обновленных данных. Этот подход прост в реализации, но может быть неэффективен при частых запросах, так как он увеличивает нагрузку на сервер и сеть.
Рефетч может происходить при нажатии кнопки, по истечению определенного количества времени, или при любом другом событии.
WebSockets
WebSockets – это протокол, который позволяет устанавливать постоянное соединение между клиентом и сервером. Благодаря этому соединению данные могут передаваться в обоих направлениях в реальном времени, что делает WebSockets более эффективным для приложений, требующих мгновенного обновления данных.
Клиент отправляет запрос, а сервер возвращает ему handshake, после чего устанавливается websocket соединение, в котором данные моментально приходят от клиента до сервера, и наоборот.
Рассмотрим примеры реализации обоих подходов
Создадим 2 приложения: в первом реализуем чат на основе поллинга, добавим пользователю кнопку “обновить”, на которую будет происходить рефетч; во втором же сделаем чат и установим web socket соединение, для моментального обновления данных.
Зависимости
Для реализации задумки мне пригодится всего 2 библиотеки:
-
shadcn/ui — для красивых ui компонентов
-
reactuse — лучшая утилитарная библиотека с огромным количеством переиспользуемых react хуков (мы возьмем оттуда useQuery и useWebsocket)
Установим зависимости и нужные нам компоненты из ui библиотеки (это начало будет одинаковым для обоих приложений)
$yarn add @siberiacancode/reactuse $yarn add tailwindcss $npx tailwindcss init $npx shadcn-ui@latest init $npx shadcn-ui@latest add button card input
Для обоих приложений создадим одинаковый компонент чата. Начнем с обычной верстки. Для этого импортируем из shadcn/ui следующие компоненты: button, input, scroll-area, separator. (см. документацию, чтобы посмотреть как добавлять компоненты)
Создадим развертку компонента Chat:
import { Button } from "./ui/button"; import { Card, CardContent, CardTitle } from "./ui/card"; import { Input } from "./ui/input"; import { ScrollArea } from "./ui/scroll-area"; import { Separator } from "./ui/separator"; const Chat = () => { return ( <Card className=" w-80 h-96"> <CardTitle> <div className="p-2">Chat</div> <Separator /> </CardTitle> <CardContent className="p-0 flex flex-col"> <ScrollArea className=" w-full h-72"> <div className=" p-2"> <div className="flex"> <div className=" bg-slate-300 text-background rounded-md p-1.5 max-w-64 shadow-slate-300 shadow-md"> Message to you </div> </div> <div className="flex justify-end"> <div className=" bg-slate-800 text-foreground rounded-md p-1.5 max-w-64 shadow-slate-800 shadow-md"> Message from you </div> </div> </div> </ScrollArea> <div className=" flex "> <Input placeholder="Your message..." className="" /> <Button className="">Send</Button> </div> </CardContent> </Card> ); }; export default Chat;
Получим следующий ui:
Добавим type Message:
type Message = { text: string; type: "client" | "server"; };
Создадим состояние messages, в котором будем хранить все сообщения:
import { useState } from "react";
const messages = useState<Message[]>([])
Немного поменяем верстку, чтобы в чате отоброжались только сообщения из стэйта messages и на них применялись определенные стили, в соответствии с тем, является ли тип сообщения “server” или “client”
<ScrollArea className=" w-full h-72"> <div className="p-2"> {messages.map((message, index) => { if (message.type === "client") return ( <div className="flex justify-end" key={index}> <div className=" bg-slate-800 text-foreground rounded-md p-1.5 max-w-64 shadow-slate-800 shadow-md"> {message.text} </div> </div> ); else return ( <div className="flex"> <div className=" bg-slate-300 text-background rounded-md p-1.5 max-w-64 shadow-slate-300 shadow-md"> {message.text} </div> </div> ); })} </div> </ScrollArea>;
Добавим хук работы с полями ввода
useField — этот хук из пакета reactuse вы можете использовать для удобного взаимодествия с полями ввода.
const messageInput = useField({initialValue: ''})
Здесь мы задали initialValue, равное пустой строке. Далее мы будем обращаться к messageInput, для взаимодействия с полем ввода.
Добавим к нашему инпуту следующее:
<Input placeholder="Your message..." {...messageInput.register()} />
Таким образом мы регистрируем и привязываем хук к этому полю ввода
1 — Метод Polling
Для каждого из способов нам понадобиться создать свой моковый сервер. В случае с polling воспользуюсь библиотекой от создатиля reactuse — ? Mock Config Server
Установим пакет:
$yarn add mock-config-server --dev
Теперь надо настроить конфигурацию пакета. Создадим файл mock-server.config.js:
/** @type {import('mock-config-server').MockServerConfig} */ let messages = []; const mockServerConfig = { rest: { baseUrl: "/api", configs: [ { path: "/messages", method: "get", routes: [{ data: messages }], }, { path: "/messages/new", method: "post", routes: [{ data: { success: "true" } }], interceptors: { response: (data, { request }) => { messages.push({ text: request.body.text, type: "client", }); messages.push({ text: request.body.text, type: "server", }); return data; }, }, }, ], }, }; export default mockServerConfig;
Здесь мы создаем переменную messages. В ней будем хранить все сообщения. Создадим маршрут /messages и запрос GET. Этот запрос будет просто отдавать переменную messages, равную всем полученным и отправленым сообщениям в массиве. Далее POST запрос /messages/new. Через interceptors создаем функцию, которая будет обрабатываться когда придет этот запрос. В этой функции мы просто перехватываем текст из сообщения и добавляем в переменную messages 2 сообщения с одинаковым текстом: от сервера и от клиента.
Конфигурация мокового эхо-чат апи готова! Запустим сервер:
npx mock-config-server
Теперь все запросы доступны по http://localhost:31299/api
Вернемся к базовой верстке:
Создадим запрос за получением сообщений с помощью хука useQuery:
import { useField, useQuery, useMutation } from "@siberiacancode/reactuse";
const { data, refetch, isRefetching, isLoading, isError, error } = useQuery( () => fetch("<http://localhost:31299/api/messages>").then((res) => res.json()), { refetchInterval: 1200000, keys: ["messages"], onSuccess: (data) => setMessages(data), } );
В параметры функции передаем callback функцию запроса и объект options. В нем указываем ключ, refetchInterval, чтобы запрос отправлялся заново автоматически через 120000 милисекунд (20 минут) и onSuccess — в случае успеха **присвоим результат запроса (data) стэйту messages. Получаем ряд стэйтов и функций, которые мы заюзаем позже.
Немного поменяем развертку контента карточки:
<ScrollArea className=" w-full h-72"> <div className="p-2"> {isLoading || isRefetching ? ( <p className="text-center">Wait...</p> ) : isError ? ( <p className="text-center">Error {error?.message}</p> ) : ( messages.map((message, index) => { if (message.type === "client") return ( <div className="flex justify-end" key={index}> <div className=" bg-slate-800 text-foreground rounded-md p-1.5 max-w-64 shadow-slate-800 shadow-md"> {message.text} </div> </div> ); else return ( <div className="flex" key={index}> <div className=" bg-slate-300 text-background rounded-md p-1.5 max-w-64 shadow-slate-300 shadow-md"> {message.text} </div> </div> ); }) )} </div> </ScrollArea>;
В случае ошибки отобразим ошибку, в случае загрузки или рефетча данных отображаем Wait… Иначе — отображаем ui
Теперь займемся рефетчем по кнопке. Просто добавим к кнопке Refetch следующее:
<Button onClick={()=>refetch()}>Refetch</Button>
Теперь при нажатии будет происходить ревалидация данных.
Теперь будем отправлять сообщения.
const { mutateAsync } = useMutation(async (text) => { await fetch("<http://localhost:31299/api/messages/new>", { method: "POST", body: JSON.stringify({ text: text }), headers: { Accept: "application/json", "Content-Type": "application/json", }, }); });
получаем функцию mutateAsync инициализируя хук useMutation. В callback функцию указываем POST запрос с body и загаловками.
Теперь мы можем вызвать функцию mutateAsync чтобы совершить мутацию данных (отправить на сервер сообщение). Вызывем ее как только пользователь нажмет Send. После отправки ресетнем форму
<Button onClick={async () => { await mutateAsync(messageInput.getValue()); messageInput.reset(); }} > Send </Button>;
Готово!
Весь код:
import { useState } from "react"; import { Button } from "./ui/button"; import { Card, CardContent, CardTitle } from "./ui/card"; import { Input } from "./ui/input"; import { ScrollArea } from "./ui/scroll-area"; import { Separator } from "./ui/separator"; import { useField, useQuery, useMutation } from "@siberiacancode/reactuse"; type Message = { text: string; type: "client" | "server"; }; const Chat = () => { const [messages, setMessages] = useState<Message[]>([]); const messageInput = useField({ initialValue: "" }); const { data, refetch, isRefetching, isLoading, isError, error } = useQuery( () => fetch("http://localhost:31299/api/messages").then((res) => res.json()), { refetchInterval: 1200000, keys: ["messages"], onSuccess: (data) => setMessages(data), } ); const { mutateAsync } = useMutation(async (text) => { await fetch("http://localhost:31299/api/messages/new", { method: "POST", body: JSON.stringify({ text: text }), headers: { Accept: "application/json", "Content-Type": "application/json", }, }); }); return ( <Card className=" w-80 h-96"> <CardTitle> <div className="p-2 flex justify-between items-center"> <p>Pokemon Chat</p> <Button onClick={() => refetch()}>Refetch</Button> </div> <Separator /> </CardTitle> <CardContent className="p-0 flex flex-col"> <ScrollArea className=" w-full h-72"> <div className="p-2"> {isLoading || isRefetching ? ( <p className="text-center">Wait...</p> ) : isError ? ( <p className="text-center">Error {error?.message}</p> ) : ( messages.map((message, index) => { if (message.type === "client") return ( <div className="flex justify-end" key={index}> <div className=" bg-slate-800 text-foreground rounded-md p-1.5 max-w-64 shadow-slate-800 shadow-md"> {message.text} </div> </div> ); else return ( <div className="flex" key={index}> <div className=" bg-slate-300 text-background rounded-md p-1.5 max-w-64 shadow-slate-300 shadow-md"> {message.text} </div> </div> ); }) )} </div> </ScrollArea> <div className=" flex "> <Input placeholder="Your message..." {...messageInput.register()} /> <Button onClick={async () => { await mutateAsync(messageInput.getValue()); messageInput.reset(); }} > Send </Button> </div> </CardContent> </Card> ); }; export default Chat;
Метод с использованием WebSockets
Как и в случае с методом поллинга нам нужно будет использовать моковый сервер, чтобы имитировать реальный бэкенд. Если в случае с поллингом мы использовали mock-config-server, и задавали ему конфигурацию, чтобы получить функционал эхо чата, то в случае с вебсокетами все попроще, так как один из основных сценариев использования вебсокетов — это чаты, то существуют готовые решения специально для этого.
wss://echo.websocket.org — мы будем обращаться по этому запросу, чтобы подключиться к weboscket соединению.
Вернемся к нашей базовой развертке компонента Chat. Добавим подключение к вебсокет серверу:
import { useField, useWebSocket, useMount } from "@siberiacancode/reactuse";
const { send, status, close, open } = useWebSocket( "wss://echo.websocket.org", { onConnected: (webSocket) => console.log(`Connected to ${webSocket.url}`), } ); useMount(() => console.log("Connecting to websocket server..."));
Здесь мы задали базовую конфигурацию и инициализацию хука. Передали адрес, по которому нужно законнектиться и в options указали, что когда произойдет событие onConnected, мы выведем в консоль сообщение об успешном присоединении к серверу.
Также ниже вызывем хук useMount, он будет кидать в консоль сообщение Connecting… как только компонент будет впервые рендериться.
(далее мы дополним options у хука useWebsocket)
Поменяем поведение кнопки. На клик она будет обрабатывать следующее:
<Button onClick={() => { const message = messageInput.getValue(); setMessages((prevMessages) => [ ...prevMessages, { text: message, type: "client" }, ]); send(message); messageInput.reset(); }} >
Сначала мы оптимистично* обновим ui, закинем в стэйт сообщений наше новое сообщение. Компонент сразу же обновится с новым сообщением. Потом отправим сообщение на сервер через вебсокет, затем ресетним input.
*Оптимистично, потому что такое поведение называется Optimistic update. Когда пользователь совершает действие (например нажатие кнопки лайка) и мы сразу обновляем ui, не дожидаясь, когда запрос на это действие дойдет
Теперь когда мы отправляем сообщения они сразу появляются в чате.
Теперь осталось реализовать ответные сообщение от сервера. И здесь все будет попроще.
onMessage: (event) => setMessages((prevMessages) => [ ...prevMessages, { text: event.data, type: "server" }, ]),
Возвращаемся в options у хука useWebsocket, добавляем, что на событие onMessage мы добавим в стэйт messages сообщение от сервера.
Теперь все готово!
Весь код:
import { useState } from "react"; import { Button } from "./ui/button"; import { Card, CardContent, CardTitle } from "./ui/card"; import { Input } from "./ui/input"; import { ScrollArea } from "./ui/scroll-area"; import { Separator } from "./ui/separator"; import { useField, useMount, useWebSocket } from "@siberiacancode/reactuse"; type Message = { text: string; type: "client" | "server"; }; const Chat = () => { const [messages, setMessages] = useState<Message[]>([]); const messageInput = useField({ initialValue: "" }); const { send, status, close, open } = useWebSocket( "wss://echo.websocket.org", { onConnected: (webSocket) => console.log(`Connected to ${webSocket.url}`), onMessage: (event) => setMessages((prevMessages) => [ ...prevMessages, { text: event.data, type: "server" }, ]), } ); useMount(() => console.log("Connecting to websocket server...")); return ( <Card className=" w-80 h-96"> <CardTitle> <div className="p-2">Pokemon Chat</div> <Separator /> </CardTitle> <CardContent className="p-0 flex flex-col"> <ScrollArea className=" w-full h-72"> <div className="p-2"> {messages.map((message, index) => { if (message.type === "client") return ( <div className="flex justify-end" key={index}> <div className=" bg-slate-800 text-foreground rounded-md p-1.5 max-w-64 shadow-slate-800 shadow-md"> {message.text} </div> </div> ); else return ( <div className="flex" key={index}> <div className=" bg-slate-300 text-background rounded-md p-1.5 max-w-64 shadow-slate-300 shadow-md"> {message.text} </div> </div> ); })} </div> </ScrollArea> <div className=" flex "> <Input placeholder="Your message..." {...messageInput.register()} /> <Button onClick={() => { const message = messageInput.getValue(); setMessages((prevMessages) => [ ...prevMessages, { text: message, type: "client" }, ]); send(message); messageInput.reset(); }} > Send </Button> </div> </CardContent> </Card> ); }; export default Chat;
Заключение
Как мы смогли увидеть — вебсокеты нужны в ситуациях, когда сообщения должны приходить моментально и моментально обновлять ui, например чаты в мессенджерах. Обычный поллинг мы можем использовать, когда данные не важно получать настолько быстро и их актуальность не столь важна, например, почтовые сервисы.
Эта статья показала вам основные различия и способы применения поллинга и веб сокетов. Лучше всего для этих кейсов использовать хуки и самую огромную коллекцию хуков в react — reactuse.
ссылка на оригинал статьи https://habr.com/ru/articles/833104/
Добавить комментарий