在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進行組件之間的通信一樣。

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