在上两期,咱们解说了 Axios
的源码:
- Axios 源码解读 —— request 篇
- 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
激励一下吧!