AJAX、Fetch、axios
AJAX
AJAX可以在不更新全局的情况下更新局部页面。通过在与服务器进行数据交换,可以使网页实现异步更新。
AJAX的原理就是通过XHR对象来向服务器发起异步请求,从服务器获得数据,然后用JS来操作DOM更新页面。领导想找小李汇报一下工作,就委托秘书去叫小李,自己就接着做其他事情,直到秘书告诉他小李已经到了,最后小李跟领导汇报工作。Ajax请求数据流程与“领导想找小李汇报一下工作”类似,上述秘书就相当于XMLHttpRequest对象,领导相当于浏览器,响应数据相当于小李。浏览器可以发送HTTP请求后,接着做其他事情,等收到XHR返回来的数据再进行操作。
创建AJAX
// 1. 创建 XMLHttpRequest 实例 let xhr = XMLHttpRequest() // 2. 打开和服务器的连接 xhr.open('get', 'URL') // 3.发送 xhr.send() // 4. 接收变化。 xhr.onreadystatechange = () => { if(xhr.readyState == 4 && xhr.status == 200){ // readyState: ajax 状态,status:http 请求状态 console.log(xhr.responseText); //响应主体 } }
- 创建AJAX实例:let xhr = new XMLHttpRequest()
- 打开请求,配置请求前的配置项:xhr.open([http method], [url], [async], [userName], [userPass])
- http methods 请求方式:post,get,delete,put,head,options,trace,connect
- url:想服务器请求的路径
- async:是否为异步请求
- userName、userPass:用户名与密码
- 通过XMLHttpRequest.open()方法与服务器建立连接
- 发送请求:XMLHttpRequest.send() 方法中如果 Ajax 请求是异步的则这个方法发送请求后就会返回,如果Ajax请求是同步的,那么请求必须知道响应后才会返回。
- 通过XMLHttpRequest对象的onreadystatechange事件监听服务器端的通信状态
- 接收数据并进行处理
- 将处理后的结果更新到页面上
AJAX的缺点:
- 本是针对MVC架构,不符合前端MVVM的浪潮
- 基于原生的XHR开发
- 配置和调用方式混乱
axios原理
axios是使用promise封装的ajax,它内部有两个拦截器,分别是request拦截器和response拦截器。
- 请求拦截器的作用是在请求发送之前进行一些操作,例如在每个请求体上加入token
- 响应拦截器的作用是接收到响应后做的一些操作,例如登录失效后需要重新登录跳转到登录页
axios的特点
- 由浏览器端发起请求,在浏览器中创建XHR
- 支持promise API
- 监听请求和返回
- 更好的格式化,自动将数据转换为json数据
- 安全性更高,可抵御CSRF攻击
axios常用的方法
axios常用的方法有get
、post
、put
、patch
、delete
等。其中get
和post
返回的都是promise
对象,可以使用promise
方法
axios.get(url[, config])
:get请求用于列表和信息查询
axios.get('apiURL', { param: { id: 1 } // param 中的的键值对最终会 ? 的形式,拼接到请求的链接上,发送到服务器。 }).then(res => { console.log(res); }) .catch( error => { console.log(error) }
axios.delete(url[, config])
:删除
axios.delete('apiURL', { params: { id: 1 }, timeout: 1000 })
axios.post(url[, data[, config]])
:post请求用于信息的添加
axios.post('apiURL',{ user: '小新', age: 18 }).then( res => { console.log(res); }) .catch( error => { console.log(error) }
axios.put(url[, data[, config]])
:更新操作
axios.put('apiURL', { name: '小新', })
axios.patch(url[, data[, config]])
:更新操作
axios.patch('apiURL', { id: 13, },{ timeout: 1000, })
put和patch的区别
patch
方法用来更新局部资源,假设我们有一个UserInfo,里面有userId,userName,userGender等10个字段。可你的编辑功能因为需求,在某个特别的页面里只能修改userName,这个时候就可以使用patch
。
put
也适用于更新数据,但必须提供完整的资源对象。
axios相关配置
- url:用于请求服务器的url
- method:请求方法,默认为get
- baseURL:会自动加到url前面
- proxy:用于配置代理
- transformRequest:允许在服务器发送请求之前修改请求数据
axios拦截器执行顺序问题
- 请求拦截:axios的请求拦截器会先执行最后指定的回调函数,再依次向前执行
- 响应拦截:axios的响应拦截器会先执行最先执行的回调函数,再依次向前执行
例如:
axios.interceptors.request.use(config => { console.log(`请求拦截1`); return config; }); axios.interceptors.request.use(config => { // 在发送请求之前做些什么 console.log(`请求拦截2`); return config; }); // 添加响应拦截器 axios.interceptors.response.use(response => { // 对响应数据做点什么 console.log(`成功的响应拦截1`); return response.data; }); // 添加响应拦截器 axios.interceptors.response.use(response => { // 对响应数据做点什么 console.log(`成功的响应拦截2`); return response; }); // 发送请求 axios.get('/posts') .then(response => { console.log('成功了'); })
执行结果为
console.log("请求拦截2");
console.log("请求拦截1");
console.log("成功的响应拦截1");
console.log("成功的响应拦截2");
console.log("成功了");
为什么axios中需要拦截器
在SPA应用中,通常会使用token进行用户身份认证,这就要求每次请求必须携带用户的身份信息,针对这个需求,为了避免在每个请求中单独处理,我们可以通过封装统一的request函数来为每隔请求统一添加token信息。
但如果想为某些请求添加缓存时间或者控制某些请求的调用频率的话,我们就需要不断地修改request函数来扩展对应的功能。此时,如果在考虑对响应进行统一处理,我们的request函数将变得越来越庞大,也越来越难维护。所以axios为我们提供了拦截器。
为什么请求拦截2会在请求拦截1之前执行呢?
在axios
源码中将发送请求分为了请求拦截器、发送请求、响应拦截器、相应回调,通过Promise的链式调用将这些部分结合起来了,这样就得到了发送请求拿到数据的全部过程。
下面分析源码:
- 代码开始构建了一个config配置对象,用于第一次执行Promise返回一个成功的Promise
- 最核心的数组chain,这个数组中保存了请求拦截器、响应拦截器和发送请求函数。该数组中间放的是发送请求的函数,左边放的是请求拦截器,右边放的是响应拦截器。在第一步中返回的Promise对象,将遍历chain数组逐一执行里面的函数,并返回新的Promise对象
- 往数组中添加请求拦截函数,依照axios请求的执行顺序,请求拦截器应该在发送请求之前执行,故应该添加在发送请求函数的前面,使用unshift方法
- 往数组中添加响应拦截器函数,依照axios请求的执行顺序,响应拦截器应该在发送请求之后执行,故应该添加在发送请求函数的后面,所以使用的是数组的push方法
- Promise遍历执行,每次从chain中取出两个 函数执行(一个成功回调,一个失败回调)
- 最后返回一个Promise对象,用于执行响应数据的回调
fetchfetch
是http请求数据的方式,它使用Promise,但不使用回调函数。fetch
采用模块化设计,通过数据流处理数据,对于请求大文件或网速慢的情况相当有用。默认情况下fetch不会接收或发送cookies。
优点:
- 采用模块化思想,将输入、输出、状态跟踪分离
- 基于promise,返回一个promise对象
缺点:
- 过于底层,有很多状态码没有进行封装
- 无法阻断请求
- 兼容性差无法检测请求进度
Fetch、ajax与axios的区别
- 传统的ajax利用的是
HMLHttpRequest这个对象
,和后端进行交互。 - 而
JQury ajax
是对原生XHR
的封装,多请求间有嵌套的话就会出现回调地狱的问题。 -
axios
使用promise
封装XHR
,解决了回调地狱的问题。而Fetch
没有使用XHR
,使用的是promise
Fetch和Ajax比有什么优点
Fetch
使用的是promise
,方便使用异步,没有回调地狱的问题。
总结
Ajax
是一种web数据交互的方式,它可以使页面在不重新加载的情况下请求数据并进行局部更新,它内部使用了XHR
来进行异步请求。Ajax
在使用XHR
发起异步请求时得到的是XML
格式的数据,如果想要JSON格式,需要进行额外的转换;Ajax
本身针对的是MVC框架
,不符合现在的MVVM架构
;Ajax
有回调地狱问题;Ajax
的配置复杂
而Fetch
是XHR的代替品,它基于Promise
实现的,并且不使用回调函数,它采用模块化结构设计,并使用数据流进行传输,对于大文件和网速慢的情况非常友好。但是Fetch
不会对请求和响应进行监听;不能阻断请求;过于底层,对一些状态码没有封装;兼容性差。
axios
是基于Promise
对XHR
进行封装,它内部封装了两个拦截器,分别是请求拦截器和响应拦截器。请求拦截器用于在请求发出之前进行一些操作,比如:设置请求体,携带Cookie、token等;响应拦截器用于在得到响应后进行一些操作,比如:登录失效后跳转到登录页面重新登录。axios
有get、post、put、patch、delete等方法。axios可以对请求和响应进行监听;返回Promise
对象,可以使用Promise
的API;返回JSON
格式的数据;由浏览器发起请求;安全性更高,可以抵御CSRF攻击。
axios源码分析
axios的执行流程
- 使用axios.create创建单独的实例,或直接使用axios实例
- 对于axios调用进入到request()中进行处理
- 执行请求拦截器
- 请求数据转换器,将传入的数据进行处理,比如
JSON.stringify(data)
- 执行适配器,判断是浏览器端还是node端,以执行不同的方法
- 响应数据转换器,对服务器端的数据进行处理,比如
JSON.parse(data)
- 执行响应拦截器,对服务器端数据进行处理,比如token失效跳转到登录页
- 返回数据
入口文件(lib/axios.js)
导出的axios就是 实例化后的对象,还在其上挂载create
方法,以供创建独立的实例,实现实例之间互不影响。
// 创建实例过程的方法 function createInstance(defaultConfig) { return instance; } // 实例化 var axios = createInstance(defaults); // 创建独立的实例,隔离作用域 axios.create = function create(instanceConfig) { return createInstance(mergeConfig(axios.defaults, instanceConfig)); }; // 导出实例 module.exports = axios;
createInstance()
function createInstance(defaultConfig) { // 实例化,创建一个上下文 var context = new Axios(defaultConfig); // 平时调用的 get/post 等等请求,底层都是调用 request 方法 // 将 request 方法的 this 指向 context(上下文),形成新的实例 var instance = bind(Axios.prototype.request, context); // Axios.prototype 上的方法 (get/post...)挂载到新的实例 instance 上, // 并且将原型方法中 this 指向 context utils.extend(instance, Axios.prototype, context); // Axios 属性值挂载到新的实例 instance 上 // 开发中才能使用 axios.default/interceptors utils.extend(instance, context); return instance; }
createInstance
执行流程:
- 通过构造函数
Axios
创建实例context
,作为下面request
方法的上下文(this指向) - 将
Axios.prototype.request
方法作为实例使用,并把this
指向context
,形成新的实例instance
- 将构造函数
Axios.prototype
上的方法挂载到新的实例instance
上,然后将原型各个方法中的this
指向context
,这样才能使用get、post
等方法 - 将
Axios
的属性挂载到instance
上
可以看到axios不是简单的创建实例context,而是在context上进行this绑定形成新的实例,然后将Axios属性和请求方法挂载到新的实例上
拦截器(lib/core/InterceptorManager.js)
拦截器涉及一个属性和三个方法:
- handler:存放use注册的回调函数
- use:注册成功和失败的回调函数
- eject:删除注册过的函数
- forEach:遍历回调函数
function InterceptorManager() { // 存放 use 注册的回调函数 this.handlers = []; } InterceptorManager.prototype.use = function use(fulfilled, rejected, options) { // 注册成功和失败的回调函数 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); } }); };
dispatchRequest(lib/core/dispatchRequest.js)
dispatchRequest主要做了以下操作:
- transformRequest: 对 config 中的 data 进行加工,比如对 post 请求的 data 进行字符串化(JSON.stringify(data))
- adapter:适配器,包含浏览器端 xhr 和 node 端的 http
- transformResponse: 对服务端响应的数据进行加工,比如 JSON.parse(data)
取消请求(lib/cancel/CancelToken.js)
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('Operation canceled by the user.');
- CancelToken 挂载 source 方法用于创建自身实例,并且返回 {token, cancel}
- token 是构造函数 CancelToken 的实例,cancel 方法接收构造函数 CancelToken 内部的一个 cancel 函数,用于取消请求
- 创建实例中,有一步是创建处于 pengding 状态的 promise,并挂在实例方法上,外部通过参数 cancelToken 将实例传递进 axios 内部,内部调用 cancelToken.promise.then 等待状态改变
- 当外部调用方法 cancel 取消请求,pendding 状态就变为 resolve,即取消请求并且抛出 reject(message)
总结
- 为了支持 axios() 简洁写法,内部使用 request 函数作为新实例
- 使用 promsie 链式调用的巧妙方法,解决顺序调用问题
- 数据转换器方法使用数组存放,支持数据的多次传输与加工
- 适配器通过兼容浏览器端和 node 端,对外提供统一 api
- 取消请求这块,通过外部保留 pendding 状态,控制 promise 的执行时机