.NET Core Web + Vue 项目集成消息推送工具SignalR

.NET Core Web + Vue 项目集成消息推送工具SignalR

本文来自于我关于SignalR的系列文章,欢迎阅读与讨论~

1.SignalR是什么?

2..NET Core Web + Vue 项目集成消息推送工具SignalR

SignalR 是一个开源的 .NET 库,用于简化向应用程序中添加实时 Web 功能。 它允许服务器端代码立即向连接的客户端推送内容,而不是等待客户端请求。

以下是.Net core web + Vue 项目中集成消息推送工具SignalR的方案:

1. .NET Core Web API 后端配置

创建 SignalR Hub

csharp 复制代码
// Hubs/ChatHub.cs
using Microsoft.AspNetCore.SignalR;

public class ChatHub : Hub
{
    // 客户端连接时调用
    public override async Task OnConnectedAsync()
    {
        await Clients.Caller.SendAsync("ReceiveMessage", "System", "连接成功!");
        await base.OnConnectedAsync();
    }

    // 发送消息到所有客户端
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // 发送消息到特定组
    public async Task SendToGroup(string groupName, string user, string message)
    {
        await Clients.Group(groupName).SendAsync("ReceiveGroupMessage", user, message);
    }

    // 加入组
    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Caller.SendAsync("ReceiveMessage", "System", $"已加入组: {groupName}");
    }

    // 离开组
    public async Task LeaveGroup(string groupName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
        await Clients.Caller.SendAsync("ReceiveMessage", "System", $"已离开组: {groupName}");
    }
}

配置 SignalR 服务

csharp 复制代码
// Program.cs
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

// 添加 SignalR 服务
builder.Services.AddSignalR();

// 配置 CORS
builder.Services.AddCors(options =>
{
    options.AddPolicy("CorsPolicy", policy =>
    {
        policy.WithOrigins("http://localhost:8080") // Vue 开发服务器地址
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials();
    });
});

builder.Services.AddControllers();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseCors("CorsPolicy");
app.UseRouting();
app.UseAuthorization();

app.MapControllers();

// 配置 SignalR Hub 路由
app.MapHub<ChatHub>("/chatHub");

app.Run();

创建消息推送控制器(可选)

csharp 复制代码
// Controllers/NotificationController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{
    private readonly IHubContext<ChatHub> _hubContext;

    public NotificationController(IHubContext<ChatHub> hubContext)
    {
        _hubContext = hubContext;
    }

    [HttpPost("broadcast")]
    public async Task<IActionResult> BroadcastMessage([FromBody] BroadcastMessageRequest request)
    {
        await _hubContext.Clients.All.SendAsync("ReceiveMessage", request.User, request.Message);
        return Ok(new { success = true });
    }

    [HttpPost("send-to-group")]
    public async Task<IActionResult> SendToGroup([FromBody] GroupMessageRequest request)
    {
        await _hubContext.Clients.Group(request.GroupName)
            .SendAsync("ReceiveGroupMessage", request.User, request.Message);
        return Ok(new { success = true });
    }
}

public class BroadcastMessageRequest
{
    public string User { get; set; }
    public string Message { get; set; }
}

public class GroupMessageRequest
{
    public string GroupName { get; set; }
    public string User { get; set; }
    public string Message { get; set; }
}

2. Vue 前端配置

安装 SignalR 客户端

bash 复制代码
npm install @microsoft/signalr

创建 SignalR 服务

javascript 复制代码
// src/services/signalRService.js
import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'

class SignalRService {
  constructor() {
    this.connection = null
    this.isConnected = false
  }

  // 初始化连接
  async initConnection() {
    this.connection = new HubConnectionBuilder()
      .withUrl('http://localhost:5000/chatHub', {
        withCredentials: true // 如果需要认证
      })
      .configureLogging(LogLevel.Information)
      .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // 重连策略
      .build()

    // 注册消息处理程序
    this.registerHandlers()

    try {
      await this.connection.start()
      this.isConnected = true
      console.log('SignalR 连接成功')
      return true
    } catch (err) {
      console.error('SignalR 连接失败:', err)
      this.isConnected = false
      return false
    }
  }

  // 注册消息处理器
  registerHandlers() {
    if (!this.connection) return

    // 接收普通消息
    this.connection.on('ReceiveMessage', (user, message) => {
      console.log(`收到消息 from ${user}: ${message}`)
      // 触发自定义事件,让组件可以监听
      window.dispatchEvent(new CustomEvent('signalr-message', {
        detail: { user, message, type: 'message' }
      }))
    })

    // 接收组消息
    this.connection.on('ReceiveGroupMessage', (user, message) => {
      console.log(`收到组消息 from ${user}: ${message}`)
      window.dispatchEvent(new CustomEvent('signalr-group-message', {
        detail: { user, message, type: 'group-message' }
      }))
    })

    // 连接状态变化
    this.connection.onreconnecting((error) => {
      console.log('SignalR 重新连接中...', error)
      this.isConnected = false
    })

    this.connection.onreconnected((connectionId) => {
      console.log('SignalR 重新连接成功:', connectionId)
      this.isConnected = true
    })

    this.connection.onclose((error) => {
      console.log('SignalR 连接关闭', error)
      this.isConnected = false
    })
  }

  // 发送消息
  async sendMessage(user, message) {
    if (!this.isConnected || !this.connection) {
      console.error('SignalR 未连接')
      return false
    }

    try {
      await this.connection.invoke('SendMessage', user, message)
      return true
    } catch (err) {
      console.error('发送消息失败:', err)
      return false
    }
  }

  // 加入组
  async joinGroup(groupName) {
    if (!this.isConnected || !this.connection) {
      console.error('SignalR 未连接')
      return false
    }

    try {
      await this.connection.invoke('JoinGroup', groupName)
      return true
    } catch (err) {
      console.error('加入组失败:', err)
      return false
    }
  }

  // 离开组
  async leaveGroup(groupName) {
    if (!this.isConnected || !this.connection) {
      console.error('SignalR 未连接')
      return false
    }

    try {
      await this.connection.invoke('LeaveGroup', groupName)
      return true
    } catch (err) {
      console.error('离开组失败:', err)
      return false
    }
  }

  // 断开连接
  async disconnect() {
    if (this.connection) {
      await this.connection.stop()
      this.isConnected = false
    }
  }
}

export default new SignalRService()

创建 Vue 组件

vue 复制代码
<!-- src/components/ChatComponent.vue -->
<template>
  <div class="chat-container">
    <div class="connection-status">
      <span :class="['status-dot', isConnected ? 'connected' : 'disconnected']"></span>
      {{ connectionStatus }}
    </div>

    <div class="chat-messages" ref="messagesContainer">
      <div
        v-for="(msg, index) in messages"
        :key="index"
        :class="['message', msg.type]"
      >
        <strong>{{ msg.user }}:</strong> {{ msg.message }}
        <small class="timestamp">{{ msg.timestamp }}</small>
      </div>
    </div>

    <div class="chat-input">
      <input
        v-model="currentMessage"
        @keyup.enter="sendMessage"
        placeholder="输入消息..."
        :disabled="!isConnected"
      />
      <button @click="sendMessage" :disabled="!isConnected || !currentMessage.trim()">
        发送
      </button>
    </div>

    <div class="group-controls">
      <input v-model="groupName" placeholder="组名称" />
      <button @click="joinGroup" :disabled="!isConnected">加入组</button>
      <button @click="leaveGroup" :disabled="!isConnected">离开组</button>
    </div>
  </div>
</template>

<script>
import signalRService from '@/services/signalRService'

export default {
  name: 'ChatComponent',
  data() {
    return {
      isConnected: false,
      messages: [],
      currentMessage: '',
      groupName: 'default-group',
      userName: 'Vue用户' + Math.floor(Math.random() * 1000)
    }
  },
  computed: {
    connectionStatus() {
      return this.isConnected ? '已连接' : '未连接'
    }
  },
  async mounted() {
    // 初始化 SignalR 连接
    this.isConnected = await signalRService.initConnection()
    
    // 监听消息事件
    window.addEventListener('signalr-message', this.handleMessage)
    window.addEventListener('signalr-group-message', this.handleGroupMessage)
  },
  beforeUnmount() {
    // 清理事件监听
    window.removeEventListener('signalr-message', this.handleMessage)
    window.removeEventListener('signalr-group-message', this.handleGroupMessage)
    
    // 断开连接
    signalRService.disconnect()
  },
  methods: {
    handleMessage(event) {
      this.addMessage({
        user: event.detail.user,
        message: event.detail.message,
        type: 'received',
        timestamp: new Date().toLocaleTimeString()
      })
    },
    
    handleGroupMessage(event) {
      this.addMessage({
        user: `${event.detail.user} [组消息]`,
        message: event.detail.message,
        type: 'group',
        timestamp: new Date().toLocaleTimeString()
      })
    },
    
    addMessage(message) {
      this.messages.push(message)
      this.$nextTick(() => {
        this.scrollToBottom()
      })
    },
    
    scrollToBottom() {
      const container = this.$refs.messagesContainer
      if (container) {
        container.scrollTop = container.scrollHeight
      }
    },
    
    async sendMessage() {
      if (!this.currentMessage.trim()) return
      
      const success = await signalRService.sendMessage(this.userName, this.currentMessage)
      if (success) {
        this.addMessage({
          user: this.userName,
          message: this.currentMessage,
          type: 'sent',
          timestamp: new Date().toLocaleTimeString()
        })
        this.currentMessage = ''
      }
    },
    
    async joinGroup() {
      if (!this.groupName.trim()) return
      await signalRService.joinGroup(this.groupName)
    },
    
    async leaveGroup() {
      if (!this.groupName.trim()) return
      await signalRService.leaveGroup(this.groupName)
    }
  }
}
</script>

<style scoped>
.chat-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.connection-status {
  margin-bottom: 10px;
  font-size: 14px;
}

.status-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 5px;
}

.status-dot.connected {
  background-color: #4caf50;
}

.status-dot.disconnected {
  background-color: #f44336;
}

.chat-messages {
  height: 300px;
  overflow-y: auto;
  border: 1px solid #eee;
  padding: 10px;
  margin-bottom: 10px;
}

.message {
  margin-bottom: 8px;
  padding: 5px;
  border-radius: 4px;
}

.message.sent {
  background-color: #e3f2fd;
  text-align: right;
}

.message.received {
  background-color: #f5f5f5;
}

.message.group {
  background-color: #fff3e0;
  border-left: 3px solid #ff9800;
}

.timestamp {
  color: #666;
  font-size: 0.8em;
  margin-left: 10px;
}

.chat-input {
  display: flex;
  margin-bottom: 10px;
}

.chat-input input {
  flex: 1;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-right: 5px;
}

.group-controls {
  display: flex;
}

.group-controls input {
  flex: 1;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-right: 5px;
}

button {
  padding: 8px 16px;
  background-color: #2196f3;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-right: 5px;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

button:hover:not(:disabled) {
  background-color: #1976d2;
}
</style>

3. 在 Vue 应用中使用

javascript 复制代码
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
vue 复制代码
<!-- src/App.vue -->
<template>
  <div id="app">
    <h1>SignalR 实时聊天示例</h1>
    <ChatComponent />
  </div>
</template>

<script>
import ChatComponent from './components/ChatComponent.vue'

export default {
  name: 'App',
  components: {
    ChatComponent
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin-top: 20px;
}
</style>

4. 部署注意事项

生产环境配置

csharp 复制代码
// 生产环境的 CORS 配置
builder.Services.AddCors(options =>
{
    options.AddPolicy("CorsPolicy", policy =>
    {
        policy.WithOrigins("https://yourdomain.com") // 生产环境域名
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials();
    });
});

Vue 生产环境配置

javascript 复制代码
// 根据环境变量配置 SignalR 地址
const signalRUrl = process.env.NODE_ENV === 'production' 
  ? 'https://your-api-domain.com/chatHub'
  : 'http://localhost:5000/chatHub'

this.connection = new HubConnectionBuilder()
  .withUrl(signalRUrl, {
    withCredentials: true
  })
  // ... 其他配置

5. 高级功能扩展

身份验证集成

csharp 复制代码
// 需要认证的 Hub
[Authorize]
public class AuthChatHub : Hub
{
    public string GetUserId()
    {
        return Context.User?.Identity?.Name;
    }
}

消息持久化

csharp 复制代码
public class ChatHub : Hub
{
    private readonly IChatService _chatService;

    public ChatHub(IChatService chatService)
    {
        _chatService = chatService;
    }

    public async Task SendMessage(string user, string message)
    {
        // 保存到数据库
        await _chatService.SaveMessage(user, message);
        
        // 发送给所有客户端
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

这样你就完成了一个完整的 .NET Core + Vue + SignalR 实时消息推送系统。这个示例包含了基本的聊天功能、组消息、连接状态管理等功能,可以根据实际需求进行扩展。

相关推荐
ttod_qzstudio2 小时前
Vue 3 Props 定义详解:从基础到进阶
前端·vue.js
茶憶2 小时前
uni-app app移动端实现纵向滑块功能,并伴随自动播放
javascript·vue.js·uni-app·html·scss
茶憶2 小时前
uniapp移动端实现触摸滑动功能:上下滑动展开收起内容,左右滑动删除列表
前端·javascript·vue.js·uni-app
lemonboy2 小时前
可视化大屏适配方案:用 Tailwind CSS 直接写设计稿像素值
前端·vue.js
鹏仔工作室2 小时前
vue中实现1小时不操作则退出登录功能
前端·javascript·vue.js
前端加油站3 小时前
几种虚拟列表技术方案调研
前端·javascript·vue.js
一 乐4 小时前
医疗管理|医院医疗管理系统|基于springboot+vue医疗管理系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·医疗管理系统
万19994 小时前
asp.net core webapi------3.AutoMapper的使用
c#·.netcore
qq. 280403398414 小时前
vue介绍
前端·javascript·vue.js