首页
视频
资源
登录
原
.net6 Signalr+Vue3 的运用(上)
6518
人阅读
2023/1/31 17:37
总访问:
2278557
评论:
0
收藏:
0
手机
分类:
.net后台框架
 >#.net6 Signalr+Vue3 的运用(上) [TOC] 什么是 SignalR? ------------ tn2>ASP.NET Core SignalR 是一个开放源代码库,可用于简化向应用添加实时 Web 功能。 实时 Web 功能使服务器端代码能够将内容推送到客户端。 什么是 Hubs? ------------ tn2>SignalR 使用Hubs在客户端和服务器之间通信。 Hub 是一种高级管道,允许客户端和服务器相互调用方法。 SignalR 自动处理跨计算机边界的调度,并允许客户端调用服务器上的方法,反之亦然。 可以将强类型参数传递给方法,从而支持模型绑定。 SignalR 提供两种内置中心协议:基于 JSON 的文本协议和基于 MessagePack 的二进制协议。 也叫做集线器。 SignalR后端项目 ------------ ### 创建项目 tn>创建signalrtest项目,我在创建时选择了OpenAPI,没有选择Https。  ### 创建强类型Hubs tn2>这样做的好处是:方便客户端调用服务器的方法时,拼音不会报错。 接下来我们在`HubClients`目录下定义客户端的调用方法的接口`IChatClient`,并且只定义`SendAll`方法。 ```csharp public interface IChatClient { Task SendAll(object message); } ``` tn2>然后我们在`Hubs`的目录下创建`ChatHub`集线器。 并定义了一个`SendMessage`的方法向所有的用户发送消息,并对客户端连接和断开状态做了一个日志记录。 ```csharp public class ChatHub : Hub<IChatClient> { ILogger<ChatHub> _logger; public ChatHub(ILogger<ChatHub> logger, CommonService common) { _logger = logger; _common = common; } readonly CommonService _common; /// <summary> /// 客户端连接服务端 /// </summary> /// <returns></returns> public override Task OnConnectedAsync() { var id = Context.ConnectionId; _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Connection Server!"); return base.OnConnectedAsync(); } /// <summary> /// 客户端断开连接 /// </summary> /// <param name="exception"></param> /// <returns></returns> public override Task OnDisconnectedAsync(Exception exception) { var id = Context.ConnectionId; _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Close Connection Server!"); return base.OnDisconnectedAsync(exception); } /** * 测试 * */ /// <summary> /// 给所有客户端发送消息 /// </summary> /// <returns></returns> public async Task SendMessage(string data) { Console.WriteLine("Have one Data!"); await Clients.All.SendAll(_common.SendAll(data)); } } ``` tn2>这里有一个`CommonService`它定义在`HubService`的目录下面,里面只有一个`SendAll`方法,该方法只是在原有的消息基础上添加`Hello`和随机数。 内容如下所示: ```csharp public class CommonService { internal object SendAll(string data) { return $"Hello {new Random().Next(0, 100)} {data} "; } } ``` ### 配置SignalR tn2>我们可以通过`AddSignalR`方法来注册SignalR相关服务,并通过`AddJsonProtocol`启用SignalR 的 JSON 协议。 `PayloadSerializerOptions`是一个`System.Text.JsonJsonSerializerOptions`对象,`PropertyNamingPolicy`属性为`null`表示保持属性名称不变(是否区分大小写,无所谓)。 ```csharp builder.Services .AddSignalR() .AddJsonProtocol(options => { options.PayloadSerializerOptions.PropertyNamingPolicy = null; }) ; builder.Services.TryAddSingleton(typeof(CommonService)); ``` tn>如果想使用`NewtonsoftJson`,可以将`AddJsonProtocol`方法改为`AddNewtonsoftJsonProtocol`。 tn2>然后我们通过`MapHub`方法,加载路由路径`/ChatHub`由`ChatHub`处理,并设置传输的方式可以使用`WebSockets`与`LongPolling`。 ```csharp app.UseDefaultFiles(); app.UseStaticFiles(); app.MapHub<ChatHub>("/ChatHub", options => { options.Transports = HttpTransportType.WebSockets | HttpTransportType.LongPolling; }); ``` SignalR前端地址项目 ------------ tn2>首先我们创建一个Vue3.0的项目,并安装`@aspnet/signalr`包。 ```bash vue create signalrtestvue cd signalrtestvue npm install @aspnet/signalr ``` tn>目前`aspnet/signalr`包已经弃用了,推荐使用`@microsoft/signalr`包,<a href="https://learn.microsoft.com/zh-cn/aspnet/core/signalr/javascript-client?view=aspnetcore-6.0&tabs=visual-studio">更多请参考</a> tn2>然后我们在`src/utils`目录下编写`signalR.js`文件。 请修改你本地的signalr服务器的连接地址。 ```javascript import * as signalR from '@aspnet/signalr' const url = "http://localhost:5102/ChatHub" const signal = new signalR.HubConnectionBuilder() .withUrl(url, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets }) .configureLogging(signalR.LogLevel.Information) .build() signal.on('SendAll', (res) => { console.log(res, '收到消息') }) signal.start().then(() => { if (window.Notification) { if (Notification.permission === 'granted') { console.log('允许通知') } else if (Notification.permission !== 'denied') { console.log('需要通知权限') Notification.requestPermission((permission) => { console.log("权限通知",permission) }) } else if (Notification.permission === 'denied') { console.log('拒绝通知') } } else { console.error('浏览器不支持Notification') } console.log('连接成功') }) signal.onclose((err)=>{ console.log("连接已经断开 执行函数onclose",err) }) export default { signal } ``` tn2>通过`HubConnectionBuilder`来连接到我们的Hubs。 `withUrl`设置连接地址。 `configureLogging`设置日志记录的级别。 接下来讲将相关的事件方法。 | 事件名 | 描述 | | ------------ | ------------ | | `on` | 接收服务器返回的方法进行处理。 | | `start` | 启动Hubs的服务器连接。 | | `onclose` | 服务器关闭触发的事件回调。 | | `stop` | 关闭Hubs的服务器连接。 | | `invoke` | 调用服务器的方法。 | tn2>然后我们在`main.js`下进行全局启用`signalr`。 ```javascript import { createApp } from 'vue' import App from './App.vue' import signalr from './utils/signalR' const app = createApp(App) app.config.globalProperties.$signalr = signalr.signal; app.mount('#app') ``` tn2>在`components`目录下找到`HellloWorld.vue`并添加一个按钮向服务器端的`SendMessage`发送消息。 ```javascript <template> <div class="hello"> <h1>{{ msg }}</h1> <button @click="onClickButton" >获取</button> </div> </template> <script> export default { name: 'HelloWorld', props: { msg: String }, data() { return { }; }, methods: { onClickButton() { console.log(this.$signalr) this.$signalr .invoke('SendMessage', "hmy") .catch(function(err) {return console.error(err) }) } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h3 { margin: 40px 0 0; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style> ``` 运行测试 ------------ ```bash npm run serve ``` tn2>通过点击获取按钮来触发事件。  由服务器端发向客户端 ------------ tn2>我们这里添加一个`ClientHubController`控制器并通过请求发送`SendMessage`消息。 ```csharp [ApiController] [Route("[controller]")] public class ClientHubController : ControllerBase { private readonly ILogger<ClientHubController> _logger; public ClientHubController( ILogger<ClientHubController> logger ) { _logger = logger; } [HttpGet(Name = "SendMessage")] public async Task SendMessage(string date, [FromServices] IHubContext<ChatHub, IChatClient> hubContext) { await hubContext.Clients.All.SendAll(date); } } ``` tn>这里我使用的是强类型`IHubContext<,>`,如果是弱类型直接`IHubContext`也是可以的。 tn2>运行测试。   tn2>如果我们希望在第一个页面点击获取按钮,并返回一个发送消息的结果,我们可以通过`Clients.Caller`来将消息返回给调用方。 首先在`CommonService`中创建一个发送给调用方的消息。 ```csharp internal object SendCaller() => "Send Successful!"; ``` tn2>再在SendMessage中进行调用。 ```csharp public async Task SendMessage(string data) { Console.WriteLine("Have one Data!"); await Clients.All.SendAll(_common.SendAll(data)); await Clients.Caller.SendAll(_common.SendCaller(data)); } ```  指定客户端发送消息 ------------ tn2>如果我们想给指定的客户端发送消息,首先我们需要获取所有连接服务器的ID,这里我做一个简易的集合进行存储,并且在`OnDisconnectedAsync`与`OnConnectedAsync`事件中进行增加与删除。 ```csharp public static class UserIdsStore { static HashSet<string> Ids = new HashSet<string>(); } ``` ```csharp /// <summary> /// 客户端连接服务端 /// </summary> /// <returns></returns> public override Task OnConnectedAsync() { var id = Context.ConnectionId; // 添加用户ID UserIdsStore.Ids.Add(id); _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Connection Server!"); return base.OnConnectedAsync(); } /// <summary> /// 客户端断开连接 /// </summary> /// <param name="exception"></param> /// <returns></returns> public override Task OnDisconnectedAsync(Exception exception) { var id = Context.ConnectionId; // 删除用户ID UserIdsStore.Ids.Remove(id); _logger.LogInformation($"Client ConnectionId=> [[{id}]] Already Close Connection Server!"); return base.OnDisconnectedAsync(exception); } ``` tn2>然后我们可以在`IChatClient`中添加`SendCustomUserMessage`接口方便客户端接收。 ```csharp Task SendCustomUserMessage(object message); ``` tn2>在`ClientHubController`中添加两个接口。 ```csharp /// <summary> /// 获取所有的用户 /// </summary> /// <returns></returns> [HttpGet("GetAllUserIds", Name = "GetAllUserIds")] public string[] GetAllUserIds() { return UserIdsStore.Ids.ToArray(); } /// <summary> /// 发送指定的消息给指定的客户端 /// </summary> /// <param name="userid"></param> /// <param name="date"></param> /// <param name="hubContext"></param> /// <returns></returns> [HttpGet("SendCustomUserMessage", Name = "SendCustomUserMessage")] public async Task<IActionResult> SendCustomUserMessage( string userid, string date, [FromServices] IHubContext<ChatHub, IChatClient> hubContext ) { await hubContext.Clients.Client(userid).SendCustomUserMessage(date); return Ok("Send Successful!"); } ``` tn2>最后我们需要在我们的前端客户端中的`SignalR.js`文件,创建`SendCustomUserMessage`事件的处理。 ```javascript signal.on('SendCustomUserMessage', (res) => { console.log(res, '收到消息') }) ``` tn2>接下来运行项目,重新启动一下前端项目进行测试。 首先获取一下所有的ID。  tn2>然后我们只取其中的ID调用`SendCustomUserMessage`接口来进行发送消息。   授权访问服务器 ------------ tn2>在我们将SignalR的用户之前,首先需要授权,这里我们可以搞一个JWT的方便。 首先安装`Microsoft.AspNetCore.Authentication.JwtBearer`包  tn2>添加`MyJWTBearer`类,自定义我们的JWT的Token生成。  ```csharp public static class MyJWTBearer { public static readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); public static readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); public static string GenerateToken(HttpContext httpContext) { // 请求时传入的用户参数为NameIdentifier claim的值 var claims = new[] { new Claim(ClaimTypes.NameIdentifier, httpContext.Request.Query["user"]) }; // 签名凭据 var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); // 生成JWT Token var token = new JwtSecurityToken("SignalRTestServer", "SignalRTests", claims, expires: DateTime.UtcNow.AddSeconds(60), signingCredentials: credentials); return JwtTokenHandler.WriteToken(token); } public static void AddMyJWTBearerAuth(this IServiceCollection services) { // 添加自定义授权 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { LifetimeValidator = (before, expires, token, parameters) => expires > DateTime.UtcNow, ValidateAudience = false, ValidateIssuer = false, ValidateActor = false, ValidateLifetime = true, IssuerSigningKey = MyJWTBearer.SecurityKey }; options.Events = new JwtBearerEvents { OnMessageReceived = context => { // 当我们收到消息时,去获取请求中的access_token字段 var accessToken = context.Request.Query["access_token"]; // 如果没有就去头上找,找到了就放入我们context.token中 if (!string.IsNullOrEmpty(accessToken)) { context.Token = accessToken; } return Task.CompletedTask; } }; }); } } ``` tn>这里在生成的时候多了一个`NameIdentifier`Claim Type的类型,这个类型是当我们发消息调用`User()`方法时需要验证的时候需要的,当然后续我们可以自定义其中的逻辑进行判断。(待会再讲) tn2>接下来我们配置一下自定义的授权。 ```csharp // 添加授权服务 builder.Services.AddMyJWTBearerAuth(); ... app.UseAuthentication(); app.UseAuthorization(); // 授权路径 app.MapGet("generatetoken", c => c.Response.WriteAsync(MyJWTBearer.GenerateToken(c))); ``` tn2>测试一下 http://localhost:5102/generatetoken?user=bob   tn2>接下来我们修改一下前端项目,我们想在连接Signalr调用前先调用Token,再使用Token进行连接我们的服务器。 所以首先安装一下`axios`。 ```bash npm install axios ``` tn2>然后我们需要修改一下`main.js`,删除以前自动连接的`signalR.js`的引用,并且的添加`axios`的引用。 ```javascript import { createApp } from 'vue' import App from './App.vue' import axios from 'axios' const app = createApp(App) axios.defaults.baseURL = "http://localhost:5102" app.config.globalProperties.$http = axios; app.mount('#app') ``` tn2>然后修改`HelloWorld.vue`页面,在填写好用户名后登录。 ```javascript <template> <div class="hello"> <h1>{{ msg }}</h1> <div> UserName: <input type="text" v-model="username" > <button @click="onConnectionClickButton" >连接</button> </div> <div> Message: <input type="text" v-model="message" > <button @click="onClickButton" >发送</button> </div> </div> </template> <script> import * as signalR from '@aspnet/signalr' const url = "http://localhost:5102/ChatHub" const signal = new signalR.HubConnectionBuilder() .withUrl(url, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets, accessTokenFactory: () => "" }) .configureLogging(signalR.LogLevel.Information) .build() signal.on('SendCustomUserMessage', (res) => { console.log(res, '收到消息') }) signal.on('SendAll', (res) => { console.log(res, '收到消息') }) export default { name: 'HelloWorld', props: { msg: String }, data() { return { username: "", message: "" } }, methods: { onClickButton() { var e = this signal .invoke('SendMessage', e.message) .catch(function(err) {return console.error(err) }) }, async onConnectionClickButton() { // 首先我们去获取Token let name = this.username let result = await this.$http.get(`generatetoken?user=${name}`) if (result.status !== 200) { console.error("Token 请求失败") return } var token = result.data console.log("获得Token",token) // 放入token signal.connection.options.accessTokenFactory = () => token // 然后我们请求连接signalr signal.start().then(() => { console.log('连接成功') }) }, } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h3 { margin: 40px 0 0; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style> ``` ### 跨域问题 tn2>这里涉及到跨域问题。  ```csharp string MyAllowSpecificOrigins = "_signalrtestcores"; builder.Services.AddCors(options => { options.AddPolicy(MyAllowSpecificOrigins, builder => builder.AllowAnyOrigin() .AllowAnyHeader() .WithMethods("GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS") ) ; }); ... app.UseCors(MyAllowSpecificOrigins); ``` tn2>然后再为`ChatHub`添加授权特性`Authorize`,表示访问该资源需要进行授权。 ```csharp [Authorize] public class ChatHub : Hub<IChatClient> ``` tn2>接下来我们运行测试一下,输入用户名后,点击连接,同时也是可以发送消息的。  tn2>可以看到我们已经成功的进行了授权。 ### 断开后重连 tn2>我们与Signalr服务器断开连接后,我们希望进行重新连接,不用每次都刷新页面。可以在`onclose`事件里面进行设置。 ```javascript <script> import * as signalR from '@aspnet/signalr' const url = "http://localhost:5102/ChatHub" const signal = new signalR.HubConnectionBuilder() .withUrl(url, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets, accessTokenFactory: () => "" }) .configureLogging(signalR.LogLevel.Information) .build() signal.on('SendCustomUserMessage', (res) => { console.log(res, '收到消息') }) signal.on('SendAll', (res) => { console.log(res, '收到消息') }) export default { name: 'HelloWorld', props: { msg: String }, data() { return { username: "bob", message: "", timer: null, connectionstatus: "init" } }, mouted() { this.timer = setInterval(()=>{},500) }, destoryed() { this.clearInterval(this.timer) }, methods: { onClickButton() { var e = this signal .invoke('SendMessage', e.message) .catch(function(err) {return console.error(err) }) }, async onConnectionClickButton() { try{ // 首先我们去获取Token let name = this.username let result = await this.$http.get(`generatetoken?user=${name}`) if (result.status !== 200) { console.error("Token 请求失败") return } var token = result.data console.log("获得Token",token) var e = this // onClose的定义 if (e.connectionstatus == "init") { signal.onclose(() => { e.connectionstatus = "close" signal.stop() console.log("连接已关闭") e.retryConnection() }); } // 放入token signal.connection.options.accessTokenFactory = () => token // 然后我们请求连接signalr signal.start().then(() => { if (e.connectionstatus == "close") { clearInterval(e.timer) } e.connectionstatus = "start" console.log('连接成功') }) }catch(e){ if (e.code == "ERR_NETWORK") { console.log("Token 请求失败") } } }, retryConnection() { var e = this if (this.connectionstatus == "init" || this.connectionstatus == "start") { return }else if(this.connectionstatus == "close"){ console.log("正在重试连接...") this.timer = setInterval(()=>{ e.onConnectionClickButton() },10000) return } } } } </script> ```  tn2>这样就不用担心服务器挂了还是没挂了。
欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739
👈{{preArticle.title}}
👉{{nextArticle.title}}
评价
{{titleitem}}
{{titleitem}}
{{item.content}}
{{titleitem}}
{{titleitem}}
{{item.content}}
尘叶心繁
这一世以无限游戏为使命!
博主信息
排名
6
文章
6
粉丝
16
评论
8
文章类别
.net后台框架
165篇
linux
17篇
linux中cve
1篇
windows中cve
0篇
资源分享
10篇
Win32
3篇
前端
28篇
传说中的c
4篇
Xamarin
9篇
docker
15篇
容器编排
101篇
grpc
4篇
Go
15篇
yaml模板
1篇
理论
2篇
更多
Sqlserver
4篇
云产品
39篇
git
3篇
Unity
1篇
考证
2篇
RabbitMq
23篇
Harbor
1篇
Ansible
8篇
Jenkins
17篇
Vue
1篇
Ids4
18篇
istio
1篇
架构
2篇
网络
7篇
windbg
4篇
AI
17篇
threejs
2篇
人物
1篇
嵌入式
2篇
python
10篇
HuggingFace
8篇
pytorch
1篇
最新文章
最新评价
{{item.articleTitle}}
{{item.blogName}}
:
{{item.content}}
关于我们
ICP备案 :
渝ICP备18016597号-1
网站信息:
2018-2023
TNBLOG.NET
技术交流:
群号656732739
联系我们:
contact@tnblog.net
欢迎加群
欢迎加群交流技术