目录
介绍
ScrumPoker应用程序
源代码
开发工具
基本步骤
后端代码
创建Hub
在Startup中注册集线器
创建持久性
让我们为客户端应用程序公开一些终端
启用Cors
前端代码
结论
SignalR现在包含在ASP.NET Core框架中,并且进行了大量改进,使其轻巧易用。令我惊讶的是,我找不到任何有关如何使用SignalR的好教程,并且无法使用它使相同的旧聊天应用程序变得有趣。我想到了用SignalR创建一些东西,而不是同一个无聊的聊天应用程序。
在本教程中,我将指导您完成创建实时应用程序所需的主要步骤。我不会在这里写完整的代码。您可以在github上找到完整的源代码。
在本教程中,我们将创建一个有趣的应用程序,名为ScrumPoker。我们生活在敏捷的世界中,因此在我们的开发过程或每个冲刺周期中进行故事评估并指出要点很普遍。过去,我们曾经计划使用扑克牌,而团队则通过这些牌来进行故事评估,但是现在一切都在线上了,我们经常进行远程工作。
用户可以创建ScrumBoard链接并与队友共享链接。团队成员可以进入那里并开始指出故事。只有当创建的用户ScrumBoard允许他们查看时,团队给出的点才会显示在仪表板上。
用户会实时添加到仪表板上,他们提交的点也会实时反映出来。
├───clientapp
├───Contracts
├───Controllers
├───Infrastructure
│├───NotificationHub
│└───Persistence
您可以从我的github下载完整的源代码。下载它,克隆它,并从https://github.com/vikas0sharma/ScrumPoker派生它。
我们将使用ASP.NET Core 3.1,React 16.3 +,Bootstrap 4,Node 10.13 +,create-react-app,Redis,Visual Studio 2019,Visual Studio Code,Yarn包管理器。
在这里,我假设您熟悉ASP.NET Core环境和React。我将指导您做一些特殊的事情以使SignalR与React一起工作。
如果您不熟悉SignalR,建议您阅读Microsoft的正式文档。
而且,如果您喜欢React,那么肯定可以轻松地建立React开发环境。
yarn create react-app my-app --template typescript
首先设置服务器端代码。在我们的应用中,我们将只有两个模型,即ScrumBoard和User。
SignalR通过集线器在客户端和服务器之间进行通信。这是我们保持通讯逻辑的中心位置。在这里,我们指定将通知哪些客户。
using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;
namespace API.Infrastructure.NotificationHub
{
public class ScrumBoardHub : Hub
{
public async override Task OnConnectedAsync()
{
await base.OnConnectedAsync();
await Clients.Caller.SendAsync("Message", "Connected successfully!");
}
public async Task SubscribeToBoard(Guid boardId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, boardId.ToString());
await Clients.Caller.SendAsync("Message", "Added to board successfully!");
}
}
}
如您所见,我们继承自SignalR Hub类。与客户端成功连接后,OnConnectedAsync将被调用。每当客户端连接到集线器时,都会向客户端推送一条消息。
我们公开了一种名为“SubscribeToBoard” 的方法,客户端可以通过提供scumboard ID 来调用该方法来订阅scumboard。如果您注意到了,我们已经使用了Hub的'Groups'属性来为特定的板创建一组客户。我们将按委员会ID创建分组,并添加所有要求对该委员会进行更新的客户。
在Dashboard上,用户可以实时查看其他人是否加入了板以及他们在仪表板上的工作。
在startup的ConfigureServices方法中,添加AddSignalR。
services.AddSignalR();
在Configure方法中,注册您的Hub类。
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub("/scrumboardhub");// Register Hub class
});
就像我之前说的,我正在使用Redis服务器存储用户执行的临时数据/活动。让我们创建一个类以使用Redis执行CRUD操作。我们将使用StackExchange nuget包。
在Startup类中设置Redis连接。
services.Configure(Configuration);
services.AddSingleton(sp =>
{
var settings = sp.GetRequiredService>().Value;
var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true);
configuration.ResolveDns = true;
return ConnectionMultiplexer.Connect(configuration);
});
Repository 类:
using API.Contracts;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace API.Infrastructure.Persistence
{
public class ScrumRepository : IScrumRepository
{
private readonly IDatabase database;
public ScrumRepository(ConnectionMultiplexer redis)
{
database = redis.GetDatabase();
}
public async Task AddBoard(ScrumBoard scrumBoard)
{
var isDone = await database.StringSetAsync
(scrumBoard.Id.ToString(), JsonSerializer.Serialize(scrumBoard));
return isDone;
}
public async Task AddUserToBoard(Guid boardId, User user)
{
var data = await database.StringGetAsync(boardId.ToString());
if (data.IsNullOrEmpty)
{
return false;
}
var board = JsonSerializer.Deserialize(data);
board.Users.Add(user);
return await AddBoard(board);
}
public async Task ClearUsersPoint(Guid boardId)
{
var data = await database.StringGetAsync(boardId.ToString());
if (data.IsNullOrEmpty)
{
return false;
}
var board = JsonSerializer.Deserialize(data);
board.Users.ForEach(u => u.Point = 0);
return await AddBoard(board);
}
public async Task> GetUsersFromBoard(Guid boardId)
{
var data = await database.StringGetAsync(boardId.ToString());
if (data.IsNullOrEmpty)
{
return new List();
}
var board = JsonSerializer.Deserialize(data);
return board.Users;
}
public async Task UpdateUserPoint(Guid boardId, Guid userId, int point)
{
var data = await database.StringGetAsync(boardId.ToString());
var board = JsonSerializer.Deserialize(data);
var user = board.Users.FirstOrDefault(u => u.Id == userId);
if (user != null)
{
user.Point = point;
}
return await AddBoard(board);
}
}
}
用户可以创建一个供其他用户创建其个人资料并开始对仪表板上的故事进行投票或估算的地方的ScrumBoard。
我们将创建一个controller类,并公开一些REST API,React客户端应用将使用该REST API发送请求。
using API.Contracts;
using API.Infrastructure.NotificationHub;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace API.Controllers
{
[Route("scrum-poker")]
[ApiController]
public class ScrumPokerController : ControllerBase
{
private readonly IScrumRepository scrumRepository;
private readonly IHubContext hub;
public ScrumPokerController(IScrumRepository scrumRepository,
IHubContext hub)
{
this.scrumRepository = scrumRepository;
this.hub = hub;
}
[HttpPost("boards")]
public async Task Post([FromBody] ScrumBoard scrumBoard)
{
var boardId = Guid.NewGuid();
scrumBoard.Id = boardId;
var isCreated = await scrumRepository.AddBoard(scrumBoard);
if (isCreated)
{
return Ok(boardId);
}
return NotFound();
}
[HttpPost("boards/{boardId}")]
public async Task UpdateUsersPoint(Guid boardId)
{
var isAdded = await scrumRepository.ClearUsersPoint(boardId);
await hub.Clients.Group(boardId.ToString())
.SendAsync("UsersAdded", await scrumRepository.GetUsersFromBoard(boardId));
if (isAdded)
{
return Ok(isAdded);
}
return NotFound();
}
[HttpPost("boards/{boardId}/users")]
public async Task AddUser(Guid boardId, User user)
{
user.Id = Guid.NewGuid();
var isAdded = await scrumRepository.AddUserToBoard(boardId, user);
await hub.Clients.Group(boardId.ToString())
.SendAsync("UsersAdded", await scrumRepository.GetUsersFromBoard(boardId));
if (isAdded)
{
return Ok(user.Id);
}
return NotFound();
}
[HttpGet("boards/{boardId}/users")]
public async Task GetUsers(Guid boardId)
{
var users = await scrumRepository.GetUsersFromBoard(boardId);
return Ok(users);
}
[HttpGet("boards/{boardId}/users/{userId}")]
public async Task GetUser(Guid boardId, Guid userId)
{
var users = await scrumRepository.GetUsersFromBoard(boardId);
var user = users.FirstOrDefault(u => u.Id == userId);
return Ok(user);
}
[HttpPut("boards/{boardId}/users")]
public async Task UpdateUser(Guid boardId, User user)
{
var isUpdated =
await scrumRepository.UpdateUserPoint(boardId, user.Id, user.Point);
await hub.Clients.Group(boardId.ToString())
.SendAsync("UsersAdded", await scrumRepository.GetUsersFromBoard(boardId));
return Ok(isUpdated);
}
}
}
如果您注意到,我们的控制器正在通过依赖项注入在其构造函数中进行请求IHubContext
启动SignalR连接的请求被CORS策略阻止,因此我们需要将ASP.NET配置为允许来自React应用的请求,因为它们将托管在不同的来源中。
ConfigureServices 方法:
services.AddCors(options =>
options.AddPolicy("CorsPolicy",
builder =>
builder.AllowAnyMethod()
.AllowAnyHeader()
.WithOrigins("http://localhost:3000")
.AllowCredentials()));
Configure 方法:
app.UseCors("CorsPolicy");
我们将为板创建、用户配置文件创建、仪表板、用户列表、标题、导航等创建单独的组件。但是这里的重点是,我们将SignalR客户端逻辑保留在UserList组件中,因为每当其他一些用户需要刷新用户列表时,用户执行一些活动。
让我们编写SignalR连接代码,但在此之前,我们需要在React应用程序中添加SignalR包。
yarn add @microsoft/signalr
UserList 组件:
import React, { useState, useEffect, FC } from 'react';
import { User } from './user/User';
import { UserModel } from '../../models/user-model';
import { useParams } from 'react-router-dom';
import {
HubConnectionBuilder,
HubConnectionState,
HubConnection,
} from '@microsoft/signalr';
import { getBoardUsers } from '../../api/scrum-poker-api';
export const UserList: FC<{ state: boolean }> = ({ state }) => {
const [users, setUsers] = useState([]);
const { id } = useParams();
const boardId = id as string;
useEffect(() => {
if (users.length === 0) {
getUsers();
}
setUpSignalRConnection(boardId).then((con) => {
//connection = con;
});
}, []);
const getUsers = async () => {
const users = await getBoardUsers(boardId);
setUsers(users);
};
const setUpSignalRConnection = async (boardId: string) => {
const connection = new HubConnectionBuilder()
.withUrl('https://localhost:5001/scrumboardhub')
.withAutomaticReconnect()
.build();
connection.on('Message', (message: string) => {
console.log('Message', message);
});
connection.on('UsersAdded', (users: UserModel[]) => {
setUsers(users);
});
try {
await connection.start();
} catch (err) {
console.log(err);
}
if (connection.state === HubConnectionState.Connected) {
connection.invoke('SubscribeToBoard', boardId).catch((err: Error) => {
return console.error(err.toString());
});
}
return connection;
};
return (
{users.map((u) => (
))}
);
};
我们已经使用创建连接的HubConnectionBuilder方法创建了setUpSignalRConnection。它还侦听来自服务器的“UserAdded”消息,并决定如何处理来自服务器的消息+有效负载。它基本上使用服务器发送的更新数据刷新用户列表。
在我们的React应用程序中,我们有不同的组件,但是它们很容易理解,这就是为什么我在这里没有提及它们。
使用React设置SignalR并为我们的应用程序提供实时功能非常容易。我刚刚提到了设置SignalR所需的重要步骤。您可以阅读完整的源代码,以了解协同工作的完整细节。当然,我们可以在应用程序中进行一些改进,就像可以使用Redux进行组件之间的通信一样。