在上兩期,我們講解了 Axios
的原始碼:
今天,我們將實現一個簡易的 Axios
,用於在 Node
端實現網路請求,並支援一些基礎配置,比如 baseURL、url、請求方法、攔截器、取消請求...
本次實現所有的原始碼都放在 這裡,感興趣的可以看看。
Axios 例項
本次我們將使用 typescript + node
來實現相關程式碼,這樣對大家理解程式碼也會比較清晰。
這裡,先來實現一個 Axios
類吧。
type AxiosConfig = {
url: string;
method: string;
baseURL: string;
headers: {[key: string]: string};
params: {};
data: {};
adapter: Function;
cancelToken?: number;
}
class Axios {
public defaults: AxiosConfig;
public createInstance!: Function;
constructor(config: AxiosConfig) {
this.defaults = config;
this.createInstance = (cfg: AxiosConfig) => {
return new Axios({ ...config, ...cfg });
};
}
}
const defaultAxios = new Axios(defaultConfig);
export default defaultAxios;
在上面,我們主要是實現了 Axios
類,使用 defaults
儲存預設配置,同時宣告瞭 createInstance
方法。該方法會建立一個新的 Axios
例項,並且會繼承上一個 Axios
例項的配置。
請求方法
接下來,我們將對 https://mbd.baidu.com/newspage/api/getpcvoicelist
發起一個網路請求,將響應返回的資料輸出在控制檯。
我們發起請求的語法如下:
import axios from './Axios';
const service = axios.createInstance({
baseURL: 'https://mbd.baidu.com'
});
(async () => {
const reply = await service.get('/newspage/api/getpcvoicelist');
console.log(reply);
})();
request 方法
我們先來給我們的 Axios
類加上一個 request
和 get
方法吧。
import { dispatchRequest } from './request';
class Axios {
//...
public request(configOrUrl: AxiosConfig | string, config?: AxiosConfig) {
if (typeof configOrUrl === 'string') {
config!.url = configOrUrl;
} else {
config = configOrUrl;
}
const cfg = { ...this.defaults, ...config };
return dispatchRequest(cfg);
}
public get(configOrUrl: AxiosConfig | string, config?: AxiosConfig) {
return this.request(configOrUrl, {...(config || {} as any), method: 'get'});
}
}
這裡 request
方法的實現和 axios
自帶的方法差異性不大。
現在,我們來編輯發起真實請求的 dispatchRequest
方法吧。
export const dispatchRequest = (config: AxiosConfig) => {
const { adapter } = config;
return adapter(config);
};
和 axios
一樣,呼叫了配置中的 adapter
來發起網路請求,而我們在 defaultConfig
中配置了預設的 adapter
。(如下)
const defaultConfig: AxiosConfig = {
url: '',
method: 'get',
baseURL: '',
headers: {},
params: {},
data: {},
adapter: getAdapter()
};
adapter 方法
接下來,我們重點來看看我們的 adapter
實現即可。
// 這裡偷個懶,直接用一個 fetch 庫
import fetch from 'isomorphic-fetch';
import { AxiosConfig } from './defaults';
// 檢測是否為超連結
const getEffectiveUrl = (config: AxiosConfig) => /^https?/.test(config.url) ? config.url : config.baseURL + config.url;
// 獲取 query 字串
const getQueryStr = (config: AxiosConfig) => {
const { params } = config;
if (!Object.keys(params).length) return '';
let queryStr = '';
for (const key in params) {
queryStr += `&${key}=${(params as any)[key]}`;
}
return config.url.indexOf('?') > -1
? queryStr
: '?' + queryStr.slice(1);
};
const getAdapter = () => async (config: AxiosConfig) => {
const { method, headers, data } = config;
let url = getEffectiveUrl(config);
url += getQueryStr(config);
const response = await fetch(url, {
method,
// 非 GET 方法才傳送 body
body: method !== 'get' ? JSON.stringify(data) : null,
headers
});
// 組裝響應資料
const reply = {
data: await response.json(),
status: response.status,
statusText: response.statusText,
headers: response.headers,
config: config,
};
return reply;
};
export default getAdapter;
在這裡,我們的實現相對來說比較簡陋。簡單來說就是幾步
- 組裝 url
- 發起請求
- 組裝響應資料
看看效果
現在來控制檯執行一下我們的程式碼,也就是下面這,看看控制檯輸出吧。
import axios from './Axios';
const service = axios.createInstance({
baseURL: 'https://mbd.baidu.com'
});
(async () => {
const reply = await service.get('/newspage/api/getpcvoicelist');
console.log(reply);
})();
從上圖可以看出,我們的 axios
最基礎的功能已經實現了(雖然偷了個懶用了 fetch
)。
接下來,我們來完善一下它的能力吧。
攔截器
現在,我想要讓我的 axios
擁有新增攔截器的能力。
- 我將在請求處新增一個攔截器,在每次請求前加上一些自定義
headers
。 - 我將在響應處新增一個攔截器,直接取出響應的資料主體(
data
)和配置資訊(config
),去除多餘的資訊。
程式碼實現如下:
// 新增請求攔截器
service.interceptors.request.use((config: AxiosConfig) => {
config.headers.test = 'A';
config.headers.check = 'B';
return config;
});
// 新增響應攔截器
service.interceptors.response.use((response: any) => ({ data: response.data, config: response.config }));
改造 Axios 類,新增 interceptors
我們先來建立一個 InterceptorManager
類,用於管理我們的攔截器。(如下)
class InterceptorManager {
private handlers: any[] = [];
// 註冊攔截器
public use(handler: Function): number {
this.handlers.push(handler);
return this.handlers.length - 1;
}
// 移除攔截器
public eject(id: number) {
this.handlers[id] = null;
}
// 獲取所有攔截器
public getAll() {
return this.handlers.filter(h => h);
}
}
export default InterceptorManager;
定義好了攔截器後,我們需要在 Axios
類中加上攔截器 —— interceptors
,如下:
class Axios {
public interceptors: {
request: InterceptorManager;
response: InterceptorManager;
}
constructor(config: AxiosConfig) {
// ...
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
}
接下來,我們在 request
方法中處理一下這些攔截器的呼叫吧。(如下)
public async request(configOrUrl: AxiosConfig | string, config?: AxiosConfig) {
if (typeof configOrUrl === 'string') {
config!.url = configOrUrl;
} else {
config = configOrUrl;
}
const cfg = { ...this.defaults, ...config };
// 將攔截器與真實請求合併在一個陣列內
const requestInterceptors = this.interceptors.request.getAll();
const responseInterceptors = this.interceptors.response.getAll();
const handlers = [...requestInterceptors, dispatchRequest, ...responseInterceptors];
// 使用 Promise 將陣列串聯呼叫
let promise = Promise.resolve(cfg);
while (handlers.length) {
promise = promise.then(handlers.shift() as any);
}
return promise;
}
這裡主要是將攔截器和真實的請求合併成一個陣列,然後再使用 Promise
進行串聯。
這裡發現了一個自己還不知道的Promise
知識點,在Promise.resolve
中,不需要顯式返回一個Promise
物件,Promise
內部會將返回的值包裝成一個Promise
物件,支援.then
語法呼叫。
現在,再執行一下我們的程式碼,看看加上攔截器後的執行效果吧。(如下圖)
從上圖可以看出,返回的內容中,只剩下了 data
和 config
欄位(響應攔截器)。並且在 config
欄位中也可以看到我們在 請求攔截器
中新增的自定義 headers
也起作用啦!
取消請求
最後,我們來實現 CancelToken
類,用於取消 axios
請求。
在實際應用中,我經常會使用 CancelToken
來自動檢測重複請求(來源於頻繁點選),然後取消掉更早的請求,僅使用最後一次請求作為有效請求。
所以,CancelToken
對我而言其實是個非常喜愛的功能,它本身的實現並不複雜,我們下面就開始來實現它吧。
我們先看看呼叫方式吧,下面我將在發起請求後 10ms 後(利用 setTimeout
),將請求取消。也就是說,只有 10ms 內完成的請求才能成功。
import axios, { CancelToken } from './Axios';
// ...
(async () => {
const source = CancelToken.source();
// 10ms 後,取消請求
setTimeout(() => {
source.cancel('Operation canceled by the user.');
}, 10);
const reply = await service.get('/newspage/api/getpcvoicelist', { cancelToken: source.token });
console.log(reply);
})();
我們先來理一理思路。
首先,我們使用了 CancelToken.source()
獲取了一個 cancelToken
,並傳給了對應的請求函式。
接下來,我們應該是使用這個 token
進行查詢,查詢該請求是否被取消,如果被取消則丟擲錯誤,結束這次請求。
CancelToken
ok,思路已經清晰了,接下來就開始實現吧,先從 CancelToken
開始吧。
class CancelError extends Error {
constructor(...options: any) {
super(...options);
this.name = 'CancelError';
}
}
class CancelToken {
private static list: any[] = [];
// 每次返回一個 CancelToken 例項,用於取消請求
public static source(): CancelToken {
const cancelToken = new CancelToken();
CancelToken.list.push(cancelToken);
return cancelToken;
}
// 通過檢測是否有 message 欄位來確定該請求是否被取消
public static checkIsCancel(token: number | null) {
if (typeof token !== 'number') return false;
const cancelToken: CancelToken = CancelToken.list[token];
if (!cancelToken.message) return false;
// 丟擲 CancelError 型別,在後續請求中處理該型別錯誤
throw new CancelError(cancelToken.message);
}
public token = 0;
private message: string = '';
constructor() {
// 使用列表長度作為 token id
this.token = CancelToken.list.length;
}
// 取消請求,寫入 message
public cancel(message: string) {
this.message = message;
}
}
export default CancelToken;
CancelToken
基本上完成了,它的主要功能就是使用一個 CancelToken
例項對應一個需要做處理的請求,然後在已取消的請求中丟擲了一個 CancelError
型別的拋錯。
處理 CancelError
接下來,我們需要在對應的請求處(dispatchRequest
)新增取消請求檢測,最後再加上一個對應的響應攔截器處理對應錯誤即可。
export const dispatchRequest = (config: AxiosConfig) => {
// 在發起請求前,檢測是否取消請求
CancelToken.checkIsCancel(config.cancelToken ?? null);
const { adapter } = config;
return adapter(config).then((response: any) => {
// 在請求成功響應後,檢測是否取消請求
CancelToken.checkIsCancel(config.cancelToken ?? null);
return response;
});
};
由於我們的攔截器實現的太粗糙,並沒有新增失敗響應攔截器(本應在這裡處理),所以我這裡直接將整個請求包裹在 try ... catch
中處理。
try {
const reply = await service.get('/newspage/api/getpcvoicelist', { cancelToken: source.token });
console.log(reply);
} catch(e) {
if (e.name === 'CancelError') {
// 如果請求被取消,則不丟擲錯誤,只在控制檯輸出提示
console.log(`請求被取消了, 取消原因: ${e.message}`);
return;
}
throw e;
}
接下來,我們執行我們的程式,看看控制檯輸出吧!(如下圖)
大功告成!
小結
到這裡,我們的簡易版 axios
就已經完成啦。
它可以用於在 Node
端實現網路請求,並支援一些基礎配置,比如 baseURL、url、請求方法、攔截器、取消請求...
但是,還是有很多不夠完善的地方,感興趣的小夥伴可以找到下面的原始碼地址,繼續往下續寫。
最後一件事
如果您已經看到這裡了,希望您還是點個贊再走吧~
您的點贊是對作者的最大鼓勵,也可以讓更多人看到本篇文章!
如果覺得本文對您有幫助,請幫忙在 github 上點亮 star
鼓勵一下吧!