在Typescript中使用ASP.NET Core SignalR和React创建实时应用程序

目录

介绍

ScrumPoker应用程序

源代码

开发工具

基本步骤

后端代码

创建Hub

在Startup中注册集线器

创建持久性

让我们为客户端应用程序公开一些终端

启用Cors

前端代码

结论


SignalR现在包含在ASP.NET Core框架中,并且进行了大量改进,使其轻巧易用。令我惊讶的是,我找不到任何有关如何使用SignalR的好教程,并且无法使用它使相同的旧聊天应用程序变得有趣。我想到了用SignalR创建一些东西,而不是同一个无聊的聊天应用程序。

介绍

在本教程中,我将指导您完成创建实时应用程序所需的主要步骤。我不会在这里写完整的代码。您可以在github上找到完整的源代码。

ScrumPoker应用程序

在本教程中,我们将创建一个有趣的应用程序,名为ScrumPoker。我们生活在敏捷的世界中,因此在我们的开发过程或每个冲刺周期中进行故事评估并指出要点很普遍。过去,我们曾经计划使用扑克牌,而团队则通过这些牌来进行故事评估,但是现在一切都在线上了,我们经常进行远程工作。

用户可以创建ScrumBoard链接并与队友共享链接。团队成员可以进入那里并开始指出故事。只有当创建的用户ScrumBoard允许他们查看时,团队给出的点才会显示在仪表板上。

用户会实时添加到仪表板上,他们提交的点也会实时反映出来。

 

 

 

 

源代码

├───clientapp
├───Contracts
├───Controllers
├───Infrastructure
│├───NotificationHub
│└───Persistence

您可以从我的github下载完整的源代码。下载它,克隆它,并从https://github.com/vikas0sharma/ScrumPoker派生它。

开发工具

我们将使用ASP.NET Core 3.1React 16.3 +Bootstrap 4Node 10.13 +create-react-appRedisVisual Studio 2019Visual Studio CodeYarn包管理器。

在这里,我假设您熟悉ASP.NET Core环境和React。我将指导您做一些特殊的事情以使SignalRReact一起工作。

如果您不熟悉SignalR,建议您阅读Microsoft的正式文档。

而且,如果您喜欢React,那么肯定可以轻松地建立React开发环境。

基本步骤

  • 首先,您需要创建ASP.NET Core Web API项目。在这里,您将创建一个控制器来处理来自React应用程序的请求。
  • 为了持久,我们将使用Redis。为什么选择Redis?因为我想保持我的应用程序简单,除此之外,它是一个仅在应用程序运行时才需要保留其数据的应用程序。
  • ASP.NET Core项目文件夹中,您需要为客户端应用程序创建一个单独的文件夹,所有我们的React应用程序代码都将驻留在该文件夹中。
  • 我正在使用Yarn作为程序包管理器。如果您喜欢NPM进行开发,这是您的选择。
  • 我相信您已经熟悉create-react-app。它为我们完成了所有繁重的工作,并创建了一个基本的应用程序结构。这里要注意的是,我们将使用Typescript编写React应用。为什么要Typescript?因为它通过在开发时捕获愚蠢的错误使开发人员的生活变得轻松。
yarn create react-app my-app --template typescript
  • 您可以使用我的源代码中的package.json文件,该文件将帮助您设置所有必需的软件包。

后端代码

首先设置服务器端代码。在我们的应用中,我们将只有两个模型,即ScrumBoardUser

创建Hub

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中注册集线器

startupConfigureServices方法中,添加AddSignalR

services.AddSignalR();

Configure方法中,注册您的Hub类。

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
    endpoints.MapHub<ScrumBoardHub>("/scrumboardhub");// Register Hub class
});

创建持久性

就像我之前说的,我正在使用Redis服务器存储用户执行的临时数据/活动。让我们创建一个类以使用Redis执行CRUD操作。我们将使用StackExchange nuget包。

<PackageReference Include="StackExchange.Redis" Version="2.1.28" />

Startup类中设置Redis连接。

services.Configure<APISettings>(Configuration);

services.AddSingleton<ConnectionMultiplexer>(sp =>
{
     var settings = sp.GetRequiredService<IOptions<APISettings>>().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<bool> AddBoard(ScrumBoard scrumBoard)
        {
            var isDone = await database.StringSetAsync
                         (scrumBoard.Id.ToString(), JsonSerializer.Serialize(scrumBoard));

            return isDone;
        }

        public async Task<bool> AddUserToBoard(Guid boardId, User user)
        {
            var data = await database.StringGetAsync(boardId.ToString());

            if (data.IsNullOrEmpty)
            {
                return false;
            }

            var board = JsonSerializer.Deserialize<ScrumBoard>(data);
            board.Users.Add(user);

            return await AddBoard(board);
        }

        public async Task<bool> ClearUsersPoint(Guid boardId)
        {
            var data = await database.StringGetAsync(boardId.ToString());

            if (data.IsNullOrEmpty)
            {
                return false;
            }

            var board = JsonSerializer.Deserialize<ScrumBoard>(data);
            board.Users.ForEach(u => u.Point = 0);

            return await AddBoard(board);
        }

        public async Task<List<User>> GetUsersFromBoard(Guid boardId)
        {
            var data = await database.StringGetAsync(boardId.ToString());

            if (data.IsNullOrEmpty)
            {
                return new List<User>();
            }

            var board = JsonSerializer.Deserialize<ScrumBoard>(data);

            return board.Users;
        }

        public async Task<bool> UpdateUserPoint(Guid boardId, Guid userId, int point)
        {
            var data = await database.StringGetAsync(boardId.ToString());
            var board = JsonSerializer.Deserialize<ScrumBoard>(data);
            var user = board.Users.FirstOrDefault(u => u.Id == userId);
            if (user != null)
            {
                user.Point = point;
            }

            return await AddBoard(board);
        }
    }
}

用户可以创建一个供其他用户创建其个人资料并开始对仪表板上的故事进行投票或估算的地方的ScrumBoard

让我们为客户端应用程序公开一些终端

我们将创建一个controller类,并公开一些REST APIReact客户端应用将使用该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<ScrumBoardHub> hub;

        public ScrumPokerController(IScrumRepository scrumRepository, 
                                    IHubContext<ScrumBoardHub> hub)
        {
            this.scrumRepository = scrumRepository;
            this.hub = hub;
        }

        [HttpPost("boards")]
        public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> GetUsers(Guid boardId)
        {
            var users = await scrumRepository.GetUsersFromBoard(boardId);

            return Ok(users);
        }

        [HttpGet("boards/{boardId}/users/{userId}")]
        public async Task<IActionResult> 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<IActionResult> 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<ScrumBoardHub>。这个上下文类将用于通知组中所有连接的客户端,无论何时将用户添加到板中,或无论何时用户提交他/她的点,或无论何时管理员清除所有用户提交的点。SendAsync方法将通知以及更新的用户列表发送到客户端。在这里,消息UsersAdded可能会误导您,但可能是您喜欢的任何东西,请记住React应用程序会使用此消息执行某些操作,因此请确保与React应用程序保持同步。

启用Cors

启动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<UserModel[]>([]);
  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 (
    <div className="container">
      {users.map((u) => (
        <User key={u.id} data={u} hiddenState={state}></User>
      ))}
    </div>
  );
};

我们已经使用创建连接的HubConnectionBuilder方法创建了setUpSignalRConnection。它还侦听来自服务器的UserAdded消息,并决定如何处理来自服务器的消息+有效负载。它基本上使用服务器发送的更新数据刷新用户列表。

在我们的React应用程序中,我们有不同的组件,但是它们很容易理解,这就是为什么我在这里没有提及它们。

结论

使用React设置SignalR并为我们的应用程序提供实时功能非常容易。我刚刚提到了设置SignalR所需的重要步骤。您可以阅读完整的源代码,以了解协同工作的完整细节。当然,我们可以在应用程序中进行一些改进,就像可以使用Redux进行组件之间的通信一样。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章