1. 程式人生 > >聊聊 Vue 中 axios 的封裝

聊聊 Vue 中 axios 的封裝

聊聊 Vue 中 axios 的封裝

axios 是 Vue 官方推薦的一個 HTTP 庫,用 axios 官方簡介來介紹它,就是:

Axios 是一個基於 promise 的 HTTP 庫,可以用在瀏覽器和 node.js 中。

作為一個優秀的 HTTP 庫,axios 打敗了曾經由 Vue 官方團隊維護的 vue-resource,獲得了 Vue 作者尤小右的大力推薦,成為了 Vue 專案中 HTTP 庫的最佳選擇。

雖然,axios 是個優秀的 HTTP 庫,但是,直接在專案中使用並不是那麼方便,所以,我們需要對其進行一定程度上的配置封裝,減少重複程式碼,方便呼叫。下面,我們就來聊聊 Vue 中 axios 的封裝。

開始

其實,網上關於 axios 封裝的程式碼不少,但是大部分都是在入口檔案(main.js)中進行 axios 全域性物件屬性定義的形式進行配置,類似於如下程式碼:

axios.defaults.timeout = 10000

該方案有兩個不足,首先,axios 封裝程式碼耦合進入入口檔案,不方便後期維護;其次,使用 axios 全域性物件屬性定義的方式進行配置,程式碼過於零散。

針對問題一,我使用了 Vue 原始碼結構中的一大核心思想——將功能拆分為檔案,方便後期的維護。單獨建立一個 http.js 或者 http.ts 檔案,在檔案中引入 axios 並對其進行封裝配置,最後將其匯出並掛載到 Vue 的原型上即可。此時,每次修改 axios 配置,只需要修改對應的檔案即可,不會影響到不相關的功能。

針對問題二,採用 axios 官方推薦的,通過配置項建立 axios 例項的方式進行配置封裝。

程式碼如下:

// http.js
import axios from 'axios'
// 建立 axios 例項
const service = axios.create({
  // 配置項
})

根據環境設定 baseURL

baseURL 屬性是請求地址字首,將自動加在 url 前面,除非 url 是個絕對地址。正常情況下,在開發環境下和生產模式下有著不同的 baseURL,所以,我們需要根據不同的環境切換不同的 baseURL。

在開發模式下,由於有著 devServer 的存在,需要根據固定的 url 字首進行請求地址重寫,所以,在開發環境下,將 baseURL 設為某個固定的值,比如:/apis

在生產模式下,根據 Java 模組的請求字首的不同,可以設定不同的 baseURL。

具體程式碼如下:

// 根據 process.env.NODE_ENV 區分狀態,切換不同的 baseURL
const service = axios.create({
    baseURL: process.env.NODE_ENV === 'production' ? `/java` : '/apis',
})

統一設定請求頭

在這裡和大家聊一個問題,什麼是封裝?在我看來,封裝是通過更少的呼叫程式碼覆蓋更多的呼叫場景。

由於,大部分情況下,請求頭都是固定的,只有少部分情況下,會需要一些特殊的請求頭,所以,在這裡,我採用的方案是,將普適性的請求頭作為基礎配置。當需要特殊請求頭時,將特殊請求頭作為引數傳入,覆蓋基礎配置。

程式碼如下:

const service = axios.create({
    ...
    headers: {
        get: {
          'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
          // 在開發中,一般還需要單點登入或者其他功能的通用請求頭,可以一併配置進來
        },
        post: {
          'Content-Type': 'application/json;charset=utf-8'
          // 在開發中,一般還需要單點登入或者其他功能的通用請求頭,可以一併配置進來
        }
  },
})

跨域、超時、響應碼處理

axios 中,提供是否允許跨域的屬性——withCredentials,以及配置超時時間的屬性——timeout,通過這兩個屬性,可以輕鬆處理跨域和超時的問題。

下面,我們來說說響應碼處理:

axios 提供了 validateStatus 屬性,用於定義對於給定的HTTP 響應狀態碼是 resolve 或 reject promise。所以,正常設定的情況下,我們會將狀態碼為 2 系列或者 304 的請求設為 resolve 狀態,其餘為 reject 狀態。結果就是,我們可以在業務程式碼裡,使用 catch 統一捕獲響應錯誤的請求,從而進行統一處理。

但是,由於我在程式碼裡面使用了 async-await,而眾所周知,async-await 捕獲 catch 的方式極為麻煩,所以,在此處,我選擇將所有響應都設為 resolve 狀態,統一在 then 處理。

此部分程式碼如下:

const service = axios.create({
    // 跨域請求時是否需要使用憑證
    withCredentials: true,
    // 請求 30s 超時
    timeout: 30000,
    validateStatus: function () {
        // 使用async-await,處理reject情況較為繁瑣,所以全部返回resolve,在業務程式碼中處理異常
        return true
    },
})

請求、響應處理

在不使用 axios 的情況下,每次請求或者接受響應,都需要將請求或者響應序列化。

而在 axios 中, transformRequest 允許在向伺服器傳送請求前,修改請求資料;transformResponse 在傳遞給 then/catch 前,允許修改響應資料。

通過這兩個鉤子,可以省去大量重複的序列化程式碼。

程式碼如下:

const service = axios.create({
    // 在向伺服器傳送請求前,序列化請求資料
    transformRequest: [function (data) {
        data = JSON.stringify(data)
        return data
    }],
    // 在傳遞給 then/catch 前,修改響應資料
    transformResponse: [function (data) {
        if (typeof data === 'string' && data.startsWith('{')) {
            data = JSON.parse(data)
        }
        return data
    }]
})

攔截器

攔截器,分為請求攔截器以及響應攔截器,分別在請求或響應被 then 或 catch 處理前攔截它們。

之前提到過,由於 async-await 中 catch 難以處理的問題,所以將出錯的情況也作為 resolve 狀態進行處理。但這帶來了一個問題,請求或響應出錯的情況下,結果沒有資料協議中定義的 msg 欄位(訊息)。所以,我們需要在出錯的時候,手動生成一個符合返回格式的返回資料。

由於,在業務中,沒有需要在請求攔截器中做額外處理的需求,所以,請求攔截器的 resolve 狀態,只需直接返回就可以了。

請求攔截器程式碼如下:

// 請求攔截器
service.interceptors.request.use((config) => {
    return config
}, (error) => {
    // 錯誤拋到業務程式碼
    error.data = {}
    error.data.msg = '伺服器異常,請聯絡管理員!'
    return Promise.resolve(error)
})

再來聊聊響應攔截器,還是之前的那個問題,除了請求或響應錯誤,還有一種情況也會導致返回的訊息體不符合協議規範,那就是狀態碼不為 2 系列或 304 時。此時,我們還是需要做一樣的處理——手動生成一個符合返回格式的返回資料。但是,有一點不一樣,我們還需要根據不同的狀態碼生成不同的提示資訊,以方便處理上線後的問題。

響應攔截器程式碼如下:

// 根據不同的狀態碼,生成不同的提示資訊
const showStatus = (status) => {
    let message = ''
    // 這一坨程式碼可以使用策略模式進行優化
    switch (status) {
        case 400:
            message = '請求錯誤(400)'
            break
        case 401:
            message = '未授權,請重新登入(401)'
            break
        case 403:
            message = '拒絕訪問(403)'
            break
        case 404:
            message = '請求出錯(404)'
            break
        case 408:
            message = '請求超時(408)'
            break
        case 500:
            message = '伺服器錯誤(500)'
            break
        case 501:
            message = '服務未實現(501)'
            break
        case 502:
            message = '網路錯誤(502)'
            break
        case 503:
            message = '服務不可用(503)'
            break
        case 504:
            message = '網路超時(504)'
            break
        case 505:
            message = 'HTTP版本不受支援(505)'
            break
        default:
            message = `連接出錯(${status})!`
    }
    return `${message},請檢查網路或聯絡管理員!`
}

// 響應攔截器
service.interceptors.response.use((response) => {
    const status = response.status
    let msg = ''
    if (status < 200 || status >= 300) {
        // 處理http錯誤,拋到業務程式碼
        msg = showStatus(status)
        if (typeof response.data === 'string') {
            response.data = { msg }
        } else {
            response.data.msg = msg
        }
    }
    return response
}, (error) => {
    // 錯誤拋到業務程式碼
    error.data = {}
    error.data.msg = '請求超時或伺服器異常,請檢查網路或聯絡管理員!'
    return Promise.resolve(error)
})

tips:友情提示,上面那一坨 switch-case 程式碼,可以使用策略模式進行優化~

支援 TypeScript

由於前段時間,我在部門內推了 TypeScript,為了滿足自己的強迫症,將所有 js 檔案改寫為了 ts 檔案。由於 axios 本身有 TypeScript 相關的支援,所以只需要把對應的型別匯入,然後賦值即可。

完整程式碼

// http.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'

const showStatus = (status: number) => {
  let message = ''
  switch (status) {
    case 400:
      message = '請求錯誤(400)'
      break
    case 401:
      message = '未授權,請重新登入(401)'
      break
    case 403:
      message = '拒絕訪問(403)'
      break
    case 404:
      message = '請求出錯(404)'
      break
    case 408:
      message = '請求超時(408)'
      break
    case 500:
      message = '伺服器錯誤(500)'
      break
    case 501:
      message = '服務未實現(501)'
      break
    case 502:
      message = '網路錯誤(502)'
      break
    case 503:
      message = '服務不可用(503)'
      break
    case 504:
      message = '網路超時(504)'
      break
    case 505:
      message = 'HTTP版本不受支援(505)'
      break
    default:
      message = `連接出錯(${status})!`
  }
  return `${message},請檢查網路或聯絡管理員!`
}

const service = axios.create({
  // 聯調
  baseURL: process.env.NODE_ENV === 'production' ? `/` : '/apis',
  headers: {
    get: {
      'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
    },
    post: {
      'Content-Type': 'application/json;charset=utf-8'
    }
  },
  // 是否跨站點訪問控制請求
  withCredentials: true,
  timeout: 30000,
  transformRequest: [(data) => {
    data = JSON.stringify(data)
    return data
  }],
  validateStatus () {
    // 使用async-await,處理reject情況較為繁瑣,所以全部返回resolve,在業務程式碼中處理異常
    return true
  },
  transformResponse: [(data) => {
    if (typeof data === 'string' && data.startsWith('{')) {
      data = JSON.parse(data)
    }
    return data
  }]
})

// 請求攔截器
service.interceptors.request.use((config: AxiosRequestConfig) => {
    return config
}, (error) => {
    // 錯誤拋到業務程式碼
    error.data = {}
    error.data.msg = '伺服器異常,請聯絡管理員!'
    return Promise.resolve(error)
})

// 響應攔截器
service.interceptors.response.use((response: AxiosResponse) => {
    const status = response.status
    let msg = ''
    if (status < 200 || status >= 300) {
        // 處理http錯誤,拋到業務程式碼
        msg = showStatus(status)
        if (typeof response.data === 'string') {
            response.data = {msg}
        } else {
            response.data.msg = msg
        }
    }
    return response
}, (error) => {
    // 錯誤拋到業務程式碼
    error.data = {}
    error.data.msg = '請求超時或伺服器異常,請檢查網路或聯絡管理員!'
    return Promise.resolve(error)
})

export default service