目录
axios.interceptor.request和axios.interceptor.response
InterceptorManager.prototype.use
XMLHttpRequest的缺点
前面我们介绍了使用XMLHttpRequest实例来发送AJAX请求,它的具体实现步骤如下:
// 创建xhr实例
const xhr = new XMLHttpRequest()
// 设置请求方式,请求URL
xhr.open('post', 'http://localhost:3000/test')
// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json')
// 设置返回对象xhr.response的数据格式
xhr.responseType = 'json'
// 设置请求体,并发送请求
xhr.send(JSON.stringify({
name: 'qfc',
age: 18
}))
// 接收异步的响应
xhr.onreadystatechange = function(){
// 当readyState为4时,表示收到完整的响应体
if(xhr.readyState === 4) {
// 如果响应状态码在200~299之间,说明服务器响应成功
if(xhr.status >= 200 && xhr.status < 300) {
// 响应状态码,状态描述
console.log(xhr.status)
console.log(xhr.statusText)
// 响应头
console.log(xhr.getAllResponseHeaders())
// 响应体
console.log(xhr.response)
}
}
}
通过以上代码,我们可以总结出XMLHttpRequest实现AJAX请求的几个缺点:
1、步骤过多,过于繁琐
2、AJAX请求响应都与xhr实例关联,严重耦合
3、事件回调的方式获得异步响应,容易产生异步回调地狱
由于XMLHttpRequest具有的以上缺点,所以我们很少直接使用XMLHttpRequest来实现AJAX,通常情况下,我们都使用基于XMLHttpRequest实现AJAX的封装库。目前较为流行的是axios库。
axios初体验
我们使用axios来重写下上面代码的逻辑
axios({
url: 'http://localhost:3000/test', // 设置请求URL
method: 'post', // 设置请求方式
headers: { // 设置请求头
'Content-Type': 'application/json'
},
data: { // 设置请求体
name: 'qfc',
age: 18
}
}).then(response => {
console.log(response.status) // 响应状态码
console.log(response.statusText) // 响应状态描述
console.log(response.headers) // 响应头
console.log(response.data) // 响应体
})
对比XMLHttpRequest实现的AJAX而言,axios实现AJAX的写法更加简单,结构更加清晰,同时通过Promise返回值来获取异步的响应,避免了回调地狱。
axios底层也是基于XMLHttpRequest实现的AJAX,但是axios做了较为深入的封装,避免了使用者接触XMLHttpRequest实例。
下面我们来介绍下axios的具体使用,主要分为:
- axios发送AJAX的API
- axios默认配置
- axios实例
- axios请求配置
- axios正常响应结果
- axios异常响应结果
- axios拦截器
- axios取消请求
axios发送AJAX的API
axios
// axios(config)
axios({
url: 'http://localhost:3000/test',
method: 'post',
headers: {
'Content-Type': 'application/json'
},
data: {
name: 'qfc',
age: 18
}
})
// axios(url, config)
axios('http://localhost:3000/test', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
data: {
name: 'qfc',
age: 18
}
})
axios.METHOD
// axios.get, axios.head, axios.options, axios.delete
// (url, config)
axios.get('http://localhost:3000/test', {
params: {
name: 'qfc',
age: 18
}
})
// axios.post, axios.put, axios.patch
// (url, data, config)
axios.post('http://localhost:3000/test', {
name: 'qfc',
age: 18
})
可以发现,axios本身可以作为函数使用,调用axios函数可以发送AJAX请求。
axios函数入参需要传入一个config配置对象,通过config配置对象设置请求URL,请求METHOD,请求HEADER,请求BODY。
但是axios函数的config入参还可以进一步简化,比如可以将请求URL提取出来,作为axios函数的入参,从而简化config。
另外config中的method也可以提取,有了jQuery金玉在前的$.get,$.post设计,我们也可以将这些请求METHOD升级为方法。
所以axios又可以被当成对象使用,调用axios.get,axios.post等发送AJAX请求,这样就又进一步简化了config。
而axios.post,axios.put,axios.patch这些方法发送AJAX请求时,一般都需要携带请求BODY,所以可以进一步将config中的data提取作为axios.post等的入参,从而再次简化config。
通过将config中的url, method, data等关键要素提取出来,其实config也没有什么特别需要使用者传入的配置了,也就是说可以直接将config入参省略。
axios默认配置
前面我们都直接使用了axios函数,或者axios.post,axios.get这样的方式来发送AJAX请求,那么axios函数对象是如何来的呢?
其实,当我们引入了axios库后,axios库会对外保留一个默认的axios函数对象,该默认的axios函数对象会自动挂载到全局对象window下,所以我们可以直接使用。
在前面使用axios发送AJAX的例子中,我们对比原生XMLHttpRequest发送AJAX来看,axios已经做到了很牛皮的步骤优化,结构优化,那么你满意了吗?
答案是还是不够完美。
axios.defaults.baseURL
我们思考一个场景,在一个网页中,有多个对接后端接口的AJAX,这些后端接口的URL可能如下:
http://localhost:3000/order/query
http://localhost:3000/order/create
http://localhost:3000/order/delete
http://localhost:3000/order/update
那么如果我们通过axios来实现AJAX,则多个axios的url参数会出现严重的冗余部分http://localhost:3000/order
有没有办法将http://localhost:3000/order提取为公共URL前缀呢?
这就需要使用到axios的默认配置了,axios作为一个(函数)对象,它身上有一个属性defaults,我们可以在axios发送AJAX之前,基于axios.defaults设置一些默认配置
axios.defaults.baseURL = 'http://localhost:3000/order'
axios.get('/query', {
params: {
id: '123456'
}
})
axios.post('/create', {
productId: 'p123',
price: 10,
amount: 1
})
axios.get('/delete', {
params: {
id: '123456'
}
})
axios.post('/update', {
productId: 'p123',
amount: 10
})
axios.defaults.baseURL的作用是,如果axios发送的AJAX请求的URL不是完整路径的话,就拼接baseURL前缀。
axios.defaults.headers
当然,除了axios.defaults.baseURL默认配置外,还有其他的如url,method,headers等,比如headers默认配置,就有很多稀奇古怪的用法,我们可以设置公共head,或者特定请求方式的head
axios.defaults.headers.common = { // 针对所有axios发送的AJAX都添加的请求头
test: 'testCommon'
}
axios.defaults.headers.post = { // 只针对axios post AJAX添加的请求头
try: 'tryPost'
}
axios({
url: 'http://localhost:3000/test',
method: 'post',
data: {
name: 'qfc',
age: 18
}
})
axios({
url: 'http://localhost:3000/test',
method: 'get',
params: {
name: 'qfc',
age: 18
}
})
axios实例
有了axios.defaults默认配置,axios近乎完美了,但是还是有一点欠缺。比如一个网页中,有多个对接后端接口的AJAX需求,但是后端接口的URL如下:
http://localhost:3000/order/query
http://localhost:3000/order/create
http://localhost:3000/user/query
http://locahost:3000/user/create
axios.defaults只用设置一个baseURL,因为axios.defaults只对默认的axios函数有用,而默认的axios函数只有一个。
那么此时该如何搞呢?
axios.create方法可以创建一个新的,微型的axios函数对象,axios.create创建出来的函数对象具备了默认axios函数对象的大部分能力,比如axios函数发送AJAX,axios.METHOD发送AJAX,axios默认配置设置。但是需要注意的是axios.create创建的axios函数不等价于默认的axios函数,而是小于默认的axios函数。
axios.create(config)接收一个config配置对象入参,我们可以通过config设置新建axios函数对象的默认配置。
const orderAxios = axios.create({
baseURL: 'http://localhost:3000/order'
})
const userAxios = axios.create({
baseURL: 'http://localhost:3000/user'
})
orderAxios({
url: '/query',
method: 'get',
params: {
id: '123456'
}
})
userAxios({
url: '/create',
method: 'post',
data: {
name: 'qfc',
age: 18
}
})
或者也可以通过axios.defaults来设置默认配置。
axios请求配置
axios(config), axios.get(url, config) ,axios.post(url, data, config),axios.default.configItem 都涉及到config请求配置对象。
我们已经知道了,可以通过config配置对象设置如下信息:
- url 请求URL
- method 请求METHOD
- headers 请求HEADER
- data 请求BODY(post,patch,put)
- params 请求URL的查询字符串参数(get,delete)
- baseURL 请求URL的默认前缀
那么还有哪些请求配置,它们的作用又是啥呢?
{
// `url` 是用于请求的服务器 URL
url: '/user',
// `method` 是创建请求时使用的方法
method: 'get', // 默认是 get
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
transformRequest: [function (data) {
// 对 data 进行任意转换处理
return data;
}],
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 对 data 进行任意转换处理
return data;
}],
// `headers` 是即将被发送的自定义请求头
headers: {'X-Requested-With': 'XMLHttpRequest'},
// `params` 是即将与请求一起发送的 URL 参数
// 必须是一个无格式对象(plain object)或 URLSearchParams 对象
params: {
ID: 12345
},
// `paramsSerializer` 是一个负责 `params` 序列化的函数
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function(params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
// `data` 是作为请求主体被发送的数据
// 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
// 在没有设置 `transformRequest` 时,必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属:FormData, File, Blob
// - Node 专属: Stream
data: {
firstName: 'Fred'
},
// `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
// 如果请求话费了超过 `timeout` 的时间,请求将被中断
timeout: 1000,
// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // 默认的
// `adapter` 允许自定义处理请求,以使测试更轻松
// 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)).
adapter: function (config) {
/* ... */
},
// `auth` 表示应该使用 HTTP 基础验证,并提供凭据
// 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头
auth: {
username: 'janedoe',
password: 's00pers3cret'
},
// `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
responseType: 'json', // 默认的
// `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
xsrfCookieName: 'XSRF-TOKEN', // default
// `xsrfHeaderName` 是承载 xsrf token 的值的 HTTP 头的名称
xsrfHeaderName: 'X-XSRF-TOKEN', // 默认的
// `onUploadProgress` 允许为上传处理进度事件
onUploadProgress: function (progressEvent) {
// 对原生进度事件的处理
},
// `onDownloadProgress` 允许为下载处理进度事件
onDownloadProgress: function (progressEvent) {
// 对原生进度事件的处理
},
// `maxContentLength` 定义允许的响应内容的最大尺寸
maxContentLength: 2000,
// `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
validateStatus: function (status) {
return status >= 200 && status < 300; // 默认的
},
// `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
// 如果设置为0,将不会 follow 任何重定向
maxRedirects: 5, // 默认的
// `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
// `keepAlive` 默认没有启用
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
// 'proxy' 定义代理服务器的主机名称和端口
// `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据
// 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。
proxy: {
host: '127.0.0.1',
port: 9000,
auth: : {
username: 'mikeymike',
password: 'rapunz3l'
}
},
// `cancelToken` 指定用于取消请求的 cancel token
// (查看后面的 Cancellation 这节了解更多)
cancelToken: new CancelToken(function (cancel) {
})
}
其中比较常用的有timeout,即设置请求超时时间,默认为0,表示无超时限制。
transformRequest,transformResponse我感觉不太好用,而且完全可以被拦截器替代。
另外可能用到的配置就是validateStatus,默认情况下axios将HTTP响应状态码200~299的情况视为成功,其他情况视为失败。
还有一个cancelToken配置,用于取消axios发出的AJAX请求的,后面介绍。
axios正常响应结果
axios收到HTTP响应也是异步的,但是axios将HTTP响应都封装进了一个对象中,并且将该对象作为了axios函数同步返回的Promise对象的结果。
axios.post('http://localhost:3000/test', {
name: 'qfc',
age: 18
}).then(response => {
console.log(response);
})
当异步的HTTP响应被axios收到时,axios的同步返回的Promise对象就会从pending状态变为fulfilled或rejected。当正常响应时,默认响应状态码在200~299时,Promise对象状态变为fulfilled,当异常响应时,即响应状态码不在200~299时,Promise对象状态变为rejected。
此时,我们讨论的是fulfilled状态Promise对象的PromiseResult结果值,即上面代码then第一个回调函数的入参response值
response是一个对象,它的结构为:
- status 响应状态码
- statusText 响应状态描述
- headers 响应头
- data 响应体
- config axios函数对象的配置对象
- request axios内部实际发送AJAX请求的XMLHttpRequest实例
从响应结果的结构设计来看,axios严格按照了HTTP响应报文的结构划分了HTTP实际响应内容,同时也加入了axios发送AJAX内部实现的关键的两个参数xhr实例(request),以及xhr实例的配置信息(config)。
axios异常响应结果
我们知道axios会将HTTP响应封装进axios同步返回的Promise对象的结果中,我们可以通过Promise对象调用then方法来获得其正常响应结果,也可以通过catch方法来获得其异常响应结果。
axios.post('http://localhost:3000/test', {
name: 'qfc',
age: 18
}).then(response => {
console.log(response)
}).catch(error => {
console.log(error.response)
})
axios根据axios.defaults.validateStatus来判断HTTP响应是否正常,默认情况下,当HTTP响应状态码在200~299时,视为正常响应,否则视为异常响应。
如下是error.response的打印信息
可以发现可以正常响应结果对象的结构一致。
axios拦截器
axios相较于其他AJAX封装库的特别之处就在于axios拦截器,这玩意和nodejs的中间件设计类似,都是做的切面工作。
axios的拦截器分为:请求拦截器和响应拦截器
axios.interceptor.request和axios.interceptor.response
在默认的axios函数,或者axios.create新建axios函数上都有一个属性interceptors,该属性指向一个对象,结构如下
在axios.interceptors对象下有两个属性request,response,它们分别对应请求拦截器和响应拦截器。
axios.interceptors.request和axios.interceptors.response都是对象,都是InterceptorManger的实例。
InterceptorManager.prototype.use
InterceptorManager类的原型上有一个use方法,它的作用就是注册拦截器函数。
InterceptorManager.prototype.use(fulfilled, rejected)
use方法需要传入两个函数,第一个函数用于拦截成功的处理结果,第二个函数用于拦截失败的处理结果。
而axios.interceptors.request/response是InterceptorManager的实例,所以可以使用use方法
而在请求拦截器上use,就是注册请求拦截器,在响应拦截器上use就是注册响应拦截器
axios.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json'
return Promise.resolve(config)
}, err => {
return Promise.reject(err)
})
axios.interceptors.response.use(response => {
return Promise.resolve(response.data)
}, err => {
return Promise.reject(err)
})
axios.post('http://localhost:3000/test', {
name: 'qfc',
age: 18
})
其实,我们很容易想要请求拦截器和响应拦截器拦截的是啥,请求拦截器拦截的是请求,而在axios的请求其实就是config配置对象,响应拦截器拦截的是响应,而axios的响应其实就是axios返回Promise对象的结果值response。
如上代码,请求拦截器fulfilled函数入参是config,我们可以在axios发送AJAX请求前,对config做一些修改。响应拦截器fulfilled函数入参是response,我们可以在axios(config).then收到response前,对response做一些修改。
另外还需要注意的是,关于拦截器通过use注册的fulfilled函数和rejected函数的返回结果类型,这里要求是Promise类型。对于fulfilled函数来说,返回要求是fulfilled状态的Promise对象,对于rejected函数来说,返回要求是rejected状态的Promis对象。
当然,我们也可以让axios帮我们生成Promise对象,因为如果我们return非Promise类型的值都会被包装为fulfilled状态的Promise对象,如果我们thorw一个值,则该值会被包装为rejected状态的Promise对象。
axios.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json'
return config
}, err => {
throw err
})
axios.interceptors.response.use(response => {
return response.data
}, err => {
throw err
})
axios.post('http://localhost:3000/test', {
name: 'qfc',
age: 18
}).then(response => {
console.log(response);
}).catch(err => {
console.log(err.response)
})
拦截器的执行顺序
还有一点需要注意的是,可以注册多个请求拦截器和响应拦截器,但是多个拦截器之间的执行顺序需要分析下。
首先明确顺序的是:
[请求拦截器] 优先于 [AJAX请求发送] 优先于 [AJAX响应收到] 优先于 [响应拦截器] 优先于 [axios().then]
但是多个请求拦截器之间的执行顺序,以及多个响应拦截器之间的执行顺序是啥呢?请看下面示例:
axios.interceptors.request.use(config => {
console.log('请求拦截器1');
return config
}, err => {
throw err
})
axios.interceptors.request.use(config => {
console.log('请求拦截器2');
return config
}, err => {
throw err
})
axios.interceptors.response.use(response => {
console.log('响应拦截器1');
return response
}, err => {
throw err
})
axios.interceptors.response.use(response => {
console.log('响应拦截器2');
return response
}, err => {
throw err
})
axios.post('http://localhost:3000/test', {
name: 'qfc',
age: 18
}).then(response => {
console.log('axios.then');
}).catch(err => {
})
可以发现请求拦截器的执行顺序和其注册顺序相反,响应拦截器的执行顺序和其注册顺序相同。
axios取消请求
我们回顾下XMLHttpRequest如何取消AJAX请求
const xhr = new XMLHttpRequest()
xhr.open('get', 'http://localhost:3000/test')
xhr.responseType = 'json'
xhr.send()
xhr.onreadystatechange = function(){
if(xhr.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.response)
}
}
}
xhr.abort() // 取消xhr发送的AJAX请求
XMLHttpRequest实例可以调用abort方法来取消已经发送AJAX请求,在服务器还没有响应成功前。如果服务器已经响应成功了,则xhr.abort()无效。
axios虽然是基于XMLHttpRequest实现的AJAX,但是axios对XMLHttpRequest做了较为全面的封装,使用者无法在服务器响应成功之前获得axios底层的xhr实例,(ps:当服务器响应成功后,axios会将底层xhr实例加入到response.request中返回)
所以我们使用axios时,无法直接使用xhr.abort()来取消已经发送的AJAX请求。
但是axios为我们提供了另外一种很绕的方式来取消已发送的AJAX请求。我们看下具体示例:
let c;
axios({
url: 'http://localhost:3000/test',
method: 'post',
data: {
name: 'qfc',
age: 18
},
cancelToken: new axios.CancelToken(cancel => {
c = cancel
})
}).then(response => {
console.log(response);
}).catch(err => {
console.log(err.response || err.message);
})
setTimeout(c, 1000, '我来终止请求') // 服务器2000ms后响应
走读以上代码,我们可以分析出,当1s后,c函数执行,触发了AJAX请求的取消操作。
我们可以猜想c函数应该控制着axios底层的xhr.abort()的执行。
然后,有两个cancelToken和CancelToken,其中
cancelToken是config配置对象的一个配置属性,用于接收一个CancelToken实例。
而CancelToken是默认axios函数对象上的一个属性,它是一个构造函数,接收一个使用者指定的executor执行器函数作为参数,axios内部会调用executor执行器函数,并传入c函数,给使用者。
也就是说:axios想在AJAX发送请求时,但是AJAX响应还没到前,将xhr.abort的控制权交给使用者,但是没有任何可用出口。即axios在收到AJAX响应前,只能单方面的接收用户的输入,而无法输出东西给用户。
所以axios只能在用户输入上做文章了。
即用户输入的config配置对象中,提供一个 new CancelToken(executor) 给axios内部,而其中executor是用户指定的函数,该函数的执行体由用户设计,但是该函数的调用由axios操控,所以axios可以通过函数入参将xhr.abort控制权(cancel)交给用户设计的executor函数执行体中。
用户设计的函数执行体,可以顺势将xhr.abort控制权再次转移出去 c = cancel。
很绕,思路也很巧妙,axios发送AJAX请求中间但凡有一个异步操作包裹,axios这种骚操作就费了。
下面一节,我们来深入axios源码。