UNPKG

mp-mini-axios

Version:

MiniAxios 是一个轻量级 HTTP 客户端库,专为【微信小程序】设计,提供类似 Axios 的 API 接口。它支持请求/响应拦截器、取消请求、自动重试、文件上传/下载等功能。构建后大小11KB,能有效节省小程序包大小。

647 lines (566 loc) 17.3 kB
// MiniAxios.js class InterceptorManager { constructor() { this.handlers = []; } use(fulfilled, rejected) { if (typeof fulfilled !== 'function' || (rejected && typeof rejected !== 'function')) { throw new Error('interceptor must be a function'); } this.handlers.push({ fulfilled, rejected }); return this.handlers.length - 1; } eject(id) { if (id >= 0 && id < this.handlers.length) { this.handlers[id] = null; } } forEach(fn) { this.handlers.forEach(handler => { if (handler !== null) { fn(handler); } }); } } class MiniAxios { constructor(config = {}) { // 默认配置 this.defaults = { baseURL: '', timeout: 60000, headers: {}, retry: false, retryDelay: 1000, ...config, }; // 拦截器 this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; // 绑定方法上下文 this.request = this.request.bind(this); this.dispatchRequest = this.dispatchRequest.bind(this); this.handleResponseSuccess = this.handleResponseSuccess.bind(this); this.handleResponseError = this.handleResponseError.bind(this); this.setupRequestEvents = this.setupRequestEvents.bind(this); this.get = this.get.bind(this); this.post = this.post.bind(this); this.put = this.put.bind(this); this.delete = this.delete.bind(this); this.head = this.head.bind(this); this.options = this.options.bind(this); this.uploadFile = this.uploadFile.bind(this); this.downloadFile = this.downloadFile.bind(this); this.create = this.create.bind(this); // 使实例可调用 const instance = this.request.bind(this); // 获取原型方法 const protoMethods = Object.getOwnPropertyNames(MiniAxios.prototype) .filter(k => k !== 'constructor'); // 复制实例属性和原型方法 Object.keys(this).concat(protoMethods).forEach(key => { if (protoMethods.includes(key)) { // 原型方法需要绑定上下文 instance[key] = this[key].bind(this); } else { // 实例属性直接复制 instance[key] = this[key]; } }); return instance; } request(config) { // 参数校验 if (typeof config !== 'object' || config === null) { return Promise.reject(new Error('config must be an object')); } // 确保URL存在 if (!config.url) { return Promise.reject(new Error('url is required')); } // 如果是文件上传请求,检查必要参数 if (config.api === 'uploadFile' && !config.filePath) { return Promise.reject(new Error('filePath is required')); } try { // 创建请求链 const chain = this.createInterceptorChain(); // 创建配置 (这时候还没走拦截器, 只是初始化配置 + 单个请求配置) config = this.makeConfig(this.defaults, config); if (config.debug) { console.log('init config', JSON.parse(JSON.stringify(config))) } // 处理方法名大小写 if (config.method) { config.method = config.method.toLowerCase(); } // 构建Promise链 let promise = Promise.resolve(config); while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise; } catch (error) { return Promise.reject(error); } } // 创建配置 makeConfig(defaults, config) { return { ...defaults, ...config, ...this.mergeConfig(defaults, config), }; } // 合并配置 mergeConfig(defaults, config) { const headers = { ...(defaults.headers || {}), ...(defaults.header || {}), ...(config.headers || {}), ...(config.header || {}), }; const data = { ...(defaults.data || {}), ...(defaults.body || {}), ...(config.data || {}), ...(config.body || {}), }; const params = { ...(defaults.params || {}), ...(defaults.query || {}), ...(config.params || {}), ...(config.query || {}), }; return { headers, header: undefined, data, body: undefined, params, query: undefined, }; } // 构建完整URL buildFullUrl(config) { if (!config.baseURL || /^https?:\/\//.test(config.url)) { return config.url; } const baseURL = config.baseURL.replace(/\/$/, '') const url = config.url.replace(/^\//, '') return `${baseURL}/${url}`; } // 添加查询参数 addQueryParams(config) { if (!config.params || Object.keys(config.params).length === 0) { return config.url; } // 手动构建查询字符串 const queryString = Object.entries(config.params) .filter(([_, value]) => value !== undefined && value !== null) .map(([key, value]) => { if (Array.isArray(value)) { return value.map(v => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`).join('&'); } else if (typeof value === 'object') { return `${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value))}`; } return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; }) .join('&'); if (!queryString) { return config.url; } // 检查URL是否已有查询参数 const separator = config.url.includes('?') ? '&' : '?'; return `${config.url}${separator}${queryString}`; } // 创建拦截器链 createInterceptorChain() { // 请求拦截器链 const requestInterceptorChain = []; this.interceptors.request.forEach(interceptor => { requestInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); // 响应拦截器链 const responseInterceptorChain = []; this.interceptors.response.forEach(interceptor => { responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); // 创建请求链 const chain = [ this.dispatchRequest, undefined ]; // 添加请求拦截器 requestInterceptorChain.reverse().forEach(handler => { chain.unshift(handler); }); // 添加响应拦截器 responseInterceptorChain.forEach(handler => { chain.push(handler); }); return chain; } dispatchRequest(config) { return new Promise((resolve, reject) => { try { // 检查取消状态 if (config.cancelToken && typeof config.cancelToken.throwIfRequested === 'function') { config.cancelToken.throwIfRequested(); } if (config.signal && typeof config.signal.throwIfAborted === 'function') { config.signal.throwIfAborted(); } // 取消与中止 let abort; const cancelToken = config.cancelToken; const signal = config.signal; // 创建超时定时器 let timeoutId; const timeout = config.timeout || this.defaults.timeout || 60000; if (timeout > 0) { timeoutId = setTimeout(() => { if (abort) { abort(); } const error = new Error(`timeout of ${timeout}ms exceeded`); error.code = 'ETIMEDOUT'; reject(error); }, timeout); } // 合并配置 (这时候已经走完拦截器, 在处理一下拦截器添加之后的配置, 注意) Object.assign(config, this.mergeConfig(this.defaults, config)) if (config.debug) { console.log('final config', config) } // 处理完整URL let url = this.buildFullUrl(config); // 处理查询参数 url = this.addQueryParams({ ...config, url }); // 发起请求 let requestApi, opts; switch (config.api) { case 'downloadFile': requestApi = wx.downloadFile; opts = { filePath: config.filePath, }; break; case 'uploadFile': requestApi = wx.uploadFile; opts = { filePath: config.filePath, name: config.name || config.fileName || 'file', formData: config.formData || config.data || {}, }; break; default: requestApi = wx.request; opts = { data: config.data, }; } const requestOpts = { url, ...opts, header: config.headers, timeout: config.timeout, method: config.method || 'get', dataType: config.dataType || 'json', responseType: config.responseType || 'text', enableCache: config.enableCache, enableHttp2: config.enableHttp2, enableQuic: config.enableQuic, success: (res) => { clearTimeout(timeoutId); this.handleResponseSuccess(res, config, requestTask, resolve, reject); }, fail: (err) => { clearTimeout(timeoutId); this.handleResponseError(err, config, requestTask, reject); }, } if (config.debug) { console.log('request config', requestOpts) } const requestTask = requestApi(requestOpts); // 保存abort函数用于取消请求 abort = () => { try { requestTask.abort(); } catch (e) { console.warn('取消请求时出错:', e); } }; // 监听取消令牌 if (cancelToken) { cancelToken.promise.then(reason => { if (abort) { abort(); } reject(reason); }); } // 监听中止信号 if (signal && typeof signal.addEventListener === 'function') { const onAbort = () => { if (abort) { abort(); } // 创建标准的中止错误 const abortError = new Error(`The request has been aborted`); abortError.code = 'ECONNABORTED'; reject(abortError); }; if (signal.aborted) onAbort(); else signal.addEventListener('abort', onAbort); } // 支持事件监听 this.setupRequestEvents(requestTask, config); } catch (error) { reject(error); } }); } handleResponseSuccess(res, config, requestTask, resolve, reject) { try { let data; // 处理返回结果 if (config.api === 'downloadFile') { // 下载文件 data = { tempFilePath: res.tempFilePath, filePath: res.filePath, }; } else { // 其他请求 data = res.data; } // 尝试解析JSON(上传文件没有parse数据) if (typeof data === 'string') { try { data = JSON.parse(data); } catch (e) { // 解析失败,保持原样 } } const response = { data: data, status: res.statusCode, statusText: 'OK', headers: res.header, config: config, request: requestTask }; // 处理非200状态码 if (res.statusCode < 200 || res.statusCode >= 300) { const error = new Error(`Request failed with status code ${res.statusCode}`); error.code = res.statusCode >= 500 ? 'ERR_BAD_RESPONSE' : 'ERR_BAD_REQUEST' error.config = config; error.request = requestTask; error.response = response; error.status = response.status; this.handleResponseError(error, config, requestTask, reject); } else { resolve(response); } } catch (error) { reject(error); } } handleResponseError(err, config, requestTask, reject) { try { let error; if (MiniAxios.isCancel(err) || err.code === 'ECONNABORTED' || err.code === 'ETIMEDOUT' || err.code === 'ERR_BAD_RESPONSE' || err.code === 'ERR_BAD_REQUEST') { // 取消错误 | 中止错误 | 超时错误 | 非200状态码错误(2个) error = err; } else { // 微信接口错误 const err = new Error(err?.errMsg || 'request error'); err.code = 'WX_API_ERROR'; err.errno = err?.errno === undefined ? -1 : err.errno; error = err; } // 添加配置和请求信息 error.config = config; error.request = requestTask; // 检查是否需要重试 if (this.shouldRetry(error, config)) { this.retryRequest(error, config, resolve => resolve(), reject); } else { // 添加重试次数信息 if (config.__retryCount) { error.retryCount = config.__retryCount; } reject(error); } } catch (e) { reject(e); } } // 检查是否需要重试 shouldRetry(error, config) { // 如果是取消错误、中止错误或超时错误,不重试 if (MiniAxios.isCancel(error) || error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') { return false; } // 检查重试配置 const retry = config.retry; if (!retry) return false; const retryCount = config.__retryCount || 0; const maxRetries = typeof retry === 'number' ? retry : 1; return retryCount < maxRetries; } // 重试请求 retryRequest(error, config, resolve, reject) { config.__retryCount = (config.__retryCount || 0) + 1; const retryDelay = this.calculateRetryDelay(config); // 触发重试事件 if (typeof config.onRetry === 'function') { config.onRetry({ retryCount: config.__retryCount, retryDelay, error }); } setTimeout(() => { this.dispatchRequest(config) .then(resolve) .catch(err => { if (this.shouldRetry(err, config)) { this.retryRequest(err, config, resolve, reject); } else { // 添加重试次数信息 if (config.__retryCount) { err.retryCount = config.__retryCount; } reject(err); } }); }, retryDelay); } // 计算重试延迟 calculateRetryDelay(config) { const retryDelay = config.retryDelay || this.defaults.retryDelay || 1000; const retryCount = config.__retryCount || 0; // 指数退避策略 return retryDelay * Math.pow(2, retryCount); } setupRequestEvents(requestTask, config) { try { const events = { onHeadersReceived: config.onHeadersReceived, onChunkReceived: config.onChunkReceived, onProgressUpdate: config.onProgressUpdate }; for (const [eventName, handler] of Object.entries(events)) { if (typeof handler === 'function' && typeof requestTask[eventName] === 'function') { requestTask[eventName](handler); } } } catch (e) { console.warn('设置请求事件监听器时出错:', e); } } // 快捷方法 get(url, config = {}) { return this.request({ ...config, url, method: 'get' }); } post(url, data = {}, config = {}) { return this.request({ ...config, url, data, method: 'post' }); } put(url, data = {}, config = {}) { return this.request({ ...config, url, data, method: 'put' }); } delete(url, config = {}) { return this.request({ ...config, url, method: 'delete' }); } head(url, config = {}) { return this.request({ ...config, url, method: 'head' }); } options(url, config = {}) { return this.request({ ...config, url, method: 'options' }); } // 文件上传方法 uploadFile(url, data = {}, config = {}) { if (url.url) { config = url; data = url.data; url = url.url; } else if (data.url) { config = data; data = data.data; } return this.request({ ...config, url, data, api: 'uploadFile' }); } // 文件下载方法 downloadFile(url, config = {}) { if (url.url) { config = url; url = url.url; } return this.request({ ...config, url, api: 'downloadFile' }); } create(config) { return new MiniAxios(config); } static isCancel(value) { return !!(value && value.__CANCEL__); } } // 创建默认实例 const miniAxios = new MiniAxios(); // 导出默认实例和类 export default miniAxios; export { MiniAxios };