1. axios
今天我们来看一下axios
里面一些方法大概的实现方式和执行过程,为什么会有这篇文章呢?因为在我平常开发的项目中都是使用axios
来请求的,平常接触很多,同时也看了不少关于它的文章,但是还是解答不了我心中的一些疑惑。比如拦截器的原理啊、如何取消的请求啊等等。所以我就在空闲的时间去看它的源码是怎么实现的。想着肯定有好多人也不理解,所以就想分享一下自己的想法来帮助大家理解。
不啰嗦了,下面我们就进入正题吧。
1. 1 初始化
- 一般我们通过下面的方法创建一个axios实例
import axios from 'axios';
// 创建一个axios实例
const instance = axios.create({
baseURL: window.location.origin + '/xxx',
timeout: 5000,
headers: {
post: { 'Content-Type': 'application/json;charset=utf-8' },
},
});
下面就让我们来看一下,axios.create
的时候都发生了什么事情吧。
- 实现
// 有删减
// https://github.com/axios/axios/blob/94fc4ea7161b1e13c55df102e3177b53d26ef00e/lib/axios.js
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
createInstance
/**
* 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);
var instance = bind(Axios.prototype.request, context);
// 将context的axios.prototype 复制到 instance
utils.extend(instance, Axios.prototype, context);
// Copy context to instance
utils.extend(instance, context);
return instance;
}
function Axios(instanceConfig) {
this.defaults = instanceConfig;
// 拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
function InterceptorManager() {
this.handlers = [];
}
- 工具函数
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);
};
};
/**
* Extends object a by mutably adding to it the properties of object b.
*
* @param {Object} a The object to be extended
* @param {Object} b The object to copy properties from
* @param {Object} thisArg The object to bind function to
* @return {Object} The resulting value of object a
*/
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;
}
axios.default
的实现- 就是设置一些默认的配置
var defaults = {
adapter: getDefaultAdapter(),
transformRequest: [function transformRequest(data, headers) {
// 格式化数据,后面会用到
return data;
}],
transformResponse: [function transformResponse(data) {
// 格式化数据,后面会用到
return data;
}],
timeout: 0, // 单位事ms,0是未超时
// 安全验证的一些header
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};
defaults.headers = {
common: {
'Accept': 'application/json, text/plain, */*'
}
};
module.exports = defaults;
getDefaultAdapter
这里先需要重点记住- 判断使用哪种请求方式
- 浏览器就用xhr
- node中就用http
- 判断使用哪种请求方式
function getDefaultAdapter() {
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;
}
上面就是创建初始化的过程,下面我们来看一下发送请求的过程
1. 2 发送请求(get、post
)
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
- 请求方法初始化
https://github.com/axios/axios/blob/94fc4ea7161b1e13c55df102e3177b53d26ef00e/lib/core/Axios.js
// 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
方法- 只列出了异步的处理方法
/**
* Dispatch a request
*
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(config) {
// 对config的参数做了一些合并和处理
// todo
// 获取请求拦截器里面所有的数据
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;
var chain = [dispatchRequest, undefined];
// 拼接请求和响应的拦截器
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
// [...requestInterceptorChain(请求拦截器), dispatchRequest(请求), undefined, ...responseInterceptorChain(响应拦截器)]
promise = Promise.resolve(config);
// 按个执行
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
- dispatchRequest
module.exports = function dispatchRequest(config) {
// 如果取消请求了就reject
throwIfCancellationRequested(config);
// 对数据进行处理和格式化
// todo
// 初始化配置时的getDefaultAdapter(),浏览器里也就是xhr
var adapter = config.adapter || defaults.adapter;
// 发送请求,成功...,失败...
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
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);
});
};
是否cancel
/**
* Throws a `Cancel` if cancellation has been requested.
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
function isCancel(value) {
return !!(value && value.__CANCEL__);
};
- xhr adapter
- 其实可以理解为我们平常自己实现一个
xhr
的过程,代码有点多没有删减,可以大概过一下
- 其实可以理解为我们平常自己实现一个
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
var responseType = config.responseType;
var request = new XMLHttpRequest();
var fullPath = buildFullPath(config.baseURL, config.url);
// xhr.open
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Set the request timeout in MS
request.timeout = config.timeout;
// 当process结束的时候
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(resolve, reject, response);
// Clean up request
request = null;
}
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
setTimeout(onloadend);
};
}
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(createError('Request aborted', config, 'ECONNABORTED', request));
// Clean up request
request = null;
};
// Handle low level network errors
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(createError('Network Error', config, null, request));
// Clean up request
request = null;
};
// Handle timeout
request.ontimeout = function handleTimeout() {
var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(createError(
timeoutErrorMessage,
config,
config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
request));
// Clean up request
request = null;
};
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
// Add withCredentials to request if needed
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// Add responseType to request if needed
if (responseType && responseType !== 'json') {
request.responseType = config.responseType;
}
// Handle progress if needed
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
if (!requestData) {
requestData = null;
}
// Send the request
request.send(requestData);
});
};
- 我们都知道
axios
是支持防止xsrf
攻击的,不懂得可以自己搜一下。下面我们就来看一下它是怎么实现的吧。- 防止xsrf
- 其实就是从cookie里面获取到一个自定义的header,放到请求头里面
- 流程
- 如果是浏览器下,并且配置了携带cookie或者是同源的情况下,就去取值是'XSRF-TOKEN', name是xsrfCookieName的cookie
- 如果能取到,那么就将’X-XSRF-TOKEN‘header设置为该值
- 原理
- 其实原理还是通过在请求头中添加自定义属性,
- 属性值为后台返回的token(一般存放在cookie中)
- 当后台服务器接受收到浏览器的请求之后,会拿到请求头中的token,进行对比。
- 防止xsrf
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
function isStandardBrowserEnv() {
if (typeof navigator !== 'undefined' &&
(
navigator.product === 'ReactNative' ||
navigator.product === 'NativeScript' ||
navigator.product === 'NS'
)
) {
return false;
}
return (
typeof window !== 'undefined' &&
typeof document !== 'undefined'
);
}
- 上满用到的
settle
方法- 其实就是判断
response
的状态- 如果是个正常的状态码,例如
200<=status<300 || status === 304
返回就resolve
- 否则就
reject
- 如果是个正常的状态码,例如
- 其实就是判断
/**
* Resolve or reject a Promise based on response status.
*
* @param {Function} resolve A function that resolves the promise.
* @param {Function} reject A function that rejects the promise.
* @param {object} response The response.
*/
module.exports = function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(createError(
'Request failed with status code ' + response.status,
response.config,
null,
response.request,
response
));
}
};
1. 3 取消请求
- 我们先来看一下使用
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token // 配置标识
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
// 取消请求
source.cancel('Operation canceled by the user.');
CancelToken
的实现
var Cancel = require('./Cancel');
function CancelToken(executor) {
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
!!!重点
// 将这promised的resolve交由外界去执行
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
/**
* 返回一个对象,该对象包含一个新的“CancelToken”和一个函数,调用该函数时,
* 取消“CancelToken”。
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
// 其实
cancel = function cancel(message) {
token.reason = new Cancel(message);
resolvePromise(token.reason);
}
- 在请求时,配置的参数
// 如果发现在配置的时候携带着cancelToken,那么就执行下面的函数
if (config.cancelToken) {
!!! 这里非常重要
// 待resolve的promise
// 当我们执行
// source.cancel('Operation canceled by the user.');
// 这句就会执行
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
// 取消请求
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
- 示例
- 其实你完全可以理解为这样的,当然这是一个很精简的实现
let resolvePromise = null;
let cancelToken = function(){
return new Promise((resolve)=>{
resolvePromise = resolve;
});
};
cancelToken().then(()=>{
// todo
console.log('3s之后取消请求');
});
setTimeout(() => {
resolvePromise();
}, 3000);
1. 4 拦截器
- 使用
// 请求拦截器1
instance.interceptors.request.use(
(config) => {
// todo
return config;
},
(err)=>{
// todo
}
);
// 请求拦截器2
instance.interceptors.request.use(
(config) => {
// todo
return config;
},
(err)=>{
// todo
}
);
// 响应拦截器1
instance.interceptors.response.use(
(response) => {
// todo
},
(err) => {
// todo
},
);
// 响应拦截器2
instance.interceptors.response.use(
(response) => {
// todo
},
(err) => {
// todo
},
);
- 其实上面的整个执行是这样的
var promise;
// 如果不是同步的
var chain = [dispatchRequest, undefined];
// 拼接请求和响应的拦截器
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
- 用上面的示例,其实就是
[
请求拦截器2的resolve, 请求拦截器2的reject, // 1组
请求拦截器1的resolve, 请求拦截器1的reject, // 2组
真实的请求, undefined, // 3组
响应拦截器1的resolve, 响应拦截器1的reject, // 4组
响应拦截器2的resolve, 响应拦截器2的reject // 5组
]
- 示例图
- 画的很糙,将就着看看吧
- 画的很糙,将就着看看吧
1. 5实例方法
axios.all
axios.spread
- 这两个是为了并发请求用的,在实际项目中我反正没用过,不过原理其实还是很简单的
- axios.all
其实就是promise.all
axios.all = function all(promises) {
return Promise.all(promises);
};
- axios.spread
module.exports = function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
};
这个大家可能不是很好理解,我一开始也理解了半天,不知道是干什么用的。然后看网上的例子大部分都是和axios.all
一起用的
axios.all([getA(), getB()]).then(
axios.spread(function(resA, resB) {
console.log(resA.data);
console.log(resB.data);
})
);
axios.all
的返回值是[getA()的结果, getB()的结果
],然后把这个数据的返回值,传入axios.spread
的回调里面去。因为axios.spread
其实最后运行的是callback.apply(null, arr)
。所以其实就是
axios.spread(function(getA()的结果, getB()的结果) {
console.log(resA.data);
console.log(resB.data);
})
说白了就是把axios.all
返回的参数给展开了,传递给axios.spread
的回调。
1. 6 其他
- 我们从上面看到了
axios
里面用到了不少promise
里面的方法,那么我们接下来就来看看promise
是怎么实现的吧。 - Promise.all
- 里面的
promise
全都resolve
之后,all
才会resolve
- 有一个
reject
,all
就会reject
。
- 里面的
Promise.prototype.all = function(promises){
const data = [];
let index = 0; // 注意这里是自定义了一个索引,而没有用forEach里面的索引。
return new Promise((resolve, reject)=>{
promises.forEach((promise, i)=>{
Promise.resolve(promise).then((res)=>{
data[index ++] = res;
if( index === promises.length ){
resolve(data);
}
}, (err)=>{
reject(err);
})
})
})
}
- Promise.race
- 取里面第一个promise的结果
Promise.prototype.race = function(promises){
return new Promise((resolve, reject)=>{
promises.forEach((promise)=>{
Promise.resolve(promise).then((res)=>{
resolve(res);
}, (err)=>{
reject(err);
})
})
})
}
- Promise.allSettled
- 跟
all
很类似,但是他是等所有的promise
都执行完之后才会返回,而且是resolve
的 - 对里面的
promise
根据不同的状态做了分类
- 跟
Promise.prototype.allSettled = function(promises){
const data = [];
let index = 0;
return new Promise((resolve, reject)=>{
promises.forEach((promise, i)=>{
Promise.resolve(promise).then((res)=>{
data[index ++] = {status: 'fulfilled', value: res};
}, (err)=>{
data[index ++] = {status: 'rejected', reason: err};
}).finally(()=>{
if( index === promises.length ){
resolve(data);
}
})
})
})
}
2. umi-request
其实以前我没有听说过这个库(好像使用umi
创建的时候就请求默认使用的这个库),因为我自己在看axios
源码的时候,偶尔看到了这个库,而且两个的实现很相似,再加上有很多axios
之外的功能,所以在兴趣和好奇心的驱使下,打算也看一下这个库是怎么实现的。
2. 1 我们先来看一下,它跟其他库/方法的对比
- 与
fetch
、axios
的异同?我们下面只介绍这个库的部分功能。比如:取消请求、拦截器、中间件。
2. 2 取消请求
CancelToken
- 和axios同理,只不过在判断取消的时候逻辑不同,抽成了一个方法
// If request options contain 'cancelToken', reject request when token has been canceled
export function cancel2Throw(opt) {
// opt就是在配置的时候传入的参数
// 下面的和axios的写法一致
return new Promise((_, reject) => {
if (opt.cancelToken) {
opt.cancelToken.promise.then(cancel => {
reject(cancel);
});
}
});
}
- 在真正发起请求的地方
- 其实就是真正的请求、取消请求、超时(若有)一起raca,哪个最先执行完取哪个的值
export default function fetchMiddleware(ctx, next) {
let response;
// 超时处理、取消请求处理
if (timeout > 0) {
response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, ctx.req)]);
} else {
response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]);
}
}
AbortController
- 因为umi-request使用的是fetch请求的,所以取消需要借助其他的方法
- 比如我们取消一个
fetch
请求
- 比如我们取消一个
const controller = new AbortController();
const { signal } = controller;
fetch("http://xxx", { signal }).then(response => {
console.log(`Request 1 is complete!`);
}).catch(e => {
console.warn(`Fetch 1 error: ${e.message}`);
});
// Abort request
controller.abort();
- 但是这个
AbortController
也是umi-request
经过polyfill
的。
import 'abort-controller/polyfill';
let AbortController = undefined;
let AbortSignal = undefined;
const g = window; // 有删减
if (g) {
AbortController = typeof g.AbortController !== 'undefined' ? g.AbortController : AcAbortController;
AbortSignal = typeof g.AbortSignal !== 'undefined' ? g.AbortSignal : AcAbortSignal;
}
export default AbortController;
export { AbortController, AbortSignal };
2.3 拦截器
- 其实这个库里面同样有
- 请求拦截器
- 响应拦截器
- 例如
// 拦截器
// request interceptor, change url or options.
request.interceptors.request.use((url, options) => {
return {
url: `${url}&interceptors=yes`,
options: { ...options, interceptors: true },
};
});
// 响应器
// handling error in response interceptor
request.interceptors.response.use(response => {
const codeMaps = {
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};
message.error(codeMaps[response.status]);
return response;
});
- 在看拦截器的实现之前,我们先看一下这个库也支持为请求添加前缀和后缀,来看一下是怎么实现的。
- 其实添加前缀后缀也是一个请求拦截器
// 前后缀拦截
const addfix = (url, options = {}) => {
const { prefix, suffix } = options;
if (prefix) {
url = `${prefix}${url}`;
}
if (suffix) {
url = `${url}${suffix}`;
}
return {
url,
options,
};
};
export default addfix;
就是这么简单的字符串拼接,下面让我们进入正题吧。
- 拦截器的实现
- 挂载到实例上
// 拦截器
umiInstance.interceptors = {
request: {
use: Core.requestUse.bind(coreInstance),
},
response: {
use: Core.responseUse.bind(coreInstance),
},
};
- 具体收集封装的类
import addfixInterceptor from './interceptor/addfix';
// core.js
class Core {
constructor(initOptions) {
this.onion = new Onion([]);
this.instanceRequestInterceptors = [];
this.instanceResponseInterceptors = [];
}
// 旧版拦截器为共享 全局的拦截器
static requestInterceptors = [addfixInterceptor];
static responseInterceptors = [];
// 请求拦截器 默认 { global: true } 兼容旧版本拦截器
static requestUse(handler, opt = { global: true }) {
if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!');
if (opt.global) {
Core.requestInterceptors.push(handler);
} else {
this.instanceRequestInterceptors.push(handler);
}
}
// 响应拦截器 默认 { global: true } 兼容旧版本拦截器
static responseUse(handler, opt = { global: true }) {
if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!');
if (opt.global) {
Core.responseInterceptors.push(handler);
} else {
this.instanceResponseInterceptors.push(handler);
}
}
}
- 使用
- 在发起请求的时候,将响应拦截器包装,并传给请求拦截器当作参数。
- 请求拦截器的整个执行过程其实就是将url和options传给每一个拦截器,使用reduce循环执行。
class Core {
request(url, options) {
const { onion } = this;
const obj = {
req: { url, options },
res: null,
cache: this.mapCache,
responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors],
};
return new Promise((resolve, reject) => {
this.dealRequestInterceptors(obj)
.then(() => onion.execute(obj))
.then(() => {
resolve(obj.res);
})
});
});
}
- 执行请求前拦截器
- 可能有点绕,但仔细看还是很简单的,就是
Array.prototype.reducer
循环执行请求拦截器对url
和options
处理
- 可能有点绕,但仔细看还是很简单的,就是
dealRequestInterceptors(ctx) {
const reducer = (p1, p2) =>
p1.then((ret = {}) => {
ctx.req.url = ret.url || ctx.req.url;
ctx.req.options = ret.options || ctx.req.options;
return p2(ctx.req.url, ctx.req.options);
});
const allInterceptors = [...Core.requestInterceptors, ...this.instanceRequestInterceptors];
return allInterceptors.reduce(reducer, Promise.resolve()).then((ret = {}) => {
ctx.req.url = ret.url || ctx.req.url;
ctx.req.options = ret.options || ctx.req.options;
return Promise.resolve();
});
}
}
- 下面我们来看一下执行的方法
onion.execute
- Onion
- 其实看名字了解的人应该就知道是什么意思了,对。就是
洋葱模型
,因为这个库是支持中间件的。
- 其实看名字了解的人应该就知道是什么意思了,对。就是
- 我们先来介绍一下
洋葱模型
- Onion
类 koa 的洋葱机制,让开发者优雅地做请求前后的增强处理,支持创建实例、全局、内核中间件。
- 实例中间件(默认):
request.use(fn)
不同实例创建的中间件相互独立不影响; - 全局中间件 :
request.use(fn, { global: true })
全局中间件,不同实例共享全局中间件; - 内核中间件 :
request.use(fn, { core: true })
内核中间件,方便开发者拓展请求内核; - 优先级:全局中间件 > 内核中间件 其他的优先级和顺序请详看官方md
- 下面我们接具体看一下
Onion
中干了什么吧
class Onion {
// 有删减
execute(params = null) {
const fn = compose([
...this.middlewares, // 实例中间件
...this.defaultMiddlewares, // []
...Onion.globalMiddlewares, // 全局中间件
...Onion.coreMiddlewares, // 内核中间件
]);
// 其实从上面的数组中也可以看到执行顺序 全局中间件 > 内核中间件
return fn(params);
// fn({
// req: { url, options },
// res: null,
// cache: this.mapCache,
// responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors],
// })
}
}
// 初始化全局中间件
const globalMiddlewares = [simplePost, simpleGet, parseResponseMiddleware]; // 具体是什么后面在看
// 初始化内核中间件
const coreMiddlewares = [fetchMiddleware]; // 具体是什么后面在看
Onion.globalMiddlewares = globalMiddlewares;
Onion.defaultGlobalMiddlewaresLength = globalMiddlewares.length;
Onion.coreMiddlewares = coreMiddlewares;
Onion.defaultCoreMiddlewaresLength = coreMiddlewares.length;
- 我们看一下上面的中间件都是处理什么的
fetchMiddleware
真正发起请求的地方simplePost
对请求参数做处理,实现query
简化、post
简化simpleGet
同上parseResponseMiddleware
格式化和解析response
的
- 下面我们来看一下
compose
方法的实现,其实就是整个洋葱模型的执行过程
// 返回一个组合了所有插件的“插件” 洋葱模型的实现
export default function compose(middlewares) {
const middlewaresLen = middlewares.length;
// params = {
// req: { url, options },
// res: null,
// cache: this.mapCache,
// responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors],
// })
return function wrapMiddlewares(params, next) {
let index = -1;
function dispatch(i) {
if (i <= index) {
return Promise.reject(new Error('next() should not be called multiple times in one middleware!'));
}
index = i;
const fn = middlewares[i] || next;
if (!fn) return Promise.resolve();
try {
// () => dispatch(i + 1) 这个其实就是next方法
return Promise.resolve(fn(params, () => dispatch(i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0);
};
}
- 最后在获取到响应之后,执行响应拦截器
export default function fetchMiddleware(ctx, next) {
const { req: { options = {}, url = '' } = {}, cache, responseInterceptors } = ctx;
// 看这里,请求的方式
const adapter = fetch;
let response;
// 超时处理、取消请求处理,上面有讲过
if (timeout > 0) {
response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, ctx.req)]);
} else {
response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]);
}
// 兼容老版本 response.interceptor
responseInterceptors.forEach(handler => {
response = response.then(res => {
// Fix multiple clones not working, issue: https://github.com/github/fetch/issues/504
let clonedRes = typeof res.clone === 'function' ? res.clone() : res;
return handler(clonedRes, options);
});
});
}
2.4 整体大概流程
- request.get('/api/v1/xxx?id=1') 发起请求
- 调用Core的request方法
- 执行请求拦截器,传入响应拦截器等
const obj = {
req: { url, options },
res: null,
cache: this.mapCache,
responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors],
};
- 按顺序执行中间件
实例中间件(自定义)
->默认中间件(自定义,默认[])
->全局中间件
->内核中间件
- 将上面的obj当作参数分别传给各个中间件
[
...this.middlewares, // 实例
...this.defaultMiddlewares, // 默认
...Onion.globalMiddlewares, // 全局
...Onion.coreMiddlewares, // 内核
]
- 执行完自定义的之后,执行全局中间件(自定义的和
[simplePost, simpleGet, parseResponseMiddleware]
)- 其实这里有个小的点,中间件执行的顺序是
simplePost, simpleGet
。- 在simplePost里判断
- 其实这里有个小的点,中间件执行的顺序是
if (['post', 'put', 'patch', 'delete'].indexOf(method.toLowerCase()) === -1) {
return next();
}
- 如果不是post会直接走向simpleGet,当然最后也会走向simpleGet
- 在
simplePost
里主要判断有没有options.data
,有就执行格式化等逻辑 - 在
simpleGet
里主要判断有没有options.params
,有就执行格式化等逻辑,否则不做任何格式化,原样返回 parseResponseMiddleware
里面主要负责格式化response
,里面的逻辑其实是等到下个中间件执行完之后才处理,也就是第6步执行之后,对返回结果进行处理
- 在
return next()
.then(() => {
// ct会传给所有的中间件,也就是上面的obj
const { res = {}, req = {} } = ctx;
// todo
})
- 执行内核中间件
fetchMiddleware
,真正发起请求的地方 fetchMiddleware
里面发起请求并且判断是否cancel
和执行响应拦截器- 获取到值,并且返回给用户