Polling vs Websockets (с примерами на React хуках)

от автора

Вступление

В современной веб-разработке существует множество способов обновления данных в реальном времени. Среди наиболее распространённых методов выделяются Polling и WebSockets. Оба подхода имеют свои уникальные особенности и применения, что делает их подходящими для различных сценариев. В этой статье мы рассмотрим основные различия между Polling и WebSockets, их преимущества и недостатки, а также приведем примеры использования каждого из них в React приложениях на хуках.

Polling

Polling – это метод, при котором клиент периодически отправляет запросы на сервер для получения обновленных данных. Этот подход прост в реализации, но может быть неэффективен при частых запросах, так как он увеличивает нагрузку на сервер и сеть.

Untitled

Untitled

Рефетч может происходить при нажатии кнопки, по истечению определенного количества времени, или при любом другом событии.

WebSockets

WebSockets – это протокол, который позволяет устанавливать постоянное соединение между клиентом и сервером. Благодаря этому соединению данные могут передаваться в обоих направлениях в реальном времени, что делает WebSockets более эффективным для приложений, требующих мгновенного обновления данных.

Untitled

Untitled

Клиент отправляет запрос, а сервер возвращает ему 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:

Untitled

Untitled

Добавим 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>; 

Готово!

Untitled

Untitled

Весь код:

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 сообщение от сервера.

Теперь все готово!

Untitled

Untitled

Весь код:

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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *