開始之前
最近開始對原來做過的項目復(fù)盤一下,找找以前項目中的存在的優(yōu)缺點,該保留的保留,需要改進的改進。大多數(shù)的項目是在餓了么組件庫上開發(fā)的。既然用了這個組件庫當(dāng)然也一定會借鑒用用該組件庫開發(fā)的一些實例,我們項目搭建借鑒了很多 vue-element-admin 這個項目,對于一個用 vue 搭建的項目,vue-element-admin 官網(wǎng)上褲衩哥講的特別好,所以做如果想用 vue 做一個企業(yè)后臺管理項目推薦手動擼一下這個代碼,個人感覺收獲還是蠻大的。這次和大家分享的是這個項目中一個小的地方——封裝的axios 和 api 接口的統(tǒng)一管理。
axios
對于 vue 項目和后臺交互獲取數(shù)據(jù)這一塊,我接觸的所有項目都是用 axios 庫,它是基于 promise 的 http,它又以下幾個特點:
- 支持瀏覽器和node.js
- 支持promise
- 能攔截請求和響應(yīng)
- 能轉(zhuǎn)換請求和響應(yīng)數(shù)據(jù)
- 能取消請求
- 自動轉(zhuǎn)換JSON數(shù)據(jù)
- 瀏覽器端支持防止CSRF(跨站請求偽造)
對于向后臺發(fā)起的請求和接收的響應(yīng),我們肯定是要處理一下來簡化代碼,統(tǒng)一管理的。褲衩哥手摸手系類教程里面對這個提到了幾點,比如說我們給每個請求統(tǒng)一添加 token;統(tǒng)一的異常處理;多環(huán)境的動態(tài)切換等。vue-element-admin 這個項目 utils/request.js 就是對 axios 的一個封裝,我們就用這個文件來說一下他的一些配置。
引入和基本設(shè)置部分
下面的代碼中引入 element-ui 的部分是用來彈窗對用戶進行提示;這個項目中登錄用戶 token 是放在 store 中的,這里引入store是用來取token,如果沒有用到就不用引入。getToken 是一個自定義獲取token的方法。
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
對于create 方法中設(shè)置了 baseURL, 這個的作用就是后面英文解釋的 每次請求我們寫在的真實 URL就是 baseURL 與 我們以后方法中寫的 request 一個拼接。舉個例子:baseURL: http://localhost:8080/; 我們調(diào)用get方法 axios.get('user') 那實際請求的就是 http://localhost:8080/user. timeout 是設(shè)置了一個超時時間,設(shè)置的規(guī)定時間里面后臺沒有響應(yīng),就認為超時了,就拋出一個錯誤。
上面的代碼中我們發(fā)送的每一個請求是 Content-Type 默認是 application/json;charset=utf-8,有的時候后臺的POST請求要求不是這個,比如后臺要 application/x-www-form-urlencoded 這個。我們可以通過下面兩種方法設(shè)置:
// 第一中在全局上設(shè)置
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
// 第二種在實例上設(shè)置
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000, // request timeout
headers:{'Content-Type':'application/x-www-form-urlencoded'}
})
上面兩種方法都可以說是統(tǒng)一設(shè)置,但是有的項目由于種種原因有時候需要一部分設(shè)置成這種,一部分設(shè)置成為另一種,這個說到實例的方法時再說。
設(shè)置好了是設(shè)置好了,但是如果我們在傳參數(shù)的時候還是按照默認的方式傳一個 json對象,后臺就會接受不到,這種情況不知道你們遇沒遇,我最近現(xiàn)在做的這個項目就遇到了,開始以為是后端的鍋,結(jié)果甩來甩去,就是還是前端的問題,還浪費了一下午時間。具體什么原因,遇到這個問題的同學(xué)可以打開控制臺看一下傳遞參數(shù)的形式,這兩種是有差別的。application/json格式是一個object的形式。application/x-www-form-urllencodes 是 key-value 的形式。改的方法也有兩種,一種是每次我們發(fā)送 post 請求的時候都手動使用 qs.stringify(data) 去更改一下數(shù)據(jù)格式(qs: npm 安裝一下),另一種是借助 axios transfromRequest屬性來統(tǒng)一配置:
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000, // request timeout
headers:{'Content-Type':'application/x-www-form-urlencoded'},
transformRequest: [function (data) {
// `transformRequest` 允許在向服務(wù)器發(fā)送前,修改請求數(shù)據(jù)
// 只能用在 'PUT', 'POST' 和 'PATCH' 這幾個請求方法
return qs.stringify(data)
}],
})
請求攔截器
請求攔截器就是再請求發(fā)出攔截下來,對它做一些處理,下面這個就是對請求加上 token 的驗證信息。
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['X-Token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
這里說一下token,一般是在登錄完成之后,將用戶的token通過localStorage或者cookie存在本地,然后用戶每次在進入頁面的時候(即在main.js中),會首先從本地存儲中讀取token,如果token存在說明用戶已經(jīng)登陸過,則更新vuex中的token狀態(tài)。然后,在每次請求接口的時候,都會在請求的header中攜帶token,后臺人員就可以根據(jù)你攜帶的token來判斷你的登錄是否過期,如果沒有攜帶,則說明沒有登錄過。這時候或許有些小伙伴會有疑問了,就是每個請求都攜帶token,那么要是一個頁面不需要用戶登錄就可以訪問的怎么辦呢?其實,你前端的請求可以攜帶token,但是后臺可以選擇不接收!
響應(yīng)攔截器
響應(yīng)攔截器很好理解,就是服務(wù)器返回給我們的數(shù)據(jù),我們在拿到之前可以對他進行一些處理。例如下面就是多響應(yīng)錯誤的統(tǒng)一處理:
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 20000) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service
褲衩哥這個響應(yīng)攔截的就是拿到的后臺返回的數(shù)據(jù)中判斷 code 的狀態(tài)。當(dāng)然了具體用什么字段就看前端跟后端的約定了。約定好了你們愛用啥用啥,誰也管不著。
前天跟一起再武漢做過項目的同時聊天,他說他們想用服務(wù)器的狀態(tài)碼,不用返回數(shù)據(jù)的 code, 想了想其實也很好做,上面把 const res = response.data 去掉,直接對 response 做判斷就好。比如:
// 響應(yīng)攔截器
axios.interceptors.response.use(
response => {
// 如果返回的狀態(tài)碼為200,說明接口請求成功,可以正常拿到數(shù)據(jù)
// 否則的話拋出錯誤
if (response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
// 服務(wù)器狀態(tài)碼不是2開頭的的情況
// 這里可以跟你們的后臺開發(fā)人員協(xié)商好統(tǒng)一的錯誤狀態(tài)碼
// 然后根據(jù)返回的狀態(tài)碼進行一些操作,例如登錄過期提示,錯誤提示等等
// 下面列舉幾個常見的操作,其他需求可自行擴展
error => {
if (error.response.status) {
switch (error.response.status) {
// 401: 未登錄
// 未登錄則跳轉(zhuǎn)登錄頁面,并攜帶當(dāng)前頁面的路徑
// 在登錄成功后返回當(dāng)前頁面,這一步需要在登錄頁操作。
case 401:
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
break;
// 403 token過期
// 登錄過期對用戶進行提示
// 清除本地token和清空vuex中token對象
// 跳轉(zhuǎn)登錄頁面
case 403:
Message({
message: '登錄過期,請重新登錄',
duration: 1000,
type: error
});
// 清除token
localStorage.removeItem('token');
store.commit('loginSuccess', null);
// 跳轉(zhuǎn)登錄頁面,并將要瀏覽的頁面fullPath傳過去,登錄成功后跳轉(zhuǎn)需要訪問的頁面
setTimeout(() => {
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
});
}, 1000);
break;
// 404請求不存在
case 404:
Message({
message: '網(wǎng)絡(luò)請求不存在',
duration: 1500,
type: error
});
break;
// 其他錯誤,直接拋出錯誤提示
default:
Message({
message: error.response.data.message,
duration: 1500,
type: error
});
}
return Promise.reject(error.response);
}
}
});
響應(yīng)攔截器很好理解,就是服務(wù)器返回給我們的數(shù)據(jù),我們在拿到之前可以對他進行一些處理。例如上面的思想:如果后臺返回的狀態(tài)碼是200,則正常返回數(shù)據(jù),否則的根據(jù)錯誤的狀態(tài)碼類型進行一些我們需要的錯誤,其實這里主要就是進行了錯誤的統(tǒng)一處理和沒登錄或登錄過期后調(diào)整登錄頁的一個操作。
褲衩哥整個vue-element-admin項目都是非常好的,但是有一個地方我就很疑惑,就是在 api 接口的時候沒有使用 get 和 post ,造成了對每一個借口我搜需要寫一遍 method 這個參數(shù)。就像下面這樣:
export function fetchArticle(id) {
return request({
url: '/article/detail',
method: 'get',
params: { id }
})
}
這個也是我最近發(fā)覺到的。因為最近在寫的一個項目我在搭建這個項目的前端框架的時候這個地方就沒有改,最近寫多了這些重復(fù)代碼了想封裝一下減少代碼量就想到了這里。其實我們可以直接使用 axios 自帶的方法就可以了。比如上面的代碼:
export function fetchAriticle(id){
return request.get('/article/detail',{
params:{id}
})
}
注意:上面是 get 請求,我們看第二個參數(shù),它是一個對象,params是它的一個屬性,這個千萬不要忘記了。
對于 post 請求我們正常些即可。
褲衩哥寫法:
export function createArticle(data) {
return request({
url: '/article/create',
method: 'post',
data
})
}
使用 axios.post 寫法:
export function createArticle(data){
return request.post('/article/create', data)
}
項目中大多數(shù)時候是使用 post 的, 不知不覺我們每個接口就少按了13次鍵盤,一個項目那么多 api 接口,省下的時間夠咱們泡個茶遛個彎了。
API 的統(tǒng)一管理
在 vue-element-admin 我們會發(fā)現(xiàn)在 src 下面有一個 api 文件夾,里面存放了該項目的所有的 api 接口定義。接口。api接口管理的一個好處就是,我們把api統(tǒng)一集中起來,如果后期需要修改接口,我們就直接在api.js中找到對應(yīng)的修改就好了,而不用去每一個頁面查找我們的接口然后再修改會很麻煩。關(guān)鍵是,萬一修改的量比較大,就規(guī)格gg了。還有就是如果直接在我們的業(yè)務(wù)代碼修改接口,一不小心還容易動到我們的業(yè)務(wù)代碼造成不必要的麻煩。
舉個例子,例如我們現(xiàn)在有這樣一個接口:
http://localhost:8080/api/user/user_edit
現(xiàn)在可以在 api.js 中這樣封裝
export const userEdit = data => request.post('/user/user_edit', data)
我們定義了一個apiAddress方法,這個方法有一個參數(shù)data,data是我們請求接口時攜帶的參數(shù)對象。而后調(diào)用了post方法,post方法的第一個參數(shù)是我們的接口地址,第二個參數(shù)是userEdit的data參數(shù),即請求接口時攜帶的參數(shù)對象。最后通過export導(dǎo)出userEdit。
然后再我們的頁面中這樣調(diào)用:
import { userEdit } from '@/api/api';// 導(dǎo)入我們的api接口
export default {
name: 'UserEdit',
methods: {
// 獲取數(shù)據(jù)
submit() {
// 調(diào)用api接口,并且提供了兩個參數(shù)
userEdit({
name: 'zhangsan',
age: 18
}).then(res => {
// 獲取數(shù)據(jù)成功后的其他操作
………………
})
}
}
}
其他的api接口,就在api.js中繼續(xù)往下面擴展就可以了。我們的項目后臺都是用swagger,這個里面后臺的同事會寫好一些注釋和說明,如果你們的項目也有類似的API管理工具,前端的接口定義最好和后臺的接口統(tǒng)一起來,這樣就共享一份注釋信息(哈哈 咱們前端就偷個懶)。如果沒有,最好為每個接口寫好注釋。
這樣其實也就差不多了,但是呢我們每次在使用接口的時候還要有一個引用的過程,感覺這一塊也是可以省略的。我在武漢做一個項目的時候,武漢的同事就對這個做了一些改進,使這個更加模塊化,使用起來也更加方便?,F(xiàn)在把思路跟大家說一下:
- 根據(jù)后臺的接口劃分,前臺也建立一個一個模塊的 api 文件,比如 user 模塊,全部放置 user 相關(guān)的 api 接口。
- 在 api 文件下面建立一個 index.js 作為全部 api 接口的出口。
- 將 api 掛在到 vue.prototype 上面。
// user.js user模塊api文件
export const userEdit = data => request.post('/user/user_edit', data)
// api.index.js 所有模塊的出口文件
// 用戶模塊接口
import user from '@/api/user';
// 其他模塊的接口...
// 導(dǎo)出接口
export default {
user,
//....
}
// main.js
import api from '@/api';
// 將 api 掛在 vue.prototype 上
Vue.prototype.$api = api
以后再頁面中調(diào)用就不用先導(dǎo)入了,直接使用 this.$api.user.userEdit 就可以了。
還有一個地方是如果一個項目 由多個服務(wù)構(gòu)成,不同服務(wù)接口路徑不一樣,又沒有網(wǎng)管層的處理,我們可以設(shè)置一個文件定義不同服務(wù)的路徑。例如:
/**
* 接口域名的管理
*/
const base = {
sq: 'https://xxxx111111.com/api/v1',
bd: 'http://xxxxx22222.com/api'
}
export default base;
我們再在 api 接口文件使用 (這樣在創(chuàng)建axios實例的時候就不要指定 baseURL 了)
// user.js user模塊api文件
import base from './base';
export const userEdit = data => request.post(`${base.sq}/user/user_edit`, data)
寫在最后
今天不用禁足了,去公司辦公一天,坐回了自己熟悉的座位,見到了好久沒見的可愛的同事們,心情是非常的好。美中不足的地方是還沒吃上公司原來可口的飯菜。感謝褲衩哥奉獻的vue-element-admin和手膜手系列教程。建議大家看一遍。如果不想看偉哥也可以手摸手教你,男的就算了/:B-)。分享使我快樂,希望能和大家能在前端學(xué)習(xí)的道路一起進步。
對了還有一個遺留問題,就是有時候項目中個別接口會有配置 比如說 content-type,這個該怎么辦,我把 axios 官網(wǎng)上的例子放到這里大家就明白了。
配置的優(yōu)先順序
配置會以一個優(yōu)先順序進行合并。這個順序是:在 lib/defaults.js 找到的庫的默認值,然后是實例的 defaults 屬性,最后是請求的 config 參數(shù)。后者將優(yōu)先于前者。這里是一個例子:
// 使用由庫提供的配置的默認值來創(chuàng)建實例
// 此時超時配置的默認值是 `0`
var instance = axios.create();
// 覆寫庫的超時默認值
// 現(xiàn)在,在超時前,所有請求都會等待 2.5 秒
instance.defaults.timeout = 2500;
// 為已知需要花費很長時間的請求覆寫超時設(shè)置
instance.get('/longRequest', {
timeout: 5000
});
// post 一樣
instance.post('/longRequest', data,{
timeout: 5000
});