← 返回Web开发目录
← 返回学习笔记首页
AJAX与Fetch API
Web开发专题 · 掌握前后端交互的核心技术
专题: Python Web开发系统学习
关键词: Python, Web开发, AJAX, Fetch API, Axios, XMLHttpRequest, 前后端通信, RESTful API, 异步请求
一、AJAX概述
1.1 什么是AJAX
AJAX(Asynchronous JavaScript and XML,异步JavaScript和XML)是一种在无需重新加载整个网页的情况下,能够更新部分页面内容的技术。它不是一种新的编程语言,而是一组已有技术的组合,包括HTML/CSS、JavaScript、DOM、XML(或JSON)以及XMLHttpRequest对象。
AJAX的核心思想是:浏览器通过JavaScript发起异步HTTP请求,服务器返回数据(通常是JSON或XML格式),然后JavaScript根据返回的数据动态更新页面内容。这一过程完全在后台进行,用户无需等待页面刷新即可看到最新数据。
1.2 异步通信的意义
在AJAX诞生之前,Web应用的交互模式是同步的:用户点击链接或提交表单后,浏览器必须向服务器发送请求,等待服务器响应,然后重新渲染整个页面。这种方式带来了明显的体验问题:页面闪烁、加载等待、状态丢失。
AJAX引入异步通信后,带来了革命性的变化:用户操作不会阻塞界面,页面部分更新而非整体刷新,带宽消耗大幅降低,用户体验显著提升。如今,几乎所有主流Web应用都离不开AJAX技术,从Gmail的邮件即时加载到Google Maps的平滑拖动,AJAX奠定了现代Web应用的基础。
核心理解: AJAX不是一项单一技术,而是一种架构模式。它的本质是让浏览器和服务器之间能够进行"悄悄对话"——用户在前台继续操作,数据在后台默默传输。
1.3 前后端分离的基础
AJAX技术的成熟直接推动了前后端分离架构的普及。在传统开发模式中,后端使用模板引擎(如JSP、Django Templates)渲染HTML,前后端代码紧密耦合。而AJAX使得前端可以独立请求数据接口(API),后端只需提供JSON数据即可,前端负责数据的呈现和交互。
这种分离带来了多方面的好处:前端和后端可以并行开发和部署;同一套后端API可以同时服务于Web页面、移动App和第三方开发者;团队分工更加清晰,前端专注用户体验,后端专注业务逻辑和数据。
二、XMLHttpRequest
2.1 基础概念
XMLHttpRequest(简称XHR)是浏览器内置的JavaScript对象,用于在后台与服务器交换数据。它是AJAX技术的核心载体,由微软在1999年首次引入Internet Explorer 5,后来被所有主流浏览器支持。尽管XHR的名字中包含"XML",但它实际上可以处理任何类型的数据,包括JSON、纯文本、二进制数据等。
2.2 基本使用步骤
使用XHR发起请求通常包含四个步骤:第一步,创建XMLHttpRequest对象;第二步,调用open()方法配置请求;第三步,注册事件监听器处理响应;第四步,调用send()方法发送请求。以下是一个标准的GET请求示例:
// 1. 创建XHR对象
var xhr = new XMLHttpRequest();
// 2. 配置请求(方法、URL、是否异步)
xhr.open('GET', 'https://api.example.com/users', true);
// 3. 注册状态变化监听
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
console.log('获取成功:', data);
} else if (xhr.readyState === 4 && xhr.status !== 200) {
console.error('请求失败:', xhr.status, xhr.statusText);
}
};
// 4. 发送请求
xhr.send();
2.3 readyState的五个状态
XHR对象的readyState属性表示请求当前所处的阶段,共有5个取值:
值 常量名 描述
0 UNSENT 已创建XHR对象,但open()尚未被调用
1 OPENED open()已被调用,但send()尚未被调用
2 HEADERS_RECEIVED send()已被调用,服务器响应头已收到
3 LOADING 响应体正在接收中
4 DONE 请求已完成,响应已就绪
2.4 GET与POST请求示例
GET请求的参数通过URL传递,POST请求的参数通过send()方法传递,且需要设置正确的Content-Type请求头:
// GET请求示例
function getUsers(page) {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users?page=' + page + '&limit=10', true);
xhr.onload = function() {
if (xhr.status === 200) {
renderUsers(JSON.parse(xhr.responseText));
}
};
xhr.send();
}
// POST请求示例
function createUser(userData) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/users', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 201) {
console.log('创建成功:', xhr.responseText);
} else {
console.error('创建失败:', xhr.status);
}
}
};
xhr.send(JSON.stringify(userData));
}
关键区别:GET请求将参数放在URL中,有长度限制(约2048字符),主要用于获取数据;POST请求将参数放在请求体中,无长度限制,主要用于提交数据。GET请求会被浏览器缓存,POST请求不会。
2.5 同步模式与异步模式
XHR的open()方法的第三个参数控制是否异步执行。传入true(默认值)表示异步模式,发出请求后JavaScript继续执行后续代码,响应到达时通过回调函数处理。传入false表示同步模式,send()方法会阻塞JavaScript执行,直到收到完整响应才继续——这会导致页面完全冻结,用户体验极差。
重要提示: 现代浏览器已经在主线程中废弃了同步XHR(Deprecated),并将其视为不良实践。在异步模式已成为标配的今天,永远不要在UI线程中使用同步XHR。
2.6 XHR的优缺点
优点: 所有浏览器都兼容(包括IE5+),成熟的API设计,支持进度事件(progress、load、error、abort),支持文件上传(FormData),支持超时设置(timeout属性)。
缺点: API设计冗长且回调嵌套容易形成"回调地狱",不符合现代Promise/async/await编程范式,需要手动处理JSON序列化/反序列化,不支持流式响应(Streaming),缺乏请求取消的优雅机制(abort()会中断连接但难以复用)。
三、Fetch API
3.1 Fetch概述
Fetch API是XMLHttpRequest的现代替代方案,由WHATWG(Web超文本应用技术工作组)标准化,在ES2015中引入。它基于Promise设计,API更加简洁清晰,语法更加直观。Fetch被所有现代浏览器原生支持(Chrome 42+、Firefox 39+、Safari 10.1+、Edge 14+),已经成为Web开发中发起HTTP请求的事实标准。
fetch()函数是Fetch API的入口点,它接受一个URL作为必选参数,返回一个Promise对象,该Promise在请求完成后resolve为一个Response对象。
// 最基本的GET请求
fetch('https://api.example.com/users')
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log('用户数据:', data);
})
.catch(function(error) {
console.error('请求失败:', error);
});
3.2 Response对象详解
fetch()返回的Promise resolve的是一个Response对象,它封装了服务器响应的全部信息。Response对象提供了一系列方法用于读取响应体数据,每种方法都返回一个Promise:
response.json() —— 将响应体解析为JSON格式,最常用的方法
response.text() —— 将响应体作为纯文本字符串返回
response.blob() —— 将响应体作为Blob(二进制大对象)返回,适合图片、文件下载
response.formData() —— 将响应体解析为FormData对象
response.arrayBuffer() —— 将响应体作为ArrayBuffer返回,适合处理二进制协议
注意:每个响应体只能被读取一次。一旦调用了上述任一方法,响应体数据就会被"消费"掉。如果需要多次访问,需要提前调用response.clone()克隆响应对象。
3.3 POST请求与请求配置
fetch()的第二个参数是配置对象(init对象),用于设置请求方法、请求头、请求体等。以下是一个完整的POST请求示例:
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
},
body: JSON.stringify({
name: '张三',
email: 'zhangsan@example.com',
role: 'admin'
})
})
.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));
3.4 重要特性:Fetch只在网络错误时reject
这是Fetch API最重要的特性之一,也是开发者最容易犯错误的地方。fetch()返回的Promise只会在网络故障(如断网、DNS解析失败、服务器不可达)时reject,而不会在服务器返回HTTP错误状态码(如404、500)时reject。这意味着,即使服务器返回了500错误,fetch()的catch方法也不会被触发。
因此,每次使用fetch()都必须手动检查response.ok属性(当状态码在200-299范围内时为true)或response.status属性:
fetch('/api/data')
.then(response => {
// 必须手动检查HTTP状态码
if (!response.ok) {
// 抛出错误以便被catch捕获
throw new Error('请求失败,状态码: ' + response.status);
}
return response.json();
})
.then(data => processData(data))
.catch(error => {
// 这里捕获网络错误和手动抛出的HTTP错误
console.error('请求出错:', error.message);
});
3.5 请求超时与中止
fetch()本身不内置超时机制,但可以通过AbortController实现。AbortController是一个用于中止一个或多个Web请求的控制器,它通过abort信号(signal)与fetch关联:
// 创建AbortController实例
const controller = new AbortController();
const signal = controller.signal;
// 设置5秒超时
setTimeout(() => controller.abort(), 5000);
fetch('https://api.example.com/heavy-task', { signal })
.then(response => response.json())
.then(data => console.log('数据:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.error('请求已超时或被用户取消');
} else {
console.error('网络错误:', error);
}
});
// 用户手动取消
document.getElementById('cancel-btn').onclick = () => controller.abort();
四、Fetch高级用法
4.1 async/await与Fetch配合
async/await语法让fetch()的代码更加线性化和可读,彻底消除了回调嵌套的困扰:
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('获取用户数据失败:', error);
throw error; // 重新抛出让调用方处理
}
}
// 使用
async function loadPage() {
try {
const user = await fetchUserData(123);
renderUserProfile(user);
} catch (error) {
showErrorMessage('用户数据加载失败,请稍后重试');
}
}
4.2 封装通用请求函数
在实际项目中,每次调用fetch()都重复设置请求头、检查状态码显然不可取。通常的做法是封装一个通用的请求函数,将公共逻辑集中处理:
// 通用请求封装
const BASE_URL = 'https://api.example.com';
async function request(endpoint, options = {}) {
const config = {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
...options.headers
},
...options.body && { body: JSON.stringify(options.body) }
};
const response = await fetch(`${BASE_URL}${endpoint}`, config);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new ApiError(
response.status,
errorData.message || response.statusText
);
}
return response.headers.get('Content-Type')?.includes('application/json')
? response.json()
: response.text();
}
// 便捷方法
const api = {
get: (url) => request(url),
post: (url, data) => request(url, { method: 'POST', body: data }),
put: (url, data) => request(url, { method: 'PUT', body: data }),
delete: (url) => request(url, { method: 'DELETE' })
};
4.3 请求与响应拦截器模式
拦截器模式允许我们在请求发送前或响应到达后执行统一的操作。例如,在请求发送前自动添加Token,在收到401错误时自动跳转到登录页面:
// 请求拦截器:统一添加认证令牌
function requestInterceptor(config) {
config.headers = {
...config.headers,
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'X-Request-ID': generateUUID()
};
return config;
}
// 响应拦截器:统一错误处理
async function responseInterceptor(response) {
if (response.status === 401) {
// Token过期,尝试刷新
const refreshed = await refreshToken();
if (refreshed) {
// 重新发起原始请求(需要在拦截器中保留原始配置)
return fetch(retryConfig.url, retryConfig);
} else {
// 刷新失败,跳转登录
window.location.href = '/login';
throw new Error('登录已过期');
}
}
if (response.status >= 500) {
// 服务器错误,上报监控系统
reportError('SERVER_ERROR', response.status);
}
return response;
}
4.4 文件上传(FormData)
使用FormData对象可以轻松实现文件上传。关键点在于不要手动设置Content-Type头——浏览器会自动设置为multipart/form-data并附带正确的boundary:
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file); // 单个文件
formData.append('description', '头像上传');
// 上传多个文件
// files.forEach(f => formData.append('photos', f));
try {
const response = await fetch('/api/upload', {
method: 'POST',
// 不要手动设置 Content-Type!
// 浏览器会自动处理 multipart/form-data
body: formData
});
if (!response.ok) throw new Error('上传失败');
return await response.json();
} catch (error) {
console.error('文件上传出错:', error);
throw error;
}
}
// 使用场景:监听文件选择
document.getElementById('file-input').onchange = async (e) => {
const file = e.target.files[0];
showProgressBar();
try {
const result = await uploadFile(file);
console.log('上传成功:', result.url);
} catch (error) {
showError('文件上传失败');
}
};
4.5 文件下载(Blob + URL.createObjectURL)
下载二进制文件时,使用response.blob()获取Blob对象,然后通过URL.createObjectURL()生成临时下载链接:
4.6 跨域请求
浏览器的同源策略(Same-Origin Policy)限制了跨域请求。Fetch API通过credentials和mode选项来控制跨域行为:
// 跨域请求配置
fetch('https://other-domain.com/api/data', {
// mode: 'cors' 是默认值,明确要求跨域
mode: 'cors',
// credentials: 控制是否携带凭据(cookie)
// 'same-origin' (默认) 仅同源请求携带
// 'include' 跨域请求也携带cookie
// 'omit' 从不携带cookie
credentials: 'include',
// 自定义请求头会触发预检请求(OPTIONS)
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
}
});
跨域要点: 跨域请求能否成功取决于服务器端的CORS配置。服务器必须在响应头中设置 Access-Control-Allow-Origin 来允许特定或所有来源的跨域请求。如果请求带有自定义头或非简单方法,浏览器会先发送一个OPTIONS预检请求。
五、Axios库
5.1 Axios vs Fetch对比
Axios是一个基于Promise的HTTP客户端库,可用于浏览器和Node.js环境。它本质上是对XHR的封装,但在API设计上比原生XHR优雅得多,而且在很多方面比Fetch API更易用。以下是两者的核心对比:
特性 Axios Fetch API
自动JSON解析 自动解析响应数据 需要手动调用response.json()
HTTP错误处理 非2xx状态码自动进入catch 仅网络错误进入catch,需手动检查ok
请求超时 内置timeout配置 需要结合AbortController实现
请求取消 CancelToken / AbortController 仅AbortController
上传进度 内置onUploadProgress 无原生支持,需用XHR
下载进度 内置onDownloadProgress Response.body可读取流,但较复杂
拦截器 内置请求/响应拦截器 需要自行封装
浏览器兼容 支持IE8+(polyfill) IE完全不支持
Node.js支持 原生支持 需安装node-fetch
5.2 Axios基本用法
// 安装: npm install axios
import axios from 'axios';
// GET请求
axios.get('/api/users?id=123')
.then(response => {
console.log('数据:', response.data); // 自动JSON解析
console.log('状态:', response.status); // HTTP状态码
console.log('头:', response.headers); // 响应头
})
.catch(error => {
if (error.response) {
// 服务器返回了错误状态码
console.error('服务器错误:', error.response.status);
} else if (error.request) {
// 请求已发出但未收到响应
console.error('网络错误或超时');
} else {
console.error('请求配置错误:', error.message);
}
});
// POST请求
axios.post('/api/users', {
name: '李四',
email: 'lisi@example.com'
}, {
headers: { 'Authorization': 'Bearer token123' },
timeout: 10000 // 10秒超时
});
5.3 拦截器与并发请求
Axios的拦截器是它最强大的特性之一,可以在请求发送前或响应返回后执行自定义逻辑:
// 请求拦截器:添加Token和日志
axios.interceptors.request.use(
config => {
console.log(`[${config.method.toUpperCase()}] ${config.url}`);
config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
return config;
},
error => Promise.reject(error)
);
// 响应拦截器:统一处理错误
axios.interceptors.response.use(
response => response.data, // 直接返回data,简化调用
error => {
if (error.response?.status === 401) {
// 未授权,清除Token并跳转
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
// 并发请求
axios.all([
axios.get('/api/users'),
axios.get('/api/roles'),
axios.get('/api/permissions')
]).then(axios.spread((users, roles, permissions) => {
console.log('用户:', users.data);
console.log('角色:', roles.data);
console.log('权限:', permissions.data);
}));
// 更推荐使用Promise.all
const [users, roles, permissions] = await Promise.all([
axios.get('/api/users'),
axios.get('/api/roles'),
axios.get('/api/permissions')
]);
选择建议:对于简单项目或追求零依赖的场景,原生Fetch API完全够用。对于中大型项目(需要拦截器、请求取消、上传进度监控),Axios能显著减少样板代码,提升开发效率。
六、RESTful API调用实践
6.1 封装API模块
在实际项目中,最佳实践是将所有API调用封装在独立的模块中,集中管理接口地址和请求逻辑。这样做的好处是:接口变更时只需修改一处,代码复用性高,调用方代码清晰简洁:
// api/index.js — API模块统一封装
import axios from 'axios';
import { getToken, refreshToken } from './auth';
const apiClient = axios.create({
baseURL: 'https://api.example.com/v1',
timeout: 15000,
headers: { 'Content-Type': 'application/json' }
});
// 请求拦截器:自动附加Token
apiClient.interceptors.request.use(config => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:自动刷新Token
apiClient.interceptors.response.use(
response => response.data,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const newToken = await refreshToken();
if (newToken) {
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return apiClient(originalRequest);
}
}
return Promise.reject(error);
}
);
// 导出各模块API
export const userApi = {
list: (params) => apiClient.get('/users', { params }),
get: (id) => apiClient.get(`/users/${id}`),
create: (data) => apiClient.post('/users', data),
update: (id, data) => apiClient.put(`/users/${id}`, data),
delete: (id) => apiClient.delete(`/users/${id}`)
};
export const articleApi = {
list: (params) => apiClient.get('/articles', { params }),
publish: (data) => apiClient.post('/articles', data),
uploadCover: (file) => {
const formData = new FormData();
formData.append('cover', file);
return apiClient.post('/articles/cover', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
};
6.2 Token认证请求
Token认证是前后端分离项目中最常用的认证方式。工作流程如下:用户登录成功后服务器返回Token(通常是JWT格式),前端将Token存储在localStorage或内存中,后续每次请求在Authorization头中携带该Token。Token有过期时间,需要在过期前刷新或在过期后重新登录。
// Token存储与刷新机制
const TOKEN_KEY = 'access_token';
const REFRESH_KEY = 'refresh_token';
function storeTokens(accessToken, refreshToken) {
localStorage.setItem(TOKEN_KEY, accessToken);
localStorage.setItem(REFRESH_KEY, refreshToken);
}
function getAccessToken() {
return localStorage.getItem(TOKEN_KEY);
}
async function refreshAccessToken() {
const refreshToken = localStorage.getItem(REFRESH_KEY);
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
if (!response.ok) {
// Refresh失败,清除所有Token
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_KEY);
window.location.href = '/login';
throw new Error('Session expired');
}
const data = await response.json();
localStorage.setItem(TOKEN_KEY, data.accessToken);
return data.accessToken;
}
6.3 错误统一处理
一个健壮的API通信层必须包含统一的错误处理机制。不仅要处理HTTP层面的错误,还要处理业务层面的错误码:
// 统一错误处理
class ApiError extends Error {
constructor(status, code, message, details) {
super(message);
this.status = status; // HTTP状态码
this.code = code; // 业务错误码
this.details = details; // 详细错误信息
}
}
async function handleApiError(error) {
if (error instanceof ApiError) {
switch (error.code) {
case 'TOKEN_EXPIRED':
return await refreshAndRetry();
case 'PERMISSION_DENIED':
showToast('您没有权限执行此操作');
break;
case 'RATE_LIMITED':
showToast('请求过于频繁,请稍后重试');
break;
case 'VALIDATION_ERROR':
showFormErrors(error.details);
break;
default:
showToast(error.message || '服务器繁忙,请稍后重试');
}
} else if (error.name === 'AbortError') {
console.log('请求已取消');
} else if (!navigator.onLine) {
showToast('网络连接已断开,请检查网络');
} else {
showToast('未知错误,请稍后重试');
reportError(error); // 上报到监控系统
}
}
6.4 请求重试机制
在网络不稳定的环境中,实现请求重试机制可以显著提高应用的健壮性。重试策略通常包括:最多重试次数、重试间隔(线性或指数退避)、可重试的错误类型(如网络超时、5xx服务端错误):
// 带重试的请求封装
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
// 对5xx错误进行重试
if (response.status >= 500 && i < retries - 1) {
const delay = Math.min(1000 * Math.pow(2, i), 10000);
console.log(`请求失败(${response.status}),${delay}ms后第${i+2}次重试...`);
await sleep(delay);
continue;
}
return response;
} catch (error) {
// 网络错误也重试
if (i < retries - 1) {
const delay = Math.min(1000 * Math.pow(2, i), 10000);
console.log(`网络错误,${delay}ms后第${i+2}次重试...`);
await sleep(delay);
continue;
}
throw error;
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 使用示例
const response = await fetchWithRetry(
'/api/important-data',
{ signal: AbortSignal.timeout(5000) },
3
);
// Axios中使用重试(配合axios-retry插件)
import axiosRetry from 'axios-retry';
axiosRetry(axios, {
retries: 3,
retryDelay: (retryCount) => {
return axiosRetry.exponentialDelay(retryCount);
},
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error)
|| error.response?.status >= 500;
}
});
实践总结: 从XMLHttpRequest到Fetch API再到Axios,前端HTTP通信技术经历了三次重大演进。理解XHR的工作原理有助于深入理解HTTP协议,掌握Fetch API是现代Web开发的必备技能,而学会使用Axios能显著提升中大型项目的开发效率。三种方案并非互斥,在实际开发中应根据项目需求选择最合适的方案。