文章目录
- 目录结构
- api 和 views
- 封装 axios
- axios 基本案例
- axios.create 示例
- axios 请求拦截器
- axios 响应拦截器
- request 库源码分析
- router-view
- 参考链接
目录结构
api 和 views
建议根据业务模块来划分 views,并且 将views 和 api 两个模块一一对应,从而方便维护。如下图:
封装 axios
axios 基本案例
const url = 'https://test.youbaobao.xyz:18081/book/home/v2'
axios.get(url, {
params: { openId: '1234' },
// 在 http header 中添加一个 token
headers: { token: 'abcd' }
}).then(response => {
console.log(response)
}).catch(err => {
// 捕获服务端抛出的异常,即返回非 200 请求
console.log(err)
})
这个案例有两个问题:
- 每个需要传入 token 的请求都需要添加 headers 对象,会造成大量重复代码
- 每个请求都需要手动定义异常处理,而异常处理的逻辑大多是一致的,如果将其封装成通用的异常处理方法,那么每个请求都要调用一遍
axios.create 示例
下面使用 axios.create 对整个请求进行重构:
const url = '/book/home/v2'
const request = axios.create({
baseURL: 'https://test.youbaobao.xyz:18081',
timeout: 5000
})
request({
url,
method: 'get',
params: {
openId: '1234'
}
})
首先通过 axios.create
生成一个函数,该函数是 axios 实例,通过执行该方法完成请求,它与直接调用 axios.get
区别如下:
- 需要传入 url 参数,
axios.get
方法的第一个参数是 url - 需要传入 method 参数,
axios.get
方法已经表示发起 get 请求
axios 请求拦截器
上述代码完成了基本请求的功能,下面需要为 http 请求的 headers 中添加 token,同时进行白名单校验,如 /login
不需要添加 token,并实现异步捕获和自定义处理:
const whiteUrl = ["/login"];
const url = "/book/home/v2";
const request = axios.create({
baseURL: "https://test.youbaobao.xyz:18081",
timeout: 5000
});
// 请求拦截器
request.interceptors.request.use(
// 拦截了axios对象
config => {
// throw new Error('error...')
// 将baseURL置空
const url = config.url.replace(config.baseURL, "");
// 如果白名单中有,直接返回axios
if (whiteUrl.some(wl => url === wl)) {
return config;
}
// 当白名单中没有时,追加token
config.headers["token"] = "abcd";
return config;
},
error => {
return Promise.reject(error);
}
);
request({
url,
method: "get",
params: {
openId: "1234"
}
})
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});
这里核心是调用了 request.interceptors.request.use
方法,即 axios 的请求拦截器,该方法需要传入两个参数,第一个参数是拦截器方法,包含一个 config 参数,我们可以在这个方法中修改 config 并且进行回传,第二个参数是异常处理方法,我们可以使用 Promise.reject(error)
将异常返回给用户进行处理,所以我们在 request 请求后可以通过 catch 捕获异常进行自定义处理
axios 响应拦截器
下面我们进一步增强 axios 功能,我们在实际开发中除了需要保障 http statusCode 为 200,还需要保证业务代码正确,上述案例中,我定义了 error_code 为 0 时,表示业务返回正常,如果返回值不为 0 则说明业务处理出错,此时我们通过 request.interceptors.response.use
方法定义响应拦截器,它仍然需要2个参数,与请求拦截器类似,注意第二个参数主要处理 statusCode 非 200 的异常请求,源码如下:
const whiteUrl = ["/login"];
const url = "/book/home/v2";
const request = axios.create({
baseURL: "https://test.youbaobao.xyz:18081",
timeout: 5000
});
// 请求拦截器
request.interceptors.request.use(
// 拦截了axios对象
config => {
// throw new Error('error...')
// 将baseURL置空
const url = config.url.replace(config.baseURL, "");
// 如果白名单中有,直接返回axios
if (whiteUrl.some(wl => url === wl)) {
return config;
}
// 当白名单中没有时,追加token
config.headers["token"] = "abcd";
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
response => {
const res = response.data;
if (res.error_code != 0) {
// 请求失败,提示错误信息
alert(res.msg);
return Promise.reject(new Error(res.msg));
} else {
// 请求成功,返回数据
return res;
}
},
error => {
return Promise.reject(error);
}
);
request({
url,
method: "get",
params: {
openId: "1234"
}
})
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});
request 库源码分析
有了上述基础后,我们再看 request 库源码就非常容易了
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
service.interceptors.request.use(
config => {
// 如果存在 token 则附带在 http header 中
if (store.getters.token) {
config.headers['X-Token'] = getToken()
}
return config
},
error => {
return Promise.reject(error)
}
)
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 20000) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 判断 token 失效的场景
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// 如果 token 失效,则弹出确认对话框,用户点击后,清空 token 并返回登录页面
MessageBox.confirm('Token已失效,是否重新登录', '确认登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
// 为了重新实例化vue-router对象 避免bug
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
router-view
创建和编辑的页面使用的是同一个component,默认情况下当这两个页面切换时并不会触发vue的created或者mounted钩子,官方说你可以通过watch $route的变化来做处理,但其实说真的还是蛮麻烦的。后来发现其实可以简单的在 router-view上加上一个唯一的key,来保证路由切换时都会重新渲染触发钩子了。这样简单的多了。
<router-view :key="key" />
computed: {
key() {
return this.$route.path
}
}