在 2026 年的前端开发生态中,fetch() 已经彻底取代了 XMLHttpRequest,成为浏览器端 HTTP 请求的事实标准。但大多数开发者对 fetch() 的使用仍停留在「发请求、拿数据」的初级阶段——一个简单的 fetch(url).then(r => r.json()) 就完事了。实际上,现代 fetch() API 配合 AbortController、ReadableStream、Promise 并发控制等能力,已经可以构建出媲美专业 HTTP 客户端库(如 Axios)的生产级请求方案,而且零依赖、体积为零。
本文将从实际生产场景出发,带你掌握 fetch() 的全部高级能力。所有代码均基于原生 API,无需任何第三方库,可直接在浏览器和 Node.js 18+ 中运行。
🔐 一、请求取消与超时控制
在真实的 Web 应用中,用户会快速切换页面、反复点击搜索按钮、在文件上传中途取消操作。如果你的代码没有处理这些场景,就会出现竞态条件(Race Condition)——旧请求的响应覆盖新请求的结果,或者已离开页面的请求仍在后台消耗资源。
AbortController 基础
AbortController 是浏览器原生提供的请求取消机制。它的核心原理是通过一个 AbortSignal 对象,在请求和调用者之间建立一条「取消通道」。
// 创建一个 AbortController 实例
const controller = new AbortController()
const signal = controller.signal
// 将 signal 传递给 fetch
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已被取消')
} else {
console.error('请求失败:', err)
}
})
// 1 秒后取消请求
setTimeout(() => controller.abort(), 1000)
⚠️ 警告:
fetch()被取消后会抛出AbortError,这是一个正常的控制流行为,不要把它当作真正的错误来处理。务必在.catch()中区分AbortError和网络错误。
超时控制的正确实现
很多开发者还在用 setTimeout + controller.abort() 手动实现超时,但实际上 AbortSignal.timeout() 已经是标准 API,一行代码搞定:
// ❌ 错误写法:手动管理超时
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000)
try {
const res = await fetch(url, { signal: controller.signal })
clearTimeout(timer)
return res
} catch (e) {
clearTimeout(timer)
throw e
}
// ✅ 正确写法:使用 AbortSignal.timeout()
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const signal = AbortSignal.timeout(timeoutMs)
const response = await fetch(url, { ...options, signal })
return response
}
💡 提示:
AbortSignal.timeout()在超时后会自动调用内部的abort(),无需手动清理定时器。浏览器兼容性方面,Chrome 103+、Firefox 100+、Safari 16+ 均已支持。
多信号合并:同时支持手动取消和超时
在真实场景中,你往往需要同时支持多种取消条件——用户手动取消、超时取消、页面卸载取消。AbortSignal.any() 可以将多个信号合并为一个:
// 合并多个取消条件
async function robustFetch(url, options = {}) {
// 用户手动取消的 controller
const userController = new AbortController()
// 超时信号:10 秒
const timeoutSignal = AbortSignal.timeout(10000)
// 页面卸载信号
const unloadSignal = AbortSignal.abort() // 触发在页面 unload 时
// 任意一个信号触发就取消
const combinedSignal = AbortSignal.any([
userController.signal,
timeoutSignal
])
// 提供取消方法供外部调用
const promise = fetch(url, {
...options,
signal: combinedSignal
})
return {
promise,
cancel: () => userController.abort('用户取消')
}
}
下面是三种超时方案的对比:
| 方案 | 代码量 | 多条件支持 | 浏览器兼容性 | 推荐 |
|---|---|---|---|---|
setTimeout + abort() |
8-10 行 | 需手动管理 | 全部 | ❌ 过时 |
AbortSignal.timeout() |
1 行 | 单条件 | Chrome 103+ | ✅ 一般场景 |
AbortSignal.any() |
3-5 行 | 多条件合并 | Chrome 116+ | ✅ 复杂场景 |
🚀 二、重试策略与指数退避
网络请求失败是常态而非异常。在移动端场景下,网络抖动导致的请求失败率可达 5%-15%。一个好的重试策略可以显著提升用户体验,但盲目重试会加剧服务端压力甚至触发限流。
指数退避(Exponential Backoff)
指数退避的核心思想是:每次重试的等待时间翻倍。这给了服务端恢复的时间,也避免了大量客户端同时重试造成的「惊群效应」。
// 生产级重试函数:指数退避 + 抖动
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, {
...options,
signal: options.signal || AbortSignal.timeout(10000)
})
// 5xx 错误才重试,4xx 不重试
if (response.status >= 500) {
throw new Error(`Server Error: ${response.status}`)
}
return response
} catch (err) {
lastError = err
// 如果是用户主动取消,不重试
if (err.name === 'AbortError') throw err
// 最后一次尝试失败,直接抛出
if (attempt === maxRetries) throw err
// 指数退避 + 随机抖动
// 基础延迟:1s, 2s, 4s(指数增长)
// 抖动范围:±50%(防止惊群)
const baseDelay = Math.pow(2, attempt) * 1000
const jitter = baseDelay * (0.5 + Math.random())
const delay = Math.min(jitter, 30000) // 最大等待 30 秒
console.log(`第 ${attempt + 1} 次重试,等待 ${Math.round(delay)}ms...`)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw lastError
}
📌 **记住:**重试策略的三个关键参数:最大重试次数(通常 2-4 次)、基础延迟(通常 1-2 秒)、抖动因子(通常 0.5-1.0)。盲目设置过多重试次数会导致用户体验严重下降。
哪些错误值得重试?
不是所有失败都应该重试。正确的重试策略需要区分错误类型:
| 错误类型 | 示例 | 是否重试 | 原因 |
|---|---|---|---|
| 网络断开 | TypeError: Failed to fetch |
✅ 重试 | 可能是暂时性网络抖动 |
| 服务端错误 | HTTP 500, 502, 503 | ✅ 重试 | 服务端可能暂时过载 |
| 请求超时 | AbortError(超时触发) |
✅ 重试 | 服务端响应过慢 |
| 客户端错误 | HTTP 400, 401, 403, 404 | ❌ 不重试 | 重试不会解决问题 |
| 用户取消 | AbortError(手动触发) |
❌ 不重试 | 用户意图 |
| 请求体过大 | HTTP 413 | ❌ 不重试 | 需要修改请求内容 |
| 限流 | HTTP 429 | ⚠️ 谨重重试 | 使用 Retry-After 头的值 |
// 智能重试:根据状态码和 Retry-After 头决策
async function smartRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options)
if (response.ok) return response
// 处理 429 限流:读取 Retry-After 头
if (response.status === 429 && attempt < maxRetries) {
const retryAfter = response.headers.get('Retry-After')
const delay = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.pow(2, attempt) * 1000
await new Promise(r => setTimeout(r, delay))
continue
}
// 5xx 可重试
if (response.status >= 500 && attempt < maxRetries) {
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000))
continue
}
// 其他错误直接返回
return response
}
}
📊 三、并发控制与请求队列
当你需要一次性发起大量请求时(比如批量上传文件、并发查询多个 API),不受控制的并发会带来两个严重问题:浏览器对同一域名的并发连接数限制(Chrome 限制为 6 个),以及服务端被瞬间流量打垮。
Promise 并发限制器
一个通用的并发限制器(Concurrency Limiter)是每个前端项目的基础设施:
// 通用并发限制器
class ConcurrencyPool {
constructor(limit = 6) {
this.limit = limit
this.running = 0
this.queue = []
}
async add(asyncFn) {
// 如果已达并发上限,等待
if (this.running >= this.limit) {
await new Promise(resolve => this.queue.push(resolve))
}
this.running++
try {
return await asyncFn()
} finally {
this.running--
// 释放一个等待中的任务
if (this.queue.length > 0) {
const next = this.queue.shift()
next()
}
}
}
}
// 使用示例:并发请求 100 个 API,同时最多 5 个
const pool = new ConcurrencyPool(5)
const urls = Array.from({ length: 100 }, (_, i) => `/api/items/${i}`)
const results = await Promise.all(
urls.map(url =>
pool.add(() => fetch(url).then(r => r.json()))
)
)
console.log(`成功获取 ${results.length} 条数据`)
⚡ **关键结论:**并发限制的最佳值取决于服务端承受能力和网络环境。对于 API 请求,建议限制在 3-6 个;对于文件上传,建议限制在 2-3 个。可以通过
navigator.connection.effectiveType动态调整——4G 网络用 6 个,3G 网络用 2 个。
进度追踪:Promise.allSettled 与逐个结果
Promise.all() 有一个致命缺陷:任何一个请求失败,整个批次都会失败。在批量操作中,你更需要的是「尽可能多成功」的策略:
// 批量请求:容错 + 进度追踪
async function fetchAllWithProgress(urls, concurrency = 5) {
const pool = new ConcurrencyPool(concurrency)
const results = []
let completed = 0
const tasks = urls.map((url, index) =>
pool.add(async () => {
try {
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const data = await response.json()
results[index] = { status: 'fulfilled', data }
} catch (err) {
results[index] = { status: 'rejected', error: err.message }
} finally {
completed++
// 上报进度(可用于进度条)
const percent = Math.round((completed / urls.length) * 100)
console.log(`进度: ${completed}/${urls.length} (${percent}%)`)
}
})
)
await Promise.all(tasks)
const succeeded = results.filter(r => r.status === 'fulfilled').length
const failed = results.filter(r => r.status === 'rejected').length
console.log(`完成: ${succeeded} 成功, ${failed} 失败`)
return results
}
取消与并发的结合
在搜索框等实时场景中,用户快速输入时会产生大量请求,你需要取消旧请求 + 并发控制的组合策略:
// 搜索框防抖 + 请求取消 + 并发控制
function createSearchClient() {
let currentController = null
return {
async search(query) {
// 取消上一次请求
if (currentController) {
currentController.abort('新请求取代旧请求')
}
currentController = new AbortController()
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: currentController.signal
})
return await response.json()
} catch (err) {
if (err.name === 'AbortError') return null // 被取消,忽略
throw err
}
},
cancel() {
if (currentController) {
currentController.abort()
currentController = null
}
}
}
}
// 配合防抖使用
const client = createSearchClient()
let debounceTimer
inputElement.addEventListener('input', (e) => {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
client.search(e.target.value)
.then(results => {
if (results) renderSearchResults(results)
})
.catch(err => showError(err.message))
}, 300) // 300ms 防抖
})
💡 四、流式响应处理
fetch() 返回的 Response 对象支持 ReadableStream,这意味着你可以边接收边处理数据,而不用等整个响应下载完成。这在处理大文件下载、AI 流式输出(SSE)、NDJSON 数据流等场景中至关重要。
流式读取 AI 响应(SSE)
2026 年几乎所有 AI API(OpenAI、Claude、国产大模型)都支持流式输出。用 fetch() 处理 Server-Sent Events 的正确方式:
// 流式读取 AI API 响应
async function streamAIResponse(prompt, onChunk, onDone) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, stream: true })
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
// 按行解析 SSE 格式
const lines = buffer.split('\n')
buffer = lines.pop() // 最后一个可能不完整,留到下次
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim()
if (data === '[DONE]') {
onDone?.()
return
}
try {
const parsed = JSON.parse(data)
const content = parsed.choices?.[0]?.delta?.content
if (content) onChunk(content)
} catch {
// 忽略非 JSON 行
}
}
}
}
}
// 使用
streamAIResponse(
'解释量子计算的基本原理',
(chunk) => {
// 每收到一小段文本就更新 UI
document.getElementById('output').textContent += chunk
},
() => console.log('流式输出完成')
)
流式下载大文件(带进度)
传统方式需要等整个文件下载完才能使用,而流式下载可以实时显示进度并逐步写入:
// 流式下载文件:带进度回调
async function downloadWithProgress(url, onProgress) {
const response = await fetch(url)
const contentLength = response.headers.get('Content-Length')
const total = contentLength ? parseInt(contentLength, 10) : 0
const reader = response.body.getReader()
const chunks = []
let received = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
if (total > 0) {
const percent = Math.round((received / total) * 100)
onProgress?.(percent, received, total)
}
}
// 合并所有 chunk 为一个 Blob
const blob = new Blob(chunks)
return blob
}
// 使用:下载文件并显示进度条
downloadWithProgress('/files/report.pdf', (percent, loaded, total) => {
progressBar.style.width = `${percent}%`
statusText.textContent = `${(loaded / 1024 / 1024).toFixed(1)}MB / ${(total / 1024 / 1024).toFixed(1)}MB`
}).then(blob => {
// 创建下载链接
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'report.pdf'
a.click()
URL.revokeObjectURL(url)
})
用 TransformStream 实现 NDJSON 流式解析
NDJSON(Newline Delimited JSON)是大数据场景下常用的流式数据格式。用 TransformStream 可以优雅地实现流式解析管道:
// NDJSON 流式解析器
function createNDJSONParser() {
let buffer = ''
return new TransformStream({
transform(chunk, controller) {
buffer += chunk
const lines = buffer.split('\n')
buffer = lines.pop() // 最后一行可能不完整
for (const line of lines) {
const trimmed = line.trim()
if (trimmed) {
try {
controller.enqueue(JSON.parse(trimmed))
} catch (err) {
console.warn('解析 NDJSON 行失败:', trimmed, err)
}
}
}
},
flush(controller) {
// 处理最后残留的数据
if (buffer.trim()) {
try {
controller.enqueue(JSON.parse(buffer.trim()))
} catch {
// 忽略
}
}
}
})
}
// 使用:流式处理 NDJSON 数据
async function streamNDJSONData(url) {
const response = await fetch(url)
const parsedStream = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(createNDJSONParser())
const reader = parsedStream.getReader()
let count = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
count++
// value 已经是解析好的 JSON 对象
processRecord(value, count)
}
console.log(`共处理 ${count} 条记录`)
}
💡 提示:
TransformStream+pipeThrough()组合是 Web Streams API 的精髓。你可以串联多个 TransformStream 来构建数据处理管道——解码 → 解析 → 过滤 → 转换,每一步都是流式的,内存占用极低。
⚙️ 五、生产级 fetch 封装最佳实践
把上面的所有能力组合起来,我们可以构建一个零依赖、生产可用的 HTTP 客户端:
// 零依赖生产级 HTTP 客户端
function createHttpClient(baseURL = '', defaultOptions = {}) {
const pool = new ConcurrencyPool(defaultOptions.maxConcurrency || 6)
async function request(url, options = {}) {
const fullURL = new URL(url, baseURL).toString()
// 合并默认 headers
const headers = {
'Content-Type': 'application/json',
...defaultOptions.headers,
...options.headers
}
// 合并 signal:支持用户传入的 signal
const timeout = options.timeout ?? defaultOptions.timeout ?? 10000
const signals = [AbortSignal.timeout(timeout)]
if (options.signal) signals.push(options.signal)
const signal = AbortSignal.any(signals)
const maxRetries = options.retries ?? defaultOptions.retries ?? 3
return pool.add(() =>
fetchWithRetry(fullURL, { ...options, headers, signal }, maxRetries)
.then(async response => {
if (!response.ok) {
const body = await response.text().catch(() => '')
throw new Error(
`HTTP ${response.status}: ${response.statusText} - ${body.slice(0, 200)}`
)
}
const contentType = response.headers.get('Content-Type') || ''
if (contentType.includes('application/json')) {
return response.json()
}
return response.text()
})
)
}
return {
get: (url, opts) => request(url, { ...opts, method: 'GET' }),
post: (url, body, opts) => request(url, { ...opts, method: 'POST', body: JSON.stringify(body) }),
put: (url, body, opts) => request(url, { ...opts, method: 'PUT', body: JSON.stringify(body) }),
delete: (url, opts) => request(url, { ...opts, method: 'DELETE' }),
}
}
// 使用示例
const api = createHttpClient('https://api.example.com', {
timeout: 8000,
retries: 2,
maxConcurrency: 5,
headers: { 'Authorization': 'Bearer token123' }
})
// GET 请求
const users = await api.get('/users')
// POST 请求
const newUser = await api.post('/users', { name: '张三', role: 'dev' })
下面是我们方案与 Axios 的对比:
| 特性 | 原生 fetch 封装(本文) | Axios |
|---|---|---|
| 包体积 | 0 KB | ~13 KB (gzip) |
| 请求取消 | AbortController(原生) |
CancelToken(已废弃) |
| 流式响应 | ReadableStream(原生) |
需额外配置 |
| 并发控制 | 自带实现 | 需第三方库 |
| 自动重试 | 自带实现 | 需 axios-retry |
| 超时控制 | AbortSignal.timeout() |
内置 timeout 选项 |
| 浏览器兼容 | Chrome 42+, Firefox 39+ | 全部(含 IE) |
| Node.js 兼容 | Node.js 18+(原生支持) | 全部 |
| 推荐场景 | ✅ 现代项目首选 | ⚠️ 需兼容 IE 时使用 |
🎯 总结与建议
fetch() 在 2026 年已经是一个非常成熟的 HTTP 客户端 API。配合 AbortController、ReadableStream、Promise 并发控制,你完全可以在不引入 Axios 等第三方库的情况下,构建出生产级的网络请求方案。
⚡ 关键结论:
- ✅ 用
AbortSignal.timeout()替代手动setTimeout+abort()——更简洁、更安全 - ✅ 用
AbortSignal.any()合并多个取消条件——用户取消、超时、页面卸载三合一 - ✅ 用指数退避 + 抖动实现重试——不要盲目重试,要区分错误类型
- ✅ 用并发限制器控制批量请求——保护浏览器连接池和服务端
- ✅ 用
ReadableStream+TransformStream处理流式数据——AI 流式输出、大文件下载、NDJSON 解析的利器 - ❌ 不要在新项目中为了
fetch封装引入 Axios——除非你需要兼容 IE
如果你正在使用 jsjson.com 的在线工具处理 JSON 数据,上述流式解析和并发控制的思路同样适用。无论是批量格式化 JSON 文件,还是处理大型 JSON 数据集的转换,ReadableStream 的流式思维都能帮你突破内存限制,处理远超单次加载大小的数据量。
📌 **记住:**最好的依赖是零依赖。在引入任何第三方 HTTP 库之前,先评估原生
fetch()是否已经能满足你的需求——在 2026 年,答案大概率是「能」。