AJAX 请求方法完全指南 GET 与 POST 的选择技巧实际应用场景与常见错误排查


最近有个刚入行的小伙伴问我,说做项目的时候每次发请求都纠结半天:到底用 GET 还是 POST?有时候明明知道要用 POST,结果忘了加请求头,服务器直接返回 405 Method Not Allowed,调试了半天才发现是小细节没注意到。今天咱们就来好好聊聊 AJAX 请求这件事,把 GET 和 POST 的前前后后、左左右右都掰扯清楚。


先搞明白:AJAX 到底是什么

AJAX(Asynchronous JavaScript and XML)这个名字虽然带着”XML”,但现在的实际应用场景里,大部分时候我们用的都是 JSON 数据。它的核心思想很简单:页面不用刷新,就能和服务器交换数据

你可以把它想象成你在餐厅吃饭,不需要每次点菜都跑回柜台重新排队,而是叫服务员过来记录你的需求,厨房做好了直接端过来就行。网页也是这个道理,用户在该干嘛干嘛,后台悄悄跟服务器打交道。

下面这张图能直观地看出传统方式和 AJAX 方式的区别:

传统方式:
用户点击按钮 → 页面跳转/刷新 → 服务器返回新页面 → 用户看到结果

AJAX 方式:
用户点击按钮 → JavaScript 发请求 → 服务器返回数据 → JavaScript 更新页面局部

在写代码之前,咱们先搞清楚浏览器原生提供的两种发请求的方式:XMLHttpRequestFetch API。前者是老前辈,兼容性无敌;后者是新秀,语法更简洁,而且支持 Promise。


GET 请求:最简单也最容易用错

基本用法

GET 请求是最常用的 HTTP 方法,它的设计初衷就是从服务器获取数据。用 XMLHttpRequest 写一个 GET 请求长这样:

// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest();

// 初始化请求,指定方法和 URL
xhr.open('GET', 'https://api.example.com/users?page=1&limit=10');

// 设置请求头(GET 请求一般不需要特殊的请求头)
xhr.setRequestHeader('Accept', 'application/json');

// 监听响应
xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
        const data = JSON.parse(xhr.responseText);
        console.log('获取到的数据:', data);
        // 在这里更新页面
    } else {
        console.error('请求失败,状态码:', xhr.status);
    }
};

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

// 发送请求
xhr.send();

Fetch API 写就简洁多了:

fetch('https://api.example.com/users?page=1&limit=10', {
    method: 'GET',
    headers: {
        'Accept': 'application/json',
        'Authorization': 'Bearer your_token_here'
    }
})
.then(response => {
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
})
.then(data => {
    console.log('获取到的数据:', data);
})
.catch(error => {
    console.error('请求失败:', error);
});

GET 请求的几个关键特性

  1. 数据放在 URL 里:参数以 query string 的形式跟在 URL 后面,比如 ?key1=value1&key2=value2
  2. 有长度限制:大多数浏览器对 URL 长度有限制,通常在 2000 个字符左右(IE 甚至更少,只有 1024 字符)
  3. 会被缓存:浏览器会对 GET 请求的结果进行缓存,相同 URL 的请求可能直接返回缓存结果
  4. 会留下记录:URL 会出现在浏览器历史、服务器日志、代理服务器日志里

什么时候该用 GET

  • 查询数据:比如搜索、分页、筛选列表
  • 幂等操作:多次请求结果应该是一样的,不会修改服务器状态
  • 数据量小:参数简单,不会超过 URL 长度限制
  • 需要分享链接:因为参数在 URL 里,方便复制和分享

举个例子,做一个搜索功能,用 GET 就很合适:

// 搜索用户
function searchUsers(keyword) {
    fetch(`/api/users/search?q=${encodeURIComponent(keyword)}`)
        .then(res => res.json())
        .then(data => {
            // 渲染搜索结果
            renderSearchResults(data.users);
        });
}

这样用户甚至可以把搜索结果的 URL 发给别人,别人打开就能看到同样的搜索结果。


POST 请求:更强大也更复杂

基本用法

POST 请求用来向服务器提交数据,比如创建资源、提交表单、上传文件等。数据放在请求体(Request Body)里,而不是 URL 里。

// 用 XMLHttpRequest 发送 POST 请求
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.example.com/users');
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.setRequestHeader('Accept', 'application/json');

xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
        const data = JSON.parse(xhr.responseText);
        console.log('创建成功,返回的数据:', data);
    } else {
        console.error('请求失败,状态码:', xhr.status);
    }
};

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

// 请求体数据
const userData = {
    name: '张三',
    email: 'zhangsan@example.com',
    age: 28
};

xhr.send(JSON.stringify(userData));

Fetch API

fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json;charset=UTF-8',
        'Accept': 'application/json',
        'Authorization': 'Bearer your_token_here'
    },
    body: JSON.stringify({
        name: '张三',
        email: 'zhangsan@example.com',
        age: 28
    })
})
.then(response => {
    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
})
.then(data => {
    console.log('创建成功:', data);
})
.catch(error => {
    console.error('请求失败:', error);
});

POST 请求的几个关键特性

  1. 数据在请求体里:不会暴露在 URL 中,相对更安全
  2. 没有长度限制:理论上可以发送任意大小的数据(实际受服务器配置限制)
  3. 不会被缓存:POST 请求默认不会被浏览器缓存
  4. 不会留下记录:参数不会出现在浏览器历史或服务器日志的 URL 中

什么时候该用 POST

  • 创建资源:比如注册用户、发表文章
  • 修改敏感数据:比如修改密码、转账
  • 数据量大:参数很多或者包含大段文本、文件上传
  • 非幂等操作:多次请求会产生不同结果

举个实际点的例子,比如做一个注册功能:

async function registerUser(userData) {
    try {
        const response = await fetch('/api/auth/register', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                username: userData.username,
                email: userData.email,
                password: userData.password
            })
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.message || '注册失败');
        }

        const result = await response.json();
        console.log('注册成功:', result);
        return result;
    } catch (error) {
        console.error('注册出错:', error);
        throw error;
    }
}

这里用 POST 而不是 GET,原因很明显:密码放 URL 里太危险了,而且注册是一个会修改服务器状态的操作。


GET vs POST:到底该怎么选

这个问题问得最多,但实际上答案并没有那么绝对。我来给你梳理几个判断维度:

1. 操作语义

这是最根本的判断依据。HTTP 协议本身对 GET 和 POST 有明确的语义定义:

维度 GET POST
目的 获取数据 提交数据
安全性 幂等(多次结果一致) 非幂等(多次可能产生不同结果)
副作用 不应修改服务器状态 可以修改服务器状态

所以简单说:只读操作用 GET,写操作用 POST。这不只是惯例,而是 RESTful 设计的基本原则。

2. 数据量大小

如果你的请求要传很多参数,或者参数里包含大段文本(比如富文本编辑器里的内容),用 POST 更合适。GET 请求的 URL 长度受限,而且太长了也不好看。

// 这个用 GET 就不合适了,参数太长
const longContent = '这是一段很长的文本内容,可能有好几KB甚至更多...';

// 用 POST 发送
fetch('/api/articles', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        title: '我的文章',
        content: longContent,
        tags: ['技术', '前端', 'AJAX']
    })
});

3. 安全性考虑

GET 请求的参数在 URL 里,意味着:

  • 会出现在浏览器历史记录里
  • 会出现在服务器日志里
  • 可能被referrer头传递给第三方网站
  • 容易被截获(虽然 HTTPS 可以缓解这个问题)

所以敏感数据千万不要用 GET。比如密码、身份证号码、手机号这些,一律用 POST,而且请求体也要加密或者用 HTTPS。

4. 缓存需求

如果你希望结果可以被缓存(比如搜索结果、列表数据),GET 是更好的选择,因为浏览器和 CDN 都能自动缓存 GET 请求的结果。POST 请求默认不会被缓存。

// 搜索建议可以用 GET,方便缓存
fetch('/api/suggestions?q=' + encodeURIComponent(query))
    .then(res => res.json())
    .then(data => {
        renderSuggestions(data);
    });

实际项目中的常见场景

下面这些都是我实际项目中经常遇到的,你看看有没有共鸣:

场景一:分页列表

// GET 请求,参数清晰,可缓存
fetch(`/api/products?page=${page}&limit=${limit}&category=${category}`)
    .then(res => res.json())
    .then(data => renderProductList(data));

场景二:用户登录

// POST 请求,密码不能暴露在 URL 里
fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        username: username,
        password: password
    })
});

场景三:上传头像

// POST 请求,携带文件数据
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);

fetch('/api/users/avatar', {
    method: 'POST',
    body: formData
    // 注意:用 FormData 时不要手动设置 Content-Type,浏览器会自动设置
});

场景四:删除文章

// 这里有个争议点:DELETE 还是 POST?
// RESTful 风格推荐用 DELETE,但很多团队因为兼容性问题用 POST
fetch('/api/articles/123', {
    method: 'DELETE'
    // 或者 method: 'POST',header 里加 X-HTTP-Method-Override: DELETE
});

常见错误排查

这部分是重点,很多坑我都踩过,分享出来希望能帮你少走弯路。

错误一:405 Method Not Allowed

这个错误最常见的原因就是用错了请求方法。比如服务器接口设计成只接受 POST,你偏偏用了 GET,或者反过来。

排查方法:

  1. 仔细看接口文档,确认期望的方法
  2. 检查代码里的 method 参数
  3. 如果是前后端分离项目,检查 CORS 预检请求(OPTIONS)是否通过
// 错误示例:接口要求 POST,用了 GET
fetch('/api/users', {
    method: 'GET',  // 错误!应该是 POST
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: '李四' })  // GET 不应该有 body
});

错误二:400 Bad Request

参数不对、格式错误、缺少必填项都可能触发这个错误。

常见原因:

  • JSON 格式错误
  • 缺少必填字段
  • 字段类型不对(比如数字传了字符串)
  • 请求头不对

排查方法:

  1. 检查浏览器开发者工具里的 Network 面板,看请求体和响应体
  2. 逐字段校验参数
  3. 看响应里的错误信息(如果有的话)
// 错误示例:参数格式不对
fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        name: '张三',
        age: '28'  // 错误!age 应该是数字,不是字符串
    })
});

错误三:CORS 错误

跨域问题是前端开发者的噩梦之一。最典型的报错是:

Access to fetch at 'https://api.example.com' from origin 'https://your-site.com' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present 
on the requested resource.

根本原因:浏览器的同源策略。简单说,你的前端页面和 API 服务器不在同一个源(协议+域名+端口),浏览器默认不允许跨域请求。

解决方案有两种思路:

思路一:后端配置 CORS 响应头

// Node.js + Express 示例
app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'https://your-site.com');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    
    if (req.method === 'OPTIONS') {
        return res.sendStatus(200);  // 预检请求直接返回
    }
    next();
});

思路二:前端配置代理(开发环境)

// Vite 配置示例
// vite.config.js
export default {
    server: {
        proxy: {
            '/api': {
                target: 'https://api.example.com',
                changeOrigin: true
            }
        }
    }
};

错误四:数据没有发送成功

这种情况经常发生在用 FormData 上传文件的时候。

// 错误示例:手动设置了 Content-Type
const formData = new FormData();
formData.append('file', fileInput.files[0]);

fetch('/api/upload', {
    method: 'POST',
    headers: {
        'Content-Type': 'multipart/form-data'  // 错误!浏览器会自动设置,包含 boundary
    },
    body: formData
});

浏览器在发送 multipart/form-data 请求时,会自动在 Content-Type 里加上 boundary 参数,手动设置反而会破坏这个机制。正确写法是不设置 Content-Type:

// 正确写法
const formData = new FormData();
formData.append('file', fileInput.files[0]);

fetch('/api/upload', {
    method: 'POST',
    body: formData  // 浏览器会自动设置正确的 Content-Type
});

错误五:请求挂起,一直没有响应

可能的原因:

  1. 服务器没收到请求(网络问题、防火墙、代理问题)
  2. 服务器处理太慢,超时了
  3. 客户端代码有问题,请求没发出去

排查方法:

  1. 打开浏览器的开发者工具,看 Network 面板
  2. 检查请求是否已经发出,状态是什么
  3. 如果是超时问题,设置合理的 timeout
// 设置超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);  // 5秒超时

fetch('/api/slow-endpoint', {
    method: 'GET',
    signal: controller.signal
})
.then(res => res.json())
.then(data => console.log(data))
.catch(error => {
    if (error.name === 'AbortError') {
        console.error('请求超时了');
    } else {
        console.error('请求失败:', error);
    }
});

错误六:中文乱码

这个问题的根源通常是编码不一致。服务器返回的是 UTF-8,但浏览器当成 GBK 解析了。

解决方法:

  1. 确保服务器返回的响应头里有 Content-Type: application/json; charset=utf-8
  2. 或者在请求头里明确指定 Accept-Charset
fetch('/api/data', {
    headers: {
        'Accept': 'application/json; charset=utf-8'
    }
});

进阶技巧:封装一个通用的请求函数

实际项目中,我们不会每次都写那么长的代码。通常会把请求逻辑封装起来,复用起来更方便。

// request.js
class HttpClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
        this.defaults = {
            timeout: 10000,
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            }
        };
    }

    // 通用的请求方法
    async request(method, url, options = {}) {
        const config = {
            ...this.defaults,
            ...options,
            method: method.toUpperCase(),
            url: this.baseURL + url
        };

        // 处理请求体
        if (config.body && !(config.body instanceof FormData)) {
            config.body = JSON.stringify(config.body);
        }

        // 处理超时
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), config.timeout);

        try {
            const response = await fetch(config.url, {
                ...config,
                signal: controller.signal
            });

            clearTimeout(timeoutId);

            if (!response.ok) {
                const errorData = await response.json().catch(() => ({}));
                throw new HttpError(response.status, errorData.message || '请求失败', errorData);
            }

            // 204 No Content 没有返回体
            if (response.status === 204) {
                return null;
            }

            return await response.json();
        } catch (error) {
            clearTimeout(timeoutId);
            throw error;
        }
    }

    // 便捷方法
    get(url, options) {
        return this.request('GET', url, options);
    }

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

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

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

// 自定义错误类
class HttpError extends Error {
    constructor(status, message, data) {
        super(message);
        this.name = 'HttpError';
        this.status = status;
        this.data = data;
    }
}

// 使用示例
const api = new HttpClient('https://api.example.com');

// GET 请求
api.get('/users?page=1')
    .then(users => console.log(users))
    .catch(error => console.error(error));

// POST 请求
api.post('/users', {
    name: '王五',
    email: 'wangwu@example.com'
})
    .then(user => console.log(user))
    .catch(error => console.error(error));

// 文件上传
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);

api.post('/users/avatar', formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
    timeout: 30000  // 上传超时时间长一点
})
    .then(result => console.log(result))
    .catch(error => console.error(error));

这样封装之后,代码整洁很多,也方便统一处理错误、添加请求拦截器、管理 token 等。


几个容易被忽视的小细节

1. GET 请求也可以有请求体,但很多服务器会忽略它

虽然 HTTP 规范没有明确禁止 GET 请求带 body,但大多数服务器会忽略 GET 请求的请求体。所以如果你发现 GET 请求发了数据但服务器收不到,别奇怪,这是正常行为。

2. POST 请求的 Content-Type 决定数据格式

// 发送 JSON
headers: { 'Content-Type': 'application/json' }

// 发送表单数据
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }

// 上传文件
// 不要手动设置 Content-Type,让浏览器自动处理
headers: { 'Content-Type': 'multipart/form-data' }  // 错误!

3. 预检请求(Preflight)是 CORS 的一部分

当请求满足以下条件之一时,浏览器会先发送一个 OPTIONS 请求(预检请求),得到服务器允许后再发真实请求:

  • 使用了 GET、POST、HEAD 以外的方法
  • 设置了自定义请求头
  • Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data、text/plain 之一

这就是为什么有时候你会看到 Network 面板里有两条请求,第一条是 OPTIONS,第二条才是你的真实请求。

4. 请求取消很重要

在现代单页应用里,用户可能快速切换页面或触发多个请求。如果前一个请求还没回来,后一个请求就发了,可能会出问题。用 AbortController 可以取消请求:

let currentController = null;

function searchUsers(query) {
    // 取消上一个请求
    if (currentController) {
        currentController.abort();
    }

    currentController = new AbortController();

    fetch(`/api/users/search?q=${encodeURIComponent(query)}`, {
        signal: currentController.signal
    })
    .then(res => res.json())
    .then(data => renderResults(data))
    .catch(error => {
        if (error.name !== 'AbortError') {
            console.error('搜索失败:', error);
        }
    });
}

最后说两句

AJAX 请求这件事,说到底就是理解 HTTP 协议的基本原理,然后根据场景选择合适的方法。GET 和 POST 没有谁比谁高级,关键是用对地方

实际开发中,我最常犯的错误就是:

  1. 该用 POST 的时候用了 GET,结果敏感数据暴露在 URL 里
  2. 忘记处理请求超时的情况
  3. 上传文件时手动设置 Content-Type

希望这篇文章能帮你避开这些坑。如果还有问题,欢迎在评论区交流,或者直接在开发者工具里调试,看看请求和响应的具体细节,比看十篇文章都管用。

记住, debugging 的时候别慌,Network 面板是你的好朋友,它会把一切都展示给你看。