尧图精选

前端必修:fetch 请求底层原理与实战排查手册

🕒 发布时间:2026/10/1 5:45:33 📁 来源:尧图网络
前后端之间最基础、也最常见的交互方式就是用 fetch 发请求。现在随便打开一个现代前端工程登录、列表、提交表单、上传文件底层几乎都是 fetch 在干活。这个 API 从浏览器标准确立到现在已经成了跟前端必须掌握的基本功不管是写 Vue、React 还是原生 JavaScript只要你的页面需要跟服务端要数据就绕不开它。这篇文章我打算从最底层开始拆解 fetch 是怎么把一次请求发出去的然后带你走一遍完整的请求生命周期把 Request、Response、Headers 这几个核心对象讲透再结合我这些年踩过的坑把那些令人抓狂的 Failed to fetch、403、could not fetch url 一类报错的排查思路整理成一份可以直接用的速查清单。无论你是刚入行的前端新人还是写了几年业务代码想系统补一遍网络请求基本功的开发者这篇文章都能给你一些超出文档之外的东西。1. 先搞明白fetch 到底是个什么东西1.1 fetch 的诞生背景与定位在 fetch 出现之前网页里发 HTTP 请求主要靠 XMLHttpRequestXHR。XHR 是上个时代的产物API 设计相当别扭要写一堆事件回调还要处理 readyState 那套状态机。虽然 Ajax 这个名词被叫了很多年但真正用过原生 XHR 的人都知道那玩意儿写起来跟现代 JavaScript 完全不是一个画风。fetch 是后来浏览器提供的原生请求 API它的核心设计目标很明确用 Promise 替代回调用更扁平、更符合直觉的方式表达请求。你写fetch(url)就是在告诉浏览器帮我去这个地址拿资源。浏览器底层会用 HTTP 协议把请求发出去拿到响应后给你返回一个 Promise。这个过程是异步的不会阻塞页面。从定位上看fetch 不是某个框架的封装而是浏览器原生能力跟document、window这些内置对象一样页面里直接就能用。Node.js 从 18 版本开始也在运行时里内置了全局 fetch这就意味着前后端可以共用同一套请求语义非常方便。1.2 fetch 和 XHR 的本质区别很多人只知道fetch 比 XHR 好用但要真正理解 fetch 的设计得先看清它和 XHR 到底差在哪。XHR 是一个基于事件驱动的 API你创建实例、配置参数、绑定事件、调 send然后等着 readyState 一路变到 4。这中间的状态变化完全靠事件通知代码一复杂就是典型的回调地狱。fetch 则是基于 Promise 的声明式 API。你告诉它我要什么它给你一个 Promise 对象成功就 resolve失败就 reject。配合async/await你可以用近乎同步的写法来处理网络请求。代码层面最能体现差异的是两者对响应状态的处理方式。XHR 里你得写xhr.status 200判断成功而 fetch 返回的 Response 对象直接给你一个ok布尔值response.ok为 true 就代表 HTTP 状态码在 200 到 299 之间。还有一个容易被忽视的点XHR 支持上传进度的监听事件xhr.upload.onprogress而 fetch 的 Request body 本身没有内置的上传进度事件。想在 fetch 里做上传进度条你只能自己想办法或者老老实实退回 XHR。这个坑我在后面会专门讲到。1.3 fetch 适合谁来学、能解决什么问题如果你是在浏览器里写前端fetch 是必须掌握的基础设施级别的 API。它解决的核心问题就是以标准化的方式发送 HTTP 请求并处理响应涵盖了普通文本、JSON、表单、文件流、二进制数据等多种数据格式同时支持请求头定制、缓存控制、重定向、跨域请求等一整套 Web 开发刚需。如果你做 Node.js 后端全局 fetch 也能帮你快速请求第三方服务。以前大家习惯装 axios 或者用 http 模块手动拼请求现在直接fetch一把梭少一个依赖就少一分维护成本。一句话总结定位fetch 是 Web 平台提供给你的标准 HTTP 客户端学会它等于你掌握了跟任何服务端打交道的通用语言。2. 从一次请求的完整生命周期看 fetch 的内部流程2.1 一次 fetch 请求的基本链路拆解我把一次完整的 fetch 请求拆成五个阶段来讲这样你对fetch 到底做了什么会有一个清晰的全局认识。第一阶段是构造请求。你调用fetch(url, options)时浏览器不会立刻发请求而是先根据你传入的 url 和 options 构造一个 Request 对象。url 会被解析成协议、主机、路径、查询参数等部分options 里的 method、headers、body 等参数都会被整合进这个请求配置里。第二阶段是发起请求。请求构造完成后浏览器开始走网络栈包括 DNS 解析域名、建立 TCP 连接、如果是 HTTPS 还要完成 TLS 握手。这个过程用户是感知不到的但这恰恰是很多网络问题的高发区。比如域名解析失败、连接超时、证书不受信任全都发生在这个阶段。第三阶段是接收响应。服务端处理完你的请求后会返回一个 HTTP 响应。此时浏览器已经拿到了原始响应数据并把它包装成一个 Response 对象。重点来了这个阶段只是头部到位了响应体 body 可能还在路上是流式的。第四阶段是读取响应体。在 fetch 中响应体是一个 ReadableStream你不能直接拿到完整的字符串而是需要调用response.text()、response.json()等方法去异步读取它、解析它。这一步同样返回 Promise所以你需要第二个 await。第五阶段是结果处理。拿到解析后的数据后你在业务代码中处理数据比如渲染到页面、写入状态管理库。到这里一次请求才算真正结束。上面这五步听起来有点绕但本质上每次请求都在走这条路。很多新手写 fetch 只await一次把response当最终数据结果发现返回的不是自己想要的东西就是没理解这个两段式的结构。2.2 两段式 await 到底是怎么回事两段式是 fetch 最核心、也最容易踩坑的设计。我给你写一个最常见的错误示例const response await fetch(https://api.example.com/data) const data response // 错response 不是数据response是一个 Response 对象它代表的是HTTP 响应本身而不是响应里的内容。你可以把 Response 想象成一个快递包裹包裹外层是状态信息运输单号、货运状态里面才是你真正订购的商品。你得先把包裹拆开才能拿到商品。正确的写法是这样的const response await fetch(https://api.example.com/data) const data await response.json()第一行await等待的是服务器返回了响应头部与响应体流第二行await才真正把 body 里的二进制流转换成 JSON 对象。如果你的接口返回是纯文本就调response.text()如果服务端返回的是二进制文件就调response.blob()或者response.arrayBuffer()。不同方法对应不同解析器这就是 fetch 给你的自由。为什么这么设计因为 HTTP 本身是分 header 和 body 两部分传输的fetch 把这一步拆开是为了让你有能力在响应体还没完全下载完时就先查看状态码和头部信息。比如你想根据Content-Type决定怎么解析 body先看头部再决定解析方式正好用得上这个特性。2.3 Request、Response、Headers 三者的关系fetch 底层依赖的其实是三个 Web API 类Request、Response 和 Headers。很多教程直接忽略这三个类但搞懂它们你对 fetch 的理解会直接上一整个台阶。Request 描述我想要什么。它包含 method、url、headers、body、credentials、mode、cache 等字段。当你调用fetch(url, options)时如果 options 里没有显式传入 Request 实例浏览器会根据参数自行构造一个。你也可以手动创建 Request 然后传给 fetchconst request new Request(https://api.example.com/login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username: demo, password: 123456 }) }) const response await fetch(request)Request 对象的好处是你可以对同一个请求进行复用或二次加工。比如先统一设置公共头再在不同场景追加参数。请求是不可变的想改就基于它再创建一个新的。Response 描述服务器给了什么。它包含 status、statusText、ok、headers、url、type、redirected 等字段以及 body 流。前文已经说过读取 body 需要用 json()、text()、blob()、formData()、arrayBuffer() 这几个方法。Headers 描述请求和响应的附加信息。它可以像 Map 一样操作const headersObj new Headers() headersObj.append(Content-Type, application/json) headersObj.set(Authorization, Bearer xxxxx) headersObj.get(Content-Type) // application/json headersObj.has(Authorization) // true headersObj.forEach((value, key) console.log(key, value))这里要特别注意浏览器对 Headers 不是完全放开的。跨域请求时你能读取的响应头是受限的受 CORS 白名单约束Set-Cookie之类的头也不会暴露给前端代码。这是浏览器的安全策略不是 bug。3. 核心细节解析那些文档里一笔带过但你必须知道的事3.1 请求方法、请求头和请求体怎么设置fetch 默认发的是 GET 请求不带 body。你要发其他类型的请求必须在 options 里指定 method。const response await fetch(https://api.example.com/users, { method: POST, headers: { Content-Type: application/json, Accept: application/json }, body: JSON.stringify({ name: 张三, age: 28 }) })method 字段支持 GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS 等任意 HTTP 方法。绝大多数业务场景用 GET 和 POST 就够了但做 RESTful 接口的同学会经常用到 PUT 和 DELETE。headers 字段是一个对象也可以直接传 Headers 实例。这里有几个实战要点第一Content-Type必须和你的 body 格式匹配。发 JSON 就用application/json发表单就设application/x-www-form-urlencoded发文件上传就设multipart/form-data。如果没设置浏览器会根据 body 类型做一些默认处理但很多时候不是你想要的。第二Accept头告诉服务端你想要什么格式的响应。服务端一些接口会依据这个头做内容协商返回 XML、JSON 或者别的东西。第三浏览器对跨域请求的自定义头有限制。你如果在前端加了Authorization之类的自定义头服务端必须允许对应的 CORS 头否则浏览器会拦下来报 CORS 错误。body 的类型同样有讲究。你要发 JSON得先JSON.stringify变成字符串要发表单可以用 URLSearchParams 或者 FormData要传二进制文件直接传 File 对象或 Blob。3.2 response 的常用字段与解析方法的取舍服务端响应回来后你需要读 Response 对象。最常用的字段和方法整理如下response.ok布尔值状态码 200-299 时为 true其他为 falseresponse.status数字比如 200、301、404、500response.statusText状态码对应的文本描述比如 OK、Not Foundresponse.headers响应头response.url最终请求的 URL发生重定向后会是最后的地址response.redirected是否发生过重定向response.type响应类型有 basic、cors、error、opaque 等取值response.json()解析 body 为 JSON 对象response.text()解析 body 为字符串response.blob()解析 body 为 Blob 对象适合下载文件、图片response.formData()解析 body 为 FormDataresponse.arrayBuffer()解析 body 为二进制数组选择哪个解析方法取决于你拿到的响应内容是什么格式。但要注意每个 Response 对象的 body 只能被读取一次。你先调了response.json()再想调response.text()拿原始字符串就会报错。因为 body 流已经消耗完了这就像矿泉水喝了一口瓶子里的水不会自己补回来。3.3 跨域请求CORS和 credential 设置跨域是前端发请求绕不开的一个话题。简单说浏览器出于安全考虑限制页面里发起的请求只能访问同源协议、域名、端口都相同的资源。如果你的页面在a.com上却要请求b.com的接口就属于跨域。fetch 默认的跨域策略是mode: cors也就是会让浏览器走 CORS 流程。这个流程本身分两块如果是简单请求GET、POST 且 Content-Type 是简单的几种直接发出去然后浏览器检查响应头看有没有Access-Control-Allow-Origin如果是预检请求比如带自定义头、使用 PUT/DELETE浏览器会先发一个 OPTIONS 请求探路服务端确认允许后才会发真正的请求。这一块经常出问题。你在浏览器里调试发现请求发出去了但控制台报 CORS 错误或者网络面板里看到一个红色的 OPTIONS 请求都是这个机制在起作用。解决方案是让服务端正确返回 CORS 响应头而不是在前端想办法绕过去。再说 credentials。如果你请求的接口需要携带 Cookie你必须在 options 里设置credentials否则浏览器默认不会带上 Cookieconst response await fetch(https://api.example.com/userinfo, { credentials: include // 同源请求也可用 same-origin })include表示无论同源还是跨域都携带凭证same-origin只在同源时携带omit则从不携带。这个设置容易被忽略但往往是接口返回 401 或者会话丢失的罪魁祸首。3.4 缓存控制cache 参数与但不是所有浏览器都认fetch 还有一个容易被忽略的参数cache。它控制浏览器对本次请求使用什么缓存策略。const response await fetch(https://api.example.com/data, { cache: no-store // 完全不走缓存 })取值有default、no-store、reload、no-cache、force-cache等。它们的区别在于怎么读取和写入 HTTP 缓存default标准缓存机制遵循请求头里的 Cache-Control、Expires 等指令no-store不读缓存也不写缓存每次都向服务器要最新数据reload不走缓存但会把响应写进缓存no-cache会回源校验但如果有有效的缓存副本可能直接使用缓存force-cache只要本地有缓存就用不管它是否过期我在实际项目中对那种实时性要求高的接口比如用户钱包余额、库存数量一般会设置cache: no-store避免用户看到旧数据。4. 实操过程从 GET 到文件上传一个接一个过4.1 最基础的 GET 请求以及带 query 参数怎么拼先从最基础的开始。拿一个公开接口举例请求https://api.example.com/posts获取文章列表async function getPosts() { try { const response await fetch(https://api.example.com/posts) if (!response.ok) { throw new Error(HTTP error! status: ${response.status}) } const posts await response.json() console.log(posts) } catch (error) { console.error(请求失败:, error) } }这个例子虽然简单但已经包含了 fetch 的完整骨架发请求、判断状态码、解析数据、捕获异常。我见过太多项目里直接response.json()完全不判断response.ok结果后端返回 404 页面时前端还把 HTML 当成 JSON 去解析报一个莫名其妙的错误。所以我的习惯是任何请求都必须先检查response.ok。如果 GET 请求要带参数直接拼 URL 即可。注意要使用encodeURIComponent处理中文和特殊字符const keyword 前端开发 const url https://api.example.com/search?keyword${encodeURIComponent(keyword)}page1pageSize20你当然也可以用 URLSearchParams 来构建看起来更优雅还能避免忘记编码的问题const params new URLSearchParams({ keyword: 前端开发, page: 1, pageSize: 20 }) const url https://api.example.com/search?${params.toString()}4.2 POST 携带 JSON是前后端联调最常用的姿势POST 加 JSON 是实际开发中最常见的请求形式登录、提交订单、创建资源全是这个模式。给你一个结构完整的示例async function createUser(userData) { const response await fetch(https://api.example.com/users, { method: POST, headers: { Content-Type: application/json, Accept: application/json }, body: JSON.stringify(userData) }) if (!response.ok) { // 这里可以尝试解析服务端返回的错误详情 const errBody await response.text().catch(() ) throw new Error(创建失败 (${response.status}): ${errBody}) } return response.json() }我这里多写了一行response.text().catch(() )是为了应对一种常见情况服务端返回 400 时有些接口会带一个 JSON 错误描述比如{ message: 用户名已存在 }有些则什么也不返回。先尝试读 body再抛错误联调时你就能从 error message 里直接看到服务端的反馈不用跑去翻网络面板。4.3 表单提交URLSearchParams 和 FormData 的区别表单耍提交会有两种数据格式很多新手混在一起我来分清楚。第一种是普通键值对表单对应application/x-www-form-urlencoded。这类数据长这样name张三age28。用 URLSearchParams 即可const formData new URLSearchParams() formData.append(name, 张三) formData.append(age, 28) const response await fetch(https://api.example.com/register, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded }, body: formData.toString() })第二种是文件上传表单对应multipart/form-data。这种场景用 FormDataconst formData new FormData() formData.append(avatar, fileInput.files[0]) formData.append(userId, 10086) const response await fetch(https://api.example.com/upload, { method: POST, body: formData // 注意不要手动设置 Content-Type })这里有个重要的易错点上传文件时千万不要手动设置Content-Type为multipart/form-data。因为multipart/form-data需要带一个随机生成的 boundary 分隔符你手动设置的话浏览器不会帮你自动补充 boundary服务端就无法正确解析文件。正确做法是把 boundary 的生成交给浏览器你只传 FormData 即可。4.4 流式读取与下载文件fetch 不仅可以上传还能下载文件。比如下载一张图片const response await fetch(https://api.example.com/image.png) const blob await response.blob() const downloadUrl URL.createObjectURL(blob) // 创建临时 a 标签触发下载 const a document.createElement(a) a.href downloadUrl a.download example.png a.click() // 释放临时 URL URL.revokeObjectURL(downloadUrl)这其实是利用了 Blob 和 createObjectURL让浏览器把网络数据当成一个可下载的本地文件。注意最后一定要revokeObjectURL释放资源否则长时间运行会内存泄漏。如果你想做流式读取比如让 AI 聊天接口的返回内容一个字一个字地蹦出来fetch 也支持。核心是使用response.body.getReader()const response await fetch(https://api.example.com/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ prompt: 你好 }) }) const reader response.body.getReader() const decoder new TextDecoder() while (true) { const { done, value } await reader.read() if (done) break const chunk decoder.decode(value, { stream: true }) console.log(收到流式数据:, chunk) }这种流式读取技术现在很流行SSEServer-Sent Events和部分大模型接口都需要用这种思路来处理。fetch 的 body 流设计给了它很强的灵活性。4.5 超时控制和请求取消fetch 默认是没有超时时间的。如果一个请求卡住了它可能会一直挂在那里直到网络栈自己报错。这在用户体验上非常糟糕所以实际项目里一定要做超时控制。实现方式使用的是 AbortControllerfunction fetchWithTimeout(url, options {}, timeout 8000) { const controller new AbortController() const timer setTimeout(() controller.abort(), timeout) return fetch(url, { ...options, signal: controller.signal }).finally(() clearTimeout(timer)) }这里controller.abort()会取消当前请求然后 fetch 返回的 Promise 会 reject 一个AbortError。你在接收端可以这样处理try { const response await fetchWithTimeout(https://api.example.com/slow, {}, 5000) const data await response.json() } catch (error) { if (error.name AbortError) { console.error(请求超时了) } else { console.error(其他错误, error) } }AbortController 还有一个用处用户主动取消请求。比如搜索页面用户输入关键字后发起请求但紧接着又输入了新内容你可以把上一次请求 abort 掉避免旧响应用于覆盖新结果。5. 环境问题排查那些 failed to fetch / could not fetch 到底在说什么5.1 常见 fetch 相关报错的真实含义很多同学的崩溃时刻是终端或浏览器里突然冒出一串英文报错。我把最常碰到的几种情况翻译成人话并给出排查路径。第一类是浏览器控制台直接报Failed to fetch。这个报错几乎是万金油它代表请求根本没有完成但没有告诉你具体原因。背后可能是网络断了、DNS 解析失败、跨域被拦、证书错误、请求被 abort或者 URL 本身有问题。要定位得按下面的思路一层层查。第二类是前端发请求后服务端返回 403。403 本质是服务端理解你的请求但拒绝执行。原因通常是权限不足、认证 token 缺失或失效、IP 地址被列入黑名单、请求头缺少必要字段。调试时重点看服务端日志和你的鉴权方式。第三类是开发命令行工具时看到的could not fetch url ...或者cannot fetch index base url ...。这类错误往往不是你自己写的 fetch 代码而是某个包管理器比如 Python 的 pip、conda在请求软件源时失败了。它背后的逻辑跟我前面讲的 fetch 是一模一样的工具在向远程源发请求结果网络出问题了。5.2 用生活化思路排查 cannot fetch index base url 类型的源错误假设你在终端里运行 pip 装包报错里带could not fetch url https://pypi.org/simple/pip/或者中文提示找不到满足要求的版本。很多人第一反应是包不存在但其实这个报错的完整意思是无法从源地址获取包列表所以找不到可用版本。要理解它做个类比你去超市买东西发现货架上空空的于是报告没找到商品。但真实原因可能是超市今天没开门源不可达门牌号写错了源地址错误你拿的购物卡过期了凭证失效或者你今天根本没走到超市网络不通。所以排查方向不是商品列表而是能不能访问那个源地址。具体的排查步骤我建议这样走先看报错里提到的源地址是什么。如果是http://开头优先怀疑是不是协议问题。现在很多源都强制 HTTPS用 http 访问会被拒绝或被劫持。在浏览器里手动访问这个源地址看能不能打开。如果浏览器能打开但命令行工具报错那就是命令行的网络配置或证书环境有问题。检查本机时间和 CA 证书。证书过期或系统时间严重偏差会导致 HTTPS 证书校验失败。换一个可达的镜像源试试。很多公开服务会因为网络环境原因导致某些源不可达切换到一个官方推荐的备用源能解决大量这类问题。检查本地缓存。这类工具通常会把获取到的包索引缓存在本地如果缓存里的数据损坏或过旧也会报同样的错误。清掉缓存重新拉取往往能解决问题。整个思路的底层逻辑就是顺着请求发出-网络传输-源服务器-响应解析这条链路一段一段查哪一段不通就修哪一段。5.3 排查思路速查表为了让你遇到问题时能快速定位我整理了一张比较通用的排查表你可以直接截图收藏。报错或现象可能原因优先排查方向浏览器控制台Failed to fetch网络断开、CORS、证书、abort先看 Network 面板里请求是否发出、红色报错的具体文本请求发出后返回 403权限不足、token 失效、IP 被限制查看服务端日志、检查请求头中的认证信息返回 404URL 拼错、接口不存在核对接口路径和 query 参数网络面板里请求是红色且提示 CORS服务端没配置 CORS 白名单请求跨域响应头Access-Control-Allow-Origin返回 301/302 但请求失败重定向配置或凭证丢失检查 redirect 配置和 cookie 携带情况pip/conda 报 could not fetch url源不可达、协议错误、缓存损坏手动访问源地址、换备用源、清缓存5.4 一个印象深刻的 403 排查实录说个我真实遇到过的案例。有一个项目前端登录后调用户信息接口总是随机出现 403。一开始我以为是登录态过期把账号登出再登入问题照旧。后来我仔细看请求头发现接口里带了一个自定义头X-Request-Scope而服务端对这个头做了严格的校验过期时间精确到秒。问题就在这个头上前端在页面初始化时生成了这个头但服务端要求每次请求都重新签名时间一长前端还在用旧值。解决办法是每次发请求前动态生成签名而不是在模块加载时算一次。这件事给我的教训是遇到 403不要只盯着 token要核对所有请求头的每一处字段是否满足服务端的最新要求。最好能抓到一条成功请求和一条失败请求做对比差异点就是问题所在。6. 封装一个更顺手的 fetch并附上几个实用技巧6.1 带重试和超时的 fetch 封装原生 fetch 已经很强大但直接裸用在项目里还是缺几个常用能力统一错误类型、超时控制、自动重试。我一般在项目里都会封装一个request函数。下面这个封装是我常用的一个版本逻辑并不复杂但非常实用async function request(url, options {}, { timeout 8000, retries 2 } {}) { const controller new AbortController() const timer setTimeout(() controller.abort(), timeout) const defaultOptions { method: GET, headers: { Content-Type: application/json }, credentials: same-origin, ...options, signal: controller.signal } let lastError for (let attempt 0; attempt retries; attempt) { try { const response await fetch(url, defaultOptions) if (!response.ok) { // 5xx 属于服务端问题值得重试4xx 通常是参数问题重试无意义 if (response.status 500 attempt retries) { lastError new Error(服务端错误 ${response.status}) continue } throw new Error(HTTP ${response.status}) } const contentType response.headers.get(Content-Type) || if (contentType.includes(application/json)) { return await response.json() } return await response.text() } catch (error) { if (error.name AbortError) { throw new Error(请求超时${timeout}ms) } lastError error // 对网络错误做一次重试后放弃 if (attempt retries) { break } } } throw lastError || new Error(请求失败) }这个封装的逻辑你可以根据自己的业务调整但核心思路是一致的方向统一处理超时、按照状态码分类处理、让 5xx 和服务端不稳定的情况具备自动恢复能力。6.2 让浏览器完全忽略缓存前端发请求最烦的事情之一就是本地缓存导致数据不新。如果你希望每次请求都拿到最新数据不加新功能也不依赖服务端改配置可以在 options 里加上const response await fetch(https://api.example.com/current, { cache: no-store, headers: { Cache-Control: no-cache, Pragma: no-cache } })cache: no-store是让浏览器完全不读本地缓存后面的两个 header 是给中间代理服务器看的双管齐下能最大程度避免缓存干扰。这在调试问题和处理实时数据时特别管用。6.3 记住这几个坑能帮你省至少一个下午查 bug第一个坑response.json()抛出 SyntaxError。这通常是因为服务端返回的不是合法 JSON可能是一段错误 HTML 或者直接空了。你用response.text()把原始内容打出来看看往往立刻就知道问题在哪。第二个坑fetch只有在网络层面失败才会 rejectHTTP 状态码是 404/500 时不会走进 catch。你必须在代码里手动检查response.ok或者response.status。第三个坑请求体已经用过一次不能重复使用。前面说过 Response.body 是一次性的Request.body 也一样。你如果想把同一个 Request 对象重发一次得先request.clone()否则会抛错。第四个坑如果你发的是application/json的 POST 请求但没有JSON.stringify直接把一个 JavaScript 对象传给 body浏览器会报错。body 需要的是字符串、二进制数据或者流不是普通对象。第五个坑credentials不设置跨域请求默认是不带 Cookie 的。很多单点登录的接口调试半天最后就是这行配置没加。7. 写在最后的实操体会我用的时间越长越发现 fetch 这个 API 的核心思想就两个字标准。它把 HTTP 请求里分层的概念——请求、响应、头部、正文、流——全部建模成了现代 JavaScript 的对象然后通过 Promise 把它们串起来。只要理解了这种分层再去用任何封装好的请求库思路都会非常通透。另外关于那些网络报错的排查我最大的体会是永远不要盯着报错文本的字面意思看要顺着请求的整条链路一级一级往上找问题。could not fetch不一定是 fetch 的锅403不一定是密码错了timeout有时候也不一定是服务端慢。把心态放平把请求的每个环节都当成可排查的黑盒逐一验证问题基本很快就能浮出水面。最后再分享一个日常小技巧调试任何 fetch 相关问题先把浏览器的 Network 面板打开把请求列表刷新出来。看请求有没有发出去、状态码是什么、响应时间是多少、重定向了几次、请求头和响应头长什么样。这一套看下来问题原因通常已经漏出七八成了。剩下的才有必要去翻代码和服务端日志。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →