如圖,面對一團糟程式碼的你~~~真的想說,What F~U~C~K!!!
迴歸正題,我們所要的說的axios的封裝和api介面的統一管理,其實主要目的就是在幫助我們簡化程式碼和利於後期的更新維護。
一、axios的封裝
在vue專案中,和後臺互動獲取資料這塊,我們通常使用的是axios庫,它是基於promise的http庫,可執行在瀏覽器端和node.js中。他有很多優秀的特性,例如攔截請求和響應、取消請求、轉換json、客戶端防禦XSRF等。所以我們的尤大大也是果斷放棄了對其官方庫vue-resource的維護,直接推薦我們使用axios庫。如果還對axios不瞭解的,可以移步axios文件。
安裝
npm install axios; // 安裝axios複製程式碼
引入
一般我會在專案的src目錄中,新建一個request資料夾,然後在裡面新建一個http.js和一個api.js檔案。http.js檔案用來封裝我們的axios,api.js用來統一管理我們的介面。
// 在http.js中引入axios
import axios from 'axios'; // 引入axios
import QS from 'qs'; // 引入qs模組,用來序列化post型別的資料,後面會提到
// vant的toast提示框元件,大家可根據自己的ui元件更改。
import { Toast } from 'vant';
複製程式碼
環境的切換
我們的專案環境可能有開發環境、測試環境和生產環境。我們通過node的環境變數來匹配我們的預設的介面url字首。axios.defaults.baseURL可以設定axios的預設請求地址就不多說了。
// 環境的切換
if (process.env.NODE_ENV == 'development') {
axios.defaults.baseURL = 'https://www.baidu.com';}
else if (process.env.NODE_ENV == 'debug') {
axios.defaults.baseURL = 'https://www.ceshi.com';
}
else if (process.env.NODE_ENV == 'production') {
axios.defaults.baseURL = 'https://www.production.com';
}複製程式碼
設定請求超時
通過axios.defaults.timeout設定預設的請求超時時間。例如超過了10s,就會告知使用者當前請求超時,請重新整理等。axios.defaults.timeout = 10000;複製程式碼
post請求頭的設定
post請求的時候,我們需要加上一個請求頭,所以可以在這裡進行一個預設的設定,即設定post的請求頭為application/x-www-form-urlencoded;charset=UTF-8
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';複製程式碼
- 請求攔截
我們在傳送請求前可以進行一個請求的攔截,為什麼要攔截呢,我們攔截請求是用來做什麼的呢?比如,有些請求是需要使用者登入之後才能訪問的,或者post請求的時候,我們需要序列化我們提交的資料。這時候,我們可以在請求被髮送之前進行一個攔截,從而進行我們想要的操作。
請求攔截
// 先匯入vuex,因為我們要使用到裡面的狀態物件
// vuex的路徑根據自己的路徑去寫
import store from '@/store/index';
// 請求攔截器axios.interceptors.request.use(
config => {
// 每次傳送請求之前判斷vuex中是否存在token
// 如果存在,則統一在http請求的header都加上token,這樣後臺根據token判斷你的登入情況
// 即使本地存在token,也有可能token是過期的,所以在響應攔截器中要對返回狀態進行判斷
const token = store.state.token;
token && (config.headers.Authorization = token);
return config;
},
error => {
return Promise.error(error);
})
複製程式碼
這裡說一下token,一般是在登入完成之後,將使用者的token通過localStorage或者cookie存在本地,然後使用者每次在進入頁面的時候(即在main.js中),會首先從本地儲存中讀取token,如果token存在說明使用者已經登陸過,則更新vuex中的token狀態。然後,在每次請求介面的時候,都會在請求的header中攜帶token,後臺人員就可以根據你攜帶的token來判斷你的登入是否過期,如果沒有攜帶,則說明沒有登入過。這時候或許有些小夥伴會有疑問了,就是每個請求都攜帶token,那麼要是一個頁面不需要使用者登入就可以訪問的怎麼辦呢?其實,你前端的請求可以攜帶token,但是後臺可以選擇不接收啊!
響應的攔截
// 響應攔截器
axios.interceptors.response.use(
response => {
// 如果返回的狀態碼為200,說明介面請求成功,可以正常拿到資料
// 否則的話丟擲錯誤
if (response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
// 伺服器狀態碼不是2開頭的的情況
// 這裡可以跟你們的後臺開發人員協商好統一的錯誤狀態碼
// 然後根據返回的狀態碼進行一些操作,例如登入過期提示,錯誤提示等等
// 下面列舉幾個常見的操作,其他需求可自行擴充套件
error => {
if (error.response.status) {
switch (error.response.status) {
// 401: 未登入
// 未登入則跳轉登入頁面,並攜帶當前頁面的路徑
// 在登入成功後返回當前頁面,這一步需要在登入頁操作。
case 401:
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
break;
// 403 token過期
// 登入過期對使用者進行提示
// 清除本地token和清空vuex中token物件
// 跳轉登入頁面
case 403:
Toast({
message: '登入過期,請重新登入',
duration: 1000,
forbidClick: true
});
// 清除token
localStorage.removeItem('token');
store.commit('loginSuccess', null);
// 跳轉登入頁面,並將要瀏覽的頁面fullPath傳過去,登入成功後跳轉需要訪問的頁面
setTimeout(() => {
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
}, 1000);
break;
// 404請求不存在
case 404:
Toast({
message: '網路請求不存在',
duration: 1500,
forbidClick: true
});
break;
// 其他錯誤,直接丟擲錯誤提示
default:
Toast({
message: error.response.data.message,
duration: 1500,
forbidClick: true
});
}
return Promise.reject(error.response);
}
}
});複製程式碼
響應攔截器很好理解,就是伺服器返回給我們的資料,我們在拿到之前可以對他進行一些處理。例如上面的思想:如果後臺返回的狀態碼是200,則正常返回資料,否則的根據錯誤的狀態碼型別進行一些我們需要的錯誤,其實這裡主要就是進行了錯誤的統一處理和沒登入或登入過期後調整登入頁的一個操作。
要注意的是,上面的Toast()方法,是我引入的vant庫中的toast輕提示元件,你根據你的ui庫,對應使用你的一個提示元件。
封裝get方法和post方法
get方法:我們通過定義一個get函式,get函式有兩個引數,第一個參數列示我們要請求的url地址,第二個引數是我們要攜帶的請求引數。get函式返回一個promise物件,當axios其請求成功時resolve伺服器返回 值,請求失敗時reject錯誤值。最後通過export丟擲get函式。
/**
* get方法,對應get請求
* @param {String} url [請求的url地址]
* @param {Object} params [請求時攜帶的引數]
*/
export function get(url, params){
return new Promise((resolve, reject) =>{
axios.get(url, {
params: params
}).then(res => {
resolve(res.data);
}).catch(err =>{
reject(err.data)
})
});}複製程式碼
/**
* post方法,對應post請求
* @param {String} url [請求的url地址]
* @param {Object} params [請求時攜帶的引數]
*/
export function post(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, QS.stringify(params))
.then(res => {
resolve(res.data);
})
.catch(err =>{
reject(err.data)
})
});
}複製程式碼
這裡有個小細節說下,axios.get()
方法和axios.post()
在提交資料時引數的書寫方式還是有區別的。區別就是,get的第二個引數是一個{},然後這個物件的params屬性值是一個引數物件的。而post的第二個引數就是一個引數物件。兩者略微的區別要留意哦!
axios的封裝基本就完成了,下面再簡單說下api的統一管理。
整齊的api就像電路板一樣,即使再複雜也能很清晰整個線路。上面說了,我們會新建一個api.js,然後在這個檔案中存放我們所有的api介面。
- 首先我們在api.js中引入我們封裝的get和post方法
/**
* api介面統一管理
*/
import { get, post } from './http'複製程式碼
現在,例如我們有這樣一個介面,是一個post請求:
http://www.baiodu.com/api/v1/users/my_address/address_edit_before複製程式碼
我們可以在api.js中這樣封裝:
export const apiAddress = p => post('api/v1/users/my_address/address_edit_before', p);複製程式碼
我們定義了一個apiAddress
方法,這個方法有一個引數p,p是我們請求介面時攜帶的引數物件。而後呼叫了我們封裝的post
方法,post
方法的第一個引數是我們的介面地址,第二個引數是apiAddress
的p引數,即請求介面時攜帶的引數物件。最後通過export匯出apiAddress
。
然後在我們的頁面中可以這樣呼叫我們的api介面:
import { apiAddress } from '@/request/api';// 匯入我們的api介面
export default {
name: 'Address',
created () {
this.onLoad();
},
methods: {
// 獲取資料
onLoad() {
// 呼叫api介面,並且提供了兩個引數
apiAddress({
type: 0,
sort: 1
}).then(res => {
// 獲取資料成功後的其他操作
………………
})
}
}
}複製程式碼
其他的api介面,就在pai.js中繼續往下面擴充套件就可以了。友情提示,為每個介面寫好註釋哦!!!
api介面管理的一個好處就是,我們把api統一集中起來,如果後期需要修改介面,我們就直接在api.js中找到對應的修改就好了,而不用去每一個頁面查詢我們的介面然後再修改會很麻煩。關鍵是,萬一修改的量比較大,就規格gg了。還有就是如果直接在我們的業務程式碼修改介面,一不小心還容易動到我們的業務程式碼造成不必要的麻煩。
好了,最後把完成的axios封裝程式碼奉上。
/**axios封裝
* 請求攔截、相應攔截、錯誤統一處理
*/
import axios from 'axios';import QS from 'qs';
import { Toast } from 'vant';
import store from '../store/index'
// 環境的切換
if (process.env.NODE_ENV == 'development') {
axios.defaults.baseURL = '/api';
} else if (process.env.NODE_ENV == 'debug') {
axios.defaults.baseURL = '';
} else if (process.env.NODE_ENV == 'production') {
axios.defaults.baseURL = 'http://api.123dailu.com/';
}
// 請求超時時間
axios.defaults.timeout = 10000;
// post請求頭
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// 請求攔截器
axios.interceptors.request.use(
config => {
// 每次傳送請求之前判斷是否存在token,如果存在,則統一在http請求的header都加上token,不用每次請求都手動新增了
// 即使本地存在token,也有可能token是過期的,所以在響應攔截器中要對返回狀態進行判斷
const token = store.state.token;
token && (config.headers.Authorization = token);
return config;
},
error => {
return Promise.error(error);
})
// 響應攔截器
axios.interceptors.response.use(
response => {
if (response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
// 伺服器狀態碼不是200的情況
error => {
if (error.response.status) {
switch (error.response.status) {
// 401: 未登入
// 未登入則跳轉登入頁面,並攜帶當前頁面的路徑
// 在登入成功後返回當前頁面,這一步需要在登入頁操作。
case 401:
router.replace({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
});
break;
// 403 token過期
// 登入過期對使用者進行提示
// 清除本地token和清空vuex中token物件
// 跳轉登入頁面
case 403:
Toast({
message: '登入過期,請重新登入',
duration: 1000,
forbidClick: true
});
// 清除token
localStorage.removeItem('token');
store.commit('loginSuccess', null);
// 跳轉登入頁面,並將要瀏覽的頁面fullPath傳過去,登入成功後跳轉需要訪問的頁面
setTimeout(() => {
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
}, 1000);
break;
// 404請求不存在
case 404:
Toast({
message: '網路請求不存在',
duration: 1500,
forbidClick: true
});
break;
// 其他錯誤,直接丟擲錯誤提示
default:
Toast({
message: error.response.data.message,
duration: 1500,
forbidClick: true
});
}
return Promise.reject(error.response);
}
}
);
/**
* get方法,對應get請求
* @param {String} url [請求的url地址]
* @param {Object} params [請求時攜帶的引數]
*/
export function get(url, params){
return new Promise((resolve, reject) =>{
axios.get(url, {
params: params
})
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
});
}
/**
* post方法,對應post請求
* @param {String} url [請求的url地址]
* @param {Object} params [請求時攜帶的引數]
*/
export function post(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, QS.stringify(params))
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
});
}
複製程式碼
如果喜歡,就給個❤❤吧(*^▽^*)
*********華麗麗的分割線******************華麗麗的分割線******************華麗麗的分割線******************華麗麗的分割線******************華麗麗的分割線*********
2018.8.14更新
axios的封裝根據需求的不同而不同。這裡非常感謝評論裡一些很中肯的建議,我也對此進行了思考和針對不同需求的改善。主要有以下改變:
1.優化axios封裝,去掉之前的get和post
2.斷網情況處理
3.更加模組化的api管理
4.介面域名有多個的情況
5.api掛載到vue.prototype上省去引入的步驟
http.js中axios封裝的優化,先直接貼程式碼:
/**
* axios封裝
* 請求攔截、響應攔截、錯誤統一處理
*/
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
}
});
}
/**
* 請求失敗後的錯誤統一處理
* @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);
}}
// 建立axios例項
var instance = axios.create({ timeout: 1000 * 12});
// 設定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);
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中控制著一個全域性的斷網提示元件的顯示隱藏
// 關於斷網元件中的重新整理重新獲取資料,會在斷網元件中說明
store.commit('changeNetwork', false);
}
});
export default instance;複製程式碼
1.去掉了之前get和post方法的封裝,通過建立一個axios例項然後export default方法匯出,這樣使用起來更靈活一些。
2.去掉了通過環境變數控制baseUrl的值。考慮到介面會有多個不同域名的情況,所以準備通過js變數來控制介面域名。這點具體在api裡會介紹。
3.增加了請求超時,即斷網狀態的處理。說下思路,當斷網時,通過更新vuex中network的狀態來控制斷網提示元件的顯示隱藏。斷網提示一般會有重新載入資料的操作,這步會在後面對應的地方介紹。
4.公用函式進行抽出,簡化程式碼,儘量保證單一職責原則。
下面說下api這塊,考慮到一下需求:
1.更加模組化
2.更方便多人開發,有效減少解決命名衝突
3.處理介面域名有多個情況
這裡這裡呢新建了一個api資料夾,裡面有一個index.js和一個base.js,以及多個根據模組劃分的介面js檔案。index.js是一個api的出口,base.js管理介面域名,其他js則用來管理各個模組的介面。
先放index.js程式碼:
/**
* api介面的統一出口
*/
// 文章模組介面
import article from '@/api/article';
// 其他模組的介面……
// 匯出介面
export default {
article,
// ……
}複製程式碼
index.js是一個api介面的出口,這樣就可以把api介面根據功能劃分為多個模組,利於多人協作開發,比如一個人只負責一個模組的開發等,還能方便每個模組中介面的命名哦。
base.js:
/**
* 介面域名的管理
*/
const base = {
sq: 'https://xxxx111111.com/api/v1',
bd: 'http://xxxxx22222.com/api'
}
export default base;複製程式碼
通過base.js來管理我們的介面域名,不管有多少個都可以通過這裡進行介面的定義。即使修改起來,也是很方便的。
最後就是介面模組的說明,例如上面的article.js:
/**
* article模組介面列表
*/
import base from './base'; // 匯入介面域名列表
import axios from '@/utils/http'; // 匯入http中建立的axios例項
import qs from 'qs'; // 根據需求是否匯入qs模組
const article = {
// 新聞列表
articleList () {
return axios.get(`${base.sq}/topics`);
},
// 新聞詳情,演示
articleDetail (id, params) {
return axios.get(`${base.sq}/topic/${id}`, {
params: params
});
},
// post提交
login (params) {
return axios.post(`${base.sq}/accesstoken`, qs.stringify(params));
}
// 其他介面…………
}
export default article;複製程式碼
1.通過直接引入我們封裝好的axios例項,然後定義介面、呼叫axios例項並返回,可以更靈活的使用axios,比如你可以對post請求時提交的資料進行一個qs序列化的處理等。
2.請求的配置更靈活,你可以針對某個需求進行一個不同的配置。關於配置的優先順序,axios文件說的很清楚,這個順序是:在 lib/defaults.js
找到的庫的預設值,然後是例項的 defaults
屬性,最後是請求的 config
引數。後者將優先於前者。
3.restful風格的介面,也可以通過這種方式靈活的設定api介面地址。
最後,為了方便api的呼叫,我們需要將其掛載到vue的原型上。在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的原型上複製程式碼
然後我們可以在頁面中這樣呼叫介面,eg:
methods: {
onLoad(id) {
this.$api.article.articleDetail(id, {
api: 123
}).then(res=> {
// 執行某些操作
})
}
}複製程式碼
再提一下斷網的處理,這裡只做一個簡單的示例:
<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>複製程式碼
這是app.vue,這裡簡單演示一下斷網。在http.js中介紹了,我們會在斷網的時候,來更新vue中network的狀態,那麼這裡我們根據network的狀態來判斷是否需要載入這個斷網元件。斷網情況下,載入斷網元件,不載入對應頁面的元件。當點選重新整理的時候,我們通過跳轉refesh頁面然後立即返回的方式來實現重新獲取資料的操作。因此我們需要新建一個refresh.vue頁面,並在其beforeRouteEnter
鉤子中再返回當前頁面。
// refresh.vue
beforeRouteEnter (to, from, next) {
next(vm => {
vm.$router.replace(from.fullPath)
})
}複製程式碼
這是一種全域性通用的斷網提示,當然了,也可以根據自己的專案需求操作。具體操作就仁者見仁智者見智了。
如果更多的需求,或者說是不一樣的需求,可以根據自己的需求進行一個改進。
如果感覺對你有幫助,那就收藏❤❤吧!