Axios 源码解析
本文在参考 Axios
(v1.3.3) 源码的基础上实现一个简易版的请求库(仅用于学习),将包含以下功能
- 基本请求
- 适配器
- 拦截器
- 请求中断
- 超时中断
前置知识
适配器
通过适配器来使 Axios
在 Browser/Node 平台使用不同的 API 来发送请求,Axios
Browser 使用的是 ajax
,Node 端用的是 http
,本文在 Browser 将用 fetch
来替代,Node 端暂时不实现。下面代码中的 adapter
其实最后返回的就是 fetch
函数了。
- adapters/index.js
- adapters/xhr.js
- adapters/http.js
import xhr from './xhr';
import http from './http';
const knownAdapters = {
http,
xhr
};
export default async function adapters(config) {
let adapter;
for (const key in knownAdapters) {
if (knownAdapters[key]) {
adapter = knownAdapters[key];
}
}
return adapter(config);
}
const isXHRAdapterSupported = typeof fetch !== 'undefined';
export default isXHRAdapterSupported &&
async function xhrAdapter(config) {
return fetch(config.url, config);
};
const isHttpAdapterSupported = typeof process !== 'undefined';
export default isHttpAdapterSupported &&
function httpAdapter(config) {
// TODO: 暂未实现
return config;
};
dispatchRequest
该部分主要用于发送请求并将 fetch 接口进行 json()
处理
import adapters from './adapters/index.js';
export default function dispatchRequest(config) {
const adapter = adapters(config);
return adapter.then(
async function onAdapterResolution(response) {
try {
const res = await response.json();
return { ...res, config };
} catch (error) {
return Promise.reject(response);
}
},
function onAdapterRejection(reason) {
return Promise.reject(reason);
}
);
}
基本请求
我们先实现一个简单的 axios.get(url, [config])
import adapters from './adapters';
export default class Axios {
constructor(config) {
this.defaultConfig = config;
}
request(config) {
// 相当于 let promise = fetch(config.url, { ...this.defaultConfig, ...config });
let promise = dispatchRequest({ ...this.defaultConfig, ...config });
return promise;
}
get(url, config) {
const mergedConfig = { ...config, url };
return this.request(mergedConfig);
}
}
到这一步我们基本就实现了一个非常简易的请求库了,无非就是将fetch
封装成axios
api 的样子
拦截器
拦截器是我们平常用的比较多的一个功能,
使用拦截器我们可以实现以下功能
- request 拦截器:修改 header 如添加 token 之类的,处理接口缓存等等...
- response 拦截器:处理 code 码(301 重定向,404 转到 notFound 页面),针对失败请求收集错误日志,mock 数据,统一处理数据返回格式,记录请求耗时等等...
示例
在实现之前先看下示例代码明确要实现的功能以及 api 长什么样
const axios = new Axios();
axios.interceptors.request.use(
function requestInterceptorFulfilled(config) {
config.headers = {
token: 'add by request interceptors'
};
config.metadata = { startTime: Date.now() };
return config;
},
function requestInterceptorRejected(error) {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
function responseInterceptorFulfilled(response) {
response.extraData = 'add by response interceptors';
// 接口请求耗时
response.duration = Date.now() - response.config.metadata.startTime;
return response;
},
function responseInterceptorRejected(error) {
if (error.status === 404) {
alert('返回状态码为404,重定向到404页面');
}
return Promise.reject(error);
}
);
上面的实例中我们在请求的参数上加上header.token
和 metadata
,在请求返回体上加上了 extraData
和 duration
(记录请求耗时) 以及处理 404 状态码。
具体实现
InterceptorManager
内部维护着一个数组,对拦截器进行增加(use
)和删除(eject
)操作
export default class InterceptorManager {
constructor() {
this.handlers = [];
}
use(fulfilled, rejected) {
this.handlers.push({
fulfilled,
rejected
});
// 返回当前的 handler 在数组中的 index,用于eject
return this.handlers.length - 1;
}
eject(index) {
if (this.handlers[index]) {
this.handlers[index] = null;
}
}
}
为Axios
this.interceptors.request
和 this.interceptors.response
各自添加InterceptorManager
对象
constructor(config) {
this.defaultConfig = config;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
接下来是最核心的 request
方法编写
request(config) {
let promise;
let i = 0;
const requestInterceptorChain = [];
const responseInterceptorChain = [];
this.interceptors.request.handlers.forEach(({ fulfilled, rejected }) => {
requestInterceptorChain.unshift(fulfilled, rejected);
});
this.interceptors.response.handlers.forEach(({ fulfilled, rejected }) => {
responseInterceptorChain.push(fulfilled, rejected);
});
// chain为成对出现的fulfilled, rejected
const chain = [
...requestInterceptorChain,
dispatchRequest,
undefined,
...responseInterceptorChain
];
let len = chain.length;
// 将config转化成promise对象以方便后面组成 Promise 链
promise = Promise.resolve({...this.defaultConfig, ...config});
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
return promise;
}
这里有一个特别的地方就是,在加入 chain 的操作中,request 是使用 unshift
为 response 是使用 push
,也就是说先加入的 request 后调用,而先加入的 response 先调用
针对前面的示例最终会转化成以下核心代码
// undefined 用来占位的,和 dispatchRequest 组成一对 fulfilled, rejected
const chain = [
requestInterceptorFulfilled,
requestInterceptorRejected,
dispatchRequest,
undefined,
responseInterceptorFulfilled,
responseInterceptorRejected
];
let len = chain.length;
// 将config转化成promise对象以组成 Promise 链
promise = Promise.resolve({ ...this.defaultConfig, ...config });
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
总结
当我们运行 axios.get('https://run.mocky.io/v3/0a4e2970-39b4-4bb5-9a12-06e47408e2a3')
时,Axios 内部其实做了下面这些事情
// 将 config 转化成 promise 对象以方便后面组成 Promise 链
Promise.resolve({
url: 'https://run.mocky.io/v3/0a4e2970-39b4-4bb5-9a12-06e47408e2a3'
})
.then(
// request 拦截器
config => {
config.headers = {
token: 'add by request interceptors'
};
config.metadata = { startTime: Date.now() };
return config;
},
error => Promise.reject(error)
)
.then(config => {
console.log('request 拦截器处理后的参数:', config);
// 请求函数 dispatchRequest
return fetch(config.url, config).then(async response => {
const json = await response.json();
return { ...json, config };
});
// undefined 占位
}, undefined)
.then(
// response 拦截器
response => {
console.log('fetch 返回的数据 + config:', JSON.stringify(response));
response.extraData = 'add by response interceptors';
// 接口请求耗时
response.duration = Date.now() - response.config.metadata.startTime;
return response;
},
error => {
if (error.status === 404) {
alert('返回状态码为404,重定向到404页面');
}
return Promise.reject(error);
}
)
.then(res => console.log('最终获得的数据:', res));
可以将上方代码复制到控制台查看输出结果
请求中断
请求中断不需要我们做其他操作,只需要在使用时传入 signal
参数即可
const abortController = new AbortController();
const signal = abortController.signal;
axios.get(`https://run.mocky.io/v3/0a4e2970-39b4-4bb5-9a12-06e47408e2a3?mocky-delay=10000ms`, {
signal
});
// 中断请求
abortController.abort();
超时中断
在请求时添加一个计时器来调用 abort()
即可
const isXHRAdapterSupported = typeof fetch !== 'undefined';
export default isXHRAdapterSupported &&
async function xhrAdapter(config) {
if (config.timeout) {
const controller = new AbortController();
const abortId = setTimeout(() => {
controller.abort();
}, config.timeout);
const res = await fetch(config.url, { ...config, signal: controller.signal });
clearTimeout(abortId);
return res;
}
return fetch(config.url, config);
};