小白前端速成:7天搞懂JS Fetch,彻底告别XMLHttpRequest
还在写?我的天,2024年都快过完了,你那是 archaeology(考古)知道吗?上次我看到有人新项目里引入 jQuery 就为了发个请求,我当场就emo了——兄弟,咱浏览器早就内置了 Fetch,免费、原生、不用打包,它不香吗?但说实话,Fetch 这玩意儿刚上手的时候确实挺反人类的。说好的"现代 API",结果你照抄文档写个 ,然后一看,undefined。再改,报 CORS 错误。再改,报
小白前端速成:7天搞懂JS Fetch,彻底告别XMLHttpRequest
- 小白前端速成:7天搞懂JS Fetch,彻底告别XMLHttpRequest
-
- 还在用 jQuery?你那是情怀还是懒啊
- Fetch 到底是啥?别被名字忽悠了
- 最简单的 GET 请求,真的一行吗
- POST 请求的坑,JSON 格式老是 400
- Headers 那些事儿,写错真的翻车
- async await 和 then,别再混着写了
- 真实项目里没人直接用 fetch,都是怎么封装的
- 跨域问题真的全是后端的锅吗
- 网络失败、超时、非200状态码,怎么优雅处理
- 别人封装的 fetch 工具为啥那么稳
- 开发时疯狂刷新,怎么避免重复请求刷爆接口
- Mock 数据没到位?用拦截加延迟模拟真实体验
- 调试三板斧,Network 面板看不明白时的绝招
- "Failed to fetch" 到底啥情况,90% 是这几个原因
- 兼容性真那么差吗,什么时候必须上 axios
- 那个大坑:fetch 默认不 reject HTTP 错误状态码
- 顺手写个带 loading、重试、缓存的小型请求管理器
- 别人问你 fetch 和 axios 有啥区别,这样回答显得专业
- 实战彩蛋:用 fetch 写个天气查询小工具
小白前端速成:7天搞懂JS Fetch,彻底告别XMLHttpRequest
还在写$.ajax?我的天,2024年都快过完了,你那是 archaeology(考古)知道吗?上次我看到有人新项目里引入 jQuery 就为了发个请求,我当场就emo了——兄弟,咱浏览器早就内置了 Fetch,免费、原生、不用打包,它不香吗?
但说实话,Fetch 这玩意儿刚上手的时候确实挺反人类的。说好的"现代 API",结果你照抄文档写个 fetch('/api/user'),然后 console.log 一看,undefined。再改,报 CORS 错误。再改,报 404。再改,得,直接 “Failed to fetch”,连404都不如。这时候你就开始怀疑人生:我特么是不是不适合干前端?
别慌,谁不是这么过来的。我当年第一次用 Fetch,在工位上 debugging 了三个小时,最后发现是后端返回的是 text 但我当成了 JSON 来 parse,当场想把键盘吃了。所以今天这篇,都是血泪史,带你从"发个请求都报错"进化到"封装 API 如丝般顺滑"。坐稳了,咱们开始。
还在用 jQuery?你那是情怀还是懒啊
先说清楚,我不是针对 jQuery,它当年确实是神,没有它就没有现代前端。但问题是,现在都是 2024 年了,ES6 都发布快十年了,浏览器都支持原生 Fetch 了,你还为了发个 HTTP 请求引入一整个 jQuery 库,这就好比你为了吃口米饭买了整个农场——土豪也不是这么当的啊。
XMLHttpRequest 更不用说了,那 API 设计得就像是故意要整你。你想发个简单 GET 请求?先 new XMLHttpRequest(),然后 open(),然后 setRequestHeader(),然后 onload,然后 send()。我数了数,最少六行代码,中间还得处理各种状态码,0 是未初始化,1 是 open 调用了,2 是 send 了,3 是 receiving,4 才是 done。我特么就想要个数据,你跟我整这么多状态是要考研啊?
Fetch 的最大优势就三个字:原生、简洁、基于 Promise。不用引库,不用 polyfill(大部分情况下),浏览器自带,直接就能用。而且返回的是 Promise,可以 async await,代码看起来就跟同步一样舒服。但别高兴太早,简洁的代价就是坑多,我们一个个来填。
Fetch 到底是啥?别被名字忽悠了
很多人以为 Fetch 就是个函数,调用一下就完事了。Naive。Fetch 其实是个 Web API,定义在 Window 接口上(或者 Worker 全局作用域),它返回的是一个 Promise,这个 Promise resolve 的时候给你一个 Response 对象。
但这里有个超级大坑,也是新手最常踩的:Fetch 的 Promise 只有在网络故障(offline、DNS 查询失败、连接被拒绝等)的时候才会 reject。如果服务器返回 404、500、403,Promise 依然会 resolve,只是 Response.ok 会变成 false。这跟 axios 完全不一样,axios 会把 4xx 和 5xx 自动当成错误 reject 掉。
所以刚学 Fetch 的时候,你肯定写过这种代码:
// 错误的示范,新手大概率会这么写
fetch('/api/data')
.then(data => console.log(data))
.catch(err => console.error(err));
看起来挺对是吧?错得离谱。首先 data 不是真的数据,是个 Response 对象。其次如果服务器返回 500,这个代码根本不会进 catch,你拿到的 data 里装着 500 错误页面,你还以为请求成功了。
正确的姿势至少得是这样:
// 稍微靠谱点的写法
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json(); // 注意这里也是个 Promise
})
.then(data => console.log('真的数据来了:', data))
.catch(err => console.error('真出错了:', err));
看到没,多了个 response.ok 的判断,而且 response.json() 返回的还是 Promise,得链式调用。这就是为什么很多人觉得 Fetch 难用——它把 HTTP 的底层细节都暴露给你了,你得自己处理状态码,自己转换数据格式,自己处理错误。自由是自由了,但也费劲啊。
最简单的 GET 请求,真的一行吗
那天我在群里看到有人吹牛:“Fetch 发 GET 请求只要一行代码!” 然后他写了:
fetch('/api/list').then(r => r.json()).then(console.log);
技术上说,这确实能跑,但生产环境你敢这么写,明天运维就能冲到你工位上真人PK。这一行代码里埋了多少雷?没错误处理、没超时控制、没 loading 状态、没考虑 204 No Content 的情况,万一返回的不是 JSON 而是纯文本,直接报错给你看。
咱们来个真正能用的 GET 请求封装,起码要带点基础的错误处理:
// 基础版 GET 请求,能应付简单场景
async function getData(url) {
try {
const response = await fetch(url);
// 这里必须检查状态码,fetch 不会自动抛错
if (!response.ok) {
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
}
// 判断返回类型,别盲目 json()
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else {
return await response.text();
}
} catch (error) {
console.error('网络请求出错了:', error);
// 这里可以统一处理网络错误,比如提示用户检查网络
throw error;
}
}
// 使用方式
getData('https://jsonplaceholder.typicode.com/posts/1')
.then(data => console.log('拿到数据:', data))
.catch(err => console.error('GG:', err));
看到注释里说的了吧,别看到接口就 response.json(),有些接口返回 text/plain,或者 204 No Content,你调 json() 会直接抛 SyntaxError,那种"Unexpected end of JSON input"的错误,排查起来能要了你半条命。
另外 GET 请求带参数怎么办?别手工拼字符串了,用 URLSearchParams:
// 带查询参数的 GET 请求
async function getWithParams(url, params) {
// 自动处理参数编码,告别手动拼接 ?a=1&b=2
const queryString = new URLSearchParams(params).toString();
const fullUrl = queryString ? `${url}?${queryString}` : url;
console.log('实际请求的 URL:', fullUrl);
const response = await fetch(fullUrl, {
method: 'GET',
headers: {
'Accept': 'application/json',
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// 使用示例
getWithParams('https://api.example.com/search', {
keyword: '前端开发',
page: 1,
pageSize: 10
});
这样写最大的好处是自动 URL 编码,你要是手动拼字符串,遇到中文或者特殊字符,百分百出问题。上次我就看到一个同事手写 ?name=张三,结果传到后端变成乱码,debug 了两小时发现没 encodeURIComponent,当场自闭。
POST 请求的坑,JSON 格式老是 400
POST 请求是新手坟场,重灾区。你是不是经常遇到这种情况:明明代码写的是 POST,后端非说没收到参数;或者后端返回 400 Bad Request,提示"JSON 解析错误"。
大概率是你没搞对 Headers 和 Body 的格式。Fetch 默认的 Content-Type 是… 等等,Fetch 默认根本没有 Content-Type!如果你直接传个对象给 body,它会自动调用 toString(),变成 [object Object],后端收到这个人都傻了。
正确的 POST 请求长这样:
// 标准的 JSON POST 请求
async function postJSON(url, data) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json', // 这行不写必翻车
'Accept': 'application/json',
},
body: JSON.stringify(data) // 必须序列化成字符串
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`请求失败 ${response.status}: ${errorText}`);
}
return response.json();
}
// 使用示例
postJSON('https://jsonplaceholder.typicode.com/posts', {
title: '我又踩坑了',
body: 'Fetch 的 Content-Type 真难搞',
userId: 1
}).then(data => console.log('创建成功:', data));
看见 JSON.stringify 了吗?必须手动转字符串。而且 Content-Type 必须显式设置成 application/json,不然后端(特别是 Spring Boot 或者 Express)不知道你要传 JSON,可能当成表单数据处理,那就对不上号了。
要是传表单数据怎么办?比如文件上传或者传统的 form post:
// 表单数据提交(multipart/form-data)
async function postForm(url, formData) {
// 注意:这时候不要手动设置 Content-Type!
// 浏览器会自动设置,并且加上 boundary 字符串
const response = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json'
// 千万别写 'Content-Type': 'multipart/form-data',不然 boundary 没了
},
body: formData // 直接传 FormData 对象
});
return response.json();
}
// 使用示例
const formData = new FormData();
formData.append('username', '张三');
formData.append('avatar', fileInput.files[0]); // 文件上传
postForm('https://api.example.com/upload', formData);
这里有个反直觉的点:当你使用 FormData 的时候,绝对不能手动设置 Content-Type。浏览器会自动帮你设置成 multipart/form-data; boundary=----WebKitFormBoundary... 这种带随机 boundary 的格式。如果你手贱自己设置了,boundary 就没了,后端解析文件上传的时候直接懵圈,收到的文件大小都是 0 字节。这种 bug 我查了一下午,最后发现是这行代码的问题,当时想抽自己。
Headers 那些事儿,写错真的翻车
Headers 是 HTTP 请求的灵魂,Fetch API 对 Headers 的处理还挺灵活的,你可以传对象,也可以传 Headers 实例。但我建议统一用对象,简单明了。
但有些 Headers 是浏览器禁止设置的,比如 Referer、User-Agent、Cookie 这些,你设置了也没用,浏览器会忽略。这是为了安全考虑,防止前端伪造请求来源。
常见的 Headers 配置 combo 我帮你整理好了:
// 常用的请求头配置
const commonHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9',
// 自定义 token,这是最常见的
'Authorization': `Bearer ${localStorage.getItem('token') || ''}`
};
// 封装一个带默认 headers 的请求
async function requestWithAuth(url, options = {}) {
const defaultOptions = {
headers: {
...commonHeaders,
...options.headers // 允许覆盖
}
};
// 特别处理:如果是 FormData,删除 Content-Type,让浏览器自动设置
if (options.body instanceof FormData) {
delete defaultOptions.headers['Content-Type'];
}
const response = await fetch(url, { ...defaultOptions, ...options });
// 统一处理 401 未授权
if (response.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login'; // 跳登录页
throw new Error('登录已过期,请重新登录');
}
return response;
}
看见最后那个 401 处理了吗?这才是真实项目该有的样子。token 过期了自动跳转登录页,别让用户一直卡在"数据加载中"的转圈圈页面。
async await 和 then,别再混着写了
我发现很多新手有个坏习惯,就是混着用 Promise 的链式调用和 async await,写出来的代码长得跟意大利面条似的:
// 这种写法看着就头疼,别这么搞
fetch('/api/data')
.then(async (response) => {
const data = await response.json();
const processed = await processData(data);
return processed;
})
.then(result => {
return fetch('/api/save', {
method: 'POST',
body: JSON.stringify(result)
});
})
.then(response => response.json())
.catch(err => console.error(err));
这代码虽然能跑,但可读性极差,debug 的时候你想加个断点都难。我的建议是:一个项目里统一风格,要么全用 async await,要么全用 then 链式。现在主流是 async await,代码看起来像是同步的,逻辑清晰多了。
但是!async await 也有坑,就是错误处理。你写一堆 await,只要中间有一个抛错,后面的都不执行了。所以要用 try-catch 包起来:
// 清晰多了对吧
async function loadUserData() {
try {
// 获取用户基本信息
const userRes = await fetch('/api/user');
if (!userRes.ok) throw new Error('获取用户失败');
const user = await userRes.json();
// 获取用户的订单列表
const ordersRes = await fetch(`/api/user/${user.id}/orders`);
if (!ordersRes.ok) throw new Error('获取订单失败');
const orders = await ordersRes.json();
// 获取第一个订单的详情
if (orders.length > 0) {
const detailRes = await fetch(`/api/orders/${orders[0].id}`);
if (!detailRes.ok) throw new Error('获取订单详情失败');
const detail = await detailRes.json();
return { user, orders, firstOrderDetail: detail };
}
return { user, orders };
} catch (error) {
console.error('加载数据出错:', error);
// 这里可以显示错误提示 UI
showErrorToast(error.message);
return null;
}
}
看到这种层级结构了吗?一眼就能看出数据依赖关系:先拿用户信息,再拿订单,再拿详情。如果用 then 链式,可能得嵌套三层,到时候括号都对不齐。
但有时候你需要并行请求,这时候 await 就得配合 Promise.all:
// 并行请求,同时发三个接口,等全部返回
async function loadDashboardData() {
try {
const [userData, statsData, noticeData] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/stats').then(r => r.json()),
fetch('/api/notices').then(r => r.json())
]);
return { userData, statsData, noticeData };
} catch (error) {
// Promise.all 是"一失败全失败",任何一个 reject 都会进这里
console.error('并行请求失败:', error);
throw error;
}
}
注意 Promise.all 的风险:只要有一个请求失败了,整个就崩了。如果你希望"坏一个不影响其他的",得用 Promise.allSettled:
// 并行但不互相影响,失败的返回 null
const results = await Promise.allSettled([
fetch('/api/a').then(r => r.json()),
fetch('/api/b').then(r => r.json()),
fetch('/api/c').then(r => r.json())
]);
const [a, b, c] = results.map(result =>
result.status === 'fulfilled' ? result.value : null
);
这样就算 b 接口挂了,a 和 c 的数据照样能用,页面不会直接白屏。
真实项目里没人直接用 fetch,都是怎么封装的
说句实话,真实生产环境里,99% 的公司不会直接用原生 Fetch,都会封装一层。为啥?因为你要处理的事情太多了:loading 状态、错误提示、token 刷新、请求取消、重试机制… 这些如果用原生 Fetch 每个请求都写一遍,代码量能膨胀十倍。
下面我给你看一个基础但实用的封装版本,适合中小项目:
// 创建一个请求类,带拦截器功能
class HttpClient {
constructor(baseURL = '') {
this.baseURL = baseURL;
this.interceptors = {
request: [],
response: []
};
}
// 添加请求拦截器
addRequestInterceptor(onFulfilled, onRejected) {
this.interceptors.request.push({ onFulfilled, onRejected });
}
// 添加响应拦截器
addResponseInterceptor(onFulfilled, onRejected) {
this.interceptors.response.push({ onFulfilled, onRejected });
}
async request(url, options = {}) {
// 拼接完整 URL
const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`;
// 默认配置
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
// 合并配置,注意 headers 要特殊合并
const finalOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...options.headers
}
};
// 执行请求拦截器
try {
for (const interceptor of this.interceptors.request) {
if (interceptor.onFulfilled) {
finalOptions.headers = await interceptor.onFulfilled(finalOptions.headers);
}
}
} catch (error) {
if (interceptor.onRejected) {
interceptor.onRejected(error);
}
throw error;
}
// 发起请求
let response;
try {
response = await fetch(fullUrl, finalOptions);
} catch (networkError) {
// 网络错误(断网、DNS 失败等)
throw new Error('网络连接失败,请检查网络设置');
}
// 执行响应拦截器
for (const interceptor of this.interceptors.response) {
try {
response = await interceptor.onFulfilled(response);
} catch (error) {
if (interceptor.onRejected) {
interceptor.onRejected(error);
}
throw error;
}
}
return response;
}
// 便捷的 GET 方法
get(url, params) {
const queryString = params ? `?${new URLSearchParams(params)}` : '';
return this.request(`${url}${queryString}`, { method: 'GET' });
}
// 便捷的 POST 方法
post(url, data) {
return this.request(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
}
// 使用示例:创建一个实例并配置
const http = new HttpClient('https://api.example.com');
// 请求拦截器:自动加 token
http.addRequestInterceptor(async (headers) => {
const token = localStorage.getItem('token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
});
// 响应拦截器:统一处理错误
http.addResponseInterceptor(
async (response) => {
// 还是老问题,fetch 不会自动抛 HTTP 错误
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `请求失败: ${response.status}`);
}
return response.json();
},
(error) => {
// 这里可以统一上报错误到监控系统,比如 Sentry
console.error('接口错误:', error);
throw error;
}
);
// 现在用起来就爽多了
http.get('/user/profile')
.then(data => console.log(data))
.catch(err => console.error(err));
http.post('/user/update', { name: '李四', age: 25 })
.then(() => alert('更新成功'))
.catch(err => alert(err.message));
这个封装虽然代码量看起来不少,但用的时候爽啊。每个请求自动带 token,自动处理 JSON 转换,自动抛错,你业务代码里只需要关注业务逻辑就行。
但我得提醒你,这只是基础版,真实的大厂项目封装会更复杂,可能要处理:
- token 过期自动刷新(无感刷新)
- 请求取消(AbortController)
- 防重复提交(同一个按钮连点五次发五个请求)
- 请求缓存(短时间内重复请求直接读缓存)
- 超时控制(fetch 默认没有超时)
咱们后面一个个说。
跨域问题真的全是后端的锅吗
说到跨域(CORS),前端新人最怕这个。报错信息通常是:
Access to fetch at ‘https://api.other.com/data’ from origin ‘http://localhost:3000’ has been blocked by CORS policy…
这时候你第一反应肯定是:艹,后端又没配 CORS。但真的是这样吗?
CORS 是浏览器的安全策略,全称 Cross-Origin Resource Sharing。简单说就是浏览器问后端:“这个前端域名我能不能访问你?” 后端说"能",才能正常请求;后端说"不能"或者没说话,浏览器就拦截了。
前端能做什么?说实话,生产环境跨域问题确实主要靠后端解决,后端要在响应头里加:
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
但开发环境你可以自己解决,不用求后端。最土的办法是配开发服务器代理。如果你用 Webpack、Vite 或者 Create React App,都可以配 proxy:
Vite 的 vite.config.js:
export default {
server: {
proxy: {
'/api': {
target: 'https://api.backend.com',
changeOrigin: true,
// 重写路径,去掉 /api 前缀
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
这样你代码里写 fetch('/api/data'),实际上会转发到 https://api.backend.com/data,浏览器看到的是同域请求,就不会报 CORS 了。
还有一种邪道方法,仅在开发时可用,就是禁用浏览器安全策略(不推荐,但确实能救命)。Chrome 启动参数加 --disable-web-security --user-data-dir=/temp,但这等于裸奔,仅限本地调试用。
另外前端要注意,简单请求和复杂请求的预检(Preflight) 区别。如果你请求方法是 GET、HEAD、POST 之一,且 Content-Type 是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain,那是简单请求,直接发。
但如果你加了自定义 Header(比如 Authorization),或者 Content-Type 是 application/json,那就是复杂请求,浏览器会先自动发一个 OPTIONS 预检请求,问后端"我能不能这么干"。这时候如果后端没处理 OPTIONS 请求,就会报 CORS 错误。所以看到控制台里一次请求发了两次(先 OPTIONS 后 POST),别慌,是正常的。
网络失败、超时、非200状态码,怎么优雅处理
前面说了,Fetch 不会自动 reject HTTP 错误状态码(404、500等),这需要你自己处理。但还有更麻烦的:网络故障和超时。
Fetch 默认没有超时机制!你的请求可以挂那儿一辈子,直到浏览器自己放弃(通常是几分钟)。这在移动端简直是灾难,用户网不好的时候,页面能转圈转五分钟。
实现超时要用 Promise.race:
// 带超时的 fetch 封装
async function fetchWithTimeout(url, options = {}, timeout = 10000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
} catch (error) {
clearTimeout(id);
if (error.name === 'AbortError') {
throw new Error('请求超时,请检查网络或稍后重试');
}
throw error;
}
}
// 使用
fetchWithTimeout('https://slow.api.com/data', {}, 5000)
.then(response => console.log('在5秒内返回了'))
.catch(err => console.error('超时或失败:', err));
AbortController 是个好东西,它不仅能用来超时,还能用来主动取消请求。比如用户进入一个页面发了个请求,然后马上点了返回或者切换了 tab,这时候你应该取消之前的请求,避免数据错乱或者内存泄漏。
let currentController = null;
function loadData() {
// 取消之前的请求
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
fetch('/api/big-data', {
signal: currentController.signal
})
.then(response => response.json())
.then(data => {
console.log('数据加载完成:', data);
})
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求被取消了(可能是用户切换页面了)');
return;
}
console.error('加载失败:', err);
});
}
// 用户点击返回按钮时
function onUserLeave() {
if (currentController) {
currentController.abort();
}
}
关于错误处理,建议你做分级:
// 错误处理 helper
function handleFetchError(error) {
// 网络错误
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
return { type: 'NETWORK', message: '网络连接失败,请检查网络' };
}
// 超时
if (error.message.includes('timeout')) {
return { type: 'TIMEOUT', message: '请求超时,服务器响应太慢' };
}
// HTTP 错误
if (error.message.includes('HTTP')) {
const statusCode = parseInt(error.message.match(/\d+/)[0]);
if (statusCode === 401) {
return { type: 'UNAUTHORIZED', message: '登录已过期' };
}
if (statusCode === 403) {
return { type: 'FORBIDDEN', message: '没有权限访问' };
}
if (statusCode === 404) {
return { type: 'NOT_FOUND', message: '请求的资源不存在' };
}
if (statusCode >= 500) {
return { type: 'SERVER', message: '服务器内部错误' };
}
return { type: 'HTTP', message: `请求失败 (${statusCode})` };
}
// 其他
return { type: 'UNKNOWN', message: error.message };
}
然后在 UI 层根据错误类型显示不同的提示,网络错误给重试按钮,401 直接跳转登录,5xx 错误提示"服务器开小差了"。
别人封装的 fetch 工具为啥那么稳
看看大厂或者开源库封装的请求库,稳定性确实高。他们通常都做了这几件事:
1. 自动重试机制
网络抖动的时候,自动重试 2-3 次,比直接报错用户体验好得多:
async function fetchWithRetry(url, options, maxRetries = 3, delay = 1000) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) return response;
// 如果是 5xx 错误,值得重试;4xx 错误重试也没用
if (response.status < 500) {
throw new Error(`Client Error: ${response.status}`);
}
lastError = new Error(`Server Error: ${response.status}`);
} catch (error) {
lastError = error;
console.log(`第 ${i + 1} 次请求失败,${delay}ms 后重试...`);
}
// 等待一段时间再重试,指数退避
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)));
}
}
throw lastError;
}
2. 请求去重(防抖)
用户疯狂点击提交按钮,只发最后一次请求,或者取消前面的:
class RequestManager {
constructor() {
this.pendingRequests = new Map();
}
async request(url, options) {
const key = `${url}-${JSON.stringify(options)}`;
// 如果已经有相同请求在进行中,返回那个 Promise
if (this.pendingRequests.has(key)) {
console.log('拦截重复请求:', url);
return this.pendingRequests.get(key);
}
const promise = fetch(url, options)
.finally(() => {
// 完成后从 pending 中移除
this.pendingRequests.delete(key);
});
this.pendingRequests.set(key, promise);
return promise;
}
}
3. 缓存机制
GET 请求结果缓存一段时间,避免重复请求:
class CacheFetch {
constructor() {
this.cache = new Map();
}
async get(url, options = {}, ttl = 60000) {
const key = url;
const cached = this.cache.get(key);
if (cached && Date.now() - cached.time < ttl) {
console.log('命中缓存:', url);
return cached.data;
}
const response = await fetch(url, options);
const data = await response.json();
this.cache.set(key, {
data,
time: Date.now()
});
return data;
}
clear() {
this.cache.clear();
}
}
这些骚操作单独看都不难,但组合在一起就是一个稳定的请求层。如果你不想自己写,直接用 axios 吧,这些功能它都内置了,这就是为什么很多人最终还是选择 axios 而不是原生 Fetch。
开发时疯狂刷新,怎么避免重复请求刷爆接口
开发阶段你肯定会遇到这种情况:写了个 useEffect 发请求,然后疯狂修改代码保存,页面自动刷新,接口被反复调用,后端同学来找你:“你的 IP 被封了,请求太频繁”。
或者用户手贱,盯着提交按钮狂点,硬生生给发了十几个创建订单的请求,结果用户账户里多了十几个相同的订单,客服电话被打爆。
解决方案前面提了一嘴,这里给完整的实现:
// 使用 AbortController 管理请求生命周期
class RequestGuard {
constructor() {
this.controllers = new Map();
}
// 执行请求,如果相同的 key 已经在请求中,先取消之前的
async fetch(key, url, options = {}) {
// 取消之前的同 key 请求
this.cancel(key);
const controller = new AbortController();
this.controllers.set(key, controller);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
this.controllers.delete(key);
return response;
} catch (error) {
this.controllers.delete(key);
throw error;
}
}
cancel(key) {
if (this.controllers.has(key)) {
console.log(`取消之前的请求: ${key}`);
this.controllers.get(key).abort();
this.controllers.delete(key);
}
}
cancelAll() {
for (const [key, controller] of this.controllers) {
controller.abort();
}
this.controllers.clear();
}
}
// 在 React 组件中的使用示例
const requestGuard = new RequestGuard();
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const loadUser = async () => {
try {
const response = await requestGuard.fetch(
`user-${userId}`, // 唯一 key
`/api/users/${userId}`
);
const data = await response.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err);
}
}
};
loadUser();
// 组件卸载时取消请求
return () => {
requestGuard.cancel(`user-${userId}`);
};
}, [userId]);
return <div>{user?.name}</div>;
}
Button 防连点也一个道理:
function SubmitButton() {
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async () => {
if (submitting) {
console.log('已经在提交了,别点了');
return;
}
setSubmitting(true);
try {
await fetch('/api/order', {
method: 'POST',
body: JSON.stringify({ items: cart.items })
});
alert('提交成功');
} catch (error) {
alert('提交失败: ' + error.message);
} finally {
setSubmitting(false);
}
};
return (
<button onClick={handleSubmit} disabled={submitting}>
{submitting ? '提交中...' : '立即提交'}
</button>
);
}
记住:永远要相信用户会做出你想不到的骚操作,他们认为点得越快提交得越快,你得帮他们节制一下。
Mock 数据没到位?用拦截加延迟模拟真实体验
后端接口还没写好,但你前端要并行开发,这时候得 Mock 数据。别直接写死 if (isMock) return fakeData,太 low 了。
我们可以用 Service Worker 拦截请求,或者更简单点,封装一层 Mock 中间件:
// Mock 管理器
class MockManager {
constructor() {
this.mocks = new Map();
this.enable = process.env.NODE_ENV === 'development';
}
// 注册 Mock
register(pattern, handler, delay = 500) {
this.mocks.set(pattern, { handler, delay });
}
async intercept(url, options) {
if (!this.enable) return null; // 生产环境不拦截
for (const [pattern, config] of this.mocks) {
if (url.includes(pattern)) {
console.log(`[Mock] 拦截请求: ${url}`);
// 模拟网络延迟,别太快返回,不真实
await new Promise(resolve => setTimeout(resolve, config.delay));
// 模拟随机失败(10% 概率),测试错误处理
if (Math.random() < 0.1) {
throw new Error('Mock 随机失败');
}
const mockResponse = config.handler(url, options);
return new Response(JSON.stringify(mockResponse), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
}
return null; // 没有匹配的 Mock,继续真实请求
}
}
// 初始化并注册一些 Mock
const mock = new MockManager();
mock.register('/api/user/profile', () => ({
id: 1,
name: 'Mock用户',
avatar: 'https://placekitten.com/100/100',
level: 'VIP8',
balance: 999999
}), 800);
mock.register('/api/products', () => [
{ id: 1, name: 'Mock商品1', price: 99 },
{ id: 2, name: 'Mock商品2', price: 199 }
], 1200);
// 修改封装层,插入 Mock 拦截
async function smartFetch(url, options) {
// 先尝试 Mock
const mockResponse = await mock.intercept(url, options);
if (mockResponse) return mockResponse;
// 真实请求
return fetch(url, options);
}
这样写的好处是:
- 有真实延迟体验,能看到 loading 状态
- 可以测试错误处理逻辑(随机失败)
- 后端接口好了直接关掉 Mock 就行,业务代码不用改
- 能模拟慢网环境(调大 delay)
如果你用现代前端框架(React/Vue/Angular),可以用 MSW(Mock Service Worker)这个库,比这更专业,能拦截网络层,连 Network 面板里都能看到请求,只是返回的是 Mock 数据。
调试三板斧,Network 面板看不明白时的绝招
遇到接口问题,新手就只会 console.log,其实 Chrome DevTools 里有很多神器。
第一招:右键请求直接 Copy as Fetch
在 Network 面板里,右键一个请求,选 Copy -> Copy as fetch(cURL),它会自动生成这段请求的 Fetch 代码,包括所有 Headers 和 Body。你要重发这个请求测试,直接粘贴到 Console 里就能跑,不用重新操作 UI。
第二招:用 debugger 断点看执行流程
别只会 console.log,在关键地方加 debugger;,代码执行到这里会自动断住,你能看到完整的调用栈、变量值。配合 async await 的时候特别有用,能看到 Promise 什么时候 resolve。
async function debugFetch() {
console.log('开始请求');
debugger; // 断点1
const res = await fetch('/api/test');
debugger; // 断点2,能看到 res 对象
const data = await res.json();
debugger; // 断点3,能看到解析后的数据
console.log(data);
}
第三招:查看 Timing 分析慢请求
Network 面板里选中一个请求,看 Timing 标签,能看到:
- Queueing:排队时间(浏览器对同域名并发限制通常是6个)
- DNS Lookup:DNS 查询时间
- SSL:HTTPS 握手时间
- Waiting (TTFB):首字节时间,这个大了说明后端处理慢
- Content Download:下载时间,这个大了说明返回数据太大
如果你看到 TTFB 要 2 秒,Content Download 只要 10ms,那肯定是后端 SQL 写烂了,去找后端撕逼的时候有证据了。
还有个隐藏技巧:在 Console 里用 $r 查看最近选中的元素,配合 Network 面板选中某个请求,然后在 Console 输入 $r,居然能看到那个请求对象的各种细节,虽然我也不知道这有什么用,但看起来很酷。
“Failed to fetch” 到底啥情况,90% 是这几个原因
看到控制台红彤彤的 “Failed to fetch”,别慌,按这个 checklist 排查:
1. URL 写错了(最常见)
检查一下你是不是写成了 fetch('/api/data') 但当前页面是 file:// 协议打开的(直接双击 HTML 文件)。Fetch 不能跨协议请求,必须用 HTTP 协议打开页面。建议上 VS Code 的 Live Server 插件。
2. CORS 问题
虽然浏览器 CORS 报错通常会详细说明被 CORS policy 拦截,但有时候就给你个 failed to fetch。看看 Network 面板里有没有预检请求(OPTIONS)返回 403,或者响应头里确实没有 Access-Control-Allow-Origin。
3. SSL 证书问题
开发环境用自签名 HTTPS 证书时,浏览器会拦截"不安全"的请求。你先手动在浏览器里打开那个 API 地址,点击"继续前往(不安全)",然后再回页面请求,可能就通了。
4. AdBlock 拦截
是的,某些广告拦截插件会误判你的 API 路径,比如包含 ad、track、analytics 这些词。如果你发现请求莫名其妙失败,试试无痕模式(通常没插件),如果无痕模式能跑,那就是插件问题。
5. 请求头过大
如果你塞了太多自定义 Header,或者 Cookie 特别大,可能超出浏览器限制,直接拒绝发送。
6. 网络真的断了
开个新标签页试试能不能打开百度,别笑,真的有人对着断网 debug 两小时。
排查顺序:先开 Network 面板看请求发出去了吗(Status 那一列是 (failed) 还是具体状态码),如果根本没发出去,大概率是 CORS 或者 URL 问题;如果发出去了但红字,看后端的响应。
兼容性真那么差吗,什么时候必须上 axios
网上有人说 Fetch 兼容性不行,不支持 IE。但大哥,IE 都死三年了,Win11 都不自带 IE 了,你还考虑它干嘛?现在 Fetch 的支持率是 97%+,主流现代浏览器(Chrome 42+、Firefox 39+、Safari 10.1+、Edge 14+)都支持。
但 Fetch 确实有些特性旧版浏览器不支持,比如:
- AbortController(Chrome 66+, Firefox 57+)
- Request/Response 对象的某些方法
- 流式处理(Streams)
如果你要支持特别老的设备(比如政府项目要求 IE11),可以用 whatwg-fetch polyfill,就几 KB,补上基本功能。
但就算在现代浏览器里,Fetch 和 axios 各有优劣:
Fetch 的优势:
- 原生支持,不用打包,体积为 0
- 基于 Promise,支持 async await
- 支持 Streams,可以处理大文件下载进度
Fetch 的劣势:
- 功能简陋,得自己封装很多东西
- 默认不带超时
- 默认不 reject HTTP 错误状态码
- 不支持请求/响应拦截器(得自己实现)
axios 的优势:
- 功能齐全,拦截器、自动 JSON 转换、超时控制、取消请求、自动防御 XSRF 都内置了
- 支持浏览器和 Node.js 环境同构(SSR 项目必备)
- 错误处理更友好
- 请求进度监控(onUploadProgress/onDownloadProgress)
所以我的建议是:小项目、学习目的、对体积敏感(比如 Chrome 插件)用 Fetch;中大型项目、团队项目、需要复杂请求管理的上 axios,别重复造轮子。
那个大坑:fetch 默认不 reject HTTP 错误状态码
我再强调一遍这个坑,因为踩过的人都说内存深刻。看代码:
// 你以为这样写能捕获 404?
try {
const res = await fetch('/api/not-found');
console.log('请求成功', res);
} catch (err) {
console.error('请求失败', err);
}
如果服务器返回 404,这段代码会输出"请求成功",因为 fetch 的 Promise 成功 resolve 了,只是 response.ok 是 false。你得手动检查:
const res = await fetch('/api/not-found');
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
对比 axios:
try {
const res = await axios.get('/api/not-found');
} catch (err) {
// axios 会自动把 4xx 5xx 扔进这里,省心多了
console.error(err.response.status);
}
这就是为什么很多人用着用着就换回 axios 了,fetch 太原始了,像个毛坯房,啥都要自己装修。
但如果你喜欢折腾,可以这样封装一个严格的 fetch:
async function strictFetch(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = response;
// 尝试解析错误信息
try {
error.data = await response.json();
} catch {
error.data = await response.text();
}
throw error;
}
return response;
}
用 strictFetch 代替 fetch,行为就和 axios 类似了。
顺手写个带 loading、重试、缓存的小型请求管理器
来,咱们综合运用前面学的,写一个 20 行左右(好吧可能稍微多几行)的实用请求管理器,放在 utils/request.js 里直接能用:
// 小而美的请求管理器
class SmartFetch {
constructor(options = {}) {
this.baseURL = options.baseURL || '';
this.timeout = options.timeout || 10000;
this.retries = options.retries || 0;
this.cache = new Map();
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const cacheKey = `${url}-${JSON.stringify(options)}`;
// 检查缓存
if (options.cacheTTL) {
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.time < options.cacheTTL) {
return cached.data;
}
}
// 重试逻辑
let lastError;
for (let attempt = 0; attempt <= this.retries; attempt++) {
try {
const result = await this._fetchWithTimeout(url, options);
// 存入缓存
if (options.cacheTTL) {
this.cache.set(cacheKey, { data: result, time: Date.now() });
}
return result;
} catch (err) {
lastError = err;
if (attempt < this.retries) {
await this._delay(1000 * Math.pow(2, attempt)); // 指数退避
}
}
}
throw lastError;
}
_fetchWithTimeout(url, options) {
return new Promise((resolve, reject) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
reject(new Error('Request timeout'));
}, this.timeout);
fetch(url, { ...options, signal: controller.signal })
.then(async res => {
clearTimeout(timeoutId);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(resolve)
.catch(reject);
});
}
_delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
clearCache() {
this.cache.clear();
}
}
// 导出单例
export const http = new SmartFetch({
baseURL: '/api',
timeout: 8000,
retries: 2 // 失败自动重试 2 次
});
// 使用示例:
// http.request('/user', { cacheTTL: 60000 }) // 缓存 1 分钟
// http.request('/order', { method: 'POST', body: JSON.stringify(data) })
没有依赖,原生 JS 就能跑,支持超时、重试、缓存,代码量也不大,适合中小型项目。
别人问你 fetch 和 axios 有啥区别,这样回答显得专业
面试官问:“Fetch 和 axios 有什么区别?”
菜鸟回答:“Fetch 是原生的,axios 是第三方库,axios 更好用。”
高手回答:
"从API设计层面看,Fetch 是底层 API,基于 Promise 和 Streams,提供了更底层的控制能力,比如我们可以直接操作 ReadableStream 来实现下载进度监控;而 axios 是高层封装,提供了拦截器、自动转换、XSRF 防护等企业级特性。
从错误处理机制看,Fetch 遵循浏览器底层语义,只有网络失败才会 reject,HTTP 错误状态(4xx/5xx)需要手动检查 response.ok;而 axios 基于拦截器实现了更友好的错误处理,会自动将非 2xx 状态转为 rejected Promise。
从运行时环境看,Fetch 是浏览器原生 API,在 Node.js 早期版本需要引入 node-fetch 等 polyfill,而 axios 同时支持浏览器和 Node.js,在 SSR 场景下能实现同构。
从体积和生态看,Fetch 零依赖,适合对包体积敏感的场景;axios 生态更完善,社区中间件丰富,适合中大型项目。我们团队在项目初期使用过原生 Fetch 配合自建拦截器,后来在微前端架构下统一迁移到了 axios,主要是为了利用其请求取消和批量处理的能力。"
你看,这样一说,面试官就知道你不仅会用,还深入理解底层原理,offer 稳了。
实战彩蛋:用 fetch 写个天气查询小工具
光说不练假把式,咱们来完整撸一个天气查询应用,调 OpenWeatherMap 或者和风天气的 API。这里用 JSONPlaceholder 模拟天气数据,你换成真实 API key 就能跑:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Fetch 天气查询</title>
<style>
body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
input { width: 70%; padding: 10px; font-size: 16px; }
button { width: 28%; padding: 10px; background: #007bff; color: white; border: none; cursor: pointer; }
button:disabled { background: #ccc; }
.weather-card { margin-top: 20px; padding: 20px; background: #f0f8ff; border-radius: 8px; display: none; }
.loading { text-align: center; color: #666; margin-top: 20px; display: none; }
.error { color: #dc3545; margin-top: 10px; }
</style>
</head>
<body>
<h2>🌤️ Fetch 天气查询 Demo</h2>
<div>
<input type="text" id="cityInput" placeholder="输入城市名(如:北京)" value="Beijing">
<button onclick="searchWeather()">查询天气</button>
</div>
<div class="loading" id="loading">加载中...</div>
<div class="error" id="error"></div>
<div class="weather-card" id="weatherCard">
<h3 id="cityName"></h3>
<p>温度: <span id="temp"></span>°C</p>
<p>天气: <span id="condition"></span></p>
<p>湿度: <span id="humidity"></span>%</p>
</div>
<script>
// 模拟天气 API(实际使用时换成真实 API)
async function fetchWeather(city) {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 800));
// 模拟数据,真实项目中应该是:
// const API_KEY = 'your_api_key';
// const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}&units=metric`;
// return fetch(url);
const mockData = {
Beijing: { temp: 22, condition: '晴天', humidity: 45 },
Shanghai: { temp: 25, condition: '多云', humidity: 60 },
Guangzhou: { temp: 30, condition: '雷阵雨', humidity: 80 }
};
const data = mockData[city] || { temp: 20, condition: '未知', humidity: 50 };
return { ok: true, json: async () => data };
}
// 带 loading 和错误处理的请求函数
async function getWeather(city) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(`https://api.weather.example.com/v1/current?city=${encodeURIComponent(city)}`, {
signal: controller.signal,
headers: { 'Accept': 'application/json' }
});
clearTimeout(timeoutId);
if (!response.ok) {
if (response.status === 404) throw new Error('城市未找到');
throw new Error(`服务错误: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') throw new Error('请求超时');
throw error;
}
}
async function searchWeather() {
const city = document.getElementById('cityInput').value.trim();
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const card = document.getElementById('weatherCard');
const btn = document.querySelector('button');
if (!city) {
error.textContent = '请输入城市名';
return;
}
// UI 状态更新
loading.style.display = 'block';
error.textContent = '';
card.style.display = 'none';
btn.disabled = true;
try {
// 这里用模拟数据,真实环境用 getWeather(city)
const mockResponse = await fetchWeather(city);
const data = await mockResponse.json();
// 更新 UI
document.getElementById('cityName').textContent = city;
document.getElementById('temp').textContent = data.temp;
document.getElementById('condition').textContent = data.condition;
document.getElementById('humidity').textContent = data.humidity;
card.style.display = 'block';
} catch (err) {
error.textContent = '查询失败: ' + err.message;
} finally {
loading.style.display = 'none';
btn.disabled = false;
}
}
// 回车键触发查询
document.getElementById('cityInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') searchWeather();
});
</script>
</body>
</html>
把这个保存成 HTML 文件双击打开就能跑。代码里包含了:
- 输入处理和 URL 编码(encodeURIComponent)
- Loading 状态管理(防止重复提交)
- 超时控制(AbortController)
- 错误处理(try-catch-finally)
- 响应数据验证
换成真实天气 API(比如和风天气、OpenWeatherMap 或者国家气象局的接口)改个 URL 就能上线,10 分钟搞定真的有。
写到这儿手指头都酸了,Fetch 这玩意儿说简单也简单,说坑也是真的坑。从当年 jQuery 的 $.ajax 到如今的 fetch,前端请求库走过了漫长的路。虽然现在很多人直接用 axios,但理解 Fetch 的底层原理对你调试网络问题、写 Chrome 插件、或者做极致体积优化的场景都很有帮助。
总之,下次同事再说"Fetch 太难用了",你就把这篇丢给他——然后记得让他请吃午饭,毕竟这么多坑都提前帮你踩平了,这交情得值顿饭吧?

更多推荐



所有评论(0)