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同級下還創建了每個請求方式屬性,用於存放不同請求所特有的請求頭字段。例如像需要數據的請求方式post、put、patch我們為其默認添加了Content-Type字段,而不需要數據的請求方式delete、get、head、options則為其留空。(其實默認配置對象里面的內容遠不止這些,詳細內容可查看這里~)
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'
}
}
通過觀察,我們發現,這兩個對象的合並可不是簡簡單單的字段合並,這里面要分情況處理:
- 對於
timeout、responseType等這些常規屬性,合並起來比較容易,即如果用戶配置了就用用戶配置的,如果用戶沒配置,則用默認配置的; - 對於一些屬性如
url、method、params、data,這些屬性都是跟每個請求息息相關的,請求不同從而千變萬化,所以像這四個屬性我們在合並的時候不管默認配置對象里面有沒有,我們只取用戶配置的; - 對於
header、auth等這些屬性就比較復雜了,這些屬性的合並可不是取這個不取那個的問題,而是要將默認配置的與用戶配置的做一次深度合並。如在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 只接受用戶配置
對於 url、method、params、data這些屬性,只接受用戶配置,不管默認配置對象里面有沒有,我們只取用戶配置的;
// 2.只接受自定義配置,不管默認配置對象里面有沒有,我們只取用戶配置的;
let valueFromUserConfig = ["url", "method", "params", "data"];
valueFromUserConfig.forEach(prop => {
userConfig = userConfig || {};
if (typeof userConfig[prop] !== 'undefined') {
config[prop] = userConfig[prop];
}
});
4.3 復雜對象深度合並
對於header、auth等這些屬性我們就要進行深度合並,例如在默認配置對象和用戶配置對象的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 字段,我們都要提取,而對於 post、get 這類提取,需要和該次請求的方法對應。
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 的方式把 common、post 的屬性拷貝到 headers 這一級,然后再把 common、post 這些屬性刪掉。最后返回的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中,我們顯示的給默認配置對象添加了 post、get 和 common 的 headers,並且我們在請求中的配置對象也配置了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,通過F12的 network 部分我們可以看到請求已正常發出,並且請求的headers如下:

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