在上两期,我们讲解了 Axios
的源码:
今天,我们将实现一个简易的 Axios
,用于在 Node
端实现网络请求,并支持一些基础配置,比如 baseURL、url、请求方法、拦截器、取消请求...
本次实现所有的源码都放在 这里,感兴趣的可以看看。
Axios 实例
本次我们将使用 typescript + node
来实现相关代码,这样对大家理解代码也会比较清晰。
这里,先来实现一个 Axios
类吧。
type AxiosConfig = {
url: string;
method: string;
baseURL: string;
headers: {[key: string]: string};
params: {};
data: {};
adapter: Function;
cancelToken?: number;
}
class Axios {
public defaults: AxiosConfig;
public createInstance!: Function;
constructor(config: AxiosConfig) {
this.defaults = config;
this.createInstance = (cfg: AxiosConfig) => {
return new Axios({ ...config, ...cfg });
};
}
}
const defaultAxios = new Axios(defaultConfig);
export default defaultAxios;
在上面,我们主要是实现了 Axios
类,使用 defaults
存储默认配置,同时声明了 createInstance
方法。该方法会创建一个新的 Axios
实例,并且会继承上一个 Axios
实例的配置。
请求方法
接下来,我们将对 https://mbd.baidu.com/newspage/api/getpcvoicelist
发起一个网络请求,将响应返回的数据输出在控制台。
我们发起请求的语法如下:
import axios from './Axios';
const service = axios.createInstance({
baseURL: 'https://mbd.baidu.com'
});
(async () => {
const reply = await service.get('/newspage/api/getpcvoicelist');
console.log(reply);
})();
request 方法
我们先来给我们的 Axios
类加上一个 request
和 get
方法吧。
import { dispatchRequest } from './request';
class Axios {
//...
public request(configOrUrl: AxiosConfig | string, config?: AxiosConfig) {
if (typeof configOrUrl === 'string') {
config!.url = configOrUrl;
} else {
config = configOrUrl;
}
const cfg = { ...this.defaults, ...config };
return dispatchRequest(cfg);
}
public get(configOrUrl: AxiosConfig | string, config?: AxiosConfig) {
return this.request(configOrUrl, {...(config || {} as any), method: 'get'});
}
}
这里 request
方法的实现和 axios
自带的方法差异性不大。
现在,我们来编辑发起真实请求的 dispatchRequest
方法吧。
export const dispatchRequest = (config: AxiosConfig) => {
const { adapter } = config;
return adapter(config);
};
和 axios
一样,调用了配置中的 adapter
来发起网络请求,而我们在 defaultConfig
中配置了默认的 adapter
。(如下)
const defaultConfig: AxiosConfig = {
url: '',
method: 'get',
baseURL: '',
headers: {},
params: {},
data: {},
adapter: getAdapter()
};
adapter 方法
接下来,我们重点来看看我们的 adapter
实现即可。
// 这里偷个懒,直接用一个 fetch 库
import fetch from 'isomorphic-fetch';
import { AxiosConfig } from './defaults';
// 检测是否为超链接
const getEffectiveUrl = (config: AxiosConfig) => /^https?/.test(config.url) ? config.url : config.baseURL + config.url;
// 获取 query 字符串
const getQueryStr = (config: AxiosConfig) => {
const { params } = config;
if (!Object.keys(params).length) return '';
let queryStr = '';
for (const key in params) {
queryStr += `&${key}=${(params as any)[key]}`;
}
return config.url.indexOf('?') > -1
? queryStr
: '?' + queryStr.slice(1);
};
const getAdapter = () => async (config: AxiosConfig) => {
const { method, headers, data } = config;
let url = getEffectiveUrl(config);
url += getQueryStr(config);
const response = await fetch(url, {
method,
// 非 GET 方法才发送 body
body: method !== 'get' ? JSON.stringify(data) : null,
headers
});
// 组装响应数据
const reply = {
data: await response.json(),
status: response.status,
statusText: response.statusText,
headers: response.headers,
config: config,
};
return reply;
};
export default getAdapter;
在这里,我们的实现相对来说比较简陋。简单来说就是几步
- 组装 url
- 发起请求
- 组装响应数据
看看效果
现在来控制台运行一下我们的代码,也就是下面这,看看控制台输出吧。
import axios from './Axios';
const service = axios.createInstance({
baseURL: 'https://mbd.baidu.com'
});
(async () => {
const reply = await service.get('/newspage/api/getpcvoicelist');
console.log(reply);
})();
从上图可以看出,我们的 axios
最基础的功能已经实现了(虽然偷了个懒用了 fetch
)。
接下来,我们来完善一下它的能力吧。
拦截器
现在,我想要让我的 axios
拥有添加拦截器的能力。
- 我将在请求处添加一个拦截器,在每次请求前加上一些自定义
headers
。 - 我将在响应处添加一个拦截器,直接取出响应的数据主体(
data
)和配置信息(config
),去除多余的信息。
代码实现如下:
// 添加请求拦截器
service.interceptors.request.use((config: AxiosConfig) => {
config.headers.test = 'A';
config.headers.check = 'B';
return config;
});
// 添加响应拦截器
service.interceptors.response.use((response: any) => ({ data: response.data, config: response.config }));
改造 Axios 类,添加 interceptors
我们先来创建一个 InterceptorManager
类,用于管理我们的拦截器。(如下)
class InterceptorManager {
private handlers: any[] = [];
// 注册拦截器
public use(handler: Function): number {
this.handlers.push(handler);
return this.handlers.length - 1;
}
// 移除拦截器
public eject(id: number) {
this.handlers[id] = null;
}
// 获取所有拦截器
public getAll() {
return this.handlers.filter(h => h);
}
}
export default InterceptorManager;
定义好了拦截器后,我们需要在 Axios
类中加上拦截器 —— interceptors
,如下:
class Axios {
public interceptors: {
request: InterceptorManager;
response: InterceptorManager;
}
constructor(config: AxiosConfig) {
// ...
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
}
接下来,我们在 request
方法中处理一下这些拦截器的调用吧。(如下)
public async request(configOrUrl: AxiosConfig | string, config?: AxiosConfig) {
if (typeof configOrUrl === 'string') {
config!.url = configOrUrl;
} else {
config = configOrUrl;
}
const cfg = { ...this.defaults, ...config };
// 将拦截器与真实请求合并在一个数组内
const requestInterceptors = this.interceptors.request.getAll();
const responseInterceptors = this.interceptors.response.getAll();
const handlers = [...requestInterceptors, dispatchRequest, ...responseInterceptors];
// 使用 Promise 将数组串联调用
let promise = Promise.resolve(cfg);
while (handlers.length) {
promise = promise.then(handlers.shift() as any);
}
return promise;
}
这里主要是将拦截器和真实的请求合并成一个数组,然后再使用 Promise
进行串联。
这里发现了一个自己还不知道的
Promise
知识点,在Promise.resolve
中,不需要显式返回一个Promise
对象,Promise
内部会将返回的值包装成一个Promise
对象,支持.then
语法调用。
现在,再运行一下我们的代码,看看加上拦截器后的运行效果吧。(如下图)
从上图可以看出,返回的内容中,只剩下了 data
和 config
字段(响应拦截器)。并且在 config
字段中也可以看到我们在 请求拦截器
中添加的自定义 headers
也起作用啦!
取消请求
最后,我们来实现 CancelToken
类,用于取消 axios
请求。
在实际应用中,我经常会使用 CancelToken
来自动检测重复请求(来源于频繁点击),然后取消掉更早的请求,仅使用最后一次请求作为有效请求。
所以,CancelToken
对我而言其实是个非常喜爱的功能,它本身的实现并不复杂,我们下面就开始来实现它吧。
我们先看看调用方式吧,下面我将在发起请求后 10ms 后(利用 setTimeout
),将请求取消。也就是说,只有 10ms 内完成的请求才能成功。
import axios, { CancelToken } from './Axios';
// ...
(async () => {
const source = CancelToken.source();
// 10ms 后,取消请求
setTimeout(() => {
source.cancel('Operation canceled by the user.');
}, 10);
const reply = await service.get('/newspage/api/getpcvoicelist', { cancelToken: source.token });
console.log(reply);
})();
我们先来理一理思路。
首先,我们使用了 CancelToken.source()
获取了一个 cancelToken
,并传给了对应的请求函数。
接下来,我们应该是使用这个 token
进行查询,查询该请求是否被取消,如果被取消则抛出错误,结束这次请求。
CancelToken
ok,思路已经清晰了,接下来就开始实现吧,先从 CancelToken
开始吧。
class CancelError extends Error {
constructor(...options: any) {
super(...options);
this.name = 'CancelError';
}
}
class CancelToken {
private static list: any[] = [];
// 每次返回一个 CancelToken 实例,用于取消请求
public static source(): CancelToken {
const cancelToken = new CancelToken();
CancelToken.list.push(cancelToken);
return cancelToken;
}
// 通过检测是否有 message 字段来确定该请求是否被取消
public static checkIsCancel(token: number | null) {
if (typeof token !== 'number') return false;
const cancelToken: CancelToken = CancelToken.list[token];
if (!cancelToken.message) return false;
// 抛出 CancelError 类型,在后续请求中处理该类型错误
throw new CancelError(cancelToken.message);
}
public token = 0;
private message: string = '';
constructor() {
// 使用列表长度作为 token id
this.token = CancelToken.list.length;
}
// 取消请求,写入 message
public cancel(message: string) {
this.message = message;
}
}
export default CancelToken;
CancelToken
基本上完成了,它的主要功能就是使用一个 CancelToken
实例对应一个需要做处理的请求,然后在已取消的请求中抛出了一个 CancelError
类型的抛错。
处理 CancelError
接下来,我们需要在对应的请求处(dispatchRequest
)添加取消请求检测,最后再加上一个对应的响应拦截器处理对应错误即可。
export const dispatchRequest = (config: AxiosConfig) => {
// 在发起请求前,检测是否取消请求
CancelToken.checkIsCancel(config.cancelToken ?? null);
const { adapter } = config;
return adapter(config).then((response: any) => {
// 在请求成功响应后,检测是否取消请求
CancelToken.checkIsCancel(config.cancelToken ?? null);
return response;
});
};
由于我们的拦截器实现的太粗糙,并没有添加失败响应拦截器(本应在这里处理),所以我这里直接将整个请求包裹在 try ... catch
中处理。
try {
const reply = await service.get('/newspage/api/getpcvoicelist', { cancelToken: source.token });
console.log(reply);
} catch(e) {
if (e.name === 'CancelError') {
// 如果请求被取消,则不抛出错误,只在控制台输出提示
console.log(`请求被取消了, 取消原因: ${e.message}`);
return;
}
throw e;
}
接下来,我们运行我们的程序,看看控制台输出吧!(如下图)
大功告成!
小结
到这里,我们的简易版 axios
就已经完成啦。
它可以用于在 Node
端实现网络请求,并支持一些基础配置,比如 baseURL、url、请求方法、拦截器、取消请求...
但是,还是有很多不够完善的地方,感兴趣的小伙伴可以找到下面的源码地址,继续往下续写。
最后一件事
如果您已经看到这里了,希望您还是点个赞再走吧~
您的点赞是对作者的最大鼓励,也可以让更多人看到本篇文章!
如果觉得本文对您有帮助,请帮忙在 github 上点亮 star
鼓励一下吧!