Fetch API
Fetch API 是通过 Request、Response、Headers 等对象发起请求和处理响应的接口。fetch() 返回 Promise,但请求成功收到响应、HTTP 状态成功、正文解析成功和业务操作成功,是四个不同的判断。
掌握 Fetch 不只是会写 await fetch(url)。页面还需要知道何时展示结果、如何取消失效请求、怎样避免旧结果覆盖新结果,以及遇到失败时能否重试。这些行为也是封装请求库时真正需要统一的内容。
1. 面试核心
先记住这几条,再读正文- HTTP 404、500 通常不会使 fetch 的 Promise 拒绝。先检查
response.ok或状态码,再读取正文;网络失败、取消和正文解析失败分别处理。卡 · HTTP 失败与 Promise 拒绝 - 拿到 Response 不代表正文已读取完成,更不代表页面已经绘制。
response.json()也返回 Promise,图片加载与解码有自己的完成信号。卡 · 请求完成与资源可用 - URL 查询参数、JSON 和 FormData 是不同编码方式。查询参数交给 URL 工具编码;上传 FormData 时让浏览器生成包含 boundary 的 Content-Type。卡 · 查询参数的编码约定
- 响应正文通常只能消费一次。需要两份读取分支,应在使用前
clone();大响应的克隆仍可能带来缓冲成本。卡 · 响应正文的消费 - 取消请求要向底层传入 AbortSignal,Promise.race 本身不会停止网络操作。取消也不等于撤销服务端已完成的业务。卡 · 取消请求与正文读取
- 搜索结果应以当前请求标识决定能否提交。取消旧请求有助于节约资源,提交前的身份判断负责避免失效结果覆盖页面。卡 · 旧请求不能覆盖新状态
- 防抖、进行中的请求合并、结果缓存、取消和服务端幂等分别解决不同问题。共享一个进行中的 Promise,不等于永久保存结果。卡 · 重复请求的几种处理方式
- 重试必须有适用条件、次数和等待上限,并尊重取消。网络失败不能证明服务端没有处理,写请求不能未经判断就重放。卡 · 断网与重试策略
- XHR、Fetch 是浏览器请求接口,Axios 是封装库,Ajax 是交互方式。Axios 适配器随环境和配置变化,不能概括为始终只使用 XHR。卡 · Ajax、XHR、Fetch 与 Axios
- 上传进度到 100% 不代表业务处理成功。仍要等待响应并判断状态和结果;加载中、失败和取消也应有可见状态。卡 · 上传进度与业务完成
2. 请求与响应的生命周期
2.1 HTTP 失败与 Promise 拒绝
浏览器收到可交给脚本的响应头后,fetch() 就可以用 Response 兑现。404 也有正常的 HTTP 响应,因此不会仅因状态码而进入 catch。response.ok 表示状态码在 200—299 范围内,不代表响应正文符合业务要求。
下面把这个项目中“成功状态且 JSON 可解析”的约定封装成 getJSON,后文示例继续使用它:
class HttpError extends Error {
constructor(status, retryAfter = null) {
super(`HTTP ${status}`);
this.name = 'HttpError';
this.status = status;
this.retryAfter = retryAfter;
}
}
async function getJSON(url, { signal } = {}) {
const response = await fetch(url, { signal });
if (!response.ok) {
await response.body?.cancel();
throw new HttpError(response.status, response.headers.get('Retry-After'));
}
if (response.status === 204) return null;
return response.json();
}
这里主动把不成功状态转换成 HttpError,因此调用方可以区分 HTTP 状态失败与其他异常。204 约定返回 null;如果接口用 200 返回空正文,json() 仍会解析失败,不能把所有空内容悄悄视为成功。
json() 会解析正文,不会验证对象是否包含业务要求的字段。即使接口返回 { "code": "FAILED" } 且 HTTP 状态为 200,网络层也不知道业务已经失败,应用仍需按接口协议判断。
| 结果 | 原生 Fetch 的表现 | 应用要做的判断 |
|---|---|---|
| HTTP 404、500 | 通常兑现为 Response | 检查状态并处理错误响应 |
| DNS、连接等网络失败 | 拒绝,浏览器中常见为 TypeError | 提示失败,结合环境排查 |
| CORS 不允许读取 | 对脚本表现为失败 | 结合网络面板和服务端配置判断 |
| AbortSignal 取消 | 拒绝或中止正文消费 | 识别取消原因,避免把主动取消当作业务故障 |
| JSON 格式错误 | json() 拒绝,常见为 SyntaxError | 检查响应格式、重定向及接口约定 |
| HTTP 200 但业务错误 | 可以正常解析 | 根据业务字段判断 |
同源策略可能在请求发送后阻止脚本读取响应,具体区别见 同源策略。Node.js 中能调用同名 Fetch,也不能推断它具有浏览器相同的 Cookie、CORS 和页面环境。
2.2 响应正文的消费
响应体可以作为流逐步到达。text()、json()、blob() 和 arrayBuffer() 会消费它,并返回相应结果;不要先 text() 打日志,再对同一个响应调用 json()。
async function readTwice() {
const response = await fetch('/api/profile');
const copy = response.clone();
const [data, text] = await Promise.all([response.json(), copy.text()]);
console.log(data.name, text.includes('name'), response.bodyUsed);
}
readTwice();
如果服务器返回 { "name": "Lin" },输出为 Lin true true。在正文已经消费或流被锁定后再克隆,会失败。bodyUsed 反映正文是否已被使用,但流被 reader 锁定时,即使它尚未读取,也可能不能再交给另一种消费方式。
clone() 建立两个读取分支,并不保证大响应零成本复制。两个消费者速度不同,尚未消费的分支可能积累缓冲;只为调试而克隆大型下载是不合适的。
直接使用 response.body.getReader() 可以逐块消费下载内容。块边界不是 JSON 对象、文本行或完整 Unicode 字符的边界;文本流应使用支持连续解码的 TextDecoder 或相应转换流,再按应用协议组装记录。
2.3 请求完成与资源可用
等待 getJSON(),意味着这个封装已经拿到并解析数据;把数据写入 DOM 仍是后续操作,浏览器何时布局和绘制又属于渲染流程。不能把 await fetch() 解释成“页面更新完成”。
图片使用自身的加载和解码机制。例如先完成解码,再把图片加入文档:
async function showImage(url, container) {
const image = new Image();
image.alt = '预览';
image.src = url;
await image.decode();
container.append(image);
return image;
}
decode() 成功表示图像已解码、可以用于显示;加载失败或解码失败会拒绝。也可以监听图片的 load 与 error,但应在设置 src 前安装监听,避免遗漏已有缓存资源的事件。image.complete 单独为 true 不足以证明图片有效,因为空地址或加载失败也可能进入完成状态。
文档的 DOMContentLoaded、load,以及布局、绘制和呈现是另外的阶段,见 浏览器渲染流程。
3. 参数、请求体与凭据
3.1 查询参数的编码约定
GET 参数应该通过 URL 和 URLSearchParams 处理,避免把用户输入中的 &、+ 或中文直接拼成 URL。数组、空值和原有同名参数怎样处理,需要先与服务端约定。
下面采用“数组重复键、空数组删除该键、null 和 undefined 不修改该键、其余值限于字符串、有限数字和布尔值”的规则:
function withQuery(input, values) {
const url = new URL(input, location.href);
for (const [key, value] of Object.entries(values)) {
if (value == null) continue;
const items = Array.isArray(value) ? value : [value];
for (const item of items) {
const valid = typeof item === 'string' || typeof item === 'boolean' ||
(typeof item === 'number' && Number.isFinite(item));
if (!valid) throw new TypeError('查询参数必须是约定的标量');
}
url.searchParams.delete(key);
for (const item of items) url.searchParams.append(key, String(item));
}
return url.href;
}
console.log(withQuery('/api/search?page=1#results', {
q: 'C++ & JS', tag: ['web', '校招'], page: 2,
}));
解析结果应当有 q=C++ & JS、两个 tag 和 page=2。URL 字符串中的空格可能编码为 +,原始加号编码为 %2B;服务端按表单查询编码解码后得到原值。片段 #results 不会发送给服务器。
不要先 encodeURIComponent 值再交给 URLSearchParams,那可能发生二次编码。嵌套对象也没有通用的默认编码协议,不能不经约定就变成字符串 [object Object]。
3.2 JSON 与 FormData
JSON 请求要把对象序列化成字符串,并声明相应内容类型:
async function createDraft(title) {
const response = await fetch('/api/drafts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
if (!response.ok) throw new HttpError(response.status);
return response.json();
}
请求体不会因为传了普通对象就自动变成 JSON。对于 Fetch 的 GET、HEAD,也不能通过 body 发送 JSON 请求体,应使用接口约定的查询参数或其他方法。
表单和文件上传适合使用 FormData:
async function uploadFile(file) {
const body = new FormData();
body.append('file', file, file.name);
body.append('purpose', 'avatar');
const response = await fetch('/api/upload', { method: 'POST', body });
if (!response.ok) throw new HttpError(response.status);
return response.json();
}
此处不要手工设置 Content-Type: multipart/form-data。浏览器需要生成分隔各部分的 boundary,并让请求头与正文一致;手写一个没有 boundary 的头可能导致服务端无法解析。
3.3 Cookie、Token 与请求来源
浏览器 Fetch 默认使用 credentials: 'same-origin'。omit 不携带这类凭据,include 允许跨源携带符合条件的凭据,但仍不能绕过 Cookie 的 SameSite、Secure、分区或浏览器隐私规则。
跨源带凭据的响应还要满足服务器的 CORS 配置。credentials: 'include' 既不是“自动登录”,也不是“取消跨域限制”。在浏览器脚本中不能任意伪造 Cookie、Origin 等受控制的请求头。
应用使用 Bearer Token 时,可以根据自己的认证协议设置 Authorization,但只应发给受信任的 API 源。请求封装若接受任意绝对 URL,就不能无条件附带用户 Token,否则可能把凭据发给无关站点。刷新 Token、登录失效和业务权限都还需要单独处理。
4. 取消、超时与请求竞态
4.1 取消请求与正文读取
AbortController 发出取消信号,API 通过对应的 AbortSignal 接受它。信号一旦取消不能恢复,新的请求流程应使用新的 controller。
const controller = new AbortController();
const pending = getJSON('/api/slow', { signal: controller.signal });
controller.abort();
pending.catch(error => console.log(error.name)); // 默认取消原因:AbortError
如果 fetch() 已经拿到响应头,但正文尚未消费完成,取消也可以让后续正文读取失败。用户主动取消、页面卸载和业务超时都可以采用这条机制,但应保留各自的原因。
abort(reason) 可以带自定义原因,因此不能认为所有取消都只会得到名为 AbortError 的异常。更稳妥的判断要结合自己持有的信号是否取消、取消原因及当前请求身份。
客户端中止等待或连接,不等于服务端回滚。 用户提交订单后取消请求,服务端可能已经创建了订单,后续需要查询结果或使用服务端幂等机制。
4.2 把超时覆盖到正文消费
超时要先定义边界:只等响应头,还是连正文读取与解析也包含在内。下面沿用 getJSON,让超时覆盖到 JSON 读取完成,并支持调用方主动取消:
async function getJSONWithTimeout(url, { timeoutMs = 3000, signal } = {}) {
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) {
throw new RangeError('timeoutMs 必须是有效的正整数毫秒');
}
const controller = new AbortController();
const forwardAbort = () => controller.abort(signal.reason);
if (signal?.aborted) forwardAbort();
else signal?.addEventListener('abort', forwardAbort, { once: true });
const timer = setTimeout(() => {
controller.abort(new DOMException('请求超时', 'TimeoutError'));
}, timeoutMs);
try {
return await getJSON(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
signal?.removeEventListener('abort', forwardAbort);
}
}
这里必须 return await getJSON(...),让 finally 在正文消费结束后执行。若直接返回 Promise,finally 会提前清掉定时器,超时就不能覆盖后续等待。
定时器不是精确的截止时钟,主线程被阻塞时,取消回调也可能延后。Promise.race 只改变哪个结果先交给调用者,不会替代 controller 的取消操作。
4.3 旧请求不能覆盖新状态
搜索请求的完成顺序不一定等于发起顺序。用户先输入 ja,再输入 java,如果 ja 的响应更晚到达,就可能把页面改回旧结果。防抖只能减少发起次数,不能保证所有已发出的响应顺序。
下面让每次搜索获得递增标识,并在提交结果和错误之前检查它是否仍然有效:
function createSearch(onState, load = getJSON) {
let currentId = 0;
let controller;
let disposed = false;
return {
async search(text) {
if (disposed) return;
const id = ++currentId;
controller?.abort();
const ownController = new AbortController();
controller = ownController;
const query = text.trim();
if (!query) {
onState({ status: 'idle' });
return;
}
onState({ status: 'loading', query });
try {
const url = new URL('/api/search', location.href);
url.searchParams.set('q', query);
const data = await load(url.href, { signal: ownController.signal });
if (!disposed && id === currentId) onState({ status: 'ready', query, data });
} catch (error) {
if (!disposed && id === currentId && !ownController.signal.aborted) {
onState({ status: 'error', query, error });
}
}
},
dispose() {
disposed = true;
currentId++;
controller?.abort();
},
};
}
load 默认是真实请求,也可以在测试中换成可控制完成顺序的函数。即使某个加载函数不响应取消,标识检查仍能防止它覆盖新状态。清空输入和卸载同样会使旧请求失效。
这个对象在组件生命周期内创建一次;每次输入调用 search,卸载调用 dispose。页面根据 idle、loading、ready、error 渲染相应状态,并按协议验证 data。不要让旧请求的 finally 无条件关闭新请求的加载状态。
5. 合并、缓存与重试
5.1 重复请求的几种处理方式
“减少重复请求”需要先确认什么算重复,以及重复造成了什么问题。
| 方法 | 主要作用 | 不能替代的机制 |
|---|---|---|
| 防抖 | 高频输入稳定后再发起 | 不保证在途响应顺序 |
| 取消旧请求 | 减少无用工作,结束过时等待 | 不保证服务器撤销操作 |
| 在途请求合并 | 相同请求共享一个 Promise | 不自动提供完成后的缓存 |
| 结果缓存 | 在有效期内复用已有结果 | 不自动知道服务端数据已更新 |
| 按钮禁用 | 限制当前界面重复点击 | 不防止多标签页或网络重放 |
| 服务端幂等 | 让重复业务操作按协议只产生一次效果 | 不取代客户端响应与错误提示 |
缓存键不能随意只取路径。请求方法、参数、用户、语言和权限等,只要会影响结果,就需要纳入隔离和失效设计。
5.2 合并进行中的请求并缓存结果
下面限定为同一登录上下文中的只读 JSON 请求:调用方传入已规范化的完整 URL,缓存最多保留 100 个条目,成功后缓存指定时长,失败则删除。登录身份变化时整体清理。
function createJSONCache(ttlMs, load = getJSON, now = Date.now) {
if (!Number.isFinite(ttlMs) || ttlMs < 0) throw new RangeError('无效 TTL');
const entries = new Map();
return {
read(url) {
const old = entries.get(url);
if (old && (old.pending || now() < old.expiresAt)) return old.promise;
entries.delete(url);
if (entries.size >= 100) entries.delete(entries.keys().next().value);
const entry = { pending: true, expiresAt: 0, promise: null };
entry.promise = Promise.resolve().then(() => load(url)).then(
value => {
entry.pending = false;
entry.expiresAt = now() + ttlMs;
return value;
},
error => {
if (entries.get(url) === entry) entries.delete(url);
throw error;
},
);
entries.set(url, entry);
return entry.promise;
},
clear() { entries.clear(); },
};
}
同一个 URL 的并发调用拿到同一个 Promise,成功后的 TTL 从完成时开始计算。失败不会永久留在缓存里;clear() 后,旧请求即使完成也不会重新插入条目。这里是简单的容量淘汰,淘汰在途条目后再读取它可能重新发起请求,不能把它当作严格的全局请求注册表。
成功值中的对象仍是共享对象。调用方不应随意修改它,否则其他消费者可能看到改动。共享请求的取消也需要单独约定:不能让一个消费者取消自己的等待,就直接取消其他消费者仍然需要的底层请求。
TanStack Query 一类工具进一步管理服务端状态的查询键、有效性、重新获取和观察者。Jotai 一类客户端状态工具则适合本地交互状态。实际能力可能有重叠,但不能仅因两者都能保存对象,就认为它们解决同一个问题。
5.3 断网与重试策略
重试至少需要回答四件事:哪些失败可以重试、最多几次、隔多久、何时停止。浏览器的 navigator.onLine 只能提供网络状态线索,不保证目标接口可达;恢复事件可触发重新检查,不能作为成功依据。
可以采用下面的处理顺序:
- 先检查主动取消和当前请求是否已失效,失效则结束。
- 根据接口语义与错误类别决定是否允许重试。只读请求的瞬时网络失败或约定的 502、503、504,可以采用有限重试;认证失败、参数错误、格式错误不应原样无限重复。
- 采用逐步增加的等待,并加入随机抖动,避免大量客户端同时重试。设置尝试次数、单次等待和整体等待上限。
- 服务端提供
Retry-After时,按其秒数或 HTTP 日期处理;如果等待超过业务允许的期限,应告知用户或结束,不要把长等待截成很短后继续轰炸服务端。 - 等待期间仍响应取消。成功后清理定时器和监听,最终失败保留可重试入口与错误状态。
POST 提交失败不代表操作未执行。自动重试支付、下单或发布等请求,需要服务端提供幂等键或其他明确协议,并能够查询结果。重试流式或已消费的请求体时,还要重新建立可用的请求体,不能直接复用已经读完的流。
离线队列又比重试多一层职责:要保存任务、决定恢复顺序、处理凭据变化与过期、协调重复提交。不能仅加一段递归调用就称为完整的断网恢复。
6. 请求库与上传进度
6.1 Ajax、XHR、Fetch 与 Axios
Ajax 指页面通过异步请求局部更新的交互方式,不是一个特定 API,也不要求响应必须是 XML。XHR 与 Fetch 都可以实现这种交互。
| 对象 | 主要定位 | 使用时需要注意 |
|---|---|---|
| XMLHttpRequest | 浏览器提供的事件式请求接口 | 处理 load、error、abort、timeout 与进度事件 |
| Fetch API | 基于 Request、Response、Promise 和流的接口 | HTTP 错误不会自动拒绝,正文消费另有步骤 |
| Axios | 提供配置、转换、拦截器和适配器的请求库 | 行为受版本、适配器和配置影响 |
Axios 1.x 在浏览器中可以使用 XHR,支持相应版本和配置时也可以使用 Fetch 适配器;Node.js 还有 HTTP 适配器。面试回答“Axios 底层是什么”,应先说明环境和适配器,再解释配置、拦截器、请求发送与响应处理链路。
Axios 默认会对不满足 validateStatus 的响应拒绝,通常是非 2xx;调整该配置会改变成功和失败分支。不能把这个默认行为直接套到原生 Fetch。
6.2 封装与拦截器的职责
请求封装适合集中处理稳定的共性:受控 base URL、编码、超时、取消、状态判断、认证附加、必要的错误结构与日志。页面的“弹哪个提示”“跳哪一个路由”最好仍有清晰的调用边界,避免每个失败都被统一变成登录跳转。
以 Axios 1.x 实例为例,拦截器需要返回配置、响应或拒绝,不能遗漏返回值:
import axios from 'axios';
const api = axios.create({ baseURL: '/api', timeout: 3000 });
const requestInterceptor = api.interceptors.request.use(config => {
config.headers.set('X-Client', 'campus-web');
return config;
});
const responseInterceptor = api.interceptors.response.use(
response => response.data,
error => Promise.reject(error),
);
// 不再需要这组拦截器时清理,避免重复注册。
function removeInterceptors() {
api.interceptors.request.eject(requestInterceptor);
api.interceptors.response.eject(responseInterceptor);
}
这个实例的成功结果已经是 response.data,调用方就不应再次读取 .data。如果失败拦截器返回一个普通值,后续 Promise 链可能被恢复为成功;要继续保留失败应抛错或返回拒绝。
Axios 1.7 的普通拦截链中,请求拦截器按后注册先执行,响应拦截器按先注册先执行。真实项目还要考虑 runWhen、同步配置和错误传播,不应依赖一个没有说明前提的“所有拦截器都先进先出”结论。
如果多条请求同时遇到凭据过期,刷新流程可以共享一个进行中的 Promise;重放次数必须有界,刷新接口要避免递归进入同一刷新流程,并确认请求体可重放。刷新失败与无权限也不应混为一类。
服务端渲染中的请求封装还要按当前请求上下文读取用户凭据,不能在跨用户复用的全局单例里保存某个用户的 Token。浏览器里的 location、存储和 Cookie 行为也不能原样移到服务端。
6.3 上传进度与业务完成
需要浏览器上传进度时,XHR 的 upload 事件是直接的选择。原生 Fetch 没有与 xhr.upload.onprogress 等价的通用上传进度事件;请求流写入了多少,也不自动等于服务器已经接收或处理了多少。
function uploadWithProgress(url, file, onProgress) {
const xhr = new XMLHttpRequest();
const promise = new Promise((resolve, reject) => {
xhr.upload.addEventListener('progress', event => {
onProgress(event.lengthComputable ? event.loaded / event.total : null);
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.response);
else reject(new HttpError(xhr.status));
});
xhr.addEventListener('error', () => reject(new TypeError('网络请求失败')));
xhr.addEventListener('abort', () => reject(new DOMException('上传取消', 'AbortError')));
xhr.addEventListener('timeout', () => reject(new DOMException('上传超时', 'TimeoutError')));
xhr.open('POST', url);
xhr.responseType = 'json';
xhr.timeout = 10000;
const body = new FormData();
body.append('file', file, file.name);
xhr.send(body);
});
return { promise, abort: () => xhr.abort() };
}
调用者等待 promise 并处理错误,需要取消时调用 abort()。XHR 的 load 表示请求完成,HTTP 500 也可能触发,所以仍要检查状态。这里以 JSON 响应为约定,调用方还要验证业务结构;不能把空值自动解释为上传成功。
lengthComputable 为 false 时应展示不确定进度;为 true 时,比例对应这个上传请求的传输进度,不能简单等同于文件内容的字节百分比。达到 100% 后,应进入“等待处理结果”,直到服务器响应确认。跨源请求注册上传监听还可能触发预检,需由服务器配合。
6.4 请求顺序与页面状态
请求存在依赖时,先取得前一步结果再发下一步;彼此独立时可以并行。并行结果按输入顺序整理,可以使用 Promise.all;需要控制资源占用时使用有限并发。具体组合机制见 Promise。
“接口完成后再渲染页面”通常意味着先展示加载状态,数据可用后展示目标内容;失败显示错误或重试入口。客户端路由也可以在进入目标内容前加载必要数据,但仍要处理取消、鉴权与失败,不应让用户一直面对无反馈的空白。
如果目标是服务端直接返回带内容的 HTML,则涉及服务端渲染的数据获取,而不是简单把浏览器中的请求换成同步调用。无论在哪个环境,都需要明确“数据可用”的定义,以及失败时用户能看到什么。
7 · 面试题卡片
本页题目与参考回答
7.1 fetch 收到 404 会进入 catch 吗?
考察重点:HTTP 状态、网络失败、解析失败与业务失败的分层。
相关内容:HTTP 失败与 Promise 拒绝。
参考回答
通常不会,404 是正常收到的 HTTP 响应,原生 Fetch 会兑现为 Response。 应检查 response.ok 或状态码,再按应用约定抛出 HTTP 错误。
网络失败和取消会使请求失败;json() 还可能因为正文格式错误而拒绝。HTTP 200 且 JSON 可解析,也不等于业务成功。
7.2 怎样判断接口或图片已经加载完成?
考察重点:响应头、正文读取、图片解码和页面呈现的区别。
相关内容:请求完成与资源可用。
参考回答
先明确“完成”指哪个阶段:Fetch 兑现只得到响应对象,读取正文还要等待 json() 等方法;图片可以等待 load 或 decode。 加载和解码失败应有独立错误处理,image.complete 单独为 true 不足以证明图像有效。
取得数据或解码图像后,DOM 更新和浏览器绘制仍是后续阶段,不能把这些完成信号混为一谈。
7.3 如何把 GET 参数对象拼接到 URL,数组和特殊字符怎么处理?
考察重点:URL 编码、重复键、空值语义和协议约定。
相关内容:查询参数的编码约定。
参考回答
先约定数组、空值和已有参数的行为,再用 URL 与 URLSearchParams 编码。 例如数组使用重复键,null 和 undefined 不修改原参数,空数组删除该键,普通标量转换成字符串。
工具会处理 &、+ 和中文等字符,不要手工拼接或提前编码后再编码一次。嵌套对象需要单独协议,片段不会发送给服务器。
7.4 为什么 response.json() 之后不能再 response.text()?
考察重点:响应流、消费、锁定与克隆的代价。
相关内容:响应正文的消费。
参考回答
它们消费同一份响应正文,已经使用或被锁定的正文不能任意再次消费。 如果确实需要两份读取分支,应在消费前 clone(),再分别读取。
克隆不等于无成本复制;两边消费速度不同,大响应可能积累缓冲。流块也不一定对应完整文本或业务记录。
7.5 如何为 Fetch 设置超时并取消旧请求?
考察重点:AbortSignal、正文消费边界、清理与服务端效果。
相关内容:取消请求与正文读取、把超时覆盖到正文消费。
参考回答
创建 AbortController,把 signal 传入 Fetch,再在超时或请求失效时调用 abort。 如果超时包含正文消费,就要等正文读取完成后再清理定时器;同时清理取消监听,并保存明确的取消原因。
仅用 Promise.race 不能停止网络操作。信号不能复位,取消也不能证明服务端已经撤销操作。
7.6 搜索请求乱序返回,如何避免旧结果覆盖新结果?
考察重点:请求身份、提交条件、取消和组件清理。
相关内容:旧请求不能覆盖新状态。
参考回答
每次搜索生成新的请求标识,只有仍与当前标识一致的结果或错误才能提交。 同时取消旧请求以减少无用工作;即使底层不支持取消,身份检查仍然有效。
清空输入和卸载也要使旧请求失效。旧请求的 finally 不能无条件改掉新请求的 loading 状态;防抖只减少发起次数,不保证响应顺序。
7.7 短时间内的重复请求,除了防抖还能怎样处理?
考察重点:合并、缓存、取消与业务幂等的区别。
相关内容:重复请求的几种处理方式。
参考回答
相同只读请求可以合并进行中的 Promise,成功结果可按有效期缓存,过时请求可以取消。 缓存键和失效条件必须覆盖会改变结果的用户、参数和权限等上下文。
防止重复提交业务,还需要服务端幂等协议。按钮禁用、前端缓存或取消请求,都不能证明服务端只执行了一次。
7.8 如何缓存请求,并让并发调用只发送一次?
考察重点:Promise 共享、成功有效期、失败淘汰与缓存清理。
相关内容:合并进行中的请求并缓存结果。
参考回答
先以请求键保存进行中的 Promise,同键调用直接复用;成功后记录过期时间,失败则删除。 TTL 可以从成功完成时开始,过期后重新请求。清理之后,旧请求的完成回调不能把失效数据重新放回缓存。
还要确定容量、身份切换、对象共享和取消规则。单个消费者取消等待,不应默认中断其他消费者需要的请求。
7.9 断网重试需要考虑哪些条件?
考察重点:可重试错误、退避、取消、凭据和业务幂等。
相关内容:断网与重试策略。
参考回答
先判断请求是否仍有效及是否允许重放,再用有次数和时间上限的退避重试。 认证、参数和格式错误不应原样无限重试;网络恢复提示也不保证接口可达。等待期间要响应取消,并处理 Retry-After。
写请求失败不能证明服务端未执行,应通过幂等键或结果查询避免重复操作。离线队列还需处理持久化、凭据变化、顺序和过期。
7.10 Ajax、XHR、Fetch 和 Axios 有什么区别,Axios 底层是什么?
考察重点:交互方式、原生接口、封装库与运行环境。
相关内容:Ajax、XHR、Fetch 与 Axios。
参考回答
Ajax 是异步请求并局部更新页面的方式,XHR 与 Fetch 是浏览器接口,Axios 是请求封装库。 Fetch 使用 Promise、Request、Response 和流;XHR 提供事件式接口与上传进度;Axios 增加配置、转换、拦截器和适配器。
Axios 底层由版本、环境和适配器决定:浏览器可以是 XHR,也有 Fetch 适配器;Node.js 有 HTTP 适配器。不能无条件回答“Axios 始终就是 XHR”。
7.11 如何封装请求库,拦截器主要做什么?
考察重点:公共请求规则、返回约定、错误传播与安全边界。
相关内容:封装与拦截器的职责。
参考回答
封装应统一确实共用的 base URL、参数编码、超时取消、状态检查、认证附加和错误结构。 拦截器在发送前处理配置、返回后处理响应,同时保留明确的返回值与拒绝语义。
若成功拦截器已经返回 data,调用方就不能再次按完整响应使用。认证信息只能附加到受信任的目标;刷新过程要避免递归和无限重放;页面提示与跳转需要清晰的职责边界。
7.12 XHR 发请求需要哪些步骤,如何显示上传进度?
考察重点:open、事件监听、send、传输进度与响应结果。
相关内容:上传进度与业务完成、JSON 与 FormData。
参考回答
创建 XHR,安装完成、错误、取消、超时及上传进度监听,调用 open 设置请求,再设置必要属性并 send。 文件可用 FormData,不手写缺少 boundary 的 multipart Content-Type。
进度可计算时使用 loaded/total;不可计算时显示不确定状态。达到 100% 只表示传输进度完成,还要等待响应并判断 HTTP 状态与业务结果。原生 Fetch 没有通用的同等上传进度事件。
7.13 怎样让接口完成后再渲染内容,请求需要排序吗?
考察重点:依赖、并行、渲染状态与错误反馈。
相关内容:请求顺序与页面状态。
参考回答
有数据依赖的请求串行,彼此独立的请求并行;页面用加载、成功和失败状态表达等待过程。 Promise.all 可按输入顺序整理独立请求结果,资源受限时增加并发上限。
数据解析成功后再展示目标内容,不意味着阻塞整个浏览器。失败和取消都应结束对应等待;服务端渲染的数据获取则需要另按服务端生命周期设计。
7.14 同一请求封装在浏览器和服务端运行,要注意什么?
考察重点:平台 API、凭据上下文、跨用户隔离与网络行为。
相关内容:Cookie、Token 与请求来源、封装与拦截器的职责。
参考回答
平台环境和用户上下文不同,不能仅凭都有 fetch 就复用所有行为。 浏览器的 location、存储、Cookie 与 CORS 行为不能原样假定在服务端存在;服务端请求应显式取得当前用户的凭据和目标地址。
尤其不能在跨请求共享的全局实例中保存某个用户的 Token。共用的数据转换可以复用,环境相关的凭据取得、路由和错误处理应有明确接口。
7.15 TanStack Query 与客户端状态工具分别解决什么问题?
考察重点:远端数据有效性与本地交互状态的职责。
相关内容:合并进行中的请求并缓存结果。
参考回答
TanStack Query 主要管理服务端状态的查询、缓存、有效性与重新获取,本地状态工具主要表达客户端交互状态。 一个是持续与远端事实协调,另一个可用于弹窗、选中项和编辑中的表单状态。
两者可以协作,也存在能力重叠。选择时看数据所有权、失效规则和共享需求,不能仅因为都能保存对象就认为可以直接互换。
8. 参考资料
- WHATWG Fetch Standard
- MDN:Using the Fetch API
- MDN:Response.clone
- MDN:URLSearchParams
- MDN:AbortController.abort
- MDN:HTMLImageElement.decode
- MDN:Using XMLHttpRequest
- MDN:XMLHttpRequest.upload
- Axios:Interceptors
- Axios 1.7.9:Axios 请求链实现
- Axios 1.7.9:默认配置与适配器
- TanStack Query:Overview
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明