使用Typescript重構axios(十四)——實現攔截器


0. 系列文章

1.使用Typescript重構axios(一)——寫在最前面
2.使用Typescript重構axios(二)——項目起手,跑通流程
3.使用Typescript重構axios(三)——實現基礎功能:處理get請求url參數
4.使用Typescript重構axios(四)——實現基礎功能:處理post請求參數
5.使用Typescript重構axios(五)——實現基礎功能:處理請求的header
6.使用Typescript重構axios(六)——實現基礎功能:獲取響應數據
7.使用Typescript重構axios(七)——實現基礎功能:處理響應header
8.使用Typescript重構axios(八)——實現基礎功能:處理響應data
9.使用Typescript重構axios(九)——異常處理:基礎版
10.使用Typescript重構axios(十)——異常處理:增強版
11.使用Typescript重構axios(十一)——接口擴展
12.使用Typescript重構axios(十二)——增加參數
13.使用Typescript重構axios(十三)——讓響應數據支持泛型
14.使用Typescript重構axios(十四)——實現攔截器
15.使用Typescript重構axios(十五)——默認配置
16.使用Typescript重構axios(十六)——請求和響應數據配置化
17.使用Typescript重構axios(十七)——增加axios.create
18.使用Typescript重構axios(十八)——請求取消功能:總體思路
19.使用Typescript重構axios(十九)——請求取消功能:實現第二種使用方式
20.使用Typescript重構axios(二十)——請求取消功能:實現第一種使用方式
21.使用Typescript重構axios(二十一)——請求取消功能:添加axios.isCancel接口
22.使用Typescript重構axios(二十二)——請求取消功能:收尾
23.使用Typescript重構axios(二十三)——添加withCredentials屬性
24.使用Typescript重構axios(二十四)——防御XSRF攻擊
25.使用Typescript重構axios(二十五)——文件上傳下載進度監控
26.使用Typescript重構axios(二十六)——添加HTTP授權auth屬性
27.使用Typescript重構axios(二十七)——添加請求狀態碼合法性校驗
28.使用Typescript重構axios(二十八)——自定義序列化請求參數
29.使用Typescript重構axios(二十九)——添加baseURL
30.使用Typescript重構axios(三十)——添加axios.getUri方法
31.使用Typescript重構axios(三十一)——添加axios.all和axios.spread方法
32.使用Typescript重構axios(三十二)——寫在最后面(總結)

項目源碼請猛戳這里!!!

1. 前言

在官方的axios中,有一個非常重要而且非常好用的功能,那就是:請求和響應攔截器。請求攔截器就是可以在每個請求發送之前為請求做一些額外的東西,例如:我們可以在請求攔截器中為所有的請求添加token認證等信息,添加后再將請求發出。而響應攔截器就是當每個請求的響應回來之后我們可以先對其進行一道預處理,處理后再將響應返回給真正的請求。像如下使用方式:

// 添加一個請求攔截器
axios.interceptors.request.use(function (config) {
  // 在發送請求之前可以做一些事情
  return config;
}, function (error) {
  // 處理請求錯誤
  return Promise.reject(error);
});
// 添加一個響應攔截器
axios.interceptors.response.use(function (response) {
  // 處理響應數據
  return response;
}, function (error) {
  // 處理響應錯誤
  return Promise.reject(error);
});

// 刪除一個請求攔截器
const myInterceptor = axios.interceptors.request.use(function () {/*...*/})
axios.interceptors.request.eject(myInterceptor)

axios 對象上有一個 interceptors 對象屬性,該屬性又有 requestresponse 2 個屬性,它們都有一個 use 方法,use 方法支持 2 個參數,第一個參數類似 Promise 的 resolve 函數,第二個參數類似 Promise 的 reject 函數。我們可以在 resolve 函數和 reject 函數中執行同步代碼或者是異步代碼邏輯。

另外,也可以使用eject刪除某個攔截器。

接下來,我們就要在我們的axios中實現該功能。

2. 需求分析

通過觀察上面官方給出的使用示例,不管是請求攔截器axios.interceptors.request還是響應攔截器axios.interceptors.response,它們都有一個添加攔截器的use方法和刪除攔截器的eject方法。那么我們不妨可以這樣想:假設有一個攔截器類,該類上有兩個實例方法,分別是添加攔截器的use方法和刪除攔截器的eject方法,而請求攔截器和響應攔截器都是該類的實例,當我們在實例化axios時,我們給axios的實例上綁定interceptors.requestinterceptors.response屬性,同時這兩個屬性分別實例化了攔截器類,這樣我們就可以通過axios.interceptors.request.use來為axios實例添加攔截器,或者通過axios.interceptors.request.eject來刪除攔截器,說的再多,不如來看下代碼,偽代碼如下:

// InterceptorManager為攔截器類
class InterceptorManager {
    use(){
        
    },
    eject(){

   }
}

// Axios是之前創建的Axios類

class Axios {
  interceptors: Interceptors

  constructor() {
    this.interceptors = {
      request: new InterceptorManager(),       
      response: new InterceptorManager()
    }
  }
}

axios = new Axios()
axios.nterceptors.request.use()
axios.nterceptors.request.eject()

OK,需求明確了,我們就先來實現一下攔截器類InterceptorManager

3. 實現攔截器類

3.1 接口定義

根據需求,攔截器類需要包含兩個方法:useeject。並且use方法接收 2 個參數,第一個必選參數是 resolve 函數,第二個可選參數是 reject 函數,對於 resolve 函數的參數在請求攔截器和響應攔截器中有所不同,請求攔截器中參數是請求的配置對象config,其類型是 AxiosRequestConfig ,而響應攔截器中參數是響應對象response,其類型是 AxiosResponse 。所以我們的攔截器類類型接口定義如下:

// src/types/index.ts

export interface AxiosInterceptorManager<T> {
  use(resolved: ResolvedFn<T>, rejected?: RejectedFn): number

  eject(id: number): void
}

export interface ResolvedFn<T=any> {
  (val: T): T | Promise<T>
}

export interface RejectedFn {
  (error: any): any
}
  • 我們定義的攔截器類型接口AxiosInterceptorManager支持傳入一個泛型參數T,而這個T就是根據看創建的是請求攔截器還是響應攔截器對應傳入的AxiosRequestConfigAxiosResponse
  • use方法除了接收上面說的兩個函數作為參數,它還返回一個創建的攔截器的id,這個id用來標識攔截器;
  • eject方法接收攔截器的id作為參數,用來標明刪除哪個攔截器;

3.2 實現攔截器類

接口定義好之后,我們就可以來實現攔截器類了。我們在src/core目錄下創建interceptorManager.ts文件

  1. 首先我們創建一個類,類名叫InterceptorManager,在該類的構造函數中我們創建一個數組,用來存放創建的所有攔截器Interceptor

    // src/core/interceptorManager.ts
    
    import { ResolvedFn, RejectedFn } from '../types'
    interface Interceptor<T> {
      resolved: ResolvedFn<T>
      rejected?: RejectedFn
    }
    
    export default class InterceptorManager<T> {
      private interceptors: Array<Interceptor<T> | null>
    
      constructor() {
        this.interceptors = []
      }
    }
    

    每個攔截器Interceptor都是一個對象,包含兩個屬性resolvedrejected,對應use方法的兩個函數參數。

  2. 接着,我們為InterceptorManager類中添加use方法

     use(resolved: ResolvedFn<T>, rejected?: RejectedFn): number {
        this.interceptors.push({
          resolved,
          rejected
        })
        return this.interceptors.length - 1
      }
    

    該方法接收上文說的兩個函數參數,我們把這兩個函數參數組成一個對象存入第一步創建的數組interceptors中,並且返回該對象在數組中的索引作為該攔截器的id

  3. 最后,我們InterceptorManager類中添加eject方法

    eject(id: number): void {
     if (this.interceptors[id]) {
       this.interceptors[id] = null
     }
    }
    

    在該方法中,我們之所以沒有簡單粗暴的使用數組的slice方法將需要刪除的攔截器直接剔除掉,是因為如果這樣做了之后interceptors數組的長度就會發生變化,而我們在第2步中用攔截器在數組中的索引作為了攔截器的唯一標識id,如果數組長度變了,那么數組里的攔截器id也就變了,所以我們在數組中將需要刪除的攔截器置為null,在后面的邏輯中,我們只需判斷攔截器為不為null,如果為null不做處理就是了。

OK,攔截器類就已經暫時創建好了。接下來我們就來實現攔截器的調用。

4. 攔截器調用順序

在實現攔截器的調用之前,我們有必要了解一下當有多個攔截器時官方對每個攔截器的調用先后順序時怎樣的。為此,我們用官方axios寫了如下試驗代碼:

// 請求攔截器1
axios.interceptors.request.use(config => {
  config.headers.test += 'requestInterceptors1---'
  return config
})

// 請求攔截器2
axios.interceptors.request.use(config => {
  config.headers.test += 'requestInterceptors2---'
  return config
})

// 響應攔截器1
axios.interceptors.response.use(response => {
  response.data.test += '響應攔截器1'
  return response
})

// 響應攔截器2
axios.interceptors.response.use(response => {
  response.data.test += '響應攔截器2'
  return response
})

axios.get("/api/getuser", { headers: { test: "NLRX---" } }).then(res => {
  console.log(res);
});

我們為axios分別添加了兩個請求攔截器和兩個響應攔截器,然后發出一個get請求,我們可以通過觀察請求headers里面的test字段,以及響應data里面的test字段,我們就可以看出所有攔截器的調用先后順序時怎樣的。結果如下:

從結果中我們可以看到:

  • 請求攔截器:先添加的后執行,后添加的先執行;
  • 響應攔截器:按添加順序執行

其實,我們可以用一張圖來描述攔截器的工作流程:

OK,以上就是攔截器的調用順序,了解了這個順序后,我們就可以來着手實現攔截器的調用了。

5. 實現攔截器調用

實現攔截器調用之前我們先需要思考如下三個問題:

  1. 調用的邏輯應該實現在哪?
  2. 請求和響應攔截器調用分別是有順序的,並且要先調用請求攔截器,然后再調用響應攔截器,該怎么實現?
  3. 每個攔截器調用都應該是鏈式的,該怎么實現?

OK,基於以上三個問題,我們分別來解答。

5.1 在哪里實現調用邏輯

我們知道,請求攔截器應該在發出請求之前調用,響應攔截器應該是在響應回來之后調用,所以這兩個都跟核心請求功能相關。而我們又知道,在之前的實現方案中,所有的請求都是通過Axios類的request方法發出的,所以自然而然,攔截器的調用應該在request里面實現。

OK,這個問題搞明白以后,我們先根據第2章中的偽代碼,在Axios類的構造函數中為Axios實例添加interceptor屬性,如下:

// src/core/Axios.ts

import { InterceptorManager } from "./InterceptorManager";
export default class Axios {
  private interceptors: {
    request: InterceptorManager<AxiosRequestConfig>;
    response: InterceptorManager<AxiosResponse<any>>;
  };
  constructor() {
    this.interceptors = {
      request: new InterceptorManager<AxiosRequestConfig>(),
      response: new InterceptorManager<AxiosResponse>()
    };
  }
}

Axios實例添加了interceptor屬性后別忘了給Axios類類型接口里面添加上這個屬性接口:

// src/types/index.ts

export interface Axios {
  interceptors: {
    request: AxiosInterceptorManager<AxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse>;
  };
  // ...  
}

5.2 實現按順序調用

通過第4章的試驗,我們得知:請求攔截器和響應攔截器調用遵循以下順序:

  • 請求攔截器:先添加的后調用,后添加的先調用。
  • 響應攔截器:先添加的先調用,后添加的后調用。
  • 請求攔截器調用完后,再調用響應攔截器。

想要實現這種順序其實也不難,我們可以創建一個數組,把所有攔截器都按照執行的順序放入該數組,然后按照數組順序去調用即可,數組可以如下:

arr = ['請求攔截器2','請求攔截器1',...,'真實請求','響應攔截器1','響應攔截器2',...]

OK,辦法有了,那就來實現一下:

  1. 我們先創建一個數組arr,該數組內默認只存儲真實請求;

    interface PromiseArr<T> {
      resolved: ResolvedFn<T> | ((config: AxiosRequestConfig) => AxiosPromise);
      rejected?: RejectedFn;
    }
    let arr: PromiseArr<any>[] = [
        {
            resolved: dispatchRequest,
            rejected: undefined
        }
    ];
    
  2. 然后遍歷所有的請求攔截器,把每個請求攔截器都從arr的頭部插入,這樣就可以保證后面添加的在數組中前面,並且處於真實請求之前。

    this.interceptors.request.interceptors.forEach(interceptor => {
        if (interceptor !== null) {
            arr.unshift(interceptor);
        }
    });
    

    this.interceptors.request.interceptors是所有添加的請求攔截器,然后遍歷這些請求攔截器,把不為null(是null的表示被刪掉的攔截器)的從arr的頭部插入。

  3. 然后遍歷所有的響應攔截器,把每個響應攔截器都從arr的尾部插入,這樣就可以保證后面添加的在數組中后面,並且處於真實請求之后。

    this.interceptors.response.interceptors.forEach(interceptor => {
        if (interceptor !== null) {
            arr.push(interceptor);
        }
    });
    

OK,這樣我們就把這個調用順序數組構造好了。

5.3 實現鏈式調用

要實現鏈式調用,我們首先想到的就是通過Promise進行鏈式調用,如下:

let promise = Promise.resolve(config)

while (arr.length) {
    const { resolved, rejected } = arr.shift()!;
    promise = promise.then(resolved, rejected);
}

return promise

定義一個已經 resolvepromise,循環 arr,拿到每個攔截器對象,把它們的 resolved 函數和 rejected 函數添加到 promise.then 的參數中,這樣就相當於通過 Promise 的鏈式調用方式,實現了攔截器一層層的鏈式調用的效果。

5.4 完整代碼

OK,三個問題解決完后,我們的攔截器調用也就實現了,request方法完整代碼如下:

import { InterceptorManager } from "./InterceptorManager";

interface PromiseArr<T> {
  resolved: ResolvedFn<T> | ((config: AxiosRequestConfig) => AxiosPromise);
  rejected?: RejectedFn;
}

export default class Axios {
  private interceptors: {
    request: InterceptorManager<AxiosRequestConfig>;
    response: InterceptorManager<AxiosResponse<any>>;
  };
  constructor() {
    this.interceptors = {
      request: new InterceptorManager<AxiosRequestConfig>(),
      response: new InterceptorManager<AxiosResponse>()
    };
  }
  request(url: any, config?: any): AxiosPromise {
    if (typeof url === "string") {
      config = config ? config : {};
      config.url = url;
    } else {
      config = url;
    }

    let arr: PromiseArr<any>[] = [
      {
        resolved: dispatchRequest,
        rejected: undefined
      }
    ];

    this.interceptors.request.interceptors.forEach(interceptor => {
      if (interceptor !== null) {
        arr.unshift(interceptor);
      }
    });
    this.interceptors.response.interceptors.forEach(interceptor => {
      if (interceptor !== null) {
        arr.push(interceptor);
      }
    });
    let promise = Promise.resolve(config);

    while (arr.length) {
      const { resolved, rejected } = arr.shift()!;
      promise = promise.then(resolved, rejected);
    }

    return promise;
  }
  
  // ...
}

接下來,我們就可以編寫demo來測試下我們實現的攔截器效果如何。

6. demo編寫

examples 目錄下創建 interceptor目錄,在 interceptor目錄下創建 index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>interceptor demo</title>
  </head>
  <body>
    <script src="/__build__/interceptor.js"></script>
  </body>
</html>

接着再創建 app.ts 作為入口文件:

import axios from "../../src/axios";

// 請求攔截器1
let requestInterceptor1 = axios.interceptors.request.use(config => {
  config.headers.test += "requestInterceptors1---";
  return config;
});

// 請求攔截器2
axios.interceptors.request.use(config => {
  config.headers.test += "requestInterceptors2---";
  return config;
});

// 請求攔截器3
axios.interceptors.request.use(config => {
  config.headers.test += "requestInterceptors3---";
  return config;
});

// 響應攔截器1
axios.interceptors.response.use(response => {
  response.data.test += "響應攔截器1";
  return response;
});

// 響應攔截器2
let responseInterceptor2 = axios.interceptors.response.use(response => {
  response.data.test += "響應攔截器2";
  return response;
});

// 響應攔截器3
axios.interceptors.response.use(response => {
  response.data.test += "響應攔截器3";
  return response;
});

axios.interceptors.request.eject(requestInterceptor1);
axios.interceptors.response.eject(responseInterceptor2);

axios.get("/api/getuser", { headers: { test: "NLRX---" } }).then(res => {
  console.log(res);
});

demo 里面我們添加了 3 個請求攔截器,和 3 個響應攔截器,並且刪除了第1個請求攔截器和第2個響應攔截器。

路由接口沿用上篇文章的接口,故不需要添加新的路由接口。

最后在根目錄下的index.html中加上啟動該demo的入口:

<li><a href="examples/interceptor">interceptor</a></li>

OK,我們在命令行中執行:

# 同時開啟客戶端和服務端
npm run server | npm start

接着我們打開 chrome 瀏覽器,訪問 http://localhost:8000/ 即可訪問我們的 demo 了,我們點擊 interceptor,通過F12network 部分我們可以看到請求已正常發出:

從結果中,我們可以看到:請求攔截器1被刪除了,請求攔截器2和3正常工作,並且順序也是先3后2;響應攔截器2被刪除了,1和3正常工作,並且順序是先1后3。

OK,攔截器到此就實現完畢了。

(完)


免責聲明!

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



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