Fetch API 使用及简单封装
Fetch API是现代浏览器提供的用于发起网络请求的原生 JavaScript API。它的设计初衷是替代老旧、基于回调的XMLHttpRequest(XHR),提供更强大、更灵活且基于Promise的异步编程体验。
虽然 Fetch 已经成为现代前端的标配,但它的设计存在一些“反直觉”的地方。以下是对 Fetch 的全面、详细说明。
一、 基础用法
Fetch 的核心是window.fetch()方法。它接收一个 URL 和一个可选的配置对象,返回一个 Promise。
1. GET 请求
// 传统 Promise 写法 fetch('https://api.example.com/users') .then(response => { // 注意:response.json() 返回的依然是一个 Promise return response.json(); }) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); // 现代 async/await 写法 (推荐) async function getUsers() { try { const response = await fetch('https://api.example.com/users'); const data = await response.json(); console.log(data); } catch (error) { console.error('Error:', error); } }2. POST 请求
POST 请求需要通过options参数配置method、headers和body。
async function createUser() { const userData = { name: '张三', age: 25 }; try { const response = await fetch('https://api.example.com/users', { method: 'POST', headers: { 'Content-Type': 'application/json' // 必须手动声明 JSON 格式 }, body: JSON.stringify(userData) // 必须手动将对象转为 JSON 字符串 }); const result = await response.json(); console.log('Success:', result); } catch (error) { console.error('Error:', error); } }⚠️ 避坑指南 (FormData):如果你要上传文件,使用了
FormData对象作为body,千万不要手动设置Content-Type。浏览器会自动将其设置为multipart/form-data并附带正确的boundary。如果你手动设置了,反而会破坏 boundary,导致后端无法解析文件。
二、 响应处理 (Response 对象)
fetchresolve 后返回的是一个Response对象,它包含了服务器的响应头、状态码和响应体。
1. 解析响应体
响应体是流 (Stream),只能被读取一次。根据数据类型,调用不同的方法:
response.json():解析为 JSON 对象(最常用)。response.text():解析为纯文本。response.blob():解析为二进制大对象(常用于下载文件、图片预览)。response.arrayBuffer():解析为原始二进制缓冲区(常用于处理音频、视频流)。
2. 常用 Response 属性
response.ok(Boolean):极其重要。当 HTTP 状态码在 200-299 之间时为true。response.status(Number):HTTP 状态码(如 200, 404, 500)。response.statusText(String):状态信息(如 "OK", "Not Found")。response.headers(Headers 对象):响应头,可通过response.headers.get('Content-Type')获取。
三、 Fetch 的四大“痛点”(为什么我们需要封装?)
尽管 Fetch 是原生 API,但直接裸写 Fetch 往往会带来很多麻烦。以下是它的核心缺陷:
痛点 1:HTTP 错误状态码(4xx, 5xx)不会触发catch
这是新手最容易踩的坑。在 Fetch 的设计中,只有网络故障(如断网、DNS 解析失败、请求被拦截)才会导致 Promise reject。 如果服务器返回了404 Not Found或500 Internal Server Error,Fetch 依然会正常 resolve,进入then。
fetch('/api/not-found') // 假设返回 404 .then(res => { console.log(res.ok); // false console.log(res.status); // 404 // 注意:这里不会进入 catch! }) .catch(err => { // 只有断网时才会进这里 });解决方案:必须手动判断response.ok,如果不为 true,则手动抛出错误。
痛点 2:原生不支持超时控制
XHR 可以通过xhr.timeout设置超时,但 Fetch 原生没有这个属性。如果服务器一直不响应,请求会无限期挂起。解决方案:必须借助AbortController结合setTimeout手动实现(如上一个回答中的封装)。
痛点 3:默认不携带 Cookie (跨域时)
在跨域请求时,Fetch 默认不会携带目标域的 Cookie。解决方案:需要显式设置credentials: 'include'。
fetch('https://api.other-domain.com/data', { credentials: 'include' // 允许跨域携带 Cookie })痛点 4:无法直接监听上传/下载进度
XHR 提供了xhr.upload.onprogress和xhr.onprogress来轻松实现进度条。Fetch 没有直接的进度事件。解决方案:下载进度可以通过ReadableStream模拟计算;上传进度目前 Fetch 原生支持极差,通常只能退回使用 XHR 或 Axios。
四、 高级特性 (Fetch 的杀手锏)
虽然 Fetch 有痛点,但它提供了 XHR 无法比拟的现代特性:
1. 中断请求 (AbortController)
这是 Fetch 最强大的特性之一,可以轻松取消正在进行的请求(例如用户快速切换页面,或防抖取消上一次请求)。
const controller = new AbortController(); const signal = controller.signal; // 发起请求 fetch('https://api.example.com/slow-data', { signal }) .then(res => res.json()) .then(data => console.log(data)) .catch(err => { if (err.name === 'AbortError') { console.log('请求被手动取消'); } else { console.error('请求失败', err); } }); // 3秒后取消请求 setTimeout(() => { controller.abort(); // 触发取消 }, 3000);2. 流式读取 (ReadableStream)
Fetch 允许你以流的方式分块读取响应体,这在处理大文件下载或SSE (Server-Sent Events) 实时推送时非常有用,不需要等整个文件下载完才开始处理。
fetch('https://example.com/large-file.zip') .then(response => { const reader = response.body.getReader(); const contentLength = +response.headers.get('Content-Length'); let receivedLength = 0; let chunks = []; return (async function read() { while(true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); receivedLength += value.length; console.log(`下载进度: ${(receivedLength / contentLength * 100).toFixed(2)}%`); } // 将所有块合并为一个 Blob let blob = new Blob(chunks); console.log('下载完成', blob); })(); });五、 总结与最佳实践
Fetch vs XMLHttpRequest vs Axios
特性 | Fetch | XMLHttpRequest (XHR) | Axios |
|---|---|---|---|
底层实现 | 现代标准 API | 老旧 API | 浏览器端基于 XHR,Node 端基于 http |
Promise 支持 | 原生支持 | 不支持 (需手动封装) | 原生支持 |
数据转换 | 需手动 ( | 需手动 | 自动转换 JSON |
超时控制 | 需手动 ( | 原生支持 ( | 原生支持 ( |
进度监听 | 极难 (需用 Stream 模拟) | 原生支持 ( | 原生支持 |
拦截器 | 无 | 无 | 支持 (请求/响应拦截) |
取消请求 | 支持 ( | 支持 ( | 支持 |
建议:
- 不要在业务代码中裸写 Fetch:因为它的痛点太多(状态码不报错、无超时、无自动 JSON 转换)。
- 中小型项目/简单脚本:使用自定义 Fetch 封装类,轻量且无第三方依赖。
- 中大型商业项目:建议使用Axios。它完美解决了 Fetch 的所有痛点,提供了拦截器、自动转换、取消请求等工程化能力,且生态极其成熟。
- 特殊场景使用原生 Fetch:当你需要处理流式数据 (SSE)、大文件分块下载,或者在 Service Worker 中拦截请求时,必须使用原生 Fetch。
六、对Fetch简单封装
由于原生fetch存在一些痛点(如:不支持超时控制、不自动转换 JSON、HTTP 4xx/5xx 不会进入 catch、GET 请求处理参数麻烦)。这个封装解决了这些问题,同时保持了代码的极简。
主要说明:
- 自动超时中断:利用
AbortController实现了原生fetch缺失的超时控制,防止请求无限挂起。 - 智能参数处理:
GET请求自动将对象序列化为 URL Query 字符串;POST/PUT自动将对象转换为 JSON 字符串。 - 状态码校验:原生
fetch遇到 404 或 500 依然会进入then,封装后统一在!response.ok时抛出异常,进入catch,符合直觉。 - 智能响应解析:自动检测
Content-Type,如果是 JSON 则自动调用.json(),否则返回.text(),避免解析报错。 - 安全过滤:拼接 URL 参数时,自动过滤掉
undefined和null的值,避免产生?key=这样的脏 URL
// http.js /** * 轻量级 Fetch 请求封装 */ class Http { /** * @param {string} baseURL - 基础请求地址 * @param {object} defaultOptions - 默认配置(如全局 headers、timeout) */ constructor(baseURL = '', defaultOptions = {}) { this.baseURL = baseURL; this.defaultOptions = { timeout: 10000, // 默认 10 秒超时 ...defaultOptions, }; } /** * 核心请求方法 * @param {string} url - 请求路径 * @param {object} options - 请求配置 { method, params, data, headers, timeout, ... } * @returns {Promise<any>} */ async request(url, options = {}) { const { method = 'GET', params, data, headers = {}, timeout = this.defaultOptions.timeout, ...rest } = options; const upperMethod = method.toUpperCase(); // 1. 拼接完整 URL 和 Query 参数 const fullUrl = this._buildUrl(url, params); // 2. 合并 Headers (默认携带 application/json) const finalHeaders = { 'Content-Type': 'application/json', ...this.defaultOptions.headers, ...headers, }; // 3. 构建 Fetch 选项 (GET/HEAD 请求不允许携带 body) const fetchOptions = { method: upperMethod, headers: finalHeaders, ...rest, }; if (data && !['GET', 'HEAD'].includes(upperMethod)) { fetchOptions.body = typeof data === 'string' ? data : JSON.stringify(data); } // 4. 超时控制 (使用 AbortController) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); fetchOptions.signal = controller.signal; try { const response = await fetch(fullUrl, fetchOptions); clearTimeout(timeoutId); // 5. 校验 HTTP 状态码 (fetch 只有在网络故障时才会 reject,4xx/5xx 需要手动判断) if (!response.ok) { throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); } // 6. 智能解析响应体 const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); } return await response.text(); } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error('Request Timeout'); } // 这里可以统一接入全局错误提示 (如 Toast.error(error.message)) throw error; } } /** * 辅助方法:拼接 URL 和 Query 参数 */ _buildUrl(url, params) { const targetUrl = url.startsWith('http') ? url : this.baseURL + url; if (!params) return targetUrl; const searchParams = new URLSearchParams(); Object.keys(params).forEach(key => { if (params[key] !== undefined && params[key] !== null) { searchParams.append(key, params[key]); } }); const queryString = searchParams.toString(); return queryString ? `${targetUrl}?${queryString}` : targetUrl; } // ================= 快捷方法 ================= get(url, params = {}, options = {}) { return this.request(url, { ...options, method: 'GET', params }); } post(url, data = {}, options = {}) { return this.request(url, { ...options, method: 'POST', data }); } put(url, data = {}, options = {}) { return this.request(url, { ...options, method: 'PUT', data }); } delete(url, params = {}, options = {}) { // RESTful 风格中,DELETE 有时带 params,有时带 data,这里默认支持 params return this.request(url, { ...options, method: 'DELETE', params }); } } // 导出 (支持 ES Modules 和 CommonJS) export default Http; // module.exports = Http; // 如果是 CommonJS 环境,取消此行注释1. 基础实例化与调用
// 实例化,配置全局基础 URL 和默认 Header (例如 Token) const api = new Http('https://api.example.com/v1', { headers: { 'Authorization': 'Bearer YOUR_TOKEN_HERE' }, timeout: 8000 // 全局默认 8 秒超时 }); // GET 请求 (自动拼接 ?id=123&type=user) api.get('/users', { id: 123, type: 'user' }) .then(res => console.log('用户数据:', res)) .catch(err => console.error('请求失败:', err.message)); // POST 请求 (自动 JSON.stringify body) api.post('/users', { name: '张三', age: 25 }) .then(res => console.log('创建成功:', res)) .catch(err => console.error('请求失败:', err.message));2. 临时覆盖默认配置
// 某次请求需要不同的 Header 或更长的超时时间 api.post( '/upload', { file: 'data' }, { timeout: 30000, // 此次请求 30 秒超时 headers: { 'Content-Type': 'multipart/form-data' } // 覆盖默认 Content-Type } );3. 在 HTML 中直接引入使用 (无需构建工具)
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Fetch 封装示例</title> <!-- 引入 http.js文件 --> <script src="./http.js"></script> </head> <body> <button id="btn">获取数据</button> <script type="module"> // 假设 Http 类已在当前作用域定义 const api = new Http('https://jsonplaceholder.typicode.com'); document.getElementById('btn').addEventListener('click', async () => { try { const data = await api.get('/posts', { _limit: 1 }); console.log('获取成功:', data); alert(JSON.stringify(data)); } catch (error) { console.error('获取失败:', error.message); alert('请求失败: ' + error.message); } }); </script> </body> </html>