當前位置: 妍妍網 > 碼農

十分鐘,帶你深入了解 axios 源碼實作

2024-01-24碼農

前端同學們對於axios應該都有所耳聞。但你有沒有好奇過,axios是如何實作的呢?源碼又是怎樣的呢?

讓我們一起花十分鐘的時間,深入學習 axios 的源碼實作!

01:axios 例項與請求流程

在深入源碼之前,先簡要了解一下axios例項的內容和整體請求流程,這將有助於我們更輕松地閱讀源碼。

下面是axios例項內容的簡圖:

axios 例項主要有三個核心內容:

  1. config :包括url、method、params、headers等配置。

  2. interceptors :攔截器,分為請求攔截器和返回攔截器。

  3. request :用於呼叫xhr或http請求的方法,參數是config。

由於可以再次傳入 config 呼叫 request 方法,但不能傳入 interceptors ,因此需要在 axios 上提前添加攔截器,無法臨時添加。

以下是 axios 的請求流程圖:

02:源碼檔結構解析

axios的源碼都在 lib 資料夾下,最核心的內容在 core 資料夾裏。

lib
│ axios.js // 最終匯出的檔
│ utils.js // 工具類
├─adapters // 介面卡相關
│ adapters.js //介面卡類
│ http.js // node請求
│ xhr.js // 瀏覽器請求
├─cancel // 取消功能相關
│ CanceledError.js //取消異常類
│ CancelToken.js //取消token類
│ isCancel.js //判斷是否取消
├─core // 核心功能相關 
│ Axios.js // axios類
│ AxiosError.js // axios異常類
│ AxiosHeaders.js // 請求頭
│ buildFullPath.js // 構造請求地址
│ dispatchRequest.js // 發送請求方法
│ InterceptorManager.js // 攔截器的類
│ mergeConfig.js // 合並配置方法
│ settle.js // 處理請求結果方法
│ transformData.js // 數據轉換執行方法
├─defaults // 預設配置
│ index.js // 預設請求參數配置
│ transitional.js // 預設transitional配置
├─env // node環境沒有FormData,
│ │ data.js
│ └─ classes
│ FormData.js
├─helpers // 各種工具類方法,看名字就可以大概猜到作用(太長了進行省略...)
│ ...
└─platform // 為不同環境下準備的方法(太長了進行省略...)
...

03:源碼檔閱讀

3.1:入口檔 axios.js

該檔建立了 axios 例項並匯出,因此我們透過 import axios from 'axios' 引入的即為該例項,無需再 new Axios({...})

以下是 axios 例項建立的程式碼:

// 核心方法,根據config建立axios例項
functioncreateInstance (defaultConfig{
// 建立axios例項
const context = new Axios(defaultConfig);
// 給Axios原型上的request方法繫結context為它的this
// 這個instance就是我們最終使用的axios
// 沒想到吧,最開始的instance其實是個函式,
// 所以我們才可以使用這個用法axios('/api/url')
// 只不過後面給它擴充套件了很多東西
const instance = bind(Axios.prototype.request, context);
// 將Axios.prototype上的內容都繫結到instance上,
// 這樣它就擁有了簡寫的請求方法,比如axios.get(),axios.post()
// 如果是函式,this繫結為context
utils.extend(instance, Axios.prototype, context, { allOwnKeystrue });
// 將context上的內容都繫結到instance上,
// 這樣它就擁有了攔截器內容,可以使用axios.interceptors.request.use()
// 因為context上的函式的this本就指向context,所以第三個參數不需要再指定
utils.extend(instance, context, null, { allOwnKeystrue });
// 給instance增加create方法,可以透過create建立一個例項
instance.create = functioncreate (instanceConfig{
// 入參為拼接配置項,以instanceConfig為優先
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
// 呼叫上面的方法,最終匯出的是axios,
// 其實是Axios.prototype.request,並擴充套件了很多內容
const axios = createInstance(defaults);
// 繼續給axios增加內容
// 這就說明如果自己透過const myAxios=axios.create({});
// 建立出來的例項就沒有下面這些內容了
// 所以下面這些內容只能透過import axios from 'axios';
// axios.all()這樣的方式來使用
axios.Axios = Axios;
// Cancel相關
axios.CanceledError = CanceledError;
axios.CancelToken = CancelToken;
axios.isCancel = isCancel;
axios.VERSION = VERSION;
// 工具函式,將物件轉為FormData
axios.toFormData = toFormData;
// Axios通用異常類
axios.AxiosError = AxiosError;
// Cancel異常類
axios.Cancel = axios.CanceledError;
// Expose all/spread
// 工具函式
axios.all = functionall (promises{
returnPromise.all(promises);
};
// 工具函式,利用apply將陣列參數改為單個傳入的參數
axios.spread = spread;
// 判斷異常是否是AxiosError
axios.isAxiosError = isAxiosError;
// 合並config物件方法
axios.mergeConfig = mergeConfig;
axios.AxiosHeaders = AxiosHeaders;
// 工具方法
axios.formToJSON = thing => formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing);
// 獲取介面卡:xhr 、http
axios.getAdapter = adapters.getAdapter;
// 請求狀態
axios.HttpStatusCode = HttpStatusCode;
axios.default = axios;
// 最終匯出
exportdefault axios


















3.2:Axios類

Axios類 是 Axios 的核心方法,其中的 request 方法是最核心的請求方法。

以下是 Axios類 的核心程式碼:

classAxios{
// 可以看到Axios的建構函式相當簡單
// 僅僅是保存了我們傳入的config,
// 然後初始化空的攔截器物件
constructor(instanceConfig) {
// 所有的配置都設定再defaults上
this.defaults = instanceConfig;
// 初始化空的攔截器物件,包含請求攔截器request和返回攔截器response
this.interceptors = {
requestnew InterceptorManager(),
responsenew InterceptorManager()
};
}
// request是Axios的核心方法
// 所有的核心都在request方法裏,
// request方法接收兩種參數,【直接傳config物件】或者【傳url和config物件】
request (configOrUrl, config) {
// 允許axios('example/url'[, config]) 這樣使用
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
else {
config = configOrUrl || {};
}
// request會使用傳入的配置merge預設配置
// 所以即使只傳了一個url,也會使用預設的Get方法
config = mergeConfig(this.defaults, config);
const { headers } = config;
// 預設get請求
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
// 說明header可以直接設定
// 也可以在common設定通用header,也可以為每種請求設定特定的header
let contextHeaders = headers && utils.merge(
headers.common,
headers[config.method]
);
headers && utils.forEach(
['delete''get''head''post''put''patch''common'],
(method) => {
delete headers[method];
}
);
// 優先使用headers下配置,再使用headers.common和headers[get,post]的配置
config.headers = AxiosHeaders.concat(contextHeaders, headers);
// 請求攔截器鏈
const requestInterceptorChain = [];
// 記錄是否使用同步的方式呼叫,我們配置攔截器的時候,預設是false,也就是異步
let synchronousRequestInterceptors = true;
this.interceptors.request.forEach(functionunshiftRequestInterceptors (interceptor{
// 如果配置了runWhen函式,那麽會先執行runWhen,如果為true,才會添加該攔截器
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
// unshift說明後傳入的請求攔截器先執行,一次放入兩個,分別是fulfilled和rejected
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 響應攔截器鏈
const responseInterceptorChain = [];
this.interceptors.response.forEach(functionpushResponseInterceptors (interceptor{
// push說明先傳入的響應攔截器先執行
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
let promise;
let i = 0;
let len;
// 預設是異步執行,也就是一個執行完再執行下一個
if (!synchronousRequestInterceptors) {
//dispatchRequest是真正的發送請求
const chain = [dispatchRequest.bind(this), undefined];
// 前面插入請求攔截器
chain.unshift.apply(chain, requestInterceptorChain);
// 後面插入響應攔截器
chain.push.apply(chain, responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
// 依次執行
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
return promise;
}
len = requestInterceptorChain.length;
let newConfig = config;
i = 0;
// 同步執行,請求攔截器
while (i < len) {
const onFulfilled = requestInterceptorChain[i++];
const onRejected = requestInterceptorChain[i++];
try {
newConfig = onFulfilled(newConfig);
catch (error) {
onRejected.call(this, error);
break;
}
}
// 發起請求
try {
promise = dispatchRequest.call(this, newConfig);
catch (error) {
returnPromise.reject(error);
}
i = 0;
len = responseInterceptorChain.length;
// 返回有異常可以繼續走下去
while (i < len) {
promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
}
return promise;
}
// 獲取請求地址
getUri (config) {
config = mergeConfig(this.defaults, config);
const fullPath = buildFullPath(config.baseURL, config.url);
return buildURL(fullPath, config.params, config.paramsSerializer);
}
}
// Provide aliases for supported request methods
// 給Axios原型註入四個請求方法,請求方法本質都是呼叫request方法
// 這四個都是不帶請求體的
utils.forEach(['delete''get''head''options'], functionforEachMethodNoData (method{
Axios.prototype[method] = function (url, config{
returnthis.request(mergeConfig(config || {}, {
method,
url,
data: (config || {}).data
}));
};
});
// 給Axios註入post,put,patch,postForm,putForm,patchForm方法
// 這幾個方法都是帶請求體的
utils.forEach(['post''put''patch'], functionforEachMethodWithData (method{
functiongenerateHTTPMethod (isForm{
returnfunctionhttpMethod (url, data, config{
returnthis.request(mergeConfig(config || {}, {
method,
headers: isForm ? {
'Content-Type''multipart/form-data'
} : {},
url,
data
}));
};
}
Axios.prototype[method] = generateHTTPMethod();
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});
exportdefault Axios;


























3.3:InterceptorManager類

攔截器的管理者,可以添加、刪除和清空攔截器。

以下是 InterceptorManager 類的程式碼:

axios.interceptors.request.use({
fulfilled:()=>{},
rejected:()=>{}
})

可以看到我們給use傳遞的是一個物件,物件包含fulfilled函式和rejected函式。

接下來看源碼:

classInterceptorManager{
// 建構函式只初始化了一個空的handlers陣列
// 攔截器就是放在這個陣列裏的
constructor() {
this.handlers = [];
}
// 添加攔截器,返回索引,可以用索引來移除攔截器
// 可以發現除了fulfilled和rejected,
// 我們還可以設定synchronous和runWhen
// runWhen函式用來動態控制是否使用該攔截器
use (fulfilled, rejected, options) {
this.handlers.push({
fulfilled,
rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
});
returnthis.handlers.length - 1;
}
// 根據添延長返回的索引去刪除攔截器
eject (id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
}
// 清空攔截器
clear () {
if (this.handlers) {
this.handlers = [];
}
}
// 提供遍歷攔截器快捷操作
forEach (fn) {
utils.forEach(this.handlers, functionforEachHandler (h{
if (h !== null) {
fn(h);
}
});
}
}
exportdefault InterceptorManager;

3.4:dispatchRequest發送請求

看完上面的程式碼,我們已經基本搞清楚了axios的整體流程:

組裝config->組裝header->呼叫請求攔截器->發送實際請求->呼叫返回攔截器。

但是我們還不知道axios具體是如何呼叫請求的,那麽接下來就要看dispatchRequest程式碼


// 暫且先記住,這個函式的作用就是用來判斷請求是否被取消,
// 如果要的話,則直接丟擲異常,
functionthrowIfCancellationRequested (config{
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
if (config.signal && config.signal.aborted) {
thrownew CanceledError(null, config);
}
}
// 發送請求核心函式
exportdefaultfunctiondispatchRequest (config{
// 剛開始請求前判斷一次是否取消
throwIfCancellationRequested(config);
config.headers = AxiosHeaders.from(config.headers);
// 執行數據轉換操作
config.data = transformData.call(
config,
config.transformRequest
);
// 預設設定請求頭的contentType為application/x-www-form-urlencoded
if (['post''put''patch'].indexOf(config.method) !== -1) {
config.headers.setContentType('application/x-www-form-urlencoded'false);
}
// 獲取介面卡,如果是瀏覽器環境獲取xhr,
// 如果是Node環境,獲取http 
// 介面卡就是最終用來發送請求的東西
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
// 請求是使用介面卡執行config
return adapter(config).then(functiononAdapterResolution (response{
// 請求完之後判斷是否要取消
throwIfCancellationRequested(config);
// 對返回結果進行轉換
response.data = transformData.call(
config,
config.transformResponse,
response
);
// 設定返回頭
response.headers = AxiosHeaders.from(response.headers);
return response;
}, functiononAdapterRejection (reason{
// 如果不是因為取消而報錯
if (!isCancel(reason)) {
// 再次判斷是否要取消,如果是會丟擲異常
throwIfCancellationRequested(config);
// 處理正常錯誤的返回值
if (reason && reason.response) {
reason.response.data = transformData.call(
config,
config.transformResponse,
reason.response
);
reason.response.headers = AxiosHeaders.from(reason.response.headers);
}
}
returnPromise.reject(reason);
});
}










3.5:adapter 請求介面卡,此處以xhr請求介面卡為例

dispatchRequest的流程還是相對簡單的,剩下的疑惑就是adapter幹了些什麽,讓我們接著往下看吧!

// 用於給上傳和下載進度增加監聽函式
functionprogressEventReducer (listener, isDownloadStream{
let bytesNotified = 0;
const _speedometer = speedometer(50250);
returne => {
const loaded = e.loaded;
const total = e.lengthComputable ? e.total : undefined;
const progressBytes = loaded - bytesNotified;
const rate = _speedometer(progressBytes);
const inRange = loaded <= total;
bytesNotified = loaded;
const data = {
loaded,
total,
progress: total ? (loaded / total) : undefined,
bytes: progressBytes,
rate: rate ? rate : undefined,
estimated: rate && total && inRange ? (total - loaded) / rate : undefined,
event: e
};
data[isDownloadStream ? 'download' : 'upload'] = true;
listener(data);
};
}
// 判斷是否支持XMLHttpRequest
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
// 介面卡的請求參數是config
exportdefault isXHRAdapterSupported && function (config{
// 返回Promise
returnnewPromise(functiondispatchXhrRequest (resolve, reject{
// 請求體
let requestData = config.data;
// 請求頭
const requestHeaders = AxiosHeaders.from(config.headers).normalize();
// 返回數據型別
const responseType = config.responseType;
let onCanceled;
// 
functiondone () {
if (config.cancelToken) {
config.cancelToken.unsubscribe(onCanceled);
}
if (config.signal) {
config.signal.removeEventListener('abort', onCanceled);
}
}
// 自動幫我們設定contentType,
// 這就是為什麽我們使用的時候都不需要
// 特別設定contentType的原因了
if (utils.isFormData(requestData)) {
if (platform.isStandardBrowserEnv || platform.isStandardBrowserWebWorkerEnv) {
// 瀏覽器環境讓瀏覽器設定
requestHeaders.setContentType(false); 
else {
requestHeaders.setContentType('multipart/form-data;'false); 
}
}
// 請求
let request = new XMLHttpRequest();
// 設定auth,幫我們轉碼好了
if (config.auth) {
const username = config.auth.username || '';
const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.set('Authorization''Basic ' + btoa(username + ':' + password));
}
// 拼接完整URL路徑
const fullPath = buildFullPath(config.baseURL, config.url);
// 開啟請求
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// 設定超時時間
request.timeout = config.timeout;
//
functiononloadend () {
if (!request) {
return;
}
// 預準備返回體的內容
const responseHeaders = AxiosHeaders.from(
'getAllResponseHeaders'in request && request.getAllResponseHeaders()
);
const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
};
// 請求完之後判斷請求是成功還是失敗
// 執行resolve和reject的操作
settle(function_resolve (value{
resolve(value);
done();
}, function_reject (err{
reject(err);
done();
}, response);
// 清除request
request = null;
}
if ('onloadend'in request) {
// 設定onloadend
request.onloadend = onloadend;
else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = functionhandleLoad () {
if (!request || request.readyState !== 4) {
return;
}
// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
// readystate之後再執行onloadend
setTimeout(onloadend);
};
}
// 處理瀏覽器請求取消事件
request.onabort = functionhandleAbort () {
if (!request) {
return;
}
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
request = null;
};
// 處理低階的網路錯誤
request.onerror = functionhandleError () {
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
request = null;
};
// 處理超時
request.ontimeout = functionhandleTimeout () {
let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
const transitional = config.transitional || transitionalDefaults;
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(new AxiosError(
timeoutErrorMessage,
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
config,
request));
request = null;
};
// 添加 xsrf
if (platform.isStandardBrowserEnv) {
const xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath))
&& config.xsrfCookieName && cookies.read(config.xsrfCookieName);
if (xsrfValue) {
requestHeaders.set(config.xsrfHeaderName, xsrfValue);
}
}
// 無請求體的話就移除contentType
requestData === undefined && requestHeaders.setContentType(null);
// 添加headers 
if ('setRequestHeader'in request) {
utils.forEach(requestHeaders.toJSON(), functionsetRequestHeader (val, key{
request.setRequestHeader(key, val);
});
}
// 添加withCredentials 
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// 添加responseType
if (responseType && responseType !== 'json') {
request.responseType = config.responseType;
}
// 增加下載過程的監聽函式
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
}
// 增加上傳過程的監聽函式
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
}
// 請求過程中取消
if (config.cancelToken || config.signal) {
onCanceled = cancel => {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
// 獲取請求協定,比如https這樣的
const protocol = parseProtocol(fullPath);
// 判斷當前環境是否支持該協定
if (protocol && platform.protocols.indexOf(protocol) === -1) {
reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
return;
}

// 發送請求
request.send(requestData || null);
});
}