AJAX请求方法详解 从表单提交到数据加载 掌握GET与POST的正确用法 解决跨域与异步回调常见问题的实用指南
嘿,朋友,欢迎来到AJAX的世界!我猜你可能正在为一个前端项目发愁,比如”为什么我点击按钮后页面会刷新?”“为什么数据加载总是慢半拍?”“为什么我的请求总是被拦截?”别急,这篇文章就是为你准备的。我会像老朋友聊天一样,把这些看似复杂的东西掰开揉碎讲给你听。
初识AJAX:网页不再”翻脸”
想象一下,你正在写一封长邮件,突然浏览器卡住了,整个页面一片空白,等你写完后刷新页面,刚才的内容全没了——是不是很崩溃?
传统网页就是这样。每次你想更新一点内容,整个页面都得重新加载,就像你要换一本书里的一页,结果却得把整本书重买一本一样。
AJAX(Asynchronous JavaScript and XML,异步JavaScript和XML)的出现,彻底改变了这一切。它的核心思想很简单:在后台悄悄地向服务器请求数据,然后只更新页面上需要改变的那一小部分。
用生活中的例子说,就像你在餐厅吃饭,你不需要因为要加一道菜就把整张桌子撤掉重摆,只需要等服务员端上新菜就行。
GET与POST:两种最常用的请求方式
GET:简单直接的”询问”
GET请求就像你去图书馆借书,你告诉管理员”我要借这本书”,然后管理员去取。GET的特点有:
- 数据通过URL参数传递(比如
/api/users?id=123) - 数据长度有限制(通常4KB左右,取决于浏览器)
- 可以被缓存、加入书签
- 不应该用于提交敏感数据(因为参数会暴露在URL中)
// 最简单的GET请求 - fetch API
fetch('/api/users?id=123')
.then(response => response.json())
.then(data => {
console.log('获取到的用户数据:', data);
document.getElementById('user-info').textContent = data.name;
})
.catch(error => {
console.error('请求出错:', error);
});
// 带查询参数的GET请求
const userId = 123;
const userName = '张三';
fetch(`/api/users?id=${userId}&name=${encodeURIComponent(userName)}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP错误! 状态: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('用户信息:', data);
renderUserProfile(data);
})
.catch(error => {
console.error('获取用户信息失败:', error);
showError('加载用户信息失败,请稍后重试');
});
// 使用Promise方式处理GET请求
function getUserById(id) {
return fetch(`/api/users/${id}`)
.then(response => {
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
return response.json();
});
}
// 调用示例
getUserById(123)
.then(user => {
console.log(`用户: ${user.name}, 年龄: ${user.age}`);
})
.catch(error => {
console.error('获取用户失败:', error.message);
});
POST:安全可靠的”提交”
POST请求就像你把一封信塞进邮筒,邮递员去取。POST的特点有:
- 数据放在请求体中,不会暴露在URL中
- 数据长度几乎没有限制
- 适合提交敏感数据(密码、个人信息等)
- 不应该被缓存
// 使用POST提交表单数据
const formData = {
username: '张三',
password: 'mySecretPassword123',
email: 'zhangsan@example.com'
};
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => {
if (!response.ok) {
throw new Error(`登录失败: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('登录成功, 欢迎:', data.username);
// 跳转到首页
window.location.href = '/dashboard';
})
.catch(error => {
console.error('登录出错:', error);
showError(error.message);
});
// 使用FormData对象提交表单(适合文件上传)
const fileInput = document.getElementById('avatar');
const formData = new FormData();
formData.append('username', '张三');
formData.append('age', 25);
formData.append('avatar', fileInput.files[0]);
fetch('/api/upload', {
method: 'POST',
body: formData // 不需要设置Content-Type,浏览器会自动设置
})
.then(response => response.json())
.then(data => {
console.log('上传成功, 文件URL:', data.fileUrl);
document.getElementById('avatar-preview').src = data.fileUrl;
})
.catch(error => {
console.error('上传失败:', error);
});
// 使用Axios发送POST请求(更简洁的方式)
import axios from 'axios';
// 创建axios实例
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000
});
// POST请求示例
async function createUser(userData) {
try {
const response = await api.post('/users', userData);
return response.data;
} catch (error) {
if (error.response) {
// 服务器返回了错误状态
console.error(`服务器错误: ${error.response.status}`);
console.error('错误详情:', error.response.data);
} else if (error.request) {
// 请求已发出但没有收到响应
console.error('网络错误: 请求失败');
} else {
// 其他错误
console.error('请求配置错误:', error.message);
}
throw error;
}
}
// 调用示例
const newUser = {
name: '李四',
email: 'lisi@example.com',
role: 'member'
};
createUser(newUser)
.then(user => {
console.log('用户创建成功:', user.id);
});
请求方法的正确使用指南
很多人分不清什么时候该用GET,什么时候该用POST。这里有一个简单的判断标准:
| 场景 | 建议方法 | 原因 |
|---|---|---|
| 获取数据(查询用户、列表等) | GET | 幂等,可缓存 |
| 提交新数据(创建用户、下单等) | POST | 数据不暴露在URL中 |
| 更新已有数据 | PUT/PATCH | 语义清晰 |
| 删除数据 | DELETE | 语义清晰 |
| 上传文件 | POST | 需要二进制数据 |
| 搜索操作 | GET | 可缓存搜索结果 |
// 错误示例:用GET提交敏感数据
fetch(`/api/update-password?password=${userPassword}`)
.then(res => res.json())
.then(data => console.log(data));
// 问题:密码会出现在URL历史记录和服务器日志中!
// 正确做法:用POST提交敏感数据
fetch('/api/update-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ password: userPassword })
})
.then(res => res.json())
.then(data => console.log(data));
// 更安全:密码在请求体中,不会出现在URL中
// 实际项目中的请求封装示例
class ApiService {
constructor(baseURL) {
this.baseURL = baseURL;
}
// GET请求
async get(endpoint, params = {}) {
const queryString = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
const url = queryString
? `${this.baseURL}${endpoint}?${queryString}`
: `${this.baseURL}${endpoint}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getToken()}`
}
});
return this.handleResponse(response);
}
// POST请求
async post(endpoint, data = {}) {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getToken()}`
},
body: JSON.stringify(data)
});
return this.handleResponse(response);
}
// 统一处理响应
async handleResponse(response) {
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `请求失败: ${response.status}`);
}
return response.json();
}
// 获取Token(从localStorage或sessionStorage)
getToken() {
return localStorage.getItem('access_token');
}
}
// 使用示例
const api = new ApiService('https://api.example.com');
// 获取用户列表
api.get('/users', { page: 1, limit: 10 })
.then(users => {
console.log('用户列表:', users);
})
.catch(error => {
console.error('获取用户列表失败:', error);
});
// 创建新用户
api.post('/users', {
name: '王五',
email: 'wangwu@example.com',
role: 'admin'
})
.then(newUser => {
console.log('新用户创建成功:', newUser);
})
.catch(error => {
console.error('创建用户失败:', error);
});
从表单提交到数据加载的完整流程
简单表单提交示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>AJAX表单提交示例</title>
</head>
<body>
<h2>用户注册</h2>
<form id="registerForm">
<div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">注册</button>
</form>
<div id="result"></div>
<script>
document.getElementById('registerForm').addEventListener('submit', async function(e) {
e.preventDefault(); // 阻止表单默认提交行为(防止页面刷新)
const formData = new FormData(this);
const data = Object.fromEntries(formData);
const resultDiv = document.getElementById('result');
resultDiv.textContent = '正在注册...';
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
resultDiv.innerHTML = `
<p style="color: green;">注册成功!</p>
<p>欢迎, ${result.username}</p>
`;
this.reset(); // 清空表单
} else {
resultDiv.innerHTML = `
<p style="color: red;">注册失败: ${result.message}</p>
`;
}
} catch (error) {
resultDiv.innerHTML = `
<p style="color: red;">网络错误, 请稍后重试</p>
`;
console.error('注册请求失败:', error);
}
});
</script>
</body>
</html>
动态数据加载示例
// 模拟从服务器加载用户数据
async function loadUsers(page = 1) {
const container = document.getElementById('user-list');
const loadingIndicator = document.getElementById('loading');
// 显示加载状态
loadingIndicator.style.display = 'block';
container.innerHTML = '';
try {
const response = await fetch(`/api/users?page=${page}&limit=10`);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
// 隐藏加载状态
loadingIndicator.style.display = 'none';
// 渲染用户列表
if (data.users && data.users.length > 0) {
const userList = data.users.map(user => `
<div class="user-card">
<h3>${user.name}</h3>
<p>邮箱: ${user.email}</p>
<p>注册日期: ${new Date(user.createdAt).toLocaleDateString()}</p>
</div>
`).join('');
container.innerHTML = userList;
} else {
container.innerHTML = '<p>暂无用户数据</p>';
}
} catch (error) {
loadingIndicator.style.display = 'none';
container.innerHTML = '<p style="color: red;">加载用户数据失败,请刷新重试</p>';
console.error('加载用户数据失败:', error);
}
}
// 页面加载完成后自动加载第一页数据
document.addEventListener('DOMContentLoaded', () => {
loadUsers(1);
});
// 搜索功能 - 带防抖的AJAX请求
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
async function searchUsers(query) {
if (!query.trim()) {
loadUsers(); // 清空时加载所有用户
return;
}
const searchResults = document.getElementById('search-results');
searchResults.innerHTML = '<p>搜索中...</p>';
try {
const response = await fetch(`/api/users/search?q=${encodeURIComponent(query)}`);
const users = await response.json();
if (users.length > 0) {
searchResults.innerHTML = users.map(user => `
<div class="search-item">
<span>${user.name}</span>
<small>${user.email}</small>
</div>
`).join('');
} else {
searchResults.innerHTML = '<p>未找到匹配的用户</p>';
}
} catch (error) {
searchResults.innerHTML = '<p>搜索失败,请重试</p>';
console.error('搜索失败:', error);
}
}
// 创建防抖版本的搜索函数
const debouncedSearch = debounce(searchUsers, 300);
// 绑定搜索输入事件
document.getElementById('search-input').addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
跨域问题:那些烦人的”被拒绝”错误
什么是跨域?
跨域是指浏览器限制从一个源(域名、协议、端口任意一个不同)的请求另一个源的资源。比如:
- 你的前端项目在
http://localhost:3000 - 你的后端API在
http://api.example.com:8080 - 这就是跨域请求
浏览器会阻止这样的请求,返回类似这样的错误:
Access to fetch at 'http://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
这是最标准的解决方案,需要在后端服务器设置响应头:
// Node.js + Express 示例
const express = require('express');
const app = express();
// 允许特定域名访问
app.use((req, res, next) => {
const allowedOrigins = ['http://localhost:3000', 'https://www.yoursite.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 处理预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
# Python Flask 示例
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app, resources={
r"/api/*": {
"origins": ["http://localhost:3000", "https://www.yoursite.com"],
"methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Content-Type", "Authorization"]
}
})
解决方案二:前端代理(开发环境常用)
如果你正在开发中,不想修改后端代码,可以使用代理:
// Vue CLI - vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://api.example.com:8080',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
};
// Webpack devServer - webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': 'http://api.example.com:8080'
}
}
};
// 使用Axios的baseURL配置
import axios from 'axios';
const apiClient = axios.create({
baseURL: '/api', // 相对路径,开发时通过代理转发
timeout: 10000
});
解决方案三:JSONP(兼容旧浏览器)
// JSONP实现 - 适用于不支持CORS的旧浏览器
function jsonp(url, callback) {
const script = document.createElement('script');
const callbackName = 'jsonp_' + Date.now() + '_' + Math.random().toString(36).substr(2);
// 定义全局回调函数
window[callbackName] = function(data) {
callback(data);
// 清理
delete window[callbackName];
document.body.removeChild(script);
};
// 构建URL
const fullUrl = `${url}?callback=${callbackName}`;
script.src = fullUrl;
script.onerror = function() {
console.error('JSONP请求失败');
delete window[callbackName];
};
document.body.appendChild(script);
}
// 使用示例
jsonp('http://api.example.com/data', function(response) {
console.log('获取到的数据:', response);
});
异步回调:从回调地狱到异步/等待
回调函数的陷阱
早期的AJAX开发大量使用回调函数,结果代码变成了这样:
// 回调地狱示例 - 这很糟糕,不要写这样的代码
fetch('/api/user/123')
.then(response => response.json())
.then(user => {
console.log('用户信息:', user);
// 嵌套请求1
fetch(`/api/orders?userId=${user.id}`)
.then(response => response.json())
.then(orders => {
console.log('订单列表:', orders);
// 嵌套请求2
fetch(`/api/products?orderId=${orders[0].id}`)
.then(response => response.json())
.then(products => {
console.log('产品列表:', products);
// 更多嵌套...
})
.catch(error => console.error('产品请求失败:', error));
})
.catch(error => console.error('订单请求失败:', error));
})
.catch(error => console.error('用户请求失败:', error));
这种”回调地狱”(Callback Hell)让代码难以阅读和维护。
Promise:更优雅的解决方案
// 使用Promise重构
function getUser(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error('获取用户失败');
return response.json();
});
}
function getOrders(userId) {
return fetch(`/api/orders?userId=${userId}`)
.then(response => {
if (!response.ok) throw new Error('获取订单失败');
return response.json();
});
}
function getProducts(orderId) {
return fetch(`/api/products?orderId=${orderId}`)
.then(response => {
if (!response.ok) throw new Error('获取产品失败');
return response.json();
});
}
// 链式调用,清晰明了
getUser(123)
.then(user => {
console.log('用户:', user.name);
return getOrders(user.id);
})
.then(orders => {
console.log('订单:', orders);
return getProducts(orders[0].id);
})
.then(products => {
console.log('产品:', products);
})
.catch(error => {
console.error('请求链失败:', error);
});
async/await:最现代的写法
// 使用async/await,代码像同步一样清晰
async function loadUserData(userId) {
try {
// 获取用户信息
const userResponse = await fetch(`/api/users/${userId}`);
if (!userResponse.ok) {
throw new Error(`获取用户信息失败: ${userResponse.status}`);
}
const user = await userResponse.json();
// 获取订单列表
const ordersResponse = await fetch(`/api/orders?userId=${userId}`);
if (!ordersResponse.ok) {
throw new Error(`获取订单失败: ${ordersResponse.status}`);
}
const orders = await ordersResponse.json();
// 获取第一个订单的产品
if (orders.length > 0) {
const productsResponse = await fetch(`/api/products?orderId=${orders[0].id}`);
if (!productsResponse.ok) {
throw new Error(`获取产品失败: ${productsResponse.status}`);
}
const products = await productsResponse.json();
return {
user,
orders,
products
};
}
return { user, orders: [], products: [] };
} catch (error) {
console.error('加载用户数据失败:', error);
throw error; // 重新抛出,让调用者处理
}
}
// 调用示例
async function renderUserPage() {
const data = await loadUserData(123);
// 渲染页面
document.getElementById('user-name').textContent = data.user.name;
document.getElementById('order-count').textContent = data.orders.length;
// 渲染产品列表
const productContainer = document.getElementById('product-list');
productContainer.innerHTML = data.products.map(p => `
<div class="product">
<h4>${p.name}</h4>
<p>价格: ¥${p.price}</p>
</div>
`).join('');
}
renderUserPage();
并发请求:让速度飞起来
// 同时发起多个请求
async function loadDashboardData() {
try {
// Promise.all并发请求
const [usersRes, ordersRes, statsRes] = await Promise.all([
fetch('/api/dashboard/users'),
fetch('/api/dashboard/orders'),
fetch('/api/dashboard/stats')
]);
// 并行获取所有数据
const [users, orders, stats] = await Promise.all([
usersRes.json(),
ordersRes.json(),
statsRes.json()
]);
// 渲染数据
renderUsers(users);
renderOrders(orders);
renderStats(stats);
} catch (error) {
console.error('加载仪表板数据失败:', error);
showError('数据加载失败,请刷新页面');
}
}
// 竞态条件处理
let fetchAbortController = null;
async function searchUsers(query) {
// 取消之前的请求
if (fetchAbortController) {
fetchAbortController.abort();
}
// 创建新的控制器
fetchAbortController = new AbortController();
try {
const response = await fetch(
`/api/users/search?q=${encodeURIComponent(query)}`,
{ signal: fetchAbortController.signal }
);
const users = await response.json();
renderSearchResults(users);
} catch (error) {
if (error.name === 'AbortError') {
// 请求被取消,忽略
console.log('请求已取消');
} else {
console.error('搜索失败:', error);
}
}
}
// 绑定输入事件
document.getElementById('search-input').addEventListener('input', (e) => {
searchUsers(e.target.value);
});
完整的实战示例:一个任务管理应用
让我们把这些知识整合到一个完整的应用中:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>任务管理系统</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.task-form {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #555;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-danger {
background: #dc3545;
color: white;
padding: 5px 10px;
font-size: 12px;
}
.task-list {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.task-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
}
.task-item:last-child {
border-bottom: none;
}
.task-info h3 {
font-size: 16px;
color: #333;
}
.task-info p {
font-size: 14px;
color: #666;
margin-top: 5px;
}
.task-info .priority {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
margin-top: 5px;
}
.priority-high {
background: #ffebee;
color: #c62828;
}
.priority-medium {
background: #fff3e0;
color: #ef6c00;
}
.priority-low {
background: #e8f5e9;
color: #2e7d32;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error {
text-align: center;
padding: 40px;
color: #dc3545;
}
.empty {
text-align: center;
padding: 40px;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<h1>任务管理系统</h1>
<!-- 任务表单 -->
<div class="task-form">
<div class="form-group">
<label for="task-title">任务标题</label>
<input type="text" id="task-title" placeholder="输入任务标题">
</div>
<div class="form-group">
<label for="task-desc">任务描述</label>
<textarea id="task-desc" placeholder="输入任务描述"></textarea>
</div>
<div class="form-group">
<label for="task-priority">优先级</label>
<select id="task-priority">
<option value="low">低</option>
<option value="medium">中</option>
<option value="high">高</option>
</select>
</div>
<button class="btn btn-primary" id="add-task-btn">添加任务</button>
</div>
<!-- 任务列表 -->
<div class="task-list">
<h2 style="margin-bottom: 15px; color: #333;">任务列表</h2>
<div id="task-container">
<div class="loading">加载中...</div>
</div>
</div>
</div>
<script>
// API基础配置
const API_BASE = 'https://api.example.com';
// 工具函数
function formatDate(dateString) {
return new Date(dateString).toLocaleString('zh-CN');
}
function showError(message) {
const container = document.getElementById('task-container');
container.innerHTML = `<div class="error">${message}</div>`;
}
// 获取所有任务
async function fetchTasks() {
const container = document.getElementById('task-container');
container.innerHTML = '<div class="loading">加载中...</div>';
try {
const response = await fetch(`${API_BASE}/tasks`);
if (!response.ok) {
throw new Error(`获取任务失败: ${response.status}`);
}
const tasks = await response.json();
renderTasks(tasks);
} catch (error) {
console.error('获取任务失败:', error);
showError('加载任务列表失败,请刷新页面重试');
}
}
// 渲染任务列表
function renderTasks(tasks) {
const container = document.getElementById('task-container');
if (!tasks || tasks.length === 0) {
container.innerHTML = '<div class="empty">暂无任务,点击右上角按钮添加</div>';
return;
}
container.innerHTML = tasks.map(task => `
<div class="task-item" data-id="${task.id}">
<div class="task-info">
<h3>${escapeHtml(task.title)}</h3>
<p>${escapeHtml(task.description || '无描述')}</p>
<span class="priority priority-${task.priority}">${getPriorityText(task.priority)}</span>
<span style="float:right; color:#999; font-size:12px;">${formatDate(task.createdAt)}</span>
</div>
<button class="btn btn-danger" onclick="deleteTask(${task.id})">删除</button>
</div>
`).join('');
}
// 添加任务
async function addTask(title, description, priority) {
const btn = document.getElementById('add-task-btn');
btn.disabled = true;
btn.textContent = '添加中...';
try {
const response = await fetch(`${API_BASE}/tasks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, description, priority })
});
if (!response.ok) {
throw new Error(`添加任务失败: ${response.status}`);
}
const newTask = await response.json();
// 刷新任务列表
fetchTasks();
// 清空表单
document.getElementById('task-title').value = '';
document.getElementById('task-desc').value = '';
document.getElementById('task-priority').value = 'medium';
} catch (error) {
console.error('添加任务失败:', error);
alert('添加任务失败: ' + error.message);
} finally {
btn.disabled = false;
btn.textContent = '添加任务';
}
}
// 删除任务
async function deleteTask(taskId) {
if (!confirm('确定要删除这个任务吗?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/tasks/${taskId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error(`删除任务失败: ${response.status}`);
}
// 刷新任务列表
fetchTasks();
} catch (error) {
console.error('删除任务失败:', error);
alert('删除任务失败: ' + error.message);
}
}
// HTML转义,防止XSS攻击
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 获取优先级文本
function getPriorityText(priority) {
const map = {
'high': '高优先级',
'medium': '中优先级',
'low': '低优先级'
};
return map[priority] || '普通';
}
// 绑定事件
document.getElementById('add-task-btn').addEventListener('click', () => {
const title = document.getElementById('task-title').value.trim();
const description = document.getElementById('task-desc').value.trim();
const priority = document.getElementById('task-priority').value;
if (!title) {
alert('请输入任务标题');
return;
}
addTask(title, description, priority);
});
// 页面加载时获取任务列表
document.addEventListener('DOMContentLoaded', fetchTasks);
</script>
</body>
</html>
常见陷阱与解决方案
1. 忘记处理网络错误
// 错误做法
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data));
// 问题:如果网络错误,代码会静默失败
// 正确做法
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => {
console.error('请求失败:', error);
// 显示用户友好的错误提示
});
2. 忽略超时处理
// 添加超时控制
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('请求超时,请稍后重试');
}
throw error;
}
}
// 使用示例
fetchWithTimeout('/api/slow-endpoint')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
console.error('请求失败:', error.message);
});
3. 重复提交表单
// 防止重复提交
function preventDoubleSubmit(form) {
const submitBtn = form.querySelector('button[type="submit"]');
let isSubmitting = false;
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (isSubmitting) return; // 防止重复提交
isSubmitting = true;
submitBtn.disabled = true;
submitBtn.textContent = '提交中...';
try {
const formData = new FormData(form);
const response = await fetch(form.action, {
method: 'POST',
body: formData
});
if (response.ok) {
form.reset();
alert('提交成功!');
} else {
alert('提交失败,请重试');
}
} catch (error) {
console.error('提交失败:', error);
alert('网络错误,请检查连接后重试');
} finally {
isSubmitting = false;
submitBtn.disabled = false;
submitBtn.textContent = '提交';
}
});
}
// 应用
preventDoubleSubmit(document.getElementById('myForm'));
4. 数据解析错误
// 安全的JSON解析
async function safeJsonFetch(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
// 检查Content-Type
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error('响应不是JSON格式');
}
const text = await response.text();
// 尝试解析JSON
try {
return JSON.parse(text);
} catch (parseError) {
throw new Error('JSON解析失败');
}
} catch (error) {
console.error('请求失败:', error);
throw error;
}
}
调试技巧:让问题无处藏身
// 网络请求调试工具
const debugFetch = async (url, options = {}) => {
console.log(`[请求] ${options.method || 'GET'} ${url}`);
console.log('[请求头]', options.headers);
console.log('[请求体]', options.body);
try {
const startTime = Date.now();
const response = await fetch(url, options);
const duration = Date.now() - startTime;
console.log(`[响应] 状态: ${response.status}, 耗时: ${duration}ms`);
console.log('[响应头]', Object.fromEntries(response.headers));
const text = await response.text();
console.log('[响应体]', text);
return {
status: response.status,
headers: response.headers,
data: JSON.parse(text)
};
} catch (error) {
console.error('[错误]', error);
throw error;
}
};
总结
AJAX是现代Web开发的基础,掌握它就像获得了一把打开实时数据世界的钥匙。记住这些关键点:
- GET用于获取数据,POST用于提交数据
- 永远要处理错误,不要假设请求会成功
- 使用
async/await让异步代码更易读 - 注意跨域问题,开发时用代理,生产环境配置CORS
- 防止重复提交,给用户良好的反馈
希望这篇文章能帮你建立起对AJAX的全面理解。记住,最好的学习方式就是动手实践,把你的项目用AJAX改造成动态的、响应式的体验吧!
