抽象思維訓練之 - axios再封裝

語言: CN / TW / HK

theme: vue-pro

此文為萬字長文詳解從零搭建企業級 vue3 + vite2+ ts4 框架全過程衍生篇之一,雖然隔得有些久了,但我還是會堅持把它更完,敬請期待~

image-20220605181348419.png

什麼是抽象?

抽象是從眾多的具體事物中,抽取共同的、本質的屬性,捨棄個別的、非本質屬性的過程

就像人們常說的梵高的畫作很抽象,是指畫的內容晦澀難懂嗎?顯然不是,而是指梵高在畫作中對藝術的表現力,豐富的感染力具備高度的提煉,讓見聞者彷彿身臨其境,能與作者感同身受。這種極具情感染力的情緒傳遞,才是梵高畫作抽象真正的意義所在。

抽象的過程就類似於寫一個公共元件,在面對不同的業務場景下既能夠有公共部分的使用避免重複編碼,也可以通過額外新增特定的屬性以適配對應的業務功能。

使用過程式碼檢查工具的小夥伴們應該知道,有一個衡量程式碼質量的標準就是“程式碼重複率”。減少編寫重複程式碼就意味著需要對封裝抽象有著較好的掌控能力,良好的封裝意味著後續更好的維護能力。

本篇通過axios的封裝來帶你體會抽象的魅力吧!

前置處理

在我們想要對某個模組進行封裝之前,先要明確兩個內容:

  1. 為什麼要封裝?
  2. 封裝後能達到的效果是什麼?

為什麼要封裝axios?

可能會有很多人好奇:axios 這麼強大了,既可以建立多個例項,又可以直接呼叫它的例項方法getpost等等去請求介面,為什麼還要封裝一層呢?是不是有些多此一舉了?封裝一層可以讓我多寫一篇文章(不是

我們通過簡單的實際開發場景模擬來了解背後的真正原因吧:

首先我們前端的服務起的是4000埠,假設後臺服務是8000埠,可以通過簡單的代理進行訪問:

js server: {    port: 4000, // 設定服務啟動埠號    cors: true, // 允許跨域 ​    // 設定代理    proxy: {        '/api': {            target: 'http://127.0.0.1:8000',            changeOrigin: true,            secure: false,            rewrite: path => path.replace(/^/api/, '')       }   } }

當我們要呼叫後臺定義的一個 get 介面/test/,就可以通過在url前新增/api正常訪問了:

  1. 有一個後臺介面http://127.0.0.1:4000/test/,請求後正常返回如下資訊

    js {    result: true, // 用於判斷本次請求操作是否成功,    data: 'result data' }

    前端如何獲取並處理呢?

  2. 首先引入Axios

    js import Axios from 'axios'

  3. 然後建立一個 axios 例項

    js const instance = Axios.create({ // 指定後臺的請求地址,通過新增字首'/api'實現代理訪問 baseURL: 'http://127.0.0.1:4000/api', })

  4. 然後直接呼叫Axios例項的get方法就能正常呼叫介面了

    js instance.get('/test/')

  5. 前端接收成功後就可以在.then裡處理資料了

    js instance.get('/test/').then(res => { console.log(res) })

    控制檯列印下可以看到返回了很多引數:

    image-20220313152907750.png 這一步可以感知到,在實際開發中其實真正需要關注的引數只有data裡的內容,其他引數都是我們不太需要關心的或者說不需要在每次返回的引數中進行處理的。

  6. 在日常使用場景中,不可避免地會出現介面掛掉的問題,比如常見的500(Internal Server Error),502(Bad Gateway)等等

    AXios 的例項方法getpost等返回的是個Promise物件,我們可以通過.catch進行捕獲異常處理:

    js instance.get('/test2/').then(res => {  console.log(res, 'res') }).catch(err => {  console.log(err, 'err') })

    模擬請求一個不存在的介面/test2/,控制檯列印如下:

    image-20220604205213472.png

    可以看到,報錯資訊會將完整的錯誤堆疊給丟擲,對使用者來說,這些都屬於無效資訊,唯一有效的是狀態碼404,所以需要用到攔截器對錯誤狀態碼處理成使用者友好的反饋資訊。

還有很多情況就不一一列舉了。到現在,你應該知道為什麼要對axios進行封裝了吧,主要就是為了在開發中:

  • 抽離公共配置,減少編寫重複的程式碼邏輯
  • 能夠專注於業務程式碼處理,而不需要額外分心考慮介面報錯、請求錯誤等問題

為了避免過度封裝反而帶來的低可維護性,我們僅主要針對以上兩點來完成封裝即可。

封裝後能達到的效果

在使用 axios 做後臺請求的時候,開發人員只需要考慮對正常請求的結果處理;對請求配置、錯誤請求、狀態異常等功能做到無感知,真正做到專注於業務開發。 image-20220604161754299.png 既然是對介面請求模組的封裝,我們自然需要熟悉介面 API 的風格規範,比如最常使用的 RESTful API 風格: 本文也是基於RESTful 風格 API 進行對應處理RESTful 架構詳解

RESTful 風格的 API具備與以下三個特徵:

  • 看 url 知道是進行什麼處理的
  • 看 http method 知道是幹什麼的
  • 看 http status code 就知道返回結果是什麼

通過請求方法和請求狀態碼的統一,能夠保證我們的“有效封裝”

功能搭建

建立一個 axios 例項:

ts import Axios, {    AxiosError,    AxiosResponse,    AxiosRequestConfig,    AxiosInstance } from 'axios' ​ const BASE_URL = 'http://127.0.0.1:4000/api' const TIME_OUT = 20 * 1000 ​ const instance: AxiosInstance = Axios.create({ baseURL: BASE_URL, // 後臺服務地址,根據實際情況調整    timeout: TIME_OUT // 設定請求最長時間 })

請求攜帶Token

當向後臺傳送請求時,有時候需要前後端通過統一的 token 傳遞用於校驗請求是否合法/避免跨域等等。

可以直接使用前置攔截器給請求頭上新增相應的引數

js instance.interceptors.request.use((config: AxiosRequestConfig) => {    config.headers && (config.headers['X-csrfToken'] = getToken())    return config })

獲取token方法:

每個專案對應的獲取token的方式不一樣,需要和後臺協同一致,這裡給出一種使用 cookie 傳遞 token 的示例

ts const getToken = (): string => {    const DEFAULT_X_CSRFTOKEN = 'NOT_PROVIDED'    const { cookie } = document    if (!cookie) return DEFAULT_X_CSRFTOKEN    // 後臺傳遞給前端的 cookie 名稱可以在模板檔案中使用 window 接收    const key = window.CSRF_COOKIE_NAME || 'csrftoken'    const patten = new RegExp(`^${key}=[\S]*`, 'g')    const value = cookie.split(';')?.find((item) => patten.test(item.trim()))    if (!value) return DEFAULT_X_CSRFTOKEN    return decodeURIComponent(value.split('=')[1] || DEFAULT_X_CSRFTOKEN) }

錯誤處理

首先定義一個錯誤返回的介面,分別是狀態碼、返回結果和錯誤資訊

ts interface IResponseError {    code: number, // 狀態碼    result: boolean, // 返回結果    message: string // 錯誤資訊 }

接著再定義一個狀態碼處理函式errorCodeHandler和錯誤資訊處理函式errorHandler,狀態碼對應資訊參考HTTP 狀態程式碼列表

這裡將狀態碼與對應的錯誤資訊提取為單獨的函式,保證函式功能單一原則的同時帶來更舒適的觀感。

ts const errorCodeHandler = (code, data): string | undefined => {    const message = data?.message    const msgMap = {        400: message || '400 error 請求無效',        401: '401 error 登入失效,請重新登入!',        403: '403 error 對不起,你沒有訪問許可權!',        404: '404 Not Found 請檢查請求路徑是否正確!',        500: message || '500 error 後臺錯誤,請聯絡開發人員!',        502: '502 error 平臺環境異常',        504: '504 error 閘道器超時,請重試!'   }    return msgMap[code] } ​ const errorHandler = (error): IResponseError => {    const { data, status, statusText } = error.response    const msg = errorMsgHandler(status, data) ||          `${status} error ${data ? data.message : statusText}`    alert(msg) // 可以引用各自的UI框架中全域性訊息提示的元件,此處使用alert簡單模擬報錯提示    return {        code: status,        result: false,        message: msg   } }

我們對常見的一些錯誤進行精準匹配並返回一些自定義的提示資訊,對使用者體驗更好,畢竟使用者對http狀態碼可能並沒有我們這些開發人員那樣熟悉。對不常見的錯誤返回直接返回原生資訊即可。

使用後置攔截器攔截錯誤請求

ts instance.interceptors.response.use(   (response: AxiosResponse) => {        const { data, status } = response        const { result, message } = data        if (result) return data        // 1、對於一些請求正常,但後臺處理失敗的內容進行攔截,返回對應錯誤資訊        alert(message || '請求異常,請重新整理重試')        return {            code: status,            message: message || '請求異常,請重新整理重試',            result: false       }   },   (error: AxiosError) => {        // 2、超出 2xx 範圍的狀態碼都會觸發該函式        return error.response ? errorHandle(error) : Promise.reject(error)   } )

後臺請求報錯一般有三種:

一種是環境因素:平臺不穩定,網路錯誤,請求超時等等,我們通過 errorHandle 對特定狀態碼做特定資訊返回處理;

一種是因為後臺程式碼錯誤導致的,一般會返回錯誤狀態碼 500(內部伺服器錯誤),也是通過 errorHandle 中對應處理了;

還有一種是請求正常,但是未能得到預期的返回結果,此時,狀態碼是返回 200 的,而由後臺控制返回欄位的 result 為 false,對應的是上文第一個攔截處理。該部分的處理需要和各自後臺開發約定好,具有一定的特異性。請以實際開發場景為準

取消請求

在實際應用場景中,取消請求通常會用在:

  1. 當在不同元件/頁面之間切換時,可以對上一個元件/頁面未完成的請求進行取消。
  2. 當用戶重複點選提交按鈕時,當第一個請求尚未完成時,取消重複的請求,避免多次提交

對於第二個場景,應該對錶單提交的互動完善,在第一次點選提交後,禁用提交按鈕或者給提交按鈕新增loading效果知道介面請求完成,這是當前大多數UI元件預設做到的事情,防止重複提交的事件被觸發,而不是取消重複請求。

因此,我們只需要做到對第一個場景的處理即可。

v0.22.0 開始,Axios 支援以 fetch API 方式—— AbortController 取消請求:

ts const controller = new AbortController(); ​ axios.get('/foo/bar', {   signal: controller.signal }).then(function(response) {   //... }); // 取消請求 controller.abort()

而大家常見的以CancelToken取消請求的方式其實已經被官方廢棄了(因為它的實現是是基於被撤銷的一個提案 cancelable promises proposal),雖然不影響使用,但與時俱進才是技術人需要的堅持!

使用前先檢視下瀏覽器的支援情況,有備無患"AbortController" | Can I use... Support tables for HTML5, CSS3, etc

image-20220605170652346.png

可以看到,稍微高一點版本的瀏覽器基本都是支援的,那就可以放心使用了。

頁面切換之前,取消未完成的所有請求,定義一個變數controllers用於存放所有請求的controller

ts let controllers: AbortController[] = []

在前置攔截器中,每當有請求發起,新增一個controller

ts // 前置攔截器(發起請求之前的攔截) instance.interceptors.request.use((config: AxiosRequestConfig) => {    const controller = new AbortController()    config.signal = controller.signal    controllers.push(controller)    return config })

定義並匯出一個取消方法,遍歷所有的controller並呼叫abort()方法

ts export const cancelRequest = () => {    controllers.forEach(controller => {        controller.abort()   })    controllers = [] }

在路由守衛中使用,每當頁面跳轉之前,呼叫cancelRequest,取消所有未完成的請求

ts import { cancelRequest } from '@/utils/axios' ​ router.beforeEach((to, from, next) => {    cancelRequest()    next() })

實現效果:

1-1654421861493.gif

請求方法匯出

將常用的請求方法做個簡單的封裝並匯出

ts const ajaxGet = (url: string, params?: any): Promise<AxiosResponse> => instance.get(url, { params }) const ajaxDelete = (url: string, params?: any): Promise<AxiosResponse> => instance.delete(url, { params }) const ajaxPost = (url: string, params: any, config?: AxiosRequestConfig): Promise<AxiosResponse> => instance.post(url, params, config) const ajaxPut = (url: string, params: any, config?: AxiosRequestConfig): Promise<AxiosResponse> => instance.put(url, params, config) const ajaxPatch = (url: string, params: any, config?: AxiosRequestConfig): Promise<AxiosResponse> => instance.patch(url, params, config) ​ ​ export {    ajaxGet,    ajaxDelete,    ajaxPost,    ajaxPut,    ajaxPatch }

使用方式

ts import { ajaxGet } from '@/utils/axios' ​ // 1. 鏈式呼叫 ajaxGet('/test1').then(res => {    console.log(res, ' res') }).catch((e) => {    console.log(e, 'error') }) ​ // 2. try await catch try {    const res = await ajaxGet('/test1')    console.log(res, 'res2') } catch (e) {    console.log(e, 'error2') }

倆種使用方式其實沒有什麼優劣之分,注意在一個專案中的統一即可

寫在最後

抽象雖好,但要避免過度,基於以上能力的一個axios 模組其實除了某些極特殊的場景外其實已經足夠用了。 諸如一些請求快取,進度條等可以根據專案情況適度新增。

原始碼倉庫地址

https://github.com/JasonLuox/fronted/blob/main/src/utils/axios.ts

參考

抽象(科學學概念)

axios/axios: Promise based HTTP client for the browser and node.js

HTTP 狀態程式碼列表

RESTful 架構詳解