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个取值:

常量名描述
0UNSENT已创建XHR对象,但open()尚未被调用
1OPENEDopen()已被调用,但send()尚未被调用
2HEADERS_RECEIVEDsend()已被调用,服务器响应头已收到
3LOADING响应体正在接收中
4DONE请求已完成,响应已就绪

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.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更易用。以下是两者的核心对比:

特性AxiosFetch API
自动JSON解析自动解析响应数据需要手动调用response.json()
HTTP错误处理非2xx状态码自动进入catch仅网络错误进入catch,需手动检查ok
请求超时内置timeout配置需要结合AbortController实现
请求取消CancelToken / AbortController仅AbortController
上传进度内置onUploadProgress无原生支持,需用XHR
下载进度内置onDownloadProgressResponse.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能显著提升中大型项目的开发效率。三种方案并非互斥,在实际开发中应根据项目需求选择最合适的方案。