1. 为什么Fetch API是现代前端开发的必备技能
作为一名经历过jQuery时代的开发者,我至今还记得第一次接触Fetch API时的震撼。相比传统的XMLHttpRequest,Fetch提供了更简洁、更强大的异步请求能力。但很多开发者(包括曾经的我)在使用时常常陷入一些看似简单却影响深远的陷阱。
Fetch API基于Promise设计,这意味着我们可以告别回调地狱。但它的强大远不止于此——从简单的GET请求到复杂的文件上传,从JSON处理到二进制数据流,Fetch都能优雅应对。然而,正是这种表面上的简单性,让很多开发者忽略了其底层机制,导致在实际项目中频频踩坑。
2. JSON数据处理的正确姿势
2.1 Content-Type的重要性
在前后端分离的架构中,JSON已经成为数据交换的事实标准。但很多开发者发送JSON请求时常常忽略一个关键点:Content-Type头。
javascript复制// 错误示范 - 缺少Content-Type
fetch('/api/user', {
method: 'POST',
body: JSON.stringify({name: '张三'})
})
这样的请求看似简单,却隐藏着严重问题。服务器无法自动识别请求体的格式,很可能返回415 Unsupported Media Type错误。
2.2 正确的JSON请求方式
javascript复制// 正确做法
fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: '张三',
age: 25
})
})
这里有几个关键点需要注意:
- Content-Type必须明确设置为application/json
- 数据必须经过JSON.stringify序列化
- 即使数据很简单,也不应该省略这些步骤
提示:在后端开发中,Express需要使用app.use(express.json())中间件来解析JSON请求体,而Spring Boot则需要@RequestBody注解。
2.3 常见错误排查
当JSON请求出现问题时,可以按照以下步骤排查:
- 检查开发者工具中的Network面板,确认请求头是否包含Content-Type: application/json
- 确认请求体确实是字符串化的JSON,而不是JavaScript对象
- 检查后端是否配置了正确的JSON解析中间件
我曾经遇到过一个棘手的bug:前端发送的JSON请求在某些浏览器上工作正常,但在另一些浏览器上却失败。经过仔细排查,发现是某个浏览器插件修改了Content-Type头。这个经历让我明白,永远不要假设运行环境是完美的。
3. 请求参数的正确传递方式
3.1 GET请求的参数处理
GET请求的参数应该通过URL的查询字符串传递。很多开发者会犯两个常见错误:
- 试图在GET请求中使用body传递参数
- 手动拼接URL导致编码问题
javascript复制// 错误示范 - 手动拼接URL
fetch(`/api/search?q=${keyword}`) // 可能引发编码问题
// 正确做法 - 使用URLSearchParams
const params = new URLSearchParams({
q: '前端开发',
page: 1,
size: 10
})
fetch(`/api/search?${params}`)
URLSearchParams会自动处理特殊字符的编码问题,避免URL格式错误。
3.2 POST请求的表单数据
当需要发送表单数据时,应该使用FormData对象而不是JSON:
javascript复制const formData = new FormData()
formData.append('username', '李四')
formData.append('password', '123456')
fetch('/api/login', {
method: 'POST',
body: formData
})
这种情况下,浏览器会自动设置Content-Type为multipart/form-data,并生成正确的请求体格式。
注意:当使用FormData时,不要手动设置Content-Type头,浏览器会自动处理。手动设置可能会破坏正确的边界标记。
4. 文件上传的实现细节
4.1 基本文件上传
文件上传是Fetch API中最容易被误用的功能之一。最常见的错误是试图用JSON格式发送文件:
javascript复制// 绝对错误的做法!
fetch('/api/upload', {
method: 'POST',
body: JSON.stringify({
file: document.getElementById('file-input').files[0]
})
})
正确的做法是使用FormData:
javascript复制const formData = new FormData()
const fileInput = document.querySelector('input[type="file"]')
formData.append('avatar', fileInput.files[0])
fetch('/api/upload', {
method: 'POST',
body: formData
})
4.2 上传进度监控
虽然Fetch API本身不提供进度事件,但我们可以通过以下方式实现进度监控:
javascript复制const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
const reader = response.body.getReader()
let receivedLength = 0
const contentLength = +response.headers.get('Content-Length')
while(true) {
const {done, value} = await reader.read()
if(done) break
receivedLength += value.length
console.log(`进度: ${(receivedLength/contentLength*100).toFixed(1)}%`)
}
4.3 大文件分片上传
对于大文件上传,分片是更好的选择:
javascript复制async function uploadFile(file) {
const CHUNK_SIZE = 5 * 1024 * 1024 // 5MB
const totalChunks = Math.ceil(file.size / CHUNK_SIZE)
for(let i = 0; i < totalChunks; i++) {
const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE)
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunkIndex', i)
formData.append('totalChunks', totalChunks)
formData.append('fileId', file.name + file.size)
await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
})
console.log(`上传进度: ${((i + 1) / totalChunks * 100).toFixed(1)}%`)
}
}
5. 二进制数据处理技巧
5.1 加载图片资源
Fetch API可以方便地处理二进制数据,如图片:
javascript复制fetch('/images/logo.png')
.then(response => response.blob())
.then(blob => {
const img = document.createElement('img')
img.src = URL.createObjectURL(blob)
document.body.appendChild(img)
// 记得在不需要时释放内存
img.onload = () => URL.revokeObjectURL(img.src)
})
5.2 下载文件
对于文件下载,我们可以结合Blob和URL.createObjectURL:
javascript复制fetch('/api/export-pdf')
.then(res => res.blob())
.then(blob => {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'report.pdf'
a.click()
setTimeout(() => URL.revokeObjectURL(url), 100)
})
5.3 ArrayBuffer处理
对于更底层的二进制数据处理,可以使用ArrayBuffer:
javascript复制fetch('/api/binary-data')
.then(res => res.arrayBuffer())
.then(buffer => {
const view = new DataView(buffer)
const magicNumber = view.getUint32(0, false)
// 处理二进制数据...
})
6. 请求控制与错误处理
6.1 中断请求
AbortController是控制Fetch请求的强大工具:
javascript复制const controller = new AbortController()
const signal = controller.signal
// 设置超时自动取消
setTimeout(() => controller.abort(), 5000)
fetch('/api/slow-request', {signal})
.then(res => res.json())
.catch(err => {
if(err.name === 'AbortError') {
console.log('请求被取消')
} else {
console.error('请求错误:', err)
}
})
6.2 请求超时处理
结合AbortController和setTimeout可以实现请求超时:
javascript复制function fetchWithTimeout(url, options = {}, timeout = 8000) {
const controller = new AbortController()
options.signal = controller.signal
const timeoutId = setTimeout(() => controller.abort(), timeout)
return fetch(url, options)
.finally(() => clearTimeout(timeoutId))
}
6.3 错误处理最佳实践
完善的错误处理应该考虑多种情况:
javascript复制async function safeFetch(url, options) {
try {
const response = await fetch(url, options)
if(!response.ok) {
const error = new Error(`HTTP错误: ${response.status}`)
error.status = response.status
throw error
}
const contentType = response.headers.get('content-type')
if(contentType.includes('application/json')) {
return await response.json()
} else if(contentType.includes('text/')) {
return await response.text()
} else {
return await response.blob()
}
} catch(error) {
if(error.name === 'AbortError') {
console.log('请求被取消')
} else if(error.name === 'TypeError') {
console.log('网络错误或跨域问题')
} else {
console.log('其他错误:', error)
}
throw error
}
}
7. 高级应用场景
7.1 并发请求控制
使用Promise.all处理多个并发请求:
javascript复制async function fetchMultiple(urls) {
try {
const responses = await Promise.all(
urls.map(url => fetch(url).then(res => res.json()))
)
return responses
} catch(error) {
console.error('一个或多个请求失败:', error)
throw error
}
}
对于大量请求,可以使用分批次处理:
javascript复制async function batchFetch(urls, batchSize = 5) {
const results = []
for(let i = 0; i < urls.length; i += batchSize) {
const batchUrls = urls.slice(i, i + batchSize)
const batchResults = await fetchMultiple(batchUrls)
results.push(...batchResults)
await new Promise(resolve => setTimeout(resolve, 1000)) // 批次间隔
}
return results
}
7.2 认证与授权
处理JWT认证的常见模式:
javascript复制function createAuthHeaders() {
const token = localStorage.getItem('jwt')
return {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
async function fetchWithAuth(url, options = {}) {
const headers = createAuthHeaders()
const response = await fetch(url, {
...options,
headers: {
...headers,
...options.headers
}
})
if(response.status === 401) {
// 处理token过期
await refreshToken()
return fetchWithAuth(url, options)
}
return response
}
7.3 缓存策略
利用Cache API实现离线功能:
javascript复制// 缓存响应
async function cacheResponse(request, response) {
const cache = await caches.open('my-cache')
await cache.put(request, response.clone())
return response
}
// 优先从缓存读取
async function fetchWithCache(url) {
const cache = await caches.open('my-cache')
const cachedResponse = await cache.match(url)
if(cachedResponse) return cachedResponse
const response = await fetch(url)
if(response.ok) {
await cacheResponse(url, response)
}
return response
}
8. 性能优化技巧
8.1 请求压缩
启用请求压缩可以减少传输数据量:
javascript复制fetch('/api/data', {
headers: {
'Accept-Encoding': 'gzip, deflate, br'
}
})
8.2 预加载关键资源
使用link rel="preload"提前获取关键资源:
html复制<link rel="preload" href="/api/critical-data" as="fetch" crossorigin>
8.3 避免不必要的请求
实现请求去重:
javascript复制const pendingRequests = new Map()
async function deduplicatedFetch(url, options) {
if(pendingRequests.has(url)) {
return pendingRequests.get(url)
}
const promise = fetch(url, options).finally(() => {
pendingRequests.delete(url)
})
pendingRequests.set(url, promise)
return promise
}
9. 跨域问题解决方案
9.1 CORS基础配置
服务端需要设置正确的CORS头:
javascript复制// Express示例
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://yourdomain.com')
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
next()
})
9.2 带凭证的请求
需要凭证的请求需要特殊处理:
javascript复制fetch('https://api.example.com/user', {
credentials: 'include'
})
服务端响应必须包含:
code复制Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://yourdomain.com // 不能是*
9.3 预检请求优化
对于复杂请求,可以使用预检缓存:
javascript复制// 服务端设置
res.header('Access-Control-Max-Age', '86400') // 24小时
10. 实战经验总结
经过多年的Fetch API使用经验,我总结了以下黄金法则:
- 永远设置Content-Type头,特别是使用JSON时
- 文件上传必须使用FormData,不能使用JSON
- 长时间运行的请求应该实现取消功能
- 错误处理要区分网络错误、HTTP错误和应用错误
- 对于关键请求,实现重试机制
- 合理使用缓存减少不必要的网络请求
- 监控和优化请求性能
一个特别容易忽视的点是内存管理。使用URL.createObjectURL创建的URL应该在不需要时及时释放:
javascript复制const blob = await response.blob()
const imageUrl = URL.createObjectURL(blob)
// 使用完成后
URL.revokeObjectURL(imageUrl)
另一个常见问题是忘记处理响应体的释放。在读取完响应体后,应该调用相应的方法释放资源:
javascript复制const response = await fetch(url)
const data = await response.json() // 或text(), blob()等
// 之后不能再读取响应体
最后,记住Fetch API虽然强大,但并不是所有场景的最佳选择。对于需要更高级功能的场景(如上传进度事件),可能需要考虑使用XMLHttpRequest或专门的库。