[toc]
前言
性能点
此文次要是基于vuecli3我的项目中axios封装及api治理的实际记录及过程中的踩坑播种,性能根本都是依据工作中需要实现。需要背景是,在同一套申请配置下,实现以下次要性能:
- [x] 自定义申请配置
- [x] 设置全局拦截器
- [x] 响应胜利及异样的全局拦挡对立解决
- [x] 避免反复申请(勾销以后反复的申请)
- [x] 路由切换勾销以后所有pending状态的申请(可配置白名单)
- [x] 独自勾销收回的某个申请
- [x] api对立治理
axios一些个性
在开始之前,首先明确一些axios的个性,这些个性会影响到某些性能的实现形式:
- 通过
axios.create()
办法创立的实例对象只有常见的数据申请办法,没有勾销申请、并发申请等办法。可通过Object.keys()
将所有的key打印进去比照得悉。 - axios拦截器是能够累加的,每增加一个拦截器,就会返回一个对应的拦截器id,也就是无奈通过新增拦挡的形式笼罩或者扭转已有拦截器的配置。但能够利用拦截器id通过
axios.interceptors.request.eject(InterceptorId)
办法移除指定拦截器。 - 对于同一个axios对象,如果全局拦截器中设置了
CancelToken
属性,就无奈在独自的申请中再通过此属性勾销申请。移除全局拦截器能够解决这个问题,但又会有另一个问题,拦截器移除后就永远生效了,影响是全局的。 - axios中以别名的模式(axios.get、axios.post)发申请,不同的申请形式参数的写法是不一样的,次要是put/post/patch三种办法与其余不太一样
自定义申请配置
根目录下新建plugins/axios/index.js
文件,自定义axios的申请配置。
这里process.env.VUE_APP_BASEURL
是一个定义好的变量,值为"/webapi";
设置超时工夫timeout
为10s。如下:
import axios from 'axios'axios.defaults.baseURL = process.env.VUE_APP_BASEURLaxios.defaults.timeout = 10000axios.defaults.headers['custom-defined-header-key'] = 'custom-defined-header-value'// 自定义申请头:对所有申请办法失效axios.defaults.headers.common['common-defined-key-b'] = 'custom value: for all methods'// 自定义申请头:只对post办法失效axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';// 自定义申请头:只对get办法失效axios.defaults.headers.get['get-custom-key'] = 'custom value: only for get method';export default axios
"main.js"文件:
import request from '@/plugins/axios/index.js'Vue.prototype.$request = request
这样在组件内就能够通过this.$request(options)
或者this.$request.get(options)
的办法来申请数据了。
对常见的响应状况对立解决
这里次要是在"响应拦截器"中,对于一些常见的申请状态码和跟后端约定好的申请返回码做对立的前置解决。
新建axios.handleResponse.js
文件,用于解决常见的失常响应:
# axios.handleResponse.js// 解决响应错误码export default (response) => { const status = response.status // 如果http响应状态码response.status失常,则间接返回数据 if ((status >= 200 && status <= 300) || status === 304) { return response } // status不失常的话,依据与后端约定好的code,做出对应的提醒与解决 // 返回一个带有code和message属性的对象 else { const code = parseInt(response.data && response.data.code) // msg为服务端返回的错误信息,字段名自定义,此处以msg为例 let message = (response.data || {}).msg switch (code) { case 400: break case 4001: if (process.server) return message = message || '登录设施数量超出限度' // store.commit('savehttpResult', { res: response.data }) break case 403: message = message || '未登录' break case 404: message = message || '申请地址谬误' break case 412: message = message || '未找到无效session' break default: // message = message || err.response.data.msg break } return { code, message } }}
新建plugins/axios/axios.handleError.js
文件,用于解决常见的异样响应:
#plugins/axios/axios.handleError.js文件export default (err) => { const { response } = err if (!response.status) { err.code = '' err.message = '有response但没有response.status的状况' } err.code = response.status switch (response.status) { case 200: err.message = '谬误响应也会有状态码为200的状况' break case 400: err.message = '申请谬误(400)' break case 401: err.message = '未受权,请从新登录(401)' break case 403: err.message = '回绝拜访(403)' break case 404: err.message = '申请出错(404)' break case 408: err.message = '申请超时(408)' break case 500: err.message = '服务器谬误(500)' break case 501: err.message = '服务未实现(501)' break case 502: err.message = '网络谬误(502)' break case 503: err.message = '服务不可用(503)' break case 504: err.message = '网络超时(504)' break case 505: err.message = 'HTTP版本不受反对(505)' break default: err.message = `连贯出错,状态码:(${err.response.status})!` } return err}
plugins/axios/index.js
文件中引入并在拦截器中配置:
- 如果申请被勾销,会进入到响应拦截器的第二个参数err解决中
#plugins/axios/index.js文件import axios from 'axios'import handleResponse from '@/plugins/axios/axios.handleResponse.js'import handleError from '@/plugins/axios/axios.handleError.js'import { Message } from 'element-ui'const showTip = (tip)=>{ Message({ type: 'warning', message: tip || '申请出错啦', duration: 1500 })}/** * 申请拦挡 */axios.interceptors.request.use( (config) => { // 在发送申请之前做些什么,例如把用户的登录信息放在申请头上 // config.headers.common['cookie-id'] = cookieId return config }, (err) => { // 对申请谬误做些什么 Promise.reject(err) })/** * 响应拦挡 */axios.interceptors.response.use( (response) => { showTip(err.message) return Promise.resolve(handleResponse(response)), } // 对异样响应解决 (err) => { if (!err) return Promise.reject(err) if (err.response) { err = handleError(err) } // 没有response(没有状态码)的状况 // eg: 超时;断网;申请反复被勾销;被动勾销申请; else { // 错误信息err传入isCancel办法,能够判断申请是否被勾销 if (axios.isCancel(err)) { throw new axios.Cancel(err.message || `申请'${request.config.url}'被勾销`) } else if (err.stack && err.stack.includes('timeout')) { err.message = '申请超时!' } else { err.message = '连贯服务器失败!' } } showTip(err.message) return Promise.reject(err) })
到这里对于一些常见的响应,例如断网、未登录、登录信息生效、超时等,咱们能够申请拦截器中通过showTip
做出对立的ui提醒,就不必每次申请之后再反复得解决这些逻辑了。
接下来就是在"申请拦截器"中配置实现避免反复申请的性能。
避免反复申请
axios提供了两种勾销申请的办法:
咱们的避免反复申请思路:
在申请拦截器中,通过第二个种办法给每个申请定义cancelToken
属性,同时申明一个变量pendingPool
,用于并保留pending状态的申请及对应的cancelFn
。
在响应拦截器中,无论申请胜利了还是失败了,都通过api地址将这个申请从pendignPool
中删除。
而后每次发动申请前做一个判断,如果pendingPool
中没有这个申请,失常收回;如果已存在阐明以后申请还是pending状态,那么执行cancelFn
勾销以后反复的申请。
pendingPool
申明为Map类型的数据结构,能够不便得通过set
/has
/delete
等进行判断、删除等操作。key值为api地址,value值为一个对象,保留cancelFn
及global
(global
用于前面的路由切换勾销所有申请,能够临时疏忽)。pendingPool
的大略构造:Map {
'/home/banner' => { cancelFn: [Function: c], global: false }, '/login' => { cancelFn: [Function: c], global: false }
}
申请拦截器中能够拿到每个申请的配置信息config,增加cancelToken
属性:
#plugins/axios/index.js文件import axios from 'axios'// 申请中的apilet pendingPool = new Map()/** * 申请拦挡 */axios.interceptors.request.use( (config) => { // 对于异样的响应也须要在pendingPool中将其删除,但响应拦截器中的异样响应有些获取不到申请信息,这里将其保留在实例上 request.config = Object.assign({}, config) // 在发送申请之前做些什么,例如把用户的登录信息放在申请头上 // config.headers.common['cookie-id'] = cookieId config.cancelToken = new axios.CancelToken((cancelFn) => { pendingPool.has(config.url) ? cancelFn(`${config.url}申请反复`) : pendingPool.set(config.url, { cancelFn, global: config.global }) }) return config }, (err) => { console.log('申请拦挡err:', err) // 对申请谬误做些什么 Promise.reject(err) })
响应拦截器中对有后果(失常及异样)的申请进行删除:
#plugins/axios/index.js文件axios.interceptors.response.use( // 解决失常响应 (response) => { // 删除 const { config } = response pendingPool.delete(config.url) showTip(err.message) return Promise.resolve(handleResponse(response)) }, // 解决异样响应 (err) => { const { config } = request // 异样响应删除须要加一个判断:是否为申请被勾销的异样,如果不是才会将这个申请从pendingPool中删除。 // 否则会呈现一种状况:网速十分慢的状况下,在网速十分慢的状况下多次重复发送同一个申请,第一个申请还在pending状态中, // 第二个申请发不进来会间接被cancel掉进入到异样响应,而后从pendignPool中删除,第三次申请收回的时候就无奈正确判断这个申请是否还是pending状态会失常收回 if (!axios.isCancel(err)) pendingPool.delete(config.url) if (!err) return Promise.reject(err) if (err.response) { err = handleError(err) } // 没有response(没有状态码)的状况 // eg: 超时;断网;申请反复被勾销;被动勾销申请; else { // 错误信息err传入isCancel办法,能够判断申请是否被勾销 if (axios.isCancel(err)) { throw new axios.Cancel(err.message || `申请'${request.config.url}'被勾销`) } else if (err.stack && err.stack.includes('timeout')) { err.message = '申请超时!' } else { err.message = '连贯服务器失败!' } } // showTip(err.message) return Promise.reject(err) })
到这里就实现了避免反复申请的性能。如果同时收回多个雷同的申请,后面申请还在pending状态的状况下,前面收回的申请都会被主动勾销并reject到申请的catch解决中。
独自勾销指定申请
但理论利用中还有一种状况:须要手动勾销指定的某个申请,例如终止文件上传。依据文章结尾提到的个性[3]得悉,此时咱们是无奈独自勾销某个特定申请的。
又因为个性[1]咱们晓得,想要独自勾销指定申请,这个axios对象须要满足两个条件:1.申请拦截器中不能配置cancelToken
2. 这个axiso对象不能通过axios.create()
办法实例化生成。
所以解决思路的大略要点是:
- 须要两个axios对象别离解决申请防重和独自勾销特定申请
- 两个axiso对象的申请配置像
baseURL
等申请头信息须要是一样的 - 间接用axios对象(除了申请头信息,不做任何其余配置)发须要独自勾销的申请,这里申明为
intactRequest
- 通过
axios.create()
办法实例化生成一个新axios对象,做最欠缺的配置(避免反复申请、革除所有pending状态申请等、响应拦挡等),作为次要发申请的对象,这里申明为request
根目录下新建plugins/axios/axios.setConfig.js
文件,导出一个自定义axios默认配置的办法:
#"axios.setConfig.js"文件/** * @param {axios} axios实例 * @param {config} 自定义配置对象,可笼罩掉默认的自定义配置 */export default (axios, config = {}) => { const defaultConfig = { baseURL: process.env.VUE_APP_BASEURL, timeout: 10000, headers: { 'Content-Type': 'application/json;charset=UTF-8', 'custom-defined-header-key': 'custom-defined-header-value', // 自定义申请头:对所有申请办法失效 common: { 'common-defined-key-b': 'custom value: for all methods' }, // 自定义申请头:只对post办法失效 post: { 'post-custom-key': 'custom value: only for post method' }, // 自定义申请头:只对get办法失效 get: { 'get-custom-key': 'custom value: only for get method' } } } Object.assign(axios.defaults, defaultConfig, config) return axios}
批改plugins/axiso/index.js
文件,通过setConfig
办法生成两个具备雷同申请头信息的axios对象,并且对request
对象做申请防重、响应封装解决:
import axios from 'axios'import setConfig from '@/plugins/axios/axios.setConfig.js'/** * intactRequest是只在axios根底上更改了申请配置。 * 而request是基于axios创立的实例,实例只有常见的数据申请办法,没有axios.isCancel/ axios.CancelToken等办法, * 也就是没有**勾销申请**和**批量申请**的办法。 * 所以如果须要在实例中调用勾销某个申请的办法(例如勾销上传),请用intactRequest。 */let intactRequest = setConfig(axios)let request = setConfig(intactRequest.create())// 申请中的apilet pendingPool = new Map()/** * 申请拦挡 */request.interceptors.request.use( //...)/** * 响应拦挡 */request.interceptors.response.use( // ...)export { intactRequest, request }
批改main.js
文件,把两个对象都挂载到Vue示实例上:
import Vue from 'vue'import * as requests from '@/plugins/axios/index'Vue.prototype.$request = requests.requestVue.prototype.$intactRequest = requests.intactRequest
这样就实现了通过this.$requese
收回的反复申请能够主动被勾销掉,并且对立解决一些常见的响应;通过this.$intactRequest
收回的申请能够通过在申请中给config.cancelToken
设置”cancel token“来手动勾销。
一键革除所有pending状态申请
在路由切换时能够勾销以后仍在pending状态的申请从而优化性能、节约资源。
下面的pendingPool
曾经保留了所有pending状态的申请,封装一个办法,拿到其中每个申请而后执行cancelFn
,而后每次路由切换的时候执行这个办法即可。但不排除有些api申请是全局的不能被勾销。所以这个办法根底上新增白名单和申请的global
参数。
在plugins/axios/index.js
中新增clearPendingPool
办法,
/** * 革除所有pending状态的申请 * @param {Array} whiteList 白名单,外面的申请不会被勾销 * 返回值 被勾销了的api申请 */function clearPendingPool(whiteList = []) { if (!pendingPool.size) return // const pendingUrlList = [...pendingPool.keys()].filter((url) => !whiteList.includes(url)) const pendingUrlList = Array.from(pendingPool.keys()).filter((url) => !whiteList.includes(url)) if (!pendingUrlList.length) return pendingUrlList.forEach((pendingUrl) => { // 革除掉所有非全局的pending状态下的申请 if (!pendingPool.get(pendingUrl).global) { pendingPool.get(pendingUrl).cancelFn() pendingPool.delete(pendingUrl) } }) return pendingUrlList}request.clearPendingPool = clearPendingPool
在路由的配置文件src/router/idnex.js
中,引入request,并在路由全局前置守卫中执行clearPendingPool
办法:
import { request } from '@/plugins/axios/index'// 路由全局前置守卫router.beforeEach((to, from, next) => { // 路由变动时勾销以后所有非全局的pending状态的申请 request.clearPendingPool() next()})
到这里就实现了路由切换勾销pending状态的申请。能够通过两种形式指定某些api不被勾销:
- 执行
clearPendingPool
时传入一个白名单列表:
const globalApi = [ '/global/banner', '/global/activity']request.clearPendingPool(globalApi)
- 发动申请的时候携带
global
参数,默认为false:
this.$request.get('/global/banner',{ params:{page: 1}, global: true})this.$request.post('/user/login',{ name: 'xxx', pwd:'123456'},{ global: true})
移除拦截器
依据个性[2]晓得拦截器是能够累加也能够移除的。封装两个移除的全局拦挡的办法并挂载到request
对象上。目前看来还没有理论利用场景,只是做一下记录。
plugins/axios/index.js
文件中新增两个办法:
/** * 申请拦挡 */const requestInterceptorId = request.interceptors.request.use( // ... )/** * 响应拦挡 */const responseInterceptorId = request.interceptors.response.use( // ... ) // 移除全局的申请拦截器function removeRequestInterceptors() { request.interceptors.request.eject(requestInterceptorId)}// 移除全局的响应拦截器function removeResponseInterceptors() { request.interceptors.response.eject(responseInterceptorId)}request.removeRequestInterceptors = removeRequestInterceptorsrequest.removeResponseInterceptors = removeResponseInterceptors
通过this.$request.removeRequestInterceptors
和this.$request.removeResponseInterceptors
调用即可。
api治理
这里咱们把所有的api地址及对应的申请形式放在一起治理,而后组件中通过别名间接调用即可。
先具体理解一下个性[3]提到aixos不同的申请形式参数的写法问题:次要因为post
、put
、patch
三种办法相比其余办法多一个data
属性,也就是须要在申请体中携带的数据,其余办法会主动疏忽data
属性;而params
属性是所有办法都有的,与申请一起发送的 URL 参数。
axios发动申请大略有两种写法:
- 间接通过axios:
axios(config)
的模式 - 通过axios别名的模式:
axiso[method]()
通过别名的模式发动申请,post
、put
、patch
三种办法须要接管三个参数:axios[method](api, data, headersConfig)
,第二个参数data
就是须要在申请体中携带的参数;而且他办法只接管两个参数,不须要第二个参数data
。
根目录下新建一个src/api/index.js
文件:
/**api治理页面 * apiMap: 对立治理所有api地址、对应的申请形式及自定义别名 * 导出一个对象requestMap,属性名为apiMap中定义的别名,也就是调用时的名称,值为理论申请办法 * 办法接管两个对象参数,第一个为须要传递的数据,第二个为申请头的配置信息。 * 语法: api[alias](paramsOrData, headersConfig) * 第一个参数:如果为put/post/patch办法中的一种,会被转化为data属性;其余则是params * 第二个参数:申请头信息 * * let xx = await this.$api.getBanner({ account: '18038018084', psw: '2' }) * let vv = await this.$api.login({ account: '18038018084', psw: '2' }) * * 如果相似post的办法须要通过url后缀模式传递参数,在第二个参数config加上params属性即可: * let vv = await this.$api.login({ account: '18038018084', psw: '2' },{ params: {} }) * * 自定义申请头信息: * let xx = await this.$api.getBanner({}, {timeout: 1000, headers:{ aaa: 111 }}) */import { request } from '@/plugins/axios/index'// import qs from 'qs'// console.log('qs:', qs)const apiMap = { getBanner: { method: 'get', url: '/home/banner' }, login: { method: 'post', url: '/login' }}function injectRequest(apiObj) { const requestMap = {} Object.keys(apiObj).forEach((alias) => { let { method, url, config } = apiObj[alias] method = method.toUpperCase() requestMap[alias] = (dataOrParams = {}, instanceConf = {}) => { const keyName = ['PUT', 'POST', 'PATCH'].includes(method) ? 'data' : 'params' return request({ method, url, // [keyName]: method === 'POST' ? qs.stringify(dataOrParams) : dataOrParams, [keyName]: dataOrParams, ...Object.assign(config || {}, instanceConf) }) } }) return requestMap}export default injectRequest(apiMap)
在mains.js
中引入并挂载到vue实例:
import Vue from 'vue'import api from '@/api/index.js'Vue.prototype.$api = api
调用示例:
// 申请头信息const headersConfig = { timeout: 5000, global: true, headers:{ aaa: 'vvv' }}// get申请this.$api.getBanner({ page: 1,})// get申请,自定义申请头信息this.$api.getBanner({ page: 1,}, headersConfig)// post申请this.$api.login({ account: 'laowang', pwd: 'xxxx'})// post申请,自定义申请头信息this.$api.login({ account: 'laowang', pwd: 'xxxx'}, headersConfig)
总结
最终我的项目中相干文件的目录构造:
├── CHANGELOG.md├── README.md├── package.json├── src│ ├── api│ │ └── index.js // api治理│ ├── plugins│ │ ├── axios // axios封装│ │ │ ├── axios.handleError.js│ │ │ ├── axios.handleResponse.js│ │ │ ├── axios.setConfig.js│ │ │ └── index.js│ ├── router│ │ ├── index.js└── yarn.lock
src/plugins/axios/index.js
文件最终:
import axios from 'axios'import setConfig from '@/plugins/axios/axios.setConfig.js'import handleResponse from '@/plugins/axios/axios.handleResponse.js'import handleError from '@/plugins/axios/axios.handleError.js'// import store from '@/store/index'// import router from '@/router/index.js'import { Message } from 'element-ui'const showTip = (tip)=>{ Message({ type: 'warning', message: tip || '申请出错啦', duration: 1500 })}/** * intactRequest是只在axios根底上更改了申请配置。 * 而request是基于axios创立的实例,实例只有常见的数据申请办法,没有axios.isCancel/ axios.CancelToken等办法, * 也就是没有**勾销申请**和**批量申请**的办法。 * 所以如果须要在实例中调用勾销某个申请的办法(例如勾销上传),请用intactRequest。 */let intactRequest = setConfig(axios)let request = setConfig(intactRequest.create())// 申请中的apilet pendingPool = new Map()/** * 申请拦挡 */const requestInterceptorId = request.interceptors.request.use( (config) => { // 对于异样的响应也须要在pendingPool中将其删除,但响应拦截器中的异样响应有些获取不到申请信息,这里将其保留在实例上 request.config = Object.assign({}, config) // 在发送申请之前做些什么 // config.headers.common['cookie-id'] = cookieId config.cancelToken = new axios.CancelToken((cancelFn) => { pendingPool.has(config.url) ? cancelFn(`${config.url}申请反复`) : pendingPool.set(config.url, { cancelFn, global: config.global }) }) return config }, (err) => { console.log('申请拦挡err:', err) // 对申请谬误做些什么 return Promise.reject(err) })/** * 响应拦挡 */const responseInterceptorId = request.interceptors.response.use( (response) => { const { config } = response pendingPool.delete(config.url) // console.log('响应response suc:', response) showTip(err.message) return Promise.resolve(handleResponse(response)) }, // 对异样响应解决 (err) => { const { config } = request if (!axios.isCancel(err)) pendingPool.delete(config.url) if (!err) return Promise.reject(err) if (err.response) { err = handleError(err) } // 没有response(没有状态码)的状况 // eg: 超时;断网;申请反复被勾销;被动勾销申请; else { // 错误信息err传入isCancel办法,能够判断申请是否被勾销 if (axios.isCancel(err)) { throw new axios.Cancel(err.message || `申请'${request.config.url}'被勾销`) } else if (err.stack && err.stack.includes('timeout')) { err.message = '申请超时!' } else { err.message = '连贯服务器失败!' } } showTip(err.message) return Promise.reject(err) })// 移除全局的申请拦截器function removeRequestInterceptor() { request.interceptors.request.eject(requestInterceptorId)}// 移除全局的响应拦截器function removeResponseInterceptor() { request.interceptors.response.eject(responseInterceptorId)}/** * 革除所有pending状态的申请 * @param {Array} whiteList 白名单,外面的申请不会被勾销 * 返回值 被勾销了的api申请 */function clearPendingPool(whiteList = []) { if (!pendingPool.size) return // const pendingUrlList = [...pendingPool.keys()].filter((url) => !whiteList.includes(url)) const pendingUrlList = Array.from(pendingPool.keys()).filter((url) => !whiteList.includes(url)) if (!pendingUrlList.length) return pendingUrlList.forEach((pendingUrl) => { // 革除掉所有非全局的pending状态下的申请 if (!pendingPool.get(pendingUrl).global) { pendingPool.get(pendingUrl).cancelFn() pendingPool.delete(pendingUrl) } }) return pendingUrlList}request.removeRequestInterceptor = removeRequestInterceptorrequest.removeResponseInterceptor = removeResponseInterceptorrequest.clearPendingPool = clearPendingPoolexport { intactRequest, request }
限时秒杀阿里云服务器ECS、云数据库MySQL、对象存储OSS等多种代金券