一套基于 React-ANTD、SignalR、.NET CORE 的消息推送解决方案

本文对应的环境:React + ANTD + .NET CORE WEB API + SignalR

  • 本文示例部分分为前端后端两部分

效果图:

前端代码

用到的包和版本如下:

前端组件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React from 'react';
import { notification, Typography } from 'antd';
import { ExclamationCircleFilled } from '@ant-design/icons';
import * as signalR from '@microsoft/signalr';
import styles from './style.less';
import config from '../../config';
import urlParam from '../../utils/UrlParamHelper';
const { ParagraphLink } = Typography;
class Notification extends React.Component {
  constructor (props) {
    super(props);
  }
  render () {
    return null;
  }
  componentDidMount () {
    const protocol = new signalR.JsonHubProtocol();
    const transport = signalR.HttpTransportType.WebSockets;
    const options = {
      transport;
    }
    this.connection = new signalR.HubConnectionBuilder()
      .withUrl(`${config.baseURL}api/TaskNotification?token=${urlParam.token}`, options)
      .withHubProtocol(protocol)
      .withAutomaticReconnect()
      .build();
    this.connection.on('TaskNotifiy'this.onNotifReceived);
    this.connection.start()
      .then(() => console.info('SignalR Connected'))
      .catch(err => console.error('SignalR Connection Error: ', err));
  }
  componentWillUnmount () {
    this.connection.stop();
  }
  onNotifReceived (taskNo, taskTitle) {
    notification.info({
      message'任务提醒:',
      description: {taskTitle},
      placement'bottomRight',
      durationnull,
      icon<ExclamationCircleFilled className={styles.icon} />
    })
  }
};
export default Notification;

后端代码:

.NET CORE 版本为 2.2

后端项目结构:

  • ∵ SingalR 客户端在连接的时候只有 ConnectionId,所以后端代码会将用户和ConnectionId 进行绑定(这里我使用的是 Redis,你可以按你自己情况来)

TaskNotificationHub.cs

  • 需在 Startup 中注册该类后

  • 需集成 Hub 类,并根据自身业务情况重写 OnConnectedAsync 和 OnDisconnectedAsync。

    • 这里我通过这 2 个方法来进行用户和 ConnectionId 的绑定和解绑。
    • 每次有新用户连接和老连接断开都会调用这 2 个方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

public class TaskNotificationHub : Hub
{
private static IServiceProvider _serviceProvider;

public static void Init(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

/// <summary>
///
/// </summary>
/// <returns></returns>
public async override Task OnConnectedAsync()
{
var token = Context.GetHttpContext().Request.Query["token"].ToString();

if (!string.IsNullOrEmpty(token))
{
/*根据令牌获取用户的信息*/
{
await RedisClient.Instance.SetAsync("缓存key-用户标识", this.Context.ConnectionId);
}
}
await base.OnConnectedAsync();
}

public async override Task OnDisconnectedAsync(Exception exception)
{
var token = Context.GetHttpContext().Request.Query["token"].ToString();
if (!string.IsNullOrEmpty(token))
{
/*根据令牌获取用户的信息*/

//清除缓存
await RedisClient.Instance.RemoveAsync("缓存key-用户标识");
}
}
await base.OnDisconnectedAsync(exception);
}
}

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void ConfigureServices(IServiceCollection services)
{
/*其他代码*/

services.AddCors(
options => options.AddPolicy(
DefaultCorsPolicyName,
builder => builder.WithOrigins("http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
)
);

services.AddSignalR(); //注册 SignalR
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactor)
{
/*其他代码*/
app.UseCors(DefaultCorsPolicyName);

var webSocketOptions = new WebSocketOptions()
{ // 这块选项设置最好跟客户端保持一致(若使用 nginx 等,亦需做下对应。)
KeepAliveInterval = TimeSpan.FromSeconds(120),
ReceiveBufferSize = 4 * 1024
};
webSocketOptions.AllowedOrigins.Add("http://localhost:3000");
app.UseWebSockets(webSocketOptions);
app.UseSignalR(routes =>
{
routes.MapHub<TaskNotificationHub>("/api/TaskNotification"); //注册 SignalR 并指定路由
});

app.UseMvc();
}
  • 需要注意的是项目是前后端分离的,在 UI 建立长连接的时候可能存在跨域的问题,需要显式设定下 CORS

  • 这里显式声明了使用 websocket。

  • 跨域站点你也可以放到 appsettings中进行。

Controller

这里直接通过 Controller 来说明如何发送消息到客户端

TaskController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private readonly IHubContext<TaskNotificationHub> _hubContext;

public TaskController(
IHubContext<TaskNotificationHub> hubContext)
{
_hubContext = hubContext;
}

[HttpPost]
public async Task<ResultInfo> NotifyAsync(string args)
{
var connectionId = await RedisClient.Instance.GetAsync<string>("缓存key");

/*你的其他业务代码*/

//推送消息
await _hubContext.Clients.Client(connectionId).SendAsync("TaskNotifiy", content1, content2,...contentN);

return ResultInfo.Success();
}
  • 这里通过使用 IHubContext<TaskNotificationHub> 来进行 DI,不然每次调用 Hub 类都是一个新的实例,无法推送,通过 IHubContext 可以让你复用上下文。

nginx

如果你使用了 nginx,则需要进行下如下配置:

1
2
3
4
5
6
7
8
9
10
11
location {你的路径}  {
proxy_connect_timeout 120;
proxy_read_timeout 120;
proxy_send_timeout 120;
proxy_pass {你的站点};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}