axios 封裝及 API 接口管理


前言

最近在跟后端對接請求的加解密時,發現之前封裝的 axios 模塊存在觸發兩次攔截器的問題,找了許久問題方才發現是 axios 沒有實例化而直接使用造成的。溯其根源,是之前封裝時並未仔細去看 axios 的文檔,沒完全理解代碼,直接就參考別人封裝好的例子,殊不知別人封裝的也是不完善。因此,結合自己的項目,梳理下 axios 的封裝及 API 接口的管理模塊。

Axios 封裝

axios 本身就是封裝好了的請求庫,為什么我們要對其進行二次封裝呢?這個原因很簡單,和大多數的封裝一樣,目的是為了簡化代碼使用以及便於管理維護。

安裝

npm install axios
// or
yarn add axios

使用

官方的文檔一開始就列舉了直接引入 axios 使用的方法,然而這樣子對我們進行后面的個性化定制不利,所以我們需要進行實例話后再使用,防止重復觸發攔截器。

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

// 判斷是否為開發模式
const isDev = process.env.NODE_ENV === 'development'
// 根據不同環境設置不同的請求地址
const baseURL = isDev ? '' : 'http://www.example.com'
// 請求超時設置
const timeout = 6000
// 創建 axios 實例
const http = axios.create({ baseURL, timeout })

// TODO: 攔截器

export { http, axios }

攔截器

在請求數據前和請求數據后我們通常需要做一些認證、錯誤處理、加解密等操作,利用 axios 提供的 interceptors 方法分別對請求和響應進行攔截處理。

// 攔截請求
http.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // TODO: 請求加密

    return config
  },
  (error: AxiosError) => {
    return Promise.reject(error)
  }
)

// 攔截響應
http.interceptors.response.use(
  (response: AxiosResponse) => {
    if (response) {
      // 返回文件類型時不做處理直接返回
      if (response.request.responseType === 'arraybuffer') return response

      const { code, msg, success } = response.data

      // TODO: 解密響應數據

      // 根據后端自定義的狀態碼進行操作
      switch (code) {
        case 7001:
          // TODO
          break
        default:
          // Pop message
          break
      }
    }

    return Promise.reject(response)
  },
  (error: AxiosError) => {
    if (error.response) {
      const { status, data } = error.response
      // 處理 HTTP 狀態碼
      switch (status) {
        case 401:
          // TODO: Login
          break
        default:
          // TODO: Pop message
          break
      }
    }

    return Promise.reject(error)
  }
)

以上只是簡單列舉了攔截器的使用,還有加解密、Loading 控制、請求超時處理等需要另外補充。

封裝

通常情況下我們可以直接使用 axios 的實例方法來調用 get、post 等請求,假如后端返回數據的默認結構為:

{
  code: 200,
  success: true,
  data: {},
  msg: '請求成功'
}

正常處理請求返回數據如下:

import { http } from './request'

http.get('/path', { id: 1 }).then(response => {
  const { data, code, success, msg } = response.data

  if (success) {
    // TODO: 處理獲取成功的數據
  } else {
    // TODO: 錯誤處理
  }
})

而通常情況下,我們請求數據只想手動處理頁面顯示所需的數據,那些異常的數據應該全部放到 catch 中處理或者交由全局進行統一處理,比如:

import { http } from './request'

// 處理特殊異常
http.get('/path', { id: 1 }).then(res => {
  // TODO: 處理獲取成功的數據
}).catch(err => {
  // TODO: 處理異常
})

// 不使用 catch 時
// 異常交由全局默認處理
http.get('/path', { id: 1 }).then(res => {
  // TODO: 處理獲取成功的數據
})

要默認處理異常,需要重寫 Promise 的 then , catch , finally 方法:

function RequestPromise(originalPromise: Promise<any>) {
  this._promise = originalPromise

  this._catchyPromise = Promise.resolve()
    .then(() => this._promise)
    .catch(err => console.error('Error', err)) // 默認處理異常

  const methods = ['then', 'catch', 'finally']

  for (const method of methods) {
    this[method] = function (...args: any) {
      this._promise = this._promise[method](...args)

      return this
    }
  }
}

從上面的方法可知,我們的異步請求會被攔截一道,獲取異常並進行錯誤輸出處理,重寫了 catch 方法,方便使用時能進行特殊處理。

// get 請求封裝
const get = <R = any>(uri: string, data?: any, config?: AxiosRequestConfig): Promise<R> => {
  // @ts-ignore
  return new RequestPromise(new Promise((resolve, reject) => {
    http
      .get(uri, { params: data, ...config })
      .then(response => {
        const { success, data } = response.data
        if (success) return resolve(data)
        return reject(response.data)
      })
      .catch(reject)
  }))
}

除了以上處理,假設我們要請求分頁列表,其后端返回的數據格式為:

{
  code: 200,
  success: true,
  data: {
    results: []
  },
  msg: '請求成功'
}

如果想默認返回 results 字段內容,我們可以在 post、get 請求方法的基礎上更進一步定制化請求方法,如:

// 列表請求封裝
const table = <R = ListResponse>(path: string, pagination?: number, data?: any, config?: AxiosRequestConfig): Promise<R> => {
  return new Promise((resolve, reject) => {
    const listConfig = {
      pageSize: 10,
      pageNo: pagination || 1
    }
    uri = `${uri}/action/table`
    get(uri, Object.assign({}, listConfig, data), config).then(data => resolve(data.results)).catch(reject)
  })
}

其實,如果是做管理后台系統,可以根據以上實例對常用的增刪查改進行封裝,調用時可大大節省時間。

掛載全局

考慮到會污染全局實例且不便於維護對接,很多人是不推薦掛載請求方法到全局實例上的,這些見仁見智吧。如果拋開不利因素,只為了在頁面中便捷地使用,可以將封裝好的請求方法綁定到全局。具體做法如下:

// main.js or main.ts

// Vue 2.x
import Vue from 'vue'

Vue.prototype.$get = get
Vue.prototype.$post = post
Vue.prototype.$table = table

// Vue 3.x
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.config.globalProperties.$get = get
app.config.globalProperties.$post = post
app.config.globalProperties.$table = table

在頁面中使用:

// xxx.vue

// Vue 2.x
this.$post()

// Vue 3.x
const { proxy } = getCurrentInstance()

proxy.$post()

如果項目中使用了 typescript ,為了能智能提示,需要在項目根目錄增加聲明文件:

// request.d.ts
import { AxiosInstance, AxiosRequestConfig } from 'axios'

declare module 'vue/types/vue' {
  interface Vue {
    $post<R = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>
    $get<R = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>
    $table<R = any>(uri: string, pagination?: number, data?: any, config?: AxiosRequestConfig): Promise<R>
  }
}

API 接口管理

如果我們將請求方法掛載到了全局,只要新建一個文件來管理請求的接口,在要使用的頁面引入對應接口即可。

// api.ts
export const ACCOUNT = {
  logout: 'account/logout',
  login: 'account/login'
}

在頁面配合請求方法進行使用:

import { ACCOUNT } from '@/config/api'

this.$post(ACCOUNT.logout).then(() => {
  // TODO
}

這樣子,請求和接口就完全分離了。再想想,還有哪里寫得不爽的?在使用的過程中,發現接口定義的時候總是要手動去命名,接口少還能接受,但是接口很多的情況下命名就是一件相當頭疼的事。可為了能夠使用 typescript 的智能提示功能,命名又是必要的。思前想后,最后通過 nodeJS 進行自動化生成接口文件,自動命名。用的時候在配置文件配置下就好,配置文件如下:

// config/api.js
/**
 * 接口配置文件
 * 接口名稱:[ 接口前綴, ...業務路徑 ]
 */
module.exports = {
  account: [
    'account',
    'logout',
    'login'
  ]
}

為了能夠實現自動更新,使用 nodemonconfig/api.js 文件進行監聽。文件更改時會自動執行生成腳本,具體的自動生成腳本可以根據自己要輸出的接口結構進行定制。

// build/api.js
// 接口生成腳本

const path = require('path')
const chalk = require('chalk')
const fs = require('fs-extra')
const api = require('../config/api')

const resolve = dir => {
  return path.join(__dirname, '..', dir)
}

/**
 * 生成 API 文件
 * 路徑:src/api/index.ts
 */
function initAPI() {
  let tmpStr = ''

  for (let key in api) {
    let uri = ''
    let str = `export const ${key.toUpperCase()} = {\n`

    const comma = index => api[key].length - 1 !== index ? ',' : ''

    api[key].forEach((item, index) => {
      const _key = key.toLowerCase()
      if (!index) {
        uri = item
        str = `const ${_key} = '${uri}/'\n` + str
        str += `  uri: ${_key}${comma(index)}\n`
      } else {
        str += `  ${item.replace(/(\/|\-)/g, '_')}: ${_key} + '${item}'${comma(index)}\n`
      }
    })
    str += '}\n\n'
    tmpStr += str
  }

  return tmpStr
}

const apiPath = resolve('src/api/index.ts')

const status = fs.outputFileSync(apiPath, initAPI())

if (status) {
  throw status
} else {
  console.log(chalk.blue('生成') + '  接口文件  ' + apiPath);
}

當然這種做法也是為了偷懶,如果有更好的做法歡迎留言討論。需要注意的是,如果通過 nodeJS 動態生成接口,需要在 .gitignore 中排除掉生成文件的提交,否則會產生提交沖突。

總結

不管是小程序、H5 還是管理系統,請求封裝還是很有必要的,倘若能統一,能在各個項目中復用那就再好不過了。接口管理不僅在項目中管理,使用時還需要配合接口文檔,項目中知道這個接口屬於那個模塊下就好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM