3、Ajax与Fetch API:原生Ajax实现、Fetch API使用、跨域问题与CORS解决方案

说到前端数据交互,Ajax 和 Fetch 是绕不开的两个核心工具。我刚开始做前端那会儿,还是 jQuery 的 $.ajax 一统天下。后来原生 Fetch 出来,我第一时间就切过去了——不是因为跟风,而是它确实更干净、更符合现代 JavaScript 的写法。

这一节,咱们就把原生 Ajax、Fetch API 以及跨域问题彻底讲透。你想想看,搞懂了这些,以后跟后端对接基本不会卡壳。

3.1 原生Ajax实现

Ajax 全称是 Asynchronous JavaScript and XML。说白了,就是让页面在不刷新的情况下,偷偷跟服务器要数据。我早期做的一个后台管理系统,所有表格操作都靠 Ajax 完成,用户体验比传统表单提交好太多了。

原生 Ajax 的核心是 XMLHttpRequest 对象。别看它名字里带 XML,现在传 JSON 才是主流。

一个标准的 GET 请求长这样:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/users', true);
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        const data = JSON.parse(xhr.responseText);
        console.log('拿到数据了:', data);
    }
};
xhr.send();

这里有几个关键点要注意:

  • readyState:0 到 4 五个状态,只有 4 表示请求完成
  • status:HTTP 状态码,200 是成功,404 是找不到,500 是服务器挂了
  • responseText:服务器返回的原始字符串
我曾经踩过的坑: 忘记用 JSON.parse 解析 responseText,直接当对象用,结果拿到的是字符串,页面渲染全乱套。排查了半天才发现是类型问题。

POST 请求稍微复杂一点,需要设置请求头:

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.example.com/users', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
    if (xhr.status === 201) {
        console.log('创建成功:', xhr.responseText);
    }
};
const body = JSON.stringify({ name: '张三', age: 28 });
xhr.send(body);

我个人习惯把 Ajax 封装成一个通用函数,避免每次重复写这些样板代码。比如这样:

function ajax(method, url, data) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open(method, url, true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject(new Error(`请求失败: ${xhr.status}`));
            }
        };
        xhr.onerror = () => reject(new Error('网络错误'));
        xhr.send(data ? JSON.stringify(data) : null);
    });
}

// 使用
ajax('GET', '/api/users').then(data => console.log(data));
小提示: 用 Promise 包装 Ajax 后,代码可读性提升很多。而且可以配合 async/await 使用,写起来跟同步代码一样舒服。

3.2 Fetch API使用

Fetch API 是 XMLHttpRequest 的现代替代方案。它基于 Promise 设计,语法更简洁。我第一次用 Fetch 时,最大的感受就是——终于不用再手动处理 readyState 了。

一个简单的 Fetch GET 请求:

fetch('https://api.example.com/users')
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('出错了:', error));

是不是清爽很多?但这里有个容易忽略的点:Fetch 只有在网络错误时才会 reject。HTTP 404、500 这些状态码,它依然会走 then,不会进 catch。

重要: Fetch 不会把 HTTP 错误状态当作异常。你需要手动检查 response.ok 或 response.status。

正确的做法是:

fetch('https://api.example.com/users')
    .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 请求的写法:

fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: '李四', age: 25 })
})
.then(response => response.json())
.then(data => console.log('创建成功:', data))
.catch(error => console.error('创建失败:', error));

我记得有一次,后端要求传 FormData 而不是 JSON。Fetch 处理起来也很方便:

const formData = new FormData();
formData.append('name', '王五');
formData.append('avatar', fileInput.files[0]);

fetch('/api/upload', {
    method: 'POST',
    body: formData  // 注意:不要手动设置 Content-Type,浏览器会自动处理
})
.then(response => response.json())
.then(data => console.log(data));
个人建议: 上传文件时,千万别手动设置 Content-Type 为 multipart/form-data。浏览器会自动生成正确的 boundary,你手动设反而会出问题。这个坑我踩过两次。

3.3 跨域问题与CORS解决方案

跨域,是前端开发中绕不开的痛。你想想看,你的页面在 a.com,却想请求 b.com 的数据。浏览器出于安全考虑,默认是不允许的。这就是同源策略。

为什么会这样?说白了,如果没有同源策略,你打开一个恶意网站,它就能偷偷读取你的银行账户信息。浏览器为了保护你,才设了这个限制。

解决跨域问题,最主流的方式就是 CORS(跨域资源共享)。它需要后端配合,在响应头里加几个字段。

后端需要设置的响应头:

响应头 说明 示例
Access-Control-Allow-Origin 允许的源 https://a.com 或 *
Access-Control-Allow-Methods 允许的 HTTP 方法 GET, POST, PUT, DELETE
Access-Control-Allow-Headers 允许的自定义请求头 Content-Type, Authorization
Access-Control-Allow-Credentials 是否允许携带 Cookie true

比如后端用 Node.js Express 框架,可以这样配置:

app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'https://a.com');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    if (req.method === 'OPTIONS') {
        return res.sendStatus(200);  // 预检请求直接返回
    }
    next();
});
关键点: 浏览器在发送复杂请求(如 PUT、DELETE 或带自定义头的请求)之前,会先发一个 OPTIONS 预检请求。后端必须正确处理这个请求,否则真正的请求发不出去。

前端这边,如果请求需要携带 Cookie,需要额外设置:

fetch('https://b.com/api/data', {
    method: 'GET',
    credentials: 'include'  // 携带 Cookie
})
.then(response => response.json())
.then(data => console.log(data));

同时,后端的 Access-Control-Allow-Origin 不能设为 *,必须指定具体的源。

我曾经踩过的坑: 有一次前端怎么都拿不到 Cookie,排查了半天才发现是后端 Access-Control-Allow-Origin 设成了 *,而 credentials 为 include 时,浏览器要求必须指定具体的源。改过来就好了。

除了 CORS,还有几种跨域方案:

  • JSONP:利用 script 标签不受同源策略限制的特性。但只支持 GET 请求,安全性也差,现在基本不用了。
  • 代理服务器:前端请求同源的后端,后端再去请求目标服务器。开发环境常用 webpack-dev-server 的 proxy 配置。
  • postMessage:用于不同窗口或 iframe 之间的通信。

我个人最推荐 CORS,它是标准方案,功能全面,安全性也有保障。只要后端配合,基本能解决 99% 的跨域场景。

嗯,这一节的内容就到这里。Ajax 和 Fetch 的核心用法、跨域问题的解决方案,你都掌握了吗?下一节我们会聊更进阶的数据交互模式,敬请期待。