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

JavaScript

JavaScript 是一种基于脚本的编程语言,主要用于在 Web 页面上实现交互式的效果和动态的内容。JavaScript 是一种解释性语言,不需要编译就可以直接在浏览器中运行。 JavaScript 的主要特点包括: 轻量级:JavaScript 代码通常比较短小,可以快速加载和执行。 可移植性:JavaScript 可以在各种不同的浏览器和操作系统上运行。 面向对象编程:JavaScript 支持面向对象编程,包括对象、继承、封装等特性,可以用于构建复杂的软件系统。 客户端脚本语言:JavaScript 主要用于在 Web 页面上实现交互式的效果和动态的内容,可以与 HTML 和 CSS 一起使用。 异步编程:JavaScript 支持异步编程,可以利用回调函数、Promise、async/await 等方式实现异步操作,提高程序的性能和响应能力。 JavaScript 在 Web 开发中扮演着非常重要的角色,它可以用于实现各种交互式的效果和动态的内容,如表单验证、动画效果、AJAX 等。同时,JavaScript 也可以用于开发各种类型的应用程序,如桌面应用程序、移动应用程序等。 如果您想成为一名 Web 开发人员,JavaScript 是必不可少的编程语言之一,需要掌握 JavaScript 的基本语法和常用的开发框架和库,如 React、Angular、Vue 等。掌握 JavaScript 可以帮助您更加高效和灵活地实现 Web 开发中的各种功能和效果,为自己的职业发展和个人成长打下坚实的基础。
JavaScript
查看更多相关内容
axios 中如何实现并发请求和取消请求?请提供代码示例## Axios 并发请求 Axios 提供了 `axios.all()` 和 `axios.spread()` 方法来处理并发请求,同时也支持使用原生的 `Promise.all()`。 ### 1. 使用 Promise.all()(推荐) ```javascript // 同时发送多个请求 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()(传统方式) ```javascript 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. 并发请求的错误处理 ```javascript 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. 限制并发数量 ```javascript // 使用 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+) ```javascript // 创建 AbortController const controller = new AbortController(); // 发送请求时传入 signal axios.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 组件中使用 ```javascript 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 组件中使用 ```javascript <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. 取消多个请求 ```javascript const controllers = new Map(); // 发送请求时保存 controller function 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. 请求超时自动取消 ```javascript 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. 取消请求的工具函数封装 ```javascript 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(); ``` ## 最佳实践 1. **组件卸载时取消请求**:避免内存泄漏和状态更新错误 2. **重复请求时取消前一个**:搜索框、表单提交等场景 3. **设置合理的超时时间**:防止请求挂起 4. **正确处理取消错误**:区分取消错误和业务错误 5. **使用 AbortController**:现代浏览器标准 API,兼容性好
服务端 · 3月7日 12:10
如何在 axios 中实现请求和响应拦截器?请举例说明实际应用场景## Axios 拦截器概述 Axios 拦截器允许你在请求发送前或响应接收后统一处理数据,是实现全局配置、错误处理、权限验证等功能的重要机制。 ## 请求拦截器(Request Interceptors) ### 基本用法 ```javascript // 添加请求拦截器 axios.interceptors.request.use( function (config) { // 在发送请求之前做些什么 return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); } ); ``` ### 实际应用场景 #### 1. 统一添加认证 Token ```javascript axios.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, error => { return Promise.reject(error); } ); ``` #### 2. 添加时间戳防止缓存 ```javascript axios.interceptors.request.use( config => { if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() }; } return config; } ); ``` #### 3. 显示加载状态 ```javascript let requestCount = 0; axios.interceptors.request.use( config => { requestCount++; if (requestCount === 1) { // 显示全局 loading showLoading(); } return config; } ); ``` ## 响应拦截器(Response Interceptors) ### 基本用法 ```javascript // 添加响应拦截器 axios.interceptors.response.use( function (response) { // 对响应数据做点什么 return response; }, function (error) { // 对响应错误做点什么 return Promise.reject(error); } ); ``` ### 实际应用场景 #### 1. 统一错误处理 ```javascript 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. 响应数据格式化 ```javascript 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. 隐藏加载状态 ```javascript axios.interceptors.response.use( response => { requestCount--; if (requestCount === 0) { hideLoading(); } return response; }, error => { requestCount--; if (requestCount === 0) { hideLoading(); } return Promise.reject(error); } ); ``` ## 移除拦截器 ```javascript const myInterceptor = axios.interceptors.request.use(() => {}); axios.interceptors.request.eject(myInterceptor); ``` ## 为实例添加拦截器 ```javascript const instance = axios.create({ baseURL: 'https://api.example.com' }); instance.interceptors.request.use(config => { // 只对当前实例生效 return config; }); ``` ## 多个拦截器的执行顺序 ```javascript axios.interceptors.request.use(config => { console.log('请求拦截器 1'); return config; }); axios.interceptors.request.use(config => { console.log('请求拦截器 2'); return config; }); // 执行顺序:请求拦截器 2 → 请求拦截器 1 → 发送请求 // 响应拦截器执行顺序与添加顺序相反 ``` ## 最佳实践 1. **错误处理要完整**:请求和响应拦截器都要处理错误情况 2. **记得返回 config/response**:否则请求不会继续 3. **使用实例隔离**:不同服务使用不同实例,避免相互影响 4. **避免副作用**:拦截器中不要修改原始参数对象 5. **添加请求标识**:方便调试和追踪
服务端 · 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 会返回一个包含以下属性的错误对象: ```javascript 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. 基础错误处理 ```javascript 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 处理错误 ```javascript 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. 全局错误处理(通过拦截器) ```javascript // 创建 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. 超时错误处理 ```javascript 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. 重试机制 ```javascript 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. 请求取消错误处理 ```javascript 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('用户取消操作'); ``` ## 最佳实践 1. **分层处理**:全局拦截器 + 业务层处理 2. **用户友好**:错误信息要清晰易懂 3. **错误分类**:区分可恢复和不可恢复错误 4. **日志记录**:记录错误便于排查问题 5. **降级策略**:网络错误时提供缓存数据或默认数据 ## 错误处理流程图 ``` 请求失败 ↓ 检查 error.response ↓ 存在 → HTTP 错误 → 根据状态码处理 ↓ 不存在 → 检查 error.request ↓ 存在 → 网络错误 → 提示用户检查网络 ↓ 不存在 → 配置错误 → 检查代码 ```
服务端 · 3月7日 12:10
axios 有哪些高级特性?如文件上传下载、进度监控、CSRF 防护等## Axios 高级特性概览 Axios 不仅支持基本的 HTTP 请求,还提供了许多高级特性,包括文件上传下载、进度监控、CSRF 防护、请求转换等。 ## 1. 文件上传 ### 基础文件上传 ```javascript // 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); }); ``` ### 多文件上传 ```javascript 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; }; ``` ### 带进度条的文件上传 ```javascript 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. 文件下载 ### 基础文件下载 ```javascript 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'); ``` ### 带进度条的文件下载 ```javascript 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 处理 ```javascript const instance = axios.create({ // 从 cookie 中读取 CSRF token 的字段名 xsrfCookieName: 'XSRF-TOKEN', // 请求头中发送 CSRF token 的字段名 xsrfHeaderName: 'X-XSRF-TOKEN', // 允许携带 cookie withCredentials: true }); ``` ### 手动设置 CSRF Token ```javascript // 从 meta 标签获取 CSRF token const 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. 请求和响应转换 ### 请求数据转换 ```javascript 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); } ] }); ``` ### 响应数据转换 ```javascript 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. 参数序列化 ### 自定义参数序列化 ```javascript 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[]=axios ``` ## 6. 代理配置 ```javascript // 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. 适配器 ### 自定义适配器 ```javascript 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. 验证状态码 ```javascript const instance = axios.create({ // 自定义合法状态码 validateStatus: (status) => { return status >= 200 && status < 300; // 默认值 // 或者接受所有状态码 // return true; // 或者只接受特定状态码 // return [200, 201, 204].includes(status); } }); ``` ## 9. 最大内容长度和重定向 ```javascript const instance = axios.create({ // 最大响应内容长度(字节) maxContentLength: 2000, // 最大请求内容长度(字节) maxBodyLength: 2000, // 最大重定向次数 maxRedirects: 5, // 在 Node.js 中遵循重定向 // 在浏览器中此配置无效(浏览器自动处理重定向) }); ``` ## 10. 完整的高级配置示例 ```javascript 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; ``` ## 最佳实践 1. **文件上传**:始终使用 FormData,设置正确的 Content-Type 2. **文件下载**:设置 responseType 为 'blob' 或 'arraybuffer' 3. **CSRF 防护**:正确配置 xsrfCookieName 和 xsrfHeaderName 4. **进度监控**:在需要用户体验的场景中使用 5. **数据转换**:统一处理请求和响应数据格式 6. **错误处理**:在 transformResponse 中统一处理业务错误
服务端 · 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(自动处理):** ```javascript // 自动将响应转换为 JSON const response = await axios.get('/api/users'); console.log(response.data); // 已经是 JavaScript 对象 // 自动将请求体转换为 JSON await axios.post('/api/users', { name: 'John' }); ``` **Fetch(手动处理):** ```javascript // 需要手动调用 .json() const response = await fetch('/api/users'); const data = await response.json(); // 额外的 await console.log(data); // 需要手动设置 headers 和 stringify await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'John' }) }); ``` ### 2. 错误处理 **Axios(自动处理 HTTP 错误):** ```javascript try { const response = await axios.get('/api/not-found'); } catch (error) { // 404 会进入 catch console.log(error.response.status); // 404 } ``` **Fetch(需要手动检查状态):** ```javascript const response = await fetch('/api/not-found'); // 需要手动检查状态码 if (!response.ok) { // 404 不会自动进入 catch throw new Error(`HTTP error! status: ${response.status}`); } ``` ### 3. 超时设置 **Axios(原生支持):** ```javascript axios.get('/api/data', { timeout: 5000 // 5秒超时 }); ``` **Fetch(需要手动实现):** ```javascript const fetchWithTimeout = (url, options = {}, timeout = 5000) => { return Promise.race([ fetch(url, options), new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), timeout) ) ]); }; ``` ### 4. 拦截器 **Axios(原生支持):** ```javascript // 请求拦截器 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(需要自行封装):** ```javascript // 需要创建包装函数 const fetchWithAuth = (url, options = {}) => { return fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token}` } }); }; ``` ### 5. 进度监控 **Axios(原生支持):** ```javascript axios.post('/api/upload', formData, { onUploadProgress: (progressEvent) => { const percent = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); console.log(`上传进度: ${percent}%`); } }); ``` **Fetch(需要手动实现):** ```javascript // Fetch 没有原生进度支持,需要使用 ReadableStream const response = await fetch('/api/download'); const reader = response.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; // 手动计算进度 } ``` ## 选择建议 ### 使用 Axios 的场景 1. **需要拦截器功能** - 统一添加认证 Token - 统一错误处理 - 统一日志记录 2. **需要进度监控** - 文件上传下载 - 大文件传输 3. **需要超时控制** - 防止请求挂起 - 提升用户体验 4. **项目复杂度较高** - 多个 API 服务 - 复杂的错误处理逻辑 - 需要请求重试机制 5. **需要 IE 支持** - 需要兼容 IE11 6. **Node.js 环境** - 服务端渲染 - Node.js 脚本 ### 使用 Fetch 的场景 1. **追求最小体积** - 对包体积敏感的项目 - 简单的单页面应用 2. **现代浏览器环境** - 不需要 IE 支持 - 现代框架(Next.js, Remix 等) 3. **简单的 HTTP 请求** - 不需要复杂的拦截器 - 简单的 GET/POST 请求 4. **学习目的** - 理解底层 HTTP API - 教学演示 ## 现代框架中的选择 ### React/Vue/Angular 项目 ```javascript // 推荐使用 Axios import 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 项目 ```javascript // 可以使用 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**
服务端 · 3月6日 23:04
使用 axios 时需要注意哪些安全问题?如何防止 XSS、CSRF 等攻击?在使用 axios 进行 HTTP 请求时,需要关注多种安全问题,包括 XSS、CSRF、敏感信息泄露等。 ## 1. XSS(跨站脚本攻击)防护 ### 问题描述 XSS 攻击可能通过 axios 请求的响应数据注入恶意脚本。 ### 防护措施 ```javascript // 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-Type axios.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(跨站请求伪造)防护 ### 问题描述 攻击者诱导用户在已认证的网站上执行非预期的操作。 ### 防护措施 ```javascript // 1. 使用 CSRF Token const api = axios.create({ xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', withCredentials: true // 允许携带 cookie }); // 2. 手动添加 CSRF Token api.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; HttpOnly ``` ## 3. 敏感信息保护 ### Token 安全存储 ```javascript // ❌ 不要直接存储在 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); } ``` ### 请求头中的敏感信息 ```javascript // 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-headers ``` ## 4. HTTPS 和证书验证 ```javascript // 1. 强制使用 HTTPS const 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. 请求参数验证 ```javascript 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. 响应安全验证 ```javascript // 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. 安全头部设置 ```javascript // 发送安全相关的请求头 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. 日志和监控 ```javascript // 安全日志记录 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. 完整的安全配置示例 ```javascript 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 过滤 * [ ] 设置适当的安全头部 * [ ] 实现请求/响应签名验证 * [ ] 记录安全日志并监控异常 * [ ] 定期更新依赖包 * [ ] 进行安全审计和渗透测试
服务端 · 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 类定义 ```javascript // 简化版 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 } ``` ### 拦截器管理器 ```javascript // 拦截器管理器实现 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); } }); } } ``` ### 请求分发器 ```javascript // 请求分发函数 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) ```javascript // 浏览器端适配器 - 基于 XMLHttpRequest function 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) ```javascript // 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. 请求流程详解 ### 完整请求流程 ```javascript // 示例:axios.get('/user') // Step 1: 调用 axios.get axios.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); }); ``` ### 拦截器执行顺序 ```javascript // 请求拦截器执行顺序:后添加的先执行 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 2 ``` ## 5. 取消请求实现原理 ### CancelToken 实现 ```javascript // 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. 数据转换流程 ```javascript // 请求数据转换 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. 配置合并策略 ```javascript // 配置合并函数 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) ```javascript // 统一接口,不同实现 const adapter = isBrowser ? xhrAdapter : httpAdapter; ``` ### 2. 责任链模式 (Chain of Responsibility) ```javascript // 拦截器链 const chain = [ requestInterceptor2, undefined, requestInterceptor1, undefined, dispatchRequest, undefined, responseInterceptor1, undefined, responseInterceptor2, undefined ]; ``` ### 3. 工厂模式 (Factory) ```javascript // 创建 Axios 实例 axios.create = function create(instanceConfig) { return new Axios(instanceConfig); }; ``` ### 4. 观察者模式 (Observer) ```javascript // 取消令牌 const source = CancelToken.source(); source.token.promise.then(onCanceled); ``` ## 总结 Axios 的核心设计亮点: 1. **统一的 Promise API**:简化异步处理 2. **拦截器机制**:灵活的请求/响应处理 3. **适配器模式**:支持多平台运行 4. **配置合并策略**:灵活的配置管理 5. **取消请求**:完善的请求控制 6. **数据转换**:自动化的数据处理 7. **错误处理**:统一的错误管理机制
服务端 · 3月6日 23:02
使用 axios 时有哪些性能优化技巧?如何减少不必要的网络请求?在使用 axios 进行 HTTP 请求时,可以通过多种方式优化性能,减少不必要的网络开销,提升用户体验。 ## 1. 请求缓存 ### 内存缓存 ```javascript 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) ```javascript // 在 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. 请求去重(防抖) ```javascript 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. 请求合并 ```javascript 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. 懒加载和分页 ```javascript // 虚拟滚动 + 分页加载 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. 请求优先级管理 ```javascript 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. 压缩和精简请求 ```javascript // 请求数据压缩 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 ```javascript // 服务端配置 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 ```javascript // 使用相同的 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. 请求超时优化 ```javascript // 根据网络状况动态调整超时 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. 错误重试策略 ```javascript 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. 离线优先策略 ```javascript // 使用 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. 监控和分析 ```javascript // 性能监控拦截器 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% 请求 | | 请求去重 | 快速连续触发 | 减少重复请求 | | 请求合并 | 批量操作 | 减少请求数量 | | 分页加载 | 长列表 | 减少初始加载时间 | | 优先级队列 | 关键/非关键请求 | 提升关键请求响应 | | 数据压缩 | 大数据传输 | 减少传输体积 | | 连接复用 | 频繁请求 | 减少连接开销 | | 智能超时 | 不稳定网络 | 提升用户体验 | | 错误重试 | 临时故障 | 提高成功率 | | 离线优先 | 弱网环境 | 提升可用性 | ​
服务端 · 3月6日 23:02
如何对使用 axios 的代码进行单元测试和 Mock?请说明常用的测试方法对使用 axios 的代码进行测试时,需要掌握单元测试、集成测试和 Mock 技术。 ## 1. 使用 Jest 和 axios-mock-adapter ### 安装依赖 ```bash npm install --save-dev jest axios-mock-adapter @testing-library/react ``` ### 基础 Mock 测试 ```javascript // api/user.js import 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.js import 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 配置 ```javascript // __tests__/api.test.js import 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) ### 安装和配置 ```bash npm install --save-dev msw ``` ```javascript // mocks/handlers.js import { 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.js import { setupServer } from 'msw/node'; import { handlers } from './handlers'; export const server = setupServer(...handlers); // jest.setup.js import { server } from './mocks/server'; beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); ``` ### 使用 MSW 进行测试 ```javascript // __tests__/user.integration.test.js import { 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 ```javascript // __tests__/dynamic-mock.test.js import { 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 ```javascript // components/UserProfile.jsx import 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.jsx import 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 ```javascript // 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.js import { 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 测试 ```javascript // hooks/useApi.js import { 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.js import { 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 ```javascript // cypress/integration/api.spec.js describe('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 ``` ### 测试工具函数 ```javascript // test-utils.js import { 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 | 端到端测试 | ## 最佳实践 1. **使用 MSW**:推荐用于集成测试,更接近真实环境 2. **分离关注点**:测试逻辑和 UI 分离 3. **清理副作用**:每次测试后清理 mocks 4. **测试错误场景**:不仅要测试成功,也要测试失败 5. **避免真实请求**:测试时不应发送真实 HTTP 请求 6. **使用数据属性**:使用 data-testid 选择元素 7. **保持测试独立**:每个测试应该独立运行
服务端 · 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 重大变更 ```javascript // v0.19.0 之前 - 错误处理 axios.get('/user') .catch(error => { // 404 错误也会进入 catch if (error.response) { // 服务器响应了错误状态码 } }); // v0.19.0 之后 - 引入 validateStatus axios.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 改进 ```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 重大变更 ```javascript // 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 适配器 ```javascript // 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. 兼容性问题与解决方案 ### 浏览器兼容性 ```javascript // 检查浏览器兼容性 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 Polyfill import 'es6-promise/auto'; // 2. Fetch API Polyfill import 'whatwg-fetch'; // 3. 完整兼容性处理 import axios from 'axios'; // 配置适配器回退 if (typeof XMLHttpRequest === 'undefined') { // Node.js 环境 axios.defaults.adapter = require('axios/lib/adapters/http'); } ``` ### Node.js 版本兼容性 ```javascript // 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 }); ``` ### 版本检测与适配 ```javascript // axios-version-compat.js import 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; } // 根据版本选择 API export 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 ```javascript // 升级检查清单 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; } ``` ### 依赖版本锁定 ```json // package.json - 锁定版本 { "dependencies": { "axios": "^1.6.0" }, "devDependencies": { "@types/axios": "^0.14.0" }, "resolutions": { "axios": "1.6.0" } } // package-lock.json / yarn.lock // 确保锁定文件提交到版本控制 ``` ## 5. 版本兼容性测试 ```javascript // __tests__/axios-compat.test.js import 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. 最佳实践 ### 版本管理策略 ```javascript // 1. 使用固定版本 // package.json { "dependencies": { "axios": "1.6.0" // 不使用 ^ 或 ~ } } // 2. 封装 axios,隔离版本影响 // api/client.js import 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.js const { 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'); } }); ``` ### 兼容性配置模板 ```javascript // config/axios.js import 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 支持 | ⚠️ | ⚠️ | ⚠️ | ✅ | ✅ | | 安全修复 | ✅ | ✅ | ✅ | ✅ | ✅ | ## 总结 1. **版本选择**:新项目建议使用 v1.6.0+,旧项目逐步迁移 2. **兼容性处理**:封装 axios 使用,隔离版本差异 3. **升级策略**:先测试后升级,使用锁定文件 4. **API 选择**:优先使用新标准 (AbortController) 5. **监控更新**:关注安全更新和重大变更
服务端 · 3月6日 23:01