一、什么是 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 常用事件
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
// 请求完成时触发(readyState === 4)
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('成功:', xhr.responseText);
} else {
console.error('HTTP 错误:', xhr.status);
}
};
// 网络错误时触发
xhr.onerror = function() {
console.error('网络请求失败');
};
// 请求超时
xhr.timeout = 5000; // 5秒
xhr.ontimeout = function() {
console.error('请求超时');
};
// 上传进度
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
console.log('上传进度:', percent + '%');
}
};
xhr.send();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 (响应数据)5.3 带 Cookie 的跨域请求
// 前端需设置 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 分层处理
// 封装统一的请求函数
async function request(url, options = {}) {
try {
const res = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
// HTTP 错误
if (!res.ok) {
const error = new Error('请求失败');
error.status = res.status;
error.statusText = res.statusText;
try {
error.body = await res.json();
} catch {
error.body = null;
}
throw error;
}
return await res.json();
} catch (error) {
if (error.name === 'AbortError') {
console.error('请求已取消');
} else if (error instanceof TypeError) {
console.error('网络错误(可能是跨域或断网)');
} else {
console.error(`HTTP ${error.status}: ${error.statusText}`, error.body);
}
throw error;
}
}
// 使用
try {
const users = await request('/api/users');
} catch (error) {
if (error.status === 401) {
// 跳转到登录页
window.location.href = '/login';
} else if (error.status === 429) {
// 显示"请求太频繁"
showToast('请求太频繁,请稍后再试');
} else if (error.status >= 500) {
// 显示服务器错误
showToast('服务器开小差了,请重试');
}
}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 封装
const api = {
baseURL: '/api',
async request(endpoint, options = {}) {
const url = this.baseURL + endpoint;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
// 自动附加 token
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
const res = await fetch(url, config);
if (!res.ok) {
const error = new Error(res.statusText);
error.status = res.status;
throw error;
}
return res.json();
},
get(endpoint) {
return this.request(endpoint);
},
post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
},
put(endpoint, data) {
return this.request(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
},
delete(endpoint) {
return this.request(endpoint, { method: 'DELETE' });
},
};
// 使用
const users = await api.get('/users');
const newUser = await api.post('/users', { name: '张三' });11.2 axios 简介
axios 是目前最流行的 HTTP 请求库,提供了更完善的 API:
import axios from 'axios';
// 创建实例
const http = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
// 请求拦截器(自动附加 token)
http.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
});
// 响应拦截器(统一错误处理)
http.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// 使用
const users = await http.get('/users');
const result = await http.post('/users', { name: '张三' });
// 并发请求
const [users, posts] = await Promise.all([
http.get('/users'),
http.get('/posts'),
]);axios 和 fetch 的对比:
| 特性 | Fetch | axios |
|---|---|---|
| 浏览器支持 | 现代浏览器 | 全浏览器(含 XHR 回退) |
| 请求拦截器 | 手动封装 | ✅ 内置 |
| 响应拦截器 | 手动封装 | ✅ 内置 |
| 上传进度 | ❌(需 XHR) | ✅ |
| 取消请求 | AbortController | ✅ CancelToken |
| 超时控制 | AbortController | ✅ timeout 参数 |
| 自动 JSON 解析 | 需手动调用 res.json() |
✅ 自动 |
| 错误处理 | 仅网络错误抛异常 | HTTP 错误也抛异常 |
| 包大小 | 内置 | ~14KB (gzip) |
十二、总结
Ajax 核心概念一览:
┌───────────────────────────────┐
│ Ajax 请求 │
│ │
│ ┌─────────────────┐ │
│ │ XMLHttpRequest │ 传统方案 │
│ │ (XHR) │ 全支持 │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ Fetch API │ 现代方案 │
│ │ (Promise) │ 简洁 │
│ └─────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ axios │ 第三方库 │
│ │ (推荐方案) │ 功能全 │
│ └─────────────────┘ │
│ │
│ 安全机制: │
│ • 同源策略(浏览器限制) │
│ • CORS(服务端授权) │
│ • JSONP(古老绕过方案) │
│ │
│ 常见场景: │
│ • RESTful API 调用 │
│ • 表单提交 │
│ • 文件上传 + 进度条 │
│ • 实时搜索 │
│ • 懒加载/无限滚动 │
└───────────────────────────────┘| 概念 | 要点 |
|---|---|
| 同源策略 | 浏览器安全机制,阻止跨域读取响应 |
| CORS | 服务端通过响应头授权跨域访问 |
| 预检请求 | 复杂跨域请求前先发 OPTIONS |
| JSONP | 利用 <script> 标签绕过限制(仅 GET) |
| Promise | Fetch 和现代 Ajax 的基础异步模式 |
| async/await | Promise 的语法糖,让异步代码像同步 |
| AbortController | 取消 Fetch 请求的标准方式 |
| FormData | 表单数据 + 文件上传 |
| HTTP 状态码 | 2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务端错误 |
Ajax 是现代 Web 应用的基石,理解这些概念后,无论是用原生 fetch、封装工具函数、还是使用 axios,你都能清楚地知道背后发生了什么。