前言
在现代前端开发中,我们习惯了用 axios 或 fetch 发请求。但面试中经常被问到:
"请手写一个 AJAX 请求,并封装一个简易的 axios"
今天我们从最底层的 XMLHttpRequest 出发,逐步封装出一个功能完整的请求库,彻底搞懂 HTTP 请求的原理。
一、AJAX 基础回顾
1.1 什么是 AJAX?
AJAX 全称 Asynchronous JavaScript and XML(异步 JavaScript 和 XML)。
核心思想:
- 异步:不阻塞主线程,请求在后台进行
- 局部更新:不需要刷新整个页面
- 数据格式:现在主要用 JSON,而不是 XML
1.2 XMLHttpRequest 的基本用法
html
<!-- index.html -->
<ul id="members"></ul>
<script>
// 1. 创建 XMLHttpRequest 实例
const xhr = new XMLHttpRequest();
console.log(xhr.readyState, '------'); // 0
// 2. 打开一个请求
// 参数:method, url, async(true=异步,false=同步)
xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', false);
console.log(xhr.readyState, '|------'); // 1
// 3. 发送请求
xhr.send();
console.log(xhr.readyState, '|---|---'); // 4(同步请求,直接完成)
// 4. 获取响应
console.log(xhr.responseText);
</script>
逐行解析:
js
const xhr = new XMLHttpRequest();
- 创建 XMLHttpRequest 实例
- 此时
readyState = 0(未初始化) - 这个对象是浏览器提供的原生 API
js
xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', false);
open方法初始化一个请求- 三个参数:
method:HTTP 方法(GET/POST/PUT/DELETE 等)url:请求地址async:是否异步(true=异步,false=同步)
- 此时
readyState = 1(已打开)
js
xhr.send();
- 真正发送请求
- 同步请求时,代码会阻塞直到响应返回
- 异步请求时,需要监听
onreadystatechange事件
js
console.log(xhr.responseText);
responseText是服务器返回的响应体(字符串格式)- 同步请求可以直接获取,异步请求需要在
readyState = 4时获取
1.3 readyState 的 5 个状态
| 状态值 | 名称 | 说明 |
|---|---|---|
| 0 | UNSENT | 实例已创建,未调用 open |
| 1 | OPENED | 已调用 open,未调用 send |
| 2 | HEADERS_RECEIVED | 已调用 send,已接收到响应头 |
| 3 | LOADING | 正在接收响应体 |
| 4 | DONE | 请求完成,响应就绪 |
1.4 异步请求的正确写法
js
const xhr = new XMLHttpRequest();
// 异步请求(第三个参数为 true)
xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', true);
// 监听状态变化
xhr.onreadystatechange = function() {
console.log(xhr.readyState, '|---|---|');
// readyState = 4 表示请求完成
if (xhr.status === 200 && xhr.readyState === 4) {
console.log(xhr.responseText);
// 解析 JSON
const data = JSON.parse(xhr.responseText);
console.log(data);
// 渲染到页面
document.getElementById('members').innerHTML =
data.map(item => `<li>${item.login}</li>`).join('');
}
}
xhr.send();
逐行解析:
js
xhr.onreadystatechange = function() {
- 监听
readyState变化 - 每次状态改变都会触发这个回调
- 会触发多次:0 → 1 → 2 → 3 → 4
js
if (xhr.status === 200 && xhr.readyState === 4) {
xhr.status:HTTP 状态码200:成功404:未找到500:服务器错误
xhr.readyState === 4:请求完成- 两个条件同时满足,才能安全地获取响应数据
js
const data = JSON.parse(xhr.responseText);
responseText是字符串- 需要用
JSON.parse解析成 JavaScript 对象
js
document.getElementById('members').innerHTML =
data.map(item => `<li>${item.login}</li>`).join('');
- 将数据渲染到页面
map生成 HTML 数组,join拼接成字符串
二、封装基础版 AJAX
2.1 封装 GET 请求
js
function ajax(url) {
return new Promise((resolve, reject) => {
// 1. 创建 XMLHttpRequest 实例
const xhr = new XMLHttpRequest();
// 2. 初始化请求(异步)
xhr.open('GET', url, true);
// 3. 监听状态变化
xhr.onreadystatechange = function() {
// 4. 请求完成
if (xhr.readyState === 4) {
// 5. 判断状态码
if (xhr.status >= 200 && xhr.status < 300) {
// 6. 解析响应
try {
const data = JSON.parse(xhr.responseText);
resolve(data);
} catch (e) {
resolve(xhr.responseText); // 非 JSON 格式
}
} else {
reject(new Error(`Request failed with status ${xhr.status}`));
}
}
};
// 7. 发送请求
xhr.send();
});
}
// 使用
ajax('https://api.github.com/orgs/lemoncode/members')
.then(data => console.log(data))
.catch(err => console.error(err));
逐行解析:
js
return new Promise((resolve, reject) => {
- 返回 Promise,支持链式调用
- 将回调地狱转为链式调用
js
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
- 创建实例并初始化 GET 请求
true表示异步
js
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
- 监听状态变化
readyState === 4表示请求完成
js
if (xhr.status >= 200 && xhr.status < 300) {
- 判断 HTTP 状态码
2xx表示成功- 也可以用
xhr.status === 200 || xhr.status === 304
js
try {
const data = JSON.parse(xhr.responseText);
resolve(data);
} catch (e) {
resolve(xhr.responseText);
}
- 尝试解析 JSON
- 如果不是 JSON,直接返回字符串
- 避免解析错误导致请求失败
js
reject(new Error(`Request failed with status ${xhr.status}`));
- 请求失败时 reject
- 传递错误信息
2.2 支持 GET 和 POST
js
function ajax(method, url, data) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 1. 初始化请求
xhr.open(method, url, true);
// 2. 设置请求头(POST 必须)
if (method === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/json');
}
// 3. 监听状态变化
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const responseData = JSON.parse(xhr.responseText);
resolve(responseData);
} catch (e) {
resolve(xhr.responseText);
}
} else {
reject(new Error(`Request failed with status ${xhr.status}`));
}
}
};
// 4. 发送请求
if (method === 'POST') {
xhr.send(JSON.stringify(data)); // POST 发送请求体
} else {
xhr.send(null); // GET 不发送请求体
}
});
}
// 使用
ajax('GET', 'https://api.github.com/users/shunwuyu')
.then(data => console.log(data));
ajax('POST', 'https://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: 'bar',
userId: 1
}).then(data => console.log(data));
逐行解析:
js
xhr.setRequestHeader('Content-Type', 'application/json');
- 设置请求头
Content-Type: application/json告诉服务器,请求体是 JSON 格式- POST 请求必须设置
js
if (method === 'POST') {
xhr.send(JSON.stringify(data));
} else {
xhr.send(null);
}
- POST 请求:将数据序列化为 JSON 字符串发送
- GET 请求:不发送请求体,传
null
GET vs POST 对比:
| 特性 | GET | POST |
|---|---|---|
| 参数位置 | URL 查询字符串 | 请求体 |
| 数据长度 | 受 URL 长度限制(约 2KB) | 理论上无限制 |
| 缓存 | 可被缓存 | 不会被缓存 |
| 安全性 | 参数暴露在 URL 中 | 相对安全 |
| 幂等性 | 幂等(多次请求结果相同) | 非幂等 |
| 适用场景 | 获取数据 | 提交数据 |
三、封装进阶版:简易 axios
3.1 设计思路
axios 的核心功能:
- 支持多种 HTTP 方法:GET/POST/PUT/DELETE
- 请求拦截器:发送请求前处理(如添加 token)
- 响应拦截器:收到响应后处理(如统一错误处理)
- 配置合并:默认配置 + 请求配置
3.2 完整实现
js
class MyAxios {
constructor(config) {
// 1. 默认配置
this.defaults = {
baseURL: '',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
};
// 2. 合并用户配置
Object.assign(this.defaults, config);
// 3. 拦截器
this.interceptors = {
request: null,
response: null
};
}
// 4. 请求拦截器
useRequestInterceptor(fn) {
this.interceptors.request = fn;
}
// 5. 响应拦截器
useResponseInterceptor(fn) {
this.interceptors.response = fn;
}
// 6. 核心请求方法
request(config) {
// 合并配置
const finalConfig = { ...this.defaults, ...config };
// 请求拦截器
if (this.interceptors.request) {
finalConfig = this.interceptors.request(finalConfig);
}
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 构建完整 URL
let url = finalConfig.baseURL + finalConfig.url;
// GET 请求拼接查询参数
if (finalConfig.method === 'GET' && finalConfig.params) {
const queryString = Object.entries(finalConfig.params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
url += (url.includes('?') ? '&' : '?') + queryString;
}
// 初始化请求
xhr.open(finalConfig.method || 'GET', url, true);
// 设置请求头
Object.entries(finalConfig.headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
// 超时处理
xhr.timeout = finalConfig.timeout;
xhr.ontimeout = () => reject(new Error('Request timeout'));
// 监听响应
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
const response = {
data: xhr.responseText,
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
config: finalConfig,
xhr: xhr
};
// 解析 JSON
try {
response.data = JSON.parse(xhr.responseText);
} catch (e) {
// 非 JSON 格式,保持原样
}
// 响应拦截器
if (this.interceptors.response) {
this.interceptors.response(response);
}
// 判断成功/失败
if (xhr.status >= 200 && xhr.status < 300) {
resolve(response);
} else {
reject(response);
}
}
};
// 发送请求
const requestData = finalConfig.data ? JSON.stringify(finalConfig.data) : null;
xhr.send(requestData);
});
}
// 7. 快捷方法
get(url, params) {
return this.request({ method: 'GET', url, params });
}
post(url, data) {
return this.request({ method: 'POST', url, data });
}
put(url, data) {
return this.request({ method: 'PUT', url, data });
}
delete(url) {
return this.request({ method: 'DELETE', url });
}
}
// 创建实例
const axios = new MyAxios({
baseURL: 'https://api.github.com',
timeout: 10000
});
// 使用拦截器
axios.useRequestInterceptor((config) => {
console.log('请求拦截器:', config);
// 添加 token
config.headers['Authorization'] = 'Bearer xxx';
return config;
});
axios.useResponseInterceptor((response) => {
console.log('响应拦截器:', response);
// 统一处理错误
return response;
});
// 发起请求
axios.get('/users/shunwuyu')
.then(response => {
console.log('用户信息:', response.data);
})
.catch(error => {
console.error('请求失败:', error);
});
逐行解析:
js
class MyAxios {
constructor(config) {
this.defaults = {
baseURL: '',
timeout: 5000,
headers: {
'Content-Type': 'application/json'
}
};
Object.assign(this.defaults, config);
- 定义默认配置
baseURL:基础 URL,避免重复写timeout:超时时间headers:默认请求头Object.assign合并用户配置
js
this.interceptors = {
request: null,
response: null
};
- 拦截器对象
request:请求拦截器函数response:响应拦截器函数
js
useRequestInterceptor(fn) {
this.interceptors.request = fn;
}
- 注册请求拦截器
- 在发送请求前执行
js
request(config) {
const finalConfig = { ...this.defaults, ...config };
- 合并默认配置和请求配置
- 请求配置优先级更高
js
if (this.interceptors.request) {
finalConfig = this.interceptors.request(finalConfig);
}
- 执行请求拦截器
- 可以修改配置(如添加 token)
js
let url = finalConfig.baseURL + finalConfig.url;
if (finalConfig.method === 'GET' && finalConfig.params) {
const queryString = Object.entries(finalConfig.params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
url += (url.includes('?') ? '&' : '?') + queryString;
}
- 构建完整 URL
- GET 请求拼接查询参数
encodeURIComponent编码特殊字符
js
xhr.timeout = finalConfig.timeout;
xhr.ontimeout = () => reject(new Error('Request timeout'));
- 设置超时时间
- 超时后 reject
js
const response = {
data: xhr.responseText,
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
config: finalConfig,
xhr: xhr
};
- 构建响应对象
- 与 axios 的响应结构一致
js
try {
response.data = JSON.parse(xhr.responseText);
} catch (e) {
// 非 JSON 格式,保持原样
}
- 尝试解析 JSON
- 失败则保持字符串
js
get(url, params) {
return this.request({ method: 'GET', url, params });
}
- 快捷方法
- 简化 GET 请求的调用
四、扩展:fetch API
4.1 fetch 的基本用法
js
// GET 请求
fetch('https://api.github.com/users/shunwuyu')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json(); // 解析 JSON
})
.then(data => console.log(data))
.catch(error => console.error(error));
// POST 请求
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'foo',
body: 'bar',
userId: 1
})
})
.then(response => response.json())
.then(data => console.log(data));
4.2 fetch vs XMLHttpRequest
| 特性 | XMLHttpRequest | fetch |
|---|---|---|
| API 设计 | 基于回调 | 基于 Promise |
| 代码复杂度 | 较高 | 较低 |
| 错误处理 | 需要判断 status | 需要判断 response.ok |
| 超时控制 | ✅ 支持 | ❌ 不支持(需手动实现) |
| 进度监听 | ✅ 支持 | ❌ 不支持 |
| 兼容性 | 所有浏览器 | IE 不支持 |
| 默认携带 Cookie | ✅ 是 | ❌ 否(需手动配置) |
4.3 fetch 的缺陷
js
// 1. 不会自动 reject HTTP 错误
fetch('https://api.github.com/404')
.then(response => {
console.log(response.ok); // false
console.log(response.status); // 404
// 不会进入 catch,需要手动判断
});
// 2. 不支持超时
// 需要手动实现
function fetchWithTimeout(url, timeout = 5000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}
// 3. 默认不携带 Cookie
fetch('https://api.example.com', {
credentials: 'include' // 需要手动配置
});
这就是为什么我们还需要 XMLHttpRequest 和 axios。
五、经典面试题
面试题 1:如何实现请求取消?
js
// 使用 AbortController
const controller = new AbortController();
const signal = controller.signal;
fetch('https://api.github.com/users/shunwuyu', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求被取消');
}
});
// 取消请求
controller.abort();
面试题 2:如何实现请求重试?
js
function fetchWithRetry(url, retries = 3, delay = 1000) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch(error => {
if (retries > 0) {
console.log(`重试剩余次数: ${retries}`);
return new Promise(resolve =>
setTimeout(resolve, delay)
).then(() => fetchWithRetry(url, retries - 1, delay));
}
throw error;
});
}
fetchWithRetry('https://api.github.com/users/shunwuyu', 3, 2000)
.then(data => console.log(data))
.catch(error => console.error('最终失败:', error));
面试题 3:如何实现并发限制?
js
async function asyncPool(limit, items, iteratorFn) {
const results = [];
const executing = new Set();
for (const item of items) {
const p = Promise.resolve().then(() => iteratorFn(item));
results.push(p);
executing.add(p);
const clean = () => executing.delete(p);
p.then(clean).catch(clean);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// 使用:最多同时 2 个请求
asyncPool(2, [1, 2, 3, 4, 5], item => {
return fetch(`https://api.example.com/data/${item}`)
.then(res => res.json());
}).then(results => console.log(results));
六、总结
核心要点
- XMLHttpRequest 是浏览器提供的原生 HTTP 请求 API
- readyState 有 5 个状态,
4表示请求完成 - status 是 HTTP 状态码,
2xx表示成功 - 封装 axios 需要支持配置合并、拦截器、快捷方法
- fetch 是基于 Promise 的现代 API,但有缺陷(不支持超时、进度监听)
封装步骤
markdown
1. 创建 XMLHttpRequest 实例
2. 初始化请求(open)
3. 设置请求头(setRequestHeader)
4. 监听状态变化(onreadystatechange)
5. 发送请求(send)
6. 解析响应(JSON.parse)
7. 返回 Promise
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| XMLHttpRequest | 功能完整,兼容性好 | API 复杂,基于回调 | 需要兼容老浏览器 |
| fetch | API 简洁,基于 Promise | 不支持超时、进度监听 | 现代浏览器 |
| axios | 功能强大,拦截器机制 | 需要引入第三方库 | 生产环境 |