跳到主要内容

使用 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.timeoutxhr.abort() 是前端工程质量的基本项。

常见追问(加分点)

  • 为什么 fetch 更常用:API 更现代、和 AbortController 更好配合;但默认不 reject 4xx/5xx,且进度事件不如 XHR 直接。
  • 怎么防止重复提交:按钮禁用 + 请求去重;服务端幂等(幂等键)是根本。