一、XHR对象的理解和使用
1. XMLHttpRequest(XML)对象
XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用。
- 使用
XMLHttpRequest(XHR)对象可以与服务器交互,也就是发送ajax请求 - 前端页面可以获取到数据,而无需刷新整个浏览器页面
- 使得web页面可以只更新页面的局部,而不影响用户的操作
2. XMLHttpRequest创建请求的API说明
XMLHttpRequest(): 创建XHR对象的构造函数status: 响应状态码值,比如200、404等statusText: 响应状态文本说明readyState: 标识请求状态的只读属性- 0: 初始
- 1:
open()之后 - 2:
send()之后 - 3: 请求中
- 4: 请求完成
onreadytatechange: 绑定readyState改变的监听函数responseType: 指定响应数据类型,如果是json,得到响应后自动解析响应体数据response: 响应体数据,类型取决于responseType的制定timeout: 指定请求的超时时间,默认为0代表没有限制ontimeout: 绑定超时的监听函数onerror: 绑定请求网络错误的监听函数open(): 初始化一个请求,参数为:(method, url[,async])(默认async为true,表示异步请求)send(data): 发送请求abort(): 中断请求getResponseHeader(name): 获取指定名称的相应头值getAllResponseHeaders(): 获取所有的相应头组成的字符串getResponseHeader(name): 绑定超时的监听函数setRequestHeader(name, value): 设置请求头
3. ajax请求和一般的http请求
ajax请求是一种特别的http请求- 对服务器来说,
ajax请求和一般的http请求没有任何区别,区别在与浏览器端 - 浏览器端发送请求,只有
XHR或者fetch发出的才是ajax请求,其他所有的请求都是非ajax请求 - 浏览器端收到响应:
- 一般请求:浏览器一般会直接显示响应体数据,也就是我们常说的刷新/跳转页面,例如,请求的css样式文件、js文件、图片文件等
ajax请求:浏览器不会对界面进行任何更新操作,只是调用监视的回调函数并传入响应的相关数据 注:我们编写发送ajax的代码,用ajax引擎发起ajax请求,并执行对应的回调函数
二、XHR封装原生ajax请求函数
1. axios基本使用说明:
axios函数返回值为promise。成功的结果为response,异常的结果为error- 能处理多种类型的请求:
GET/POST/PUT/DELETE - 函数的参数为一个配置对象:
{url: '请求地址', method: '请求方式', params: 'GET/DELETE请求的query参数', data: 'POST/PUT请求的请求提参数'} - 响应
json数据自动解析为json
2. 仿照axios封装简单的ajax请求函数:
function axios({
url,
method = 'GET',
params = {},
data = {}
}){
// 返回一个promise对象
return new Promise((resolve, reject) => {
// 修正url请求路径
let queryString = ''
Object.keys(params).forEach(key => {
queryString += `${key}=${params[key]}&`
})
if(queryString){
url += url.includes('?') ? '&' : '?' + queryString.substring(0, queryString.length-1)
}
// 修正请求方式
method = method.toUpperCase()
// 1. 执行异步的ajax请求
// 创建xhr对象
const xhr = new XMLHttpRequest()
// 打开连接(初始化请求,没有发送请求)
xhr.open(method, url, true)
// 发送请求
if(method === 'GET' || method === 'DELETE'){
xhr.send(null)
}else if(method === 'POST' || method === 'PUT'){
// Content-Type请求头告诉服务器,请求数据为json数据
xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8')
xhr.send(JSON.stringify(data))
}
// 监听异步请求成功的回调函数
xhr.onreadystatechange = () => {
if (xhr.readyState === 4){
const {status, statusText} = xhr
if(xhr.status >= 200 && xhr.status < 300){
// 2.1 请求成功了,调用resolve()
const response = {
data: JSON.parse(xhr.response),
status,
statusText
}
resolve(response)
} else {
// 2.2 请求失败了,调用reject()
reject(new Error('request error status is ' + status))
}
}
}
})
}
三、axios的理解和使用
1. axios的特点
- 是基于promise的异步ajax请求库
- 浏览器和node端都可以使用
- 支持请求/响应拦截器
- 支持请求取消
- 请求/响应数据转换
- 支持批量发送多个请求(实际都是用Promise.all代替这个功能)
2. axios常用API语法
axios(config): 统一/基本的发送任意类型请求的方式axios(url[, config]): 只指定url发送get请求axios.request(config): 等同于axios(config)axios.get(url[, config]): 发送get请求axios.post(url[, config]): 发送post请求axios.put(url[, config]): 发送put请求axios.delete(url[, config]): 发送delete请求- =======================================
axios.default.xxx: 请求的默认全局配置axios.interceptors.request.use(): 添加请求拦截器axios.interceptors.response.use(): 添加请求拦截器- =======================================
axios.create([config]): 根据配置创建一个新的axios(是拥有对应配置的axios函数,不是axios的实例,只是相对于axios没有下面的功能)- =======================================
axios.Cancel(): 用于创建取消请求的错误对象axios.CancelToken(): 用于创建取消请求的token对象axios.isCancel(): 是否是一个取消请求的错误axios.all(promises): 用于批量执行多个异步请求,所有请求都成功了才算成功,类似Promise.all()axios.spread(): 获取指定接受所有成功数据的回调函数的方法
3. axios难点语法的理解和使用
(1) axios.create(config)
- 根据制定配置创建一个新的
axios,也就是每个新的axios都有自己的配置 - 新的
axios只是没有取消请求和批量发送请求的方法,其他所有的语法都是一致的 - 为什么设计这个语法?
- 需求:项目中有部分接口需要的配置与另一部分接口需要的配置不太一样,如何处理?
- 解决:创建2个新的
axios,每个都有自己特有的配置,分别应用到不同要求的接口请求中
- axios.create()返回的对象与axios的区别
(2) 拦截器函数/ajax请求/请求的回调函数的执行顺序
- 拦截器函数
- 请求拦截器: 在真正发请求前, 可以对请求进行检查或配置进行特定处理的函数, 包括成功/失败的函数, 传递的必须是
config - 响应拦截器: 在请求返回后, 可以对响应数据进行特定处理的函数,包括成功/失败的函数, 传递的默认是
response
- 请求拦截器: 在真正发请求前, 可以对请求进行检查或配置进行特定处理的函数, 包括成功/失败的函数, 传递的必须是
- 使用拦截器调用接口测试
axios.defaults.baseURL = 'http://localhost:3001';
// 添加请求拦截器1
axios.interceptors.request.use((config) => {
console.log('add request interceptors1')
return config
})
// 添加请求拦截器2
axios.interceptors.request.use((config) => {
console.log('add request interceptors2')
return config
})
// 添加响应拦截器1
axios.interceptors.response.use((response) => {
console.log('add response interceptors1')
return response
})
// 添加响应拦截器2
axios.interceptors.response.use((response) => {
console.log('add response interceptors2')
return response
})
// 执行发送请求操作
function getDate(){
axios.get('/user').then((result) => {
console.log('Get date:', result.data)
}).catch((err) => {
console.log('Get err:', err)
});
}
==============响应数据如下图==============
从上面的答应结果可以看出:
- 调用
axios()并不是立即发送ajax请求, 而是需要经历一个较长的流程 - 请求拦截器2 => 请求拦截器1 => 发ajax请求 => 响应拦截器1 => 响应拦截器2 => 请求的回调
- 此流程是通过
promise串连起来的, 请求拦截器传递的是config, 响应拦截器传递的是response错误流程控制与错误处理
- 拦截器首先被执行的是请求拦截器,在执行响应的拦截器,请求拦截器按照添加的顺序从后往前执行,响应拦截器按照添加的顺序从前往后执行;
- 请求拦截器是在真正发请求前,对请求头和请求体的数据进行特定的处理,用请求拦截器返回的配置参数,调用
axios.request()方法发送ajax请求; - 响应拦截器是在接口真正返回数据前,对响应数据进行特定处理,再将处理后的数据传给执行请求结束后的成功或者失败回调函数。
四、axios源码分析
1. 源码目录组成结构
├── /dist/ # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── /adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现http适配器(包装http包)
│ │ └── xhr.js # 实现xhr适配器(包装xhr对象)
│ ├── /cancel/ # 定义取消功能
│ ├── /core/ # 一些核心功能
│ │ ├── Axios.js # axios的核心主类
│ │ ├── dispatchRequest.js # 用来调用http请求适配器方法发送请求的函数
│ │ ├── InterceptorManager.js # 拦截器的管理器
│ │ └── settle.js # 根据http响应状态,改变Promise的状态
│ ├── /helpers/ # 一些辅助方法
│ ├── axios.js # 对外暴露接口
│ ├── defaults.js # axios的默认配置
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置TypeScript的声明文件
└── index.js # 入口文件
2. axios与Axios的关系
axios函数对应的是Axios.prototype.request方法通过bind(Axiox的实例)产生的函数axios有Axios原型上的所有发特定类型请求的方法:get()/post()/put()/delete()axios有Axios的实例上的所有属性:defaults/interceptors- 后面又添加了
create()/CancelToken()/all()
3. axios.create()返回的对象与axios的区别
- 相同:
- 都是一个能发任意请求的函数:
request(config) - 都有发特定请求的各种方法:
get()/post()/put()/delete() - 都有默认配置和拦截器的属性:
defaults/interceptors
- 都是一个能发任意请求的函数:
- 不同:
- 默认匹配的值不一样
instance没有axios后面添加的一引起方法:create()/CancelToken()/all()
4. axios发请求的流程
- 整体流程:
request(config)===>dispatchRequest(config)===>xhrAdapter(config) - request(config): 将请求拦截器 /
dispatchRequest()/ 响应拦截器, 通过promise链串连起来, 返回promise - dispatchRequest(config): 转换请求数据 ===> 调用
xhrAdapter()发请求 ===> 请求返回后转换响应数据, 返回promise - xhrAdapter(config): 创建
XHR对象, 根据config进行相应设置, 发送特定请求, 并接收响应数据, 返回promise
5. axios的请求/响应拦截器
- 请求拦截器: 在真正发请求前, 可以对请求进行检查或配置进行特定处理的函数, 包括成功/失败的函数, 传递的必须是config。
- 响应拦截器: 在请求返回后, 可以对响应数据进行特定处理的函数,包括成功/失败的函数, 传递的默认是response。
// 添加请求拦截器1
axios.interceptors.request.use(
config => {
console.log('request interceptor1 onResolved()')
return config
},
error => {
console.log('request interceptor1 onRejected()')
return Promise.reject(error);
}
)
// 添加请求拦截器2
axios.interceptors.request.use(
config => {
console.log('request interceptor2 onResolved()')
return config
},
error => {
console.log('request interceptor2 onRejected()')
return Promise.reject(error);
}
)
// 添加响应拦截器1
axios.interceptors.response.use(
response => {
console.log('response interceptor1 onResolved()')
return response
},
function (error) {
console.log('response interceptor1 onRejected()')
return Promise.reject(error);
}
)
// 添加响应拦截器2
axios.interceptors.response.use(
response => {
console.log('response interceptor2 onResolved()')
return response
},
function (error) {
console.log('response interceptor2 onRejected()')
return Promise.reject(error);
}
)
// 发送请求
axios.get('http://localhost:3000/posts')
.then(response => {
console.log('data', response.data)
})
.catch(error => {
console.log('error', error.message)
})
-
从
InterceptorManager.js文件中可以看到,在执行axios.interceptors.request.use()和axios.interceptors.response.use()时会讲添加的请求/响应拦截器的成功和失败回调函数以对象的形式一对一对的存起来; -
然后在
Axios.js文件中,首先拿合并后的config配置,Promise.resolve()创建一个成功的promise对象,再将请求/响应拦截器的回调函数对象存储在Axios实例的interceptors属性上,再去执行Axios.prototype.request函数:- 首先声明了一个
chain的回调函数数组,构建promise的回调链,默认为[dispatchRequest, undefined], 其中dispatchRequest为正常发送请求的函数,区分Node端(http请求模块实现)和浏览器端(xhr对象实现),undefined为后面一对一对的取成功和失败回调函数时的占位符 - 遍历实例对象的拦截器存储属性
interceptors的request数组,依次添加到chain队列的前面,这也是为什么后添加的请求拦截器先执行的原因 - 遍历实例对象的拦截器存储属性
interceptors的response数组,依次添加到chain队列的后面
- 首先声明了一个
-
再循环遍历
chain回调函数数组,从前往后每次取两个回调函数,作为上面promise对象的成功和失败回调函数循环执行,全部回调函数执行完成之后在返回promise上面案例中添加两个请求响应的拦截器,并发送请求,组成的链式队列解析如下:
// 默认初始化的promise回调链chain:
chain: [dispatchRequest, undefined]
// 添加请求拦截器的回调函数队列:
requestInterceptors: [{
fulfilled: fulfilledA1(){},
rejected: rejectedB1(){}
}, {
fulfilled: fulfilledA2(){},
rejected: rejectedB2(){}
}]
// 添加响应拦截器的回调函数队列:
responseInterceptors: [{
fulfilled: fulfilledC1(){},
rejected: rejectedC2(){}
}, {
fulfilled: fulfilledD1(){},
rejected: rejectedD2(){}
}]
// 遍历请求/响应拦截器后组合出来的promise回调链chain:
chain: [
fulfilledA2, rejectedB2, fulfilledA1, rejectedB1,
dispatchReqeust, undefined,
fulfilledC1, rejectedC2, fulfilledD1, rejectedD2
]
// 拿合并好的config配置生产的初始promise对象:
var promise = Promise.resolve(config)
// 通过promise的then()串连起所有的请求拦截器/请求方法/响应拦截器
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
// 所有promise回调链chain执行完成之后最终返回promise
返回的promise对象最后会在请求结束之后,执行用户自定义的成功和失败回调
6. axios的请求/响应数据转换器
- 请求拦截器: 对请求头和请求体数据进行特定处理的函数。
setContentTypeIfUnset:setRequestHeader(headers, 'application/json;charset=utf-8');
request-data:JSON.stringify(data)
- 响应拦截器: 将响应体json字符串解析为js对象或数组的函数。
return response.data = JSON.parse(response.data)
// 响应成功返回的数据体结构
{
data,
status,
statusText,
headers,
config,
request
}
// 响应失败返回的数据体结构
{
message,
request,
response
}
7. 取消未完成的请求
- 基本流程:
- 配置
cancelToken对象 - 缓存用于取消请求的
cancel函数 - 在后面特定时机调用
cancel函数取消请求 - 在错误回调中用
axios.isCancel()判断error是cancel的错误,做对应的逻辑处理
- 配置
- 实现功能:
- 执行
cancel函数, 传入错误信息message - 内部会让
cancelPromise变为成功, 且成功的值为一个Cancel对象 - 在
cancelPromise的成功回调中执行中断请求的处理逻辑, 并让发请求的proimse失败, 失败的reason为Cancel对象
- 执行
axios.defaults.baseURL = 'http://localhost:3001';
let cancel; // 用于保存取消请求的函数
function getDate(){
axios({
url: '/user',
cancelToken: new axios.CancelToken((c) => {
// 参数c是用于取消当前请求的函数,保存到全局后面调用即可取消请求
cancel = c
})
}).then((result) => {
console.log('Get date:', result.data)
}).catch((err) => {
// err报错信息,如果err为cancel导致的错误,可以用axios.isCancel()函数检测
if(axios.isCancel(err)){
console.log('Get request canceled:', err.message)
}else{
console.log('Get err:', err)
}
}).finally(() => {
// 请求结束之后将cancel重置为空
cancel = null
})
setTimeout(() => {
// 执行取消请求,如果请求已经结束,cancel已经重置为空,则什么也不做,
// 求未结束,可以执行cancel函数,取消请求。
cancel && cancel('请求被取消了')
},7)
}