axios封裝以及前端介面處理策略
在大型前端專案中,當有很多介面實現資料輸入、流出並附加攔截,結合狀態管理,抵禦XSRF攻擊等時,統一管理API介面就成為大型前端專案必須面對的環節。axios作為最流行的基於Promise的HTTP庫可以同時執行在瀏覽器端和伺服器端,已經成為大部分前端專案的首選。
POST序列化
通過JSON.stringify
我們同樣可以實現序列化,但是對於複雜ObjectJSON.stringify
的支援行不如qs.stringify
。所以通過引入qs這個庫,qs可以幫我們對深層巢狀的JSON以及Array形式進行序列化,讓我們的API封裝相容更多的場景。
var a = {name:'hehe',age:10}; qs.stringify(a) // 'name=hehe&age=10' JSON.stringify(a) // '{"name":"hehe","age":10}' 複製程式碼
例外:現在後臺工程大多可以在body裡面獲取json,array等,某些情況下,可能後臺是直接讀取的字串資訊,這種情況下,qs.stringify封裝引數中的JSON以及Array格式無法獲取,需要使用JSON.stringify
去處理
axios 對於配置的處理
在正式進行axios二次封裝之前,簡單瞭解一下axios對於配置項的處理;可以從axios暴露出來的方法瞭解,可以在axios.defaults上配置config,也可以在攔截器上以及新的instance config上去配置;通過閱讀原始碼,發現其實axios的config配置是通過merge方法去實現的:
axios.create = function create(instanceConfig) { return createInstance(mergeConfig(axios.defaults, instanceConfig)); }; 複製程式碼
axios default以及新的instance的config之外,也提供了初始化的預設config;
var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' }; function setContentTypeIfUnset(headers, value) { if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) { headers['Content-Type'] = value; } } 複製程式碼
axios的config是這樣一個邏輯:
1.預設的初始化config與defaultconfig進行merge
2.將第一步得到的結果和新instance上的config進行merge
通過分析,我們可以直接將請求的介面進行配置化處理,更方便的一步化適應各種場景
default config
axios給我提供了一個default系列的屬性,可以直接向axios.default
的一些屬性賦值,這個axios.default
的賦值會作用給所用axios請求;官方文件給我提供了一些參考:比如設定預設的baseURL,為基於token的請求把token放到header的Authorization中,以及設定post的請求型別;
axios.defaults.baseURL = 'https://api.example.com'; axios.defaults.headers.common['Authorization'] = AUTH_TOKEN; axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; 複製程式碼
請求攔截以及響應攔截
作為一個出色的http請求庫,axios提供了強大請求攔截和響應攔截功能。
請求攔截
//引入vuex import store from '@/store' ... axios.interceptors.request.use(config => { //將token新增到了request的header裡面 const token = store.state.token; config.headers.common['Authorization'] = token // loading return config }, error => { console.log(error) return Promise.reject(error) }) 複製程式碼
通過攔截器可以實現請求的前置操作,例如,這裡實現了比較常見的將token新增到header中。當然,在default中處理token也是可以的。所以網上大部分對攔截器的操作都是可以放到defaults中執行的,並沒有什麼區別;個人認為請求前攔截可以結合一些定時器已經前端監控相關外掛的使用。
響應攔截
需要注意一下響應攔截的執行順序,先執行axios.interceptors.response.use
然後再執行正常的響應處理;
// 響應攔截器 axios.interceptors.response.use( response => { // 這裡的response返回的HTTP狀態碼為2XX的情況,可以在這裡集中處理200+JSON形式中JSON中前後端約定的狀態碼 if (response.status === 200) { return Promise.resolve(response); } else { return Promise.reject(response); } }, //這裡的error返回的是HTTP狀態碼不是2XX的情況,可以在這裡處理不同HTTP的status error => { if (error.response.status) { switch (error.response.status) { case 401: //未登入的處理 case 403: //許可權不足的處理 break; case 404: // 404請求不存在的處理 break; // 其他錯誤,直接丟擲錯誤提示 default: //預設處理 } return Promise.reject(error.response); } } }); 複製程式碼
axios完全配置化
通過上面對axios的具體config分析,我們可以通過增加merge結合封裝方法來實現多種場景的配置;可以實現諸如,是否跨域攜帶cookie,是否附帶loading,配置特殊介面的請求時常等待等
可以建立一個config.js
const configMap = { defaultConfig: { withCredentials: false, baseURL: path.baseUrl, headers: { post: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, }, }, longrequest: { timeout: 60000, }, nocookie: { withCredentials: false, }, ... 複製程式碼
通過在httpjs中引入
import configMap from './config' import { showFullScreenLoading, tryHideFullScreenLoading } from './loading' import merge from 'lodash.merge' ... merge(axios.defaults, configMap['defaultConfig']) function handleTypeString(type) { type.toLowerCase().split('-').map(item => merge(axios.defaults, configMap[item])) } export default { post(url, data, type) { handleTypeString(type) return axios({ method: 'post', 複製程式碼
這樣可以實現多個介面請求的配置組合,後面的會覆蓋前面的,一個接一個,實現axios請求的完全配置化處理;
在vue中使用
import http from "@/api/http"; import path from "@/api/path"; //配合async await更加優雅 async test() { const res = await http.post(path.test, params, "long-nocookie"); 複製程式碼
有些人比較喜歡使用偏函式的方式再包裝一層,也可以再增加封裝一層使呼叫時候直接使用。
test(param) 複製程式碼
全域性loading狀態的處理
封裝loading.js來處理部分url請求介面需要loading菊花圖的情況;需要設定needLoadingCount來記錄處理多個需要loading請求介面處理的情況。
import { Loading } from 'element-ui'; let loading; function startLoading() { // 使用Element loading.tart 方法 loading = Loading.service({ lock: true, text: 'loading……', background: 'rgba(0, 0, 0, 0.5)', }); } function endLoading() { // 使用Element loading.close 方法 loading.close(); } //通過needLoadingCount來記錄,在多個地方使用loading時候處理 let needLoadingCount = 0; export function showLoading() { if (needLoadingCount === 0) { startLoading(); } needLoadingCount++;//eslint-disable-line } export function tryHideLoading() { if (needLoadingCount >= 0) needLoadingCount--;//eslint-disable-line if (needLoadingCount === 0) { endLoading(); } } 複製程式碼
在http中配置
if (type === 'long') { showFullScreenLoading() return axios(config1).then( (response) => { return checkStatus(response) } ).then( (res) => { tryHideFullScreenLoading() return checkCode(res) } ) } //其他無loading的axios請求 return axios(config1).then( 複製程式碼
多種環境切換的封裝
在前後端分離的spa場景下,axios的baseUrl等各種環境引數是預先設定好的;然後打包成靜態檔案,上傳到nginx或者tomcat類似的http伺服器中,從本地開發到測試,提供靜態檔案給不同的後臺去使用,可能不同後臺設定的介面地址是不一樣的,為了避免一個個的去打包,我們需要配置一個針對不同域名環境的封裝;
import merge from 'lodash.merge' const path = { baseUrl: 'http://localhost:3000', login: '/users/login', test: '/test', }; const pathMap = { 'http://localhost:3001': { baseUrl: 'http://localhost:3001' }, 'http://localhost:3002': { baseUrl: 'http://localhost:3002',login }, } const getClientIdByLocation = () => { const { href } = window.location; const matchedKey = Object.keys(clientIdMap).filter(url => href.indexOf(url) > -1); merge(path, pathMap[matchedKey]) }; export default path; 複製程式碼
可以通過直接在pathMap中配置不同的url對應的path物件,來實現處理不同的url對應的baseUrl以及各種子路由情況不同時候的情況