在前端项目开发中,Axios 几乎是标配 HTTP 请求库。 为了实现鉴权、日志、错误处理、重复请求取消、接口重试、缓存等能力,我们几乎都会对 Axios 进行二次封装。
但传统封装普遍存在这些痛点:
今天给大家带来一套模仿 Vue 插件机制的 Axios 插件化封装:
createAxios().use(插件1).use(插件2)
真正做到:解耦、可插拔、高复用、易维护、生产可用。
.use() 链式调用这是整个架构的灵魂,负责创建 Axios 实例、管理插件生命周期。
javascriptimport axios from 'axios';
export function createAxios(config = {}) {
const instance = axios.create(config);
const installedPlugins = new Set();
const pluginOrder = [];
function use(plugin, options = {}) {
if (typeof plugin === 'function') {
plugin = { install: plugin };
}
if (!plugin || typeof plugin.install !== 'function') {
console.error('[AxiosUse] 插件必须提供 install 方法');
return instance;
}
if (installedPlugins.has(plugin)) {
console.warn('[AxiosUse] 插件已安装,跳过');
return instance;
}
installedPlugins.add(plugin);
try {
plugin.install(instance, options);
console.log(`[AxiosUse] ✅ 插件安装成功: ${plugin.name || 'anonymous'}`);
} catch (error) {
console.error('[AxiosUse] ❌ 插件安装失败', error);
}
return instance;
}
function useBatch(plugins) {
plugins.forEach(({ plugin, options = {} }) => {
use(plugin, options);
});
return instance;
}
instance.use = use;
instance.useBatch = useBatch;
instance.getInstalledPlugins = () => [...pluginOrder];
instance.hasPlugin = (plugin) => installedPlugins.has(plugin);
return instance;
}
和 Vue 插件完全一致,必须提供 install 方法。
const MyPlugin = { name: 'MyPlugin', install(axiosInstance, options) { axiosInstance.interceptors.request.use(config => { return config; }); } }; const http = createAxios().use(MyPlugin);
const http = createAxios({ baseURL: '/api' }) .use(LogPlugin) .use(TokenPlugin, { getToken: () => localStorage.getItem('token'), whiteList: ['/login', '/register'] }) .use(CancelDuplicatePlugin) .use(RetryPlugin, { maxRetries: 3 }) .use(LoadingPlugin) .use(CachePlugin, { ttl: 5 * 60 * 1000 }) .use(ErrorHandlerPlugin);
import { createAxiosWithPreset } from '@/utils/axios'; // 极简模式 const httpMin = createAxiosWithPreset('minimal'); // 默认模式 const httpDefault = createAxiosWithPreset('default'); // 全量模式 const httpFull = createAxiosWithPreset('full');
/** * Axios 插件管理工具 - 类似 Vue 的 use 链式调用风格 */ import axios from 'axios'; export function createAxios(config = {}) { const instance = axios.create(config); const installedPlugins = new Set(); const pluginOrder = []; function use(plugin, options = {}) { if (typeof plugin === 'function') { plugin = { install: plugin }; } if (!plugin || typeof plugin.install !== 'function') { console.error('[AxiosUse] 插件必须提供 install 方法:', plugin); return instance; } if (installedPlugins.has(plugin)) { console.warn('[AxiosUse] 插件已安装,跳过:', plugin.name || 'anonymous'); return instance; } installedPlugins.add(plugin); const pluginInfo = { name: plugin.name || 'anonymous', options, installTime: Date.now() }; pluginOrder.push(pluginInfo); try { plugin.install(instance, options); console.log(`[AxiosUse] ✅ 插件安装成功: ${pluginInfo.name}`); } catch (error) { console.error(`[AxiosUse] ❌ 插件安装失败: ${pluginInfo.name}`, error); } return instance; } function useBatch(plugins) { plugins.forEach(({ plugin, options = {} }) => { use(plugin, options); }); return instance; } instance.use = use; instance.useBatch = useBatch; instance.getInstalledPlugins = () => [...pluginOrder]; instance.hasPlugin = (plugin) => installedPlugins.has(plugin); return instance; } // ==================== 内置插件 ==================== export const LogPlugin = { name: 'LogPlugin', install(instance, options = {}) { const { logRequest = true, logResponse = true, logError = true, prefix = '[Axios]' } = options; instance.interceptors.request.use( (config) => { config._startTime = Date.now(); logRequest && console.log(`${prefix} 📤 ${config.method?.toUpperCase()} ${config.url}`); return config; }, (error) => { logError && console.error(`${prefix} ❌ 请求错误:`, error.message); return Promise.reject(error); } ); instance.interceptors.response.use( (response) => { const duration = Date.now() - response.config._startTime; logResponse && console.log(`${prefix} 📥 ${response.config.url} - ${response.status} (${duration}ms)`); return response; }, (error) => { if (logError) { const duration = Date.now() - (error.config?._startTime || Date.now()); console.error( `${prefix} ❌ ${error.config?.url} - ${error.response?.status || 'Network Error'} (${duration}ms)` ); } return Promise.reject(error); } ); } }; export const TokenPlugin = { name: 'TokenPlugin', install(instance, options = {}) { const { getToken = () => localStorage.getItem('token'), headerName = 'Authorization', tokenPrefix = 'Bearer ', whiteList = [] } = options; instance.interceptors.request.use( (config) => { if (whiteList.some(url => config.url?.includes(url))) { return config; } const token = getToken(); if (token) { config.headers[headerName] = `${tokenPrefix}${token}`; } return config; }, (error) => Promise.reject(error) ); } }; export const CancelDuplicatePlugin = { name: 'CancelDuplicatePlugin', install(instance, options = {}) { const { generateKey = (config) => `${config.method}_${config.url}_${JSON.stringify(config.params)}`, message = '取消重复请求' } = options; const pendingRequests = new Map(); instance.interceptors.request.use( (config) => { const key = generateKey(config); if (pendingRequests.has(key)) { pendingRequests.get(key).cancel(message); } const source = axios.CancelToken.source(); config.cancelToken = source.token; pendingRequests.set(key, source); config._cancelKey = key; return config; }, (error) => Promise.reject(error) ); instance.interceptors.response.use( (response) => { const key = response.config._cancelKey; if (key) pendingRequests.delete(key); return response; }, (error) => { const key = error.config?._cancelKey; if (key) pendingRequests.delete(key); return Promise.reject(error); } ); } }; export const RetryPlugin = { name: 'RetryPlugin', install(instance, options = {}) { const { maxRetries = 3, retryDelay = 1000, retryDelayMultiplier = 2, maxRetryDelay = 30000, retryCondition = (error) => { return !error.response || error.response.status >= 500; } } = options; instance.interceptors.response.use( (response) => response, async (error) => { const config = error.config; if (!config) return Promise.reject(error); config._retryCount = config._retryCount || 0; if (config._retryCount >= maxRetries || !retryCondition(error)) { return Promise.reject(error); } config._retryCount++; const delay = Math.min( retryDelay * Math.pow(retryDelayMultiplier, config._retryCount - 1), maxRetryDelay ); await new Promise(resolve => setTimeout(resolve, delay)); return instance(config); } ); } }; export const LoadingPlugin = { name: 'LoadingPlugin', install(instance, options = {}) { const { showLoading = () => console.log('Loading...'), hideLoading = () => console.log('Loading done'), minDuration = 0 } = options; let requestCount = 0; let minDurationTimer = null; instance.interceptors.request.use( (config) => { if (config.showLoading !== false) { if (requestCount === 0) { showLoading(); } requestCount++; } config._showLoading = config.showLoading !== false; return config; }, (error) => Promise.reject(error) ); const hide = () => { requestCount--; if (requestCount <= 0) { requestCount = 0; if (minDuration > 0) { clearTimeout(minDurationTimer); minDurationTimer = setTimeout(() => { hideLoading(); }, minDuration); } else { hideLoading(); } } }; instance.interceptors.response.use( (response) => { response.config._showLoading && hide(); return response; }, (error) => { error.config?._showLoading && hide(); return Promise.reject(error); } ); } }; export const CachePlugin = { name: 'CachePlugin', install(instance, options = {}) { const { ttl = 5 * 60 * 1000, maxSize = 100, keyGenerator = (config) => `${config.url}_${JSON.stringify(config.params)}` } = options; const cache = new Map(); const accessOrder = []; instance.interceptors.request.use( (config) => { if (config.method !== 'get' || config.cache === false) return config; const key = keyGenerator(config); const cached = cache.get(key); if (cached && Date.now() < cached.expireTime) { const source = axios.CancelToken.source(); config.cancelToken = source.token; setTimeout(() => { source.cancel(JSON.stringify({ __fromCache: true, data: cached.data })); }, 0); } config._cacheKey = key; return config; }, (error) => Promise.reject(error) ); instance.interceptors.response.use( (response) => { const key = response.config._cacheKey; if (key && response.config.method === 'get') { if (cache.size >= maxSize) { const oldest = accessOrder.shift(); cache.delete(oldest); } cache.set(key, { data: response.data, expireTime: Date.now() + ttl }); accessOrder.push(key); } return response; }, (error) => { if (axios.isCancel(error) && error.message) { try { const parsed = JSON.parse(error.message); if (parsed.__fromCache) { return Promise.resolve({ data: parsed.data, config: error.config, status: 200, statusText: 'OK (from cache)', headers: {} }); } } catch {} } return Promise.reject(error); } ); } }; export const ErrorHandlerPlugin = { name: 'ErrorHandlerPlugin', install(instance, options = {}) { const { handler = (error) => console.error('[ErrorHandler]', error.message), errorMap = { 400: '请求参数错误', 401: '未授权,请重新登录', 403: '拒绝访问', 404: '请求的资源不存在', 500: '服务器内部错误', 502: '网关错误', 503: '服务不可用', 504: '网关超时' } } = options; instance.interceptors.response.use( (response) => response, (error) => { if (error.response) { const status = error.response.status; error.message = errorMap[status] || `请求失败: ${status}`; } else if (error.request) { error.message = '网络错误,请检查网络连接'; } handler(error); return Promise.reject(error); } ); } }; // ==================== 预设 ==================== export function createAxiosWithPreset(preset = 'default', config = {}) { const instance = createAxios(config); const presets = { minimal: [LogPlugin], default: [ { plugin: LogPlugin }, { plugin: TokenPlugin }, { plugin: CancelDuplicatePlugin }, { plugin: ErrorHandlerPlugin } ], full: [ { plugin: LogPlugin }, { plugin: TokenPlugin }, { plugin: CancelDuplicatePlugin }, { plugin: RetryPlugin, options: { maxRetries: 3 } }, { plugin: LoadingPlugin }, { plugin: CachePlugin }, { plugin: ErrorHandlerPlugin } ] }; (presets[preset] || presets.default).forEach(({ plugin, options }) => { instance.use(plugin, options); }); return instance; } export default createAxios;


本文作者:繁星
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!