参考:
文档:www.axios-js.com/zh-cn/docs/…
若川分析博客:lxchuan12.gitee.io/axios/#_4-1…
属性api图:lxchuan12.gitee.io/assets/img/…
ts手写:www.bbsmax.com/A/x9J2PnnZd…
axios分析博客:mp.weixin.qq.com/s/fIUf8lVL2…
axios优点:mp.weixin.qq.com/s?__biz=MzI…
特性
-
自动判别环境,在浏览器通过XMLHttpRequests,node通过http发出请求
-
支持PromiseAPI
-
拦截请求和响应
-
转换请求和响应数据,自动转换 JSON 数据
-
取消请求
-
客户端支持防御CSRF
流程
调用方式
// 方式 1 axios(config)
axios({
method: 'get',
url: 'xxx',
data: {}
});
// 方式 2 axios(url[, config]),默认 get 请求
axios('http://xxx');
// 方式 3 使用别名进行请求
axios.request(config)
axios.get(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
...
// 方式 4 创建 axios 实例,自定义配置,我项目基本都是这种
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
axios#request(config) 其实是instance.request(config),我真用axios#request去试的时候结果报错了
axios#get(url[, config])
axios#post(url[, data[, config]])
axios#put(url[, data[, config]])
目录
lib
└─ adapters
├─ http.js // node 环境下利用 http 模块发起请求
├─ xhr.js // 浏览器环境下利用 xhr 发起请求
└─ cancel
├─ Cancel.js
├─ CancelToken.js
├─ isCancel.js
└─ core
├─ Axios.js // 生成 Axios 实例
├─ InterceptorManager.js // 拦截器
├─ dispatchRequest.js // 调用适配器发起请求
...
└─ helpers
├─ mergeConfig.js // 合并配置
├─ ...
├─ axios.js // 入口文件
├─ defaults.js // axios 默认配置项
├─ utils.js
源码
构建函数
# axios.js
'use strict';
var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');
/**
* Axios构建函数上有default和interceptors属性;
* 原型上有get,put等方法
* 把原型上的request bind到构建的context上形成新函数,赋值给instance,这样直接调用instance就相当于直接用request方法
* 把原型的get,pull等方法添加到instance上
* 把Axios构建函数的属性贴到instance上
* 在instance再加上create,all,spread,cancel等方法
*
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
* @return {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
//方式1:Axios.prototype.request.bind(content) 看例子
// 把 instance 指向 Axios.prototype.request 方法,方式1和2就是直接执行request函数
var instance = bind(Axios.prototype.request, context);
//function request(a, b) {
// return a + b
//}
//let context = null
//let instance = request.bind(context)
//console.log(instance(2, 3))//5
// 把 Axios.prototype 上的方法(get,post等)扩展到 instance 上,指定上下文是 context
//这里是能用方式3和4原因
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);
// 把 context 上的方法扩展到 instance 上
// axios.defaults 和拦截器 axios.interceptors 可以使用
// Copy context to instance
utils.extend(instance, context);
//这里是和默认配置合并的配置
// Factory for creating new instances
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
// Create the default instance to be exported
var axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
axios.Axios = Axios;
// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.VERSION = require('./env/data').version;
// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
// Expose isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');
module.exports = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;
bind
var instance = bind(Axios.prototype.request, context);
=》 Axios.prototype.request.bind(context)
//下面是为了兼容才写的这么繁琐
module.exports = function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};
extend
utils.extend(instance, Axios.prototype, context);
//Extends object a by mutably adding to it the properties of object b.
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
function forEach(obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return;
}
// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj];
}
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
Axios.prototype
Axios.prototype.getUri = function getUri(config) {
config = mergeConfig(this.defaults, config);
return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
//方式3和4,4的create里的是config,和3的axios.get(url[, config])里的config可以说是一个意思,并没有新构建函数
// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function (url, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: (config || {}).data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function (url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
}));
};
});
请求流程
调用 Axios.prototype.request(有请求拦截器的情况下执行请求拦截器),
中间会执行 dispatchRequest方法(数据转化,处理header),
dispatchRequest 之后调用 adapter (xhrAdapter)(判断是走xmlrequest还是http)
最后调用 Promise 中的函数dispatchXhrRequest,(真正的请求,有响应拦截器的情况下最后会再调用响应拦截器),
最后得到结果,resolve出来
request
和拦截器们组成promise链表,开始执行请求拦截器+请求+响应拦截器
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
//方式2
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
//方式1
config = config || {};
}
// 合并配置项
config = mergeConfig(this.defaults, config);
// 设置 请求方法,默认 get 。
// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// 组成`Promise`链
。。。
//拦截器用的时候是: axios.interceptors.request.use(resovlve函数,reject函数) 会存到this.interceptors.request里
// filter out skipped interceptors
//请求拦截器数组,由于是unshift进来的,所以是拦截器写后面的反而放在数组前面
var requestInterceptorChain = [];
。。。
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
。。。
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
//响应拦截,依次
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
if (!synchronousRequestInterceptors) {
//真正的请求 放在chain里
var chain = [dispatchRequest, undefined];
//我是觉得chain = requestInterceptorChain.concat(chain,responseInterceptorChain);会比较好看些
//总之这里就是[请求拦截,请求,响应拦截]
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain = chain.concat(responseInterceptorChain);
//遍历chain,组成promise链,下面有图
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
//返回最终的结果
return promise;
}
//如果没有请求拦截器,就走下面的代码。我感觉这里代码有点赘余
var newConfig = config;
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
}
return promise;
};
dispatchRequest
请求拦截器执行完成后,走到这里执行真正的请求
1.如果已经取消,则 throw
原因报错,使Promise
走向rejected
。
2.确保 config.header
存在。
3.利用用户设置的和默认的请求转换器转换数据。
4.拍平 config.header
。
5.删除一些 config.header
。
6.返回适配器adapter
(Promise
实例)执行后 then
执行后的 Promise
实例。返回结果传递给响应拦截器处理。
module.exports = function dispatchRequest(config) {
/**
* 抛出 错误原因,使`Promise`走向`rejected`
*/
throwIfCancellationRequested(config);
// Ensure headers exist
config.headers = config.headers || {};
// Transform request data
// 转换请求的数据,后面有transformData,下面细讲
config.data = transformData.call(
config,
config.data,
config.headers,
config.transformRequest
);
// Flatten headers
// 拍平 headers
config.headers = utils.merge(
config.headers.common || {}, //里面是'Accept': 'application/json, text/plain, */*'
//为'post', 'put', 'patch'时是 'Content-Type': 'application/x-www-form-urlencoded'
config.headers[config.method] || {},
config.headers
);
//删除一些 `config.header`。
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
//可以用自己写的adapter函数,默认用默认的
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data 转换数据,比如处理对象,转成json字符串
response.data = transformData.call(
config,
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
// Transform response data
if (reason && reason.response) {
reason.response.data = transformData.call(
config,
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
};
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
if (config.signal && config.signal.aborted) {
throw new Cancel('canceled');
}
}
module.exports = function transformData(data, headers, fns) {
var context = this || defaults;
/*eslint no-param-reassign:0*/
utils.forEach(fns, function transform(fn) {
//就是遍历执行transformRequest,可以是默认的,也可以是自己写在config里的转换函数
data = fn.call(context, data, headers);
});
return data;
};
# 默认的transfromResquest 转请求数据
transformRequest: [function transformRequest(data, headers) {
//把config里的类Accept属性改成Accept,如accept
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
//一些数据的处理,过
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
//一块内存。。
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
//这里重点,传的数据是对象的话自动转json字符串并加json头
if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
setContentTypeIfUnset(headers, 'application/json');
return stringifySafely(data);
}
return data;
}],
module.exports = function normalizeHeaderName(headers, normalizedName) {
utils.forEach(headers, function processHeader(value, name) {
if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {
headers[normalizedName] = value;
delete headers[name];
}
});
};
# 默认的transformResponse 转响应数据 JSON.parse(data);
transformResponse: [function transformResponse(data) {
。。。
if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
try {
return JSON.parse(data);
} catch (e) {
if (strictJSONParsing) {
if (e.name === 'SyntaxError') {
throw enhanceError(e, this, 'E_JSON_PARSE');
}
throw e;
}
}
}
return data;
}],
adapter
adapter: getDefaultAdapter(),
function getDefaultAdapter() {
// 浏览器有 XMLHttpRequest ,node有 process
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
//浏览器走XMLHttpRequest
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
function onloadend() {
if (!request) {
return;
}
// Prepare the response
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);
// Clean up request
request = null;
}
//这里ajax就是这样嘛,readyState=4就是响应完成,得到响应数据再resolve出去,串起来,响应数据再去转化数据然后再去响应拦截器执行
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
setTimeout(onloadend);
};
// Send the request
request.send(requestData);
})
}
自定义adapter
可以用来自定义mock
const mockUrl = {
'/mock': {data: xxx}
};
const instance = Axios.create({
adapter: (config) => {
//如果不是要mock的链接,走默认的adapter
if (!mockUrl[config.url]) {
// 调用默认的适配器处理需要删除自定义适配器,否则会死循环
delete config.adapter
return Axios(config)
}
return new Promise((resolve, reject) => {
resolve({
data: mockUrl[config.url],
status: 200,
})
})
}
})
新增、重写数据转化方法transformRequest
在执行 adapter 前 会去转换请求数据
// Transform request data
// 转换请求的数据
config.data = transformData.call(
config,
config.data,
config.headers,
config.transformRequest
);
//用transformRequest的函数数组遍历执行转化data
module.exports = function transformData(data, headers, fns) {
var context = this || defaults;
/*eslint no-param-reassign:0*/
utils.forEach(fns, function transform(fn) {
data = fn.call(context, data, headers);
});
return data;
};
其中config是默认合并我们有定义的数据,如下 mergeConfig(config1, config2):New object resulting from merging config2 to config1
// Factory for creating new instances
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
- 默认的transformRequest
transformRequest: [function transformRequest(data, headers) {
。。。
return data;
}],
// 重写转换请求数据的过程
axios.default.transformRequest = [(data, headers) => {
...
return data
}];
// 增加对请求数据的处理
axios.default.transformRequest.push(
(data, headers) => {
...
return data
});
//如果是请求前的数据转化,那就写在请求拦截器里或者axios.default.transformRequest.unshift
请求取消
api:www.axios-js.com/zh-cn/docs/…
两种方式:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// cancel the request
cancel();
注意: 可以使用同一个 cancel token 取消多个请求
原理
#config有配cancelToken
#xhr.js
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function (cancel) {
if (!request) {
return;
}
//5
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
request.abort();
request = null;
};
//这里注册了取消请求的函数,详情见下面
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
# CancelToken.js
//这里维护一个_listeners数组,存储这些取消请求的回调函数
CancelToken.prototype.subscribe = function subscribe(listener) {
if (this.reason) {
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
};
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
配置里的cancelToken: source.token,这里的token就是new CancelToken(function executor(c) {cancel = c;});,表示以参数函数调用时的参数赋值给cancel
而这个cancel为function cancel(message) {。。。},具体为何看下面的代码
source.cancel('error message')具体也看下面的代码1,2,3,
# new CancelToken(function executor(c) {cancel = c;});
function CancelToken(executor) {
。。。
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
//3.这里的cancel即'error message'
// eslint-disable-next-line func-names
this.promise.then(function(cancel) {
if (!token._listeners) return;
var i;
var l = token._listeners.length;
//4.然后这里遍历执行那些_listeners里的取消回调函数,reject出去并取消请求。这就是我理解的取消请求了。
for (i = 0; i < l; i++) {
token._listeners[i](cancel);
}
token._listeners = null;
});
// eslint-disable-next-line func-names
this.promise.then = function(onfulfilled) {
var _resolve;
// eslint-disable-next-line func-names
var promise = new Promise(function(resolve) {
token.subscribe(resolve);
_resolve = resolve;
}).then(onfulfilled);
promise.cancel = function reject() {
token.unsubscribe(_resolve);
};
return promise;
};
//构造函数里面执行这个函数 executor(function cancel(message){})
//故 function executor(c) {cancel = c;}里面的cancel等于 function cancel(message) {。。。}
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
//1.source.cancel('error message') 这里就有message传入了,赋值给token.reason
token.reason = new Cancel(message);
//2.resolvePromise(token.reason);就是rosolve('error message'),然后走then
resolvePromise(token.reason);
});
}
方式2好像不用讲了
取消重复的请求
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel);
}
});
原理:
维护一个变量map,在请求拦截器中添加这个请求url到map,在请求拦截器中可以通过map得知当前是否是重复的请求,若是就取消前面的请求,
在响应拦截器中,将map中对应的完成的url去除
很精彩,但我觉得应该注意怎么阻止重复请求,如防抖,在请求完成前不允许重复的请求才对吧,反而取消之前的请求,这种情景应该很少见吧
关于data和params
在get方法里写data是无效的
其中get的params之所以有效是因为
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
其中post中的data之所以有效是因为
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function (url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
}));
};
});
拦截器
# InterceptorManager.js
function InterceptorManager() {
this.handlers = [];
}
//可以看到这里use使用拦截器时,就是往handlers里加入一个新的对象,并返回这个对象对应的索引
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
};
//而这个删除拦截器,传入id,即上面获取到的索引,就是将handlers里这个索引对应的对象清除即可
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
CSRF
「跨站请求伪造」
攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作
防御手段:
1.检查referer字段,这个容易被改
2.同步表单校验:在表单请求里携带csrf的查询参数
3.双重cookie:传cookie又传token
axios的防御手段就是双重cookie
Axios 提供了 xsrfCookieName
和 xsrfHeaderName
两个属性来分别设置 CSRF 的 Cookie 名称和 HTTP 请求头的名称,它们的默认值如下所示:
// lib/defaults.js
var defaults = {
adapter: getDefaultAdapter(),
// 省略部分代码
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
};
前面我们已经知道在不同的平台中,Axios 使用不同的适配器来发送 HTTP 请求,这里我们以浏览器平台为例,来看一下 Axios 如何防御 CSRF 攻击:
简单说就是会在header里添加'X-XSRF-TOKEN':cookie
// lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestHeaders = config.headers;
var request = new XMLHttpRequest();
// 省略部分代码
// 添加xsrf头部
if (utils.isStandardBrowserEnv()) {
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
request.send(requestData);
});
};
和其他的区别
执行过程基于koa式的中间件,上传下载进度的处理比axios的回调函数更好
网络请求基于fetch,更强大,axios有的他都有,类koa的洋葱机制,让开发者优雅地做请求前后的增强处理,支持创建实例、全局、内核中间件。
fetch
没拦截器,不能取消请求啥的
特性 | umi-request | fetch | axios |
---|---|---|---|
实现 | 浏览器原生支持 | 浏览器原生支持 | XMLHttpRequest |
大小 | 9k | 4k (polyfill) | 14k |
query 简化 | ✅ | ❌ | ✅ |
post 简化 | ✅ | ❌ | ❌ |
超时 | ✅ | ❌ | ✅ |
缓存 | ✅ | ❌ | ❌ |
错误检查 | ✅ | ❌ | ❌ |
错误处理 | ✅ | ❌ | ✅ |
拦截器 | ✅ | ❌ | ✅ |
前缀 | ✅ | ❌ | ❌ |
后缀 | ✅ | ❌ | ❌ |
处理 gbk | ✅ | ❌ | ❌ |
中间件 | ✅ | ❌ | ❌ |
取消请求 | ✅ | ❌ | ✅ |