乐闻世界logo
搜索文章和话题

JavaScript面试题手册

axios 中如何实现并发请求和取消请求?请提供代码示例

Axios 并发请求Axios 提供了 axios.all() 和 axios.spread() 方法来处理并发请求,同时也支持使用原生的 Promise.all()。1. 使用 Promise.all()(推荐)// 同时发送多个请求async function fetchMultipleData() { try { const [users, posts, comments] = await Promise.all([ axios.get('/api/users'), axios.get('/api/posts'), axios.get('/api/comments') ]); console.log('Users:', users.data); console.log('Posts:', posts.data); console.log('Comments:', comments.data); return { users: users.data, posts: posts.data, comments: comments.data }; } catch (error) { console.error('至少一个请求失败:', error); throw error; }}2. 使用 axios.all()(传统方式)axios.all([ axios.get('/api/users'), axios.get('/api/posts'), axios.get('/api/comments')]).then(axios.spread((users, posts, comments) => { // 所有请求都成功时执行 console.log('Users:', users.data); console.log('Posts:', posts.data); console.log('Comments:', comments.data);})).catch(error => { // 任一请求失败时执行 console.error('请求失败:', error);});3. 并发请求的错误处理async function fetchWithErrorHandling() { const requests = [ axios.get('/api/users'), axios.get('/api/posts'), axios.get('/api/comments') // 可能失败的请求 ]; // 使用 Promise.allSettled 等待所有请求完成 const results = await Promise.allSettled(requests); results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`请求 ${index} 成功:`, result.value.data); } else { console.error(`请求 ${index} 失败:`, result.reason.message); } }); // 过滤出成功的结果 const successfulResults = results .filter(result => result.status === 'fulfilled') .map(result => result.value.data); return successfulResults;}4. 限制并发数量// 使用 p-limit 或自定义实现限制并发async function fetchWithConcurrencyLimit(urls, limit = 3) { const results = []; const executing = []; for (const [index, url] of urls.entries()) { const promise = axios.get(url).then(res => ({ index, data: res.data })); results.push(promise); if (urls.length >= limit) { executing.push(promise); if (executing.length >= limit) { await Promise.race(executing); executing.splice( executing.findIndex(p => p === promise), 1 ); } } } return Promise.all(results);}// 使用const urls = ['/api/data/1', '/api/data/2', '/api/data/3', '/api/data/4'];fetchWithConcurrencyLimit(urls, 2);Axios 取消请求1. 使用 AbortController(推荐,v0.22.0+)// 创建 AbortControllerconst controller = new AbortController();// 发送请求时传入 signalaxios.get('/api/data', { signal: controller.signal}).then(response => { console.log(response.data);}).catch(error => { if (axios.isCancel(error)) { console.log('请求已取消:', error.message); } else { console.error('请求失败:', error); }});// 取消请求controller.abort('用户取消操作');// 5秒后自动取消setTimeout(() => { controller.abort('请求超时');}, 5000);2. 在 React 组件中使用import { useEffect } from 'react';function UserList() { useEffect(() => { const controller = new AbortController(); const fetchUsers = async () => { try { const response = await axios.get('/api/users', { signal: controller.signal }); // 处理数据 } catch (error) { if (axios.isCancel(error)) { console.log('组件卸载,请求已取消'); } else { console.error('获取用户失败:', error); } } }; fetchUsers(); // 组件卸载时取消请求 return () => { controller.abort('组件卸载'); }; }, []); return <div>User List</div>;}3. 在 Vue 组件中使用<script setup>import { onMounted, onUnmounted } from 'vue';let controller;onMounted(() => { controller = new AbortController(); axios.get('/api/data', { signal: controller.signal }) .then(response => { console.log(response.data); }) .catch(error => { if (axios.isCancel(error)) { console.log('请求已取消'); } });});onUnmounted(() => { controller?.abort('组件卸载');});</script>4. 取消多个请求const controllers = new Map();// 发送请求时保存 controllerfunction fetchWithCancel(key, url) { // 取消之前的同名请求 if (controllers.has(key)) { controllers.get(key).abort('重复请求,取消前一个'); } const controller = new AbortController(); controllers.set(key, controller); return axios.get(url, { signal: controller.signal }) .finally(() => { controllers.delete(key); });}// 使用:搜索框防抖场景fetchWithCancel('search', '/api/search?q=keyword');5. 请求超时自动取消async function fetchWithTimeout(url, timeout = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(`请求超时 (${timeout}ms)`); }, timeout); try { const response = await axios.get(url, { signal: controller.signal }); clearTimeout(timeoutId); return response.data; } catch (error) { clearTimeout(timeoutId); throw error; }}6. 取消请求的工具函数封装class RequestManager { constructor() { this.controllers = new Map(); } // 发送请求 async request(key, config) { // 取消之前的同名请求 this.cancel(key); const controller = new AbortController(); this.controllers.set(key, controller); try { const response = await axios({ ...config, signal: controller.signal }); return response; } finally { this.controllers.delete(key); } } // 取消指定请求 cancel(key, message = '请求被取消') { if (this.controllers.has(key)) { this.controllers.get(key).abort(message); this.controllers.delete(key); } } // 取消所有请求 cancelAll(message = '所有请求被取消') { this.controllers.forEach(controller => { controller.abort(message); }); this.controllers.clear(); }}// 使用const requestManager = new RequestManager();// 发送请求requestManager.request('userList', { method: 'GET', url: '/api/users'});// 取消指定请求requestManager.cancel('userList');// 取消所有请求(如页面切换时)requestManager.cancelAll();最佳实践组件卸载时取消请求:避免内存泄漏和状态更新错误重复请求时取消前一个:搜索框、表单提交等场景设置合理的超时时间:防止请求挂起正确处理取消错误:区分取消错误和业务错误使用 AbortController:现代浏览器标准 API,兼容性好
阅读 0·3月7日 12:10

如何在 axios 中实现请求和响应拦截器?请举例说明实际应用场景

Axios 拦截器概述Axios 拦截器允许你在请求发送前或响应接收后统一处理数据,是实现全局配置、错误处理、权限验证等功能的重要机制。请求拦截器(Request Interceptors)基本用法// 添加请求拦截器axios.interceptors.request.use( function (config) { // 在发送请求之前做些什么 return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); });实际应用场景1. 统一添加认证 Tokenaxios.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, error => { return Promise.reject(error); });2. 添加时间戳防止缓存axios.interceptors.request.use( config => { if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() }; } return config; });3. 显示加载状态let requestCount = 0;axios.interceptors.request.use( config => { requestCount++; if (requestCount === 1) { // 显示全局 loading showLoading(); } return config; });响应拦截器(Response Interceptors)基本用法// 添加响应拦截器axios.interceptors.response.use( function (response) { // 对响应数据做点什么 return response; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); });实际应用场景1. 统一错误处理axios.interceptors.response.use( response => response, error => { const { response } = error; if (response) { switch (response.status) { case 401: // 未授权,清除 token 并跳转到登录页 localStorage.removeItem('token'); window.location.href = '/login'; break; case 403: message.error('没有权限访问该资源'); break; case 404: message.error('请求的资源不存在'); break; case 500: message.error('服务器内部错误'); break; default: message.error(response.data.message || '请求失败'); } } else { message.error('网络错误,请检查网络连接'); } return Promise.reject(error); });2. 响应数据格式化axios.interceptors.response.use( response => { // 假设后端统一返回格式:{ code: 0, data: {}, message: '' } const res = response.data; if (res.code !== 0) { message.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; // 直接返回 data,简化组件中的调用 });3. 隐藏加载状态axios.interceptors.response.use( response => { requestCount--; if (requestCount === 0) { hideLoading(); } return response; }, error => { requestCount--; if (requestCount === 0) { hideLoading(); } return Promise.reject(error); });移除拦截器const myInterceptor = axios.interceptors.request.use(() => {});axios.interceptors.request.eject(myInterceptor);为实例添加拦截器const instance = axios.create({ baseURL: 'https://api.example.com'});instance.interceptors.request.use(config => { // 只对当前实例生效 return config;});多个拦截器的执行顺序axios.interceptors.request.use(config => { console.log('请求拦截器 1'); return config;});axios.interceptors.request.use(config => { console.log('请求拦截器 2'); return config;});// 执行顺序:请求拦截器 2 → 请求拦截器 1 → 发送请求// 响应拦截器执行顺序与添加顺序相反最佳实践错误处理要完整:请求和响应拦截器都要处理错误情况记得返回 config/response:否则请求不会继续使用实例隔离:不同服务使用不同实例,避免相互影响避免副作用:拦截器中不要修改原始参数对象添加请求标识:方便调试和追踪
阅读 0·3月7日 12:10

axios 中如何进行错误处理?请详细说明错误类型和处理策略

Axios 错误类型Axios 请求可能产生的错误主要分为以下几类:1. 请求配置错误URL 格式错误请求方法错误配置参数错误2. 网络错误无网络连接请求超时DNS 解析失败CORS 跨域错误3. HTTP 错误状态码4xx 客户端错误(400, 401, 403, 404 等)5xx 服务器错误(500, 502, 503 等)4. 请求取消错误主动取消请求组件卸载时取消错误对象结构当请求失败时,Axios 会返回一个包含以下属性的错误对象:try { await axios.get('/api/data');} catch (error) { console.log(error.message); // 错误信息 console.log(error.response); // 服务器响应(如果有) console.log(error.request); // 请求对象 console.log(error.config); // 请求配置 console.log(error.code); // 错误代码}错误处理策略1. 基础错误处理axios.get('/api/data') .then(response => { console.log(response.data); }) .catch(error => { if (error.response) { // 服务器返回了错误状态码 console.log('Status:', error.response.status); console.log('Data:', error.response.data); } else if (error.request) { // 请求已发送但没有收到响应 console.log('No response received'); } else { // 请求配置出错 console.log('Error:', error.message); } });2. 使用 async/await 处理错误async function fetchData() { try { const response = await axios.get('/api/data'); return response.data; } catch (error) { handleAxiosError(error); }}function handleAxiosError(error) { if (error.response) { // 服务器响应错误 const { status, data } = error.response; switch (status) { case 400: throw new Error(`请求参数错误: ${data.message}`); case 401: // 清除登录状态并跳转 logout(); throw new Error('登录已过期,请重新登录'); case 403: throw new Error('没有权限执行此操作'); case 404: throw new Error('请求的资源不存在'); case 422: throw new Error(`数据验证失败: ${data.message}`); case 500: throw new Error('服务器内部错误,请稍后重试'); case 502: throw new Error('网关错误'); case 503: throw new Error('服务暂时不可用'); default: throw new Error(data.message || '请求失败'); } } else if (error.request) { // 网络错误 if (error.code === 'ECONNABORTED') { throw new Error('请求超时,请检查网络连接'); } if (error.code === 'ERR_NETWORK') { throw new Error('网络错误,请检查网络连接'); } throw new Error('无法连接到服务器'); } else { // 其他错误 throw new Error(error.message); }}3. 全局错误处理(通过拦截器)// 创建 axios 实例const instance = axios.create({ timeout: 10000});// 响应拦截器统一处理错误instance.interceptors.response.use( response => response, error => { // 统一错误处理 const errorMessage = getErrorMessage(error); // 显示错误提示 message.error(errorMessage); // 记录错误日志 logError(error); return Promise.reject(error); });function getErrorMessage(error) { if (error.response) { const { status, data } = error.response; const messageMap = { 400: '请求参数错误', 401: '登录已过期', 403: '没有权限', 404: '资源不存在', 500: '服务器错误', 502: '网关错误', 503: '服务不可用' }; return data.message || messageMap[status] || '请求失败'; } if (error.request) { return '网络连接失败,请检查网络'; } return error.message || '未知错误';}4. 超时错误处理const instance = axios.create({ timeout: 5000, // 5秒超时 timeoutErrorMessage: '请求超时,请稍后重试'});// 或者自定义超时处理instance.interceptors.response.use( response => response, error => { if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) { // 超时错误特殊处理 return Promise.reject(new Error('请求响应时间过长,请稍后重试')); } return Promise.reject(error); });5. 重试机制axios.interceptors.response.use(null, async (error) => { const { config } = error; // 设置重试次数 if (!config || !config.retry) { return Promise.reject(error); } config.retryCount = config.retryCount || 0; if (config.retryCount >= config.retry) { return Promise.reject(error); } config.retryCount += 1; // 延迟重试 const delayRetry = new Promise(resolve => { setTimeout(resolve, config.retryDelay || 1000); }); await delayRetry; return axios(config);});// 使用axios.get('/api/data', { retry: 3, retryDelay: 2000});6. 请求取消错误处理const controller = new AbortController();try { const response = await axios.get('/api/data', { signal: controller.signal });} catch (error) { if (axios.isCancel(error)) { console.log('请求被取消:', error.message); } else { // 处理其他错误 console.error('请求失败:', error); }}// 取消请求controller.abort('用户取消操作');最佳实践分层处理:全局拦截器 + 业务层处理用户友好:错误信息要清晰易懂错误分类:区分可恢复和不可恢复错误日志记录:记录错误便于排查问题降级策略:网络错误时提供缓存数据或默认数据错误处理流程图请求失败 ↓检查 error.response ↓存在 → HTTP 错误 → 根据状态码处理 ↓不存在 → 检查 error.request ↓存在 → 网络错误 → 提示用户检查网络 ↓不存在 → 配置错误 → 检查代码
阅读 0·3月7日 12:10

axios 有哪些高级特性?如文件上传下载、进度监控、CSRF 防护等

Axios 高级特性概览Axios 不仅支持基本的 HTTP 请求,还提供了许多高级特性,包括文件上传下载、进度监控、CSRF 防护、请求转换等。1. 文件上传基础文件上传// HTML// <input type="file" id="fileInput" />const uploadFile = async (file) => { const formData = new FormData(); formData.append('file', file); try { const response = await axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); return response.data; } catch (error) { console.error('上传失败:', error); throw error; }};// 使用const fileInput = document.getElementById('fileInput');fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; uploadFile(file);});多文件上传const uploadMultipleFiles = async (files) => { const formData = new FormData(); files.forEach((file, index) => { formData.append(`file${index}`, file); }); // 或者使用相同字段名 // files.forEach(file => { // formData.append('files', file); // }); const response = await axios.post('/api/upload-multiple', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); return response.data;};带进度条的文件上传const uploadWithProgress = (file, onProgress) => { const formData = new FormData(); formData.append('file', file); return axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { if (progressEvent.lengthComputable) { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); onProgress(percentCompleted); } } });};// React 组件中使用function FileUpload() { const [progress, setProgress] = useState(0); const handleUpload = async (file) => { try { const result = await uploadWithProgress(file, setProgress); console.log('上传成功:', result); } catch (error) { console.error('上传失败:', error); } }; return ( <div> <input type="file" onChange={(e) => handleUpload(e.target.files[0])} /> <progress value={progress} max="100" /> <span>{progress}%</span> </div> );}2. 文件下载基础文件下载const downloadFile = async (url, filename) => { try { const response = await axios.get(url, { responseType: 'blob' // 重要:设置响应类型为 blob }); // 创建下载链接 const blob = new Blob([response.data]); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(downloadUrl); } catch (error) { console.error('下载失败:', error); }};// 使用downloadFile('/api/download/report.pdf', 'report.pdf');带进度条的文件下载const downloadWithProgress = async (url, filename, onProgress) => { const response = await axios.get(url, { responseType: 'blob', onDownloadProgress: (progressEvent) => { if (progressEvent.lengthComputable) { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); onProgress(percentCompleted); } } }); const blob = new Blob([response.data]); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(downloadUrl);};3. CSRF 防护自动 CSRF Token 处理const instance = axios.create({ // 从 cookie 中读取 CSRF token 的字段名 xsrfCookieName: 'XSRF-TOKEN', // 请求头中发送 CSRF token 的字段名 xsrfHeaderName: 'X-XSRF-TOKEN', // 允许携带 cookie withCredentials: true});手动设置 CSRF Token// 从 meta 标签获取 CSRF tokenconst csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');const instance = axios.create({ headers: { 'X-CSRF-TOKEN': csrfToken }});// 或者通过拦截器动态设置instance.interceptors.request.use(config => { const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); if (token) { config.headers['X-CSRF-TOKEN'] = token; } return config;});4. 请求和响应转换请求数据转换const instance = axios.create({ // 在请求发送到服务器之前修改请求数据 transformRequest: [ function (data, headers) { // 对 data 进行转换 if (data instanceof FormData) { return data; } // 添加时间戳防止缓存 if (data && typeof data === 'object') { data._timestamp = Date.now(); } return JSON.stringify(data); } ]});响应数据转换const instance = axios.create({ // 在传递给 then/catch 之前修改响应数据 transformResponse: [ function (data) { // 解析 JSON const parsed = JSON.parse(data); // 统一处理响应格式 if (parsed.code !== 0) { throw new Error(parsed.message); } return parsed.data; } ]});5. 参数序列化自定义参数序列化import qs from 'qs';const instance = axios.create({ // 自定义 params 序列化 paramsSerializer: { encode?: (param: string): string => encodeURIComponent(param), // 自定义编码函数 serialize?: (params: Record<string, any>, options?: ParamsSerializerOptions): string => { // 使用 qs 库进行序列化 return qs.stringify(params, { arrayFormat: 'brackets' }); }, indexes: false // 数组参数不使用索引 }});// 使用instance.get('/api/search', { params: { q: 'keyword', tags: ['javascript', 'axios'] }});// 结果: /api/search?q=keyword&tags[]=javascript&tags[]=axios6. 代理配置// Node.js 环境const instance = axios.create({ proxy: { protocol: 'https', host: '127.0.0.1', port: 9000, auth: { username: 'mikeymike', password: 'rapunz3l' } }});// 或者使用环境变量const instance = axios.create({ proxy: false // 禁用代理});7. 适配器自定义适配器const instance = axios.create({ adapter: (config) => { return new Promise((resolve, reject) => { // 自定义请求实现 const xhr = new XMLHttpRequest(); xhr.open(config.method.toUpperCase(), config.url); xhr.onload = () => { resolve({ data: xhr.response, status: xhr.status, statusText: xhr.statusText, headers: {}, config, request: xhr }); }; xhr.onerror = () => reject(new Error('Request failed')); xhr.send(config.data); }); }});8. 验证状态码const instance = axios.create({ // 自定义合法状态码 validateStatus: (status) => { return status >= 200 && status < 300; // 默认值 // 或者接受所有状态码 // return true; // 或者只接受特定状态码 // return [200, 201, 204].includes(status); }});9. 最大内容长度和重定向const instance = axios.create({ // 最大响应内容长度(字节) maxContentLength: 2000, // 最大请求内容长度(字节) maxBodyLength: 2000, // 最大重定向次数 maxRedirects: 5, // 在 Node.js 中遵循重定向 // 在浏览器中此配置无效(浏览器自动处理重定向)});10. 完整的高级配置示例import axios from 'axios';import qs from 'qs';const advancedApi = axios.create({ baseURL: 'https://api.example.com', timeout: 30000, // 请求头 headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, // 携带 cookie withCredentials: true, // CSRF 防护 xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', // 响应类型 responseType: 'json', // 参数序列化 paramsSerializer: (params) => { return qs.stringify(params, { arrayFormat: 'repeat' }); }, // 请求转换 transformRequest: [ (data, headers) => { // 添加认证信息 const token = localStorage.getItem('token'); if (token) { headers.Authorization = `Bearer ${token}`; } // 如果不是 FormData,转换为 JSON if (data && !(data instanceof FormData)) { return JSON.stringify(data); } return data; } ], // 响应转换 transformResponse: [ (data) => { // 统一错误处理 if (data && data.code !== 0) { throw new Error(data.message); } return data?.data || data; } ], // 状态码验证 validateStatus: (status) => { return status >= 200 && status < 500; }, // 最大内容长度 maxContentLength: 50 * 1024 * 1024, // 50MB // 最大重定向次数 maxRedirects: 5});// 添加进度监控拦截器advancedApi.interceptors.request.use(config => { if (config.onUploadProgress || config.onDownloadProgress) { console.log('请求包含进度监控'); } return config;});export default advancedApi;最佳实践文件上传:始终使用 FormData,设置正确的 Content-Type文件下载:设置 responseType 为 'blob' 或 'arraybuffer'CSRF 防护:正确配置 xsrfCookieName 和 xsrfHeaderName进度监控:在需要用户体验的场景中使用数据转换:统一处理请求和响应数据格式错误处理:在 transformResponse 中统一处理业务错误
阅读 0·3月7日 12:10

axios 和 fetch API 有什么区别?在什么场景下应该选择使用 axios?

Axios vs Fetch API 对比Axios 和 Fetch API 都是用于发送 HTTP 请求的工具,但它们在功能、易用性和兼容性方面存在显著差异。核心区别对比表| 特性 | Axios | Fetch API ||------|-------|-----------|| API 设计 | 基于 Promise,API 更友好 | 原生 Promise,API 较底层 || JSON 处理 | 自动转换 JSON 数据 | 需要手动调用 .json() || 请求拦截器 | ✅ 原生支持 | ❌ 需要自行封装 || 响应拦截器 | ✅ 原生支持 | ❌ 需要自行封装 || 请求取消 | ✅ 支持 AbortController | ✅ 支持 AbortController || 超时设置 | ✅ 原生支持 timeout | ❌ 需要手动实现 || 进度监控 | ✅ 原生支持 onUploadProgress/onDownloadProgress | ❌ 需要手动实现 || 错误处理 | HTTP 错误自动 reject | 只有网络错误才 reject || 浏览器兼容 | IE11+ | Chrome 42+, Edge 14+, Firefox 39+ || 体积 | ~13KB (gzip) | 原生支持,无额外体积 || Node.js 支持 | ✅ 支持 | ❌ 不支持(需 polyfill) |详细对比分析1. JSON 数据处理Axios(自动处理):// 自动将响应转换为 JSONconst response = await axios.get('/api/users');console.log(response.data); // 已经是 JavaScript 对象// 自动将请求体转换为 JSONawait axios.post('/api/users', { name: 'John' });Fetch(手动处理):// 需要手动调用 .json()const response = await fetch('/api/users');const data = await response.json(); // 额外的 awaitconsole.log(data);// 需要手动设置 headers 和 stringifyawait fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'John' })});2. 错误处理Axios(自动处理 HTTP 错误):try { const response = await axios.get('/api/not-found');} catch (error) { // 404 会进入 catch console.log(error.response.status); // 404}Fetch(需要手动检查状态):const response = await fetch('/api/not-found');// 需要手动检查状态码if (!response.ok) { // 404 不会自动进入 catch throw new Error(`HTTP error! status: ${response.status}`);}3. 超时设置Axios(原生支持):axios.get('/api/data', { timeout: 5000 // 5秒超时});Fetch(需要手动实现):const fetchWithTimeout = (url, options = {}, timeout = 5000) => { return Promise.race([ fetch(url, options), new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout) ) ]);};4. 拦截器Axios(原生支持):// 请求拦截器axios.interceptors.request.use(config => { config.headers.Authorization = `Bearer ${token}`; return config;});// 响应拦截器axios.interceptors.response.use( response => response.data, error => { if (error.response.status === 401) { redirectToLogin(); } return Promise.reject(error); });Fetch(需要自行封装):// 需要创建包装函数const fetchWithAuth = (url, options = {}) => { return fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } });};5. 进度监控Axios(原生支持):axios.post('/api/upload', formData, { onUploadProgress: (progressEvent) => { const percent = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); console.log(`上传进度: ${percent}%`); }});Fetch(需要手动实现):// Fetch 没有原生进度支持,需要使用 ReadableStreamconst response = await fetch('/api/download');const reader = response.body.getReader();while (true) { const { done, value } = await reader.read(); if (done) break; // 手动计算进度}选择建议使用 Axios 的场景需要拦截器功能统一添加认证 Token统一错误处理统一日志记录需要进度监控文件上传下载大文件传输需要超时控制防止请求挂起提升用户体验项目复杂度较高多个 API 服务复杂的错误处理逻辑需要请求重试机制需要 IE 支持需要兼容 IE11Node.js 环境服务端渲染Node.js 脚本使用 Fetch 的场景追求最小体积对包体积敏感的项目简单的单页面应用现代浏览器环境不需要 IE 支持现代框架(Next.js, Remix 等)简单的 HTTP 请求不需要复杂的拦截器简单的 GET/POST 请求学习目的理解底层 HTTP API教学演示现代框架中的选择React/Vue/Angular 项目// 推荐使用 Axiosimport axios from 'axios';const api = axios.create({ baseURL: process.env.VUE_APP_API_URL, timeout: 10000});// 添加拦截器api.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config;});Next.js / Remix 项目// 可以使用 Fetch(配合框架的数据获取函数)// app/page.js (Next.js App Router)async function getData() { const res = await fetch('https://api.example.com/data', { next: { revalidate: 3600 } // 缓存配置 }); if (!res.ok) { throw new Error('Failed to fetch'); } return res.json();}总结| 场景 | 推荐工具 ||------|----------|| 企业级应用 | Axios || 需要拦截器 | Axios || 文件上传下载 | Axios || 需要超时控制 | Axios || 需要 IE 支持 | Axios || Node.js 环境 | Axios || 简单项目 | Fetch || 对体积敏感 | Fetch || 现代浏览器 | Fetch || 学习目的 | Fetch |一般建议:中大型项目、需要复杂 HTTP 处理 → 选择 Axios小型项目、简单请求、追求轻量 → 选择 Fetch
阅读 0·3月6日 23:04

使用 axios 时需要注意哪些安全问题?如何防止 XSS、CSRF 等攻击?

在使用 axios 进行 HTTP 请求时,需要关注多种安全问题,包括 XSS、CSRF、敏感信息泄露等。1. XSS(跨站脚本攻击)防护问题描述XSS 攻击可能通过 axios 请求的响应数据注入恶意脚本。防护措施// 1. 响应数据转义import DOMPurify from 'dompurify';axios.interceptors.response.use( (response) => { // 对响应数据进行 XSS 过滤 if (response.data && typeof response.data === 'object') { response.data = sanitizeData(response.data); } return response; });function sanitizeData(data) { if (typeof data === 'string') { return DOMPurify.sanitize(data); } if (Array.isArray(data)) { return data.map(sanitizeData); } if (typeof data === 'object' && data !== null) { return Object.keys(data).reduce((acc, key) => { acc[key] = sanitizeData(data[key]); return acc; }, {}); } return data;}// 2. 设置安全的 Content-Typeaxios.defaults.headers.common['Content-Type'] = 'application/json; charset=utf-8';// 3. 防止 JSON 注入axios.interceptors.request.use( (config) => { if (config.data && typeof config.data === 'object') { // 确保发送的是 JSON,不是可执行的 JavaScript config.data = JSON.stringify(config.data); } return config; });2. CSRF(跨站请求伪造)防护问题描述攻击者诱导用户在已认证的网站上执行非预期的操作。防护措施// 1. 使用 CSRF Tokenconst api = axios.create({ xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', withCredentials: true // 允许携带 cookie});// 2. 手动添加 CSRF Tokenapi.interceptors.request.use((config) => { // 从 meta 标签获取 const token = document.querySelector('meta[name="csrf-token"]')?.content; if (token) { config.headers['X-CSRF-Token'] = token; } // 或从 cookie 获取 const csrfToken = getCookie('csrfToken'); if (csrfToken) { config.headers['X-CSRF-Token'] = csrfToken; } return config;});// 3. 双重 Cookie 验证api.interceptors.request.use((config) => { const sessionId = getCookie('sessionId'); const csrfToken = getCookie('csrfToken'); if (sessionId && csrfToken) { config.headers['X-CSRF-Token'] = csrfToken; // 验证 token 和 session 的关联性 } return config;});// 4. SameSite Cookie 设置// 服务端设置:Set-Cookie: sessionId=xxx; SameSite=Strict; Secure; HttpOnly3. 敏感信息保护Token 安全存储// ❌ 不要直接存储在 localStorage(XSS 风险)localStorage.setItem('token', token);// ✅ 使用 httpOnly cookie(推荐)// 服务端设置:Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict// ✅ 如果必须使用 localStorage,添加额外的安全措施const secureStorage = { set(key, value) { // 添加时间戳和签名 const data = { value, timestamp: Date.now(), signature: generateSignature(value) }; localStorage.setItem(key, JSON.stringify(data)); }, get(key) { const item = localStorage.getItem(key); if (!item) return null; try { const data = JSON.parse(item); // 验证签名 if (data.signature !== generateSignature(data.value)) { this.remove(key); return null; } // 检查过期时间(例如 24 小时) if (Date.now() - data.timestamp > 24 * 60 * 60 * 1000) { this.remove(key); return null; } return data.value; } catch { return null; } }, remove(key) { localStorage.removeItem(key); }};function generateSignature(value) { // 使用简单的哈希或 HMAC return btoa(value + SECRET_KEY);}请求头中的敏感信息// 1. 避免在 URL 中传递敏感信息// ❌ 错误axios.get(`/api/user?password=${password}`);// ✅ 正确axios.post('/api/user', { password });// 2. 请求头加密import CryptoJS from 'crypto-js';axios.interceptors.request.use((config) => { // 对敏感头部进行加密 if (config.headers.Authorization) { config.headers['X-Encrypted'] = '1'; // 或使用自定义加密 // config.headers.Authorization = encrypt(config.headers.Authorization); } return config;});// 3. 限制请求头暴露// 服务端设置:Access-Control-Expose-Headers: limited-headers4. HTTPS 和证书验证// 1. 强制使用 HTTPSconst api = axios.create({ baseURL: 'https://api.example.com', // 必须使用 HTTPS});// 2. Node.js 环境中的证书验证const https = require('https');const fs = require('fs');const api = axios.create({ httpsAgent: new https.Agent({ // 生产环境不要设置为 false rejectUnauthorized: true, // 使用自定义 CA 证书 ca: fs.readFileSync('path/to/ca-cert.pem') })});// 3. 证书固定(Certificate Pinning)const httpsAgent = new https.Agent({ checkServerIdentity: (host, cert) => { const expectedFingerprint = 'AA:BB:CC:DD:EE:FF:...'; const actualFingerprint = cert.fingerprint256; if (actualFingerprint !== expectedFingerprint) { throw new Error('Certificate fingerprint mismatch'); } }});5. 请求参数验证import Joi from 'joi';// 1. 请求参数校验const requestSchema = Joi.object({ email: Joi.string().email().required(), password: Joi.string().min(8).max(32).required(), age: Joi.number().integer().min(0).max(150)});axios.interceptors.request.use((config) => { if (config.data) { const { error } = requestSchema.validate(config.data); if (error) { return Promise.reject(new Error(`Validation error: ${error.message}`)); } } return config;});// 2. URL 参数编码axios.interceptors.request.use((config) => { if (config.params) { config.params = Object.keys(config.params).reduce((acc, key) => { acc[key] = encodeURIComponent(config.params[key]); return acc; }, {}); } return config;});// 3. 防止 SQL 注入(前端层面)function sanitizeInput(input) { if (typeof input !== 'string') return input; // 移除或转义危险字符 return input .replace(/[;\"']/g, '') .replace(/--/g, '') .replace(/\/\*/g, '') .replace(/\*\//g, '');}axios.interceptors.request.use((config) => { if (config.data) { config.data = Object.keys(config.data).reduce((acc, key) => { acc[key] = sanitizeInput(config.data[key]); return acc; }, {}); } return config;});6. 响应安全验证// 1. 验证响应内容类型axios.interceptors.response.use( (response) => { const contentType = response.headers['content-type']; // 确保响应是 JSON if (!contentType || !contentType.includes('application/json')) { return Promise.reject(new Error('Invalid content type')); } return response; });// 2. 验证响应数据签名axios.interceptors.response.use( (response) => { const signature = response.headers['x-response-signature']; const data = JSON.stringify(response.data); if (signature && !verifySignature(data, signature)) { return Promise.reject(new Error('Invalid response signature')); } return response; });function verifySignature(data, signature) { const expectedSignature = CryptoJS.HmacSHA256(data, SECRET_KEY).toString(); return signature === expectedSignature;}7. 安全头部设置// 发送安全相关的请求头const secureApi = axios.create({ headers: { // 防止 MIME 类型嗅探 'X-Content-Type-Options': 'nosniff', // 启用 XSS 过滤器 'X-XSS-Protection': '1; mode=block', // 点击劫持防护 'X-Frame-Options': 'DENY', // 内容安全策略 'Content-Security-Policy': "default-src 'self'", // 严格的传输安全 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains' }});8. 日志和监控// 安全日志记录axios.interceptors.request.use((config) => { // 记录请求日志(不包含敏感信息) securityLogger.info({ type: 'request', url: config.url, method: config.method, timestamp: new Date().toISOString(), // 不要记录 headers 或 data 中的敏感信息 }); return config;});axios.interceptors.response.use( (response) => { securityLogger.info({ type: 'response', url: response.config.url, status: response.status, timestamp: new Date().toISOString() }); return response; }, (error) => { securityLogger.error({ type: 'error', url: error.config?.url, status: error.response?.status, message: error.message, timestamp: new Date().toISOString() }); return Promise.reject(error); });// 异常检测function detectAnomalies(response) { // 检测异常大的响应 const responseSize = JSON.stringify(response.data).length; if (responseSize > 10 * 1024 * 1024) { // 10MB securityLogger.warn('Unusually large response detected'); } // 检测异常的响应时间 const duration = response.config.metadata?.duration; if (duration > 30000) { // 30秒 securityLogger.warn('Slow response detected'); }}9. 完整的安全配置示例import axios from 'axios';import DOMPurify from 'dompurify';import CryptoJS from 'crypto-js';const secureApi = axios.create({ baseURL: 'https://api.example.com', timeout: 10000, withCredentials: true, xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', headers: { 'Content-Type': 'application/json; charset=utf-8', 'X-Content-Type-Options': 'nosniff', 'X-XSS-Protection': '1; mode=block' }});// 请求拦截器 - 安全处理secureApi.interceptors.request.use( (config) => { // 1. 参数验证和清理 if (config.data) { config.data = sanitizeData(config.data); } // 2. 添加请求签名 const timestamp = Date.now(); config.headers['X-Timestamp'] = timestamp; config.headers['X-Signature'] = generateRequestSignature(config, timestamp); // 3. 记录安全日志 logSecurityEvent('request', config); return config; }, (error) => Promise.reject(error));// 响应拦截器 - 安全处理secureApi.interceptors.response.use( (response) => { // 1. 验证响应签名 if (!verifyResponseSignature(response)) { return Promise.reject(new Error('Invalid response signature')); } // 2. XSS 清理 if (response.data) { response.data = sanitizeData(response.data); } // 3. 异常检测 detectAnomalies(response); return response; }, (error) => { logSecurityEvent('error', null, error); return Promise.reject(error); });export default secureApi;安全检查清单[ ] 使用 HTTPS 传输所有数据[ ] 启用 CSRF 防护[ ] 敏感信息使用 httpOnly Cookie 存储[ ] 对输入数据进行验证和清理[ ] 对输出数据进行 XSS 过滤[ ] 设置适当的安全头部[ ] 实现请求/响应签名验证[ ] 记录安全日志并监控异常[ ] 定期更新依赖包[ ] 进行安全审计和渗透测试
阅读 0·3月6日 23:03

axios 的底层实现原理是什么?请说明其核心架构和请求流程

Axios 是一个基于 Promise 的 HTTP 客户端,其核心架构设计优雅,支持浏览器和 Node.js 环境。1. 核心架构概览整体架构图┌─────────────────────────────────────────────────────────────┐│ Axios 入口 ││ axios.create() / axios.get() / axios.post() │└──────────────────────┬──────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ Axios 实例 ││ - defaults (默认配置) ││ - interceptors (拦截器) ││ - request / get / post 等方法 │└──────────────────────┬──────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ 请求处理流程 ││ 1. 合并配置 (mergeConfig) ││ 2. 请求拦截器 (request interceptors) ││ 3. 转换请求数据 (transformRequest) ││ 4. 适配器执行请求 (adapter) ││ 5. 转换响应数据 (transformResponse) ││ 6. 响应拦截器 (response interceptors) │└──────────────────────┬──────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ 适配器层 (Adapter) ││ 浏览器: XMLHttpRequest ││ Node.js: http / https 模块 │└─────────────────────────────────────────────────────────────┘2. 核心源码解析Axios 类定义// 简化版 Axios 类结构class Axios { constructor(instanceConfig) { // 默认配置 this.defaults = instanceConfig; // 拦截器管理器 this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; } // 核心请求方法 request(config) { // 1. 配置处理 if (typeof config === 'string') { config = arguments[1] || {}; config.url = arguments[0]; } // 2. 合并配置 config = mergeConfig(this.defaults, config); // 3. 设置请求方法 if (config.method) { config.method = config.method.toLowerCase(); } // 4. 构建拦截器链 const chain = [dispatchRequest, undefined]; let promise = Promise.resolve(config); // 请求拦截器 this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { chain.unshift(interceptor.fulfilled, interceptor.rejected); }); // 响应拦截器 this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { chain.push(interceptor.fulfilled, interceptor.rejected); }); // 5. 执行链 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise; } // 便捷方法 get(url, config) { return this.request(mergeConfig(config || {}, { method: 'get', url })); } post(url, data, config) { return this.request(mergeConfig(config || {}, { method: 'post', url, data })); } // ... delete, head, options, put, patch}拦截器管理器// 拦截器管理器实现class InterceptorManager { constructor() { this.handlers = []; } // 添加拦截器 use(fulfilled, rejected, options) { this.handlers.push({ fulfilled, rejected, synchronous: options?.synchronous || false, runWhen: options?.runWhen || null }); return this.handlers.length - 1; } // 移除拦截器 eject(id) { if (this.handlers[id]) { this.handlers[id] = null; } } // 遍历拦截器 forEach(fn) { this.handlers.forEach((handler) => { if (handler !== null) { fn(handler); } }); }}请求分发器// 请求分发函数function dispatchRequest(config) { // 1. 检查请求是否已取消 throwIfCancellationRequested(config); // 2. 转换请求数据 config.data = transformData( config.data, config.headers, config.transformRequest ); // 3. 合并 headers config.headers = flattenHeaders( config.headers, config.method ); // 4. 获取适配器 const adapter = config.adapter || defaults.adapter; // 5. 执行适配器 return adapter(config).then( function onAdapterResolution(response) { // 检查请求是否已取消 throwIfCancellationRequested(config); // 转换响应数据 response.data = transformData( response.data, response.headers, config.transformResponse ); return response; }, function onAdapterRejection(reason) { if (!isCancel(reason)) { throwIfCancellationRequested(config); // 转换错误信息 if (reason && reason.response) { reason.response.data = transformData( reason.response.data, reason.response.headers, config.transformResponse ); } } return Promise.reject(reason); } );}3. 适配器层实现浏览器适配器 (XHR)// 浏览器端适配器 - 基于 XMLHttpRequestfunction xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { const requestData = config.data; const requestHeaders = config.headers; const responseType = config.responseType; // 创建 XHR 对象 const request = new XMLHttpRequest(); // 配置请求 request.open(config.method.toUpperCase(), buildURL(config.url, config.params), true); // 设置超时 request.timeout = config.timeout; // 设置响应类型 if (responseType) { request.responseType = responseType; } // 设置 withCredentials if (config.withCredentials) { request.withCredentials = true; } // 设置请求头 Object.keys(requestHeaders).forEach((key) => { if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { delete requestHeaders[key]; } else { request.setRequestHeader(key, requestHeaders[key]); } }); // 处理取消请求 if (config.cancelToken) { config.cancelToken.promise.then(function onCanceled(cancel) { request.abort(); reject(cancel); }); } // 监听状态变化 request.onreadystatechange = function handleLoad() { if (!request || request.readyState !== 4) { return; } // 准备响应对象 const response = { data: responseType === 'text' ? request.responseText : request.response, status: request.status, statusText: request.statusText, headers: parseHeaders(request.getAllResponseHeaders()), config, request }; // 根据状态码处理响应 settle(resolve, reject, response); }; // 处理错误 request.onerror = function handleError() { reject(new Error('Network Error')); }; // 处理超时 request.ontimeout = function handleTimeout() { reject(new Error(`timeout of ${config.timeout}ms exceeded`)); }; // 处理下载进度 if (config.onDownloadProgress) { request.onprogress = config.onDownloadProgress; } // 处理上传进度 if (config.onUploadProgress) { request.upload.onprogress = config.onUploadProgress; } // 发送请求 request.send(requestData); });}Node.js 适配器 (HTTP)// Node.js 端适配器 - 基于 http/https 模块function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolve, reject) { const { url, method, data, headers, timeout, httpAgent, httpsAgent } = config; // 解析 URL const parsed = new URL(url); const isHttps = parsed.protocol === 'https:'; // 选择模块 const http = isHttps ? require('https') : require('http'); // 配置选项 const options = { hostname: parsed.hostname, port: parsed.port, path: parsed.pathname + parsed.search, method: method.toUpperCase(), headers, agent: isHttps ? httpsAgent : httpAgent, timeout }; // 创建请求 const req = http.request(options, (res) => { let responseData = []; res.on('data', (chunk) => { responseData.push(chunk); }); res.on('end', () => { const response = { data: Buffer.concat(responseData).toString(), status: res.statusCode, statusText: res.statusMessage, headers: res.headers, config }; settle(resolve, reject, response); }); }); // 处理错误 req.on('error', (err) => { reject(err); }); // 处理超时 req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); }); // 发送数据 if (data) { req.write(data); } req.end(); });}4. 请求流程详解完整请求流程// 示例:axios.get('/user')// Step 1: 调用 axios.getaxios.get('/user', { params: { id: 123 } });// Step 2: 进入 Axios.request 方法// - 合并配置// - 构建拦截器链// Step 3: 执行请求拦截器// 拦截器 2 (fulfilled) → 拦截器 1 (fulfilled) → dispatchRequest// Step 4: dispatchRequest// - 转换请求数据// - 获取适配器// - 执行适配器// Step 5: 适配器执行// 浏览器: 创建 XHR → 配置 → 发送 → 监听回调// Node.js: 创建 HTTP 请求 → 配置 → 发送 → 监听回调// Step 6: 处理响应// - 转换响应数据// - 执行响应拦截器// - 返回结果// 完整流程代码示例axios.get('/user', { params: { id: 123 } }) .then(response => { // 响应拦截器处理后的结果 console.log(response.data); }) .catch(error => { // 错误处理 console.error(error); });拦截器执行顺序// 请求拦截器执行顺序:后添加的先执行axios.interceptors.request.use(config => { console.log('Request Interceptor 1'); return config;});axios.interceptors.request.use(config => { console.log('Request Interceptor 2'); return config;});// 执行顺序: Interceptor 2 → Interceptor 1 → Request// 响应拦截器执行顺序:先添加的先执行axios.interceptors.response.use(response => { console.log('Response Interceptor 1'); return response;});axios.interceptors.response.use(response => { console.log('Response Interceptor 2'); return response;});// 执行顺序: Response → Interceptor 1 → Interceptor 25. 取消请求实现原理CancelToken 实现// CancelToken 类class CancelToken { constructor(executor) { let resolvePromise; // 创建 Promise this.promise = new Promise((resolve) => { resolvePromise = resolve; }); // 执行器函数 executor((message) => { if (this.reason) { return; // 已经取消过 } this.reason = new Cancel(message); resolvePromise(this.reason); }); } // 静态方法:source static source() { let cancel; const token = new CancelToken((c) => { cancel = c; }); return { token, cancel }; }}// Cancel 类class Cancel { constructor(message) { this.message = message; } toString() { return `Cancel${this.message ? ': ' + this.message : ''}`; }}// 使用示例const source = CancelToken.source();axios.get('/user', { cancelToken: source.token});// 取消请求source.cancel('Operation canceled by the user.');6. 数据转换流程// 请求数据转换function transformRequest(data, headers) { // 如果是对象,转换为 JSON 字符串 if (typeof data === 'object' && data !== null) { headers['Content-Type'] = 'application/json'; return JSON.stringify(data); } return data;}// 响应数据转换function transformResponse(data) { // 尝试解析 JSON if (typeof data === 'string') { try { data = JSON.parse(data); } catch (e) { // 不是 JSON,保持原样 } } return data;}// 在配置中使用transformRequest: [ function(data, headers) { // 自定义转换逻辑 return data; }],transformResponse: [ function(data) { // 自定义转换逻辑 return data; }]7. 配置合并策略// 配置合并函数function mergeConfig(config1, config2) { const config = {}; // 默认配置 const defaultToConfig2 = [ 'url', 'method', 'data', 'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer', 'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'maxContentLength', 'validateStatus', 'maxRedirects', 'httpAgent', 'httpsAgent', 'cancelToken', 'socketPath' ]; defaultToConfig2.forEach(prop => { if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } else if (typeof config1[prop] !== 'undefined') { config[prop] = config1[prop]; } }); // Headers 需要深度合并 config.headers = mergeHeaders(config1.headers, config2.headers); // Params 需要合并 config.params = { ...config1.params, ...config2.params }; return config;}8. 核心设计模式1. 适配器模式 (Adapter)// 统一接口,不同实现const adapter = isBrowser ? xhrAdapter : httpAdapter;2. 责任链模式 (Chain of Responsibility)// 拦截器链const chain = [ requestInterceptor2, undefined, requestInterceptor1, undefined, dispatchRequest, undefined, responseInterceptor1, undefined, responseInterceptor2, undefined];3. 工厂模式 (Factory)// 创建 Axios 实例axios.create = function create(instanceConfig) { return new Axios(instanceConfig);};4. 观察者模式 (Observer)// 取消令牌const source = CancelToken.source();source.token.promise.then(onCanceled);总结Axios 的核心设计亮点:统一的 Promise API:简化异步处理拦截器机制:灵活的请求/响应处理适配器模式:支持多平台运行配置合并策略:灵活的配置管理取消请求:完善的请求控制数据转换:自动化的数据处理错误处理:统一的错误管理机制
阅读 0·3月6日 23:02

使用 axios 时有哪些性能优化技巧?如何减少不必要的网络请求?

在使用 axios 进行 HTTP 请求时,可以通过多种方式优化性能,减少不必要的网络开销,提升用户体验。1. 请求缓存内存缓存class AxiosCache { constructor() { this.cache = new Map(); this.ttl = 5 * 60 * 1000; // 5分钟缓存 } generateKey(config) { return `${config.method}-${config.url}-${JSON.stringify(config.params)}`; } get(config) { const key = this.generateKey(config); const cached = this.cache.get(key); if (cached && Date.now() - cached.timestamp < this.ttl) { return cached.data; } this.cache.delete(key); return null; } set(config, data) { const key = this.generateKey(config); this.cache.set(key, { data, timestamp: Date.now() }); } clear() { this.cache.clear(); }}const cache = new AxiosCache();// 使用缓存的 axios 实例const cachedApi = axios.create();cachedApi.interceptors.request.use(config => { // 检查缓存 const cached = cache.get(config); if (cached) { // 返回缓存数据,取消请求 config.adapter = () => Promise.resolve({ data: cached, status: 200, statusText: 'OK', headers: {}, config }); } return config;});cachedApi.interceptors.response.use(response => { // 缓存响应数据 if (response.config.method === 'get') { cache.set(response.config, response.data); } return response;});使用 Cache API(Service Worker)// 在 Service Worker 中缓存请求self.addEventListener('fetch', event => { if (event.request.url.includes('/api/')) { event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } return fetch(event.request).then(response => { const clone = response.clone(); caches.open('api-cache').then(cache => { cache.put(event.request, clone); }); return response; }); }) ); }});2. 请求去重(防抖)class RequestDeduper { constructor() { this.pendingRequests = new Map(); } generateKey(config) { return `${config.method}-${config.url}-${JSON.stringify(config.params)}-${JSON.stringify(config.data)}`; } async request(config) { const key = this.generateKey(config); // 如果有正在进行的相同请求,返回该 Promise if (this.pendingRequests.has(key)) { return this.pendingRequests.get(key); } // 创建新请求 const promise = axios(config).finally(() => { this.pendingRequests.delete(key); }); this.pendingRequests.set(key, promise); return promise; }}const deduper = new RequestDeduper();// 使用const fetchUser = (id) => deduper.request({ method: 'GET', url: `/api/users/${id}`});// 同时调用多次,只会发送一次请求fetchUser(1);fetchUser(1);fetchUser(1); // 三次调用,一次请求3. 请求合并class RequestBatcher { constructor() { this.batch = []; this.timeout = null; this.delay = 50; // 50ms 内的请求合并 } addRequest(request) { return new Promise((resolve, reject) => { this.batch.push({ request, resolve, reject }); clearTimeout(this.timeout); this.timeout = setTimeout(() => this.flush(), this.delay); }); } async flush() { if (this.batch.length === 0) return; const currentBatch = this.batch; this.batch = []; // 合并请求 const ids = currentBatch.map(item => item.request.id); try { const response = await axios.post('/api/batch', { ids }); // 分发结果 currentBatch.forEach((item, index) => { item.resolve(response.data[index]); }); } catch (error) { currentBatch.forEach(item => { item.reject(error); }); } }}4. 懒加载和分页// 虚拟滚动 + 分页加载class VirtualListLoader { constructor(api, pageSize = 20) { this.api = api; this.pageSize = pageSize; this.cache = new Map(); this.loadingPages = new Set(); } async loadPage(page) { // 检查缓存 if (this.cache.has(page)) { return this.cache.get(page); } // 防止重复加载 if (this.loadingPages.has(page)) { return new Promise(resolve => { const check = setInterval(() => { if (this.cache.has(page)) { clearInterval(check); resolve(this.cache.get(page)); } }, 100); }); } this.loadingPages.add(page); try { const response = await this.api.get('/api/items', { params: { page, pageSize: this.pageSize } }); this.cache.set(page, response.data); return response.data; } finally { this.loadingPages.delete(page); } }}5. 请求优先级管理class PriorityRequestQueue { constructor() { this.queue = []; this.maxConcurrent = 6; // 浏览器最大并发数 this.running = 0; } add(config, priority = 0) { return new Promise((resolve, reject) => { this.queue.push({ config, priority, resolve, reject }); this.queue.sort((a, b) => b.priority - a.priority); this.process(); }); } async process() { if (this.running >= this.maxConcurrent || this.queue.length === 0) { return; } this.running++; const { config, resolve, reject } = this.queue.shift(); try { const response = await axios(config); resolve(response); } catch (error) { reject(error); } finally { this.running--; this.process(); } }}// 使用const queue = new PriorityRequestQueue();// 高优先级请求queue.add({ url: '/api/critical-data' }, 10);// 低优先级请求queue.add({ url: '/api/background-data' }, 1);6. 压缩和精简请求// 请求数据压缩const compressRequest = (data) => { // 移除 undefined 和 null 值 const cleaned = JSON.parse(JSON.stringify(data)); return cleaned;};// 字段精简const minimizeFields = (fields) => { // 只请求需要的字段 return fields.join(',');};axios.get('/api/users', { params: { fields: minimizeFields(['id', 'name', 'avatar']), include: minimizeFields(['posts', 'comments']) }});7. 使用 HTTP/2 Server Push// 服务端配置 HTTP/2 Push// 在响应头中添加 Link 头// Link: </api/related-data>; rel=preload; as=fetch// 客户端预加载const preloadResources = () => { const links = document.querySelectorAll('link[rel=preload][as=fetch]'); links.forEach(link => { axios.get(link.href, { headers: { 'Purpose': 'prefetch' } }); });};8. 连接复用和 Keep-Alive// 使用相同的 axios 实例以复用连接const api = axios.create({ baseURL: 'https://api.example.com', // 启用 keep-alive(在 Node.js 中) httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true })});// 浏览器端自动复用连接9. 请求超时优化// 根据网络状况动态调整超时const getTimeout = () => { const connection = navigator.connection; if (connection) { switch (connection.effectiveType) { case '4g': return 10000; case '3g': return 20000; case '2g': return 30000; default: return 15000; } } return 10000;};axios.get('/api/data', { timeout: getTimeout()});10. 错误重试策略axios.interceptors.response.use(null, async (error) => { const { config } = error; if (!config || !config.retry) { return Promise.reject(error); } config.retryCount = config.retryCount || 0; if (config.retryCount >= config.retry) { return Promise.reject(error); } config.retryCount += 1; // 指数退避 const backoff = Math.pow(2, config.retryCount) * 1000; await new Promise(resolve => setTimeout(resolve, backoff)); return axios(config);});// 使用axios.get('/api/data', { retry: 3, retryDelay: 1000});11. 离线优先策略// 使用 IndexedDB 缓存const offlineFirstRequest = async (config) => { try { // 先尝试网络请求 const response = await axios(config); // 缓存到 IndexedDB await saveToIndexedDB(config, response.data); return response; } catch (error) { // 网络失败,尝试从缓存读取 const cached = await getFromIndexedDB(config); if (cached) { return { data: cached, fromCache: true }; } throw error; }};12. 监控和分析// 性能监控拦截器axios.interceptors.request.use(config => { config.metadata = { startTime: Date.now() }; return config;});axios.interceptors.response.use(response => { const duration = Date.now() - response.config.metadata.startTime; // 上报性能数据 analytics.track('api_request', { url: response.config.url, method: response.config.method, duration, status: response.status, size: JSON.stringify(response.data).length }); // 慢请求警告 if (duration > 3000) { console.warn(`Slow request: ${response.config.url} took ${duration}ms`); } return response;});最佳实践总结| 优化策略 | 适用场景 | 预期效果 || ----- | -------- | ------------ || 请求缓存 | 不频繁变化的数据 | 减少 50-90% 请求 || 请求去重 | 快速连续触发 | 减少重复请求 || 请求合并 | 批量操作 | 减少请求数量 || 分页加载 | 长列表 | 减少初始加载时间 || 优先级队列 | 关键/非关键请求 | 提升关键请求响应 || 数据压缩 | 大数据传输 | 减少传输体积 || 连接复用 | 频繁请求 | 减少连接开销 || 智能超时 | 不稳定网络 | 提升用户体验 || 错误重试 | 临时故障 | 提高成功率 || 离线优先 | 弱网环境 | 提升可用性 |​
阅读 0·3月6日 23:02

如何对使用 axios 的代码进行单元测试和 Mock?请说明常用的测试方法

对使用 axios 的代码进行测试时,需要掌握单元测试、集成测试和 Mock 技术。1. 使用 Jest 和 axios-mock-adapter安装依赖npm install --save-dev jest axios-mock-adapter @testing-library/react基础 Mock 测试// api/user.jsimport axios from 'axios';export const fetchUser = async (userId) => { const response = await axios.get(`/api/users/${userId}`); return response.data;};export const createUser = async (userData) => { const response = await axios.post('/api/users', userData); return response.data;};// __tests__/user.test.jsimport axios from 'axios';import MockAdapter from 'axios-mock-adapter';import { fetchUser, createUser } from '../api/user';describe('User API', () => { let mock; beforeEach(() => { mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); }); test('fetchUser should return user data', async () => { const userData = { id: 1, name: 'John', email: 'john@example.com' }; mock.onGet('/api/users/1').reply(200, userData); const result = await fetchUser(1); expect(result).toEqual(userData); }); test('fetchUser should handle error', async () => { mock.onGet('/api/users/999').reply(404, { message: 'User not found' }); await expect(fetchUser(999)).rejects.toThrow(); }); test('createUser should create new user', async () => { const newUser = { name: 'Jane', email: 'jane@example.com' }; const createdUser = { id: 2, ...newUser }; mock.onPost('/api/users').reply(201, createdUser); const result = await createUser(newUser); expect(result).toEqual(createdUser); });});高级 Mock 配置// __tests__/api.test.jsimport axios from 'axios';import MockAdapter from 'axios-mock-adapter';describe('API Testing', () => { let mock; beforeEach(() => { mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); }); test('mock network error', async () => { mock.onGet('/api/data').networkError(); await expect(axios.get('/api/data')).rejects.toThrow('Network Error'); }); test('mock timeout', async () => { mock.onGet('/api/data').timeout(); await expect(axios.get('/api/data')).rejects.toThrow('timeout'); }); test('mock with function', async () => { mock.onPost('/api/users').reply((config) => { const data = JSON.parse(config.data); if (!data.email) { return [400, { error: 'Email is required' }]; } return [201, { id: 1, ...data }]; }); // 测试成功 const response = await axios.post('/api/users', { name: 'John', email: 'john@example.com' }); expect(response.status).toBe(201); // 测试失败 await expect( axios.post('/api/users', { name: 'John' }) ).rejects.toThrow(); }); test('mock with headers', async () => { mock.onGet('/api/protected', { headers: { Authorization: 'Bearer token123' } }).reply(200, { data: 'protected' }); const response = await axios.get('/api/protected', { headers: { Authorization: 'Bearer token123' } }); expect(response.data).toEqual({ data: 'protected' }); }); test('mock with query params', async () => { mock.onGet('/api/search', { params: { q: 'test' } }) .reply(200, { results: [] }); const response = await axios.get('/api/search', { params: { q: 'test' } }); expect(response.data).toEqual({ results: [] }); });});2. 使用 MSW (Mock Service Worker)安装和配置npm install --save-dev msw// mocks/handlers.jsimport { rest } from 'msw';export const handlers = [ // GET 请求 rest.get('/api/users', (req, res, ctx) => { return res( ctx.status(200), ctx.json([ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ]) ); }), // GET 单个用户 rest.get('/api/users/:id', (req, res, ctx) => { const { id } = req.params; if (id === '999') { return res( ctx.status(404), ctx.json({ message: 'User not found' }) ); } return res( ctx.status(200), ctx.json({ id: Number(id), name: 'John' }) ); }), // POST 请求 rest.post('/api/users', async (req, res, ctx) => { const body = await req.json(); return res( ctx.status(201), ctx.json({ id: 3, ...body }) ); }), // PUT 请求 rest.put('/api/users/:id', async (req, res, ctx) => { const { id } = req.params; const body = await req.json(); return res( ctx.status(200), ctx.json({ id: Number(id), ...body }) ); }), // DELETE 请求 rest.delete('/api/users/:id', (req, res, ctx) => { return res(ctx.status(204)); })];// mocks/server.jsimport { setupServer } from 'msw/node';import { handlers } from './handlers';export const server = setupServer(...handlers);// jest.setup.jsimport { server } from './mocks/server';beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());使用 MSW 进行测试// __tests__/user.integration.test.jsimport { fetchUser, createUser, updateUser, deleteUser } from '../api/user';describe('User API Integration Tests', () => { test('should fetch all users', async () => { const users = await fetchUsers(); expect(users).toHaveLength(2); expect(users[0]).toHaveProperty('id'); expect(users[0]).toHaveProperty('name'); }); test('should fetch single user', async () => { const user = await fetchUser(1); expect(user).toEqual({ id: 1, name: 'John' }); }); test('should handle 404 error', async () => { await expect(fetchUser(999)).rejects.toThrow(); }); test('should create user', async () => { const newUser = { name: 'Bob', email: 'bob@example.com' }; const created = await createUser(newUser); expect(created).toMatchObject(newUser); expect(created).toHaveProperty('id'); }); test('should update user', async () => { const updates = { name: 'John Updated' }; const updated = await updateUser(1, updates); expect(updated.name).toBe('John Updated'); }); test('should delete user', async () => { await expect(deleteUser(1)).resolves.not.toThrow(); });});动态覆盖 Handler// __tests__/dynamic-mock.test.jsimport { rest } from 'msw';import { server } from '../mocks/server';test('should handle server error', async () => { // 临时覆盖 handler server.use( rest.get('/api/users', (req, res, ctx) => { return res( ctx.status(500), ctx.json({ error: 'Internal Server Error' }) ); }) ); await expect(fetchUsers()).rejects.toThrow();});test('should handle network error', async () => { server.use( rest.get('/api/users', (req, res) => { return res.networkError('Failed to connect'); }) ); await expect(fetchUsers()).rejects.toThrow('Failed to connect');});3. React 组件测试使用 React Testing Library// components/UserProfile.jsximport React, { useEffect, useState } from 'react';import axios from 'axios';export const UserProfile = ({ userId }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchUser = async () => { try { setLoading(true); const response = await axios.get(`/api/users/${userId}`); setUser(response.data); } catch (err) { setError(err.message); } finally { setLoading(false); } }; fetchUser(); }, [userId]); if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> );};// __tests__/UserProfile.test.jsximport React from 'react';import { render, screen, waitFor } from '@testing-library/react';import { UserProfile } from '../components/UserProfile';import { server } from '../mocks/server';import { rest } from 'msw';describe('UserProfile Component', () => { test('should display loading state initially', () => { render(<UserProfile userId={1} />); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); test('should display user data after loading', async () => { render(<UserProfile userId={1} />); await waitFor(() => { expect(screen.getByText('John')).toBeInTheDocument(); }); expect(screen.getByText('john@example.com')).toBeInTheDocument(); }); test('should display error message on failure', async () => { server.use( rest.get('/api/users/999', (req, res, ctx) => { return res(ctx.status(404)); }) ); render(<UserProfile userId={999} />); await waitFor(() => { expect(screen.getByText(/Error:/)).toBeInTheDocument(); }); });});4. Vue 组件测试使用 Vue Test Utils// components/UserProfile.vue<template> <div> <div v-if="loading">Loading...</div> <div v-else-if="error">Error: {{ error }}</div> <div v-else> <h1>{{ user.name }}</h1> <p>{{ user.email }}</p> </div> </div></template><script>import { ref, onMounted } from 'vue';import axios from 'axios';export default { props: ['userId'], setup(props) { const user = ref(null); const loading = ref(true); const error = ref(null); onMounted(async () => { try { const response = await axios.get(`/api/users/${props.userId}`); user.value = response.data; } catch (err) { error.value = err.message; } finally { loading.value = false; } }); return { user, loading, error }; }};</script>// __tests__/UserProfile.spec.jsimport { mount } from '@vue/test-utils';import { describe, it, expect } from 'vitest';import UserProfile from '../components/UserProfile.vue';import { server } from '../mocks/server';describe('UserProfile', () => { it('should display loading state initially', () => { const wrapper = mount(UserProfile, { props: { userId: 1 } }); expect(wrapper.text()).toContain('Loading...'); }); it('should display user data after loading', async () => { const wrapper = mount(UserProfile, { props: { userId: 1 } }); await wrapper.vm.$nextTick(); await new Promise(resolve => setTimeout(resolve, 0)); expect(wrapper.text()).toContain('John'); });});5. 自定义 Hook/Composable 测试// hooks/useApi.jsimport { useState, useEffect } from 'react';import axios from 'axios';export const useApi = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { setLoading(true); const response = await axios.get(url); setData(response.data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchData(); }, [url]); return { data, loading, error };};// __tests__/useApi.test.jsimport { renderHook, waitFor } from '@testing-library/react';import { useApi } from '../hooks/useApi';describe('useApi Hook', () => { test('should return loading state initially', () => { const { result } = renderHook(() => useApi('/api/data')); expect(result.current.loading).toBe(true); expect(result.current.data).toBeNull(); expect(result.current.error).toBeNull(); }); test('should return data after successful request', async () => { const { result } = renderHook(() => useApi('/api/users')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.data).toHaveLength(2); expect(result.current.error).toBeNull(); }); test('should return error on failed request', async () => { const { result } = renderHook(() => useApi('/api/error')); await waitFor(() => { expect(result.current.loading).toBe(false); }); expect(result.current.error).not.toBeNull(); expect(result.current.data).toBeNull(); });});6. E2E 测试使用 Cypress// cypress/integration/api.spec.jsdescribe('API Tests', () => { beforeEach(() => { // 拦截 API 请求 cy.intercept('GET', '/api/users', { statusCode: 200, body: [ { id: 1, name: 'John' }, { id: 2, name: 'Jane' } ] }).as('getUsers'); }); it('should display users list', () => { cy.visit('/users'); cy.wait('@getUsers'); cy.get('[data-testid="user-list"]').should('have.length', 2); cy.contains('John').should('be.visible'); }); it('should handle API error', () => { cy.intercept('GET', '/api/users', { statusCode: 500, body: { error: 'Server Error' } }).as('getUsersError'); cy.visit('/users'); cy.wait('@getUsersError'); cy.contains('Error loading users').should('be.visible'); }); it('should create new user', () => { cy.intercept('POST', '/api/users', { statusCode: 201, body: { id: 3, name: 'New User' } }).as('createUser'); cy.visit('/users/new'); cy.get('input[name="name"]').type('New User'); cy.get('button[type="submit"]').click(); cy.wait('@createUser').its('request.body').should('deep.equal', { name: 'New User' }); cy.url().should('include', '/users'); });});7. 测试最佳实践测试文件组织src/├── api/│ ├── user.js│ └── __tests__/│ └── user.test.js├── components/│ ├── UserProfile.jsx│ └── __tests__/│ └── UserProfile.test.jsx├── hooks/│ ├── useApi.js│ └── __tests__/│ └── useApi.test.js└── mocks/ ├── handlers.js └── server.js测试工具函数// test-utils.jsimport { render } from '@testing-library/react';import { QueryClient, QueryClientProvider } from '@tanstack/react-query';export const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, }, },});export function renderWithClient(ui) { const testQueryClient = createTestQueryClient(); const { rerender, ...result } = render( <QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider> ); return { ...result, rerender: (rerenderUi) => rerender( <QueryClientProvider client={testQueryClient}>{rerenderUi}</QueryClientProvider> ), };}测试策略总结| 测试类型 | 工具 | 适用场景 || ------- | -------------------------------------- | ------------ || 单元测试 | Jest + axios-mock-adapter | 测试 API 函数 || 集成测试 | MSW | 测试组件与 API 交互 || 组件测试 | React Testing Library / Vue Test Utils | 测试 UI 组件 || Hook 测试 | React Testing Library | 测试自定义 Hooks || E2E 测试 | Cypress / Playwright | 端到端测试 |最佳实践使用 MSW:推荐用于集成测试,更接近真实环境分离关注点:测试逻辑和 UI 分离清理副作用:每次测试后清理 mocks测试错误场景:不仅要测试成功,也要测试失败避免真实请求:测试时不应发送真实 HTTP 请求使用数据属性:使用 data-testid 选择元素保持测试独立:每个测试应该独立运行
阅读 0·3月6日 23:02

axios 的版本更新历史中有哪些重要变化?如何处理不同版本的兼容性问题

Axios 从 0.x 版本发展到 1.x 版本,经历了多次重大更新,了解这些变化对于维护项目和升级非常重要。1. 版本更新历史概览主要版本里程碑┌─────────────────────────────────────────────────────────────────┐│ Axios 版本演进时间线 │├─────────────────────────────────────────────────────────────────┤│ 2014 │ v0.1.0 │ 初始发布,基于 Promise 的 HTTP 客户端 ││ 2015 │ v0.9.0 │ 添加拦截器功能 ││ 2016 │ v0.12.0 │ 添加取消请求功能 (CancelToken) ││ 2017 │ v0.16.0 │ 支持 async/await ││ 2018 │ v0.18.0 │ 安全更新,修复 XSS 漏洞 ││ 2019 │ v0.19.0 │ 改进错误处理,添加 validateStatus ││ 2020 │ v0.20.0 │ 支持 TypeScript 类型改进 ││ 2020 │ v0.21.0 │ 重大安全更新 ││ 2022 │ v1.0.0 │ 正式发布 1.0 版本,Promise 化改进 ││ 2023 │ v1.6.0 │ 支持 Fetch API 适配器 │└─────────────────────────────────────────────────────────────────┘2. 重要版本变更详解v0.19.0 重大变更// v0.19.0 之前 - 错误处理axios.get('/user') .catch(error => { // 404 错误也会进入 catch if (error.response) { // 服务器响应了错误状态码 } });// v0.19.0 之后 - 引入 validateStatusaxios.get('/user', { validateStatus: function (status) { // 默认:status >= 200 && status < 300 return status < 500; // 只有 500+ 才抛出错误 }});// 自定义错误处理const instance = axios.create({ validateStatus: (status) => { return status >= 200 && status < 300; // 默认值 }});v0.20.0 TypeScript 改进// v0.20.0 之前的类型定义import axios from 'axios';// 类型定义不够完善axios.get('/user').then(response => { // response.data 类型为 any});// v0.20.0 之后的改进import axios, { AxiosResponse } from 'axios';interface User { id: number; name: string;}// 泛型支持axios.get<User>('/user').then((response: AxiosResponse<User>) => { // response.data 类型为 User const user: User = response.data;});// 请求配置类型import { AxiosRequestConfig } from 'axios';const config: AxiosRequestConfig = { url: '/user', method: 'get', headers: { 'Content-Type': 'application/json' }};v1.0.0 重大变更// v1.0.0 之前 - CancelToken (已废弃)import axios from 'axios';const CancelToken = axios.CancelToken;const source = CancelToken.source();axios.get('/user', { cancelToken: source.token});source.cancel('Operation canceled');// v1.0.0 之后 - AbortController (推荐)const controller = new AbortController();axios.get('/user', { signal: controller.signal});controller.abort('Operation canceled');// 兼容性处理 - 同时支持两种方式function makeRequest(url, cancelTokenOrSignal) { const config = {}; if (cancelTokenOrSignal instanceof AbortSignal) { config.signal = cancelTokenOrSignal; } else { config.cancelToken = cancelTokenOrSignal; } return axios.get(url, config);}v1.6.0 Fetch API 适配器// v1.6.0 引入 Fetch API 适配器import axios from 'axios';// 使用 Fetch API 适配器const instance = axios.create({ adapter: 'fetch' // 或 require('axios/adapters/fetch')});// 传统 XHR 适配器(默认)const xhrInstance = axios.create({ adapter: 'http' // 或 require('axios/adapters/xhr')});// 条件选择适配器const instance = axios.create({ adapter: typeof window !== 'undefined' && 'fetch' in window ? 'fetch' : 'xhr'});3. 兼容性问题与解决方案浏览器兼容性// 检查浏览器兼容性function checkAxiosCompatibility() { const issues = []; // 检查 Promise 支持 if (typeof Promise === 'undefined') { issues.push('Promise not supported'); } // 检查 XMLHttpRequest if (typeof XMLHttpRequest === 'undefined') { issues.push('XMLHttpRequest not supported'); } // 检查 Fetch API (如果使用 fetch 适配器) if (typeof fetch === 'undefined') { issues.push('Fetch API not supported (optional)'); } return issues;}// Polyfill 方案// 1. Promise Polyfillimport 'es6-promise/auto';// 2. Fetch API Polyfillimport 'whatwg-fetch';// 3. 完整兼容性处理import axios from 'axios';// 配置适配器回退if (typeof XMLHttpRequest === 'undefined') { // Node.js 环境 axios.defaults.adapter = require('axios/lib/adapters/http');}Node.js 版本兼容性// package.json 中的引擎要求{ "engines": { "node": ">=12.0.0" }}// 运行时检查const semver = require('semver');const nodeVersion = process.version;if (!semver.satisfies(nodeVersion, '>=12.0.0')) { console.warn(`Axios requires Node.js >= 12.0.0, current: ${nodeVersion}`);}// 不同 Node 版本的适配const http = require('http');const https = require('https');const instance = axios.create({ httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }), // Node.js 12+ 支持 maxBodyLength: Infinity, maxContentLength: Infinity});版本检测与适配// axios-version-compat.jsimport axios from 'axios';// 获取 axios 版本const axiosVersion = axios.VERSION;// 版本比较工具function compareVersions(v1, v2) { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const p1 = parts1[i] || 0; const p2 = parts2[i] || 0; if (p1 > p2) return 1; if (p1 < p2) return -1; } return 0;}// 根据版本选择 APIexport function createCompatibleInstance(config = {}) { const isV1 = compareVersions(axiosVersion, '1.0.0') >= 0; const instance = axios.create({ ...config, // v1.0.0+ 的默认配置 ...(isV1 && { transitional: { clarifyTimeoutError: true } }) }); // 版本特定的拦截器 if (isV1) { // v1.x 的拦截器 instance.interceptors.request.use( (config) => { // v1.x 特有的处理 return config; }, (error) => Promise.reject(error) ); } else { // v0.x 的拦截器 instance.interceptors.request.use( (config) => config, (error) => Promise.reject(error) ); } return instance;}// 取消请求的兼容封装export function createCancelToken() { const isV1 = compareVersions(axiosVersion, '1.0.0') >= 0; if (isV1) { // v1.x 使用 AbortController const controller = new AbortController(); return { token: controller.signal, cancel: (message) => controller.abort(message) }; } else { // v0.x 使用 CancelToken const source = axios.CancelToken.source(); return source; }}4. 升级指南从 v0.x 升级到 v1.x// 升级检查清单const upgradeChecklist = { // 1. 检查 CancelToken 使用 checkCancelToken: () => { // 替换为 AbortController // 旧代码 const source = axios.CancelToken.source(); // 新代码 const controller = new AbortController(); }, // 2. 检查错误处理 checkErrorHandling: () => { // 确保 validateStatus 配置正确 axios.defaults.validateStatus = (status) => { return status >= 200 && status < 300; }; }, // 3. 检查 TypeScript 类型 checkTypeScript: () => { // 更新类型导入 // import { AxiosResponse } from 'axios'; }, // 4. 检查适配器配置 checkAdapter: () => { // 如果使用自定义适配器,需要更新 }};// 自动升级脚本function migrateAxiosCode(code) { // 替换 CancelToken code = code.replace( /axios\.CancelToken\.source\(\)/g, 'new AbortController()' ); // 替换 cancelToken 配置 code = code.replace( /cancelToken:\s*source\.token/g, 'signal: controller.signal' ); // 替换 cancel 调用 code = code.replace( /source\.cancel\(/g, 'controller.abort(' ); return code;}依赖版本锁定// package.json - 锁定版本{ "dependencies": { "axios": "^1.6.0" }, "devDependencies": { "@types/axios": "^0.14.0" }, "resolutions": { "axios": "1.6.0" }}// package-lock.json / yarn.lock// 确保锁定文件提交到版本控制5. 版本兼容性测试// __tests__/axios-compat.test.jsimport axios from 'axios';describe('Axios Version Compatibility', () => { test('should have correct version', () => { expect(axios.VERSION).toBeDefined(); expect(axios.VERSION).toMatch(/^\d+\.\d+\.\d+/); }); test('should support AbortController in v1.x', () => { const [major] = axios.VERSION.split('.').map(Number); if (major >= 1) { const controller = new AbortController(); expect(() => { axios.get('/test', { signal: controller.signal }); }).not.toThrow(); } }); test('should support legacy CancelToken', () => { if (axios.CancelToken) { const source = axios.CancelToken.source(); expect(source.token).toBeDefined(); expect(typeof source.cancel).toBe('function'); } }); test('should handle errors consistently', async () => { try { await axios.get('http://invalid-domain-that-does-not-exist.com'); } catch (error) { expect(error).toBeDefined(); expect(error.message).toBeDefined(); } });});6. 最佳实践版本管理策略// 1. 使用固定版本// package.json{ "dependencies": { "axios": "1.6.0" // 不使用 ^ 或 ~ }}// 2. 封装 axios,隔离版本影响// api/client.jsimport axios from 'axios';const apiClient = axios.create({ baseURL: process.env.API_URL, timeout: 10000, headers: { 'Content-Type': 'application/json' }});// 封装请求方法,隐藏 axios 细节export const api = { get: (url, config) => apiClient.get(url, config), post: (url, data, config) => apiClient.post(url, data, config), // ... 其他方法};// 3. 定期更新策略// 创建更新脚本 scripts/update-axios.jsconst { execSync } = require('child_process');const axios = require('axios/package.json');console.log(`Current axios version: ${axios.version}`);// 检查最新版本fetch('https://registry.npmjs.org/axios') .then(res => res.json()) .then(data => { const latest = data['dist-tags'].latest; console.log(`Latest axios version: ${latest}`); if (latest !== axios.version) { console.log('Update available. Run: npm update axios'); } });兼容性配置模板// config/axios.jsimport axios from 'axios';// 检测环境const isBrowser = typeof window !== 'undefined';const isNode = !isBrowser;const axiosVersion = axios.VERSION;// 基础配置const baseConfig = { timeout: 10000, headers: { 'Content-Type': 'application/json' }};// 环境特定配置const envConfig = isNode ? { // Node.js 配置 httpAgent: new (require('http').Agent)({ keepAlive: true }), httpsAgent: new (require('https').Agent)({ keepAlive: true })} : { // 浏览器配置 withCredentials: true, xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN'};// 版本特定配置const versionConfig = axiosVersion.startsWith('1.') ? { // v1.x 配置 transitional: { clarifyTimeoutError: true, forcedJSONParsing: true }} : { // v0.x 配置};// 创建实例const instance = axios.create({ ...baseConfig, ...envConfig, ...versionConfig});export default instance;版本兼容性速查表| 特性 | v0.18.x | v0.19.x | v0.20.x | v0.21.x | v1.0.0+ || --------------- | ------- | ------- | ------- | ------- | ------- || CancelToken | ✅ | ✅ | ✅ | ✅ | ⚠️ 废弃 || AbortController | ❌ | ❌ | ❌ | ❌ | ✅ || Fetch 适配器 | ❌ | ❌ | ❌ | ❌ | ✅ || validateStatus | ✅ | ✅ 改进 | ✅ | ✅ | ✅ || TypeScript | ⚠️ | ⚠️ | ✅ 改进 | ✅ | ✅ || ESM 支持 | ⚠️ | ⚠️ | ⚠️ | ✅ | ✅ || 安全修复 | ✅ | ✅ | ✅ | ✅ | ✅ |总结版本选择:新项目建议使用 v1.6.0+,旧项目逐步迁移兼容性处理:封装 axios 使用,隔离版本差异升级策略:先测试后升级,使用锁定文件API 选择:优先使用新标准 (AbortController)监控更新:关注安全更新和重大变更
阅读 0·3月6日 23:01