你有封装过 axios 吗?主要是封装哪些方面?如何中断 axios 请求?
一、封装过哪些方面?
基本逻辑如上,主要是对请求封装和 api 接口封装。
1.1 http 封装
http 封装,其实就是实例化一个 axios 对象,并对其进行一些配置:
-
设置请求超时
-
post 头设置
-
请求拦截
-
响应拦截
-
重复请求取消(这里涉及到了如何取消请求)
-
错误处理
-
断网处理
-
工具函数
import axios from 'axios' import router from '../router' import store from '../store/index' import { Toast } from 'vant' // 提示函数 const tip = msg => { Toast({ message: msg, duration: 1000, forbidClick: true }) } // 跳转登录页,携带当前页面路由,登录后返回当前页面 const toLogin = () => { router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }) } // 错误处理 const errorHandle = (status, other) => { switch (status) { // 未登录 case 401: toLogin(); break; // 403 token过期,清除token并跳转登录页 case 403: tip('登录过期,请重新登录'); localStorage.removeItem('token'); store.commit(loginSuccess, null); setTimeout(() => { toLogin(); }, 1000); break; //请求不存在 case 404: tip('请求资源不存在'); break; default: console.log(other) } } // 用于存储目前状态为pending的请求标识信息 let pendingRequest = []; // 取消请求-请求拦截中的处理 const CancelToken = config => { // 区别请求的唯一标识,这里用方法名+请求路径 const requestMark = `${config.method} ${config.url}`; // 找当前请求的标识是否存在pendingRequest中,即是否重复请求了 const markIndex = pendingRequest.findIndex(item => { return item.name === requestMark; }); // 存在,即重复了 if (markIndex > -1) { // 取消上个重复的请求 pendingRequest[markIndex].cancel(); // 删掉在pendingRequest中的请求标识 pendingRequest.splice(markIndex, 1); } // (重新)新建针对这次请求的axios的cancelToken标识 const CancelToken = axios.CancelToken; const source = CancelToken.source(); config.cancelToken = source.token; // 设置自定义配置requestMark项,主要用于响应拦截中 config.requestMark = requestMark; // 记录本次请求的标识 pendingRequest.push({ name: requestMark, cancel: source.cancel, }); return config; }; // 取消请求-响应拦截中的处理 const CancelTokenResponse = config => { // 根据请求拦截里设置的requestMark配置来寻找对应pendingRequest里对应的请求标识 const markIndex = pendingRequest.findIndex(item => { return item.name === config.requestMark; }); // 找到了就删除该标识 markIndex > -1 && pendingRequest.splice(markIndex, 1); } // 创建axios实例 var instance = axios.create({ timeout: 1000 * 12 }) // 设置post请求头 instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; // 请求拦截器 instance.interceptors.request.use( config => { config = CancelToken(config); const token = store.state.token; token && (config.headers.Authorization = token); return config; }, error => Promise.reject(error); ) // 响应拦截器 instance.interceptors.response.use( res => { // 请求结束就从pendingRequest删除请求标志 CancelTokenResponse(res.config); return res.status === 200 ? Promise.resolve(res) : Promise.reject(res); }, error => { const { response } = error; // 请求已经发出,返回结果不在2xx的范围 if (response) { CancelTokenResponse(response.config); errorHandle(response.status, response.data.message); return Promise.reject(response); } else { // 断网情况,刷新重新获取数据 if (!window.navigator.onLine) { store.commit('changeNetwork', false); } else { return Promise.reject(error); } } } ) export default instance;
1.2 api 封装
api 封装,主要是用于单个模块所需要的接口进行管理。其中包括了
- 总 api 接口的映射
- 环境变量的切换
- 本地 mock 的功能
- 单个模块的接口列表
1.2.1 总 api 接口的映射
-
api 中定义一个 index.js ,用于所有模块接口的管理
//api接口统一出口 import user from "./user"; import productList from "./product"; //... export { user, productList };
1.2.2 建立一个 get_url.js,用于获取域名地址,实现环境切换可配置
-
get_url.js
import config from "@/config"; import urlMap from "@/config/urlMap"; //api控制接口类型,1为远程接口,0为mock接口 export default function getUrl(url, api = 1) { return api === 0 ? urlMap[url] : config.apiDomain + url; }
-
新建一个配置文件夹 config,其中的 index 文件用于切换环境,urlMap 用于本地 mock 地址的映射
//config/index.js /** * 配置编译环境和线上环境之间的切换 * 默认三套可以增添 */ let apiDomain; switch (process.env.NODE_ENV) { case "dev": apiDomain = "https://www.dev.com"; break; case "prod": apiDomain = "https://www.prod.com"; break; case "test": apiDomain = "https://www.test.com"; break; } export { apiDomain };
//urlMap.js /** * 远程接口地址和本地mock地址映射表 * key:接口地址 * value:本地地址 */ const mockBaseUrl = "http://rap2api.taobao.org/app/mock"; export default { "/user/login": mockBaseUrl + "/223948/login", "/user/info": mockBaseUrl + "/223948/info", "/user/logout": mockBaseUrl + "/223948/logout", };
1.2.3 单个模块接口定义
//产品列表接口
import axios from "@/http";
import getUrl from "@/api/get_url";
const product = {
// 新闻列表
productList() {
return axios({
url: getUrl("/topics"),
method: "get",
});
},
// 新闻详情,演示
productDetail(id, params) {
return axios({
url: getUrl(`/topic/${id}`),
params: params,
method: "get",
});
},
// post提交
login(data) {
return axios({
url: getUrl(`${base.sq}/accesstoken`),
method: "post",
data,
});
},
//...更多接口
};
export default product;
1.2.4 断网处理
-
http 封装中,当断网时,会对 Vue 的网络状态进行更新
-
在 App.vue 中,根据网络情况,判断是否需要加载断网组件
-
全局定义一个断网组件,实现跳转重新获取页面的操作
<!-- App.vue --> <template> <div id="app"> <div v-if="!network"> <h3>我没网了</h3> <div @click="onRefresh">刷新</div> </div> <router-view /> </div> </template> <script> import { mapState } from "vuex"; export default { name: "App", computed: { ...mapState(["network"]), }, methods: { // 通过跳转一个空页面再返回的方式来实现刷新当前页面数据的目的 onRefresh() { this.$router.replace("/refresh"); }, }, }; </script>
http.js
中断网的时候,会更新 vue 中的 network 的状态。此时,根据 network 的状态来判断是否需要加载这个断网组件。当点击刷新的时候,我们通过跳转 refresh 页面,然后立即返回的方式,来实现重新获取数据的操作。因此,我们需要新建一个refresh.vue
页面,并在其beforeRouteEnter
钩子中再返回当前页面。// refresh.vue beforeRouteEnter (to, from, next) { next(vm => { vm.$router.replace(from.fullPath) }) }
1.3 将 api 挂载到全局
//main.js
import Vue from "vue";
import App from "./App";
import router from "./router"; // 导入路由文件
import store from "./store"; // 导入vuex文件
import api from "./api"; // 导入api接口
Vue.prototype.$api = api; // 将api挂载到vue的原型上
1.4 中断 axios 请求
上面避免重复请求中,就使用到了 cancel token 取消请求,使用 CancelToken 工厂方法创建 cancel token
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios
.get("/user/12345", {
cancelToken: source.token,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log("Request canceled", thrown.message);
} else {
// 处理错误
}
});
axios.post(
"/user/12345",
{
name: "new name",
},
{
cancelToken: source.token,
}
);
// 取消请求(message 参数是可选的)
source.cancel("Operation canceled by the user.");