赞
踩
Axios 是一个基于 Promise 的 HTTP 库,它的概念及使用方法本文不过多赘述,请参考:axios传送门
本文重点讲述下在项目中是如何利用 axios 封装 http 请求。
在 /const/preset.js 中配置预先设置一些全局变量
window.$env = process.env.NODE_ENV === 'development' ? 'DEV' : 'PROD' // 默认开发环境 let config = { baseURL: location.origin, httpBaseURL: location.origin + '/api', webBaseURL: location.origin + location.pathname, vipAddress: '/necp/mapp/sc', // 后端微服务的统一入口 } // 生产环境 if (window.$env !== 'DEV') { if (location.href.indexOf('/ecs/') > -1) { config.baseURL = location.href.replace(/\/ecs.+/, '') config.httpBaseURL = config.baseURL } } // 文件资源请求路径 config.fileUrl = config.httpBaseURL + config.vipAddress + 'file/download' window.$globals = config
在 main.js 中引入
import Vue from 'vue'
import './const/preset'
// ...
// 把 vue 示例挂载到 window 下
window.$vm = new Vue({
render: h => h(App),
router
}).$mount('#app')
因为生产环境部署的差异,http 请求的 baseURL 并非都是统一的,所以不单独配置默认的 axios.defaults.baseURL,而是通过此文件预设的变量进行设置。
全局预设变量中的 config.httpBaseURL 将添加到请求的 URL 中,对于代码中的 location.href.indexOf(‘/ecs/’) > -1 判断只是举例,可根据实际需求决定是否需要。
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'
axios.defaults.timeout = 60000
axios.defaults.crossDomain = true
此三条配置分别对应以下作用:
提示:覆盖默认超时时间,可在 axios 发送请求的参数 config 对象中设置 timeout 属性即可
请求拦截器是在发送请求前执行的函数,它可以用于修改请求的配置或者在请求发送前进行一些操作。最常用的功能就是使用请求拦截器实现身份验证。
一个常见的实现是用户登录之后,服务端会响应用户的登录信息,并且把用户的身份认证 token 存储到 cookie 中,然后在请求拦截器中将 cookie 中获取到的 token 设置到请求头中,每次发送请求都会携带上此 token 发送到服务端,服务端再获取请求头的 token 来判断用户是否登录状态或者登录已过期,作出不同的响应。
axios.interceptors.request.use(
config => {
const token = cookie.get(TOKEN_COOKIE_KEY)
if (token) {
config.headers[TOKEN_REQ_KEY] = token
}
return config
},
error => {
return Promise.reject(error)
}
)
响应拦截器是在接收到响应后执行的函数,它可以用于修改响应的数据或者在接收到响应后进行一些操作。
响应拦截器主要作用包括修改响应数据、错误处理、统一处理响应等功能,因把响应数据及错误的处理都放在了发送请求的回调中,所以只定义了最简单的响应拦截器。
axios.interceptors.response.use(response => {
return response
}, error => {
return Promise.reject(error)
})
此函数接收四个参数:请求方法,请求的 api 接口,请求参数,请求的 config 配置项,返回一个 Promise 的实例。此函数完成了正常响应处理、异常处理、重复请求取消等功能。
const apiInterceptor = api => { if (api.startsWith('http')) { // 自定义请求路径 return api.slice(4) } if (api.startsWith('_SC_')) { // 项目统一的api前缀 api = $globals.vipAddress + api.slice(4) } return $globals.httpBaseURL + api } const request = async (method = 'post', api, params = {}, config = {}) => { // 省略... let url = apiInterceptor(api) let opts = { method, url, headers: config.headers || {}, withCredentials: config.withCredentials || true // 跨域请求时是否需要使用凭证 } // 省略... }
调用 apiInterceptor 函数来拼接完整的请求 url,如果 api 是以 http 开头,则表示自定义 api 的请求路径,否则请求路径使用 preset.js 中预设的全局变量来拼接完整的 url。
const jsonObj2FormData = jsonObj => { let formData = new FormData() Object.keys(jsonObj).forEach(key => { if (jsonObj[key] instanceof Array) { formData.append(key, JSON.stringify(jsonObj[key])) } else { formData.append(key, jsonObj[key]) } }) return formData } // 省略... if (config.formDataFormat) { opts.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8' params = jsonObj2FormData(params) } if (method == 'post') { opts.data = params } else { opts.params = params }
使用 axios(opts) 发起请求,得到的是一个 Promise,在 then 的第一个参数中传入一个正常的响应处理函数,这个函数接收响应拦截器中返回的 response 作为参数。
return new Promise((resolve, reject) => { axios(opts).then(response => { let res = response.data if (config.customHandler) { // 自定义响应处理 if (config.responseAll) return resolve(response) return resolve(res) } if (res) { if (res.code === 000) { // 登录超时 $vm.$toast.error(res.message) $vm.$store.dispatch('REMOVE_USER') // 移除 cookie、session、storage 存储的信息 reject(res.message) if (window.self === window.top) { $vm.$router.push('/login') // 跳转登录页 } } else if (res.code === 200) { resolve(res.data) } else { $vm.$toast.error(res.message || '接口异常, 请稍后重试') reject(res) } } else { $vm.$toast.error('接口无返回内容') } }) })
提示:$vm 指向全局的 Vue 实例,$toast 则是将 element 的 Message 组件实例挂载到了 Vue 的原型上
config.customHandler = true
,表示自定义响应处理,并且 config.responseAll = true
时,会把响应拦截器中得到的 response 直接返回,这个参数主要用于调用服务端响应字节流的接口时使用。异常处理在 axios(opts).then() 的第二个参数中传入处理函数,这个函数接收响应拦截器中返回的 Promise.reject(error) 作为参数。
异常处理主要针对 http 响应状态码不等于 200 的情况,包括常见的请求超时,404请求资源不存在,50X 服务器异常等情况。
axios(opts).then(response => { // 省略... }, error => { // 如果自定义处理 if (config.customHandler) { reject(error) return } // 请求超时 if (error.code == 'ECONNABORTED' && error.message.indexOf('timeout') > -1) { $vm.$toast.error(`请求超时,接口地址:${url}`) reject(error) return } if (error.response) { // 401未登录或登录失效 if (error.response.status === 401) { reject(error) if (window.self === window.top) { $vm.$router.push('/login') } return } switch (error.response.status) { case 404: $vm.$toast.error(`请求的资源不存在,异常服务接口地址:${url}`) break case 408: $vm.$toast.error('请求超时') break case 500: $vm.$toast.error('服务异常') break case 502: $vm.$toast.error(error.message || '服务未响应') break case 503: $vm.$toast.error(error.message || '服务暂不可访问') break default: $vm.$toast.error(error.response.statusText || '服务异常, 请稍后重试') } } else { $vm.$toast.error(error.response.statusText || '未知错误, 请稍后重试') } reject(error) })
在一些特定情况下,比如用户快速点击提交表单,短时间内同时触发同一个请求多次,我们可以借助 axios.cancelToken 来取消前几次请求,只保留最后一次请求。
主要实现的原理如下:
config.cancelTokenWidthParams = true
,时,在 key 后面拼接 JSON.stringify(params) 作为 key。const CANCEL_TOKEN = axios.CancelToken const HTTP_CANCEL_MAP = $globals.httpCancelMap = new Map() const IS_CANCELED_MSG = 'canceled' const checkHttpCancel = reqKey => { HTTP_CANCEL_MAP.forEach((v, k) => { if (k.slice(0, -14) === reqKey) { v() HTTP_CANCEL_MAP.delete(k) } }) } const request = async (method = 'post', api, params = {}, config = {}) => { let reqKey = method + api + JSON.stringify(config) if (config.cancelTokenWidthParams) reqKey += JSON.stringify(params) let reqUniqueKey = reqKey + '_' + new Date().getTime() checkHttpCancel(reqKey) // 省略... opts.cancelToken = new CANCEL_TOKEN(c => HTTP_CANCEL_MAP.set(reqUniqueKey, c)) // ... axios(opts).then(response => { HTTP_CANCEL_MAP.delete(reqUniqueKey) // ... }, error => { HTTP_CANCEL_MAP.delete(reqUniqueKey) if (axios.isCancel(error)) { reject(new Error(IS_CANCELED_MSG)) return } // ... }) })
注意
- 此项目使用的 axios 版本为 0.21.1,从 v0.22.0 开始,Axios 支持以 fetch API 方式—— AbortController 取消请求,CancelToken API被弃用
- 可以使用同一个 cancel token 取消多个请求
import axios from 'axios' import { TOKEN_REQ_KEY, TOKEN_COOKIE_KEY } from '@/const/common' import { session, cookie, jsonObj2FormData } from '@/util/common' axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8' axios.defaults.timeout = 120000 axios.defaults.crossDomain = true axios.interceptors.request.use( config => { const token = cookie.get(TOKEN_COOKIE_KEY) if (token) { config.headers[TOKEN_REQ_KEY] = token } return config }, error => { return Promise.reject(error) } ) axios.interceptors.response.use(response => { return response }, error => { return Promise.reject(error) }) const CANCEL_TOKEN = axios.CancelToken const HTTP_CANCEL_MAP = $globals.httpCancelMap = new Map() const IS_CANCELED_MSG = 'canceled' const checkHttpCancel = reqKey => { HTTP_CANCEL_MAP.forEach((v, k) => { if (k.slice(0, -14) === reqKey) { v() HTTP_CANCEL_MAP.delete(k) } }) } const apiInterceptor = api => { if (api.startsWith('http')) { // 自定义请求路径 return api.slice(4) } if (api.startsWith('_SC_')) { // 项目统一的api前缀 api = $globals.vipAddress + api.slice(4) } return $globals.httpBaseURL + api } const request = async (method = 'post', api, params = {}, config = {}) => { let reqKey = method + api + JSON.stringify(config) if (config.cancelTokenWidthParams) reqKey += JSON.stringify(params) let reqUniqueKey = reqKey + '_' + new Date().getTime() checkHttpCancel(reqKey) return new Promise((resolve, reject) => { if (config.loading) $vm.$loading.show() let url = apiInterceptor(api) let opts = { method, url, headers: config.headers || {}, withCredentials: config.withCredentials || true // 跨域请求时是否需要使用凭证 } if (config.formDataFormat) { opts.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8' params = jsonObj2FormData(params) } if (config.timeout) opts.timeout = config.timeout if (config.extends) opts = Object.assign(opts, config.extends) // 如果有并列层级的参数扩展 if (method == 'post') { opts.data = params } else { opts.params = params } opts.cancelToken = new CANCEL_TOKEN(c => HTTP_CANCEL_MAP.set(reqUniqueKey, c)) if (config.responseType) opts.responseType = config.responseType // 发起 axios 请求 axios(opts).then(response => { HTTP_CANCEL_MAP.delete(reqUniqueKey) if (config.loading) $vm.$loading.close() let res = response.data if (config.customHandler) { // 自定义响应处理 if (config.responseAll) return resolve(response) return resolve(res) } if (res) { if (res.code === 000) { // 登录超时 $vm.$toast.error(res.message) $vm.$store.dispatch('REMOVE_USER') // 移除 cookie、session、storage 存储的信息 reject(res.message) if (window.self === window.top) { $vm.$router.push('/login') // 跳转登录页 } } else if (res.code === 200) { resolve(res.data) } else { $vm.$toast.error(res.message || '接口异常, 请稍后重试') reject(res) } } else { $vm.$toast.error('接口无返回内容') } }, error => { HTTP_CANCEL_MAP.delete(reqUniqueKey) if (axios.isCancel(error)) { reject(new Error(IS_CANCELED_MSG)) return } if (config.loading) $vm.$loading.close() // 如果自定义处理 if (config.customHandler) { reject(error) return } // 请求超时 if (error.code == 'ECONNABORTED' && error.message.indexOf('timeout') > -1) { $vm.$toast.error(`请求超时,接口地址:${url}`) reject(error) return } if (error.response) { // 401未登录或登录失效 if (error.response.status === 401) { reject(error) if (window.self === window.top) { $vm.$router.push('/login') } return } switch (error.response.status) { case 404: $vm.$toast.error(`请求的资源不存在,异常服务接口地址:${url}`) break case 408: $vm.$toast.error('请求超时') break case 413: $vm.$toast.error('请求实体大小超过服务器最大限制') break case 500: $vm.$toast.error('服务异常') break case 502: $vm.$toast.error(error.message || '服务未响应') break case 503: $vm.$toast.error(error.message || '服务暂不可访问') break default: $vm.$toast.error(error.response.statusText || '服务异常, 请稍后重试') } } else { $vm.$toast.error(error.response.statusText || '未知错误, 请稍后重试') } reject(error) }) }) } export default { get: (api, params = {}, config = {}) => { return request('get', api, params, config) }, post: (api, params = {}, config = {}) => { return request('post', api, params, config) }, image: id => { return `${$globals.fileUrl}?fileId=${id}` }, isCanceled: error => { if (error && error.message === IS_CANCELED_MSG) return true return false } }
/plugins/http/install.js
import httpService from '@/service/http'
export default {
install: Vue => {
Vue.prototype.$http = httpService
}
}
例如,根据业务可划分为文档,评论等模块,在 service 目录下分别创建对应的模块存放 api 的 js 文件,对 api 进行统一管理。
强烈建议给每个 api 备注功能,提高可维护性
/service/comment.js
/**
* @name 获取评论列表
* @param {Object} params 请求参数对象
*/
export const getCommentListPromise = params => {
params = Object.assign({
page: 0, // 页码
pageSize: 5, // 每页数量
}, params)
return $vm.$http.get('_SC_/comment/findCommentList', params)
}
在 Comment.vue 页面中使用
import { getCommentListPromise } from '@/service/comment'
async findCommentList() {
const data = await getCommentListPromise()
console.log(data)
}
本文主要讲述了如何使用 axios 进行 http 封装的详细过程,及在项目中如何使用封装的 http 请求,请求拦截器和响应拦截器都是比较简单,没有处理很多的逻辑,逻辑处理基本是集中在 request 函数中。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。