请求封装我做过多轮:重试、超时、取消、错误分类,最后收敛成一层统一的拦截逻辑。网络题最后都落到这些工程细节上。
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,用户操作触发局部请求。就像银行大厅有了自助查询机,查余额、查流水都不用排队——只在办理复杂业务时才去窗口。
Q2: XMLHttpRequest 的核心机制
XHR 是最早也是最底层的浏览器请求 API,理解它是理解一切上层封装的基础。XHR 的回调地狱与同步阻塞
Q3: Fetch API 的设计与 Promise 化
Fetch API 是 XMLHttpRequest 的现代替代品,基于 Promise 设计,提供了更简洁、更强大的 API。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 客户端库。请求取消:从 CancelToken 到 AbortController
Q5: 跨域请求与 CORS 原理
浏览器的同源策略限制了跨域请求。CORS 是浏览器与服务器协商跨域访问的标准机制。常见的跨域解决方案
Q6: 请求缓存策略与强缓存/协商缓存
ETag vs Last-Modified
Q7: HTTP/1.1 vs HTTP/2 vs HTTP/3 的演进
Q8: 请求超时、重试与错误处理的最佳实践
底层源码:Fetch 与 Axios 是如何实现的?
浏览器的 Fetch API 底层是怎么实现的?
Chromium 的 Fetch 实现在 Blink 渲染引擎中(详见 Chromium 源码:third_party/blink/renderer/modules/fetch/):
Axios 的拦截器是怎么实现的?
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 和 Axios 底层的请求是同一批发送的吗?
Fetch 和 Axios 底层的请求是同一批发送的吗?
答题要点:
要说明浏览器网络层的请求调度机制。高分回答:
“Fetch 和 XHR/Axios 最终都调用浏览器的网络模块(Chromium 中是 NetworkService)。浏览器对同一域名的并发连接数有限制:HTTP/1.1 下最多 6 个 TCP 连接,HTTP/2 下只有一个连接但支持多路复用。超过并发限制的请求会在浏览器内部排队等待。Axios 基于 XHR,Fetch 是独立的 API,它们的请求队列在 Blink 内部是分开管理的,但最终共享同一个网络栈。”
CORS 预检请求的结果会被缓存吗?
CORS 预检请求的结果会被缓存吗?
答题要点:
说明
Access-Control-Max-Age 的作用和默认行为。高分回答:
“会!服务器可以通过 Access-Control-Max-Age 响应头来缓存预检结果。比如设置 Access-Control-Max-Age: 86400,浏览器在 24 小时内对相同的跨域请求不再发 OPTIONS 预检。默认值因浏览器而异——Chrome 默认 5 秒,Firefox 默认 24 小时。预检缓存只对相同的方法和请求头组合有效——如果你改了请求方法或新增了自定义头,浏览器会重新发预检请求。”如何实现请求的并发控制?
如何实现请求的并发控制?
答题要点:
用队列 + 计数器实现信号量模式。高分回答:
“浏览器本身有并发限制(HTTP/1.1 最多 6 个同域连接),但业务层可能需要更严格的控制。核心是信号量(Semaphore)模式:通过计数器控制同时运行的请求数,超出的放入等待队列。”
HTTP/2 的多路复用具体是怎么工作的?
HTTP/2 的多路复用具体是怎么工作的?
答题要点:
必须说明 Stream、Frame、Connection 三层关系。高分回答:
“HTTP/2 核心概念三层:Connection(TCP 连接)、Stream(逻辑流)、Frame(数据帧)。一个 TCP 连接承载多个 Stream,每个 Stream 有唯一 ID(奇数是客户端发起,偶数是服务端推送)。不同 Stream 的 Frame 可以交错发送——这就是’多路复用’。但 TCP 层仍然是有序的——如果某个 TCP 包丢了,整个连接都要等待重传,这就是 HTTP/3 用 QUIC(基于 UDP)解决的核心问题。”
Service Worker 能拦截 Fetch 请求吗?和浏览器缓存是什么关系?
Service Worker 能拦截 Fetch 请求吗?和浏览器缓存是什么关系?
答题要点:
说明拦截优先级和缓存分层。高分回答:
“可以。Service Worker 拦截的是 Fetch API 发出的请求,它在浏览器缓存之前执行。优先级是:Service Worker → 浏览器缓存(强缓存/协商缓存)→ 网络。Service Worker 的缓存(Cache Storage)和浏览器 HTTP 缓存是独立的两套系统,可以叠加使用。在离线场景下,Service Worker 可以拦截所有请求并返回缓存内容,实现 PWA 的离线访问能力。”
如何用 Fetch 实现文件上传并监控上传进度?
如何用 Fetch 实现文件上传并监控上传进度?
答题要点:
Fetch 原生不支持上传进度,需要用 XMLHttpRequest。高分回答:
“Fetch API 原生不支持上传进度监控——企业项目中通常直接用 XMLHttpRequest 的 upload.onprogress,而不是在 Fetch 上做 hack。”
response.body 的 ReadableStream 只能监控下载进度。要监控上传进度,最简单的方案是用 XMLHttpRequest:🔥 如果一个页面同时发了 100 个请求,会发生什么?
🔥 如果一个页面同时发了 100 个请求,会发生什么?
答题要点:
分浏览器并发限制、操作系统、服务器三层分析。高分回答:
“会分三个层面排队:1. 浏览器层: HTTP/1.1 下同域最多 6 个 TCP 连接,前 6 个立刻发出,其余 94 个在浏览器内部队列排队。2. 操作系统层: 每个 TCP 连接占用一个端口,100 个请求不会耗尽端口,但可能触及文件描述符限制。3. 服务器层: 服务器通常有并发连接限制,可能触发限流(429 Too Many Requests)。优化方案:HTTP/2 多路复用解决浏览器并发限制;业务层用请求合并或请求队列。”
🔥 如何实现一个支持重试、超时、取消、并发限制的完整请求库?
🔥 如何实现一个支持重试、超时、取消、并发限制的完整请求库?
答题要点:
展示架构设计能力,用代码说明各个模块如何组合。高分回答:
“核心思路是用一个 关键设计点:并发控制用信号量、重试用指数退避、超时用 AbortController、拦截器用 Promise 链。“
request 函数作为基础,在外层包装各种能力:延伸阅读与引用参考
- 🔗 MDN Fetch API 文档:Fetch API 的权威参考,包含 Request、Response、Headers 等完整说明。[1]
- 📖 Jake Archibald: I’m Excited About Fetch!:深入解释了 Fetch 的设计哲学和 Streams 能力。[2]
- 🟢 MDN HTTP 缓存:强缓存、协商缓存的完整机制说明。[3]
- 📖 High Performance Browser Networking:HTTP/2、HTTP/3、QUIC 的权威技术深度解析。[4]
- 🟢 Axios 官方文档:拦截器、取消请求、错误处理的完整 API 说明。[5]
