mp-mini-axios
Version:
MiniAxios 是一个轻量级 HTTP 客户端库,专为【微信小程序】设计,提供类似 Axios 的 API 接口。它支持请求/响应拦截器、取消请求、自动重试、文件上传/下载等功能。构建后大小11KB,能有效节省小程序包大小。
647 lines (566 loc) • 17.3 kB
JavaScript
// 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
};