一、SignalR 是什么?能解决啥问题?
简单说,SignalR 就是一个帮网页实现 “实时聊天” 的工具。以前是客户端不停问服务器要数据(轮询),现在换成服务器主动给客户端发消息,比如后台处理到第 100 条数据了,马上告诉前端更新进度条。
具体解决这 3 个痛点:
- 进度看得见:批量抓取企业信息时,实时显示 “已处理 200 条,失败 5 条” 这样的动态信息,不再让用户干等。
- 不卡不费资源:用长连接代替频繁请求,就像开了个热线电话,有消息直接传,省流量还快。
- 能随时喊停:用户觉得任务太慢想取消?前端点个按钮,后台立刻停止处理,不浪费资源。
二、前端怎么写?一步步跟着来
1. 先准备好 “聊天工具”
在网页里引入 SignalR 的客户端库,就像安装微信才能发消息一样:
@section Scripts{
<!-- 基础库 -->
<script src="~/Scripts/jquery.min.js"></script>
<!-- SignalR客户端,用来连接服务器 -->
<script src="~/Scripts/jquery.signalR-2.4.3.min.js"></script>
<!-- 自动生成的"聊天房间"代码,后端定义的ProgressHub会在这里出现 -->
<script src="~/signalr/hubs"></script>
}
2. 建立连接:先和服务器搭上话
// 初始化连接对象
var hubConnection = $.connection.hub;
// 对应后端的ProgressHub,就像找到对应的聊天房间
var progressHub = $.connection.ProgressHub;
// 告诉前端:当服务器发进度消息时,要做什么
progressHub.client.ReceiveProgress = function (progress) {
// 更新网页上的进度显示
$('#progressMessage').html(progress.Message);
// 任务完成了就关掉进度弹窗
if (progress.IsCompleted) layer.close(progressLayer);
};
// 启动连接(这里用"长轮询"方式,兼容性好)
function startSignalR() {
return new Promise((resolve, reject) => {
if (hubConnection.state === $.signalR.connectionState.connected) {
resolve(); // 已经连上了就直接下一步
return;
}
// 开始连接,成功了走resolve,失败了走reject
hubConnection.start({ transport: 'longPolling' })
.done(resolve)
.fail(reject);
});
}
这里用 Promise 是为了保证 “必须先连上服务器,再开始任务”,就像打电话要等对方接通了再说话。
3. 启动任务 + 控制流程:用户点按钮,后台开始干活
function startBatchTask(taskEvent, taskName) {
// 先弹出一个进度弹窗,告诉用户任务开始了
progressLayer = layer.open({
type: 1,
title: `正在处理${taskName}`,
content: '<div class="layui-text-center"><p id="progressMessage">加载中...</p></div>'
});
// 连接成功后,告诉后端开始任务
startSignalR().then(() => {
$.ajax({
url: `/xxxx/xxxxx/${taskEvent}`,
success: function (res) {
if (res.Success) {
currentTaskId = res.TaskId; // 记住任务ID,后续用来停任务
console.log(`任务开始,ID是${currentTaskId}`);
} else {
layer.close(progressLayer);
layer.msg('任务启动失败', { icon: 2 });
}
}
});
});
}
// 用户关闭弹窗时,终止任务
function handleCloseOperation() {
if (currentTaskId) {
// 告诉服务器:把这个ID的任务停掉!
progressHub.server.stopTask(currentTaskId);
}
// 断开连接+刷新表格
disconnectAndRefresh();
}
这里就像用户点了 “开始下载”,先弹出下载进度条,下载过程中也能随时点 “取消”。
三、后端怎么写?让服务器会 “主动报信”
1. 定义 "聊天房间"Hub:允许前后端互相调用
[HubName("ProgressHub")] // 前端通过这个名字找到我
public class ProgressHub : Hub {
// 用字典记录每个任务的取消令牌,方便停任务
private static readonly Dictionary<string, CancellationTokenSource> TaskCancellationTokenSources = new Dictionary<string, CancellationTokenSource>();
// 前端调用这个方法来停任务
public async Task StopTask(string taskId) {
lock (TaskCancellationTokenSources) { // 保证线程安全,别让多个任务抢资源
if (TaskCancellationTokenSources.TryGetValue(taskId, out var cts)) {
cts.Cancel(); // 取消正在进行的任务
TaskCancellationTokenSources.Remove(taskId); // 从字典里删掉,避免内存泄漏
}
}
// 告诉所有客户端(这里其实只需要告诉发起任务的客户端)任务终止了
await Clients.All.SendAsync("ReceiveProgress", new { TaskId = taskId, Message = "任务已终止" });
}
}
Hub 就像一个中转站,前端说 “停任务”,Hub 收到后通知后台取消,同时能给前端发消息。
2. 处理任务 + 发进度:后台干活时不忘报信
public JsonResult GetAllEnterpriseBaseInfo() {
var taskId = Guid.NewGuid().ToString("N"); // 生成唯一任务ID
// 用Task.Run开一个后台线程处理,别阻塞主线程
Task.Run(() => ProcessEnterpriseDataAsync(taskId, _enterpriseService.GetFactoryList(), _baseInfoUrl, "企业基本信息"));
return Json(new { Success = true, TaskId = taskId }); // 告诉前端任务启动成功,给个ID
}
private async Task ProcessEnterpriseDataAsync(string taskId, List<FactoryModel> factoryList, string apiUrl, string taskType) {
var totalCount = factoryList.Count;
var processedCount = 0;
var failedCount = 0;
try {
for (int i = 0; i < totalCount; i += 200) { // 分批处理,一次处理200条
var batch = factoryList.Skip(i).Take(200).ToList();
// 处理每一条数据...中间省略具体逻辑...
processedCount += batch.Count; // 记录处理了多少条
// 每处理10条或者处理完了,就给前端发一次进度
if (processedCount % 10 == 0 || processedCount == totalCount) {
SendProgressUpdateAsync(taskId, totalCount, processedCount, failedCount, taskType, processedCount >= totalCount);
}
}
} catch (Exception ex) {
// 出错了也告诉前端
SendProgressUpdateAsync(taskId, totalCount, processedCount, totalCount, taskType, true, "任务出错啦:" + ex.Message);
}
}
private void SendProgressUpdateAsync(string taskId, int total, int done, int failed, string taskName, bool isDone, string msg = null) {
var hubContext = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>(); // 获取Hub实例
var message = msg ?? (isDone ?
$"完成啦!总共{total}条,失败{failed}条" :
$"处理中...已完成{done}/{total}条");
// 给所有客户端发消息,这里其实应该只发给发起任务的客户端,不过示例简化了
hubContext.Clients.All.ReceiveProgress(new ProgressUpdate {
TaskId = taskId,
Message = message,
IsCompleted = isDone
});
}
核心就是:后台每处理一批数据,就调用SendProgressUpdateAsync
告诉前端当前进度,就像快递员每到一个站点就更新物流信息。
四、SignalR 背后的原理:到底怎么做到实时的?
1. 连接方式:就像不同的通信工具
SignalR 会根据浏览器能力自动选择最合适的连接方式:
- WebSocket:现代浏览器首选,像开了个实时聊天窗口,双向通信最快。
- 长轮询:浏览器不支持 WebSocket 时用,就像客户端问 “有新消息吗?”,服务器没消息就等着,有消息马上回,比频繁轮询省流量。
- 还有一些兼容旧浏览器的方式,比如 IFrame,不过现在用得少了。
2. 双向通信:前后端能互相调用
- 前端可以调用后端 Hub 里的方法,比如
progressHub.server.stopTask()
,就像给服务器发了一条指令:“把这个任务停了”。 - 后端可以调用前端的方法,比如
hubContext.Clients.All.ReceiveProgress()
,相当于服务器给前端发消息:“进度更新啦,快显示给用户看”。
3. 异步处理:后台干活不阻塞
用Task.Run
和async/await
让耗时操作(比如网络请求、数据库插入)在后台线程执行,就像你一边下载文件一边浏览网页,互不影响,服务器能同时处理更多任务。
五、为啥非得用 SignalR?对比一下就知道
方案 | 优点 | 缺点 | 适合场景 |
---|---|---|---|
轮询 | 简单,兼容性好 | 巨费流量,巨慢 | 数据更新极慢场景 |
WebSocket | 最快,实时性最强 | 自己处理兼容性超麻烦 | 高实时性需求 |
SignalR | 自动适配连接方式,代码少 | 多引入一个库(很小) | 90% 的实时场景 |
简单说:SignalR 帮你把复杂的底层通信搞定了,你只需要关注 “什么时候该发进度” 和 “收到进度怎么显示”,开发效率飙升!
六、效果图长啥样?交互流程走一遍
1. 大概长这样(示意图):
(实际图会有 “开始任务” 按钮、进度文字、取消按钮,任务完成后自动关闭)
2. 用户操作步骤:
- 点击 “一键获取企业信息” → 弹出进度弹窗,显示 “正在连接服务器”。
- 连接成功后,后台开始处理,弹窗显示 “已处理 50 条 / 1000 条”。
- 处理过程中用户可点击弹窗关闭按钮 → 后台任务终止,弹窗显示 “任务已取消”。
- 任务正常完成后,弹窗自动关闭,数据表格刷新显示最新结果。
七、新手注意!这些坑别踩
- 连接失败要重试:网络不好时可能连接不上,前端加个重试逻辑,比如 3 秒后再连一次。
- 别发太多消息:后台每处理 10 条发一次进度就够了,别每条都发,不然前端会卡。
- 任务 ID 要保密:别把任务 ID 随便暴露,防止有人恶意终止别人的任务,后端停任务前检查用户权限。
- 服务端客户端事件名称要一致:服务端发送的方法要和页面注册的名称一致,要不然发送不了
- 旧浏览器兼容:如果用户用 IE8 之类的古董,可能需要启用 Forever Frame 模式,但现在基本没人用了,建议引导用户升级浏览器。
- -先连接后通信:千万要记住先要通了话,才能说话。要不然即使后端发送了消息,等前端连接上也接收不到,消息就会报废掉
总结:SignalR 就是实时通信的 “懒人工具”
用 SignalR,就像点外卖用美团,不用自己找骑手,平台都帮你搞定。前端简单几行代码就能接收进度,后端调用 Hub 发消息就行,开发效率翻倍,用户体验还好。下次遇到需要实时更新的场景,直接上 SignalR 准没错!