我和数据差个“axios" -- axios的原理解析
写在前面:
距离2021年 还有两个月~11月份的开始,决定勤奋一波
关于axios
axios 不用更多的介绍,vue官方的推荐是使用axios,vue-resource淡出框架的依赖,在我们进行项目开发和搭建的时候,axios是我们连接和后端接口的桥梁,当然选择axios,自然是其的一些特点,能够更好的满足我们日常的工作;
能学习到什么呢
通过本篇文章,你将大概了解到axios创建实例过程、配置合并流程以及原理,还有进行请求的流程;深入了解到axios的响应拦截器原理,支持多平台(浏览器和node)环境下使用原理、请求响应参数的设置原理、整个的执行过程~嘻嘻,有些地方大家简单大概了解,还是需要看下源码的执行原理滴;
进入axios
想要了解axios的原理,可以从其特点出发;
进入原理探究过程(搓手手)
首先自行下载axios的包,查看其目录结构
在这个里面有两个助手包,一个helpers、一个是utils,这两个包都是axios的工具包,helpers是服务于axios,utils更加广泛的工具,可以在其他的插件中进行引入使用,我们每个插件其实都会自定义自己的工具包~
配置阶段
axios的配置共分为3种,全局配置、实例配置、请求配置;这三个配置和我们进行的操作息息相关;
全局配置
全局配置,更加明确的说其实是axios里面给我们提供的一个默认的选项,当我们导入axios的包的时候,其实就已经进行了默认选项的配置;
默认配置项如下:
而这些全局配置选项也正是我们在进行实例化的时候,可以进行自定义配置的,主要的属性包括;
- transformRequest :Array 允许在向服务器前发送信息的时候,统一处理请求信息默认配置选项功能:
- 序列化请求参数
- 为不同类型的请求参数添加请求头
-
transformResponse :Array 则是对响应数据的操作处理
- timeout 、headers等默认的配置
- 默认配置如图所示
实例配置
实例配置其实是我们针对自己项目进行的个性化的配置,在导入axios时候,就给我们提供了一个create的函数,而这个函数的作用其实是创建一个新的实例,并返回给我们;应用于整个项目配置
在一个项目中每个接口都有共同的配置,或者几个接口有共同的表现形式,每个实例化的axios都有自己的配置,通过全局配置进行初始化,或者合并成一个新的配置项目实例化配置一般是我们自己配置的,我们在项目中可能包含多个模块,项目的接口名字不一致,因此需要配置不同的axios的实例信息,这样我们配置moduleA和moduleB的实例配置,每次返回的新的实例不会互相影响;
create的方法
axios.create = function create(instanceConfig) { // console.log("实例配置",mergeConfig(axios.defaults, instanceConfig)) return createInstance(mergeConfig(axios.defaults, instanceConfig)); };
//定义创建的axios function createInstance(defaultConfig) { var context = new Axios(defaultConfig); // instance 绑定axios的默认数据 instance 返回wrap函数 var instance = bind(Axios.prototype.request, context); // console.log(instance.prototype) // 复制Axios的原型到扩展到实例中 utils.extend(instance, Axios.prototype, context); //将Axios的属性扩展到实例上 utils.extend(instance, context); //instance 进行复制 return instance; }
这块儿其实有点难以理解 ,就是我们创建实例时候,直接new Axios就可以了,为什么还需要进行包装呢??
先来看bind方法 ,bind的方法其实很好理解,就是将Axios原型方法上的request方法绑定在context的上下文中,返回一个wrap的方法
module.exports = function bind(fn, thisArg) { return function wrap() { var args = new Array(arguments.length); for (var i = 0; i < args.length; i++) { args[i] = arguments[i]; } return fn.apply(thisArg, args); }; };
》》 utils.extend(instance, Axios.prototype, context); 这个的意思又是什么呢
function extend(a, b, thisArg) { // console.log("参数b",b) forEach(b, function assignValue(val, key) { if (thisArg && typeof val === 'function') { a[key] = bind(val, thisArg); } else { a[key] = val; } }); return a; }
迷糊人员?这里其实是将Axios上原型上的方法复制到我们的instance的实例上,这样的操作?保持迷惑?
最后一个的extend utils.extend(instance, context); 是将刚刚创建的Axios的实例,复制到instance上,整个创建实例包含的步骤有:
- 将b的属性内容复制给a
- 此时将Axios原型上的方法复制到instance中
- 最后把axios实例复制给instance 形成一个真正的instance
查阅资料,说这样的操作是为了更好的使用axios,如果我们只是返回一个new Axios的实例,那么我们在进行调用的方式是比较单一的,这样子配置了后;调用的方式就多了起来
axios({config}).then()
axios.get('url').then()
这种的实现方式,利用了拷贝继承,打印instance的构造函数;
请求配置
同一个实例会有一些公用的配置项目,如baseUrl,但是很多时候,不同的请求具体的配置是不一样的,如url、method等,所以在请求的时候需要传入的配置与实例配置进行合并;
请求的时候,也进行了相关的配置项目,这样形成了三个配置项,主要采用后配置后优先的原则,优先级顺序:请求配置>实例配置>全局配置;
这个时候便涉及到了合并的问题;主要涉及的配置合并
配置合并主要在/lib/core/mergeConfig.js中进行
module.exports = function mergeConfig(config1, config2) { // eslint-disable-next-line no-param-reassign config2 = config2 || {}; var config = {}; var valueFromConfig2Keys = ['url', 'method', 'params', 'data']; //需要进行深拷贝的属性 var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy']; //默认的配置项目key var defaultToConfig2Keys = [ 'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer', 'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'maxContentLength', 'validateStatus', 'maxRedirects', 'httpAgent', 'httpsAgent', 'cancelToken', 'socketPath' ]; //将传入的配置项目内容先赋值到config中 utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) { if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } }); // 遍历需要进行深拷贝的属性 /** * config2中的如果是对象 则进行深拷贝 * 如果不是则直接进行赋值,如果该属性对应的值, * 则直接进行拷贝config1的内容 */ utils.forEach(mergeDeepPropertiesKeys, function mergeDeepProperties(prop) { if (utils.isObject(config2[prop])) { config[prop] = utils.deepMerge(config1[prop], config2[prop]); } else if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } else if (utils.isObject(config1[prop])) { config[prop] = utils.deepMerge(config1[prop]); } else if (typeof config1[prop] !== 'undefined') { config[prop] = config1[prop]; } }); //defaultToConfig2Keys 一些配置 config2的内容存在则使用config2的,不存在使用config1的内容 utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) { if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } else if (typeof config1[prop] !== 'undefined') { config[prop] = config1[prop]; } }); //请求的一些key var axiosKeys = valueFromConfig2Keys .concat(mergeDeepPropertiesKeys) .concat(defaultToConfig2Keys); //其他的配置key 传入的key的值 var otherKeys = Object .keys(config2) .filter(function filterAxiosKeys(key) { return axiosKeys.indexOf(key) === -1; }); // 将config2中的自定义key的值 存入config中 utils.forEach(otherKeys, function otherKeysDefaultToConfig2(prop) { if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } else if (typeof config1[prop] !== 'undefined') { config[prop] = config1[prop]; } }); //返回config return config; };
mergeConfig中主要涉及到三个主要key值的合并;
-
valueFromConfig2Keys 是在请求的时候要进行添加的项目内容
-
mergeDeepPropertiesKeys 需要神拷贝的key,比如headers、proxy等(主要是对象)
-
defaultToConfig2Keys 浅拷贝字符串
合并的规则如下:
- 进行key遍历,如果config2中某个key存在内容,则取config2的内容
- 三个key遍历完成后,进行属性的合并,因为config2中有些属性是我们自己自定义设置的,将自定义的设置增加到config中
整体配置就完成了~
创建请求
配置已经完成,接下来就是创建请求了;创建请求其实很重要起的作用的就是我们刚刚创建的实例Axios
封装了几乎是http所支持的所有请求方法, 主要的一个作用就是
- 合并请求配置和实例配置
- 规整化请求方法信息
- 收集请求拦截器和响应拦截器
- 进行发送请求
- 返回当前的promise
我们进行的请求方法主要存放文件在axios/lib/core/dispatchRequest.js中
执行transformRequest方法,对数据进行相关操作,获取到要进行传入的data
根据请求方法配置headers 此时有的确定的方法 删除无用的headers
根据获取的请求适配器传入config进行请求调用 以浏览器为例:会创建XmlRequest的对象进行请求和发送数据;Xhr的过程
执行完成后,进行回调后的对数据操作的方法;
这样整个执行的流程就完毕了,
拦截器
拦截器作用:当我们发送数据的时候,能够拦截到发送内容并进行更改内容,适用于统一性的发送信息,比如token、自定义的请求头,通过统一封装的request函数为每个请求添加统一的信息;
存问问题:
后期如果需要为某些 GET 请求设置缓存时间或者控制某些请求的调用频率的话,我们就需要不断修改 request 函数来扩展对应的功能。此时,如果在考虑对响应进行统一处理的话,我们的 request 函数将变得越来越庞大,也越来越难维护
而axios也给我们提供了一种方法,分为请求拦截器和响应拦截器
请求拦截器:该类拦截器的作用是在请求发送前统一执行某些操作,比如在请求头中添加 token 字段。
响应拦截器:该类拦截器的作用是在接收到服务器响应后统一执行某些操作,比如发现响应状态码为 9998 时,自动跳转到登录页
Axios 的作用是用于发送 HTTP 请求,而请求拦截器和响应拦截器的本质都是一个实现特定功能的函数,拦截器的过程主要可以分为三个步骤
- 任务配置收集拦截器
- 任务编排(按照顺序进行存储)时的调度
- 按照拦截器的顺序进行执行的拦截器
进入任务收集
我们在进行配置拦截器的使用,使用了实例上的interceptors的request的属性,进入源码中发现,在Axios的实例上,存在request的属性,是INterceptorManager的实例。调用use的方法时候,其实就是调用了InterceptorManager中的方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) { this.handlers.push({ fulfilled: fulfilled, rejected: rejected }); return this.handlers.length - 1; };
发现其实利用了一个handlers进行收集我们的拦截方法,fullfilled是成功,reject是失败,
响应拦截器的原理也是一样 ,都是利用handlers进行收集 ,
任务编排
到这里,我们请求拦截器和响应拦截器的任务配置已经收集完毕,接下来就是任务的编排过程;我们必须要保证,请求拦截器在我们请求之前,响应拦截器在实际请求之后执行;这里便引入了异步的promise方法,利用promise 中then的方法进行调用
//请求拦截的中间件 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); });
先存储当前的实际请求,然后将请求拦截器放置在请求队列的前面,响应拦截器放置在响应拦截器的后面;这样便实际形成了一个数据的队列
该队列以请求函数作为区分,前面部分分别分别存储请求成功时候函数和请求拦截失败时候函数,后面响应拦截函数;
任务执行
到这里任务开始执行,
//当响应拦截器还存在的时候,压入的promis中 去请求 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); }
利用了promise的串行、顺序执行的特点,Promise 的执行串原理 因此需要每一个请求拦截器的resolve中返回config 供下一个promise函数使用;
适配器;
axios不仅能够在vue、react等项目中使用,还能够应用在node环境中,其主要是浏览器环境和node环境,支持不同的平台,自然是进行了请求适配~
在我们的全局默认配置中存在一个adapter的属性,该属性就是在引入实例或者创建新的实例的时候进行了适配器的选择,主要的代码如下:
//选择哪一个请求器 如果当前存在xmlHttpRequest的话 就去请求这个否则将调用node的http模块的访问 function getDefaultAdapter() { var adapter; if (typeof XMLHttpRequest !== 'undefined') { ///通过判断XMLHttpRequest是否存在,来判断是否是浏览器环境 adapter = require('axios/lib/adapters/xhr'); } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // 不再浏览器环境将用node环境 adapter = require('axios/lib/adapters/http'); } return adapter; }
Axios 同时支持浏览器和 Node.js 环境,对于浏览器环境来说,我们可以通过 XMLHttpRequest 或 fetch API 来发送 HTTP 请求,而对于 Node.js 环境来说,我们可以通过 Node.js 内置的 http 或 https 模块来发送 HTTP 请求。
默认适配器:http
取消请求实现
进行取消请求设置
在请求的发送中:
axios.CancelToken.source()
创建了一个 CancelToken 实例给 token, CancelToken 的参数是一个函数,将函数参数再赋值给 cancel
将 { token: token,cancel: cancel } 作为新对象返回
CancelToken 具体做了什么 ?
创建了一个 Promise , 同时保存 Promise resolve 的具体实现
执行上一步传递的函数 A ,并将 取消操作的具体实现函数 作为参数传递给 A ,A 将其赋值给 cancel 传递给用户
取消操作是执行了 Promise.resolve,同时将用户设定的 message 封装后作为结果返回给 then 的实现
其实都是进行了promise上的操作流程;
其他
axios中还有其他的功能:如错误处理机制
进行双重cookie预防Csrf攻击;
整体流程依赖
核心模块依赖类图,发送和任务拦截收集均在Axios中完成,整个过程依赖Promise的then方法保证串行、 顺序执行;
整个的活动图如图所示:
axios主要配置使用
统一status拦截
axios给我们提供了拦截函数,如果正在项目中所有的场景接口都是一样的,对统一的状态码进行处理;此时我们在拦截函数中,需要进行控制操作;
if(err.response.status){ const errCode = err.response.status const msg = err.response.message const errorMsgMap = { 400: '错误请求', 401: '请检查用户名和密码', 403: '身份过期请重新登录', 404: '请求错误,未找到该资源', 408: '请求超时', 500: '服务器端出错', 501: '网络未实现', 502: '网络错误', 503: '服务不可用', 504: '网络超时', 'other':'未知错误' } var message = errorMsgMap[errCode] ? errorMsgMap[errCode] :errorMsgMap['other'] ; err.message = message Toast.show({content:message}) }
这样管理方便
鉴权:
鉴权是验证用户是否拥有权限访问系统;传统的鉴权是通过密码来验证的。这种方式的前提是,每个获得密码的用户都已经被授权。在建立用户时,就为此用户分配一个密码,用户的密码可以由管理员指定,也可以由用户自行申请。这种方式的弱点十分明显:一旦密码被偷或用户遗失密码,情况就会十分麻烦,需要管理员对用户密码进行重新修改,而修改密码之前还要人工验证用户的合法身份。
常用的鉴权方法:
在前后端分离的项目中,jwt方式比较多;
- 客户端使用用户名和密码登录
- 服务端收到请求,校验用户名和密码
- 返回客户端一个token
- 客户端收到token 进行存储
- 客户端请求服务端接口 携带token信息
- 服务端收到请求 验证token内容
服务端生成token 此时利用node的三方插件 ,前端收到token时候进行存储使用 ,发送的时候进行在请求拦截时候,使用
服务端接收和校验,前端根据返回的响应状态码进行操作
服务端校验标准
- 请求信息中不存在token,则直接进行返回登录状态
- 存在token,解析token,解析生成uid ,校验uid是否存在
- 不存在可能是因为token过期 直接返回登录状态
除了axios的鉴权,vue-router也可以进行鉴权操作,不过还是依赖服务端返回的用户角色等信息~
写到结尾:
不知不觉,又重新回顾了自己学习过的内容,其实有的时候感觉自己写不出来,但是了解了思想和写法,在后续使用中想到这种实现模式;