内置 fetch 与 WebSocket 客户端
本教程共 76 篇 · 第 29 篇 · 更新于 2026-07-25 · 约 5 分钟阅读
29. 内置 fetch 与 WebSocket 客户端
本节目标:用内置 fetch(undici)发请求,用 WebSocket 客户端做实时通信。
写服务端代码,免不了要调用别人的接口。以前我们装 node-fetch 或 axios,但从 v18 开始,Node.js 直接内置了浏览器标准的 fetch API。到 v24 LTS,它已经足够稳定,日常发请求不用再额外装包。
这一章讲两件事:用内置 fetch 做 HTTP 客户端,以及用内置 WebSocket 连外部的 WebSocket 服务。
内置 fetch 的来历
Node.js 的 fetch 并不是把浏览器的代码照搬过来,而是基于 Undici——一个专为 Node.js 从头写的 HTTP 客户端。它 bypass 了老旧的 http 内置模块,性能更好,语义也更现代。
Note
fetch自 v18.0.0 起实验性引入,v21 默认开启,v24 LTS 已完全稳定。如果你的项目还在用 v16,那就得老老实实装node-fetch@2。
基础 GET 请求
语法和浏览器里一模一样:
const res = await fetch('https://api.github.com/users/nodejs');
const data = await res.json();
console.log(data.login, data.public_repos);
fetch 返回的是 Response 对象,常用方法有:
| 方法 | 用途 |
|---|---|
res.json() | 按 JSON 解析 |
res.text() | 按纯文本返回 |
res.blob() | 返回 Blob 对象 |
res.arrayBuffer() | 返回 ArrayBuffer |
res.ok | 状态码 200-299 时为 true |
res.status | HTTP 状态码 |
res.headers | 响应头(Headers 对象) |
注意一个跟 axios 不一样的地方:fetch 不会因为状态码是 404 或 500 就抛异常,只有网络故障才会 reject。所以拿到 Response 后,最好手动检查 res.ok:
const res = await fetch('https://api.github.com/users/notexist-12345');
if (!res.ok) {
console.error(`HTTP ${res.status}: ${res.statusText}`);
return;
}
const data = await res.json();
POST 与自定义请求
第二个参数传一个配置对象,控制方法、头部和 body:
const res = await fetch('https://httpbin.org/post', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
},
body: JSON.stringify({ name: 'Tom', age: 20 }),
});
const data = await res.json();
console.log(data);
如果要发 application/x-www-form-urlencoded:
const params = new URLSearchParams();
params.append('name', 'Tom');
params.append('age', '20');
const res = await fetch('https://httpbin.org/post', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
});
Tip
URLSearchParams直接当body传即可,fetch会自动处理编码,不用手动toString()。
流式下载大文件
fetch 的 Response.body 是一个 ReadableStream,适合处理大文件,不用一次性塞进内存:
import { createWriteStream } from 'node:fs';
const res = await fetch('https://example.com/large-file.zip');
if (!res.ok) throw new Error('Download failed');
const file = createWriteStream('./large-file.zip');
for await (const chunk of res.body) {
file.write(chunk);
}
file.end();
console.log('Download complete');
AbortController 取消请求
请求发出后想中途取消?用 AbortController:
const controller = new AbortController();
// 3 秒后自动取消
setTimeout(() => controller.abort(), 3000);
try {
const res = await fetch('https://httpbin.org/delay/10', {
signal: controller.signal,
});
console.log(await res.json());
} catch (err) {
if (err.name === 'AbortError') {
console.log('Request was cancelled');
} else {
throw err;
}
}
这个特性在做搜索联想、翻页请求时特别有用——旧请求还没回来,用户已经输入了新内容,这时候把旧请求取消掉,避免竞争。
超时处理
fetch 本身没有 timeout 选项,但配合 AbortController 很容易实现:
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(id));
}
const res = await fetchWithTimeout('https://httpbin.org/delay/1', {}, 3000);
console.log(res.ok);
内置 WebSocket 客户端
Node.js 不仅能发 HTTP 请求,还能当 WebSocket 客户端。内置的 WebSocket 自 v21 引入,v22.4.0 起标记为稳定。注意,它只提供客户端能力,如果你想搭建 WebSocket 服务端(接受浏览器连接),还是需要 ws 或 Socket.IO 这类库。
基本用法
const socket = new WebSocket('wss://echo.websocket.org/');
socket.addEventListener('open', () => {
console.log('Connected');
socket.send('Hello from Node.js');
});
socket.addEventListener('message', event => {
console.log('Received:', event.data);
});
socket.addEventListener('close', event => {
console.log('Connection closed', event.code, event.reason);
});
socket.addEventListener('error', err => {
console.error('WebSocket error:', err.message);
});
收发 JSON
实际项目中,WebSocket 消息大多是 JSON:
const socket = new WebSocket('wss://example.com/socket');
socket.addEventListener('open', () => {
socket.send(JSON.stringify({ type: 'subscribe', channel: 'price' }));
});
socket.addEventListener('message', event => {
try {
const msg = JSON.parse(event.data);
console.log('Price update:', msg.price);
} catch (err) {
console.error('Invalid JSON:', event.data);
}
});
手动关闭连接
// 正常关闭
socket.close(1000, 'Done');
// 常用关闭码
// 1000 - 正常关闭
// 1001 - 服务端或客户端离开
// 1006 - 异常断开(不会由代码主动发送)
// 1008 - 收到不符合格式的消息
fetch 与旧客户端的对比
如果你之前用 http.get 或第三方库,可能会关心这些差异:
| 特性 | 内置 fetch | http.get / request |
|---|---|---|
| API 风格 | Promise / async-await | 回调 / 流事件 |
| 返回值 | Response 对象 | IncomingMessage |
| 自动 JSON | 手动 .json() | 手动拼接 buffer |
| 取消请求 | AbortController | req.destroy() |
| 浏览器兼容 | 完全一致 | Node.js 独有 |
我个人的迁移经验是:新项目一律 fetch,老项目如果 axios 用得很深(拦截器、并发控制、自动重试),没必要为了换而换。但如果是写脚本、微服务、测试用例,内置 fetch 绝对是最省事的选择。