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 往往会带来很多麻烦。以下是它的核心缺陷痛点 1HTTP 错误状态码4xx, 5xx不会触发catch这是新手最容易踩的坑。在 Fetch 的设计中只有网络故障如断网、DNS 解析失败、请求被拦截才会导致 Promise reject。 如果服务器返回了404 Not Found或500 Internal Server ErrorFetch 依然会正常 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特性FetchXMLHttpRequest (XHR)Axios底层实现现代标准 API老旧 API浏览器端基于 XHRNode 端基于 httpPromise 支持原生支持不支持 (需手动封装)原生支持数据转换需手动 (res.json(),JSON.stringify)需手动自动转换 JSON超时控制需手动 (AbortController)原生支持 (timeout)原生支持 (timeout)进度监听极难 (需用 Stream 模拟)原生支持 (onprogress)原生支持拦截器无无支持 (请求/响应拦截)取消请求支持 (AbortController)支持 (abort())支持建议不要在业务代码中裸写 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 {Promiseany} */ 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 只有在网络故障时才会 reject4xx/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 请求 (自动拼接 ?id123typeuser) 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 langzh-CN head meta charsetUTF-8 titleFetch 封装示例/title !-- 引入 http.js文件 -- script src./http.js/script /head body button idbtn获取数据/button script typemodule // 假设 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
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 往往会带来很多麻烦。以下是它的核心缺陷痛点 1HTTP 错误状态码4xx, 5xx不会触发catch这是新手最容易踩的坑。在 Fetch 的设计中只有网络故障如断网、DNS 解析失败、请求被拦截才会导致 Promise reject。 如果服务器返回了404 Not Found或500 Internal Server ErrorFetch 依然会正常 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特性FetchXMLHttpRequest (XHR)Axios底层实现现代标准 API老旧 API浏览器端基于 XHRNode 端基于 httpPromise 支持原生支持不支持 (需手动封装)原生支持数据转换需手动 (res.json(),JSON.stringify)需手动自动转换 JSON超时控制需手动 (AbortController)原生支持 (timeout)原生支持 (timeout)进度监听极难 (需用 Stream 模拟)原生支持 (onprogress)原生支持拦截器无无支持 (请求/响应拦截)取消请求支持 (AbortController)支持 (abort())支持建议不要在业务代码中裸写 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 {Promiseany} */ 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 只有在网络故障时才会 reject4xx/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 请求 (自动拼接 ?id123typeuser) 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 langzh-CN head meta charsetUTF-8 titleFetch 封装示例/title !-- 引入 http.js文件 -- script src./http.js/script /head body button idbtn获取数据/button script typemodule // 假设 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