AJAX请求方法详解GET与POST的实战区别表单提交文件上传跨域问题解决常见错误排查完整指南
嗨,朋友!欢迎来到AJAX的世界。想象一下,AJAX就是你网页上的”快递员”,它在后台悄悄地去服务器取数据,然后让你的页面在不刷新、不卡顿的情况下更新内容。这感觉就像你在看视频的时候,弹幕还在不停滚动,而页面本身纹丝不动——这就是AJAX的魔力。
今天咱们就把AJAX里最核心的GET和POST两大请求方法掰开揉碎讲清楚,顺便把表单提交、文件上传、跨域问题这些头疼的事都过一遍。别担心,我会尽量用大白话来讲,你边看边学,慢慢就明白了。
GET和POST,这两位老朋友到底有什么区别
我打赌你第一次学AJAX的时候,老师一定说”GET用来获取数据,POST用来提交数据”。这句话没错,但太浅了。咱们来点实在的。
本质上的区别
GET请求会把数据附加在URL后面,就像你在浏览器地址栏里看到的那些参数一样:
https://api.example.com/users?id=123&name=张三
而POST请求呢?数据是放在请求体(body)里的,URL干干净净:
POST https://api.example.com/users
数据在body里长这样:
{
"id": 123,
"name": "张三"
}
你看,这就好比GET是把东西放在明信片上寄出去,所有人都能看到;POST是把东西装进信封里寄出去,只有收信人知道里面是什么。
数据长度的限制
GET请求的数据长度是有限制的,原因很简单——数据全写在URL里了,而浏览器的URL长度是有限制的。Chrome大概支持2048个字符,Firefox支持65536个字符,但不同浏览器不一样。如果你的数据超过了这个限制,GET就炸了。
POST请求理论上没有长度限制,因为数据在body里,而body的长度主要受服务器配置的影响。你传10KB或者10MB,浏览器不会拦你,服务器才会说”哎呀太多了”。
安全性问题
很多人以为POST比GET安全,这个说法不完全对。HTTPS加密之后,GET和POST的数据在传输过程中都是加密的。区别在于:
- GET的数据在URL里,会出现在浏览器历史记录、服务器日志、代理服务器日志里, anyonewhoever能看到
- POST的数据在body里,不会出现在这些地方
所以如果你的请求包含敏感信息(比如密码、身份证号),用POST更合适。但记住,光靠POST不够,一定要上HTTPS。
缓存行为
GET请求是可以被浏览器缓存的。你第一次请求某个GET接口,浏览器可能会把结果存起来,下次再请求同样的URL就直接用缓存,不发送网络请求了。这个特性在某些场景下是好事,在某些场景下会让人抓狂。
POST请求不会被缓存。每次POST都是全新的请求,服务器必须重新处理。
幂等性
这个概念听起来有点高级,其实很简单。”幂等”的意思就是:同一个请求做多次,结果和做一次是一样的。
GET是幂等的。你请求100次”获取用户信息”,服务器返回的数据应该是一样的,不会因为你请求了100次就多了一个用户。
POST不是幂等的。你请求100次”提交订单”,服务器可能会创建100个订单。这就是为什么GET和POST在语义上有本质的区别。
实战代码对比
咱们写个代码来看看:
// GET请求的写法
fetch('https://api.example.com/users?id=123&name=张三')
.then(response => response.json())
.then(data => {
console.log('获取到的用户数据:', data);
})
.catch(error => {
console.error('请求失败:', error);
});
// 或者用axios
axios.get('https://api.example.com/users', {
params: {
id: 123,
name: '张三'
}
})
.then(response => {
console.log('获取到的用户数据:', response.data);
});
// POST请求的写法
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: 123,
name: '张三'
})
})
.then(response => response.json())
.then(data => {
console.log('提交成功:', data);
})
.catch(error => {
console.error('提交失败:', error);
});
// 或者用axios
axios.post('https://api.example.com/users', {
id: 123,
name: '张三'
})
.then(response => {
console.log('提交成功:', response.data);
});
看到了吗?GET的请求参数直接拼在URL后面,POST的请求参数放在body里。这就是最本质的区别。
什么时候用GET,什么时候用POST
这里有一个简单的判断逻辑:
- 查询数据 → 用GET。比如搜索、分页、筛选、获取详情
- 修改/创建数据 → 用POST。比如登录、注册、提交表单、创建订单
- 涉及敏感信息 → 用POST。比如密码、token、个人信息
- 数据量较大 → 用POST。GET有长度限制
- 需要幂等性 → 用GET。重复请求不会有副作用
当然,现实世界不是非黑即白的。有时候你会看到用GET请求来提交数据的,也有用POST请求来查询数据的。这不是绝对不可以,但违反了RESTful的规范,会让其他开发者困惑。
表单提交的那些事儿
表单提交是AJAX最常见的应用场景之一。想象一下,用户注册了一个新账号,填写完表单后点击提交。如果用传统的方式,整个页面会刷新,用户会看到一片白,然后才看到注册成功的提示。用AJAX的话,页面不动,数据悄悄提交,体验丝滑多了。
传统表单提交 vs AJAX表单提交
传统的方式是这样的:
<form action="/register" method="POST">
<input type="text" name="username" placeholder="用户名" required>
<input type="password" name="password" placeholder="密码" required>
<input type="email" name="email" placeholder="邮箱" required>
<button type="submit">注册</button>
</form>
用户点击提交后,浏览器会跳转到/register页面,整个页面刷新。这个过程慢,体验差,而且用户看不到任何反馈,不知道自己提交成功没有。
用AJAX改写之后:
<form id="registerForm">
<input type="text" id="username" name="username" placeholder="用户名" required>
<input type="password" id="password" name="password" placeholder="密码" required>
<input type="email" id="email" name="email" placeholder="邮箱" required>
<button type="submit">注册</button>
</form>
<script>
document.getElementById('registerForm').addEventListener('submit', function(e) {
// 阻止默认的表单提交行为(防止页面刷新)
e.preventDefault();
// 获取表单数据
const formData = new FormData(this);
// 发送AJAX请求
fetch('/register', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('注册成功!');
this.reset(); // 清空表单
} else {
alert('注册失败:' + data.message);
}
})
.catch(error => {
alert('网络错误,请稍后重试');
console.error('注册失败:', error);
});
});
</script>
代码看起来有点多,但逻辑很简单:
e.preventDefault():阻止表单的默认提交行为,不让页面刷新new FormData(this):把表单数据打包成一个FormData对象fetch:发送POST请求,把FormData作为body传过去
用FormData对象处理表单数据
FormData是一个非常有用的API,它能帮你轻松处理表单数据。而且它不仅能处理文本输入,还能处理文件上传——这一点后面会详细讲。
// 创建FormData对象
const formData = new FormData();
// 添加普通字段
formData.append('username', '张三');
formData.append('age', 25);
formData.append('email', 'zhangsan@example.com');
// 添加多个值(比如多选框)
formData.append('hobbies', 'reading');
formData.append('hobbies', 'gaming');
// 添加文件
const fileInput = document.getElementById('avatar');
formData.append('avatar', fileInput.files[0]);
// 发送请求
fetch('/api/upload', {
method: 'POST',
body: formData
// 注意:使用FormData时不要手动设置Content-Type
// 浏览器会自动设置,并加上boundary
})
.then(response => response.json())
.then(data => console.log('成功:', data))
.catch(error => console.error('失败:', error));
这里有一个非常非常重要的点:当你使用FormData时,不要手动设置Content-Type为application/json。因为FormData有自己的编码方式,浏览器会自动帮你设置正确的Content-Type,并加上boundary参数。如果你手动设置了Content-Type为application/json,服务器会无法解析你的数据。
用axios处理表单提交
axios比fetch更简洁一些,特别是处理表单数据的时候:
import axios from 'axios';
const form = document.getElementById('registerForm');
form.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
try {
const response = await axios.post('/register', formData);
console.log('注册成功:', response.data);
this.reset();
} catch (error) {
console.error('注册失败:', error.response?.data || error.message);
alert('注册失败,请稍后重试');
}
});
用async/await的写法更清晰,不用链式调用.then(),代码看起来更像同步的逻辑,读起来更容易理解。
文件上传——AJAX最强大的能力之一
文件上传是传统表单提交处理不了的(或者说处理起来很麻烦),但用AJAX就轻松多了。你可以上传一张头像照片、一份简历、甚至一个视频文件,全程页面不刷新,用户还能看到上传进度。
基础的文件上传
<input type="file" id="fileInput" accept="image/*">
<button id="uploadBtn">上传文件</button>
<div id="progressBar" style="display:none;">
<progress max="100" value="0"></progress>
<span id="progressText">0%</span>
</div>
<div id="result"></div>
document.getElementById('uploadBtn').addEventListener('click', function() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('请先选择一个文件');
return;
}
// 限制文件大小(比如不超过5MB)
if (file.size > 5 * 1024 * 1024) {
alert('文件太大啦,请选择5MB以内的文件');
return;
}
// 限制文件类型
if (!file.type.startsWith('image/')) {
alert('只能上传图片文件');
return;
}
const formData = new FormData();
formData.append('avatar', file);
const xhr = new XMLHttpRequest();
// 监听上传进度
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
document.getElementById('progressBar').style.display = 'block';
document.querySelector('progress').value = percent;
document.getElementById('progressText').textContent = percent + '%';
}
});
// 监听上传完成
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
const result = JSON.parse(xhr.responseText);
document.getElementById('result').innerHTML =
'<img src="' + result.url + '" style="max-width:200px;">';
document.getElementById('progressBar').style.display = 'none';
} else {
alert('上传失败,状态码:' + xhr.status);
}
});
// 监听错误
xhr.addEventListener('error', function() {
alert('网络错误,上传失败');
document.getElementById('progressBar').style.display = 'none';
});
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
这段代码用了原生的XMLHttpRequest,因为它能监听上传进度。fetch和axios默认不支持上传进度的监听,如果需要监听进度,还是得用XHR。
用fetch上传文件(不带进度条)
如果你不需要显示上传进度,用fetch更简洁:
document.getElementById('uploadBtn').addEventListener('click', async function() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('请先选择一个文件');
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
// 不要设置Content-Type,浏览器会自动设置
});
const data = await response.json();
if (data.url) {
document.getElementById('result').innerHTML =
'<img src="' + data.url + '" style="max-width:200px;">';
} else {
alert('上传失败:' + data.message);
}
} catch (error) {
alert('网络错误:' + error.message);
}
});
多文件上传
有时候你需要一次上传多个文件,比如用户要上传多张头像照片:
<input type="file" id="multiFileInput" accept="image/*" multiple>
<button id="multiUploadBtn">上传多个文件</button>
<div id="multiResult"></div>
document.getElementById('multiUploadBtn').addEventListener('click', async function() {
const fileInput = document.getElementById('multiFileInput');
const files = fileInput.files;
if (files.length === 0) {
alert('请先选择文件');
return;
}
const formData = new FormData();
// 遍历所有文件,逐个添加
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]); // 注意key是files[],数组形式
}
try {
const response = await fetch('/api/upload/multi', {
method: 'POST',
body: formData
});
const data = await response.json();
console.log('上传结果:', data);
} catch (error) {
console.error('上传失败:', error);
}
});
在服务器端,你需要用数组的方式接收这些文件。不同的服务器框架有不同的处理方式,这里就不展开了。
上传大文件——分片上传
如果用户上传的是一个几百MB甚至几个GB的视频文件,一次性上传会非常慢,而且一旦中断就要重新来。这时候就需要分片上传了:
async function uploadLargeFile(file) {
const CHUNK_SIZE = 5 * 1024 * 1024; // 每片5MB
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
formData.append('fileName', file.name);
try {
const response = await fetch('/api/upload/chunk', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.success) {
throw new Error('分片上传失败:' + result.message);
}
console.log(`进度:${i + 1}/${totalChunks}`);
} catch (error) {
console.error('上传出错:', error);
throw error;
}
}
// 所有分片上传完成,通知服务器合并
const formData = new FormData();
formData.append('fileName', file.name);
formData.append('totalChunks', totalChunks);
const response = await fetch('/api/upload/merge', {
method: 'POST',
body: formData
});
const result = await response.json();
return result;
}
// 使用示例
document.getElementById('largeFileInput').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
uploadLargeFile(file)
.then(result => alert('上传成功!'))
.catch(error => alert('上传失败:' + error.message));
}
});
分片上传的核心思想很简单:把大文件切成小块,一块一块地上传,最后告诉服务器”我传完了,你帮我拼起来吧”。这样即使网络中断,也可以从断开的地方继续传,不需要从头开始。
跨域问题——AJAX最让人头疼的坑
跨域问题是每一个前端开发者都会遇到的”拦路虎”。我第一次遇到的时候,控制台里报了一堆红字,完全不知道怎么回事。后来学了才明白,这是浏览器的同源策略在起作用。
什么是同源策略
浏览器的同源策略规定:只有当两个URL的协议、域名、端口完全一致时,才算是”同源”。否则就是”跨域”。
举个例子:
https://www.example.com/api和https://www.example.com/users→ 同源 ✅https://www.example.com/api和http://www.example.com/api→ 不同源(协议不同)❌https://www.example.com/api和https://api.example.com/data→ 不同源(域名不同)❌https://www.example.com:8080/api和https://www.example.com:9090/api→ 不同源(端口不同)❌
当你的前端页面和API服务不在同一个源时,浏览器会拦截AJAX请求,这就是跨域问题。
跨域错误的表现
当跨域请求被拦截时,你会在控制台看到这样的错误:
Access to XMLHttpRequest at 'https://api.example.com/users' from origin
'https://www.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
翻译一下就是:你的页面在www.example.com,但请求了api.example.com的数据,服务器没有返回Access-Control-Allow-Origin这个响应头,所以浏览器拒绝了这个请求。
CORS——跨域资源共享
CORS(Cross-Origin Resource Sharing)是解决跨域问题的标准方案。它的原理很简单:服务器在响应头里加上一些特定的字段,告诉浏览器”我允许这个来源的请求”。
比如,服务器可以这样返回响应:
Access-Control-Allow-Origin: https://www.example.com
或者,如果服务器允许所有来源:
Access-Control-Allow-Origin: *
浏览器看到这些头,就会放行请求。
前端的解决方案
前端能做的事情其实不多,因为跨域问题主要是服务器端需要配置。但前端可以做这些:
1. 使用代理(开发环境)
在开发时,你可以配置一个代理,让所有API请求都走本地的开发服务器,这样就不会有跨域问题:
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true
}
}
}
};
这样你请求/api/users,代理会帮你转发到https://api.example.com/api/users,浏览器看到的是同源请求,不会拦截。
2. JSONP(老旧方案)
JSONP是一种比较老的跨域方案,只能用于GET请求。它的原理是利用<script>标签不受同源策略限制的特点:
function jsonp(url, callback) {
const script = document.createElement('script');
const callbackName = 'jsonpCallback_' + Date.now();
// 在全局暴露回调函数
window[callbackName] = function(data) {
callback(data);
// 清理
delete window[callbackName];
document.body.removeChild(script);
};
// 拼接URL
script.src = url + '?callback=' + callbackName;
document.body.appendChild(script);
}
// 使用
jsonp('https://api.example.com/data', function(result) {
console.log('获取到的数据:', result);
});
JSONP的原理很简单:服务器收到请求后,不会直接返回JSON数据,而是返回一段JavaScript代码,调用你提供的回调函数。比如:
jsonpCallback_1234567890({
"name": "张三",
"age": 25
});
浏览器执行这段代码,就拿到了数据。但JSONP有个致命的缺陷:它只能发GET请求,而且依赖服务器支持。现代开发中基本不用了。
3. Nginx反向代理(生产环境)
在生产环境,跨域问题通常由服务器或者Nginx来解决:
# Nginx配置
server {
listen 80;
server_name www.example.com;
location /api/ {
proxy_pass https://api.example.com/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
root /usr/share/nginx/html;
index index.html;
}
}
这样前端请求/api/users,Nginx会转发到https://api.example.com/api/users,浏览器看到的是同源请求。
服务器端的CORS配置
如果服务器支持CORS,配置起来很简单:
Node.js + Express
const express = require('express');
const cors = require('cors');
const app = express();
// 允许所有来源
app.use(cors());
// 或者只允许特定来源
app.use(cors({
origin: 'https://www.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.get('/api/users', (req, res) => {
res.json({ name: '张三', age: 25 });
});
app.listen(3000);
Python + Flask
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "https://www.example.com"}})
@app.route('/api/users')
def get_users():
return jsonify({"name": "张三", "age": 25})
if __name__ == '__main__':
app.run(port=3000)
Java + Spring Boot
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("https://www.example.com");
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
预检请求(Preflight Request)
有些请求比较复杂,浏览器在发送实际请求之前,会先发送一个OPTIONS请求来”试探”服务器是否允许跨域。这个OPTIONS请求就是预检请求。
什么样的请求会触发预检?
- 使用了GET/POST之外的方法(如PUT、DELETE、PATCH)
- 设置了自定义请求头(如
Authorization、X-Custom-Header) - Content-Type不是
application/x-www-form-urlencoded、multipart/form-data或text/plain
预检请求的响应需要包含这些头:
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Access-Control-Max-Age告诉浏览器,这个预检结果可以缓存多久(单位是秒)。设置一个合理的值可以减少预检请求的频率,提升性能。
带Cookie的跨域请求
有时候你的跨域请求需要携带Cookie(比如用户登录状态)。这时候不能简单地用*作为Access-Control-Allow-Origin,需要指定具体的域名,并且设置Access-Control-Allow-Credentials: true。
// 前端请求
fetch('https://api.example.com/user', {
method: 'GET',
credentials: 'include' // 重要:告诉浏览器带上Cookie
})
.then(response => response.json())
.then(data => console.log(data));
// 服务器响应
res.setHeader('Access-Control-Allow-Origin', 'https://www.example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
注意:当Allow-Credentials为true时,Allow-Origin不能设置为*,必须指定具体的域名。这是浏览器的安全限制。
常见错误排查——那些年踩过的坑
AJAX开发过程中,错误无处不在。下面我把常见的错误类型整理出来,每个都配上具体的例子和解决方案。
1. 404错误——接口不存在
这是最简单的错误,说明你请求的URL不对。检查一下:
- URL路径是否正确
- 大小写是否正确(很多服务器对大小写敏感)
- 域名是否正确(是不是漏了http/https)
- 是否多了一个斜杠或少了一个斜杠
// 错误示例
fetch('https://api.example.com/users/') // 注意末尾的斜杠
// 正确示例
fetch('https://api.example.com/users')
2. 403错误——权限不足
403说明你找到了接口,但服务器拒绝你的请求。常见原因:
- 需要登录才能访问,但你的请求没有携带token
- IP被限制
- 请求频率过高,被限流
解决方案:
fetch('https://api.example.com/users', {
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
});
3. 401错误——未授权
401和403的区别在于:401是”你是谁?”,403是”我知道你是谁,但你不准进”。401通常意味着登录过期了,需要重新登录。
fetch('/api/users')
.then(response => {
if (response.status === 401) {
// 跳转到登录页
window.location.href = '/login';
}
return response.json();
});
4. 500错误——服务器内部错误
500说明服务器端出了问题,不是你前端的问题。这时候你需要:
- 查看服务器的错误日志
- 联系后端开发人员
- 检查你发送的数据格式是否正确(有时候数据格式不对会导致服务器崩溃)
5. Network Error——网络错误
Network Error是一个比较笼统的错误,可能有很多原因:
- 服务器宕机了
- 网络中断了
- 跨域问题(有时候跨域错误会显示为Network Error)
- SSL证书问题
排查方法:打开浏览器的开发者工具,查看Network面板,看看请求的详细信息。
6. CORS错误——跨域问题
前面已经讲了很多,这里再补充几个常见的CORS错误:
错误一:Access-Control-Allow-Origin缺失
Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'https://www.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
原因:服务器没有设置Access-Control-Allow-Origin头。
解决:让后端在响应头里加上Access-Control-Allow-Origin: https://www.example.com。
错误二:Credentials错误
Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'https://www.example.com' has been blocked by CORS policy:
The value of the 'Access-Control-Allow-Origin' header in the response must
not be the wildcard '*' when the request's credentials mode is 'include'.
原因:你设置了credentials: 'include',但服务器的Access-Control-Allow-Origin是*。
解决:服务器把Access-Control-Allow-Origin改成具体的域名。
错误三:预检请求失败
Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'https://www.example.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
原因:你的请求触发了预检(OPTIONS请求),但服务器没有正确处理。
解决:确保服务器正确处理OPTIONS请求,并返回正确的CORS头。
7. 数据解析错误
有时候请求成功了,但数据解析报错:
fetch('/api/users')
.then(response => response.json()) // 这里可能会报错
.then(data => console.log(data));
如果服务器返回的不是JSON格式(比如返回的是HTML错误页面),response.json()就会抛出异常。
解决方案:
fetch('/api/users')
.then(response => {
// 先检查Content-Type
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error('返回的不是JSON数据');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('解析错误:', error));
8. 请求超时
有时候请求会卡住很久,最后超时。可以设置超时时间:
// 用AbortController设置超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
fetch('https://api.example.com/users', {
signal: controller.signal
})
.then(response => response.json())
.then(data => {
clearTimeout(timeoutId);
console.log(data);
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
console.error('请求超时了');
} else {
console.error('请求失败:', error);
}
});
9. JSON序列化/反序列化错误
这是新手最容易犯的错误之一。把对象转成JSON字符串的时候漏掉了JSON.stringify,或者从JSON字符串解析的时候漏掉了JSON.parse。
// 错误:没有序列化成JSON字符串
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: { name: '张三', age: 25 } // 这是对象,不是字符串
});
// 正确:使用JSON.stringify
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: '张三', age: 25 })
});
10. 异步处理顺序错误
AJAX是异步的,很多时候新手会犯顺序错误:
// 错误示例
let userData;
fetch('/api/users/1')
.then(response => response.json())
.then(data => {
userData = data;
});
console.log(userData); // undefined!因为fetch还没完成
// 正确示例:用async/await
async function getUserData() {
const response = await fetch('/api/users/1');
const userData = await response.json();
console.log(userData); // 这里才有数据
}
getUserData();
最佳实践和性能优化
写完了那么多代码,咱们最后来聊聊最佳实践。好的代码习惯能让你的AJAX请求更稳定、更快、更安全。
1. 统一封装请求方法
不要让每个页面都写一遍fetch或axios的代码。封装一个通用的请求方法:
// request.js
import axios from 'axios';
const request = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
response => {
return response.data;
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
window.location.href = '/login';
break;
case 403:
alert('你没有权限访问这个资源');
break;
case 404:
alert('请求的资源不存在');
break;
case 500:
alert('服务器出错了,请稍后重试');
break;
default:
alert('请求失败:' + error.response.status);
}
} else {
alert('网络错误,请检查网络连接');
}
return Promise.reject(error);
}
);
export default request;
// 使用封装好的请求方法
import request from './request';
// 获取用户列表
async function getUsers() {
try {
const users = await request.get('/users');
return users;
} catch (error) {
console.error('获取用户列表失败:', error);
return [];
}
}
// 创建用户
async function createUser(userData) {
try {
const result = await request.post('/users', userData);
return result;
} catch (error) {
console.error('创建用户失败:', error);
throw error;
}
}
封装之后,你就不用每次手写重复的代码了。拦截器还能统一处理token、错误提示等逻辑。
2. 防抖和节流
有些操作会频繁触发AJAX请求,比如搜索框的输入。如果每次按键都发送请求,服务器会承受很大的压力。这时候可以用防抖(debounce)或节流(throttle):
// 防抖:延迟执行,最后一次触发后才执行
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流:固定间隔执行
function throttle(fn, delay) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last > delay) {
fn.apply(this, args);
last = now;
}
};
}
// 使用防抖的搜索功能
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', debounce(async function(e) {
const keyword = e.target.value.trim();
if (keyword.length < 2) return;
const response = await fetch('/api/search?q=' + encodeURIComponent(keyword));
const results = await response.json();
console.log('搜索结果:', results);
}, 300));
防抖和节流的区别:防抖是”等一等,看看还有没有新的触发”,节流是”每隔一段时间执行一次”。搜索功能用防抖,滚动加载用节流。
3. 请求取消
有时候用户触发了一个请求,但又快速触发了另一个请求,第一个请求的结果就会覆盖第二个的结果。用AbortController可以取消请求:
let currentController = null;
async function searchUsers(keyword) {
// 取消之前的请求
if (currentController) {
currentController.abort();
}
currentController = new AbortController();
try {
const response = await fetch('/api/users?keyword=' + keyword, {
signal: currentController.signal
});
const data = await response.json();
console.log('搜索结果:', data);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('搜索失败:', error);
}
}
}
searchInput.addEventListener('input', function(e) {
searchUsers(e.target.value);
});
4. 缓存策略
对于不常变化的数据,可以用缓存减少请求次数:
const cache = new Map();
async function getCachedData(url, ttl = 60000) {
const cached = cache.get(url);
if (cached && Date.now() - cached.time < ttl) {
return cached.data;
}
const response = await fetch(url);
const data = await response.json();
cache.set(url, {
data: data,
time: Date.now()
});
return data;
}
5. 错误重试
网络不稳定时,请求可能会失败。可以适当重试:
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error('HTTP错误:' + response.status);
}
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
总结
AJAX的GET和POST请求虽然看起来简单,但里面藏着不少学问。GET适合查询、POST适合修改,这个原则要牢记。表单提交用FormData来处理,文件上传要分片处理大文件,跨域问题大部分靠服务器配置CORS来解决。
最重要的是,遇到问题不要慌。先看控制台报什么错,再查Network面板看请求和响应的详细信息,大多数问题都能很快定位到。
AJAX是现代网页开发的基石之一,理解它、用好它,你的网页会变得又快又好。希望这篇文章能帮到你,如果还有疑问,随时来问!
