Skip to main content

Axios 源码解析

本文在参考 Axios(v1.3.3) 源码的基础上实现一个简易版的请求库(仅用于学习),将包含以下功能

  • 基本请求
  • 适配器
  • 拦截器
  • 请求中断
  • 超时中断

Demo 地址

前置知识

适配器

通过适配器来使 Axios 在 Browser/Node 平台使用不同的 API 来发送请求,Axios Browser 使用的是 ajax,Node 端用的是 http,本文在 Browser 将用 fetch 来替代,Node 端暂时不实现。下面代码中的 adapter 其实最后返回的就是 fetch 函数了。

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);
}

dispatchRequest

该部分主要用于发送请求并将 fetch 接口进行 json() 处理

dispatchRequest.js
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])

Axios.js
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.tokenmetadata,在请求返回体上加上了 extraDataduration(记录请求耗时) 以及处理 404 状态码。

具体实现

InterceptorManager 内部维护着一个数组,对拦截器进行增加(use)和删除(eject)操作

InterceptorManager.js
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.requestthis.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);
};