异步请求超时数据传不全 GET POST 方法选错怎么办 AJAX 请求实用指南

嗨,朋友!你是不是也遇到过这种抓狂的情况:页面上数据死活加载不出来,控制台报个超时错误;或者明明传了参数,后端却收不到;再或者 GET 请求参数传了中文直接乱码…… 别急,这篇文章就是把这些问题一次性说清楚,让你以后写 AJAX 请求不再踩坑。


一、先搞懂一件事:AJAX 到底是什么

AJAX 的全称是 Asynchronous JavaScript and XML,说白了就是:不用刷新整个页面,就能跟服务器偷偷交换数据。

你平时刷微博、看微信,那些不用跳转页面就能加载新内容,背后基本都是 AJAX 在干活。它最核心的工具就是 XMLHttpRequest(简称 XHR),现代浏览器也提供了更简洁的 fetch API。

// 最原始的 XHR 写法(了解一下就行,现在很少直接这么写)
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};
xhr.send();

现在主流写法是用 fetch,干净得多:

fetch('/api/data')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('请求失败了', error));

二、异步请求超时怎么办?

超时这个事,说白了就是服务器 response 回来的太慢,或者网络卡了。你有三种方式处理它:

方式一:给 fetch 设置超时(原生支持不太好,得用 AbortController)

function fetchWithTimeout(url, options = {}, timeout = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    return fetch(url, {
        ...options,
        signal: controller.signal
    })
    .then(response => {
        clearTimeout(timeoutId);
        return response;
    })
    .catch(error => {
        if (error.name === 'AbortError') {
            throw new Error('请求超时了,请稍后再试');
        }
        throw error;
    });
}

// 使用
fetchWithTimeout('/api/slow-data', {}, 3000)
    .then(res => res.json())
    .then(data => console.log(data))
    .catch(err => console.error(err.message));

方式二:用 axios(推荐!)

axios 内置了超时配置,写起来最舒服:

import axios from 'axios';

axios.get('/api/slow-data', {
    timeout: 5000,  // 5秒超时
    headers: {
        'Content-Type': 'application/json'
    }
})
.then(response => {
    console.log('成功拿到数据', response.data);
})
.catch(error => {
    if (axios.isAxiosError(error) && error.code === 'ECONNABORTED') {
        console.error('请求超时了!');
    } else {
        console.error('其他错误', error.message);
    }
});

方式三:XHR 的 timeout 属性(老项目还在用)

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.timeout = 5000;  // 设置超时时间(毫秒)

xhr.ontimeout = function() {
    console.error('请求超时!');
};

xhr.onerror = function() {
    console.error('网络错误!');
};

xhr.onload = function() {
    if (xhr.status === 200) {
        console.log('请求成功', xhr.responseText);
    } else {
        console.error('服务器返回错误状态码', xhr.status);
    }
};

xhr.send();

小贴士:超时时间设置多少合适?一般 3~10 秒比较合理。太短了容易误判,太长了用户等得不耐烦。如果是图片加载、文件下载这种,可以适当给长一点。


三、数据传不全?99% 是这几个原因

原因一:Content-Type 没设对

这是最常见的问题!你的浏览器和服务器之间有个”协议”,就是 Content-Type。设错了,服务器就解析不了你的数据。

// ❌ 错误示范:用了 JSON 格式传数据,但没告诉服务器
fetch('/api/user', {
    method: 'POST',
    headers: {
        // 忘记设 Content-Type 了!
    },
    body: JSON.stringify({ name: '张三', age: 25 })
});

// ✅ 正确写法:告诉服务器我传的是 JSON
fetch('/api/user', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: '张三', age: 25 })
});

原因二:表单数据用错格式

有时候你要传的是表单数据(比如登录、注册),不能直接用 JSON.stringify,要用 URLSearchParams:

// ✅ 传表单数据的标准写法
const formData = new URLSearchParams();
formData.append('username', '张三');
formData.append('password', '123456');
formData.append('remember', 'true');

fetch('/api/login', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: formData.toString()
})
.then(res => res.json())
.then(data => console.log('登录结果', data));

或者直接用 FormData 对象(适合上传文件):

// 上传文件的写法
const uploadForm = new FormData();
uploadForm.append('avatar', fileInput.files[0]);
uploadForm.append('userId', '12345');

fetch('/api/upload', {
    method: 'POST',
    body: uploadForm
    // 注意:用 FormData 的时候不要手动设 Content-Type
    // 浏览器会自动帮你加上 boundary 参数
})
.then(res => res.json())
.then(data => console.log(data));

原因三:参数嵌套太深,序列化出问题

// ❌ 直接 stringify 嵌套对象可能出问题
const user = {
    name: '张三',
    profile: {
        age: 25,
        address: {
            city: '北京',
            district: '朝阳'
        }
    }
};

// ✅ 用 qs 库或者手动展开
import qs from 'qs';

const encoded = qs.stringify({
    name: '张三',
    'profile.age': 25,
    'profile.address.city': '北京'
});

// 或者用 axios 自带的序列化
axios.post('/api/user', user, {
    headers: { 'Content-Type': 'application/json' }
});

原因四:中文乱码

// 后端如果不支持中文,传中文参数会乱码
// 解决方案:传之前 encode
const username = '张三';
const encoded = encodeURIComponent(username);

fetch(`/api/search?name=${encoded}`)
    .then(res => res.json())
    .then(data => console.log(data));

四、GET 和 POST,到底该选哪个?

这是一个经典问题,很多人搞混。记住这个原则:

GET POST
用途 查询数据(查) 提交/修改数据(增删改)
参数位置 URL 后面(?key=value) 请求体(body)里
参数长度 有限制(一般几千字符) 基本无限制
安全性 参数可见,不安全 参数在 body 里,相对安全
缓存 可被缓存 默认不缓存
幂等性 幂等(多次结果一样) 非幂等

GET 的正确用法

// 搜索用户
fetch('/api/users?name=张三&age=25&page=1')
    .then(res => res.json())
    .then(data => console.log(data));

// 或者用模板字符串更清晰
const searchParams = new URLSearchParams({
    name: '张三',
    age: 25,
    page: 1,
    pageSize: 10
});

fetch(`/api/users?${searchParams}`)
    .then(res => res.json())
    .then(data => console.log(data));

POST 的正确用法

// 创建用户
fetch('/api/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: '张三',
        age: 25,
        email: 'zhangsan@example.com'
    })
})
.then(res => res.json())
.then(data => console.log('创建成功', data));

一个容易踩的坑:用 GET 传大量数据

// ❌ 错误:GET 请求 body 没意义,很多服务器会忽略
fetch('/api/search', {
    method: 'GET',
    body: JSON.stringify({ keywords: 'AJAX', page: 1 })
});

// ✅ 正确:GET 请求参数放 URL 里
const params = new URLSearchParams({
    keywords: 'AJAX',
    page: 1
});
fetch(`/api/search?${params}`);

五、AJAX 请求的完整最佳实践

1. 封装一个自己的请求工具

别每次都写一遍 fetch,封装一个通用的:

// request.js
class Request {
    constructor(baseURL = '', timeout = 10000) {
        this.baseURL = baseURL;
        this.timeout = timeout;
    }

    async request(method, url, data = null, options = {}) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), this.timeout);

        const config = {
            method: method.toUpperCase(),
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            },
            signal: controller.signal,
            ...options
        };

        if (data && method.toLowerCase() !== 'get') {
            config.body = JSON.stringify(data);
        }

        try {
            const response = await fetch(this.baseURL + url, config);
            clearTimeout(timeoutId);

            if (!response.ok) {
                throw new Error(`HTTP 错误!状态码:${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) {
            clearTimeout(timeoutId);
            if (error.name === 'AbortError') {
                throw new Error('请求超时,请检查网络连接');
            }
            throw error;
        }
    }

    get(url, data, options) {
        const params = new URLSearchParams(data).toString();
        return this.request('get', params ? `${url}?${params}` : url, null, options);
    }

    post(url, data, options) {
        return this.request('post', url, data, options);
    }

    put(url, data, options) {
        return this.request('put', url, data, options);
    }

    delete(url, data, options) {
        return this.request('delete', url, data, options);
    }
}

// 导出单例
const request = new Request('https://api.example.com', 8000);
export default request;

使用方式超简单:

import request from './request';

// GET 请求
request.get('/users', { page: 1, pageSize: 10 })
    .then(users => console.log(users))
    .catch(err => console.error(err));

// POST 请求
request.post('/users', {
    name: '张三',
    email: 'zhangsan@example.com'
})
.then(result => console.log(result))
.catch(err => console.error(err));

2. 加个请求拦截器(类似 axios 的拦截器)

// 全局请求拦截:自动加 token
fetch('/api/user', {
    headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`
    }
});

// 或者统一处理
const originalFetch = window.fetch;
window.fetch = async function(...args) {
    const [url, config] = args;
    
    // 自动加 token
    if (!config?.headers) config = { ...config, headers: {} };
    config.headers = {
        'Authorization': `Bearer ${localStorage.getItem('token')}`,
        ...config.headers
    };
    
    return originalFetch.call(this, url, config);
};

3. 错误处理和重试机制

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
    let lastError;
    
    for (let i = 0; i < maxRetries; i++) {
        try {
            const response = await fetch(url, options);
            if (response.ok) {
                return await response.json();
            }
            
            // 4xx 错误不重试
            if (response.status >= 400 && response.status < 500) {
                throw new Error(`客户端错误:${response.status}`);
            }
            
            throw new Error(`服务器错误:${response.status}`);
        } catch (error) {
            lastError = error;
            console.warn(`请求失败,第 ${i + 1} 次重试`, error.message);
            
            // 等待一段时间后重试(指数退避)
            await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
        }
    }
    
    throw lastError;
}

// 使用
fetchWithRetry('/api/some-data', { timeout: 5000 })
    .then(data => console.log(data))
    .catch(err => console.error('重试多次后仍失败', err));

六、实际项目中的常见坑和解决方案

坑一:跨域问题(CORS)

Access to fetch at 'http://api.example.com' from origin 'http://localhost:3000' 
has been blocked by CORS policy

这个错误说明前后端域名不一样,浏览器安全策略拦截了请求。解决方法在后端,前端改不了:

// 后端 Node.js 示例(Express)
const cors = require('cors');
app.use(cors({
    origin: 'http://localhost:3000',  // 允许的前端域名
    credentials: true  // 如果需要带 cookie
}));

坑二:大文件上传超时

// 上传大文件,超时时间要设长一点
const formData = new FormData();
formData.append('file', largeFile);

axios.post('/api/upload', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
    timeout: 60000,  // 60秒超时
    onUploadProgress: (progressEvent) => {
        // 显示上传进度
        const percent = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
        );
        console.log(`上传进度:${percent}%`);
    }
});

坑三:并发请求太多把服务器打崩了

// 用 Promise.all 同时发多个请求要小心
const [users, orders, products] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/orders').then(r => r.json()),
    fetch('/api/products').then(r => r.json())
]);

// 如果请求很多,可以用并发控制
async function concurrentFetch(urls, concurrency = 3) {
    const results = [];
    let index = 0;
    
    async function worker() {
        while (index < urls.length) {
            const url = urls[index++];
            try {
                const response = await fetch(url);
                results.push(await response.json());
            } catch (error) {
                console.error(`请求 ${url} 失败`, error);
            }
        }
    }
    
    const workers = Array.from({ length: concurrency }, () => worker());
    await Promise.all(workers);
    return results;
}

七、快速排查清单

下次请求出问题,按这个顺序检查:

  • [ ] 超时了吗? 检查 timeout 设置,看网络是否正常
  • [ ] Content-Type 对吗? JSON 请求要设 application/json
  • [ ] 参数序列化了吗? 表单数据用 URLSearchParams,不要直接传对象
  • [ ] GET/POST 选对了吗? 查数据用 GET,改数据用 POST
  • [ ] 中文编码了吗? 用 encodeURIComponent 处理中文参数
  • [ ] 跨域了吗? 检查后端是否配置了 CORS
  • [ ] Token 带了吗? 检查 Authorization 头
  • [ ] 后端收到数据了吗? 看后端日志,确认不是后端的问题

写到这里,你应该对 AJAX 请求有了比较全面的认识。其实这些坑都不是什么高深的问题,大部分都是粗心或者不了解细节导致的。多写几次,注意一下这些细节,以后写请求就不会再踩坑了。

如果你还有什么具体问题,欢迎在评论区留言,咱们一起讨论!