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
如下:
從結果中我們可以看到,跟我們之前預測的結果完全相符,至此,默認配置合並就已經實現了。
(完)