SignalR + React

  • Михаил
  • 8 мин. на прочтение
  • 176
  • 27 Jun 2022
  • 27 Jun 2022

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

NetCore

Создайте в своем проекте папку Hubs и добавьте новый класс ChatHub.cs.

public class ChatHub : Hub
{
 public override Task OnConnectedAsync()
 {
   Console.WriteLine("A Client Connected: " + Context.ConnectionId);
   return base.OnConnectedAsync();
 }
 public override Task OnDisconnectedAsync(Exception exception)
 {
   Console.WriteLine("A client disconnected: " + Context.ConnectionId);
   return base.OnDisconnectedAsync(exception);
 }
}

Настройте SignalR, добавив следующее в Program.cs

using MyApp.Hubs;
builder.Services.AddSignalR();
// I'm prefixing signal r rotuers with r and my controller routes with api
// This is just a style choice for the urls to make the routes more obvious
app.MapHub<ChatHub>("/r/chatHub");

Концентраторы используются для отправки сообщений клиентам и получения сообщений от клиентов. Что-то вроде контроллера, но для двунаправленной связи в реальном времени.

Единственное, что этот концентратор будет делать прямо сейчас, — регистрировать подключение и отключение клиента.

React

Создайте новое приложение React:

yarn create vite chat-app

Настройте прокси на сервер:

export default defineConfig({
 server: {
   proxy: {
     "/api": "http://127.0.0.1:5001",
     "/r": {
       target: "http://127.0.0.1:5001",
       ws: true,
     },
   },
 },
 plugins: [react()],
});

Пересылать все запросы, начинающиеся с сервера /api или на него ./r

Замените контексты App.ts следующим:

import { useEffect, useState } from "react";
import "./App.css";
import {
 HubConnection,
 HubConnectionBuilder,
 LogLevel,
} from "@microsoft/signalr";
export default function App() {
 let [connection, setConnection] = useState<HubConnection | undefined>(
   undefined
 );
 useEffect(() => {
   // Cancel everything if this component unmounts
   let canceled = false;
   // Build a connection to the signalR server. Automatically reconnect if the connection is lost.
   const connection = new HubConnectionBuilder()
     .withUrl("/r/chat")
     .withAutomaticReconnect()
     .configureLogging(LogLevel.Information)
     .build();
   // Try to start the connection
   connection
     .start()
     .then(() => {
       if (!canceled) {
         setConnection(connection);
       }
     })
     .catch((error) => {
       console.log("signal error", error);
     });
   // Handle the connection closing
   connection.onclose((error) => {
     if (canceled) {
       return;
     }
     console.log("signal closed");
     setConnection(undefined);
   });
   // If the connection is lost, it won't close. Instead it will try to reconnect.
   // So we need to treat this is a lost connection until `onreconnected` is called.
   connection.onreconnecting((error) => {
     if (canceled) {
       return;
     }
     console.log("signal reconnecting");
     setConnection(undefined);
   });
   // Connection is back, yay
   connection.onreconnected((error) => {
     if (canceled) {
       return;
     }
     console.log("signal reconnected");
     setConnection(connection);
   });
   // Clean up the connection when the component unmounts
   return () => {
     canceled = true;
     connection.stop();
   };
 }, []);
 return (
   <div className="App">
     <h1>SignalR Chat</h1>
     <p>{connection ? "Connected" : "Not connected"}</p>
   </div>
 );
}

Прочтите комментарии в коде, чтобы лучше понять, что происходит. Кода много, но на самом деле это всего лишь базовый шаблонный код.

Попробуйте подключиться к концентратору signalR и устранить любые ошибки подключения, всегда пытаясь переподключиться.

Запустите сервер и приложение React, чтобы убедиться, что все работает.
Если вы перезапустите сервер, вы должны увидеть изменение статуса соединения с Connected на Not connected, а затем обратно на Connected.

Хуки

Поскольку на самом деле это всего лишь установочный код, давайте переместим его в специальный хук. Это очистит основной компонент и облегчит использование соединения в других компонентах, если нам когда-нибудь понадобится в будущем.

Создайте новый файл useSignalR.ts в src папке и добавьте следующий код:

import { useEffect, useState } from "react";
import {
 HubConnection,
 HubConnectionBuilder,
 LogLevel,
} from "@microsoft/signalr";
export default function useSignalR(url) {
 let [connection, setConnection] = useState<HubConnection | undefined>(
   undefined
 );
 useEffect(() => {
   let canceled = false;
   const connection = new HubConnectionBuilder()
     .withUrl(url)
     .withAutomaticReconnect()
     .configureLogging(LogLevel.Information)
     .build();
   connection
     .start()
     .then(() => {
       if (!canceled) {
         setConnection(connection);
       }
     })
     .catch((error) => {
       console.log("signal error", error);
     });
   connection.onclose((error) => {
     if (canceled) {
       return;
     }
     console.log("signal closed");
     setConnection(undefined);
   });
   connection.onreconnecting((error) => {
     if (canceled) {
       return;
     }
     console.log("signal reconnecting");
     setConnection(undefined);
   });
   connection.onreconnected((error) => {
     if (canceled) {
       return;
     }
     console.log("signal reconnected");
     setConnection(connection);
   });
   // Clean up the connection when the component unmounts
   return () => {
     canceled = true;
     connection.stop();
   };
 }, []);
 return { connection };
}

Затем обновите свой, App.tsx чтобы использовать хуки:

import { useEffect, useState } from "react";
import "./App.css";
import useSignalR from "./useSignalR";
export default function App() {
 const { connection } = useSignalR("/r/chat");
 return (
   <div className="App">
     <h1>SignalR Chat</h1>
     <p>{connection ? "Connected" : "Not connected"}</p>
   </div>
 );
}

Поведение должно быть таким же, но теперь App.tsx стало намного чище.

Отправка и получение сообщений
Чтобы отправить сообщение в хаб, у хаба должен быть метод, который можно вызвать. Затем клиент может вызвать этот метод для отправки сообщения.

Итак, если бы мы добавили SendMessage в хаб такой метод:

public class ChatHub : Hub
{
   public async Task SendMessage(string message)
   {
       Console.WriteLine($"Received message: {message}");
   }

Тогда приложение реагирования сможет вызвать эту SendMessage функцию следующим образом:

export default function App() {
 const { connection } = useSignalR("/r/chat");
 const [message, setMessage] = useState("")
 const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
   e.preventDefault()
   // Send the message to signal r
   connection?.invoke("SendMessage", message)
 }
 return (
   <div className="App">
     <h1>SignalR Chat</h1>
     <p>{connection ? "Connected" : "Not connected"}</p>
     <form onSubmit={handleSubmit}>
       <input type="text" value={message} onChange={e => setMessage(e.target.value)} />
       <button type="submit">Send</button>
     </form>
   </div>
 );
}

Мы также могли бы сделать так, чтобы сервер транслировал это сообщение всем клиентам следующим образом:

public class ChatHub : Hub
{
   public async Task SendMessage(string message)
   {
       // Every single time a client sends a message to the server
       // Broadcast that messsage to every single client that is listening
       await Clients.All.SendAsync("ReceiveMessage", message);
   }

Тогда наши клиенты смогут прослушивать эти сообщения следующим образом:

useEffect(() => {
 if (!connection) {
   return
 }
 // listen for messages from the server
 connection.on("ReceiveMessage", (message) => {
   console.log("message from the server", message)
 })
 return () => {
   connection.off("ReceiveMessage")
 }
}, [connection])

POST-запрос
Вместо просмотра сообщений через signalR мы собираемся использовать POSTзапрос на отправку сообщения на сервер, а затем использовать signalR для уведомления всех клиентов о получении нового сообщения. Итак, нашему контроллеру нужен доступ к хабу. Мы можем получить к этому доступ, передав концентратор в конструктор контроллера.

   private readonly DatabaseContext _context;
   private readonly IHubContext<ChatHub> _hub;
   public ChannelsController(DatabaseContext context, IHubContext<ChatHub> hub)
   {
       _context = context;
       _hub = hub;
   }

Теперь, когда мы публикуем сообщение, мы можем транслировать это сообщение всем клиентам, прослушивающим хаб.

   [HttpPost]
   public async Task<ActionResult<Channel>> PostChannel(Channel channel)
   {
       _context.Channels.Add(channel);
       await _context.SaveChangesAsync();
       // Send a message to all clients listening to the hub
       await _hub.Clients.All.SendAsync("ReceiveMessage", channel);
       return CreatedAtAction("GetChannel", new { id = channel.Id }, channel);
   }

И приложение реагирования должно отправить сообщение на сервер:

export default function App() {
 const { connection } = useSignalR("/r/chat");
 const [message, setMessage] = useState("")
 const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
   e.preventDefault()
   await fetch("/api/channels/1/messages", {
     method: "POST",
     headers: {
       "Content-Type": "application/json"
     },
     body: JSON.stringify({
       text: input,
       userName: "saM"
     })
   })
 }

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

Чтобы добиться этого, мы можем настроить SignalR для использования групп. Поэтому, когда клиент подключается к хабу, мы можем добавить его в группу. Затем, когда мы хотим отправить сообщение на определенный канал, мы можем отправить это сообщение группе, которая представляет этот канал.

Добавьте в класс методы AddToGroup и .RemoveFromGroupChatHub

using Microsoft.AspNetCore.SignalR;
namespace MyApp.Hubs;
public class ChatHub : Hub
{
 public async Task AddToGroup(string groupName)
 {
   await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
   await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has joined the group {groupName}.");
 }
 public async Task RemoveFromGroup(string groupName)
 {
   await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
   await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has left the group {groupName}.");
 }
}

Обновите, App.tsx чтобы вызывать эти методы, когда пользователь присоединяется к каналу или покидает его.

useEffect(() => {
 if (!connection) {
   return
 }
 // Only listen for messages coming from a certain chat room
 connection.invoke("AddToGroup", "1")
 // listen for messages from the server
 connection.on("ReceiveMessage", (message) => {
   console.log("message from the server", message)
 })
 return () => {
   connection.invoke("RemoveFromGroup", "1")
   connection.off("ReceiveMessage")
 }
}, [connection])

Обновите, ChannelsController чтобы отправить сообщение группе, представляющей канал.

   [HttpPost("{channelId}/Messages")]
   public async Task<Message> PostChannelMessage(int channelId, Message Message)
   {
       Message.ChannelId = channelId;
       _context.Messages.Add(Message);
       await _context.SaveChangesAsync();
       await _hub.Clients.Group(channelId.ToString()).SendAsync("ReceiveMessage", Message);
       return Message;
   }

Дополните приложение чата, добавив функции CRUD для каналов и сообщений и используя signalR для просмотра новых сообщений в режиме реального времени.