使用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. 前言

在實際項目中,所有請求的請求配置對象config中有些字段其實都是相同的,例如請求超時事件timeout,亦或者說我們需要給所有請求都添加一個相同的字段,例如在進行身份認證的時候我們需要給所有請求都添加Authorization。我們現在實現的axios所有請求配置都是獨立的,也就是說如果你需要給所有請求都加上某個配置字段,那么你需要在配置axios的配置對象的時候都加上這一字段,這無疑將會產生許多重復代碼。而官方的axios為我們提供了默認配置對象axios.defaults,我們可以把所有相同的配置字段都寫入該默認配置對象,那么這個配置字段將會在所有的請求中都生效。

接下來,我們也要實現這一默認配置功能。其實,這沒有多么復雜,我們默認提供一個配置對象,然后只需將用戶配置對象與默認配置對象進行合並,然后發出請求即可。

OK,我們接下來就來實現它。

2. 創建默認配置對象defaults

根據官方axios文檔給出的默認配置示例:

axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

其中:

  • axios.defaults.headers.common['Authorization'] = AUTH_TOKEN表示給所有請求的headers都添加Authorization,並且值為AUTH_TOKEN
  • axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';表示給所有POST請求的headers都添加Content-Type,並且值為application/x-www-form-urlencoded

有了以上概念,我們就可以創建出默認對象defaultes,我們在src目錄下新建defaultes.ts文件,在該文件內創建默認對象defaultes,如下:

// src/defaultes.ts

import { AxiosRequestConfig } from "./types";

const defaults: AxiosRequestConfig = {
  timeout: 0,
  headers: {
    common: {
      Accept: "application/json, text/plain, */*"
    }
  }
};

const methodsNoData = ["delete", "get", "head", "options"];

methodsNoData.forEach(method => {
  defaults.headers[method] = {};
});

const methodsWithData = ["post", "put", "patch"];

methodsWithData.forEach(method => {
  defaults.headers[method] = {
    "Content-Type": "application/x-www-form-urlencoded"
  };
});
export default defaults;

我們暫時為默認配置對象defaults中只添加了默認請求超時時間timeout和請求頭headers,並且我們在headers中設置了common屬性,用於存放所有請求都需要的請求頭字段,另外與common同級下還創建了每個請求方式屬性,用於存放不同請求所特有的請求頭字段。例如像需要數據的請求方式postputpatch我們為其默認添加了Content-Type字段,而不需要數據的請求方式deletegetheadoptions則為其留空。(其實默認配置對象里面的內容遠不止這些,詳細內容可查看這里~

OK,默認配置對象defaults就已經創建好了。

3. 向Axios類中添加默認配置對象

在官方axios中,從axios對象上可以點出來defaults對象,所以我們還需要將創建好的默認配置對象添加到Axios類中,從而可以在實例axios對象上點出來defaults

// src/core/Axios.ts

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

僅僅是這樣還不行,雖然現在axios對象可以點出來defaults,但是點出來defaults卻是一個空{},我們還應該把上面創建的默認配置對象傳進來,確保axios對象點出來的是真正的defaults

我們把上面創建的默認配置對象通過Axios類的構造函數傳進來,如下:


export default class Axios {
  defaults: AxiosRequestConfig;
  interceptors: {
    request: InterceptorManager<AxiosRequestConfig>;
    response: InterceptorManager<AxiosResponse<any>>;
  };
  constructor(defaultConfig: AxiosRequestConfig) {
    this.defaults = defaultConfig;
    this.interceptors = {
      request: new InterceptorManager<AxiosRequestConfig>(),
      response: new InterceptorManager<AxiosResponse>()
    };
  }
}

然后在src/axios.ts中創建axios實例的地方接收該配置對象:

import { AxiosInstance, AxiosRequestConfig } from "./types";
import Axios from "./core/Axios";
import { extend } from "./helpers/util";
import defaults from "./defaultes";

function getAxios(config: AxiosRequestConfig): AxiosInstance {
  const context = new Axios(config);
  const axios = Axios.prototype.request.bind(context);

  extend(axios, context);

  return axios as AxiosInstance;
}

const axios = getAxios(defaults);

export default axios;

這樣我們就可以在執行 getAxios創建 axios 對象的時候,把默認配置傳入了。現在才算是把創建的默認配置對象defaults真正的添加到Axios類中了,另外,別忘了給Axios類的類型接口定義中添加該字段:

export interface Axios {
  defaults: AxiosRequestConfig;
  // ...
}

默認配置對象有了之后,接下來,我們就該把用戶的配置對象跟默認配置對象做一合並,把合並后配置對象隨着請求發出就大功告成啦。

4. 合並配置對象

所謂合並配置對象,就是將默認配置對象defaults與用戶自己配置的請求配置對象config進行合並,然后將合並后的配置對象作為真正的請求配置對象發出請求。合並之前,我們先來觀察一下要合並的兩個對象:

defaults = {
  method: 'get',

  timeout: 0,

  headers: {
    common: {
      Accept: 'application/json, text/plain, */*'
    }
  }
}

userConfig = {
  url: '/config/post',
  method: 'post',
  data: {
    a: 1
  },
  headers: {
    test: '321'
  }
}

mergedConfig = {
  url: '/config/post',
  method: 'post',
  data: {
    a: 1
  },
  timeout: 0,
  headers: {
    common: {
      Accept: 'application/json, text/plain, */*'
    }
    test: '321'
  }
}

通過觀察,我們發現,這兩個對象的合並可不是簡簡單單的字段合並,這里面要分情況處理:

  • 對於timeoutresponseType等這些常規屬性,合並起來比較容易,即如果用戶配置了就用用戶配置的,如果用戶沒配置,則用默認配置的;
  • 對於一些屬性如 urlmethodparamsdata,這些屬性都是跟每個請求息息相關的,請求不同從而千變萬化,所以像這四個屬性我們在合並的時候不管默認配置對象里面有沒有,我們只取用戶配置的;
  • 對於headerauth等這些屬性就比較復雜了,這些屬性的合並可不是取這個不取那個的問題,而是要將默認配置的與用戶配置的做一次深度合並。如在headers中,字段不相同的要拷貝合並在一起,字段相同的,內容不同也要拷貝合並在一起;

了解了以上三種情況后,接下來我們在合並的時候就要分情況處理。

首先,在src/core目錄下創建mergeConfig.ts文件,在該文件內編寫合並函數,函數框架如下:

import { AxiosRequestConfig } from "../types";

export default function mergeConfig(
  defaultConfig: AxiosRequestConfig,
  userConfig?: AxiosRequestConfig
): AxiosRequestConfig {
  let config = Object.create(null); // 創建空對象,作為最終的合並結果

  // 1.常規屬性,如果用戶配置了就用用戶配置的,如果用戶沒配置,則用默認配置的;

  // 2.只接受用戶配置,不管默認配置對象里面有沒有,我們只取用戶配置的;

  // 3.復雜對象深度合並

  return config;
}

OK,接下里我們就根據不同情況分別處理。

4.1 常規屬性

對於常規屬性,我們遵循如果用戶配置了就用用戶配置的,如果用戶沒配置,則用默認配置的;

// 1.常規屬性,如果用戶配置了就用用戶配置的,如果用戶沒配置,則用默認配置的;
  let defaultToUserConfig = [
    "baseURL",
    "transformRequest",
    "transformResponse",
    "paramsSerializer",
    "timeout",
    "withCredentials",
    "adapter",
    "responseType",
    "xsrfCookieName",
    "xsrfHeaderName",
    "onUploadProgress",
    "onDownloadProgress",
    "maxContentLength",
    "validateStatus",
    "maxRedirects",
    "httpAgent",
    "httpsAgent",
    "cancelToken",
    "socketPath"
  ];
  defaultToUserConfig.forEach(prop => {
    userConfig = userConfig || {};
    // 如果用戶配置里有
    if (typeof userConfig[prop] !== "undefined") {
      // 則用用戶配置里的
      config[prop] = userConfig[prop];
      // 如果用戶配置里沒有,默認配置里有
    } else if (typeof defaultConfig[prop] !== "undefined") {
      // 則用默認配置里的
      config[prop] = defaultConfig[prop];
    }
  });

4.2 只接受用戶配置

對於 urlmethodparamsdata這些屬性,只接受用戶配置,不管默認配置對象里面有沒有,我們只取用戶配置的;

// 2.只接受自定義配置,不管默認配置對象里面有沒有,我們只取用戶配置的;
let valueFromUserConfig = ["url", "method", "params", "data"];
valueFromUserConfig.forEach(prop => {
  userConfig = userConfig || {};
  if (typeof userConfig[prop] !== 'undefined') {
    config[prop] = userConfig[prop];
  }
});

4.3 復雜對象深度合並

對於headerauth等這些屬性我們就要進行深度合並,例如在默認配置對象和用戶配置對象的headers屬性中,我們需要把兩個headers內字段不相同的屬性要拷貝合並在一起,如果屬性字段相同的,那么屬性內容不同也要拷貝合並在一起;

// 3.復雜對象深度合並
let mergeDeepProperties = ["headers", "auth", "proxy"];
mergeDeepProperties.forEach(prop => {
    userConfig = userConfig || {};
    if (isObject(userConfig[prop])) {
        config[prop] = deepMerge(defaultConfig[prop], userConfig[prop]);
    } else if (typeof userConfig[prop] !== 'undefined') {
        config[prop] = userConfig[prop];
    } else if (isObject(defaultConfig[prop])) {
        config[prop] = deepMerge(defaultConfig[prop]);
    } else if (typeof defaultConfig[prop] !== 'undefined') {
        config[prop] = defaultConfig[prop];
    }
});

對於上述代碼,還是拿headers屬性舉個例子說明一下:

  • 如果在用戶配置對象userConfig中配置了headers屬性,並且該屬性是個對象,那么就調用deepMerge函數把默認配置對象defaultConfig中的headers和用戶配置對象userConfig中的headers進行合並,最后把合並結果放入最終返回的config對象中的headers;
  • 如果userConfig中的headers不是對象,並且不為空,那直接就把它放入最終返回的config對象中的headers;
  • 如果userConfig中的headers為空,表示用戶沒有配置該屬性,並且如果defaultConfig中的headers是個對象,那就直接把defaultConfig中的headers深拷貝一份放入最終返回的config對象中的headers`;
  • 如果userConfig中的headers為空,並且defaultConfig中的headers不是對象,也不為空,那直接就把它放入最終返回的config對象中的headers;

這就是深度合並的邏輯,另外,這里面還調用的一個深度合並的工具函數deepMerge,接下來,我們就在src/helpers/util.ts中實現這個工具函數,該函數支持傳入若干個對象,把傳入的所有對象進行合並,最后返回。如下:

export function deepMerge(...objs: any[]): any {
  const result = Object.create(null);
  
  for (let i = 0; i < objs.length; i++) {
    const obj = objs[i];
    for (let key in obj) {
      assignValue(obj[key], key);
    }
  }
    
  function assignValue(val: any, key: string) {
    if (isObject(result[key]) && isObject(val)) {
      result[key] = deepMerge(result[key], val);
    } else if (isObject(val)) {
      result[key] = deepMerge({}, val);
    } else {
      result[key] = val;
    }
  }
  return result;
}

代碼說明:

  • 函數內部先創建了一個空對象result,作為最終返回的結果對象;
  • 然后遍歷傳進來所有對象,每個對象再遍歷所有的屬性,調用assignValue子函數將當前遍歷的對象中的每個屬性都拷貝到result上;
  • 把所有傳進來的對象遍歷完畢后,即把所有對象的所有屬性都拷貝到了result上,最終將result返回;

4.4 添加到request方法中

OK,合並邏輯實現好之后,我們就可以在Axios類的request方法中將默認配置對象與用戶配置對象進行合並了。

// src/core/Axios.ts

import mergeConfig from "./mergeConfig";

request(url: any, config?: any): AxiosPromise {
    if (typeof url === "string") {
      config = config ? config : {};
      config.url = url;
    } else {
      config = url;
    }

    config = mergeConfig(this.defaults, config);
    
    // ...
}

5. 扁平化headers

經過上面的配置對象合並后,其他屬性都可以了,但是合並出來的headers卻是如下形式的:

headers: {
  common: {
    Accept: 'application/json, text/plain, */*'
  },
  post: {
    'Content-Type':'application/x-www-form-urlencoded'
  }
}

而真正發請求是所需要的headers是這樣的:

headers: {
  Accept: 'application/json, text/plain, */*',
 'Content-Type':'application/x-www-form-urlencoded'
}

所以,我們還需要把合並后的headers扁平化,即把所有的屬性提取出來放入headers下。這里要注意的是,對於 common 中定義的 header 字段,我們都要提取,而對於 postget 這類提取,需要和該次請求的方法對應。

OK,那么我們就來實現一個函數,用於將合並后的headers扁平化,在src/helpers/headers.ts中創建flattenHeaders函數,如下:

// src/helpers/headers.ts

export function flattenHeaders(headers: any, method: Method): any {
  if (!headers) {
    return headers
  }
  headers = deepMerge(headers.common || {}, headers[method] || {}, headers)

  const methodsToDelete = ['delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'common']

  methodsToDelete.forEach(method => {
    delete headers[method]
  })

  return headers
}

我們通過 deepMerge 的方式把 commonpost 的屬性拷貝到 headers 這一級,然后再把 commonpost 這些屬性刪掉。最后返回的headers就是我們想要的扁平化后的headers

實現好之后,我們就在src/core/dispatchRequest.ts文件中真正發送請求之前調用它:

function processConfig(config: AxiosRequestConfig): void {
  config.url = transformUrl(config);
  config.headers = transformHeaders(config);
  config.data = transformRequestData(config);
  config.headers = flattenHeaders(config.headers,config.method!)
}

這樣確保我們了配置中的 headers 是可以正確添加到請求 header 中的。

OK,終於該合並的已經合並完了,接下來,我們就可以編寫demo來測試下效果如何。

6. demo編寫

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

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

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

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

axios.defaults.headers.common["NLRX"] = "Hello NLRX";
axios.defaults.headers.post["NLRX1"] = "post NLRX";
axios.defaults.headers.get["NLRX2"] = "get NLRX";

axios({
  url: "/api/mergeConfig",
  method: "post",
  data: qs.stringify({
    a: 1
  }),
  headers: {
    test: "321"
  }
}).then(res => {
  console.log(res.data);
});

在該demo中,我們顯示的給默認配置對象添加了 postgetcommonheaders,並且我們在請求中的配置對象也配置了headers,另外,我們的默認配置對象默認的會給post請求加上 Content-Type 字段,它的值是 application/x-www-form-urlencoded

我們可以預測下該請求中的headers應該包含哪些內容,由於這個請求時post類型,故axios.defaults.headers.get["NLRX2"] = "get NLRX";不應該生效,所以它的headers至少應該包含如下:

headers = {
  // ...
  Accept: 'application/json, text/plain, */*',
  Content-Type:'application/x-www-form-urlencoded',
  NLRX:"Hello NLRX",
  NLRX1 : "post NLRX",
  test: "321",
  // ...  
}

我們可以在demo結果中觀察驗證是否如此。

接着在 server/server.js 添加新的接口路由:

// 默認配置合並
router.post("/api/mergeConfig", function(req, res) {
  res.json(req.body);
});

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

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

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

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

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

從結果中我們可以看到,跟我們之前預測的結果完全相符,至此,默認配置合並就已經實現了。

(完)


免責聲明!

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



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