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

服务端面试题手册

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

Babel 的编译流程是怎样的?请详细说明各个阶段

Babel 的编译过程主要分为三个阶段:解析(Parsing):将源代码转换为 AST转换(Transforming):遍历和修改 AST生成(Generating):将 AST 转换回代码详细流程第一阶段:解析(Parsing)将源代码字符串转换为抽象语法树(AST)。// 源代码const add = (a, b) => a + b;// 解析后的 AST(简化版){ "type": "VariableDeclaration", "declarations": [{ "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "add" }, "init": { "type": "ArrowFunctionExpression", "params": [ { "type": "Identifier", "name": "a" }, { "type": "Identifier", "name": "b" } ], "body": { "type": "BinaryExpression", "operator": "+", "left": { "type": "Identifier", "name": "a" }, "right": { "type": "Identifier", "name": "b" } } } }]}涉及的包:@babel/parser(基于 Babylon)第二阶段:转换(Transforming)遍历 AST 并应用插件进行转换。// 转换前(ArrowFunctionExpression)const add = (a, b) => a + b;// 转换后(FunctionExpression)var add = function add(a, b) { return a + b;};转换过程:遍历(Traverse):深度优先遍历 AST访问(Visit):遇到节点时调用对应的访问者方法修改(Modify):通过访问者模式修改节点// 插件示例:转换箭头函数const arrowFunctionPlugin = { visitor: { ArrowFunctionExpression(path) { // 将箭头函数转换为普通函数 path.replaceWith( t.functionExpression( null, path.node.params, t.blockStatement([ t.returnStatement(path.node.body) ]) ) ); } }};涉及的包:@babel/traverse第三阶段:生成(Generating)将修改后的 AST 转换回代码字符串。// AST 节点{ "type": "FunctionExpression", "id": null, "params": [...], "body": { ... }}// 生成的代码var add = function add(a, b) { return a + b;};涉及的包:@babel/generator完整流程图┌─────────────────┐│ 源代码输入 ││ const add = ... │└────────┬────────┘ │ ▼┌─────────────────┐│ @babel/parser ││ 解析阶段 │└────────┬────────┘ │ ▼┌─────────────────┐│ AST ││ 抽象语法树 │└────────┬────────┘ │ ▼┌──────────────────┐│ @babel/traverse ││ 转换阶段 ││ (插件应用) │└────────┬─────────┘ │ ▼┌─────────────────┐│ 修改后的 AST │└────────┬────────┘ │ ▼┌──────────────────┐│ @babel/generator ││ 生成阶段 │└────────┬─────────┘ │ ▼┌─────────────────┐│ 编译后代码 ││ var add = ... │└─────────────────┘核心概念1. Visitor 模式const visitor = { // 进入节点时调用 Identifier: { enter(path) { console.log('Enter:', path.node.name); }, // 离开节点时调用 exit(path) { console.log('Exit:', path.node.name); } }};2. Path 对象表示节点在树中的位置提供节点操作方法(替换、删除、插入等)包含作用域信息3. State 状态在遍历过程中传递数据插件间共享信息调试技巧// 查看 ASTconst parser = require('@babel/parser');const ast = parser.parse('const a = 1');console.log(JSON.stringify(ast, null, 2));// 使用 babel-node 调试// npx babel-node --inspect-brk script.js​
阅读 0·3月6日 23:01

Service Worker 中的 Cache Storage API 如何使用?

Cache Storage API 是 Service Worker 的核心 API,用于管理请求/响应对的缓存,实现离线访问和性能优化。基本概念Cache Storage API 提供了类似数据库的接口来存储和检索网络请求和响应:// Cache Storage 是 caches 全局对象console.log(caches); // CacheStorage 对象// 主要方法caches.open(cacheName) // 打开/创建缓存caches.match(request) // 跨缓存匹配请求caches.keys() // 获取所有缓存名称caches.delete(cacheName) // 删除指定缓存核心方法详解1. 打开和创建缓存// 打开缓存(如果不存在则创建)async function openCache() { const cache = await caches.open('my-cache-v1'); console.log('缓存已打开:', cache); return cache;}// 使用版本号管理缓存const CACHE_VERSION = 'v2';const CACHE_NAME = `app-cache-${CACHE_VERSION}`;2. 添加资源到缓存// 方法1:使用 add() - 获取并缓存单个请求async function addToCache(url) { const cache = await caches.open('my-cache'); await cache.add(url); // 自动 fetch 并缓存 console.log(`已缓存: ${url}`);}// 方法2:使用 addAll() - 批量缓存async function addMultipleToCache(urls) { const cache = await caches.open('my-cache'); await cache.addAll(urls); console.log(`已批量缓存 ${urls.length} 个资源`);}// 使用示例const urlsToCache = [ '/', '/index.html', '/styles.css', '/app.js', '/images/logo.png'];addMultipleToCache(urlsToCache);// 方法3:使用 put() - 手动存储请求/响应对async function putInCache(request, response) { const cache = await caches.open('my-cache'); await cache.put(request, response);}// 示例:缓存自定义响应const request = new Request('/custom-data');const response = new Response(JSON.stringify({ data: 'value' }), { headers: { 'Content-Type': 'application/json' }});putInCache(request, response);3. 从缓存中检索// 方法1:match() - 匹配单个请求async function getFromCache(url) { const cache = await caches.open('my-cache'); const response = await cache.match(url); if (response) { console.log('缓存命中:', url); return response; } console.log('缓存未命中:', url); return null;}// 方法2:跨所有缓存匹配caches.match('/api/data').then(response => { if (response) { // 在所有缓存中找到匹配 return response; }});// 方法3:matchAll() - 获取所有匹配async function getAllMatches(url) { const cache = await caches.open('my-cache'); const responses = await cache.matchAll(url); return responses;}4. 删除缓存中的资源// 删除特定请求async function deleteFromCache(url) { const cache = await caches.open('my-cache'); const deleted = await cache.delete(url); console.log(deleted ? '删除成功' : '资源不存在');}// 删除整个缓存async function deleteCache(cacheName) { const deleted = await caches.delete(cacheName); console.log(deleted ? '缓存已删除' : '缓存不存在');}// 清理旧缓存(常用在 activate 事件)async function cleanOldCaches(currentCacheName) { const cacheNames = await caches.keys(); const oldCaches = cacheNames.filter(name => name !== currentCacheName); await Promise.all( oldCaches.map(name => caches.delete(name)) ); console.log(`已清理 ${oldCaches.length} 个旧缓存`);}5. 查看缓存内容// 列出所有缓存async function listAllCaches() { const cacheNames = await caches.keys(); console.log('所有缓存:', cacheNames); return cacheNames;}// 查看特定缓存中的所有请求async function listCacheContents(cacheName) { const cache = await caches.open(cacheName); const requests = await cache.keys(); console.log(`缓存 "${cacheName}" 内容:`); requests.forEach(request => { console.log(' -', request.url); }); return requests;}实际应用场景场景1:预缓存静态资源// sw.jsconst STATIC_CACHE = 'static-v1';const STATIC_ASSETS = [ '/', '/index.html', '/styles.css', '/app.js', '/manifest.json', '/icons/icon-192x192.png'];self.addEventListener('install', event => { event.waitUntil( caches.open(STATIC_CACHE) .then(cache => cache.addAll(STATIC_ASSETS)) .then(() => self.skipWaiting()) );});场景2:动态缓存运行时资源// sw.jsconst DYNAMIC_CACHE = 'dynamic-v1';self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(response => { // 返回缓存或请求网络 return response || fetch(event.request).then(fetchResponse => { // 动态缓存新资源 return caches.open(DYNAMIC_CACHE).then(cache => { cache.put(event.request, fetchResponse.clone()); return fetchResponse; }); }); }) );});场景3:缓存 API 响应// sw.jsconst API_CACHE = 'api-v1';const API_MAX_AGE = 5 * 60 * 1000; // 5分钟self.addEventListener('fetch', event => { if (event.request.url.includes('/api/')) { event.respondWith( caches.open(API_CACHE).then(cache => { return cache.match(event.request).then(cachedResponse => { // 检查缓存是否过期 if (cachedResponse) { const cachedTime = new Date( cachedResponse.headers.get('sw-cached-time') ).getTime(); if (Date.now() - cachedTime < API_MAX_AGE) { return cachedResponse; } } // 请求网络并缓存 return fetch(event.request).then(networkResponse => { const clonedResponse = networkResponse.clone(); // 添加自定义缓存时间头 const headers = new Headers(clonedResponse.headers); headers.set('sw-cached-time', new Date().toISOString()); const modifiedResponse = new Response( clonedResponse.body, { ...clonedResponse, headers } ); cache.put(event.request, modifiedResponse); return networkResponse; }); }); }) ); }});场景4:缓存清理策略// sw.jsconst CACHE_MAX_ITEMS = 50; // 最大缓存数量async function trimCache(cacheName, maxItems) { const cache = await caches.open(cacheName); const requests = await cache.keys(); if (requests.length > maxItems) { // 删除最旧的缓存 const toDelete = requests.slice(0, requests.length - maxItems); await Promise.all( toDelete.map(request => cache.delete(request)) ); console.log(`已清理 ${toDelete.length} 个旧缓存项`); }}// 定期清理self.addEventListener('activate', event => { event.waitUntil( trimCache('dynamic-v1', CACHE_MAX_ITEMS) );});注意事项1. Response 只能使用一次// ❌ 错误:Response 只能读取一次const response = await fetch('/api/data');await cache.put(request, response); // 存储到缓存return response; // 已经消耗,无法返回// ✅ 正确:使用 clone()const response = await fetch('/api/data');await cache.put(request, response.clone()); // 存储副本return response; // 返回原始响应2. 存储限制// 检查存储配额async function checkStorageQuota() { if ('storage' in navigator && 'estimate' in navigator.storage) { const estimate = await navigator.storage.estimate(); console.log(`已使用: ${estimate.usage} bytes`); console.log(`配额: ${estimate.quota} bytes`); console.log(`使用率: ${(estimate.usage / estimate.quota * 100).toFixed(2)}%`); }}3. 缓存键匹配规则// URL 必须完全匹配(包括查询参数)await cache.match('/api/data'); // ✅ 匹配 /api/dataawait cache.match('/api/data?id=1'); // ❌ 不匹配 /api/data// 使用 ignoreSearch 选项忽略查询参数await cache.match('/api/data?id=1', { ignoreSearch: true // 忽略查询参数});// 其他匹配选项await cache.match(request, { ignoreSearch: true, // 忽略查询参数 ignoreMethod: true, // 忽略 HTTP 方法 ignoreVary: true // 忽略 Vary 头});最佳实践版本控制:使用版本号管理缓存,便于更新定期清理:在 activate 事件中清理过期缓存存储限制:注意浏览器的存储配额限制错误处理:添加适当的错误处理和降级方案性能优化:避免缓存过多数据,定期清理
阅读 0·3月6日 22:58

DNS 在 CDN 加速中是如何工作的

CDN(Content Delivery Network,内容分发网络)通过在全球部署边缘节点,将网站内容缓存到离用户最近的服务器,从而加速内容传输。DNS 在 CDN 中扮演着智能调度的核心角色,决定用户访问哪个边缘节点。CDN 的工作原理传统访问 vs CDN 访问传统访问:用户(北京)→ 源站服务器(美国)→ 返回内容 ↓ 延迟高,速度慢CDN 加速:用户(北京)→ DNS 智能调度 → CDN 北京节点 → 返回缓存内容 ↓ 延迟低,速度快DNS 在 CDN 中的作用1. CNAME 接入用户将域名指向 CDN 提供的 CNAME 地址:; 用户域名配置www.example.com. 600 IN CNAME www.example.com.cdn-provider.com.2. 智能 DNS 解析CDN 的 DNS 服务器根据多种因素决定返回哪个边缘节点的 IP:用户查询 www.example.com ↓ CNAME 到 CDN 域名 ↓ CDN DNS 服务器 ↓ 智能调度算法 ↓ 返回最优节点 IPCDN DNS 调度策略1. 地理位置调度原理:根据用户的地理位置,返回最近的 CDN 节点北京用户 → 返回北京 CDN 节点 IP上海用户 → 返回上海 CDN 节点 IP广州用户 → 返回广州 CDN 节点 IP美国用户 → 返回美国 CDN 节点 IP实现方式:根据用户 DNS 查询来源 IP 判断位置使用 GeoIP 数据库返回地理位置最近的节点2. 运营商调度原理:根据用户所属运营商,返回相同运营商的节点电信用户 → 返回电信线路 CDN 节点联通用户 → 返回联通线路 CDN 节点移动用户 → 返回移动线路 CDN 节点优势:避免跨运营商访问减少网络延迟提高传输速度3. 负载均衡调度原理:根据节点负载情况,将用户分配到负载较低的节点节点 A:负载 80% → 减少分配节点 B:负载 50% → 正常分配节点 C:负载 30% → 优先分配监控指标:CPU 使用率带宽使用率并发连接数响应时间4. 健康检查调度原理:自动检测节点健康状态,剔除故障节点节点 A:健康 ✅ → 正常服务节点 B:故障 ❌ → 自动剔除节点 C:健康 ✅ → 正常服务健康检查方式:HTTP 状态码检查TCP 端口检查响应时间检查CDN DNS 解析流程详解完整解析过程1. 用户输入 www.example.com ↓2. 本地 DNS 查询 ↓3. 返回 CNAME: www.example.com.cdn-provider.com ↓4. 查询 CDN DNS 服务器 ↓5. CDN DNS 分析用户来源 IP ↓6. 应用调度策略(地理位置 + 运营商 + 负载) ↓7. 返回最优 CDN 节点 IP(如 203.0.113.10) ↌8. 用户访问 CDN 节点获取内容首次访问 vs 缓存访问首次访问(未缓存):用户 → CDN 节点 → 回源到源站 → 缓存内容 → 返回用户缓存访问:用户 → CDN 节点(直接返回缓存内容)CDN DNS 配置示例场景 1:全站加速; 主域名 CNAME 到 CDNwww.example.com. 600 IN CNAME example.cdn-provider.com.; 静态资源域名 CNAME 到 CDNstatic.example.com. 600 IN CNAME static.example.cdn-provider.com.; 图片域名 CNAME 到 CDNimg.example.com. 600 IN CNAME img.example.cdn-provider.com.场景 2:动静分离; 动态内容(不经过 CDN)www.example.com. 600 IN A 192.0.2.1; 静态内容(使用 CDN)static.example.com. 600 IN CNAME static.example.cdn-provider.com.css.example.com. 600 IN CNAME static.example.cdn-provider.com.js.example.com. 600 IN CNAME static.example.cdn-provider.com.场景 3:多 CDN 厂商; 主 CDNwww.example.com. 600 IN CNAME example.cdn1-provider.com.; 备用 CDN(通过权重或地理位置分流); 部分 DNS 服务支持智能分流CDN DNS 的 TTL 设置TTL 对 CDN 的影响| TTL 设置 | 优点 | 缺点 || ------------------ | --------------- | ------------- || 短 TTL(60-300秒) | 故障切换快,调度灵活 | DNS 查询增加,源站压力 || 长 TTL(3600秒+) | 减少 DNS 查询,缓存效果好 | 故障切换慢,调度不灵活 |推荐配置; 主域名 - 中等 TTLwww.example.com. 600 IN CNAME example.cdn-provider.com.; 静态资源 - 较长 TTLstatic.example.com. 3600 IN CNAME static.example.cdn-provider.com.CDN DNS 常见问题问题 1:DNS 缓存导致调度失效现象:用户被调度到故障节点更换节点后部分用户仍访问旧节点解决:降低 TTL 值等待缓存过期使用 CDN 的强制刷新功能问题 2:跨运营商访问慢现象:电信用户访问联通 CDN 节点延迟高,速度慢解决:确保 CDN 支持运营商调度检查 DNS 配置是否正确联系 CDN 服务商优化问题 3:源站 IP 暴露现象:直接 ping 源站域名获得真实 IP攻击者绕过 CDN 直接攻击源站解决:源站只允许 CDN IP 段访问使用防火墙限制直接访问分离源站域名和访问域名主流 CDN 服务商的 DNS 配置Cloudflare; 修改 NS 记录到 Cloudflareexample.com. 86400 IN NS lara.ns.cloudflare.com.example.com. 86400 IN NS greg.ns.cloudflare.com.特点:接管整个域名的 DNS自动 CDN 加速支持 CNAME Flattening阿里云 CDN; CNAME 接入www.example.com. 600 IN CNAME www.example.com.w.kunlunar.com.特点:支持智能调度(地理位置 + 运营商)国内节点丰富AWS CloudFront; CNAME 接入www.example.com. 600 IN CNAME d1234abcd5678.cloudfront.net.特点:全球节点分布与 AWS 生态集成CDN DNS 优化技巧1. 使用 HTTP DNS绕过本地 DNS,直接通过 HTTP 获取最优节点:客户端 → HTTP DNS 服务 → 返回最优 CDN IP优势:避免 DNS 劫持更精确的调度实时更新2. 预热和刷新预热:在高峰期前将热门内容推送到 CDN 节点刷新:主动清除 CDN 缓存,强制回源3. 监控和分析监控各节点响应时间分析用户地理分布优化调度策略面试常见问题Q: CDN 是如何知道用户位置的?A:通过用户 DNS 查询的来源 IP 地址使用 GeoIP 数据库查询 IP 对应的地理位置返回距离最近的 CDN 节点Q: 为什么 CDN 的 CNAME 记录 TTL 通常设置较短?A:便于快速故障切换支持动态调度策略适应节点扩缩容但会增加 DNS 查询量Q: 如果 CDN 节点故障,DNS 如何切换?A:CDN 的健康检查系统检测到节点故障自动将该节点从 DNS 解析结果中移除用户下次查询时获得健康节点 IP故障恢复后自动加回总结| 方面 | 说明 || ---------- | ------------------ || 核心作用 | 智能调度用户到最优 CDN 节点 || 调度策略 | 地理位置、运营商、负载、健康状态 || 接入方式 | CNAME 指向 CDN 提供的域名 || TTL 设置 | 平衡调度灵活性和 DNS 查询量 || 优化方向 | HTTP DNS、预热刷新、监控分析 |DNS 是 CDN 的"大脑",通过智能调度算法,确保用户始终访问到最优的边缘节点,从而实现加速效果。
阅读 0·3月6日 22:57

DNS 中的 CNAME 和 A 记录有什么区别,使用时需要注意什么

A 记录和CNAME 记录是 DNS 中最常用的两种记录类型,它们都用于将域名指向服务器,但工作方式和使用场景有很大不同。A 记录详解什么是 A 记录**A 记录(Address Record)**直接将域名映射到 IPv4 地址,是最基础、最直接的 DNS 记录类型。A 记录格式; 基本格式www.example.com. 3600 IN A 192.0.2.1; 多个 A 记录(负载均衡)www.example.com. 3600 IN A 192.0.2.1www.example.com. 3600 IN A 192.0.2.2www.example.com. 3600 IN A 192.0.2.3A 记录的特点✅ 直接解析:域名直接解析到 IP 地址,查询效率高✅ 根域名可用:根域名(@)可以使用 A 记录✅ 与其他记录共存:可以与 MX、TXT 等记录共存✅ 性能最优:一次查询即可获得 IP 地址CNAME 记录详解什么是 CNAME 记录**CNAME 记录(Canonical Name Record)**创建域名的别名,指向另一个域名而非直接指向 IP 地址。CNAME 记录格式; 基本格式blog.example.com. 3600 IN CNAME example.github.io.; 指向同域名的另一个子域名www.example.com. 3600 IN CNAME example.com.CNAME 解析过程用户查询 blog.example.com ↓DNS 返回 CNAME: example.github.io. ↓用户需要再次查询 example.github.io. ↓最终获得 IP 地址CNAME 记录的特点✅ 灵活性高:目标 IP 变化时,CNAME 自动跟随✅ 便于管理:一个目标域名可以被多个 CNAME 指向✅ 适合第三方服务:接入 CDN、云服务等❌ 额外查询:需要两次 DNS 查询才能获得 IP❌ 根域名限制:根域名不能使用 CNAME❌ 记录冲突:不能与其他记录类型共存CNAME vs A 记录对比| 特性 | A 记录 | CNAME 记录 || --------- | --------------- | -------------------- || 指向目标 | IPv4 地址 | 另一个域名 || 查询次数 | 1 次 | 2 次(CNAME + 目标 A 记录) || 根域名支持 | ✅ 支持 | ❌ 不支持 || 记录共存 | ✅ 可与 MX、TXT 等共存 | ❌ 不能与其他记录共存 || 灵活性 | IP 变更需手动修改 | 自动跟随目标域名 || 性能 | 最优 | 略差(额外查询) || 适用场景 | 自有服务器、根域名 | 第三方服务、CDN |使用限制和注意事项CNAME 的重要限制1. 根域名不能使用 CNAME; ❌ 错误:根域名使用 CNAME@ 3600 IN CNAME example.herokuapp.com.; ✅ 正确:根域名使用 A 记录或 ALIAS/ANAME@ 3600 IN A 192.0.2.1原因:根域名必须有 NS 记录和 SOA 记录CNAME 与其他记录类型冲突2. CNAME 与其他记录冲突; ❌ 错误:CNAME 与 MX 记录冲突www.example.com. 3600 IN CNAME example.com.www.example.com. 3600 IN MX 10 mail.example.com.; ✅ 正确:使用 A 记录www.example.com. 3600 IN A 192.0.2.1www.example.com. 3600 IN MX 10 mail.example.com.冲突的记录类型:MX 记录(邮件交换)NS 记录(域名服务器)SOA 记录(区域管理)其他 CNAME 记录3. CNAME 链长度限制; ❌ 避免过长的 CNAME 链a.example.com → b.example.com → c.example.com → d.example.com; ✅ 推荐:直接指向最终目标a.example.com → final-target.com建议:CNAME 链不超过 3-4 级,避免性能问题实际应用场景场景 1:使用 A 记录自有服务器; 网站服务器www.example.com. 3600 IN A 192.0.2.1; 邮件服务器mail.example.com. 3600 IN A 192.0.2.2; 根域名(必须)@ 3600 IN A 192.0.2.1配合 MX 记录@ 3600 IN A 192.0.2.1@ 3600 IN MX 10 mail.example.com.@ 3600 IN TXT "v=spf1 include:_spf.google.com ~all"场景 2:使用 CNAME 记录接入 CDN; 使用 CDN 加速www.example.com. 3600 IN CNAME example.cdn-provider.com.第三方托管服务; GitHub Pagesblog.example.com. 3600 IN CNAME username.github.io.; Herokuapp.example.com. 3600 IN CNAME example-app.herokuapp.com.; Vercelwww.example.com. 3600 IN CNAME cname.vercel-dns.com.子域名统一管理; 多个子域名指向同一目标www.example.com. 3600 IN CNAME example.com.blog.example.com. 3600 IN CNAME example.com.shop.example.com. 3600 IN CNAME example.com.场景 3:根域名的特殊处理问题:根域名需要 CNAME 功能; 根域名不能使用 CNAME; 但需要指向 CDN 或第三方服务解决方案 1:ALIAS/ANAME 记录部分 DNS 服务商提供的特殊记录:; Cloudflare CNAME Flattening@ 3600 IN CNAME example.cdn-provider.com.; DNSimple ALIAS@ 3600 IN ALIAS example.herokuapp.com.原理:DNS 服务器在解析时自动将 CNAME 展开为 A 记录解决方案 2:A 记录 + 定期更新; 手动配置 CDN 提供的 IP@ 3600 IN A 203.0.113.1@ 3600 IN A 203.0.113.2缺点:CDN IP 变更时需要手动更新性能考虑DNS 查询次数对比A 记录:查询 www.example.com → 返回 IP总计:1 次查询CNAME 记录:查询 blog.example.com → 返回 CNAME (example.github.io)查询 example.github.io → 返回 IP总计:2 次查询性能优化建议关键路径使用 A 记录主网站使用 A 记录减少 DNS 查询时间合理使用 CNAME第三方服务使用 CNAME避免过长的 CNAME 链利用 DNS 缓存设置合理的 TTL减少重复查询常见面试问题Q: 为什么根域名不能使用 CNAME?A:DNS 协议规定,CNAME 记录不能与其他记录类型共存根域名必须有 NS 记录和 SOA 记录如果根域名使用 CNAME,会违反这一规则Q: CNAME 和 A 记录可以同时存在于一个域名吗?A: 不可以。如果域名设置了 CNAME 记录,就不能再设置 A 记录、MX 记录等其他记录类型(除 DNSSEC 相关记录外)。Q: 什么时候应该使用 CNAME 而不是 A 记录?A:使用第三方服务(CDN、GitHub Pages、Heroku 等)目标 IP 可能经常变化需要统一管理多个子域名的指向不是根域名Q: CNAME 记录有什么性能影响?A:需要额外的 DNS 查询(至少多一次)增加解析延迟(通常 10-50ms)但对于现代网络,影响通常可以忽略最佳实践总结| 场景 | 推荐记录类型 | 原因 || ------------ | ------------ | --------- || 根域名(@) | A 记录 / ALIAS | CNAME 不允许 || 自有服务器 | A 记录 | 性能最优 || 需要 MX 记录的子域名 | A 记录 | 避免冲突 || CDN 加速 | CNAME | 灵活性高 || 第三方托管 | CNAME | 自动更新 || 多子域名统一指向 | CNAME | 便于管理 |总结A 记录:直接、高效、灵活,适合大多数场景CNAME 记录:间接、灵活、便于管理,适合第三方服务关键原则:根域名用 A 记录,第三方服务用 CNAME,注意记录冲突
阅读 0·3月6日 22:57

axios 实例如何创建和配置?请说明 axios.create() 的使用方法

使用 axios.create() 方法可以创建具有自定义配置的 axios 实例,这在需要多个不同配置的 HTTP 客户端时非常有用。基本用法const instance = axios.create({ baseURL: 'https://api.example.com', timeout: 1000, headers: {'X-Custom-Header': 'foobar'}});// 使用实例发送请求instance.get('/users') .then(response => { console.log(response.data); });配置选项详解1. 基础配置const instance = axios.create({ // 基础 URL,会自动添加到请求 URL 前面 baseURL: 'https://api.example.com/v1', // 请求超时时间(毫秒) timeout: 10000, // 自定义请求头 headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, // 请求方法 method: 'get', // URL 参数 params: { key: 'value' }, // 请求体数据(仅适用于 PUT、POST、DELETE 和 PATCH) data: { firstName: 'John' }});2. 高级配置const instance = axios.create({ // 允许携带 cookie withCredentials: true, // 响应类型 responseType: 'json', // 'arraybuffer', 'document', 'json', 'text', 'stream', 'blob' // 响应编码 responseEncoding: 'utf8', // xsrf token 名称 xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', // 上传/下载进度监控 onUploadProgress: function (progressEvent) { // 上传进度 }, onDownloadProgress: function (progressEvent) { // 下载进度 }, // 最大重定向次数 maxRedirects: 5, // 最大请求体长度 maxContentLength: 2000, // 最大响应体长度 maxBodyLength: 2000, // 代理配置 proxy: { protocol: 'https', host: '127.0.0.1', port: 9000, auth: { username: 'mikeymike', password: 'rapunz3l' } }});多实例配置场景场景 1:多个后端服务// 用户服务const userService = axios.create({ baseURL: 'https://api.user-service.com', timeout: 5000});// 订单服务const orderService = axios.create({ baseURL: 'https://api.order-service.com', timeout: 10000});// 支付服务const paymentService = axios.create({ baseURL: 'https://api.payment-service.com', timeout: 15000, headers: { 'X-Payment-Key': 'secret-key' }});// 使用userService.get('/users/1');orderService.get('/orders/123');paymentService.post('/payments', { amount: 100 });场景 2:不同环境配置// 开发环境const devInstance = axios.create({ baseURL: 'http://localhost:3000', timeout: 5000});// 生产环境const prodInstance = axios.create({ baseURL: 'https://api.production.com', timeout: 10000, headers: { 'X-Environment': 'production' }});// 根据环境导出const api = process.env.NODE_ENV === 'production' ? prodInstance : devInstance;export default api;场景 3:带认证的 API 和普通 API// 需要认证的 APIconst authApi = axios.create({ baseURL: 'https://api.example.com', timeout: 10000});// 添加认证拦截器authApi.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config;});// 公开 API(无需认证)const publicApi = axios.create({ baseURL: 'https://api.example.com', timeout: 5000});// 使用publicApi.get('/public/data'); // 无需认证authApi.get('/private/profile'); // 需要认证实例方法创建实例后,可以使用以下方法:const instance = axios.create({ baseURL: 'https://api.example.com' });// 请求方法instance.request(config)instance.get(url, config)instance.delete(url, config)instance.head(url, config)instance.options(url, config)instance.post(url, data, config)instance.put(url, data, config)instance.patch(url, data, config)// 获取 URI(不发送请求)instance.getUri(config)默认配置修改修改实例默认配置const instance = axios.create();// 修改默认配置instance.defaults.baseURL = 'https://api.example.com';instance.defaults.headers.common['Authorization'] = 'AUTH_TOKEN';instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';instance.defaults.timeout = 10000;请求时覆盖配置const instance = axios.create({ baseURL: 'https://api.example.com', timeout: 5000});// 单次请求覆盖配置instance.get('/long-operation', { timeout: 30000 // 覆盖默认的 5000ms});配置优先级配置会按以下优先级合并(后面的覆盖前面的):库默认值axios.create() 中的配置实例的 defaults 属性请求时的配置参数// 库默认值axios.defaults.timeout = 0;// 实例创建配置const instance = axios.create({ timeout: 5000 // 覆盖库默认值});// 实例 defaults 配置instance.defaults.timeout = 10000; // 覆盖创建时的配置// 请求配置instance.get('/data', { timeout: 20000 // 最终使用这个值});完整封装示例// api.jsimport axios from 'axios';// 创建实例const api = axios.create({ baseURL: process.env.VUE_APP_API_URL || 'http://localhost:3000', timeout: 10000, headers: { 'Content-Type': 'application/json' }});// 请求拦截器api.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, error => { return Promise.reject(error); });// 响应拦截器api.interceptors.response.use( response => response.data, error => { if (error.response?.status === 401) { localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); });// 导出实例和方法export default api;// 特定服务 APIexport const userApi = { getProfile: () => api.get('/users/profile'), updateProfile: (data) => api.put('/users/profile', data), changePassword: (data) => api.post('/users/change-password', data)};export const orderApi = { getOrders: (params) => api.get('/orders', { params }), createOrder: (data) => api.post('/orders', data), cancelOrder: (id) => api.delete(`/orders/${id}`)};使用示例import api, { userApi, orderApi } from './api';// 使用默认实例const fetchData = async () => { const data = await api.get('/data'); return data;};// 使用特定服务 APIconst getUserProfile = async () => { const profile = await userApi.getProfile(); return profile;};const createNewOrder = async (orderData) => { const order = await orderApi.createOrder(orderData); return order;};​
阅读 0·3月6日 22:56

在 Vue 项目中如何正确使用 axios?请说明最佳实践和与 Vue 3 的组合式 API 结合方式

在 Vue 项目中使用 axios 时,需要考虑 Vue 的响应式系统、生命周期、组合式 API 等特性。1. 基础封装创建 Axios 实例// utils/request.jsimport axios from 'axios';import { ElMessage, ElLoading } from 'element-plus';// 创建实例const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000, headers: { 'Content-Type': 'application/json' }});// 请求拦截器service.interceptors.request.use( config => { // 添加 token const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } // 添加时间戳防止缓存 if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() }; } return config; }, error => { return Promise.reject(error); });// 响应拦截器service.interceptors.response.use( response => { const res = response.data; // 根据业务状态码处理 if (res.code !== 200) { ElMessage.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res.data; }, error => { const { response } = error; if (response) { switch (response.status) { case 401: ElMessage.error('登录已过期,请重新登录'); localStorage.removeItem('token'); window.location.href = '/login'; break; case 403: ElMessage.error('没有权限访问'); break; case 404: ElMessage.error('请求的资源不存在'); break; case 500: ElMessage.error('服务器内部错误'); break; default: ElMessage.error(response.data?.message || '请求失败'); } } else { ElMessage.error('网络错误,请检查网络连接'); } return Promise.reject(error); });export default service;按模块组织 API// api/user.jsimport request from '@/utils/request';export const userApi = { // 获取用户信息 getInfo: () => request.get('/user/info'), // 更新用户信息 updateInfo: (data) => request.put('/user/info', data), // 上传头像 uploadAvatar: (file) => { const formData = new FormData(); formData.append('file', file); return request.post('/user/avatar', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); }, // 修改密码 changePassword: (data) => request.post('/user/password', data)};// api/article.jsexport const articleApi = { getList: (params) => request.get('/articles', { params }), getDetail: (id) => request.get(`/articles/${id}`), create: (data) => request.post('/articles', data), update: (id, data) => request.put(`/articles/${id}`, data), delete: (id) => request.delete(`/articles/${id}`)};2. 在 Vue 2 中使用Options API 方式<template> <div class="user-profile"> <div v-if="loading">加载中...</div> <div v-else-if="error">{{ error }}</div> <div v-else> <h1>{{ user.name }}</h1> <p>{{ user.email }}</p> </div> </div></template><script>import { userApi } from '@/api/user';export default { data() { return { user: null, loading: false, error: null }; }, created() { this.fetchUserInfo(); }, beforeDestroy() { // 取消未完成的请求 if (this.cancelToken) { this.cancelToken.cancel('组件销毁'); } }, methods: { async fetchUserInfo() { this.loading = true; this.error = null; // 创建取消令牌 this.cancelToken = axios.CancelToken.source(); try { const data = await userApi.getInfo({ cancelToken: this.cancelToken.token }); this.user = data; } catch (err) { if (!axios.isCancel(err)) { this.error = err.message; } } finally { this.loading = false; } } }};</script>3. 在 Vue 3 中使用Composition API 方式<template> <div class="user-profile"> <div v-if="loading">加载中...</div> <div v-else-if="error">{{ error }}</div> <div v-else> <h1>{{ user?.name }}</h1> <p>{{ user?.email }}</p> <button @click="refresh">刷新</button> </div> </div></template><script setup>import { ref, onMounted, onUnmounted } from 'vue';import { userApi } from '@/api/user';const user = ref(null);const loading = ref(false);const error = ref(null);let controller = null;const fetchUserInfo = async () => { // 取消之前的请求 if (controller) { controller.abort(); } controller = new AbortController(); loading.value = true; error.value = null; try { const data = await userApi.getInfo({ signal: controller.signal }); user.value = data; } catch (err) { if (err.name !== 'AbortError') { error.value = err.message; } } finally { loading.value = false; }};const refresh = () => { fetchUserInfo();};onMounted(() => { fetchUserInfo();});onUnmounted(() => { if (controller) { controller.abort(); }});</script>封装可复用的 Composable// composables/useApi.jsimport { ref, onMounted, onUnmounted } from 'vue';import axios from 'axios';export function useApi(apiFunction, options = {}) { const { immediate = true, defaultValue = null } = options; const data = ref(defaultValue); const loading = ref(false); const error = ref(null); let controller = null; const execute = async (...params) => { if (controller) { controller.abort(); } controller = new AbortController(); loading.value = true; error.value = null; try { const result = await apiFunction(...params, { signal: controller.signal }); data.value = result; return result; } catch (err) { if (!axios.isCancel(err)) { error.value = err; throw err; } } finally { loading.value = false; } }; onUnmounted(() => { if (controller) { controller.abort(); } }); if (immediate) { onMounted(() => execute()); } return { data, loading, error, execute };}// 使用// composables/useUser.jsexport function useUser(userId) { const { data: user, loading, error, execute } = useApi( () => userApi.getInfo(userId), { immediate: true } ); return { user, loading, error, refresh: execute };}使用 Pinia 管理状态// stores/user.jsimport { defineStore } from 'pinia';import { ref, computed } from 'vue';import { userApi } from '@/api/user';export const useUserStore = defineStore('user', () => { // State const userInfo = ref(null); const loading = ref(false); const error = ref(null); // Getters const isLoggedIn = computed(() => !!userInfo.value); const userName = computed(() => userInfo.value?.name || ''); // Actions const fetchUserInfo = async () => { loading.value = true; error.value = null; try { const data = await userApi.getInfo(); userInfo.value = data; return data; } catch (err) { error.value = err.message; throw err; } finally { loading.value = false; } }; const updateUserInfo = async (data) => { const result = await userApi.updateInfo(data); userInfo.value = { ...userInfo.value, ...result }; return result; }; const logout = () => { userInfo.value = null; localStorage.removeItem('token'); }; return { userInfo, loading, error, isLoggedIn, userName, fetchUserInfo, updateUserInfo, logout };});// 在组件中使用<script setup>import { useUserStore } from '@/stores/user';const userStore = useUserStore();// 直接访问状态和 actionsconsole.log(userStore.userName);await userStore.fetchUserInfo();</script>4. 全局加载状态管理// composables/useLoading.jsimport { ref } from 'vue';const requestCount = ref(0);export function useLoading() { const showLoading = () => { requestCount.value++; }; const hideLoading = () => { if (requestCount.value > 0) { requestCount.value--; } }; const isLoading = computed(() => requestCount.value > 0); return { showLoading, hideLoading, isLoading };}// 在请求拦截器中使用service.interceptors.request.use(config => { const { showLoading } = useLoading(); showLoading(); return config;});service.interceptors.response.use( response => { const { hideLoading } = useLoading(); hideLoading(); return response; }, error => { const { hideLoading } = useLoading(); hideLoading(); return Promise.reject(error); });5. 文件上传组件封装<template> <div class="file-upload"> <input ref="fileInput" type="file" :accept="accept" @change="handleFileChange" style="display: none" /> <el-button @click="triggerUpload" :loading="uploading"> {{ uploading ? `上传中 ${progress}%` : '选择文件' }} </el-button> <div v-if="fileName" class="file-name">{{ fileName }}</div> </div></template><script setup>import { ref } from 'vue';import { userApi } from '@/api/user';const props = defineProps({ accept: { type: String, default: 'image/*' }});const emit = defineEmits(['success', 'error']);const fileInput = ref(null);const uploading = ref(false);const progress = ref(0);const fileName = ref('');const triggerUpload = () => { fileInput.value?.click();};const handleFileChange = async (e) => { const file = e.target.files[0]; if (!file) return; fileName.value = file.name; uploading.value = true; progress.value = 0; try { const formData = new FormData(); formData.append('file', file); const result = await userApi.uploadAvatar(formData, { onUploadProgress: (e) => { if (e.total) { progress.value = Math.round((e.loaded * 100) / e.total); } } }); emit('success', result); ElMessage.success('上传成功'); } catch (error) { emit('error', error); ElMessage.error('上传失败'); } finally { uploading.value = false; fileInput.value.value = ''; }};</script>6. 请求防抖和节流// composables/useDebounceApi.jsimport { ref } from 'vue';import { debounce, throttle } from 'lodash-es';export function useDebounceApi(apiFunction, wait = 300) { const loading = ref(false); const error = ref(null); const execute = debounce(async (...params) => { loading.value = true; error.value = null; try { return await apiFunction(...params); } catch (err) { error.value = err; throw err; } finally { loading.value = false; } }, wait); return { execute, loading, error };}// 搜索框使用示例<script setup>import { watch } from 'vue';import { useDebounceApi } from '@/composables/useDebounceApi';const searchQuery = ref('');const { execute: search, loading, error } = useDebounceApi(searchApi.search, 500);watch(searchQuery, (newVal) => { if (newVal) { search({ keyword: newVal }); }});</script>最佳实践总结封装请求层:统一处理配置、拦截器、错误提示使用 AbortController:组件卸载时取消请求创建 Composables:复用请求逻辑,配合 Vue 3 组合式 API状态管理:使用 Pinia 管理全局状态和用户信息加载状态:统一管理全局 loading,避免重复显示文件上传:封装组件,显示上传进度防抖节流:搜索等场景使用防抖减少请求次数错误处理:统一处理错误,根据状态码给出友好提示
阅读 0·3月6日 22:56