都過了多少個愚人節了,還用擔心axios是否適合自己的業務?

京東菜鳥全球通快遞小哥發表於2020-04-01

封裝 axios 請求大同小異,本文提供思路和原始碼,不要以偏概全,適合自己業務才是最重要的!

功能點實現

  1. 基於 Restful 風格進行封裝
  2. 取消請求與攔截重複請求
  3. token 攔截
  4. 介面白名單
  5. token 自動登入(看自己個人業務是否需要,一般用於移動端)
  6. 匯出封裝 GET/POST/DELETE/PUT/PATCH/...請求函式

話不多說,萬事總要開頭,先來引入開頭

import axios from 'axios'
複製程式碼

1. 基於 Restful 風格進行封裝

GETPOST明顯的區別就是引數放置位置不同,前者放在url?後面,後者放在body體內。

然鵝,Restful 風格在其基礎新增了DELETEPUTPATCH請求型別,其中DELETE請求引數和GET一樣放在url?後面,PUTPATCH這兩個的請求引數則是和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

請求後的 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...簡歷彷彿入了海底一下...

個人部落格

相關文章