axios为何被广泛使用,有什么优势?如何封装,从ajax谈起
简单了解ajax
为什么会出现?
传统的网页如果需要更新内容的话,必须重载整个页面,例如提交表单场景,然后在新页面告诉你操作是成功了还是失败了。如果碰到网速太慢或其他原因,就会得到一个404的页面了。所以ajax的出现大大改变了这种现状,可以局部刷新页面,用户体验感提升,较少网络数据的传输量,同时也节省网络带宽。
语义:Asynchronous JavaScript and XML(异步的 JavaScript 和 XML),它不是新的变成语言,而是一种使用现有标准的新方法,在不重新加载整个页面的情况下,局部更新部分页面
-
工作原理
在用户和服务器之间加了一个中间层(AJAX引擎),使用户操作与服务器响应异步化
-
创建对象
jsconst xhr = new XMLHttpRequest() // xhr即为ajax实例 // 旧版本的ie ie5 ie6 使用ActiveX对象 const xhr = new ActiveXObject("Microsoft.XMLHTTP");
-
请求
向服务器发送请求,使用实例对象的open和send方法
js/** open方法的三个参数: method -- 请求类型 GET POST PUT DELETE OPTIONS TRACE等皆可 url -- 请求资源位置 async -- 布尔类型 true代表异步 false代表同步 对于post请求时,send可以携带body参数 例如:xhr.send('name=zs&age=18') get请求时参数可以直接写到open方法url参数用问号拼接 例如:xhr.open("GET", "xxx.txt?name=zs", true) setRequestHeader -- 设置请求头,两个参数,第一个为请求头键名 第二个为请求头的值 */ xhr.open("GET", "xxx.txt", true) xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); // 发送给服务器 xhr.send()
-
响应
responseText -- 获得字符串形式的响应数据 只有当 readyState 属性值变为4时,responseText 属性才可用
responseXML -- 获得XML形式的响应数据 如果来自服务器的响应是 XML,而且需要作为 XML 对象进行解析
-
onreadystatechange事件
当发送一个请求后,客户端需要确定请求什么时候完成,此事件就是用来捕获请求状态
每当readyState改变时,都会触发此事件
jsxhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { console.log(xhr.responseText); } }
readyState的取值
- 0 -- 请求未初始化,还没有调用open方法
- 1 -- 服务器连接已建立,但没有发送,没有调用send方法
- 2 -- 请求已接收
- 3 -- 请求处理中
- 4 -- 请求已完成,且响应就绪
status http状态码
了解axios
基于promise的请求库,作用于nodejs和浏览器中,同构应用程序(即同一套代码可以运行在浏览器和nodejs中),在服务器端使用nodejs的http模块,在客户端使用XMLHttpRequests
-
特点
- 支持Promise api
- 转换请求和响应数据
- 取消请求
- 超时处理
- 自动将请求序列化
- 自动转换JSON数据 ...
-
基本用法
js// 采用ES Modules方法引入,作为举例 // 安装,以下三种方式皆可 npm install axios / bower install axios / yarn add axios // 也可CDN引入 <script src="https://unpkg.com/axios/dist/axios.min.js"></script> // 引入 import axios from 'axios' // 请求 // 向给定ID的用户发起请求 axios.get('/user?ID=12345').then(function (response) { // 处理成功情况 console.log(response); }) // 也可采用config对象的形式 axios({ method: 'post', url: '/user/12345', data: { firstName: 'Fred', lastName: 'Flintstone' } })
-
axios实例
关于创建实例的参数, 以最新版本的axios(v1.4.0)为例
ts// 请求的服务器URL url?: string; // 创建请求时使用的方法 get delete head options post put patch purge link unlink中的一种,大小写都可 method?: Method | string; // 为axios实例传递相对URL,自动拼接到请求路径前 baseURL?: string; // 允许在向服务器发送之前,修改请求数据,只能用于put post patch这几个请求方法 // 数组中最后一个函数必须返回一个字符串,一个Buffer实例,ArrayBuffer,FormData或Stream // 可以修改请求头 // 参数为data, headers transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[]; // e.g. transformRequest: [ function (data, headers) { // 对发送的数据进行任意转换处理 return data } ] // 在传递给 then/catch 前,允许修改响应数据 对接收的数据进行任意处理 transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[]; // e.g. transformResponse: [ function(data) { // 对接收的数据进行任意转换处理 return data } ] // 请求头 headers?: (RawAxiosRequestHeaders & MethodsHeaders) | AxiosHeaders; // 参数,params的参数会出现在url path中 params?: any; // 用于序列化params paramsSerializer?: ParamsSerializerOptions | CustomParamsSerializer; // 请求body参数,该方式传的参数出现在 Request Payload中,即body中 data?: D; // 请求时间限制 timeout?: Milliseconds; // 请求超时提示错误信息 timeoutErrorMessage?: string; // 跨域请求时是否需要使用凭证 withCredentials?: boolean; // 允许自定义处理请求,使测试更加容易 // 返回一个promise并提供一个有效的响应 // 例子:https://github.com/axios/axios/blob/v1.x/lib/adapters/README.md adapter?: AxiosAdapterConfig | AxiosAdapterConfig[]; // HTTP Basic Auth auth?: AxiosBasicCredentials = { username: string; password: string; }; // 响应类型 arraybuffer blob document json text stream responseType?: ResponseType; // 解码响应的编码 默认值为utf8 // ascii ansi binary base64 base64url hex latin1 ucs-2 ucs2 utf-8 utf8 utf16le 以上编码还可以转换为大写传入 responseEncoding?: responseEncoding | string; // xsrf token 的值,被用作 cookie 的名称 xsrfCookieName?: string; // 带有 xsrf token 值的http 请求头名称 xsrfHeaderName?: string; // 允许为上传处理进度事件 -- 浏览器专属 onUploadProgress?: (progressEvent: AxiosProgressEvent) => void; // 允许为下载处理进度事件 -- 浏览器专属 onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void; // AxiosProgressEvent 的定义 export interface AxiosProgressEvent { loaded: number; total?: number; progress?: number; bytes: number; rate?: number; estimated?: number; upload?: boolean; download?: boolean; event?: BrowserProgressEvent; } // 定义了node.js中允许的HTTP响应内容的最大字节数 maxContentLength?: number; // 验证响应状态 2xx 3xx 4xx 5xx等 返回一个布尔值 validateStatus?: ((status: number) => boolean) | null; // (仅Node)定义允许的http请求内容的最大字节数 maxBodyLength?: number; // 定义了在node.js中要遵循的最大重定向数 如果值为0 则不进行重定向 maxRedirects?: number; // 最大上传下载速率 maxRate?: number | [MaxUploadRate, MaxDownloadRate]; // 重定向之前处理 beforeRedirect?: (options: Record<string, any>, responseDetails: { headers: Record<string, string> }) => void; // 定义了在nodejs中使用的unix套接字 // e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。 // 只能指定socketPath或proxy,如果都指定,则使用socketPath socketPath?: string | null; transport?: any; // 定义执行http时要使用的自定义代理和https请求 httpAgent?: any; httpsAgent?: any; // false禁用代理 // 为对象时,host port为必填 proxy?: { host: string; port: number; auth?: { username: string; password: string; }; protocol?: string; }; // 取消请求 cancelToken?: CancelToken; // 是否应该自动地压缩响应正文,如果设置为true还将删除 content-encoding 头部字段 decompress?: boolean; // 属性均为非必填 // silentJSONParsing -- 不进行json格式化 forcedJSONParsing -- 被迫进行json格式化 clarifyTimeoutError -- 声明超时错误 transitional?: TransitionalOptions = { silentJSONParsing?: boolean; forcedJSONParsing?: boolean; clarifyTimeoutError?: boolean; }; // 信号,用来配合取消请求使用 signal?: GenericAbortSignal = { readonly aborted: boolean; onabort?: ((...args: any) => any) | null; addEventListener?: (...args: any) => any; removeEventListener?: (...args: any) => any; }; // 不安全的http解析 insecureHTTPParser?: boolean; // 环境 env?: { FormData?: new (...args: any[]) => object; }; // 表单序列化 formSerializer?: FormSerializerOptions = { visitor?: (value, key, path, helpers) => { // helpers = { // defaultVisitor: { visitor, dots, metaTokens, indexes }; -- 等同于当前对象 // convertValue: (value: any) => any; // isVisitable: (value: any) => boolean; // } return true }; dots?: boolean; metaTokens?: boolean; indexes?: boolean | null; }; // 家族成员 family?: 4 | 6 | undefined; // 查找 lookup?: ((hostname: string, options: object, cb: (err: Error | null, address: string, family: number) => void) => void) | ((hostname: string, options: object) => Promise<[address: string, family: number] | string>);
常见创建实例方法
jsconst service = axios.create({ baseURL: 'http://localhost:8080/', timeout: 10000 })
-
请求配置 -- 与创建实例时参数相同
js// 两种写法皆可 // 1. export const getData = (params) => { return service.get('/api/getData', params) } // 2. export function getData(params) { return service({ url: '/api/getData', method: 'get', params }) } // 使用时常见的两种方式 import { getData } from 'xxx' import * as api from 'xxx'
-
响应结构
js{ // 由服务器提供的响应 data: { message: '', code: 200, data: {} }, // 服务器响应的http状态码 status: 200, // 服务器响应的http状态信息 statusText: 'OK', // 服务器响应头,所有的header名称都是小写,可以使用方括号形式访问 // response.headers['content-type'] headers: { // 键值对形式 cache-control: 'no-cache', content-type: 'application/json', ... }, // axios请求的配置信息 config: { // 配置信息 baseURL: '', method: '', timeout: '', ... }, // 生成此响应的请求,在nodejs中它是最后一个ClientRequest实例 // 在浏览器中则是XMLHttpRequest实例 request: { // 请求信息 readyState: 4, status: 200, timeout: 300000, ... } }
-
拦截器
在请求发送前和响应接收前的操作可以在拦截器中进行,可以给axios自身或者axios实例添加
js
// 返回一个id,方便使用eject方法清除拦截器
const myInterceptor = service.interceptors.request.use(onFulfilled, onRejected, options)
// e.g.
// 创建请求拦截器
const myInterceptor = service.interceptors.request.use(
config => {
// 可在此添加请求头信息
return config
},
err => {
// 在此处理错误
},
{
synchronous: false,
runWhen: (config) => {
// config参数继承自请求配置,即与创建实例配置一致
return false
}
}
)
// 清除拦截器
service.interceptors.request.eject(myInterceptor);
// 清空请求拦截器
service.interceptors.request.clear()
- 错误处理
js
import { getData } from 'xxx'
getData().then(res => {
}).catch(e => {
console.log(e)
})
- 取消请求
官网的取消请求的例子,可以了解以下
axios拦截器封装
简易拦截器封装
js
import axios from 'axios';
import { Message as ELMessage } from 'element-ui'
const service = axios.create({
baseURL: 'http://localhost:8081', // localhost替换成本地ip
timeout: 100000
})
service.interceptors.request.use(config => {
// 在此可以做请求头的处理,添加你所需要的请求头
config.headers['userId'] = 'userId'
config.headers['userName'] = 'userName'
config.headers.Authorization = 'tk'
return config
},
err => {
// 用饿了么弹窗提示
ELMessage({
showClose: true,
message: error && error.data.error.message,
type: 'error'
});
return Promise.reject(error.data.error.message)
}
)
service.interceptors.response.use(
res => {
const { data = {}, config = {}, headers = {} } = res || {}
const { code, message } = data
const { url, cancelMessage, responseType } = config
// 对响应的code message做处理
if (code && ![200, '200'].includes(code)) {
return Promise.reject()
}
// 此外还可以对文件下载做处理 responseType
// blob arraybuffer stream等
if (responseType === 'blob') {}
return data
},
error => {
if(axios.isCancel(error)) {
return ELMessage.error(error.message)
} else {
const { response = {} } = error
const { status, data = {} } = response
let { message = '系统错误' } = data
// 这里可以提示对应状态码的错误信息,也可以跳转到指定的页面,比方说404跳转到404的页面
const statusMap = {
400: '请求错误(400)',
403: '拒绝访问(403)',
500: '服务器错误(500)'
}
// 对响应状态status进行判断,判断几种特殊的值 401 - 未授权 400 - 请求错误 403 - 拒绝访问 501 - 服务未实现等状态码
if (statusMap.hasOwnProperty(status)) {
// 对于401后直接退出
if(status === 401) {
// logout
} else {
let _message = message || statusMap[status]
ELMessage.error(_message)
}
// 弹出提示信息框
} else {
ELMessage.error(message)
}
return Promise.reject(error)
}
}
)
export default service
接口防抖 - 即统一处理取消请求
一段时间内同样的接口(包含params和body参数及参数值)只能请求一次,如果重复相同的请求则取消后续的请求 取消的请求还是会发出该请求,但浏览器不接受该响应了,实际并未真正意义上的取消
关于取消请求,有多种方式,axios从v0.22.0
开始已经支持以fetch API方式 -- AbortController
取消请求,至于多种取消请求的方式,可以参考官网的例子自己进行拓展
这里我采用的方式是new axios.CancelToken
的方式
js
// 第一个版本 axios + lodash
import axios from 'axios'
import { isEqual } from 'lodash'
class CancelToken {
constructor({ timeout = 500 } = {}) {
this.requestMap = [] // 请求列表
this.timeout = timeout // 防抖时间,在timeout时间范围内的后续同样(url,method,data,params均一致)请求会被取消
}
// 每个请求添加唯一值,之后用作判断
uniqueKey(config) {
// 解构,config为请求拦截的config配置,这里data和params给了默认值
const { url, method, data = {}, params = {} } = config || {}
let uniqueKey = `${url}-${method}-${JSON.stringify(data)}-${JSON.stringify(params)}`
return uniqueKey
}
// 添加处理
append(config) {
const key = this.uniqueKey(config)
// 寻找与当前请求一致的之前的请求,用key做比较
const index = this.requestMap.findIndex(item => isEqual(item.key, key))
// 采用官网提供的 new axios.CancelToken 方式
config.cancelToken = new axios.CancelToken(executor => {
if (index === -1) {
// 如果没有则直接添加
this.requestMap.push({
fun: executor,
key,
config,
time: new Date().getTime()
})
} else {
// 如果有的话则查看是否在时间范围内
if (new Date().getTime() - this.requestMap[index].time < this.timeout) {
// 直接取消,这里不需要再对data和params参数及参数值作比较,前面寻找索引的时候已经通过key对比
// key里包含了data和params等
executor('取消请求')
} else {
// 更新时间,方便下次进行时间比较,否则会一直进入该else判断
this.requestMap[index].time = new Date().getTime()
}
}
})
}
}
export default CancelToken
// 第二种:
import axios from 'axios'
import { isEqual } from 'lodash'
class CancelToken {
constructor({ timeout = 500 } = {}) {
this.pending = []
this.timeout = timeout
}
// 比较配置是否相等,返回布尔值
isEqual(config1, config2) {
const urlEqual = config1.url === config2.url
const methodEqual = config1.method === config2.method
const dataEqual = isEqual(config1.data, config2.data)
const paramsEqual = isEqual(config1.params, config2.params)
return urlEqual && methodEqual && dataEqual && paramsEqual
}
// 添加请求,不讲之前有无此请求,一律进行添加,判断交给remove方法
append(config) {
config.cancelToken = new axios.CancelToken(executor => {
this.pending.push({
fun: executor,
config,
time: new Date().getTime()
})
})
}
// 去除请求
remove(config) {
// 过滤掉不在时间范围内的, 就只剩下在时间范围内的了
this.pending = this.pending.filter(item => item && new Date().getTime() - item.time < this.timeout)
// 在时间范围内的进行循环,比较有无一致(url、method、data、params)的请求,如果有,则进行取消
for (let i = 0; i < this.pending.length; i++) {
// 书写采用es6可选链运算符,如果不支持,可以选择安装插件,或者改为 && 判断也可
if (this.isEqual(this.pending[i]?.config, config) && this.pending[i]?.config !== config) {
const index = this.pending.findIndex(i => i?.config === config)
this.pending[index]?.fun?.('取消请求')
this.pending[i] = null
}
}
}
prevent(config) {
this.append(config)
this.remove(config)
}
}
export default CancelToken
// 使用 -- 在axios拦截器页面进行引入,将config传入
import CancelToken from './cancelToken.js'
const cancelToken = new CancelToken({}) // 可以传入配置对象,当前仅支持timeout配置,如果不提供配置对象,timeout默认500
// 创建axios实例
const service = axios.create({
baseURL: 'http://localhost:8081', // localhost替换成本地ip
timeout: 100000
})
service.interceptors.request.use(config => {
// 第一种使用
cancelToken.append(config)
// 第二种使用
cancelToken.prevent(config)
return config
})
两种方法皆可实现取消请求的功能
axios取消请求,实际并未真正的取消,而是将浏览器中的接口状态 status,修改为canceled,响应还会发出,浏览器不对此接口做回应
如果项目中没有使用到lodash库的话,我觉得可以手写一个比较两个对象的方法,下面是手写比较方法
js
function isEqual(value, other) {
function isObjectLike(value) {
return value != null && typeof value == 'object'
}
if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {
return value !== value && other !== other
}
const toStr = Object.prototype.toString,
objTag = '[object Object]',
arrTag = '[object Array]'
if(((toStr.call(value) !== objTag) && (toStr.call(other) !== objTag)) || (toStr.call(value) !== arrTag) && (toStr.call(other) !== arrTag)) {
return value === other || JSON.stringify(value) === JSON.stringify(other)
}
let valueKeys = Object.keys(value),
otherKeys = Object.keys(other)
if(valueKeys.length !== otherKeys.length) {
return false
}
for (let i = 0; i < valueKeys.length; i ++) {
let key = valueKeys[i]
let item1 = value[key],
item2 = other[key]
if(((toStr.call(item1) === objTag) && (toStr.call(item2) === objTag)) || (toStr.call(item1) === arrTag) && (toStr.call(item2) === arrTag)) {
// 值是对象或数组则再次进行比较
return isEqual(item1, item2)
} else if(item1 === item2) {
return true
} else {
return false
}
}
}
let obj1 = { 'b': { 'c': { 'd': 333 } } }
let obj2 = { 'b': { 'c': { 'd': 222 } } }
console.log(isEqual(obj1, obj2)); // false
let arr1 = [1,2,{'a': 12, 'b': [1,2,3]}]
let arr2 = [1,2,{'a': 12, 'b': [1,2,3]}]
console.log(isEqual(arr1, arr2)); // true