Azure Web PubSub TypeScript SDK

azure-web-pubsub-ts
分类通用
作者Agentic Awesome Skills 社区
许可MIT
评分4.40/5
使用16.4K

Azure Web PubSub TypeScript SDK

基于 WebSocket 连接和发布/订阅模式的实时消息传递。

安装

bash
# 服务端管理
npm install @azure/web-pubsub @azure/identity

客户端实时消息传递

npm install @azure/web-pubsub-client

用于事件处理程序的 Express 中间件

npm install @azure/web-pubsub-express

环境变量

bash
WEBPUBSUB_CONNECTION_STRING=Endpoint=https://<resource>.webpubsub.azure.com;AccessKey=<key>;Version=1.0;
WEBPUBSUB_ENDPOINT=https://<resource>.webpubsub.azure.com

服务端:WebPubSubServiceClient

身份验证

typescript
import { WebPubSubServiceClient, AzureKeyCredential } from "@azure/web-pubsub";
import { DefaultAzureCredential } from "@azure/identity";

// 使用连接字符串
const client = new WebPubSubServiceClient(
process.env.WEBPUBSUB_CONNECTION_STRING!,
"chat" // hub 名称
);

// 使用 DefaultAzureCredential (推荐)
const client2 = new WebPubSubServiceClient(
process.env.WEBPUBSUB_ENDPOINT!,
new DefaultAzureCredential(),
"chat"
);

// 使用 AzureKeyCredential
const client3 = new WebPubSubServiceClient(
process.env.WEBPUBSUB_ENDPOINT!,
new AzureKeyCredential("<access-key>"),
"chat"
);

生成客户端访问令牌

typescript
// 基础令牌
const token = await client.getClientAccessToken();
console.log(token.url);  // wss://...?access_token=...

// 包含用户 ID 的令牌
const userToken = await client.getClientAccessToken({
userId: "user123",
});

// 包含权限的令牌
const permToken = await client.getClientAccessToken({
userId: "user123",
roles: [
"webpubsub.joinLeaveGroup",
"webpubsub.sendToGroup",
"webpubsub.sendToGroup.chat-room", // 特定组
],
groups: ["chat-room"], // 连接时自动加入
expirationTimeInMinutes: 60,
});

发送消息

typescript
// 广播给 hub 中的所有连接
await client.sendToAll({ message: "Hello everyone!" });
await client.sendToAll("Plain text", { contentType: "text/plain" });

// 发送给特定用户(其所有连接)
await client.sendToUser("user123", { message: "Hello!" });

// 发送给特定连接
await client.sendToConnection("connectionId", { data: "Direct message" });

// 使用过滤器发送 (OData 语法)
await client.sendToAll({ message: "Filtered" }, {
filter: "userId ne 'admin'",
});

组管理

typescript
const group = client.group("chat-room");

// 将用户/连接添加到组
await group.addUser("user123");
await group.addConnection("connectionId");

// 从组中移除
await group.removeUser("user123");

// 发送给组内所有成员
await group.sendToAll({ message: "Group message" });

// 关闭组内所有连接
await group.closeAllConnections({ reason: "Maintenance" });

连接管理

typescript
// 检查是否存在
const userExists = await client.userExists("user123");
const connExists = await client.connectionExists("connectionId");

// 关闭连接
await client.closeConnection("connectionId", { reason: "Kicked" });
await client.closeUserConnections("user123");
await client.closeAllConnections();

// 权限管理
await client.grantPermission("connectionId", "sendToGroup", { targetName: "chat" });
await client.revokePermission("connectionId", "sendToGroup", { targetName: "chat" })


客户端:WebPubSubClient

连接

typescript
import { WebPubSubClient } from "@azure/web-pubsub-client";

// 直接使用 URL
const client = new WebPubSubClient("<client-access-url>");

// 通过 negotiate 接口动态获取 URL
const client2 = new WebPubSubClient({
getClientAccessUrl: async () => {
const response = await fetch("/negotiate");
const { url } = await response.json();
return url;
},
});

// 在启动前注册处理器
client.on("connected", (e) => {
console.log(已连接: ${e.connectionId});
});

client.on("group-message", (e) => {
console.log(${e.message.group}: ${e.message.data});
});

await client.start();

发送消息

typescript
// 先加入组
await client.joinGroup("chat-room");

// 发送到组
await client.sendToGroup("chat-room", "Hello!", "text");
await client.sendToGroup("chat-room", { type: "message", content: "Hi" }, "json");

// 发送选项
await client.sendToGroup("chat-room", "Hello", "text", {
noEcho: true, // 不回显给发送者
fireAndForget: true, // 不等待确认响应 (ack)
});

// 向服务器发送事件
await client.sendEvent("userAction", { action: "typing" }, "json");

事件处理器

typescript
// 连接生命周期
client.on("connected", (e) => {
  console.log(已连接: ${e.connectionId}, 用户: ${e.userId});
});

client.on("disconnected", (e) => {
console.log(已断开: ${e.message});
});

client.on("stopped", () => {
console.log("客户端已停止");
});

// 消息处理
client.on("group-message", (e) => {
console.log([${e.message.group}] ${e.message.fromUserId}: ${e.message.data});
});

client.on("server-message", (e) => {
console.log(服务器消息: ${e.message.data});
});

// 重新加入组失败
client.on("rejoin-group-failed", (e) => {
console.log(重新加入 ${e.group} 失败: ${e.error});
});

Express 事件处理器

typescript
import express from "express";
import { WebPubSubEventHandler } from "@azure/web-pubsub-express";

const app = express();

const handler = new WebPubSubEventHandler("chat", {
path: "/api/webpubsub/hubs/chat/",

// 阻塞式:批准/拒绝连接
handleConnect: (req, res) => {
if (!req.claims?.sub) {
res.fail(401, "需要身份验证");
return;
}
res.success({
userId: req.claims.sub[0],
groups: ["general"],
roles: ["webpubsub.sendToGroup"],
});
},

// 阻塞式:处理自定义事件
handleUserEvent: (req, res) => {
console.log(来自 ${req.context.userId} 的事件:, req.data);
res.success(已收到: ${req.data}, "text");
},

// 非阻塞式
onConnected: (req) => {
console.log(客户端已连接: ${req.context.connectionId});
},

onDisconnected: (req) => {
console.log(客户端已断开: ${req.context.connectionId});
},
});

app.use(handler.getMiddleware());

// Negotiate 接口
app.get("/negotiate", async (req, res) => {
const token = await serviceClient.getClientAccessToken({
userId: req.user?.id,
});
res.json({ url: token.url });
});

app.listen(8080);

关键类型

typescript
// 服务端
import {
  WebPubSubServiceClient,
  WebPubSubGroup,
  GenerateClientTokenOptions,
  HubSendToAllOptions,
} from "@azure/web-pubsub";

// 客户端
import {
WebPubSubClient,
WebPubSubClientOptions,
OnConnectedArgs,
OnGroupDataMessageArgs,
} from "@azure/web-pubsub-client";

// Express
import {
WebPubSubEventHandler,
ConnectRequest,
UserEventRequest,
ConnectR


javascript
esponseHandler,
} from "@azure/web-pubsub-express";

最佳实践

1. 使用 Entra ID 认证 - 生产环境建议使用 DefaultAzureCredential
2. 在启动前注册处理器 - 避免遗漏初始事件
3. 使用组(Groups)管理频道 - 按主题或房间组织消息
4. 处理重连 - 客户端默认支持自动重连
5. 在 handleConnect 中进行验证 - 尽早拒绝未经授权的连接
6. 使用 noEcho - 根据需要防止消息回显给发送者

适用场景

本技能适用于执行概览中所描述的工作流或操作。

局限性

  • 仅在任务与上述范围明确匹配时使用此技能。
  • 不要将输出结果视为针对特定环境的验证、测试或专家评审的替代方案。
  • 如果缺少必要的输入、权限、安全边界或成功标准,请停止操作并请求澄清。