目标
封装的意义在于使用的时候尽量方便。
此次封装的目标,是使用尽量简单的配置完成接口的请求。
思来想去,决定使用以下配置:
// 配置介绍
[调用方法名]:{
type: '', // 请求方式
url: '', // 请求url
autoCancel: Boolean, // 重复频繁请求时是否取消上次请求
headers: {}, // 请求头配置
... // axios支持的其他配置
}
// 例如
const api = {
getUserInfo: {
type: 'get',
url: '/common/xxx'
},
saveSomething: {
type: 'post',
url: '/common/xxx',
autoCancel: true
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
}
使用的时候,直接这样调用即可:
import api from 'api'
api.getUserInfo(data).then(() => {
// ...
})
开始封装
其中 1. 请求及返回拦截 2. 处理异常 这两块内容基本都大同小异,简单快速浏览即可
api/axios.js里部分代码借鉴自掘友,由于时间较长找不见原文章,如有知道的可以联系添加原文链接
请求及返回拦截
// api/axios.js
// 创建axios实例
let instance = axios.create({timeout: 1000 * 20})
// 设置post请求头
instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
/**
* 请求拦截器
* 每次请求前,如果存在token则在请求头中携带token
*/
instance.interceptors.request.use(
config => {
// 登录流程控制中,根据本地是否存在token判断用户的登录情况
// 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token
// 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码
// 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作。
// const token = store.state.token
// token && (config.headers.Authorization = token)
if (config.data && config.headers && config.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
config.data = qs.stringify(config.data, { allowDots: true })
}
return config
},
error => Promise.error(error)
)
// 响应拦截器
instance.interceptors.response.use(
// 请求成功
res => res.status === 200 ? Promise.resolve(res) : Promise.reject(res),
// 请求失败
error => {
const { response } = error
if (response) {
// 请求已发出,但是不在2xx的范围
errorHandle(response.status, response.data.message)
return Promise.reject(response)
} else {
// 处理断网的情况
// eg:请求超时或断网时,更新state的network状态
// network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
// 关于断网组件中的刷新重新获取数据,会在断网组件中说明
if (!window.navigator.onLine) {
store.commit('changeNetwork', false)
} else {
return Promise.reject(error)
}
}
}
)
处理异常
// api/axios.js
/**
* 请求失败后的错误统一处理
* @param {Number} status 请求失败的状态码
*/
const errorHandle = (status, other) => {
// 状态码判断
switch (status) {
// 401: 未登录状态,跳转登录页
case 401:
toLogin()
break
// 403 token过期
// 清除token并跳转登录页
case 403:
tip('登录过期,请重新登录')
localStorage.removeItem('token')
store.commit('loginSuccess', null)
setTimeout(() => {
toLogin()
}, 1000)
break
// 404请求不存在
case 404:
tip('请求的资源不存在')
break
default:
console.log(other)
}
}
其中的tip()是全局提示的方法,如下:
// api/axios.js
import { Toast } from 'vant'
/**
* 提示函数
* 禁止点击蒙层、显示一秒后关闭
*/
const tip = msg => {
Toast({
message: msg,
duration: 1000,
forbidClick: true
})
}
对外暴露方法
// api/axios.js
/**
* 请求数据的方法,所有请求都调用此方法
* @param {string} type 请求方法,默认为get
* @param {string} url 请求url
* @param {object} data post时的数据
* @param {object} params get时的数据
* @param {object} headers 请求头配置
* @param {object} config 其他axios配置
* @param {boolean} autoCancel 是否自动取消请求(取消重复请求)
* @returns {Promise<any>}
*/
function request ({type = 'get', url, data, params, headers, config = {}, autoCancel}) {
type = type.toLowerCase()
// get请求时,如果params没值,则取data(防止用户使用参数错误)
if (type === 'get') {
params = params || data
data = undefined
}
if (autoCancel) {
let cc = window.cancelApiMap[url]
cc && cc.cancel('cancel repeat request') // 如果已有则取消
let newcc = axios.CancelToken.source()
window.cancelApiMap[url] = newcc
config.cancelToken = newcc.token // 重新添加cancelToken标志
}
return new Promise((resolve, reject) => {
instance({
method: type,
url,
data,
params,
headers,
...config
}).then(({data}) => {
// 请求成功,删除cancel标记
window.cancelApiMap[url] && (delete window.cancelApiMap[url])
resolve(data)
// data.success ? resolve(data) : reject(data)
// if (data.success) {
// resolve(data)
// } else {
// reject(data)
// }
}).catch(error => {
reject(error)
})
})
}
export default request
支持取消页面内全部请求
有时候,切换页面时我们需要清空当前页面内还未完成的请求,添加以下代码即可
// api/axios.js
// 设一个开关,开启或关闭取消页面内全部请求的功能
const API_CANCEL_PAGE = true
// 在请求拦截器里添加
if (API_CANCEL_PAGE) {
let newcc = axios.CancelToken.source()
window.cancelApiMap[config.url] = newcc
config.cancelToken = newcc.token
}
// 导出一个方法
// 遍历我们上边存储cancelToken信息的对象,分别调用cancel方法,再将对象清空
export function cancelAllApi () {
Object.values(window.cancelApiMap).forEach(api => {
api.cancel ? api.cancel('cancel all api') : api('cancel all api')
})
window.cancelApiMap = {}
}
引入接口配置文件
我们项目中一般都是分模块进行编写接口的,这里对模块接口配置文件进行规范化,
统一使用[模块名].api.js的写法,方便后续的配置。如:common.api.js、home.api.js等,
下边给出一个例子。
// api/home.api,js
export default {
getTime: {
type: 'get',
url: '/common/get_time'
},
getXkMode: {
type: 'post',
url: '/common/get_xk_mode.do',
autoCancel: true
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
// }
}
}
引入的时候,使用webpack提供的require.context()方法直接引入,
避免每次增删接口配置文件时手动增删引入。
// api/index.js
let apiObj = {}
function dealFileName (str, replaceStr) {
return str.slice(2).replace(replaceStr, '')
}
/**
* 导入定义的接口配置
* @param files
*/
function importFile (files) {
files.keys().forEach(name => {
apiObj[dealFileName(name, '.api.js')] = files(name).default
})
}
//require.context的使用方法此处不做详细介绍
importFile(require.context('.', true, /\.api\.js$/))
此时apiObj的内容如下:
apiObj: {
common: {
getUserInfo: {
type: 'get',
url: 'xxx'
}
},
home: {
getCommonInfo: {
type: 'get',
utl: 'xxx'
}
}
}
将配置转化为方法
现在我们已经拿到了所有的接口配置文件,现在需要做的就是将配置转化为对应的接口请求方法。 处理方法如下:
// api/index.js
import instance from './axios'
let apiMap = {}
Object.keys(apiObj).forEach(key => {
apiMap[key] = {}
Object.keys(apiObj[key]).forEach(api => {
apiMap[key][api] = function (data) {
let query = {
[apiObj[key][api].type === 'post' ? 'data' : 'params']: data
}
return instance({...apiObj[key][api], ...query})
}
})
})
此时apiMap结构如下:
apiMap: {
common: {
getUserInfo: function
},
home: {
getCommonInfo: function
}
}
最后将apiMap导出即可使用
使用
// src/xxx.vue
import api from '@/api'
api.common.getUserInfo(data).then(() => {
// ...
})
以上为axios的封装,接下来我们看看CancelToken内部是怎么实现的。
CancelToken源码解析
cancelToken基本使用方法
axios.get('/api/app/course',{
params: params,
cancelToken: new CancelToken(function executor(c) {
// cancel函数赋值给cancelRequest属性
// 从而可以通过cancelRequest执行取消请求的操作
that.cancelRequest = c
})
})
// 当需要取消请求的时候,调用cancelRequest即可
that.cancelRequest()
接下来我们一起看看源码
// axios/lib/cancel/CancelToken.js 源码
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
// 1. 关键代码,创建Promise对象,将resolve权限交给外部的resolvePromise
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
// 2. 将cancel方法当做参数传给回调函数,回调函数接收cancel方法
// 假设回调函数用cancelRequest字段接收cancel,cancelRequest()即执行了cancel方法
// cancel接收一个message参数,作为描述信息
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
// 3. 执行resolvePromise 将1处的promise状态置为resolve
// 触发promise.then 转到下方的4处
resolvePromise(token.reason);
});
}
// axios/lib/adapters/xhr.js 源码
// ...
if (config.cancelToken) {
// Handle cancellation
// 4. 调用cancel会触发这里的onCanceled方法
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
// 调用xmlhttprequest的abort方法取消请求
request.abort();
// 将axios请求的状态置为reject
// cancel是上边3处的token.reason
reject(cancel);
// Clean up request
request = null;
});
}
// ...
关键部分在代码里做了注释,我们再看看下边的代码,可以帮助理解第1处的代码。
let resolveHandle;
new Promise((resolve)=>{
resolveHandle=resolve;
}).then((val)=>{
console.log('resolve',val);
});
将resolve交给外部变量resolveHandle,当执行resolveHandle()即将promise对象的状态变为resolve,然后触发then的回调
// 执行
resolveHandle('ok');
// 打印
// resolve ok
以上即为CancelToken的基本原理,多看几遍理解更清晰
CancelToken还给我们提供了一个source方法
// axios/lib/cancel/CancelToken.js 源码
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
将cancel方法和token放在一个对象里返给我们
let cancelObj = axios.CancelToken.token.source()
// 这么使用
axios.get('/api/app/course',{
params: params,
cancelToken: cancelObj.token
})
cancelObj.cancel() //取消请求
上边封装里用的就是此方法。
以上。