Ajax 中的各种常见概念

2025-05-25 前端

一、什么是 Ajax

Ajax(Asynchronous JavaScript and XML)是一种在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术。

┌──────────────────────────────────────────┐
│               浏览器                       │
│                                          │
│   用户操作 → JS 事件 → Ajax 请求           │
│                        ↓                │
│                   服务器处理 ← 后端 API     │
│                        ↓                │
│   JS 接收响应 → 更新 DOM(局部刷新)        │
└──────────────────────────────────────────┘

Ajax 的典型流程

① 用户触发事件(点击、滚动、表单提交……)
② JavaScript 创建 XMLHttpRequest 或调用 fetch()
③ 向服务器发送 HTTP 请求
④ 服务器处理请求并返回数据(JSON/XML/文本)
⑤ JavaScript 接收响应并更新页面

Ajax 不是一门新技术,而是一套现有技术的组合方案,核心是 XMLHttpRequest 或 Fetch API。


二、XMLHttpRequest(XHR)

XMLHttpRequest 是 Ajax 的传统实现方式,所有浏览器都支持。

2.1 基本用法

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

// 2. 配置请求(method, url, async)
xhr.open('GET', '/api/users', true);

// 3. 设置回调
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        const data = JSON.parse(xhr.responseText);
        console.log(data);
    }
};

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

2.2 readyState 的五种状态

readyState 常量 含义
0 UNSENT 已创建 XHR 对象,但未调用 open()
1 OPENED 已调用 open(),尚未调用 send()
2 HEADERS_RECEIVED 已调用 send(),已收到响应头
3 LOADING 正在接收响应体数据
4 DONE 请求完成(成功或失败)
xhr.onreadystatechange = function() {
    console.log('readyState:', xhr.readyState, 'status:', xhr.status);
};

2.3 常用事件

2.4 POST 请求与 FormData

// 发送 JSON
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/users');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ name: '张三', age: 25 }));

// 发送表单数据(文件上传)
const formData = new FormData();
formData.append('name', '张三');
formData.append('avatar', fileInput.files[0]);

const xhr2 = new XMLHttpRequest();
xhr2.open('POST', '/api/upload');
xhr2.send(formData);
// 注意:用 FormData 时不要手动设置 Content-Type,浏览器会自动设

三、Fetch API

Fetch API 是 XMLHttpRequest 的现代替代方案,基于 Promise,语法更简洁。

3.1 基本用法

// GET 请求
fetch('/api/users')
    .then(response => {
        if (!response.ok) {
            throw new Error('HTTP ' + response.status);
        }
        return response.json();
    })
    .then(data => console.log(data))
    .catch(error => console.error('请求失败:', error));

3.2 各种请求方法

// GET
fetch('/api/users?page=1&limit=10');

// POST — 发送 JSON
fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: '张三', age: 25 }),
});

// PUT — 更新资源
fetch('/api/users/123', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: '李四' }),
});

// DELETE — 删除资源
fetch('/api/users/123', { method: 'DELETE' });

// PATCH — 部分更新
fetch('/api/users/123', {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ age: 26 }),
});

3.3 处理不同类型的响应

const res = await fetch('/api/data');

// 解析为 JSON
const json = await res.json();

// 解析为文本
const text = await res.text();

// 解析为 Blob(图片、文件)
const blob = await res.blob();

// 解析为 FormData
const formData = await res.formData();

// 解析为 ArrayBuffer
const buffer = await res.arrayBuffer();

3.4 自定义请求头

fetch('/api/protected', {
    headers: {
        'Authorization': 'Bearer ' + token,
        'X-Requested-With': 'XMLHttpRequest',
        'Accept': 'application/json',
    },
});

3.5 超时控制

Fetch API 本身没有超时参数,需要用 AbortController 实现:

// 创建 AbortController
const controller = new AbortController();
const signal = controller.signal;

// 5秒超时
setTimeout(() => controller.abort(), 5000);

try {
    const res = await fetch('/api/slow-endpoint', { signal });
    const data = await res.json();
} catch (error) {
    if (error.name === 'AbortError') {
        console.error('请求超时');
    } else {
        console.error('请求失败:', error);
    }
}

3.6 上传进度

Fetch API 不支持原生上传进度,如果需要进度条,仍需回退到 XMLHttpRequest:

// Fetch 方式(带进度需要 XHR)
function uploadWithProgress(url, file, onProgress) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', url);

        xhr.upload.onprogress = (e) => {
            if (e.lengthComputable) {
                onProgress((e.loaded / e.total) * 100);
            }
        };

        xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) {
                resolve(JSON.parse(xhr.responseText));
            } else {
                reject(new Error('HTTP ' + xhr.status));
            }
        };

        xhr.onerror = () => reject(new Error('网络错误'));

        const formData = new FormData();
        formData.append('file', file);
        xhr.send(formData);
    });
}

四、同源策略(Same-Origin Policy)

4.1 什么是同源

同源 = 协议 + 域名 + 端口 三者完全一致:

https://www.example.com:443
└────┘ └──────────────┘ └─┘
  协议        域名       端口
URL 是否同源 原因
https://www.example.com/api ✅ 同源 完全一致
http://www.example.com/api ❌ 不同源 协议不同(https vs http)
https://api.example.com ❌ 不同源 域名不同
https://www.example.com:8080 ❌ 不同源 端口不同

4.2 浏览器限制

同源策略会阻止以下跨域行为:

// ❌ 跨域读取响应数据
fetch('https://other-site.com/api/data')
    .then(res => res.json())  // 被浏览器拦截

// ✅ 跨域请求仍然会发出,但 JS 无法读取响应

同源策略是浏览器的安全机制,不是服务器的。跨域请求会发到服务器,服务器也会处理并返回数据,但浏览器会拦截响应,不让 JavaScript 读取。


五、CORS(跨域资源共享)

CORS 是服务器通过 HTTP 响应头告诉浏览器允许跨域请求的机制。

5.1 服务端配置

最常见的 CORS 响应头配置:

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
// Node.js (Express) 示例
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', 'https://www.example.com');
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Access-Control-Allow-Credentials', 'true');

    // 预检请求
    if (req.method === 'OPTIONS') {
        return res.sendStatus(204);
    }

    next();
});

5.2 简单请求 vs 预检请求

简单请求(满足以下所有条件):

  • 方法:GET、HEAD、POST
  • 请求头:仅包含 Accept、Accept-Language、Content-Language、Content-Type(值限 text/plain、multipart/form-data、application/x-www-form-urlencoded)
  • 不使用 ReadableStream 或 AbortController 的 signal

预检请求:不满足简单请求条件时,浏览器会先发一个 OPTIONS 请求进行预检。

// 这个请求会触发预检(OPTIONS)
fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',  // 不是简单请求允许的类型
        'Authorization': 'Bearer xxx',       // 非简单请求头
    },
    body: JSON.stringify({ name: '张三' }),
});
预检流程:

浏览器 → OPTIONS /users → 服务器
                      ← 200 (允许跨域)
浏览器 → POST /users   → 服务器(正式请求)
                      ← 200 (响应数据)
// 前端需设置 credentials
fetch('https://api.example.com/data', {
    credentials: 'include',  // 携带 Cookie
});

// 服务端必须允许
// Access-Control-Allow-Origin: https://www.example.com  (不能是 *)
// Access-Control-Allow-Credentials: true

六、JSONP(JSON with Padding)

JSONP 是一种绕过同源策略的古老方式,利用 <script> 标签不受同源限制的特性。

6.1 工作原理

// 前端
function handleResponse(data) {
    console.log('收到数据:', data);
}

const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);
// 服务端返回一个函数调用
handleResponse({ "name": "张三", "age": 25 });

6.2 封装成通用函数

function jsonp(url, callbackName = 'callback') {
    return new Promise((resolve, reject) => {
        const funcName = 'jsonp_' + Date.now() + '_' + Math.random().toString(36).slice(2);
        window[funcName] = function(data) {
            delete window[funcName];
            document.body.removeChild(script);
            resolve(data);
        };

        const separator = url.includes('?') ? '&' : '?';
        const script = document.createElement('script');
        script.src = url + separator + callbackName + '=' + funcName;

        script.onerror = function() {
            delete window[funcName];
            document.body.removeChild(script);
            reject(new Error('JSONP 请求失败'));
        };

        document.body.appendChild(script);
    });
}

// 使用
jsonp('https://api.example.com/users')
    .then(data => console.log(data))
    .catch(err => console.error(err));

6.3 JSONP 的局限性

限制 说明
仅支持 GET 无法发送 POST/PUT/DELETE
错误处理弱 无法获取 HTTP 状态码
安全性差 服务端返回的 JS 代码直接执行,存在 XSS 风险
无法取消 没有 abort 机制
响应大小有限 URL 长度限制

现在基本已被 CORS 取代,只有在无法修改服务端配置的旧系统中才会看到。


七、跨域解决方案总结

方案 原理 支持方法 安全性
CORS 服务端通过响应头声明允许跨域 所有 HTTP 方法 ✅ 安全
JSONP 利用 <script> 标签绕过限制 仅 GET ⚠️ 有 XSS 风险
代理服务器 同源请求 → 代理转发 → 目标 API 所有方法 ✅ 安全
WebSocket WS 协议不受同源限制 双向通信 ✅ 安全
postMessage 窗口间消息传递 跨窗口通信 ⚠️ 需校验 origin
Nginx 反向代理 在 Nginx 层解决跨域 所有方法 ✅ 安全
// Nginx 代理配置
location /api/ {
    proxy_pass https://target-api.com/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

八、HTTP 状态码

Ajax 请求中常见的 HTTP 状态码:

状态码 含义 常见场景
200 OK 请求成功
201 Created POST 创建资源成功
204 No Content 删除成功,无返回体
301 Moved Permanently 资源永久重定向
302 Found 临时重定向
304 Not Modified 缓存有效
400 Bad Request 参数错误
401 Unauthorized 未登录或 token 失效
403 Forbidden 无权限
404 Not Found 资源不存在
405 Method Not Allowed 请求方法不允许
409 Conflict 资源冲突(如重复创建)
422 Unprocessable Entity 请求体格式正确但语义错误
429 Too Many Requests 请求频率超限
500 Internal Server Error 服务器内部错误
502 Bad Gateway 网关错误
503 Service Unavailable 服务暂时不可用
504 Gateway Timeout 网关超时

九、常见请求头与响应头

9.1 请求头(Request Headers)

Content-Type: application/json         # 请求体格式
Authorization: Bearer <token>          # 身份认证
Accept: application/json               # 期望的响应格式
X-Requested-With: XMLHttpRequest       # 标识是 Ajax 请求
Cache-Control: no-cache                # 禁用缓存
If-Modified-Since: ...                 # 条件请求(缓存)
If-None-Match: "etag-value"            # 条件请求(ETag)

9.2 响应头(Response Headers)

Content-Type: application/json; charset=utf-8
Content-Length: 1234
Cache-Control: max-age=3600
Set-Cookie: sessionId=abc123; HttpOnly; Secure
X-Request-Id: req-uuid-123             # 请求追踪 ID
X-RateLimit-Remaining: 98              # 剩余请求次数
Access-Control-Allow-Origin: *         # CORS

十、错误处理

10.1 分层处理

10.2 全局错误拦截

// 统一处理认证失效
window.addEventListener('unhandledrejection', function(event) {
    if (event.reason?.status === 401) {
        // Token 过期,跳转登录
        window.location.href = '/login?redirect=' + encodeURIComponent(window.location.pathname);
    }
});

十一、实用封装

11.1 轻量级 fetch 封装

11.2 axios 简介

axios 是目前最流行的 HTTP 请求库,提供了更完善的 API:

axios 和 fetch 的对比:

特性 Fetch axios
浏览器支持 现代浏览器 全浏览器(含 XHR 回退)
请求拦截器 手动封装 ✅ 内置
响应拦截器 手动封装 ✅ 内置
上传进度 ❌(需 XHR) ✅
取消请求 AbortController ✅ CancelToken
超时控制 AbortController ✅ timeout 参数
自动 JSON 解析 需手动调用 res.json() ✅ 自动
错误处理 仅网络错误抛异常 HTTP 错误也抛异常
包大小 内置 ~14KB (gzip)

十二、总结

概念 要点
同源策略 浏览器安全机制,阻止跨域读取响应
CORS 服务端通过响应头授权跨域访问
预检请求 复杂跨域请求前先发 OPTIONS
JSONP 利用 <script> 标签绕过限制(仅 GET)
Promise Fetch 和现代 Ajax 的基础异步模式
async/await Promise 的语法糖,让异步代码像同步
AbortController 取消 Fetch 请求的标准方式
FormData 表单数据 + 文件上传
HTTP 状态码 2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务端错误

Ajax 是现代 Web 应用的基石,理解这些概念后,无论是用原生 fetch、封装工具函数、还是使用 axios,你都能清楚地知道背后发生了什么。