SignalR + React
Думайте о 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
для просмотра новых сообщений в режиме реального времени.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.