封裝 axios 請求大同小異,本文提供思路和原始碼,不要以偏概全,適合自己業務才是最重要的!
功能點實現
- 基於 Restful 風格進行封裝
- 取消請求與攔截重複請求
- token 攔截
- 介面白名單
- token 自動登入(看自己個人業務是否需要,一般用於移動端)
- 匯出封裝 GET/POST/DELETE/PUT/PATCH/...請求函式
話不多說,萬事總要開頭,先來引入開頭
import axios from 'axios'
複製程式碼
1. 基於 Restful 風格進行封裝
GET和POST明顯的區別就是引數放置位置不同,前者放在
url?
後面,後者放在body
體內。
然鵝,Restful 風格在其基礎新增了DELETE、PUT和PATCH請求型別,其中DELETE請求引數和GET一樣放在
url?
後面,PUT、PATCH這兩個的請求引數則是和POST一樣放在body
體內。至於怎麼放,看你們規範怎麼定
至於想怎麼改更請求引數放置位置,請繼續往下看!
型別 | 請求引數放置位置 | 含義 |
---|---|---|
GET |
url? 後面 |
請求資源(查詢使用者) |
DELETE |
url? 後面 |
刪除資料(刪除使用者) |
POST |
body 體內 |
建立資料(建立使用者) |
PUT |
body 體內 |
更新全部資料(修改使用者資訊,暱稱,簽名,郵箱等等...全部更新) |
PATCH |
body 體內 |
更新部分資料(修改使用者狀態) |
先建立一個axios
例項,然後為其賦值預設引數。
const service = axios.create({
// 設定預設請求頭
// 比如 BASE_API = 'api'
// 那麼訪問就是 {你當前的域名}/api/{介面地址} http://localhost:8080/api/users
baseURL: process.env.BASE_API,
// 這裡因為我後臺把 http狀態碼 跟 定製返回體狀態碼 看為一樣的,所以 http.status === result.status
validateStatus: status => status < 500, // 攔截狀態碼大於500
// 修改預設請求頭,採用json格式傳輸,對陣列物件極其方便,不用專門下qs格式化引數
// common對應的是引數放在請求url上(get/delete),patch/post/put引數放在body體內
// 這裡對應http Request Header.Accept
headers: {
common: { Accept: 'application/json; charset=UTF-8' },
patch: { 'Content-Type': 'application/json; charset=UTF-8' },
post: { 'Content-Type': 'application/json; charset=UTF-8' },
put: { 'Content-Type': 'application/json; charset=UTF-8' }
},
timeout: 60000 // 請求超時時間
})
複製程式碼
2. 取消請求與攔截重複請求
官方原話:Axios 的 cancel token API 基於cancelable promises proposal,它還處於第一階段。
使用
axios.CancelToken
進行攔截
// 每次請求都會記錄在此物件裡,用於判斷是否重複
const pending = {}
// axios.CancelToken
const { CancelToken } = axios
const paramsList = ['get', 'delete']
const dataList = ['post', 'put', 'patch']
// 區分引數位置
const isTypeList = method => {
if (paramsList.includes(method)) {
return 'params'
} else if (dataList.includes(method)) {
return 'data'
}
}
/**
* 獲取請求唯一值(key)
* @param {Object} config - axios攔截器的config
* @param {Boolean} isResult - 擷取url唯一,這裡區別請求前和請求後,因為前者和後者的url不同,所以需要區分一下
*/
function getRequestIdentify(config, isResult = false) {
const url = !isResult
? config.url
: config.baseURL + config.url.substring(1, config.url.length)
const params = { ...(config[isTypeList(config.method)] || {}) }
delete params.t
return encodeURIComponent(url + JSON.stringify(params))
}
/**
* 每次請求前 清除上一個跟它相同的還在請求中的介面
* @param {String} key - url唯一值
* @param {Boolean} isRequest - 是否執行取消重複請求
*/
function removePending(key, isRequest = false) {
if (pending[key] && isRequest) {
pending[key]('取消重複請求')
}
delete pending[key]
}
複製程式碼
請求前的 url
請求後的 url
在攔截器裡面設定
// 請求前
service.interceptors.request.use(
config => {
// 獲取該次請求的唯一值
const requestData = getRequestIdentify(config, true)
// 刪除上一個相同的請求
removePending(requestData, true)
// 例項化取消請求,並同時注入pending
config.cancelToken = new CancelToken(c => {
pending[requestData] = c
})
return config
},
error => {
Promise.reject(error)
}
)
// 請求完成後
service.interceptors.response.use(
response => {
// 把已經完成的請求從 pending 中移除
const requestData = getRequestIdentify(response.config)
removePending(requestData)
// ...
},
error => {
// ...
}
)
複製程式碼
3. token 攔截
token
是比較常見的前後端校驗攔截方式
// 引入store
import store from 'store'
service.interceptors.request.use(config => {
// ...上面的程式碼
const token = store.getters.GET_TOKEN
if (!token) {
// 取消該次請求
pending[requestData]('cancelToken')
store.dispatch('logOut')
} else {
// 在業務約定的headers裡面某個欄位定義token,方便後端提取校驗
config.headers['Authorization'] = token
}
return config
})
複製程式碼
4. 介面白名單
所謂白名單,就是不用任何校驗許可權(token)的介面,比如使用者登入、使用者註冊、修改使用者密碼等等
// 定義介面白名單
const noLogin = [
// ...
'/login',
'/register'
]
service.interceptors.request.use(config => {
// ...上面程式碼
if (!noLogin.includes(config.url.replace(config.baseURL, ''))) {
// 當不在白名單內時則校驗token
// ...上面程式碼
}
return config
})
複製程式碼
5. token 自動登入
通過每次請求完成後,如果該請求返回的是登入失效(
401
),則用一個陣列裝在該次的返回物件config
。
然後重啟發起該獲取 token 請求,完成後把陣列 map 重新發起一次請求,然後清除陣列
前提是把賬號密碼記錄儲存在瀏覽器或者有重新整理 token 介面
準備好以下這些方法
// 是否正在重新整理的標記
const isRefreshing = false
// 重試佇列,每一項將是一個待執行的函式形式
const retryRequests = []
// 重新請求流程處理
async function reRequest(response) {
const { config } = response
if (!isRefreshing) {
isRefreshing = true
const reRes = await refreshTokenFn()
if (reRes) {
config.headers['Authorization'] = store.getters.GET_TOKEN
// 已經重新整理了token,將所有佇列中的請求進行重試
retryRequests.map(cb => cb(store.getters.GET_TOKEN))
// 清空列隊
retryRequests = []
config.baseURL = ''
isRefreshing = false
return service(config)
} else {
// 重新整理token失敗,跳回登入頁
store.dispatch('logOut')
}
isRefreshing = false
} else {
return new Promise(resolve => {
// 將resolve放進列隊,用一個函式形式儲存,等token重新整理後直接執行
retryRequests.push(token => {
config.baseURL = ''
config.headers['Authorization'] = token
resolve(service(config))
})
})
}
}
// 重新整理token方法
async function refreshTokenFn() {
try {
const { loginName, password } = store.getters.GET_USER_INFO
// 自己定義的獲取方法
const res = await store.dispatch('getToken', { loginName, password })
return res
} catch (error) {
return false
}
}
複製程式碼
使用
// 請求前
service.interceptors.request.use(config => {
const token = store.getters.GET_TOKEN
if (!token) {
// 當token不存在時,自動重新發起請求
return reRequest({ config })
} else {
config.headers['Authorization'] = token
}
return config
})
service.interceptors.response.use(response => {
const res = response.data
if (res.status === 401) {
// 登入失效
// 重新重新整理token併發起請求
return reRequest(response)
}
return data
})
複製程式碼
6. 匯出封裝 GET/POST/DELETE/PUT/PATCH/...請求函式
自定義請求引數位置,在使用例項好的
service(params)
時,通過傳引數的 params 或者 data 決定放置位置
如果有傳引數,則會覆蓋預設的
引數 | 含義 | 預設值 |
---|---|---|
url | 請求地址 | —— |
method | 請求型別,有 GET/POST/DELETE/PUT/PATCH 可選 | GET |
params | 請求引數在url? 後面時,則把引數放在 params,適用於 GET/DELETE |
null |
data | 請求引數在body 體內時,則把引數放在 data 裡,適用於 POST/PUT/PATCH |
null |
headers | 請求頭,跟axios.create({ headers }) 一樣 |
—— |
responseType | 伺服器響應的資料型別,可選'arraybuffer', 'blob', 'document', 'json', 'text', 'stream' | json |
/**
* get請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} params - 請求引數
* @returns
*/
export const get = (url, params = {}) => {
params.t = new Date().getTime() // get方法加一個時間引數,解決ie下可能快取問題.
return service({
url: url,
method: 'GET',
params
})
}
/**
* delete請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} params - 請求引數
* @returns
*/
export const del = (url, params = {}) => {
params.t = new Date().getTime() // get方法加一個時間引數,解決ie下可能快取問題.
return service({
url: url,
method: 'DELETE',
params
})
}
/**
* post請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} data - 請求引數
* @returns
*/
export const post = (url, data = {}) => {
return service({
url,
method: 'POST',
data
})
}
/**
* put請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} data - 請求引數
* @returns
*/
export const put = (url, data = {}) => {
return service({
url,
method: 'PUT',
data
})
}
/**
* patch請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} data - 請求引數
* @returns
*/
export const patch = (url, data = {}) => {
return service({
url,
method: 'PATCH',
data
})
}
/**
* 當以上方法不滿足,則自定義引數和配置請求
* @param {Object} options
*/
export const fetch = options => service(options)
複製程式碼
原始碼
import axios from 'axios'
import store from 'store'
import { Message, MessageBox } from 'element-ui'
// 建立axios例項
const service = axios.create({
baseURL: process.env.BASE_API,
validateStatus: status => status < 500, // 攔截狀態碼大於500
headers: {
common: { Accept: 'application/json; charset=UTF-8' },
patch: { 'Content-Type': 'application/json; charset=UTF-8' },
post: { 'Content-Type': 'application/json; charset=UTF-8' },
put: { 'Content-Type': 'application/json; charset=UTF-8' }
},
timeout: 60000 // 請求超時時間
})
const paramsList = ['get', 'delete', 'patch']
const dataList = ['post', 'put']
const isTypeList = method => {
if (paramsList.includes(method)) {
return 'params'
} else if (dataList.includes(method)) {
return 'data'
}
}
const pending = {}
const CancelToken = axios.CancelToken
const removePending = (key, isRequest = false) => {
if (pending[key] && isRequest) {
pending[key]('取消重複請求')
}
delete pending[key]
}
const getRequestIdentify = (config, isResult = false) => {
let url = config.url
if (isResult) {
url = config.baseURL + config.url.substring(1, config.url.length)
}
const params = { ...(config[isTypeList(config.method)] || {}) }
delete params.t
return encodeURIComponent(url + JSON.stringify(params))
}
// 不需要token的介面
const noLogin = [
'/account/random',
'/account/token',
'/account/defaultPassword',
'/account/changeDefaultPassword'
]
// 是否正在重新整理的標記
// const isRefreshing = false
// 重試佇列,每一項將是一個待執行的函式形式
// const retryRequests = []
// request攔截器
service.interceptors.request.use(
config => {
const requestData = getRequestIdentify(config, true)
removePending(requestData, true)
config.cancelToken = new CancelToken(c => {
pending[requestData] = c
})
if (!noLogin.includes(config.url.replace(config.baseURL, ''))) {
const token = store.getters.GET_TOKEN
if (!token) {
// 當token不存在時,自動重新發起請求
// return reRequest({ config })
// 取消該次請求
pending[requestData]('cancelToken')
store.dispatch('logOut')
} else {
config.headers['Authorization'] = token
}
}
// 處理為空的引數,設定為null
handlerNullParams(config)
return config
},
error => {
Promise.reject(error)
}
)
// response攔截器
service.interceptors.response.use(
response => {
// 把已經完成的請求從 pending 中移除
const requestData = getRequestIdentify(response.config)
removePending(requestData)
const res = response.data
if (res.status === 401) {
// 登入失效
MessageBox.alert('登入失效,請重新登入', '許可權提示', {
confirmButtonText: '退出',
callback: () => store.dispatch('logOut')
})
// 重新重新整理token併發起請求
// return reRequest(response)
} else if (res.status !== 200) {
Message.error(res.msg || '系統異常')
return Promise.reject(res.msg || '系統異常')
}
return res
},
error => {
if (
!(
error &&
(error.message === '取消重複請求' ||
~error.message.indexOf('cancelToken'))
)
) {
if (error.code === 'ECONNABORTED') {
Message.error('請求超時')
} else if (error && error.response) {
// error.response.status
Message.error(error.response.data.msg || '系統異常')
} else {
Message.error('系統異常')
console.log(error)
}
}
return Promise.reject(error)
}
)
export default service
// 重新請求流程處理
// async function reRequest(response) {
// const { config } = response
// if (!isRefreshing) {
// isRefreshing = true
// const reRes = await refreshTokenFn()
// if (reRes) {
// config.headers['Authorization'] = store.getters.GET_TOKEN
// // 已經重新整理了token,將所有佇列中的請求進行重試
// retryRequests.map(cb => cb(store.getters.GET_TOKEN))
// // 清空列隊
// retryRequests = []
// config.baseURL = ''
// isRefreshing = false
// return service(config)
// } else {
// // 重新整理token失敗,跳回登入頁
// store.dispatch('logOut')
// }
// isRefreshing = false
// } else {
// return new Promise((resolve) => {
// // 將resolve放進列隊,用一個函式形式儲存,等token重新整理後直接執行
// retryRequests.push(token => {
// config.baseURL = ''
// config.headers['Authorization'] = token
// resolve(service(config))
// })
// })
// }
// }
// // 重新整理token方法
// async function refreshTokenFn() {
// try {
// const { loginName, password } = store.getters.GET_USER_INFO
// const res = await store.dispatch('getToken', { loginName, password })
// return res
// } catch (error) {
// return false
// }
// }
/**
* get請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} params - 請求引數
* @returns
*/
export const get = (url, params = {}) => {
params.t = new Date().getTime() // get方法加一個時間引數,解決ie下可能快取問題.
return service({
url: url,
method: 'GET',
params
})
}
/**
* delete請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} params - 請求引數
* @returns
*/
export const del = (url, params = {}) => {
params.t = new Date().getTime() // get方法加一個時間引數,解決ie下可能快取問題.
return service({
url: url,
method: 'DELETE',
params
})
}
/**
* post請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} data - 請求引數
* @returns
*/
export const post = (url, data = {}) => {
return service({
url,
method: 'POST',
data
})
}
/**
* put請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} data - 請求引數
* @returns
*/
export const put = (url, data = {}) => {
return service({
url,
method: 'PUT',
data
})
}
/**
* patch請求方法
* @export axios
* @param {String} url - 請求地址
* @param {Object} data - 請求引數
* @returns
*/
export const patch = (url, data = {}) => {
return service({
url,
method: 'PATCH',
data
})
}
/**
* post上傳檔案請求方法
* !! 必須使用formData方式
* @export axios
* @param {String} url - 請求地址
* @param {Object} data - 請求引數
* @returns
*/
export const postFile = (url, data = {}) => {
return service({
url,
method: 'POST',
data,
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 1000 * 60 * 3
})
}
/**
* get匯出檔案
* @export axios
* @param {String} url - 請求地址
* @param {Object} data - 請求引數
* @returns
*/
export const getExport = (url, params = {}) => {
return service({
url,
method: 'GET',
params,
responseType: 'blob',
timeout: 1000 * 60 * 3
})
}
/**
* 當以上方法不滿足,則自定義引數和配置請求
* @param {Object} options
*/
export const fetch = options => service(options)
複製程式碼
結語
廣州找工作img...簡歷彷彿入了海底一下...