封装 axios 请求大同小异,本文提供思路和源码,不要以偏概全,适合自己业务才是最重要的!
功能点实现
- 基于 Restful 风格进行封装
- 取消请求与拦截重复请求
- token 拦截
- 接口白名单
- token 自动登录(看自己个人业务是否需要,一般用于移动端)
- 导出封装 GET/POST/DELETE/PUT/PATCH/...请求函数
话不多说,万事总要开头,先来引入开头
import axios from 'axios'
1. 基于 Restful 风格进行封装
GET和POST明显的区别就是参数放置位置不同,前者放在
url?
后面,后者放在body
体内。
然鹅,Restful 风格在其基础添加了DELETE、PUT和PATCH请求类型,其中DELETE请求参数和GET一样放在
url?
后面,PUT、PATCH这两个的请求参数则是和POST一样放在body
体内。至于怎么放,看你们规范怎么定
至于想怎么改更请求参数放置位置,请继续往下看!
类型 | 请求参数放置位置 | 含义 |
---|---|---|
GET |
url? 后面 |
请求资源(查询用户) |
DELETE |
url? 后面 |
删除数据(删除用户) |
POST |
body 体内 |
创建数据(创建用户) |
PUT |
body 体内 |
更新全部数据(修改用户信息,昵称,签名,邮箱等等...全部更新) |
PATCH |
body 体内 |
更新部分数据(修改用户状态) |
先创建一个axios
实例,然后为其赋值默认参数。
const service = axios.create({
// 设置默认请求头
// 比如 BASE_API = 'api'
// 那么访问就是 {你当前的域名}/api/{接口地址} http://localhost:8080/api/users
baseURL: process.env.BASE_API,
// 这里因为我后台把 http状态码 跟 定制返回体状态码 看为一样的,所以 http.status === result.status
validateStatus: status => status < 500, // 拦截状态码大于500
// 修改默认请求头,采用json格式传输,对数组对象极其方便,不用专门下qs格式化参数
// common对应的是参数放在请求url上(get/delete),patch/post/put参数放在body体内
// 这里对应http Request Header.Accept
headers: {
common: { Accept: 'application/json; charset=UTF-8' },
patch: { 'Content-Type': 'application/json; charset=UTF-8' },
post: { 'Content-Type': 'application/json; charset=UTF-8' },
put: { 'Content-Type': 'application/json; charset=UTF-8' }
},
timeout: 60000 // 请求超时时间
})
2. 取消请求与拦截重复请求
官方原话:Axios 的 cancel token API 基于cancelable promises proposal,它还处于第一阶段。
使用
axios.CancelToken
进行拦截
// 每次请求都会记录在此对象里,用于判断是否重复
const pending = {}
// axios.CancelToken
const { CancelToken } = axios
const paramsList = ['get', 'delete']
const dataList = ['post', 'put', 'patch']
// 区分参数位置
const isTypeList = method => {
if (paramsList.includes(method)) {
return 'params'
} else if (dataList.includes(method)) {
return 'data'
}
}
/**
* 获取请求唯一值(key)
* @param {Object} config - axios拦截器的config
* @param {Boolean} isResult - 截取url唯一,这里区别请求前和请求后,因为前者和后者的url不同,所以需要区分一下
*/
function getRequestIdentify(config, isResult = false) {
const url = !isResult
? config.url
: config.baseURL + config.url.substring(1, config.url.length)
const params = { ...(config[isTypeList(config.method)] || {}) }
delete params.t
return encodeURIComponent(url + JSON.stringify(params))
}
/**
* 每次请求前 清除上一个跟它相同的还在请求中的接口
* @param {String} key - url唯一值
* @param {Boolean} isRequest - 是否执行取消重复请求
*/
function removePending(key, isRequest = false) {
if (pending[key] && isRequest) {
pending[key]('取消重复请求')
}
delete pending[key]
}
请求前的 url
请求后的 url
在拦截器里面设置
// 请求前
service.interceptors.request.use(
config => {
// 获取该次请求的唯一值
const requestData = getRequestIdentify(config, true)
// 删除上一个相同的请求
removePending(requestData, true)
// 实例化取消请求,并同时注入pending
config.cancelToken = new CancelToken(c => {
pending[requestData] = c
})
return config
},
error => {
Promise.reject(error)
}
)
// 请求完成后
service.interceptors.response.use(
response => {
// 把已经完成的请求从 pending 中移除
const requestData = getRequestIdentify(response.config)
removePending(requestData)
// ...
},
error => {
// ...
}
)
3. token 拦截
token
是比较常见的前后端校验拦截方式
// 引入store
import store from 'store'
service.interceptors.request.use(config => {
// ...上面的代码
const token = store.getters.GET_TOKEN
if (!token) {
// 取消该次请求
pending[requestData]('cancelToken')
store.dispatch('logOut')
} else {
// 在业务约定的headers里面某个字段定义token,方便后端提取校验
config.headers['Authorization'] = token
}
return config
})
4. 接口白名单
所谓白名单,就是不用任何校验权限(token)的接口,比如用户登录、用户注册、修改用户密码等等
// 定义接口白名单
const noLogin = [
// ...
'/login',
'/register'
]
service.interceptors.request.use(config => {
// ...上面代码
if (!noLogin.includes(config.url.replace(config.baseURL, ''))) {
// 当不在白名单内时则校验token
// ...上面代码
}
return config
})
5. token 自动登录
通过每次请求完成后,如果该请求返回的是登录失效(
401
),则用一个数组装在该次的返回对象config
。
然后重启发起该获取 token 请求,完成后把数组 map 重新发起一次请求,然后清除数组
前提是把账号密码记录存储在浏览器或者有刷新 token 接口
准备好以下这些方法
// 是否正在刷新的标记
const isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
const retryRequests = []
// 重新请求流程处理
async function reRequest(response) {
const { config } = response
if (!isRefreshing) {
isRefreshing = true
const reRes = await refreshTokenFn()
if (reRes) {
config.headers['Authorization'] = store.getters.GET_TOKEN
// 已经刷新了token,将所有队列中的请求进行重试
retryRequests.map(cb => cb(store.getters.GET_TOKEN))
// 清空列队
retryRequests = []
config.baseURL = ''
isRefreshing = false
return service(config)
} else {
// 刷新token失败,跳回登录页
store.dispatch('logOut')
}
isRefreshing = false
} else {
return new Promise(resolve => {
// 将resolve放进列队,用一个函数形式保存,等token刷新后直接执行
retryRequests.push(token => {
config.baseURL = ''
config.headers['Authorization'] = token
resolve(service(config))
})
})
}
}
// 刷新token方法
async function refreshTokenFn() {
try {
const { loginName, password } = store.getters.GET_USER_INFO
// 自己定义的获取方法
const res = await store.dispatch('getToken', { loginName, password })
return res
} catch (error) {
return false
}
}
使用
// 请求前
service.interceptors.request.use(config => {
const token = store.getters.GET_TOKEN
if (!token) {
// 当token不存在时,自动重新发起请求
return reRequest({ config })
} else {
config.headers['Authorization'] = token
}
return config
})
service.interceptors.response.use(response => {
const res = response.data
if (res.status === 401) {
// 登录失效
// 重新刷新token并发起请求
return reRequest(response)
}
return data
})
6. 导出封装 GET/POST/DELETE/PUT/PATCH/...请求函数
自定义请求参数位置,在使用实例好的
service(params)
时,通过传参数的 params 或者 data 决定放置位置
如果有传参数,则会覆盖默认的
参数 | 含义 | 默认值 |
---|---|---|
url | 请求地址 | —— |
method | 请求类型,有 GET/POST/DELETE/PUT/PATCH 可选 | GET |
params | 请求参数在url? 后面时,则把参数放在 params,适用于 GET/DELETE |
null |
data | 请求参数在body 体内时,则把参数放在 data 里,适用于 POST/PUT/PATCH |
null |
headers | 请求头,跟axios.create({ headers }) 一样 |
—— |
responseType | 服务器响应的数据类型,可选'arraybuffer', 'blob', 'document', 'json', 'text', 'stream' | json |
/**
* get请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} params - 请求参数
* @returns
*/
export const get = (url, params = {}) => {
params.t = new Date().getTime() // get方法加一个时间参数,解决ie下可能缓存问题.
return service({
url: url,
method: 'GET',
params
})
}
/**
* delete请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} params - 请求参数
* @returns
*/
export const del = (url, params = {}) => {
params.t = new Date().getTime() // get方法加一个时间参数,解决ie下可能缓存问题.
return service({
url: url,
method: 'DELETE',
params
})
}
/**
* post请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} data - 请求参数
* @returns
*/
export const post = (url, data = {}) => {
return service({
url,
method: 'POST',
data
})
}
/**
* put请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} data - 请求参数
* @returns
*/
export const put = (url, data = {}) => {
return service({
url,
method: 'PUT',
data
})
}
/**
* patch请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} data - 请求参数
* @returns
*/
export const patch = (url, data = {}) => {
return service({
url,
method: 'PATCH',
data
})
}
/**
* 当以上方法不满足,则自定义参数和配置请求
* @param {Object} options
*/
export const fetch = options => service(options)
源码
import axios from 'axios'
import store from 'store'
import { Message, MessageBox } from 'element-ui'
// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API,
validateStatus: status => status < 500, // 拦截状态码大于500
headers: {
common: { Accept: 'application/json; charset=UTF-8' },
patch: { 'Content-Type': 'application/json; charset=UTF-8' },
post: { 'Content-Type': 'application/json; charset=UTF-8' },
put: { 'Content-Type': 'application/json; charset=UTF-8' }
},
timeout: 60000 // 请求超时时间
})
const paramsList = ['get', 'delete', 'patch']
const dataList = ['post', 'put']
const isTypeList = method => {
if (paramsList.includes(method)) {
return 'params'
} else if (dataList.includes(method)) {
return 'data'
}
}
const pending = {}
const CancelToken = axios.CancelToken
const removePending = (key, isRequest = false) => {
if (pending[key] && isRequest) {
pending[key]('取消重复请求')
}
delete pending[key]
}
const getRequestIdentify = (config, isResult = false) => {
let url = config.url
if (isResult) {
url = config.baseURL + config.url.substring(1, config.url.length)
}
const params = { ...(config[isTypeList(config.method)] || {}) }
delete params.t
return encodeURIComponent(url + JSON.stringify(params))
}
// 不需要token的接口
const noLogin = [
'/account/random',
'/account/token',
'/account/defaultPassword',
'/account/changeDefaultPassword'
]
// 是否正在刷新的标记
// const isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
// const retryRequests = []
// request拦截器
service.interceptors.request.use(
config => {
const requestData = getRequestIdentify(config, true)
removePending(requestData, true)
config.cancelToken = new CancelToken(c => {
pending[requestData] = c
})
if (!noLogin.includes(config.url.replace(config.baseURL, ''))) {
const token = store.getters.GET_TOKEN
if (!token) {
// 当token不存在时,自动重新发起请求
// return reRequest({ config })
// 取消该次请求
pending[requestData]('cancelToken')
store.dispatch('logOut')
} else {
config.headers['Authorization'] = token
}
}
// 处理为空的参数,设置为null
handlerNullParams(config)
return config
},
error => {
Promise.reject(error)
}
)
// response拦截器
service.interceptors.response.use(
response => {
// 把已经完成的请求从 pending 中移除
const requestData = getRequestIdentify(response.config)
removePending(requestData)
const res = response.data
if (res.status === 401) {
// 登录失效
MessageBox.alert('登录失效,请重新登录', '权限提示', {
confirmButtonText: '退出',
callback: () => store.dispatch('logOut')
})
// 重新刷新token并发起请求
// return reRequest(response)
} else if (res.status !== 200) {
Message.error(res.msg || '系统异常')
return Promise.reject(res.msg || '系统异常')
}
return res
},
error => {
if (
!(
error &&
(error.message === '取消重复请求' ||
~error.message.indexOf('cancelToken'))
)
) {
if (error.code === 'ECONNABORTED') {
Message.error('请求超时')
} else if (error && error.response) {
// error.response.status
Message.error(error.response.data.msg || '系统异常')
} else {
Message.error('系统异常')
console.log(error)
}
}
return Promise.reject(error)
}
)
export default service
// 重新请求流程处理
// async function reRequest(response) {
// const { config } = response
// if (!isRefreshing) {
// isRefreshing = true
// const reRes = await refreshTokenFn()
// if (reRes) {
// config.headers['Authorization'] = store.getters.GET_TOKEN
// // 已经刷新了token,将所有队列中的请求进行重试
// retryRequests.map(cb => cb(store.getters.GET_TOKEN))
// // 清空列队
// retryRequests = []
// config.baseURL = ''
// isRefreshing = false
// return service(config)
// } else {
// // 刷新token失败,跳回登录页
// store.dispatch('logOut')
// }
// isRefreshing = false
// } else {
// return new Promise((resolve) => {
// // 将resolve放进列队,用一个函数形式保存,等token刷新后直接执行
// retryRequests.push(token => {
// config.baseURL = ''
// config.headers['Authorization'] = token
// resolve(service(config))
// })
// })
// }
// }
// // 刷新token方法
// async function refreshTokenFn() {
// try {
// const { loginName, password } = store.getters.GET_USER_INFO
// const res = await store.dispatch('getToken', { loginName, password })
// return res
// } catch (error) {
// return false
// }
// }
/**
* get请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} params - 请求参数
* @returns
*/
export const get = (url, params = {}) => {
params.t = new Date().getTime() // get方法加一个时间参数,解决ie下可能缓存问题.
return service({
url: url,
method: 'GET',
params
})
}
/**
* delete请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} params - 请求参数
* @returns
*/
export const del = (url, params = {}) => {
params.t = new Date().getTime() // get方法加一个时间参数,解决ie下可能缓存问题.
return service({
url: url,
method: 'DELETE',
params
})
}
/**
* post请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} data - 请求参数
* @returns
*/
export const post = (url, data = {}) => {
return service({
url,
method: 'POST',
data
})
}
/**
* put请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} data - 请求参数
* @returns
*/
export const put = (url, data = {}) => {
return service({
url,
method: 'PUT',
data
})
}
/**
* patch请求方法
* @export axios
* @param {String} url - 请求地址
* @param {Object} data - 请求参数
* @returns
*/
export const patch = (url, data = {}) => {
return service({
url,
method: 'PATCH',
data
})
}
/**
* post上传文件请求方法
* !! 必须使用formData方式
* @export axios
* @param {String} url - 请求地址
* @param {Object} data - 请求参数
* @returns
*/
export const postFile = (url, data = {}) => {
return service({
url,
method: 'POST',
data,
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 1000 * 60 * 3
})
}
/**
* get导出文件
* @export axios
* @param {String} url - 请求地址
* @param {Object} data - 请求参数
* @returns
*/
export const getExport = (url, params = {}) => {
return service({
url,
method: 'GET',
params,
responseType: 'blob',
timeout: 1000 * 60 * 3
})
}
/**
* 当以上方法不满足,则自定义参数和配置请求
* @param {Object} options
*/
export const fetch = options => service(options)
结语
广州找工作img...简历仿佛入了海底一下...