使用 XMLHttpRequest 封装一个 GET 和 POST 请求
面试速答(30 秒版 TL;DR)
- 用 XHR 封装的核心点:返回 Promise;正确处理
onload/onerror/ontimeout/onabort;拼 query;设置 header;序列化 body;对 status 做分支;支持超时与取消(xhr.abort())。 - GET:参数放在 URL query;一般不带 body。
- POST:常见两种 body:
application/json(JSON.stringify)和application/x-www-form-urlencoded(URLSearchParams)。
心智模型:把“事件驱动的 XHR”变成“可组合的 Promise API”
最小可用封装(支持 GET/POST、JSON、超时、取消)
/**
* 极简 XHR 请求封装。
* 约定:默认解析 JSON;非 2xx 视为错误;支持 timeout 与 abort。
*/
export function xhrRequest(
method,
url,
{
params,
data,
headers,
timeout = 10000,
responseType = "json",
withCredentials = false,
} = {}
) {
const fullUrl = buildUrl(url, params);
const xhr = new XMLHttpRequest();
const promise = new Promise((resolve, reject) => {
xhr.open(method, fullUrl, true);
xhr.timeout = timeout;
xhr.withCredentials = withCredentials;
// responseType = 'json' 在部分场景可能返回 null,这里做兜底解析
xhr.responseType = responseType === "json" ? "text" : responseType;
if (headers) {
Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, String(v)));
}
xhr.onload = () => {
const status = xhr.status;
const ok = status >= 200 && status < 300;
const raw = xhr.response;
const parsed =
responseType === "json" ? safeJsonParse(raw) : raw;
if (ok) resolve(parsed);
else {
reject({
type: "http",
status,
statusText: xhr.statusText,
data: parsed,
url: fullUrl,
});
}
};
xhr.onerror = () => {
reject({ type: "network", message: "Network error", url: fullUrl });
};
xhr.ontimeout = () => {
reject({ type: "timeout", message: `Timeout after ${timeout}ms`, url: fullUrl });
};
xhr.onabort = () => {
reject({ type: "abort", message: "Request aborted", url: fullUrl });
};
const body = method === "GET" || method === "HEAD" ? null : data;
xhr.send(body);
});
return {
promise,
abort: () => xhr.abort(),
};
}
function buildUrl(url, params) {
if (!params) return url;
const qs = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => {
if (v === undefined || v === null) return;
qs.append(k, String(v));
});
const joiner = url.includes("?") ? "&" : "?";
const query = qs.toString();
return query ? `${url}${joiner}${query}` : url;
}
function safeJsonParse(text) {
if (text === "" || text == null) return null;
try {
return JSON.parse(text);
} catch {
return text; // 不是 JSON 就原样返回,避免直接抛异常
}
}
这段封装的面试要点是:你把 XHR 的事件回调收敛成统一的 Promise 结果,并且把“超时、取消、非 2xx、网络错误”区分开。
封装 GET/POST(再包一层,面试更好讲)
export function xhrGet(url, { params, headers, timeout } = {}) {
return xhrRequest("GET", url, { params, headers, timeout, responseType: "json" }).promise;
}
export function xhrPostJson(url, { params, json, headers, timeout } = {}) {
const h = { "Content-Type": "application/json", ...(headers || {}) };
return xhrRequest("POST", url, {
params,
headers: h,
timeout,
responseType: "json",
data: json == null ? null : JSON.stringify(json),
}).promise;
}
export function xhrPostForm(url, { params, form, headers, timeout } = {}) {
const h = { "Content-Type": "application/x-www-form-urlencoded", ...(headers || {}) };
const body = new URLSearchParams(form || {}).toString();
return xhrRequest("POST", url, {
params,
headers: h,
timeout,
responseType: "json",
data: body,
}).promise;
}
典型题 & 标准答法
Q:XHR 封装需要注意哪些点?
- 回调转 Promise:把事件回调统一到
resolve/reject。 - 区分错误类型:HTTP 非 2xx、网络错误、超时、取消要分开,方便上层做提示与埋点。
- GET 参数拼接:处理
undefined/null,避免拼出脏 query。 - POST body 与 Content-Type 对齐:JSON 和表单要分别序列化。
- 超时与取消:
xhr.timeout、xhr.abort()是前端工程质量的基本项。
常见追问(加分点)
- 为什么 fetch 更常用:API 更现代、和
AbortController更好配合;但默认不 reject 4xx/5xx,且进度事件不如 XHR 直接。 - 怎么防止重复提交:按钮禁用 + 请求去重;服务端幂等(幂等键)是根本。