1、前言
一个好的程序员一般都具备一个良好的习惯,如果觉得这个东西好吃,就一定会想办法去拆解它,从而具备自行生产的能力。往往在这个过程中,会见识到很多新奇的东西,然后再不禁感慨道:“wocao,还可以这样啊,666”,然后在将来某个项目里面运用从这儿学到的奇技淫巧,当被同事看到的时候,别人直呼“666”,然后心中充满成就感。哈哈哈,画面感十足啊。
读完此文:
1、你将对axios的运行原理有清晰的认识
2、你将学会如何使用策略模式和模板方法模式;
3、你将学会如何使用发布订阅模式;
4、你将对Promise.prototype.then()方法有更深刻的认识;
如果亲爱的读者读完此文还搞不懂,别急,你目前的功力尚欠,可以先点赞收藏,等将来你打怪升级以后再回顾本文,你就能理解了,哈哈哈。
本文基于axios@0.21.1的版本进行阐述,解读axios中应用的一些优秀的设计模式和编程技巧。
首先安装axios到我们的项目目录中:
$ npm install axios@0.21.1 -S
复制代码
安装成功后,我们通过VSCode打开axios的源码开始分析。
axios的目录如图(本文不会逐文件阐述,将会挑重点文件进行阐述):
dist目录是axios使用构建工具输出的UMD形式的文件,一般我们只会在非打包环境下引入,入口文件是index.js,这个文件引入了lib文件目录下的axios.js文件,因此我们的目光将主要聚焦在lib目录并开始分析。
2、核心模块
2.1、axios.js
在这个文件中,引入了axios的核心构造函数Axios,引入了axios的一些默认配置
'use strict';
// 已省略非关键代码
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
// 默认配置将会在后面章节介绍
var defaults = require('./defaults');
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);
// Copy context to instance
utils.extend(instance, context);
return instance;
}
// 创建Axios的实例,这就是我们能够直接使用axios进行请求的原因。
var axios = createInstance(defaults);
// 暴露Axios的构造函数,可以供外界继承
axios.Axios = Axios;
// 暴露Axios的createAPI
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// 暴露CancelToken,是用来执行请求可取消的接口
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
module.exports = axios;
// 对ES6模块的支持
module.exports.default = axios;
复制代码
2.2、Axios的构造函数
axios的构造函数位于lib/core/Axios.js中,我们顺着axios.js进入到这个文件中。
'use strict';
// 已省略非关键代码
var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');
/**
* Create a new instance of Axios
*/
function Axios(instanceConfig) {
// 定义默认配置
this.defaults = instanceConfig;
// 定义自身内部的请求和响应拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
/**
* Axios的核心请求方法
*/
Axios.prototype.request = function request(config) {
/*
* 处理参数,使得axios即可以支持fetch API的调用形式,又可以支持axios({...params})的调用形式
*/
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
// 定义请求方式,如果没有设置则默认GET
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// 管道处理中间件,这儿也是我觉得比较经典的一处处理,我们即将阐述dispatchRequest
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 将request拦截器全部插入在管道的前端
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 将response拦截器全部插入在管道的后端
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 应用管道中的所有拦截器
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
// 定义原型上的请求方法别名,使得其支持axios.get等类似的请求方式
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
}));
};
});
module.exports = Axios;
复制代码
在这个文件中,我们需要关注几个点,首先是为什么axios可以支持axios({…params})或axios(‘url’, {…params})这样的写法;其次是为什么axios可以支持axios.get(path, config)这样的写法;然后是axios是怎么样处理我们所配置的请求或响应拦截器的(这个点是这个文件的重中之重)。
首先,我们来回忆一下,我们写拦截器的场景
import store from '@/store'
import axios from 'axios';
axios.interceptors.request.use(
config => {
const token = store.state.token;
token && (config.headers.Authorization = token);
return config;
},
error => {
return Promise.error(error);
})
复制代码
然后我们再回到阮一峰老师的ES6入门Promise一节中对Promise.prototype.then()的阐述
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
首先我们先看这个chain里面初始化的时候定义了[dispatchRequest,undefined],先来到core/dispatchRequest.js中,看看这个dispatchRequest是个什么东西。
'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
/**
* Throws a `Cancel` if cancellation has been requested.
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
/**
* Dispatch a request to the server using the configured adapter.
*/
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// Ensure headers exist
config.headers = config.headers || {};
// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// Flatten headers
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers
);
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
// 真正干活儿的方法
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData(
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(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
};
复制代码
dispatchRequest就是axios核心的内置的请求中间件,并且部署了内置的请求(正常我们的请求拦截器一般都做一些配置操作,因此我将它对请求的配置也视为一个请求拦截器)和响应拦截器而已,并在拦截器里面引入了throwIfCancellationRequested,这是我们的请求可以取消的基石,稍后我们在可取消Promise节将会详细介绍。
将request拦截器插入到这个内置中间件之前,然后将response拦截器插入到这个内置中间件之后执行。根据ES6 Promise.prototype.then()的定义,我们相当于部署了N个中间件,但并不立即触发,将会在上一个Promise的状态发生改变的时候触发,这儿的流程有点儿像多米诺骨牌,首先拿着被包裹的配置内容经过请求拦截器,然后部署dispatchRequest,然后再部署响应拦截器。当我们的dispatchRequest状态改变的时候,挨着走响应拦截器,最终经过一些列处理,成为用户最终拿到的response结果。
我相信,此时已经有同学已经高呼“666”了,哈哈哈,的确是这样啊,因为当我第一次看到这儿的时候也发出了感慨,真是厉害啊,原来我们对Promise的理解一点儿都不深刻,害。
3、默认配置
前两节阐述了那么多,我们基本上阐述了axios的主线处理流程,但是axios可以在极简的配置下正常运行。在这节,我们来瞅瞅axios为我们做了哪些默认配置。来到defaults.js中,
'use strict';
var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');
var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded'
};
function setContentTypeIfUnset(headers, value) {
if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
headers['Content-Type'] = value;
}
}
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;
}
var defaults = {
adapter: getDefaultAdapter(),
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
// 支持了所有的文件流,浏览器只支持FormData和Blob
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data)) {
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}],
// 使得前台的调用者可以直接拿到一个JSON对象
transformResponse: [function transformResponse(data) {
/*eslint no-param-reassign:0*/
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}],
/**
* A timeout in milliseconds to abort a request. If set to 0 (default) a
* timeout is not created.
*/
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};
defaults.headers = {
common: {
'Accept': 'application/json, text/plain, */*'
}
};
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
defaults.headers[method] = {};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
module.exports = defaults;
复制代码
在这儿,有一个关键的地方是是对于adaptor的获取,如果当前环境存在XMLHttpRequest,则说明是浏览器环境,如果存在process对象,则说明是nodejs环境,这儿是一个策略模式的具体应用。
我们在实际使用策略模式的时候,往往会和模板方法模式(符合里氏代换原则、开闭原则)一同使用,通常我们会在父类里面定义一系列的行为规范,然后在子类中分别基于各自的策略实现行为,因此,如果拿这个例子举例,可以写出如下伪代码:
//base-request.ts
export abstract class BaseRequest{
// 具备共同抽象特征的方法
abstract void abort();
abstract void send();
// ...省略其它方法
}
复制代码
//http-request.ts
export class HttpRequest extends BaseRequest{
// 根据自身特征实现抽象方法
void abort() {
console.log('http策略实现取消请求')
}
void send(){
console.log('http策略实现发送请求')
}
// ...省略其它方法
}
复制代码
//xhr-request.ts
export class XhrRequest extends BaseRequest{
// 根据自身特征实现抽象方法
void abort() {
console.log('xhr策略实现取消请求')
}
void send(){
console.log('xhr策略实现发送请求')
}
// ...省略其它方法
}
复制代码
因此,我们在实际的编码过程中,应当注意对业务的共同特征进行抽象,这样更有助于我们编写出高内聚,低耦合的代码。
4、拦截器管理
axios对拦截器的管理位于libs/core/InterceptorManager.js中,在这儿其实是一个发布订阅模式的应用
'use strict';
var utils = require('./../utils');
function InterceptorManager() {
// 持有订阅者
this.handlers = [];
}
/**
* 订阅消息
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
/**
* 取消订阅
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
/**
* 通知订阅者
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
module.exports = InterceptorManager;
复制代码
axios并没有设计基于channel的发布订阅模式,如果我们要实现基于channel的发布订阅模式的话,可以基于上述代码进行改造:
function Manager() {
// 持有订阅者
this.handlers = {};
}
/**
* 订阅消息 支持单次订阅
*/
Manager.prototype.subscribe = function(channel, handler, once =false) {
if(!this.handlers[channel]) {
this.handlers[channel] = [{
handler,
once
}];
} else {
this.handlers[channel].push({
handler,
once,
});
}
};
/**
* 取消订阅
*/
Manager.prototype.unsubscribe = function(channel) {
if (this.handlers[channel]) {
this.handlers[channel] = null;
}
};
/**
* 通知特征订阅者
*/
Manager.prototype.notify = function(channel, ...args) {
var subscribers = this.handlers[channel];
subscribers.forEach((ele) => {
const { handler, once } = ele || {};
typeof handler === 'function' && handler.apply(this, args);
if(once) {
ele.destroy = true;
}
})
this.handlers[channel] = subscribers.filter(x => !x.destroy);
};
/**
* 通知所有订阅者
*/
Manager.prototype.notifyAll = function(...args) {
for(var channel in this.handlers) {
if(this.handlers.hasOwnProperty(channel)) {
var subscribers = this.handlers[channel];
if(!Array.isArray(subscribers)) {
continue;
}
subscribers.forEach(ele => {
const { handler, once } = ele || {};
typeof handler === 'function' && handler.apply(this, args);
if(once) {
ele.destroy = true;
}
})
this.handlers[channel] = subscribers.filter(x => !x.destroy)
}
}
};
复制代码
5、可取消的Promise
这个特性是最吸引我的,也是促使我阅读axios源码的动力。当某个接口的请求时间过长,我不想请求了,我想取消,如果基于XMLHttpRequest编程,大家一定想的到调用XMLHttpRequest.prototyp.abort()方法,并且可以在XMLHttpRequest.prototyp.onabort()回调里面做一些处理,但是众所周知,当前的Promise是不可以取消的(一旦new出来就不能取消),因此,我们接下来看看axios是如何实现“可取消的Promise”的。
首先是adaptors下面的xhr实现类,关键代码如下:
// 已省略部分非关键代码
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
var request = new XMLHttpRequest();
var fullPath = buildFullPath(config.baseURL, config.url);
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Listen for ready state
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// 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() {
reject(createError('Network Error', config, null, request));
// Clean up request
request = null;
};
// 如果用户配置了CancelToken,在此注册异步取消操作,当用户触发取消以后,调用xhr的abort方法取消xhr,并做一些清理工作
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
// Send the request
request.send(requestData);
});
};
复制代码
找到定义可取消行为的关键代码,这部分代码位于cancel目录下,其中Cancel.js和isCancel比较简单,大家也一看就明白,此处不讨论。我们主要来讨论一下CancelToken.js里面的内容,我们先将目光集中在这个部分:
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
复制代码
我们回忆一下axios的CancelToken的使用方式:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// cancel the request
cancel('我不想搞了');
复制代码
可以看到,我们外部调用的cancel方法其实就是在执行上述的resolvePromise()方法,而resolvePromise指向的是这个内部promise的resolve方法;
而我们在xhr里面设置的异步回调执行监听的这个promise状态的改变立即调用xhr的取消方法,至此,我们搞懂了为什么axios的“Promise能取消”。
其实axios实现的“可取消Promise”并没有真正的可取消,在xhr.js中我们可以看到,如果用户执行了取消操作的话,只是让Promise走的reject态而已。
现在再回到最开始我们在核心模块说过的一个方法叫throwIfCancellationRequested,axios在自己的默认拦截器中调用了它,它就是用来确保用户如果在请求之前就取消的话,直接走reject态。
最后再看看一看我们刚才忽略的一段代码:
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
复制代码
axios的作者可能考虑到之前的写法比较复杂,于是为我们实现了一个默认的供懒人使用吧,哈哈哈。
6、总结
本文仅仅挑选了一些比较核心的模块进行了分析,这只是axios的冰山一角,有兴趣的朋友可以进行跟详细的阅读(尤其是可探究一下axios如何发送http请求的),相信您一定可以学到很多意想不到的知识呢。
从axios的API设计中,我们可以看出作者为了简化我们的调用付出了相当的努力,各位读者平时在设计API的时候务必要考虑调用者的使用体验,我在开发中一直追求的信念是“恶心自己(在设计API的时候支持更简单的调用方式,做更多的默认设置),成全别人(尽量减少信息的传递,降低使用者阅读API的时间消耗,让使用者可以傻瓜式的调用接口)”,哈哈哈。
读完此文,你对axios的整个工作流程应该已经有了清晰的认识吧;你对Promise.prototype.then()一定又有了新的认识吧;你对发布订阅模式认识也又更加深刻了吧;你对策略模式和模板方法模式的使用认识也更深刻了吧;愿每个努力的人都不会被辜负。
由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,请联系作者本人,邮箱404189928@qq.com,你们的意见将会帮助我更好的进步。本文乃作者原创,若转载请联系作者本人。