https://www.kancloud.cn/yunye/axios/234845
1一、axios的封装
2在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御XSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。如果还对axios不了解的,可以移步axios文档。
3
4安装
5npm install axios; // 安装axios
6引入
7一般我会在项目的src目录中,新建一个request文件夹,然后在里面新建一个http.js和一个api.js文件。http.js文件用来封装我们的axios,api.js用来统一管理我们的接口。
8
9// 在http.js中引入axios
10import axios from 'axios'; // 引入axios
11import QS from 'qs'; // 引入qs模块,用来序列化post类型的数据,后面会提到
12// vant的toast提示框组件,大家可根据自己的ui组件更改。
13import { Toast } from 'vant';
14环境的切换
15我们的项目环境可能有开发环境、测试环境和生产环境。我们通过node的环境变量来匹配我们的默认的接口url前缀。axios.defaults.baseURL可以设置axios的默认请求地址就不多说了。
16
17// 环境的切换
18if (process.env.NODE_ENV == 'development') {
19 axios.defaults.baseURL = 'https://www.baidu.com';}
20else if (process.env.NODE_ENV == 'debug') {
21 axios.defaults.baseURL = 'https://www.ceshi.com';
22}
23else if (process.env.NODE_ENV == 'production') {
24 axios.defaults.baseURL = 'https://www.production.com';
25}
26设置请求超时
27通过axios.defaults.timeout设置默认的请求超时时间。例如超过了10s,就会告知用户当前请求超时,请刷新等。
28
29axios.defaults.timeout = 10000;
30post请求头的设置
31post请求的时候,我们需要加上一个请求头,所以可以在这里进行一个默认的设置,即设置post的请求头为application/x-www-form-urlencoded;charset=UTF-8
32
33axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
34请求拦截
35我们在发送请求前可以进行一个请求的拦截,为什么要拦截呢,我们拦截请求是用来做什么的呢?比如,有些请求是需要用户登录之后才能访问的,或者post请求的时候,我们需要序列化我们提交的数据。这时候,我们可以在请求被发送之前进行一个拦截,从而进行我们想要的操作。
36
37请求拦截
38// 先导入vuex,因为我们要使用到里面的状态对象
39// vuex的路径根据自己的路径去写
40import store from '@/store/index';
41
42// 请求拦截器axios.interceptors.request.use(
43 config => {
44 // 每次发送请求之前判断vuex中是否存在token
45 // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况
46 // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断
47 const token = store.state.token;
48 token && (config.headers.Authorization = token);
49 return config;
50 },
51 error => {
52 return Promise.error(error);
53})
54这里说一下token,一般是在登录完成之后,将用户的token通过localStorage或者cookie存在本地,然后用户每次在进入页面的时候(即在main.js中),会首先从本地存储中读取token,如果token存在说明用户已经登陆过,则更新vuex中的token状态。然后,在每次请求接口的时候,都会在请求的header中携带token,后台人员就可以根据你携带的token来判断你的登录是否过期,如果没有携带,则说明没有登录过。这时候或许有些小伙伴会有疑问了,就是每个请求都携带token,那么要是一个页面不需要用户登录就可以访问的怎么办呢?其实,你前端的请求可以携带token,但是后台可以选择不接收啊!
55
56响应的拦截
57// 响应拦截器
58axios.interceptors.response.use(
59 response => {
60 // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
61 // 否则的话抛出错误
62 if (response.status === 200) {
63 return Promise.resolve(response);
64 } else {
65 return Promise.reject(response);
66 }
67 },
68 // 服务器状态码不是2开头的的情况
69 // 这里可以跟你们的后台开发人员协商好统一的错误状态码
70 // 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等
71 // 下面列举几个常见的操作,其他需求可自行扩展
72 error => {
73 if (error.response.status) {
74 switch (error.response.status) {
75 // 401: 未登录
76 // 未登录则跳转登录页面,并携带当前页面的路径
77 // 在登录成功后返回当前页面,这一步需要在登录页操作。
78 case 401:
79 router.replace({
80 path: '/login',
81 query: {
82 redirect: router.currentRoute.fullPath
83 }
84 });
85 break;
86
87 // 403 token过期
88 // 登录过期对用户进行提示
89 // 清除本地token和清空vuex中token对象
90 // 跳转登录页面
91 case 403:
92 Toast({
93 message: '登录过期,请重新登录',
94 duration: 1000,
95 forbidClick: true
96 });
97 // 清除token
98 localStorage.removeItem('token');
99 store.commit('loginSuccess', null);
100 // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
101 setTimeout(() => {
102 router.replace({
103 path: '/login',
104 query: {
105 redirect: router.currentRoute.fullPath
106 }
107 });
108 }, 1000);
109 break;
110
111 // 404请求不存在
112 case 404:
113 Toast({
114 message: '网络请求不存在',
115 duration: 1500,
116 forbidClick: true
117 });
118 break;
119 // 其他错误,直接抛出错误提示
120 default:
121 Toast({
122 message: error.response.data.message,
123 duration: 1500,
124 forbidClick: true
125 });
126 }
127 return Promise.reject(error.response);
128 }
129 }
130});
131响应拦截器很好理解,就是服务器返回给我们的数据,我们在拿到之前可以对他进行一些处理。例如上面的思想:如果后台返回的状态码是200,则正常返回数据,否则的根据错误的状态码类型进行一些我们需要的错误,其实这里主要就是进行了错误的统一处理和没登录或登录过期后调整登录页的一个操作。
132
133要注意的是,上面的Toast()方法,是我引入的vant库中的toast轻提示组件,你根据你的ui库,对应使用你的一个提示组件。
134
135封装get方法和post方法
136我们常用的ajax请求方法有get、post、put等方法,相信小伙伴都不会陌生。axios对应的也有很多类似的方法,不清楚的可以看下文档。但是为了简化我们的代码,我们还是要对其进行一个简单的封装。下面我们主要封装两个方法:get和post。
137
138get方法:我们通过定义一个get函数,get函数有两个参数,第一个参数表示我们要请求的url地址,第二个参数是我们要携带的请求参数。get函数返回一个promise对象,当axios其请求成功时resolve服务器返回 值,请求失败时reject错误值。最后通过export抛出get函数。
139
140/**
141 * get方法,对应get请求
142 * @param {String} url [请求的url地址]
143 * @param {Object} params [请求时携带的参数]
144 */
145export function get(url, params){
146 return new Promise((resolve, reject) =>{
147 axios.get(url, {
148 params: params
149 }).then(res => {
150 resolve(res.data);
151 }).catch(err =>{
152 reject(err.data)
153 })
154});}
155post方法:原理同get基本一样,但是要注意的是,post方法必须要使用对提交从参数对象进行序列化的操作,所以这里我们通过node的qs模块来序列化我们的参数。这个很重要,如果没有序列化操作,后台是拿不到你提交的数据的。这就是文章开头我们import QS from 'qs';的原因。如果不明白序列化是什么意思的,就百度一下吧,答案一大堆。
156
157/**
158 * post方法,对应post请求
159 * @param {String} url [请求的url地址]
160 * @param {Object} params [请求时携带的参数]
161 */
162export function post(url, params) {
163 return new Promise((resolve, reject) => {
164 axios.post(url, QS.stringify(params))
165 .then(res => {
166 resolve(res.data);
167 })
168 .catch(err =>{
169 reject(err.data)
170 })
171 });
172}
173这里有个小细节说下,axios.get()方法和axios.post()在提交数据时参数的书写方式还是有区别的。区别就是,get的第二个参数是一个{},然后这个对象的params属性值是一个参数对象的。而post的第二个参数就是一个参数对象。两者略微的区别要留意哦!
174
175axios的封装基本就完成了,下面再简单说下api的统一管理。
176整齐的api就像电路板一样,即使再复杂也能很清晰整个线路。上面说了,我们会新建一个api.js,然后在这个文件中存放我们所有的api接口。
177
178首先我们在api.js中引入我们封装的get和post方法
179
180/**
181 * api接口统一管理
182 */
183import { get, post } from './http'
184现在,例如我们有这样一个接口,是一个post请求:
185
186http://www.baiodu.com/api/v1/users/my_address/address_edit_before
187我们可以在api.js中这样封装:
188
189export const apiAddress = p => post('api/v1/users/my_address/address_edit_before', p);
190我们定义了一个apiAddress方法,这个方法有一个参数p,p是我们请求接口时携带的参数对象。而后调用了我们封装的post方法,post方法的第一个参数是我们的接口地址,第二个参数是apiAddress的p参数,即请求接口时携带的参数对象。最后通过export导出apiAddress。
191
192然后在我们的页面中可以这样调用我们的api接口:
193
194import { apiAddress } from '@/request/api';// 导入我们的api接口
195export default {
196 name: 'Address',
197 created () {
198 this.onLoad();
199 },
200 methods: {
201 // 获取数据
202 onLoad() {
203 // 调用api接口,并且提供了两个参数
204 apiAddress({
205 type: 0,
206 sort: 1
207 }).then(res => {
208 // 获取数据成功后的其他操作
209 ………………
210 })
211 }
212 }
213}
214其他的api接口,就在pai.js中继续往下面扩展就可以了。友情提示,为每个接口写好注释哦!!!
215
216api接口管理的一个好处就是,我们把api统一集中起来,如果后期需要修改接口,我们就直接在api.js中找到对应的修改就好了,而不用去每一个页面查找我们的接口然后再修改会很麻烦。关键是,万一修改的量比较大,就规格gg了。还有就是如果直接在我们的业务代码修改接口,一不小心还容易动到我们的业务代码造成不必要的麻烦。
217
218好了,最后把完成的axios封装代码奉上。
219
220/**axios封装
221 * 请求拦截、相应拦截、错误统一处理
222 */
223import axios from 'axios';import QS from 'qs';
224import { Toast } from 'vant';
225import store from '../store/index'
226
227// 环境的切换
228if (process.env.NODE_ENV == 'development') {
229 axios.defaults.baseURL = '/api';
230} else if (process.env.NODE_ENV == 'debug') {
231 axios.defaults.baseURL = '';
232} else if (process.env.NODE_ENV == 'production') {
233 axios.defaults.baseURL = 'http://api.123dailu.com/';
234}
235
236// 请求超时时间
237axios.defaults.timeout = 10000;
238
239// post请求头
240axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
241
242// 请求拦截器
243axios.interceptors.request.use(
244 config => {
245 // 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
246 // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断
247 const token = store.state.token;
248 token && (config.headers.Authorization = token);
249 return config;
250 },
251 error => {
252 return Promise.error(error);
253 })
254
255// 响应拦截器
256axios.interceptors.response.use(
257 response => {
258 if (response.status === 200) {
259 return Promise.resolve(response);
260 } else {
261 return Promise.reject(response);
262 }
263 },
264 // 服务器状态码不是200的情况
265 error => {
266 if (error.response.status) {
267 switch (error.response.status) {
268 // 401: 未登录
269 // 未登录则跳转登录页面,并携带当前页面的路径
270 // 在登录成功后返回当前页面,这一步需要在登录页操作。
271 case 401:
272 router.replace({
273 path: '/login',
274 query: { redirect: router.currentRoute.fullPath }
275 });
276 break;
277 // 403 token过期
278 // 登录过期对用户进行提示
279 // 清除本地token和清空vuex中token对象
280 // 跳转登录页面
281 case 403:
282 Toast({
283 message: '登录过期,请重新登录',
284 duration: 1000,
285 forbidClick: true
286 });
287 // 清除token
288 localStorage.removeItem('token');
289 store.commit('loginSuccess', null);
290 // 跳转登录页面,并将要浏览的页面fullPath传过去,登录成功后跳转需要访问的页面
291 setTimeout(() => {
292 router.replace({
293 path: '/login',
294 query: {
295 redirect: router.currentRoute.fullPath
296 }
297 });
298 }, 1000);
299 break;
300 // 404请求不存在
301 case 404:
302 Toast({
303 message: '网络请求不存在',
304 duration: 1500,
305 forbidClick: true
306 });
307 break;
308 // 其他错误,直接抛出错误提示
309 default:
310 Toast({
311 message: error.response.data.message,
312 duration: 1500,
313 forbidClick: true
314 });
315 }
316 return Promise.reject(error.response);
317 }
318 }
319);
320/**
321 * get方法,对应get请求
322 * @param {String} url [请求的url地址]
323 * @param {Object} params [请求时携带的参数]
324 */
325export function get(url, params){
326 return new Promise((resolve, reject) =>{
327 axios.get(url, {
328 params: params
329 })
330 .then(res => {
331 resolve(res.data);
332 })
333 .catch(err => {
334 reject(err.data)
335 })
336 });
337}
338/**
339 * post方法,对应post请求
340 * @param {String} url [请求的url地址]
341 * @param {Object} params [请求时携带的参数]
342 */
343export function post(url, params) {
344 return new Promise((resolve, reject) => {
345 axios.post(url, QS.stringify(params))
346 .then(res => {
347 resolve(res.data);
348 })
349 .catch(err => {
350 reject(err.data)
351 })
352 });
353}
354axios的封装根据需求的不同而不同。
3551.优化axios封装,去掉之前的get和post
356
3572.断网情况处理
358
3593.更加模块化的api管理
360
3614.接口域名有多个的情况
362
3635.api挂载到vue.prototype上省去引入的步骤
364
365http.js中axios封装的优化,先直接贴代码:
366
367/**
368 * axios封装
369 * 请求拦截、响应拦截、错误统一处理
370 */
371import axios from 'axios';
372import router from '../router';
373import store from '../store/index';
374import { Toast } from 'vant';
375
376/**
377 * 提示函数
378 * 禁止点击蒙层、显示一秒后关闭
379 */
380const tip = msg => {
381 Toast({
382 message: msg,
383 duration: 1000,
384 forbidClick: true
385 });
386}
387
388/**
389 * 跳转登录页
390 * 携带当前页面路由,以期在登录页面完成登录后返回当前页面
391 */
392const toLogin = () => {
393 router.replace({
394 path: '/login',
395 query: {
396 redirect: router.currentRoute.fullPath
397 }
398 });
399}
400
401/**
402 * 请求失败后的错误统一处理
403 * @param {Number} status 请求失败的状态码
404 */
405const errorHandle = (status, other) => {
406 // 状态码判断
407 switch (status) {
408 // 401: 未登录状态,跳转登录页
409 case 401:
410 toLogin();
411 break;
412 // 403 token过期
413 // 清除token并跳转登录页
414 case 403:
415 tip('登录过期,请重新登录');
416 localStorage.removeItem('token');
417 store.commit('loginSuccess', null);
418 setTimeout(() => {
419 toLogin();
420 }, 1000);
421 break;
422 // 404请求不存在
423 case 404:
424 tip('请求的资源不存在');
425 break;
426 default:
427 console.log(other);
428 }}
429
430// 创建axios实例
431var instance = axios.create({ timeout: 1000 * 12});
432// 设置post请求头
433instance.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
434/**
435 * 请求拦截器
436 * 每次请求前,如果存在token则在请求头中携带token
437 */
438instance.interceptors.request.use(
439 config => {
440 // 登录流程控制中,根据本地是否存在token判断用户的登录情况
441 // 但是即使token存在,也有可能token是过期的,所以在每次的请求头中携带token
442 // 后台根据携带的token判断用户的登录情况,并返回给我们对应的状态码
443 // 而后我们可以在响应拦截器中,根据状态码进行一些统一的操作。
444 const token = store.state.token;
445 token && (config.headers.Authorization = token);
446 return config;
447 },
448 error => Promise.error(error))
449
450// 响应拦截器
451instance.interceptors.response.use(
452 // 请求成功
453 res => res.status === 200 ? Promise.resolve(res) : Promise.reject(res),
454 // 请求失败
455 error => {
456 const { response } = error;
457 if (response) {
458 // 请求已发出,但是不在2xx的范围
459 errorHandle(response.status, response.data.message);
460 return Promise.reject(response);
461 } else {
462 // 处理断网的情况
463 // eg:请求超时或断网时,更新state的network状态
464 // network状态在app.vue中控制着一个全局的断网提示组件的显示隐藏
465 // 关于断网组件中的刷新重新获取数据,会在断网组件中说明
466 if (!window.navigator.onLine) {
467 store.commit('changeNetwork', false);
468 } else {
469 return Promise.reject(error);
470 }
471 }
472 });
473
474export default instance;
475这个axios和之前的大同小异,做了如下几点改变:
476
4771.去掉了之前get和post方法的封装,通过创建一个axios实例然后export default方法导出,这样使用起来更灵活一些。
478
4792.去掉了通过环境变量控制baseUrl的值。考虑到接口会有多个不同域名的情况,所以准备通过js变量来控制接口域名。这点具体在api里会介绍。
480
4813.增加了请求超时,即断网状态的处理。说下思路,当断网时,通过更新vuex中network的状态来控制断网提示组件的显示隐藏。断网提示一般会有重新加载数据的操作,这步会在后面对应的地方介绍。
482
4834.公用函数进行抽出,简化代码,尽量保证单一职责原则。
484
485下面说下api这块,考虑到一下需求:
486
4871.更加模块化
488
4892.更方便多人开发,有效减少解决命名冲突
490
4913.处理接口域名有多个情况
492
493这里这里呢新建了一个api文件夹,里面有一个index.js和一个base.js,以及多个根据模块划分的接口js文件。index.js是一个api的出口,base.js管理接口域名,其他js则用来管理各个模块的接口。
494
495先放index.js代码:
496
497/**
498 * api接口的统一出口
499 */
500// 文章模块接口
501import article from '@/api/article';
502// 其他模块的接口……
503
504// 导出接口
505export default {
506 article,
507 // ……
508}
509index.js是一个api接口的出口,这样就可以把api接口根据功能划分为多个模块,利于多人协作开发,比如一个人只负责一个模块的开发等,还能方便每个模块中接口的命名哦。
510
511base.js:
512
513/**
514 * 接口域名的管理
515 */
516const base = {
517 sq: 'https://xxxx111111.com/api/v1',
518 bd: 'http://xxxxx22222.com/api'
519}
520
521export default base;
522通过base.js来管理我们的接口域名,不管有多少个都可以通过这里进行接口的定义。即使修改起来,也是很方便的。
523
524最后就是接口模块的说明,例如上面的article.js:
525
526/**
527 * article模块接口列表
528 */
529
530import base from './base'; // 导入接口域名列表
531import axios from '@/utils/http'; // 导入http中创建的axios实例
532import qs from 'qs'; // 根据需求是否导入qs模块
533
534const article = {
535 // 新闻列表
536 articleList () {
537 return axios.get(`${base.sq}/topics`);
538 },
539 // 新闻详情,演示
540 articleDetail (id, params) {
541 return axios.get(`${base.sq}/topic/${id}`, {
542 params: params
543 });
544 },
545 // post提交
546 login (params) {
547 return axios.post(`${base.sq}/accesstoken`, qs.stringify(params));
548 }
549 // 其他接口…………
550}
551
552export default article;
5531.通过直接引入我们封装好的axios实例,然后定义接口、调用axios实例并返回,可以更灵活的使用axios,比如你可以对post请求时提交的数据进行一个qs序列化的处理等。
554
5552.请求的配置更灵活,你可以针对某个需求进行一个不同的配置。关于配置的优先级,axios文档说的很清楚,这个顺序是:在 lib/defaults.js 找到的库的默认值,然后是实例的 defaults 属性,最后是请求的 config 参数。后者将优先于前者。
556
5573.restful风格的接口,也可以通过这种方式灵活的设置api接口地址。
558
559最后,为了方便api的调用,我们需要将其挂载到vue的原型上。在main.js中:
560
561import Vue from 'vue'
562import App from './App'
563import router from './router' // 导入路由文件
564import store from './store' // 导入vuex文件
565import api from './api' // 导入api接口
566
567Vue.prototype.$api = api; // 将api挂载到vue的原型上
568然后我们可以在页面中这样调用接口,eg:
569
570methods: {
571 onLoad(id) {
572 this.$api.article.articleDetail(id, {
573 api: 123
574 }).then(res=> {
575 // 执行某些操作
576 })
577 }
578}
579再提一下断网的处理,这里只做一个简单的示例:
580
581<template>
582 <div id="app">
583 <div v-if="!network">
584 <h3>我没网了</h3>
585 <div @click="onRefresh">刷新</div>
586 </div>
587 <router-view/>
588 </div>
589</template>
590
591<script>
592 import { mapState } from 'vuex';
593 export default {
594 name: 'App',
595 computed: {
596 ...mapState(['network'])
597 },
598 methods: {
599 // 通过跳转一个空页面再返回的方式来实现刷新当前页面数据的目的
600 onRefresh () {
601 this.$router.replace('/refresh')
602 }
603 }
604 }
605</script>
606这是app.vue,这里简单演示一下断网。在http.js中介绍了,我们会在断网的时候,来更新vue中network的状态,那么这里我们根据network的状态来判断是否需要加载这个断网组件。断网情况下,加载断网组件,不加载对应页面的组件。当点击刷新的时候,我们通过跳转refesh页面然后立即返回的方式来实现重新获取数据的操作。因此我们需要新建一个refresh.vue页面,并在其beforeRouteEnter钩子中再返回当前页面。
607
608
609// refresh.vue
610beforeRouteEnter (to, from, next) {
611 next(vm => {
612 vm.$router.replace(from.fullPath)
613 })
614}
个人笔记记录 2021 ~ 2025