持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情
1.axios 设计之美
axios 是一个被前端广泛使用的请求库,对应上述分层结构中,属于框架/类库层,我们来总结一下它的功能特点: 在浏览器端,使用 XMLHttpRequest 发送请求;
- 支持 Node.js 端发送请求;
- 支持 Promise API,使用 Promise 风格语法;
- 支持请求和响应拦截;
- 支持自定义修改请求和返回内容;
- 支持请求取消;
- 默认支持 XSRF 防御。
下面,我们主要从拦截器思想、适配器思想、安全思想三方面展开,分析 axios 设计的可取之处。
2.拦截器思想
拦截器思想是 axios 带来的最具启发性的思想之一。它赋予了分层开发时借助拦截行为,注入自定义能力的功能。简单来说,axios 的拦截器主要由:任务注册 → 任务编排 → 任务调度(执行)三步组成。
我们先看任务注册,在请求发出前,可以使用axios.interceptors.request.use方法注入拦截逻辑,比如:
axios.interceptors.request.use(function (config) {
// 请求发送前做一些事情,比如添加 headers
return config;
}, function (error) {
// 请求出现错误时,处理逻辑
return Promise.reject(error);
});
在请求返回后,用axios.interceptors.response.use方法注入拦截逻辑,比如:
axios.interceptors.response.use(function (response) {
// 响应返回 2xx 时,做一些操作,比如响应状态码为 401 时,自动跳转到登录页
return response;
}, function (error) {
// 响应返回 2xx 外响应码时,错误处理逻辑
return Promise.reject(error);
});
任务注册部分的源码实现也不复杂:
// lib/core/Axios.js
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// lib/core/InterceptorManager.js
function InterceptorManager() {
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
// 返回当前的索引,用于移除已注册的拦截器
return this.handlers.length - 1;
};
如上代码,我们定义的请求/响应拦截器,会在每一个 axios 实例的 Interceptors 属性中维护,this.interceptors.request和this.interceptors.response也都是一个 InterceptorManager 实例,该实例的handlers属性以数组的形式存储了使用方定义的一个个拦截器逻辑。
注册了任务,我们再来看看任务编排时是如何将拦截器串联起来,并在任务调度阶段执行各个拦截器的。如下源码:
// lib/core/Axios.js
Axios.prototype.request = function request(config) {
config = mergeConfig(this.defaults, config);
// ...
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
// 任务编排
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 任务调度
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
我们通过chain数组来编排调度任务,dispatchRequest方法实际执行请求的发送,编排过程实现:在实际发送请求的方法dispatchRequest前插入请求拦截器,在dispatchRequest方法后,插入响应拦截器。
任务调度其实就是通过一个 While 循环,通过一个 Promise 实例,遍历迭代chain数组方法,并基于 Promise 回调特性,将各个拦截器串联执行起来。
3. 适配器思想
axios 同时支持 Node.js 环境和浏览器环境发送请求,在浏览器中我们可以选用 XMLHttpRequest 或 Fetch 方法发送请求,但是在 Node.js 中,需要通过 HTTP 模块发送请求。对此,axiso 是如何设计实现的呢?
为了支持适配不同环境,axios 实现了适配器:Adapter,具体实现在dispatchRequest方法中:
// lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
// ...
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// ...
return response;
}, function onAdapterRejection(reason) {
// ...
return Promise.reject(reason);
});
};
如上代码,axios 支持使用方实现自己的 Adapter,自定义不同环境中的请求实现方式,也提供了默认的 Adapter。默认 Adapter 逻辑代码如下:
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 浏览器端使用 XMLHttpRequest 方法
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' &&
Object.prototype.toString.call(process) === '[object process]') {
// Node.js 端,使用 HTTP 模块
adapter = require('./adapters/http');
}
return adapter;
}
一个 Adapter 需要返回一个 Promise 实例(这是因为axios 内部通过 Promise 链式调用完成请求调度),我们分别看看在浏览器端和 Node.js 端具体 Adapter 实现逻辑:
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() {
// ....
};
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
// ...
};
// Handle low level network errors
request.onerror = function handleError() {
// ...
};
// Handle timeout
request.ontimeout = function handleTimeout() {
// ...
};
// ...
request.send(requestData);
});
};
如上代码,就是一个典型的使用 XMLHttpRequest 发送请求的实现内容。在 Node.js 端的实现,精简后代码如下:
var http = require('http');
/*eslint consistent-return:0*/
module.exports = function httpAdapter(config) {
return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
var resolve = function resolve(value) {
resolvePromise(value);
};
var reject = function reject(value) {
rejectPromise(value);
};
var data = config.data;
var headers = config.headers;
var options = {
// ...
};
var transport = http;
var req = http.request(options, function handleResponse(res) {
// ...
});
// Handle errors
req.on('error', function handleRequestError(err) {
// ...
});
// Send the request
if (utils.isStream(data)) {
data.on('error', function handleStreamError(err) {
reject(enhanceError(err, config, null, req));
}).pipe(req);
} else {
req.end(data);
}
});
};
上述代码主要是调用 Node.js HTTP 模块,进行请求的发送和处理,当然,真实源码实现还需要考虑 HTTPS 以及 Redirect 等问题,这里我们不再展开。
讲到这里,可能你会问,什么场景下,才会需要自定义 Adapter 进行请求发送呢?比如在测试阶段或特殊环境中,我们可以 mock 请求:
if (isEnv === 'ui-test') {
adapter = require('axios-mock-adapter')
}
实现一个自定义的 Adapter 也并不困难,说到底它也只是一个 Node.js 模块,导出一个 Promise 实例即可:
module.exports = function myAdapter(config) {
// ...
return new Promise(function(resolve, reject) {
// ...
sendRequest(resolve, reject, response);
// ....
});
}
相信你学会了这些内容,就对 axios-mock-adapter 这个库的实现原理了然于胸了。
4. 安全思想
说到请求,自然关联着安全问题。在本小节最后部分,我们对 axios 中的一些安全机制进行解析,涉及相关攻击手段:CSRF。
Cross—Site Request Forgery,攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说,这个请求是完全合法的,但是却完成了攻击者期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至购买商品、虚拟货币转账等。
在 axios 中,主要依赖双重 cookie 的方式防御 CSRF。具体来说,对于攻击者,获取用户 cookie 是比较困难的,因此,我们可以在请求中携带一个 cookie 值,来保证请求的安全性。这里我们将相关流程梳理为:
- 用户访问页面,后端向请求域中注入一个 cookie,一般该 cookie 值为加密随机字符串;
- 在前端通过 Ajax 请求数据时,取出上述 cookie,添加到 URL 参数或者请求 header 中;
- 后端接口验证请求中携带的 cookie 值是否合法,不合法(不一致),则拒绝请求。
我们看 axios 源码:
// lib/defaults.js
var defaults = {
adapter: getDefaultAdapter(),
// ...
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
};
在这里,axios 默认配置了默认xsrfCookieName和xsrfHeaderName,实际开发中可以按具体情况传入配置。在具体请求时,以lib/adapters/xhr.js为例:
// 添加 xsrf header
if (utils.isStandardBrowserEnv()) {
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
由此可见,对一个成熟请求库的设计来说,安全防范这个话题永不过时。