4、强类型Hub:为什么要用强类型、定义接口、实现强类型Hub、客户端调用

好,咱们今天聊聊强类型Hub。

说实话,我第一次接触SignalR的时候,用的就是那种传统的弱类型写法。那时候觉得挺爽的——客户端调服务端方法,直接传个字符串方法名,服务端调客户端也是,Clients.Caller.SendAsync("methodName", data),简单粗暴。

但后来项目一复杂,问题就来了。

为什么要用强类型?

我举个例子。你想想看,一个实时聊天系统,可能有十几个方法:SendMessageJoinGroupLeaveGroupTyping……每个方法名都是字符串。万一哪个地方拼写错了,比如把SendMessage写成了SendMesssage,编译器不会报错,运行时才会炸。嗯,这种坑我踩过不止一次。

强类型Hub说白了,就是把方法名和参数类型都固定下来。用接口约束,让编译器帮你检查。好处很明显:

  • 编译时检查:方法名写错了?参数类型不对?编译阶段就告诉你,不用等到运行时崩溃。
  • 智能提示:在Visual Studio里写代码,敲个点就能看到所有可调用的方法,不用去翻文档。
  • 重构友好:哪天想改方法名,直接改接口定义,所有引用地方自动更新。我曾经在一个大项目里重构过Hub方法,用弱类型的话,光改字符串就得改十几个文件,还怕漏掉。
  • 文档即代码:接口定义本身就是一份清晰的契约。新同事接手,看一眼接口就知道客户端和服务端能互相调用什么。

核心观点:强类型Hub不是炫技,是工程化落地的必需品。项目规模越大,收益越明显。

定义接口

咱们先定义服务端调客户端的接口。注意,这里的接口是给服务端用的,描述的是「服务端能对客户端做什么」。

public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
    Task UserJoined(string user);
    Task UserLeft(string user);
    Task ErrorOccurred(string errorMessage);
}

然后定义客户端调服务端的接口。这个接口是给客户端用的,描述的是「客户端能对服务端做什么」。

public interface IChatHub
{
    Task SendMessage(string user, string message);
    Task JoinGroup(string groupName);
    Task LeaveGroup(string groupName);
    Task Typing(string user);
}

我个人习惯把这两个接口放在一个独立的类库项目里。这样服务端和客户端都能引用,保证契约一致。我在项目中遇到过,有人把接口定义在服务端项目里,结果客户端那边复制粘贴了一份,两边不同步,调试了一下午才发现是接口签名对不上。

实现强类型Hub

有了接口,实现起来就清晰了。服务端Hub继承Hub<T>,泛型参数就是刚才定义的客户端接口。

public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        // 业务逻辑:保存消息、过滤敏感词等
        await Clients.All.ReceiveMessage(user, message);
    }

    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).UserJoined(Context.User?.Identity?.Name ?? "anonymous");
    }

    public async Task LeaveGroup(string groupName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).UserLeft(Context.User?.Identity?.Name ?? "anonymous");
    }

    public async Task Typing(string user)
    {
        await Clients.Others.ReceiveTypingNotification(user);
    }

    public override async Task OnConnectedAsync()
    {
        await base.OnConnectedAsync();
        // 记录连接日志
    }
}

注意看,这里调用客户端方法时,不再是SendAsync("ReceiveMessage", ...),而是直接Clients.All.ReceiveMessage(...)。方法名是强类型的,参数也是。如果你写错了方法名,编译器直接报错。

小技巧Clients.All返回的是IChatClient接口,所以你能调用的方法就是接口里定义的那些。不会多,也不会少。

客户端调用

客户端这边,以JavaScript为例。注册回调的时候,方法名必须和服务端接口里定义的一致。

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chatHub")
    .build();

// 注册服务端会调用的方法
connection.on("ReceiveMessage", (user, message) => {
    console.log(`${user}: ${message}`);
    // 更新UI
});

connection.on("UserJoined", (user) => {
    console.log(`${user} 加入了聊天`);
});

connection.on("UserLeft", (user) => {
    console.log(`${user} 离开了聊天`);
});

connection.on("ErrorOccurred", (errorMessage) => {
    console.error(`错误: ${errorMessage}`);
});

// 调用服务端方法
document.getElementById("sendBtn").addEventListener("click", async () => {
    const user = document.getElementById("userInput").value;
    const message = document.getElementById("messageInput").value;
    try {
        await connection.invoke("SendMessage", user, message);
    } catch (err) {
        console.error(err);
    }
});

这里有个细节:connection.on注册的回调方法名,必须和接口IChatClient里的方法名完全一致。大小写敏感。我曾经有个同事把ReceiveMessage写成了receivemessage,结果客户端一直收不到消息,查了半天才发现是大小写问题。

注意:强类型Hub只约束了服务端调用客户端时的类型安全。客户端调用服务端时,connection.invoke的第一个参数仍然是字符串。不过别担心,服务端Hub的方法名也是强类型的——你定义在IChatHub接口里的方法,在Hub类里必须实现,否则编译不过。

避坑指南

嗯,这里再分享几个我踩过的坑:

  • 接口方法必须返回Task:SignalR的Hub方法是异步的,接口里定义的方法返回值必须是TaskTask<T>。如果写成void,运行时不会报错,但行为可能不符合预期。
  • 不要重载接口方法:SignalR不支持方法重载。比如SendMessage(string user, string message)SendMessage(string user, string message, int priority),客户端调用时无法区分。我建议每个方法保持唯一签名。
  • 接口命名规范:服务端调客户端的接口,我习惯叫I{HubName}Client,比如IChatClient。客户端调服务端的接口,叫I{HubName},比如IChatHub。这样一看就知道谁调谁。

总结

强类型Hub不是什么高深的技术,但它能让你少掉很多头发。说白了,就是把运行时错误提前到编译时发现。我个人建议,只要是新项目,一律用强类型Hub。老项目重构的时候,也值得花时间改过来。

下一节咱们聊聊Hub的生命周期管理,以及如何优雅地处理连接断开和重连。到时候我会分享一个生产环境下的真实案例,保证让你有收获。