AJAX 请求方法详解:GET 和 POST 区别、跨域报错怎么办、请求失败如何处理、从回调地狱到 Promise 和 async 的进化之路前端开发实战教程
你第一次和服务器”对话”时发生了什么?
想象一下,你是一家餐厅的顾客(浏览器),厨师在厨房(服务器)。你点菜的方式有很多种——可以站起来走到厨房门口大声喊(GET),也可以把纸条递给服务员让她转交(POST)。AJAX 就是那个让你不用刷新整个页面就能”点菜”的技术,它让你的网页变得像 App 一样流畅。
今天我们就来聊聊这个让前端工程师又爱又恨的东西,我会用大白话把它讲透,保证你看完就能上手写。
GET 和 POST:搞清楚这两个”性格完全不同”的方法
GET 请求
GET 是最简单的请求方式,它的特性决定了它适合只读数据:
- 参数直接拼在 URL 后面,就像
https://api.example.com/users?id=123 - 有长度限制(不同浏览器不一样,通常 2KB-8KB)
- 会被浏览器缓存
- 会出现在浏览器的历史记录里
- 不适合传递敏感信息
// 最简单的 GET 请求
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/users?id=123');
xhr.send();
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('拿到了数据:', JSON.parse(xhr.responseText));
} else {
console.error('请求失败,状态码:', xhr.status);
}
};
POST 请求
POST 更灵活,适合提交数据:
- 数据放在请求体(Body)里,不在 URL 里显示
- 没有明显的长度限制
- 不会被缓存
- 不会出现在历史记录里
- 可以传递大量数据,包括文件
// POST 请求
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.example.com/users');
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
const userData = {
name: '小明',
age: 18,
email: 'xiaoming@example.com'
};
xhr.send(JSON.stringify(userData));
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('创建成功,返回:', JSON.parse(xhr.responseText));
} else {
console.error('创建失败:', xhr.status);
}
};
一张表说清楚区别
| 特性 | GET | POST |
|---|---|---|
| 数据位置 | URL 参数 | 请求体 |
| 长度限制 | 有 | 无 |
| 缓存 | 会缓存 | 不缓存 |
| 安全性 | 较低(URL 可见) | 较高(数据隐藏) |
| 适用场景 | 查询数据 | 提交数据 |
一个小记忆技巧:GET 就像是”查电话本”,POST 就像是”填申请表”。
跨域报错:浏览器给你的”安全墙”
什么是跨域?
跨域是指你的网页(比如 http://localhost:3000)去请求另一个域名/端口/协议的接口(比如 https://api.example.com)。浏览器出于安全考虑,会阻止这种”越界”访问。
你会看到的报错长这样
Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
这段话翻译成大白话就是:浏览器说,”你不认识这个服务器,我不让你访问。”
解决方案(从简单到复杂)
方案一:让后端加 CORS 头(最推荐)
告诉后端同学,在他们的响应头里加上这一行:
Access-Control-Allow-Origin: *
或者更精确地指定来源:
Access-Control-Allow-Origin: http://localhost:3000
方案二:本地开发用代理(前端自己解决)
如果你没法改后端,可以用开发服务器的代理功能。
用 Vite:
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
然后你的请求就变成相对路径:
fetch('/api/users') // 开发时会自动代理到 https://api.example.com/users
用 Webpack(vue-cli / create-react-app):
// webpack.config.js 或 package.json 里配置
module.exports = {
devServer: {
proxy: {
'/api': 'https://api.example.com'
}
}
}
方案三:JSONP(老项目可能还在用)
JSONP 利用 <script> 标签不受跨域限制的特性,不过它只支持 GET,现代项目基本不推荐了:
// 前端
function handleData(data) {
console.log('拿到数据:', data);
}
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleData';
document.body.appendChild(script);
// 后端响应(注意后端要配合返回这种格式)
handleData({ name: '小明', age: 18 });
请求失败了怎么办?完整的错误处理
请求失败是常态,写前端就像开车,你得学会怎么处理”抛锚”。
常见的失败情况
- 网络断了 — 用户没网
- 服务器挂了 — 500 错误
- 接口不存在 — 404
- 权限不够 — 401 / 403
- 参数错了 — 400
给 XHR 加上完整的错误处理
function makeRequest(method, url, data = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.setRequestHeader('Content-Type', 'application/json');
// 设置超时时间(20秒)
xhr.timeout = 20000;
// 请求超时
xhr.ontimeout = function() {
reject(new Error('请求超时,请稍后重试'));
};
// 网络错误
xhr.onerror = function() {
reject(new Error('网络连接失败,请检查网络'));
};
// 请求完成
xhr.onload = function() {
const response = JSON.parse(xhr.responseText);
// 2xx 表示成功
if (xhr.status >= 200 && xhr.status < 300) {
resolve(response);
}
// 401 未授权
else if (xhr.status === 401) {
reject(new Error('登录已过期,请重新登录'));
}
// 403 无权限
else if (xhr.status === 403) {
reject(new Error('你没有权限访问这个资源'));
}
// 404 不存在
else if (xhr.status === 404) {
reject(new Error('请求的资源不存在'));
}
// 500 服务器错误
else if (xhr.status === 500) {
reject(new Error('服务器出错了,请稍后再试'));
}
// 其他错误
else {
reject(new Error(`请求失败:${xhr.status} ${response.message || ''}`));
}
};
xhr.send(data ? JSON.stringify(data) : null);
});
}
使用 fetch 的写法(更现代)
async function fetchData(url) {
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token-here'
}
});
// fetch 不会把 4xx/5xx 当作错误,需要自己判断
if (!response.ok) {
throw new Error(`HTTP 错误!状态码:${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// 网络断开、JSON 解析失败等都会走到这里
console.error('请求失败:', error.message);
throw error; // 抛出去让上层处理
}
}
注意:
fetch在网络错误(如断网)时才会触发 catch,而 HTTP 错误状态码(404、500)不会自动抛错,需要手动判断response.ok。
回调地狱:一个真实的前端”噩梦”
在你刚学 AJAX 的时候,可能会写出这样的代码:
getUser(1, function(user) {
console.log('用户:', user.name);
getOrders(user.id, function(orders) {
console.log('订单:', orders);
getOrderItems(orders[0].id, function(items) {
console.log('商品:', items);
// 如果再嵌套一层...
getItemDetail(items[0].id, function(detail) {
console.log('详情:', detail);
// 再嵌套...
getReviews(detail.id, function(reviews) {
console.log('评论:', reviews);
// 看到这里,你已经晕了吧?
});
});
});
});
});
这段代码的问题很明显:
- 嵌套太深,读起来像楼梯一样一层一层往下走
- 错误处理困难,每一层都要写 try-catch
- 难以维护,改一个地方可能影响所有嵌套
业界管这个叫 “回调地狱”(Callback Hell) 或者 “金字塔恶魔”。
Promise:把金字塔拉平成一条直线
Promise 的本质是一个未来才知道结果的对象,它有三个状态:
- Pending(进行中)
- Fulfilled(已成功)
- Rejected(已失败)
把回调改写成 Promise
// 把之前的回调写法包成 Promise
function getUser(id) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/users/${id}`);
xhr.send();
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('获取用户失败'));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
});
}
function getOrders(userId) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/orders?userId=${userId}`);
xhr.send();
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('获取订单失败'));
}
};
});
}
function getOrderItems(orderId) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/orders/${orderId}/items`);
xhr.send();
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('获取商品失败'));
}
};
});
}
用 .then() 链式调用,代码立刻清爽了
getUser(1)
.then(user => {
console.log('用户:', user.name);
return getOrders(user.id); // 返回下一个 Promise
})
.then(orders => {
console.log('订单:', orders);
return getOrderItems(orders[0].id); // 继续返回
})
.then(items => {
console.log('商品:', items);
})
.catch(error => {
// 任何一层出错都会来到这里
console.error('出错了:', error.message);
});
看,金字塔变成了直线。这就是 Promise 的威力。
Promise 的几个实用工具方法
// 1. Promise.all — 所有请求并行,全部完成才继续
const [users, orders, items] = await Promise.all([
getUser(1),
getOrders(1),
getOrderItems(100)
]);
// 2. Promise.race — 谁先完成就用谁的结果(常用于超时控制)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('超时')), 5000)
);
const result = await Promise.race([
getUser(1),
timeoutPromise
]);
// 3. Promise.allSettled — 等所有请求都结束,不管成功失败
const results = await Promise.allSettled([
getUser(1),
getUser(2),
getUser(3)
]);
// results 里每个元素都有 status: 'fulfilled' 或 'rejected'
async/await:最优雅的终极形态
Promise 已经很好了,但 then() 链还是有点长。ES2017 引入了 async/await,让异步代码看起来像同步代码一样自然。
基本用法
async function loadData() {
try {
// 看起来像同步代码,但实际是异步的
const user = await getUser(1);
console.log('用户:', user.name);
const orders = await getOrders(user.id);
console.log('订单:', orders);
const items = await getOrderItems(orders[0].id);
console.log('商品:', items);
} catch (error) {
console.error('出错了:', error.message);
}
}
loadData();
并行请求这样写(性能更好)
async function loadDashboard() {
try {
// 用 Promise.all 让三个请求并行发出,而不是串行等待
const [user, orders, notifications] = await Promise.all([
getUser(1),
getOrders(1),
getNotifications(1)
]);
console.log('用户信息:', user);
console.log('订单列表:', orders);
console.log('通知消息:', notifications);
} catch (error) {
console.error('加载失败:', error.message);
}
}
串行 vs 并行的区别:假设每个请求耗时 200ms,串行走 600ms,并行走 200ms。对用户来说,快 3 倍。
实际项目中的完整封装
class ApiService {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
// 通用请求方法
async request(method, endpoint, data = null) {
const url = `${this.baseUrl}${endpoint}`;
const options = {
method,
headers: {
'Content-Type': 'application/json'
}
};
if (data) {
options.body = JSON.stringify(data);
}
// 添加 token(假设存在 localStorage)
const token = localStorage.getItem('token');
if (token) {
options.headers['Authorization'] = `Bearer ${token}`;
}
try {
const response = await fetch(url, options);
if (!response.ok) {
// 401 未授权,跳转登录
if (response.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
throw new Error('登录已过期');
}
throw new Error(`请求失败:${response.status}`);
}
// 空响应处理
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error(`[${method}] ${endpoint} 请求出错:`, error);
throw error;
}
}
// 封装常用方法
async get(endpoint) {
return this.request('GET', endpoint);
}
async post(endpoint, data) {
return this.request('POST', endpoint, data);
}
async put(endpoint, data) {
return this.request('PUT', endpoint, data);
}
async delete(endpoint) {
return this.request('DELETE', endpoint);
}
}
// 使用示例
const api = new ApiService('https://api.example.com');
async function initApp() {
try {
// 并行加载数据
const [user, posts] = await Promise.all([
api.get('/users/me'),
api.get('/posts?limit=10')
]);
console.log('当前用户:', user);
console.log('最新文章:', posts);
} catch (error) {
alert('加载失败,请刷新重试');
}
}
initApp();
请求取消:一个容易被忽略的能力
有时候用户导航走了,但请求还在跑,这是浪费资源。现代浏览器支持 AbortController:
// 创建一个控制器
const controller = new AbortController();
// 把信号传给请求
fetch('https://api.example.com/data', {
signal: controller.signal
})
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('其他错误:', err);
}
});
// 3秒后取消请求
setTimeout(() => {
controller.abort();
}, 3000);
在组件卸载时取消请求(React 示例):
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(data => setState(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
// 组件卸载时取消请求
return () => controller.abort();
}, []);
请求拦截器:统一处理所有请求
大一点的项目里,每个请求都加 token、加超时时间很麻烦。用拦截器可以一劳永逸:
// 简单的请求拦截器实现
class InterceptorManager {
constructor() {
this.fulfilled = [];
this.rejected = [];
}
use(fulfilledHandler, rejectedHandler) {
return {
eject: () => {
const index = this.fulfilled.indexOf(fulfilledHandler);
if (index !== -1) this.fulfilled.splice(index, 1);
if (rejectedHandler) {
const rIndex = this.rejected.indexOf(rejectedHandler);
if (rIndex !== -1) this.rejected.splice(rIndex, 1);
}
}
};
}
async processRequest(config) {
this.fulfilled.forEach(handler => {
config = handler(config);
});
return config;
}
async processResponse(response) {
this.fulfilled.forEach(handler => {
response = handler(response);
});
return response;
}
async processError(error) {
let finalError = error;
for (const handler of this.rejected) {
try {
finalError = await handler(finalError);
} catch (e) {
finalError = e;
}
}
throw finalError;
}
}
// 使用拦截器
const requestInterceptors = new InterceptorManager();
const responseInterceptors = new InterceptorManager();
// 添加请求拦截:自动加 token
requestInterceptors.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 添加请求拦截:自动加时间戳防缓存
requestInterceptors.use(config => {
if (config.method === 'GET') {
const separator = config.url.includes('?') ? '&' : '?';
config.url += `${separator}_t=${Date.now()}`;
}
return config;
});
// 添加响应拦截:统一处理错误
responseInterceptors.use(response => {
return response;
}, async error => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
});
给小白的入门路径建议
如果你刚开始学 AJAX,不要一次性把上面的所有内容都吃透,按这个顺序来:
第一阶段:先能用
- 学会用
fetch发 GET 请求拿到数据 - 学会用 POST 提交表单数据
- 看懂跨域报错是什么意思
第二阶段:能处理异常
- 学会用
.catch()处理错误 - 学会判断
response.ok - 给请求加超时处理
第三阶段:能写复杂逻辑
- 理解 Promise 的状态和链式调用
- 学会用
async/await写异步代码 - 学会用
Promise.all并行请求
第四阶段:能写工程化代码
- 封装自己的请求工具类
- 用拦截器统一处理 token 和错误
- 学会取消请求和优化性能
最后说几句
AJAX 是现代前端开发最基础也最重要的技能之一。你每天都会和它打交道,不管是点一个”点赞”按钮,还是加载一篇文章的列表。
记住三个核心:
- GET 查数据,POST 提交数据
- 永远要有错误处理,不要假设请求一定成功
- 用 async/await 代替回调,代码会干净很多
技术一直在进化,从 XHR 到 fetch,从回调到 Promise 再到 async/await,每一次进化都在让代码更好读、更好写。希望你也能跟上这个节奏,写出既强大又优雅的代码。
