axios的原理
vue自2.0开始,vue-resource不再作为官方推荐的ajax方案,转而推荐使用axios。
按照作者的原话来说:
“Ajax 本身跟 Vue 并没有什么需要特别整合的地方,使用 fetch polyfill 或是 axios、superagent 等等都可以起到同等的效果,vue-resource 提供的价值和其维护成本相比并不划算,所以决定在不久以后取消对 vue-resource 的官方推荐。已有的用户可以继续使用,但以后不再把 vue-resource 作为官方的 ajax 方案。”
除了维护成本方面的原因,axios本身的优点也使得它在一众ajax异步请求的框架中脱颖而出,下面我们通过分析部分axios的源码来看看,是什么让axios成为大多数人的选择。
GitHub上axios的主页标注了它具有如下特性:
- 从浏览器中创建 XMLHttpRequest
- 从 node.js 发出 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防止 CSRF/XSRF
1.同时支持浏览器端和服务端的请求。
由于axios的这一特性,vue的服务端渲染对于axios简直毫无抵抗力。 让我们一起来读读源码,看看它是如何实现的。
在axios/lib/core/dispatchRequest.js文件中暴露的dispatchRequest方法就是axios发送请求的方法,其中有一段代码为:
//定义适配器,判断是在服务器环境还是浏览器环境 var adapter = config.adapter || defaults.adapter; return adapter(config).then(function onAdapterResolution(response) { throwIfCancellationRequested(config); // 处理返回的数据 response.data = transformData( response.data, response.headers, config.transformResponse ); return response; }, function onAdapterRejection(reason) { if (!isCancel(reason)) { throwIfCancellationRequested(config); // 处理失败原因 if (reason && reason.response) { reason.response.data = transformData( reason.response.data, reason.response.headers, config.transformResponse ); } } return Promise.reject(reason); }); };
这段代码首先定义了一个适配器,然后返回了适配器处理后的内容。 如果没有在传入的配置参数中指定适配器,则取默认配置文件中定义的适配器,再让我们来看看默认文件/lib/defaults.js定义的适配器:
function getDefaultAdapter() { var adapter; if (typeof XMLHttpRequest !== ‘undefined‘) { //通过判断XMLHttpRequest是否存在,来判断是否是浏览器环境 adapter = require(‘./adapters/xhr‘); } else if (typeof process !== ‘undefined‘) { //通过判断process是否存在,来判断是否是node环境 adapter = require(‘./adapters/http‘); } return adapter; }
到这里真相大白,XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能;process 对象是一个 global (全局变量),提供有关信息,控制当前 Node.js 进程。原来作者是通过判断XMLHttpRequest和process这两个全局变量来判断程序的运行环境的,从而在不同的环境提供不同的http请求模块,实现客户端和服务端程序的兼容。
同理,我们在做ssr服务端渲染时,也可以使用这个方法来判断代码当前的执行环境。
2、支持promise
/** * 处理一个请求 * * @param config 请求的配置 */ Axios.prototype.request = function request(config) { // 如果是字符串,则直接赋值给配置的url属性 if (typeof config === ‘string‘) { config = utils.merge({ url: arguments[0] }, arguments[1]); } // 合并默认配置和配置参数 config = utils.merge(defaults, this.defaults, { method: ‘get‘ }, config); config.method = config.method.toLowerCase(); // 连接拦截器中间件 var chain = [dispatchRequest, undefined]; var promise = Promise.resolve(config); //依次在处理链路数组中,从头部添加请求拦截器中间件(unshift:可向数组的开头添加一个或更多元素,并返回新的长度。) 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); }); //依次执行 请求拦截器中间件-> 请求 -> 返回拦截器中间件(shift:用于把数组的第一个元素从其中删除,并返回第一个元素的值。 ) while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } //返回promise对象 return promise; }
这一段是axios请求整体流程的核心方法,可以看到请求返回的是一个promise对象。这样可以让我们的异步请求天然的支持promise,方便我们对于异步的处理。
3、支持请求和和数据返回的拦截
依然是上面的核心流程代码,在设置好请求参数后,作者定义了一个chain数组,同时放入了dispatchRequest, undefined这两个元素对应promise的resolve和reject方法,之后将请求拦截器的成功和失败处理依次压入chain数组头部,将返回拦截器的成功和失败处理依次推入chain数组尾部。 最后循环取出chain数组,先依次取出chain数组中成对的请求拦截处理方法,promise执行,然后取出最初定义的dispatchRequest, undefined这两个元素执行请求,最后依次取出chain数组中成对的返回拦截器。
它的流程可以归纳为
resolve(request interceptor fulfilled N),reject(request interceptor rejected N)
-> …
-> resolve(request interceptor fulfilled 0),reject(request interceptor rejected 0)
-> resolve(dispatchRequest ),reject(undefined)
-> resolve(response interceptor fulfilled 0),reject(response interceptor rejected 0)
-> …
-> resolve(response interceptor fulfilled N),reject(response interceptor rejected N)
4、转换请求返回数据,自动转换JSON数据
在axios/lib/core/dispatchRequest.js文件中暴露的核心方法dispatchRequest中,有这样两段:
// 转换请求的数据 config.data = transformData( config.data, config.headers, config.transformRequest );
// 转换返回的数据 response.data = transformData( response.data, response.headers, config.transformResponse ); //axios通过设置transformResponse,可自动转换请求返回的json数据 transformResponse: [function transformResponse(data) { /*eslint no-param-reassign:0*/ if (typeof data === ‘string‘) { try { data = JSON.parse(data); } catch (e) { /* Ignore */ } } return data; }],
5、取消请求
文档上给了两种示例:
方法一:
var CancelToken = axios.CancelToken; var source = CancelToken.source(); axios.get(‘/user/12345‘, { cancelToken: source.token }).catch(function(thrown) { if (axios.isCancel(thrown)) { console.log(‘Request canceled‘, thrown.message); } else { // 处理错误 } }); // 取消请求(message 参数是可选的) source.cancel(‘取消请求‘);
方法二:
var CancelToken = axios.CancelToken; var cancel; axios.get(‘/user/12345‘, { cancelToken: new CancelToken(function executor(c) { // executor 函数接收一个 cancel 函数作为参数 cancel = c; }) }); // 取消请求 cancel();
这两种方法都可以取消发出的请求。先看具体的流程:
执行 cancel 方法 -> CancelToken.promise获取到resolve -> request.abort(); -> reject(cancel);
下面来看源码是如何实现的。
在xhr.js或http.js中都有这样一段,下面取自/lib/adapters/xhr.js
if (config.cancelToken) { // 处理取消请求的方法 config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // 清空请求 request = null; }); }
在请求时如果设置了cancelToken参数,就会监听来自cancelToken的promise,一旦来自cancelToken的promise被触发,就会执行取消请求的流程。
cancelToken的具体实现为(lib\cancel\CancelToken.js):
/** * 可以进行取消请求操作的对象 * * @param {Function} executor 具体的执行方法. */ function CancelToken(executor) { //判断executor是一个可执行的函数 if (typeof executor !== ‘function‘) { throw new TypeError(‘executor must be a function.‘); } //定义一个promise的resolve回调 var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); var token = this; //executor的参数为取消请求时需要执行的cancel函数 executor(function cancel(message) { if (token.reason) { // 已经取消了请求 return; } token.reason = new Cancel(message); //触发promise的resolve resolvePromise(token.reason); }); }
上面是方法二的源码实现,CancelToken会给自己添加一个promise属性,一旦cancel方法被触发就会执行取消请求的流程。(执行 cancel 方法 -> CancelToken.promise获取到resolve -> request.abort(); -> reject(cancel);)
方法一的源码实现(原理同方法二的源码实现,只是CancelToken.source会返回这个cancel):
CancelToken.source = function source() { var cancel; var token = new CancelToken(function executor(c) { cancel = c; }); return { token: token, cancel: cancel }; };
利用这个方法,一方面可以在按钮的重复点击方面大显身手,另一方面可以在数据的获取方面直接获取最新的数据。
6、客户端防止xsrf攻击
先来了解一下XSRF,以下内容来自维基百科。
XSRF跨站请求伪造(Cross-site request forgery),是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。
axios是如何防止xsrf攻击的?
// 添加 xsrf 请求头 // 只在标准浏览器环境中才会起作用 if (utils.isStandardBrowserEnv()) { var cookies = require(‘./../helpers/cookies‘); // 添加 xsrf 请求头 var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ? cookies.read(config.xsrfCookieName) : undefined; if (xsrfValue) { requestHeaders[config.xsrfHeaderName] = xsrfValue; } }
首先,axios会检查是否是标准的浏览器环境,然后在标准的浏览器环境中判断,如果设置了跨域请求时需要凭证且请求的域名和页面的域名相同时,读取cookie中xsrf token 的值,并设置到承载 xsrf token 的值的 HTTP 头中。
7、在node端支持设置代理
如果在标准浏览器环境则执行/lib/adapters/xhr.js,axios不支持proxy;如果在node环境运行,用/lib/adapters/http.js,支持proxy。
在组内的一个项目中,使用了vue的ssr服务端渲染的技术,其中ajax方案就是采用了axios。由于ssr是在服务端请求,因此在开发、测试、上线的过程中需要相应的host环境。例如在开发过程中,要么需要在程序的请求地址中写上ip地址替代域名,要么需要设置电脑的host文件,改变域名映射的ip地址。而在测试环境中,同样需要更改代码或者测试环境的host文件。这样一来,如果是改代码的方案则影响了代码的稳定性,每一次部署都需要修改代码;如果是修改host文件,则会影响环境的一致性,假如环境还部署了其他的服务,还会影响其他服务的测试。因此我们组内的男神便使用了axios的proxy功能,轻松的解决了这一问题。
//判断当前的部署环境 const isDev = process.env.NODE_ENV !== ‘production‘ if(isDev){ let proxy = null; //如果不是线上环境,且配置了代理地址则进行代理的设置,devHost是具体的ip配置 if(devHost.https){ proxy = { host: devHost.https, port: 443 }; Axios.defaults.proxy = proxy; }else if(devHost.http){ proxy = { host: devHost.http, port: 80 }; Axios.defaults.proxy = proxy; }else { //do nothing } }
而在axios的源码/lib/adapters/http.js中,则是如此实现代理的:
//如果设置了代理 if (proxy) { //取代理的域名为请求的域名 options.hostname = proxy.host; options.host = proxy.host; options.headers.host = parsed.hostname + (parsed.port ? ‘:‘ + parsed.port : ‘‘); options.port = proxy.port; options.path = protocol + ‘//‘ + parsed.hostname + (parsed.port ? ‘:‘ + parsed.port : ‘‘) + options.path; // Basic proxy authorization if (proxy.auth) { zar base64 = new Buffer(proxy.auth.username + ‘:‘ + proxy.auth.password, ‘utf8‘).toString(‘base64‘); options.headers[‘Proxy-Authorization‘] = ‘Basic ‘ + base64; } }
内部一些针对具体项目环境的二次封装
上面基于源码具体分析了axios的各项特性,下面再来讲一讲我们在具体使用时的一些二次封装。由于axios使用get方式设置参数时,都需要使用params的方式,例如:
axios.get(‘/user‘, { params: { ID: 12345 } }).then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); });
而之前使用vue-resource则习惯直接写上参数,形如:
axios.get(‘/user‘, { ID: 12345 } ) .then(function (response) { console.log(response); }).catch(function (error) { console.log(error); });
因此,对于组内的axios统一加了一层封装,承接之前的使用习惯:
let get = Axios.get; /** * 对原方法的get做一层装饰,可以传参时不必写params参数,直接传递参数对象,同时对已有的params写法兼容 * @param args 参数 url config * @returns {*} */ Axios.get = (...args) => { let param = args[1]; //如果以参数方式传递query,同时不存在axios需要的key:params,则为它添加 if (param && !param.hasOwnProperty(‘params‘)) { args[1] = { params: param } } return get(...args) }
这里需要注意的是,要确保在提交到服务端的query参数中不包含‘params’字段,不然还是要使用默认的参数格式。
而对于post方式,则做了如下封装:
let post = Axios.post; /** * axios的post请求默认会依据数据类型设置请求头,但是目前后台没有识别json,因此统一将请求的数据设置为x-www-form-urlencoded需要的字符串格式 * @param args 参数 url data config * @returns {*} */ Axios.post = (...args) => { let data = args[1]; //判断是对象就转化为字符串 if (data && typeof data === ‘object‘) { args[1] = qs.stringify(data) } return post(...args) }
axios使用方便,功能齐备强大,其中的一些编程思想也很不入俗套,是一款前后端通用的ajax请求框架,目前在github上已经有接近36K的赞,其优秀程度可见一斑。本文通过他的一些特性,分析了部分源码,旨在能够在使用它的同时,更加懂得它。
转自:https://www.cnblogs.com/zhuanzhuanfe/p/8458421.html
另外可参照阅读:https://blog.csdn.net/weixin_44475093/article/details/111940352