深入淺出 axios(一):預設 axios 物件、Axios 類別、攔截器
axios 是一個 Promise based 的 HTTP 請求工具,他可以運行在「瀏覽器環境」與「Node.js」中。相信在 AJAX 技術被廣泛應用的今日,稍微有一點經驗的捧油門對他一定都不陌生。因此這系列分享不會特別著重在如何使用 axios,而是針對幾個我覺得 axios 有趣、好用的地方,研究他的原始碼是如何撰寫的,從中吸收寶貴的經驗。那就讓我們一起看下去吧!
前言
本篇的 axios 版本為 0.21.0
這是一個系列的分享,預計會有兩篇,本文是該系列的第一篇。在本文當中會提到以下這些內容:
- 預設導入的 axios 設計。
- Axios 類別(Class)設計。
- 攔截器類別(InterceptorManager Class)設計。
axios 可應用在「瀏覽器環境」與「Node.js」環境中。在瀏覽器環境下使用了 XMLHttpRequest
而在 Node.js 環境則使用了 http
模組。由於目前工作上的使用經驗還是以瀏覽器端為主,因此本系列暫時也只會針對瀏覽器端的功能做研究,分享。
在開始先小小提一下,自從學習了 TypeScript 後,在使用一套工具過程中,我會很習慣不斷地確認這個工具提供的 interface 有哪些(如果有提供的話)並搭配文件使用。
axios 請求流程
一開始,先來看看當透過 axios 發出請求(request)到取得到資料(response)的過程中發生了那些事情,以下是我自製的 axios 請求流程圖:
從流程圖可以知道,當透過 axios 發出一個請求後,會先經過請求攔截器(Interceptors),之後依照執行環境選擇適當的請求適配器(adapter,介面)發出請求。取得請求的回應後,經過處理回應的攔截器,最後回傳給使用者,完成整個 HTTP 請求。
預設導入的 axios 設計
要瞭解 axios 提供了哪些方法(methods)與屬性(properties),我們可以先透過 axios 官方提供的 interface 快速瀏覽:
export interface AxiosInstance {
(config: AxiosRequestConfig): AxiosPromise;
(url: string, config?: AxiosRequestConfig): AxiosPromise;
defaults: AxiosRequestConfig;
interceptors: {
request: AxiosInterceptorManager<AxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
getUri(config?: AxiosRequestConfig): string;
request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>;
get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
delete<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
head<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
options<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
post<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
put<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
patch<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
}
export interface AxiosStatic extends AxiosInstance {
create(config?: AxiosRequestConfig): AxiosInstance;
Cancel: CancelStatic;
CancelToken: CancelTokenStatic;
isCancel(value: any): boolean;
all<T>(values: (T | Promise<T>)[]): Promise<T[]>;
spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
isAxiosError(payload: any): payload is AxiosError;
}
declare const axios: AxiosStatic;
當我們使用 import axios from 'axios'
時,此時的 axios 型別為 AxiosStatic
。之後如果是透過 axios.create()
取得的回傳值,型別則會是 AxiosInstance
。
這裡可以發現,如果是透過 axios.create()
建立的實例,就不會有像是 create()
、isCancel()
、 isAxiosError()
諸如此類的方法可以用,也不能取得 Cancel
、CancelToken
等屬性。
createInstance
我們知道,不論是預設導入的 axios
或是 axios.create()
的回傳值都可以直接用來發送 HTTP 請求,像這樣:
axios({/** config */})
// 或
axios('url', {/** config */})
而這部分在原始碼的部分是這樣設計的:
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
return instance;
}
var axios = createInstance(defaults);
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// 略
// 這個地方會再將一些方法或屬性掛到要導出 axios 上。
// 這裡也是預設導入的 `axios` 與 `axios.create()` 的回傳值型別會不太一樣的原因。
module.exports.default = axios;
我們可以看到,不論是預設導入的 axios
或是 axios.create()
的回傳值,他們都是由 createInstance()
這個 function 回傳的 function。
在 createInstance()
中,首先會建立一個 Axios 類別的實例(context
),但不是直接將這個實例回傳出來,而是回傳了一個變數(instance
),這個 instance
存了一個綁定了以 context
為 this
的 Axios 原型上的 request
方法。
說起來很饒口,但就是每當呼叫 axios({ /** config */ })
時,執行的其等同於執行:
Axios.prototype.request.bind(context)({ /** config */ })
所以我們知道,不論是預設導入的 axios
或是 axios.create()
回傳的都是一個 request
的 function。不過除此之外我們還可以這樣使用:
axios.request({ /** config */ })
axios.delete('url', { /** config */ })
axios.get('url', { /** config */ })
axios.head('url', { /** config */ })
axios.options('url', { /** config */ })
axios.post('url', { /** data */ }, { /** config */ })
axios.put('url', { /** data */ }, { /** config */ })
axios.patch('url', { /** data */ }, { /** config */ })
這又是怎麼做到的呢?因為在 JavaScript 的世界中,function 其實也是一個物件,所以就算是 function 也可以用物件的方式存取屬性與其他方法,而在 createInstance
裡面還做了兩件事情:
var utils = require('./utils');
function createInstance(defaultConfig) {
// 略
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
return instance;
}
// lib/utils.js
/**
* Extends object a by mutably adding to it the properties of object b.
*
* @param {Object} a The object to be extended
* @param {Object} b The object to copy properties from
* @param {Object} thisArg The object to bind function to
* @return {Object} The resulting value of object a
*/
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
module.exports = {
// 略
extend: extend
}
由上面的原始碼可見,extend
會將 b
物件上有的方法或屬性複製到 a
物件上,如果是複製方法,則還需要傳入 thisArg
。
了解 extend
後就可以解釋了。首先,先將 Axios
類別原型上的方法複製到 instance
上,並綁定 this
為 context
。再來將 context
上的屬性也複製到 instance
上。因此回傳的 instance
除了可以當 function 使用外,也可以存取到 createInstance
中建立的 context
上的屬性與方法。
補充:
mergeConfig
是 axios 中合併預設 config 與傳入的config
的方法。而utils
囊括了各種好用的小 function。
- axios mergeConfig GitHub 連結 -
mergeConfig
原始碼。- axios utils GitHub 連結 -
utils
原始碼。
Axios 類別(Class)設計
再來要進到 Axios 類別的部分。
透過官方提供的 interface 我們可以得知,Axios 類別原始 interface 大致上長這樣:
// 官方並沒有針對 Axios 類別提供 interface
interface Axios {
defaults: AxiosRequestConfig;
interceptors: {
request: AxiosInterceptorManager<AxiosRequestConfig>;
response: AxiosInterceptorManager<AxiosResponse>;
};
getUri(config?: AxiosRequestConfig): string;
request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>;
get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
delete<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
head<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
options<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
post<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
put<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
patch<T = any, R = AxiosResponse<T>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R>;
}
屬性
Axios 類別生成的屬性有兩個:
defaults
:- 型別:Object
- 該 axios 實例的預設 config,也就是建構時傳入的
instanceConfig
。
interceptors
- 型別:Object
- 該 axios 實例的
request
與response
攔截器。
建構函式原始碼如下:
var InterceptorManager = require('./InterceptorManager');
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
可以看到,Axios 的攔截器是由一個叫 InterceptorManager
的類別建立的,它提供了一些間單的操作來新增、刪除攔截器,細節會在下個部分會詳細探究。
方法
類別方法部分,包含了一個 request
以及其他與 HTTP request methods 小寫同名的方法,外加一個 getUri
。接著來看看 request
做了哪些事情吧!
發出請求 axios.request()
var dispatchRequest = require('./dispatchRequest');
// 略
Axios.prototype.request = function request(config) {
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
return new Promise(function (resolve, reject) {
try {
dispatchRequest(config).then(resolve, reject)
} catch (e) {
reject(e);
}
})
};
從上面的設計可以得知,在 request()
中會先對傳入的 config
與預設的 config this.default
合併,補齊一些必要的屬性,像是 method
。如果合併後 config
裡面還是沒有 method
這個屬性,預設會使用 GET 方法。最後觸發 dispatchRequest()
發出請求。
補充:
dispatchRequest()
中會針對請求資料做最後的轉換(Transform request data)並依照依照執行環境選擇適當的請求適配器發出請求。收到回應資料後,會再將回應資料專換(Transform response data)過後再交由回應攔截器處理。
補充: 由一開始的條件判斷可以得知,其實
request()
方法提供了兩種使用方式,如下:// 官方並沒有針對 Axios 類別提供 interface interface Axios { // 略 request<T = any, R = AxiosResponse<T>> (config: AxiosRequestConfig): Promise<R>; request<T = any, R = AxiosResponse<T>> (url: string, config: AxiosRequestConfig): Promise<R>; // 略 }
不過第二種方式在
AxiosInstance
並沒有提供型別,在文件中也沒有提及,我猜這應該是因為第二種方法其實只是為了要服務axios(url[, config])
這種用法而產生的。
到這邊基本的 request()
已經可以運作,但還有一個很重要的功能沒有實踐:攔截器(Interceports)
加入攔截器(Interceports)
如果有使用過像是 Express.js 的捧油,可能會聽過 中間件(Middleware) 這個功能。攔截器的概念跟中間件很相似,可以用於例如:處理身分驗證,或是共同的邏輯處理。
從上面的建構式中得知,攔截器是透過 InterceptorManager
類別建構出的實例。我們還沒有介紹到這個類別,但現在可以先想像他是個陣列,並有一個 forEach
的方法可以遍歷所有存在陣列中的攔截器。
Axios 實例上的攔截器又分成「請求攔截器」與「回應攔截器」兩個,分別在發出請求前執行,與接收到回應後執行。因此 axios.request()
的設計會這樣調整:
Axios.prototype.request = function request(config) {
// 略
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
我們先將原本要觸發的 dispatchRequest
與一個 undefined
存進一個名為 chain
的陣列中。為什麼要這樣呢?可以先看看 MDN 上 Promise.prototype.then()
的語法:
// 取自 MDN
p.then(onFulfilled[, onRejected]);
所以 chain
的設計會是 onFulfilled
與 onRejected
一組一組的陣列。在陣列中的 0、2、4、6 位置會是前一個非同步成功後的 onFulfilled
,1、3、5、7 位置則會是當前一個非同步發生錯誤時呼叫的 onRejected
。
在每次發出請求前,都會分別將所有攔截器串在 chain
的前後。如果是「請求攔截器」,就用 Array.prototype.unshift
一組一組放到 chain
前面;如果是「回應攔截器」,就用 Array.prototype.push
一組一組推到 chain
後面。
var chain = [
/**
* 請求攔截器
*/
requestFulfilled, requestRejected,
/**
* 發出請求
*/
dispatchRequest, undefined,
/**
* 回應攔截器
*/
responseFulfilled, responseFulfilled
]
將所有攔截器串進 chain
後再用 while
迴圈搭配 Array.prototype.shift
將他們兩個一組的串起來:
Promise.resolve(config)
/**
* 請求攔截器
* 發出請求前一個一個執行
*/
.then(requestFulfilled, requestRejected)
/**
* 發出請求
*/
.then(dispatchRequest, undefined)
/**
* 回應攔截器
* 接收到回應後一個一個執行
*/
.then(responseFulfilled, responseFulfilled)
這樣就完成了整個 axios.request()
的設計。
注意
const promise1 = promise.then(onFulfilled, onRejected) const promise2 = promise.then(onFulfilled).catch(onRejected)
我們有兩種方法針對 Promise 鍊的錯誤處理有兩種寫法,但者兩者的意義有很大的不同。在
promise1
與promise2
中的promise
發生錯誤,兩種的onRejected
都可以接到錯誤,但如果錯誤是發生在onFulfilled
,在promise1
的onRejected
不會接到錯誤,可是promise2
可以接到。
其他與 HTTP request methods 小寫同名方法
axios 除了 axios.request()
可以發出請求外,還有像是 get()
、post()
、put()
、patch()
、delete()
、head()
、options()
等方法。不過本質上他們都是對 axios.request()
再做一層包裝,所以直接上程式碼:
/**
* 對應
* - axios.delete(url[, config])
* - axios.get(url[, config])
* - axios.head(url[, config])
* - axios.options(url[, config])
*/
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: (config || {}).data
}));
};
});
/**
* 對應
* - axios.post(url[, data[, config]])
* - axios.put(url[, data[, config]])
* - axios.patch(url[, data[, config]])
*/
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
}));
};
});
除了 getUri()
外,以上就是 Axios 類別的實作設計。
攔截器類別(InterceptorManager Class)設計
先來確認一下攔截器的用法:
攔截器型別
export interface AxiosInterceptorManager<V> {
use(onFulfilled?: (value: V) => V | Promise<V>, onRejected?: (error: any) => any): number;
eject(id: number): void;
}
axios 文件 - 攔截器 連結
註冊攔截器 use()
:
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
如果要刪除指定攔截器可以用 eject()
方法:
const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
知道了攔截器的介面,我們可以開始看 InterceptorManager
是如何實作。
在 InterceptorManager
實例上,我們需要一個陣列存放已註冊的攔截器:
function InterceptorManager() {
this.handlers = [];
}
之後不論是透過 use()
註冊的攔截器或是用 eject()
移除,都是對 handlers
這個陣列的操作,下面一一介紹。
use()
註冊攔截器
分析
註冊攔截器時,我們需要傳入 fulfilled
與 rejected
,並存放到 handlers
中。每當註冊一個攔截器,就回傳一個數字用做之後移除用的參數。
如果記得 Axios.prototype.request
如何使用攔截器,我們就可以知道 handlers
的型別大致如下:
interface InterceptorHandler = {
fulfilled?: (value: V) => V | Promise<V>,
rejected?: (error: any) => any
}
type InterceptorHandlers = InterceptorHandler[]
實作
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
每當註冊一個攔截器,就會將他包成物件推入 handlers
陣列中,並回傳物件在該陣列的位置為何。之後如果要移除改攔截器,只要找到這個位置,就可以將該攔截器刪除。
eject()
刪除攔截器
分析
需要接收一個指定位置(數字)將該位置的攔截器移除。
實作
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
這邊需要注意,刪除攔截器不能動到陣列的長度,在這裡是將原本的攔截器設定為 null
,因為如果改變了長度,會造成先前註冊攔截器的位置錯亂,之後刪除傳入其他的位置很有可能會無法刪到真正想刪除的攔截器。
forEach()
遍歷(私有方法)
這裡會特別提到這個私有方法是因為我們可以發現,eject()
會將刪除的攔截器設定為 null
。但前面知道 Axios.prototype.request
會將 handlers
中每一個攔截器的 fulfilled
與 rejected
推入陣列,但如果改成 null
不就會出錯了嗎?
所以這邊只是要提醒,在這裡的 forEach
需要檢查遍歷到的位置攔截器是否還存在,存在才去將攔截器串起來,否則就略過該位置。
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
結語
本篇中我們瞭解了預設導入的 axios
與 axios.create()
建立的物件是如何透過 createInstance
初始化,也瞭解到為什麼 axios
可以當作一個 function 使用,又可以像物件一樣存取到其他的方法與屬性。
後面也看了 Axios 類別與他用來管理攔截器的 InterceptorManager 類別的設計。在操作上活用了 Promise 與陣列的各項操作,真的非常令人玩味。
目前預計之後會分別探討 axios 使用到的 XMLHttpRequest
介紹與 CancelToken
類別設計,可以期待一下。
參考資料
請我喝杯咖啡
如果這裡的內容有幫助到你的話,一杯咖啡就是對我最大的鼓勵。