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改造成动态的、响应式的体验吧!