Skip to main content
请求封装我做过多轮:重试、超时、取消、错误分类,最后收敛成一层统一的拦截逻辑。网络题最后都落到这些工程细节上。

Q1: 网络请求的历史?从 XMLHttpRequest 到 Fetch API

1

1999年: XMLHttpRequest 诞生

微软在 IE5 中首次以 ActiveX 控件 XMLHTTP 的形式提供了这套能力,后续被标准化为 XMLHttpRequest(XHR),让 JS 可以在不刷新页面的情况下向服务器发送请求并获取数据。从此网页从”整页刷新”时代进入了”局部更新”时代。
2

2005年: Ajax 概念普及

Jesse James Garrett 提出 Ajax 概念。Google Maps 和 Gmail 让业界意识到网页可以像桌面应用一样流畅。jQuery 的 $.ajax() 封装了 XHR 的繁琐 API,成为事实标准。
3

2015年: Fetch API 登场

Fetch API 基于 Promise 设计,解决了 XHR 回调地狱的问题,同时提供了对 Streams 的原生支持。但 Fetch 没有内置请求取消、超时、拦截器等功能,因此 Axios 仍然广泛使用。
网络请求技术解决的核心问题:
XMLHttpRequest 开启了异步请求时代,Fetch API 用 Promise 重新设计了请求 API,Axios 在其上封装了拦截器、取消请求等企业级能力。

🚫 没有异步请求

每次用户操作都要整页刷新。就像你去银行办业务,每次问一个问题都要取号、排队、重新到窗口——问三个问题就要排三次队。

✅ 引入异步请求

有了 XHR/Fetch,用户操作触发局部请求。就像银行大厅有了自助查询机,查余额、查流水都不用排队——只在办理复杂业务时才去窗口。
回答时先说 XHR 的痛点(回调地狱、API 反人类),再说 Fetch 的改进(Promise 化、Stream 支持),最后点出 Fetch 的不足以及 Axios 如何补齐。

Q2: XMLHttpRequest 的核心机制

XHR 是最早也是最底层的浏览器请求 API,理解它是理解一切上层封装的基础。
只有 readyState === 4 时才能安全读取 responseText,否则可能拿到不完整的数据。
XHR 就像你寄快递:open() 是填快递单,setRequestHeader() 是在包裹上贴标签,send() 是把包裹交给快递员。onreadystatechange 就是你不断查看物流状态——从”已揽收”到”已签收”。

XHR 的回调地狱与同步阻塞


Q3: Fetch API 的设计与 Promise 化

Fetch API 是 XMLHttpRequest 的现代替代品,基于 Promise 设计,提供了更简洁、更强大的 API。
即使服务器返回 404 或 500,Fetch 的 Promise 也不会 reject。 只有网络错误才会 reject。你必须手动检查 response.ok。

Fetch vs XHR 的核心差异

面试追问: 如果这个字节流是 UTF-8 文本,一个 emoji 的字节被拆到两个 chunk,应该如何还原?复用同一个 TextDecoder,对每块使用 decode(value, { stream: true }),正常结束时再调用 decode() 刷新缓冲。reader.releaseLock() 只释放独占读取锁,并不会取消流;如果要提前停止,应明确选择 reader.cancel(),或通过传给 fetch 的 AbortSignal 中止请求。 可选练习:Stream to Text — ReadableStream → string(免费题目与完整解答,无需登录),练习跨 chunk 的 UTF-8 解码、结束刷新、取消读取及 reader 清理。

Q4: Axios 的封装与拦截器机制

Axios 在 XHR(浏览器端)和 http(Node.js 端)之上提供了统一的 API,是目前最流行的 HTTP 客户端库。
  1. 请求拦截器:后添加的先执行(类似栈,后进先出)
  2. 响应拦截器:先添加的先执行(类似队列,先进先出)

请求取消:从 CancelToken 到 AbortController


Q5: 跨域请求与 CORS 原理

浏览器的同源策略限制了跨域请求。CORS 是浏览器与服务器协商跨域访问的标准机制。
简单请求(满足以下全部条件):方法为 GET/HEAD/POST,Content-Type 为三种之一,无自定义请求头。预检请求(不满足简单请求条件):浏览器先发 OPTIONS 请求询问服务器,服务器返回允许的头后才发正式请求。

常见的跨域解决方案


Q6: 请求缓存策略与强缓存/协商缓存

ETag vs Last-Modified


Q7: HTTP/1.1 vs HTTP/2 vs HTTP/3 的演进

HTTP/2 核心概念三层:Connection(TCP 连接)、Stream(逻辑流)、Frame(数据帧)。不同 Stream 的 Frame 可以交错发送,实现了多路复用。但 TCP 层仍然是有序的——如果某个 TCP 包丢了,整个连接都要等待重传。这就是 HTTP/3 用 QUIC(基于 UDP)解决的核心问题。

Q8: 请求超时、重试与错误处理的最佳实践

只有幂等请求(GET、PUT、DELETE)适合自动重试。POST 请求通常不是幂等的——重试可能导致重复创建资源:

底层源码:Fetch 与 Axios 是如何实现的?

Chromium 的 Fetch 实现在 Blink 渲染引擎中(详见 Chromium 源码:third_party/blink/renderer/modules/fetch/):
Axios 的核心是 lib/core/Axios.js 中的 request 方法(详见 Axios 源码):
请求拦截器用 unshift 加入数组(后进先出),响应拦截器用 push 加入数组(先进先出)。所有拦截器和请求被串成一条 Promise 链。

及格回答 vs 满分回答

问题:“说说前端网络请求的完整体系”

及格回答(60-70分):
“前端用 fetch 或 axios 发请求。fetch 是原生 API,基于 Promise。axios 是第三方库,支持拦截器和取消请求。跨域用 CORS,缓存用 Cache-Control。”
问题出在:知道有哪些工具,但不理解底层机制和设计取舍。 满分回答(90-100分):
前端网络请求的核心是在单线程环境下非阻塞地与服务器通信。底层是浏览器的网络模块(Chromium 中是 NetworkService),JS 通过 XHR 或 Fetch API 与之交互。XHR 基于回调和状态机(readyState 0-4);Fetch API 用 Promise 重新设计了请求流程,并原生支持 ReadableStream 实现流式传输。[1]
跨域是浏览器安全模型的核心限制。CORS 通过预检请求(OPTIONS)让服务器声明哪些跨域访问是允许的。简单请求直接发送,复杂请求先预检再发送。[2]
缓存策略直接影响用户体验。强缓存(Cache-Control: max-age)直接使用本地缓存不发请求,协商缓存(ETag/If-None-Match)向服务器验证资源是否更新。正确的策略是:HTML 用协商缓存,静态资源用强缓存+文件名哈希。[3]
生产环境的请求封装需要考虑:超时控制(AbortController)、失败重试(指数退避+幂等键)、全局错误处理(拦截器统一处理 401/403/500)、请求取消(搜索防抖场景)。

常见追问

答题要点: 要说明浏览器网络层的请求调度机制。高分回答: “Fetch 和 XHR/Axios 最终都调用浏览器的网络模块(Chromium 中是 NetworkService)。浏览器对同一域名的并发连接数有限制:HTTP/1.1 下最多 6 个 TCP 连接,HTTP/2 下只有一个连接但支持多路复用。超过并发限制的请求会在浏览器内部排队等待。Axios 基于 XHR,Fetch 是独立的 API,它们的请求队列在 Blink 内部是分开管理的,但最终共享同一个网络栈。”
答题要点: 说明 Access-Control-Max-Age 的作用和默认行为。高分回答: “会!服务器可以通过 Access-Control-Max-Age 响应头来缓存预检结果。比如设置 Access-Control-Max-Age: 86400,浏览器在 24 小时内对相同的跨域请求不再发 OPTIONS 预检。默认值因浏览器而异——Chrome 默认 5 秒,Firefox 默认 24 小时。预检缓存只对相同的方法和请求头组合有效——如果你改了请求方法或新增了自定义头,浏览器会重新发预检请求。”
答题要点: 用队列 + 计数器实现信号量模式。高分回答: “浏览器本身有并发限制(HTTP/1.1 最多 6 个同域连接),但业务层可能需要更严格的控制。核心是信号量(Semaphore)模式:
通过计数器控制同时运行的请求数,超出的放入等待队列。”
答题要点: 必须说明 Stream、Frame、Connection 三层关系。高分回答: “HTTP/2 核心概念三层:Connection(TCP 连接)、Stream(逻辑流)、Frame(数据帧)。一个 TCP 连接承载多个 Stream,每个 Stream 有唯一 ID(奇数是客户端发起,偶数是服务端推送)。不同 Stream 的 Frame 可以交错发送——这就是’多路复用’。但 TCP 层仍然是有序的——如果某个 TCP 包丢了,整个连接都要等待重传,这就是 HTTP/3 用 QUIC(基于 UDP)解决的核心问题。”
答题要点: 说明拦截优先级和缓存分层。高分回答: “可以。Service Worker 拦截的是 Fetch API 发出的请求,它在浏览器缓存之前执行。优先级是:Service Worker → 浏览器缓存(强缓存/协商缓存)→ 网络。Service Worker 的缓存(Cache Storage)和浏览器 HTTP 缓存是独立的两套系统,可以叠加使用。在离线场景下,Service Worker 可以拦截所有请求并返回缓存内容,实现 PWA 的离线访问能力。”
答题要点: Fetch 原生不支持上传进度,需要用 XMLHttpRequest。高分回答: “Fetch API 原生不支持上传进度监控——response.body 的 ReadableStream 只能监控下载进度。要监控上传进度,最简单的方案是用 XMLHttpRequest:
企业项目中通常直接用 XMLHttpRequest 的 upload.onprogress,而不是在 Fetch 上做 hack。”
答题要点: 分浏览器并发限制、操作系统、服务器三层分析。高分回答: “会分三个层面排队:1. 浏览器层: HTTP/1.1 下同域最多 6 个 TCP 连接,前 6 个立刻发出,其余 94 个在浏览器内部队列排队。2. 操作系统层: 每个 TCP 连接占用一个端口,100 个请求不会耗尽端口,但可能触及文件描述符限制。3. 服务器层: 服务器通常有并发连接限制,可能触发限流(429 Too Many Requests)。优化方案:HTTP/2 多路复用解决浏览器并发限制;业务层用请求合并或请求队列。”
答题要点: 展示架构设计能力,用代码说明各个模块如何组合。高分回答: “核心思路是用一个 request 函数作为基础,在外层包装各种能力:
关键设计点:并发控制用信号量、重试用指数退避、超时用 AbortController、拦截器用 Promise 链。“

延伸阅读与引用参考