前言
在现代前端开发中,我们经常使用 axios
、fetch
等库进行网络请求,但理解其底层原理对于成为优秀的前端工程师至关重要。本文将带你从头实现一个简单的 AJAX 函数,并逐步封装成类似 axios 的 Promise 风格库。
什么是 AJAX?
AJAX(Asynchronous JavaScript and XML)是一种创建快速动态网页的技术,通过在后台与服务器进行少量数据交换,实现网页的异步更新,这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
原生 XMLHttpRequest 基础
在开始封装之前,我们先了解浏览器原生的 XMLHttpRequest 对象:
javascript
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send();
XMLHttpRequest 的核心方法详解
1. xhr.open(method, url, async)
参数说明
open()
方法初始化一个请求,它有五个参数,其中前三个最常用:
- method:HTTP 请求方法,如 'GET'、'POST'、'PUT'、'DELETE' 等
- url:请求的 URL 地址,可以是相对路径或绝对路径
- async (可选):是否异步执行操作,默认为 true
true
:异步模式,请求在后台执行,不阻塞页面false
:同步模式,请求会阻塞页面直到完成(不推荐使用)
- user(可选):用于认证的用户名
- password(可选):用于认证的密码
javascript
// 异步 GET 请求
xhr.open('GET', '/api/users', true);
// 同步 POST 请求(不推荐)
xhr.open('POST', '/api/users', false);
2. xhr.send(data)
参数说明
send()
方法发送请求到服务器,参数根据请求类型有所不同:
- GET/HEAD 请求 :通常为
null
或undefined
- POST/PUT 请求 :包含请求体的数据,可以是多种格式:
String
:普通文本,如"name=John&age=30"
FormData
:用于文件上传或表单数据Blob
:二进制大对象ArrayBuffer
:原始二进制数据Document
:XML 文档null
:无请求体
javascript
// GET 请求,不需要请求体
xhr.send();
// POST 请求,发送表单数据
xhr.send('name=John&age=30');
// POST 请求,发送 JSON 数据
xhr.send(JSON.stringify({ name: 'John', age: 30 }));
// POST 请求,发送 FormData(文件上传)
const formData = new FormData();
formData.append('file', fileInput.files[0]);
xhr.send(formData);
3. readyState
状态说明
readyState
属性表示请求的状态变化,共有 5 个可能的值:
值 | 状态 | 描述 |
---|---|---|
0 | UNSENT | XMLHttpRequest 对象已创建,但 open() 方法尚未调用 |
1 | OPENED | open() 方法已调用,建立了与服务器的连接 |
2 | HEADERS_RECEIVED | send() 方法已调用,已接收到响应头 |
3 | LOADING | 正在接收响应体,responseText 属性包含部分数据 |
4 | DONE | 请求完成,响应数据接收完毕 |
javascript
xhr.onreadystatechange = function() {
switch(xhr.readyState) {
case 0:
console.log('请求未初始化');
break;
case 1:
console.log('服务器连接已建立');
break;
case 2:
console.log('请求已接收,响应头信息可用');
console.log('状态码:', xhr.status);
break;
case 3:
console.log('请求处理中,响应体部分数据可用');
console.log('已接收数据:', xhr.responseText.length, '字符');
break;
case 4:
console.log('请求完成,数据接收完毕');
if (xhr.status === 200) {
console.log('成功响应:', xhr.responseText);
}
break;
}
};
XMLHttpRequest 的其他关键属性
status
: HTTP 状态码(200、404、500 等)statusText
: HTTP 状态文本("OK"、"Not Found" 等)responseText
: 响应文本内容responseXML
: 响应 XML 文档(如果内容是 XML)response
: 根据响应类型返回相应格式的数据onreadystatechange
: 状态改变事件处理器
手写基础 AJAX 函数
让我们从最简单的 AJAX 函数开始:
javascript
function ajax(method = 'GET', url = '', data = '') {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error('请求失败:', xhr.status);
}
}
};
xhr.send(data);
}
这个基础版本虽然能用,但存在几个明显问题:
- 无法处理 GET 请求的查询参数
- 缺乏错误处理机制
- 回调方式不够现代化
- 不支持请求头设置
改进版 AJAX 函数
让我们修复这些问题:
javascript
function ajax(method = 'GET', url = '', data = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 处理 GET 请求的查询参数
let requestUrl = url;
if (method.toUpperCase() === 'GET' && data) {
const params = new URLSearchParams(data).toString();
requestUrl = `${url}${url.includes('?') ? '&' : '?'}${params}`;
}
xhr.open(method, requestUrl, true);
// 设置请求头
if (method.toUpperCase() === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText);
} else {
reject(new Error(`请求失败: ${xhr.status}`));
}
}
};
xhr.onerror = function() {
reject(new Error('网络错误'));
};
// 发送请求体数据
const sendData = method.toUpperCase() === 'GET' ? null : data;
xhr.send(sendData);
});
}
这个版本已经相当实用,支持 Promise,正确处理了 GET 和 POST 请求。
封装 MyAxios 库
现在让我们封装一个更加强大、类似 axios 的库:
javascript
function myAxios(config) {
// 处理配置参数
const {
method = 'GET',
url,
data = null,
headers = {},
timeout = 0
} = typeof config === 'string' ? { url: config } : config;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 处理查询参数
let requestUrl = url;
if (method.toUpperCase() === 'GET' && data && typeof data === 'object') {
const params = new URLSearchParams(data).toString();
requestUrl = `${url}${url.includes('?') ? '&' : '?'}${params}`;
}
xhr.open(method, requestUrl, true);
// 设置请求头
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key]);
});
// 设置超时
if (timeout > 0) {
xhr.timeout = timeout;
xhr.ontimeout = function() {
reject(new Error(`请求超时: ${timeout}ms`));
};
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
// 尝试解析 JSON 响应
let response = xhr.responseText;
if (xhr.getResponseHeader('Content-Type')?.includes('application/json')) {
try {
response = JSON.parse(response);
} catch (e) {
// 保持原始响应
}
}
resolve({
data: response,
status: xhr.status,
statusText: xhr.statusText,
headers: xhr.getAllResponseHeaders(),
config
});
} else {
reject(new Error(`请求失败: ${xhr.status} ${xhr.statusText}`));
}
}
};
xhr.onerror = function() {
reject(new Error('网络错误'));
};
// 发送请求体
let sendData = null;
if (method.toUpperCase() !== 'GET' && data) {
if (headers['Content-Type'] === 'application/json') {
sendData = JSON.stringify(data);
} else {
sendData = data;
}
}
xhr.send(sendData);
});
}
// 添加便捷方法
myAxios.get = function(url, config = {}) {
return myAxios({ ...config, method: 'GET', url });
};
myAxios.post = function(url, data = {}, config = {}) {
return myAxios({ ...config, method: 'POST', url, data });
};
使用示例
javascript
// GET 请求
myAxios.get('https://api.example.com/users', {
params: { page: 1, limit: 10 },
timeout: 5000
})
.then(response => {
console.log('获取用户列表:', response.data);
})
.catch(error => {
console.error('请求失败:', error.message);
});
// POST 请求
myAxios.post('https://api.example.com/users', {
name: 'John Doe',
email: 'john@example.com'
}, {
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
console.log('创建用户成功:', response.data);
});
// 使用完整配置
myAxios({
method: 'PUT',
url: 'https://api.example.com/users/1',
data: { name: 'Jane Doe' },
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
timeout: 10000
});
功能对比
功能 | 基础 AJAX | 改进版 AJAX | MyAxios |
---|---|---|---|
Promise 支持 | ❌ | ✅ | ✅ |
请求方法处理 | 基础 | 基础 | 完整 |
请求头设置 | ❌ | 基础 | 完整 |
超时处理 | ❌ | ❌ | ✅ |
响应拦截 | ❌ | ❌ | ✅ |
便捷方法 | ❌ | ❌ | ✅ |
错误处理 | 基础 | 完整 | 完整 |
总结
通过手动实现 AJAX 和封装 MyAxios,我们深入理解了网络请求的底层原理。这不仅帮助我们更好地使用现有的请求库,还能在遇到问题时快速定位和解决。
理解 xhr.open()
、xhr.send()
和 readyState
这些核心概念对于掌握 AJAX 至关重要。它们分别负责初始化请求、发送数据和监控请求状态,是 XMLHttpRequest 对象的三大支柱。
实际开发中,我们通常使用成熟的库如 axios,但理解其背后的原理对于提升技术水平至关重要。希望本文能帮助你更深入地理解前端网络请求的工作原理。