# WebSocket
> Nitro 基于 CrossWS 和 H3 提供跨平台的 WebSocket 支持。
WebSocket 可以在客户端与服务器之间实现实时双向通信。Nitro 的 WebSocket 集成适用于所有受支持的部署目标,包括 Node.js、Bun、Deno 以及 Cloudflare Workers。
## 启用 WebSocket
在 Nitro 配置中启用 WebSocket 支持:
```ts [nitro.config.ts]
import { defineConfig } from "nitro";
export default defineConfig({
features: {
websocket: true,
},
});
```
## 用法
使用 `defineWebSocketHandler` 创建一个 WebSocket 处理器,并从路由文件中导出。WebSocket 处理器与普通请求处理器一样,遵循相同的[文件路由](/docs/routing)规则。
```ts [routes/_ws.ts]
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
open(peer) {
console.log("已连接:", peer.id);
},
message(peer, message) {
console.log("收到消息:", message.text());
peer.send("来自服务器的问候!");
},
close(peer, details) {
console.log("已断开:", peer.id, details.code, details.reason);
},
error(peer, error) {
console.error("错误:", error);
},
});
```
你可以为 WebSocket 处理器使用任意路由路径。例如,`routes/chat.ts` 会处理 `/chat` 上的 WebSocket 连接。
### 从客户端连接
使用浏览器的 [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) 进行连接:
```js
const ws = new WebSocket("ws://localhost:3000/_ws");
ws.addEventListener("open", () => {
console.log("已连接!");
ws.send("来自客户端的问候!");
});
ws.addEventListener("message", (event) => {
console.log("收到:", event.data);
});
```
## 钩子
WebSocket 处理器支持以下生命周期钩子:
### `upgrade`
在 WebSocket 连接建立之前调用。你可以用它来校验请求、设置命名空间,或给 peer 附加上下文数据。
```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
upgrade(request) {
const url = new URL(request.url);
const token = url.searchParams.get("token");
if (!isValidToken(token)) {
throw new Response("未授权", { status: 401 });
}
return {
context: { userId: getUserId(token) },
};
},
open(peer) {
console.log("用户已连接:", peer.context.userId);
},
// ...
});
```
`upgrade` 钩子可以返回一个对象,包含:
|
属性
|
类型
|
说明
|
headers
|
HeadersInit
|
要包含在升级响应中的响应头
|
namespace
|
string
|
覆盖此连接的发布/订阅命名空间
|
context
|
object
|
附加到
peer.context
的数据
|
抛出一个 `Response` 即可拒绝升级请求。
### `open`
当 WebSocket 连接建立完成,且 peer 已可收发消息时调用。
```ts
open(peer) {
peer.send("欢迎!");
}
```
### `message`
在收到 peer 发来的消息时调用。
```ts
message(peer, message) {
const text = message.text();
const data = message.json();
}
```
### `close`
当 WebSocket 连接关闭时调用。会收到一个 `details` 对象,其中可能包含 `code` 和 `reason`。
```ts
close(peer, details) {
console.log(`已关闭:${details.code} - ${details.reason}`);
}
```
### `error`
在 WebSocket 连接发生错误时调用。
```ts
error(peer, error) {
console.error("WebSocket 错误:", error);
}
```
## Peer
`peer` 对象表示一个已连接的 WebSocket 客户端。除 `upgrade` 外,其他所有钩子中都可以访问它。
### 属性
|
属性
|
类型
|
说明
|
id
|
string
|
当前 peer 的唯一标识
|
namespace
|
string
|
当前 peer 所属的发布/订阅命名空间
|
context
|
object
|
在
upgrade
阶段设置的任意上下文数据
|
request
|
Request
|
原始升级请求
|
peers
|
Set
|
同一命名空间下的所有已连接 peer
|
topics
|
Set
|
当前 peer 订阅的主题
|
remoteAddress
|
string?
|
客户端 IP 地址(取决于适配器)
|
websocket
|
WebSocket
|
底层的 WebSocket 实例
|
### 方法
#### `peer.send(data, options?)`
直接向当前 peer 发送消息。支持字符串、对象(会序列化为 JSON)以及二进制数据。
```ts
peer.send("你好!");
peer.send({ type: "greeting", text: "你好!" });
```
#### `peer.subscribe(topic)`
让当前 peer 订阅一个发布/订阅主题。
```ts
peer.subscribe("notifications");
```
#### `peer.unsubscribe(topic)`
取消当前 peer 对某个主题的订阅。
```ts
peer.unsubscribe("notifications");
```
#### `peer.publish(topic, data, options?)`
向同一命名空间内订阅了该主题的所有 peer 广播消息。发送消息的 peer **不会**收到这条消息。
```ts
peer.publish("chat", { user: "小明", text: "大家好!" });
```
#### `peer.close(code?, reason?)`
优雅地关闭 WebSocket 连接。
```ts
peer.close(1000, "正常关闭");
```
#### `peer.terminate()`
立即终止连接,不发送关闭帧。
## Message
`message` 钩子中的 `message` 对象提供了多种方法,用于以不同格式读取传入数据。
|
方法
|
返回类型
|
说明
|
text()
|
string
|
将消息作为 UTF-8 字符串读取
|
json()
|
T
|
将消息解析为 JSON
|
uint8Array()
|
Uint8Array
|
将消息读取为字节数组
|
arrayBuffer()
|
ArrayBuffer
|
将消息读取为 ArrayBuffer
|
blob()
|
Blob
|
将消息读取为 Blob
|
```ts
message(peer, message) {
// 按文本解析
const text = message.text();
// 按带类型的 JSON 解析
const data = message.json<{ type: string; payload: unknown }>();
}
```
## 发布/订阅
发布/订阅(pub/sub)允许你通过主题向一组已连接的 peer 广播消息。peer 订阅主题后,就能收到发布到这些主题的消息。
```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
open(peer) {
peer.subscribe("chat");
peer.publish("chat", { system: `${peer} 加入了聊天室` });
peer.send({ system: "欢迎来到聊天室!" });
},
message(peer, message) {
// 广播给其他所有订阅者
peer.publish("chat", {
user: peer.toString(),
text: message.text(),
});
// 回显给发送者
peer.send({ user: "你", text: message.text() });
},
close(peer) {
peer.publish("chat", { system: `${peer} 离开了聊天室` });
},
});
```
`peer.publish()` 会将消息发送给该主题的所有订阅者,但**不包括**发送消息的 peer 自己。如果也要发给发布者,请使用 `peer.send()`。
### 命名空间
命名空间为 WebSocket 连接提供隔离的发布/订阅分组。每个 peer 都属于某一个命名空间,而 `peer.publish()` 只会向同一命名空间内的 peer 广播。
默认情况下,命名空间由请求 URL 的路径名推导而来。这与[动态路由](/docs/routing#dynamic-routes)能够自然配合,也就是说每个路径都会拥有自己独立的命名空间:
```ts [routes/rooms/[room].ts]
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
open(peer) {
peer.subscribe("messages");
peer.publish("messages", `${peer} 加入了 ${peer.namespace}`);
},
message(peer, message) {
// 只会发送给同一房间内的 peer
peer.publish("messages", `${peer}: ${message.text()}`);
},
close(peer) {
peer.publish("messages", `${peer} 离开了`);
},
});
```
在这个示例中,连接到 `/rooms/game` 的客户端会与连接到 `/rooms/lobby` 的客户端彼此隔离,因为每个路径都对应自己的命名空间。
如果要覆盖默认命名空间,可以在 `upgrade` 钩子中返回自定义的 `namespace`:
```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";
export default defineWebSocketHandler({
upgrade(request) {
// 按查询参数分组连接,而不是按路径名
const url = new URL(request.url);
const channel = url.searchParams.get("channel") || "general";
return {
namespace: `chat:${channel}`,
};
},
open(peer) {
peer.subscribe("messages");
peer.publish("messages", `${peer} 加入了`);
},
message(peer, message) {
peer.publish("messages", `${peer}: ${message.text()}`);
},
close(peer) {
peer.publish("messages", `${peer} 离开了`);
},
});
```
## Server-Sent Events(SSE)
[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) 在只需要服务端到客户端单向流式推送时,是一种更简单的替代方案。与 WebSocket 不同,SSE 使用标准 HTTP,并支持自动重连。
```ts [routes/sse.ts]
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";
export default defineHandler((event) => {
const stream = createEventStream(event);
const interval = setInterval(async () => {
await stream.push(`Message @ ${new Date().toLocaleTimeString()}`);
}, 1000);
stream.onClosed(() => {
clearInterval(interval);
});
return stream.send();
});
```
客户端可以通过 [EventSource API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) 连接:
```js
const source = new EventSource("/sse");
source.onmessage = (event) => {
console.log(event.data);
};
```
### 结构化消息
SSE 消息支持可选的 `id`、`event` 和 `retry` 字段:
```ts [routes/events.ts]
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";
export default defineHandler((event) => {
const stream = createEventStream(event);
let id = 0;
const interval = setInterval(async () => {
await stream.push({
id: String(id++),
event: "update",
data: JSON.stringify({ value: Math.random() }),
retry: 3000,
});
}, 1000);
stream.onClosed(() => {
clearInterval(interval);
});
return stream.send();
});
```