一文讀懂Axios核心源碼思想
大廠技術(shù)??高級前端??Node進階
點擊上方?程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
閱讀完本文,下面的問題會迎刃而解,
Axios 的適配器原理是什么? Axios 是如何實現(xiàn)請求和響應(yīng)攔截的? Axios 取消請求的實現(xiàn)原理? CSRF 的原理是什么?Axios 是如何防范客戶端 CSRF 攻擊? 請求和響應(yīng)數(shù)據(jù)轉(zhuǎn)換是怎么實現(xiàn)的?
全文約兩千字,閱讀完大約需要 6 分鐘,文中 Axios 版本為 0.21.1
我們以特性作為入口,解答上述問題的同時一起感受下 Axios 源碼極簡封裝的藝術(shù)。
Features
從瀏覽器創(chuàng)建 XMLHttpRequest 從 Node.js 創(chuàng)建 HTTP 請求 支持 Promise API 攔截請求與響應(yīng) 取消請求 自動裝換 JSON 數(shù)據(jù) 支持客戶端 XSRF 攻擊
前兩個特性解釋了為什么 Axios 可以同時用于瀏覽器和 Node.js 的原因,簡單來說就是通過判斷是服務(wù)器還是瀏覽器環(huán)境,來決定使用 XMLHttpRequest 還是 Node.js 的 HTTP 來創(chuàng)建請求,這個兼容的邏輯被叫做適配器,對應(yīng)的源碼在 lib/defaults.js 中,
//?defaults.js
function?getDefaultAdapter()?{
??var?adapter;
??if?(typeof?XMLHttpRequest?!==?'undefined')?{
????//?For?browsers?use?XHR?adapter
????adapter?=?require('./adapters/xhr');
??}?else?if?(typeof?process?!==?'undefined'?&&?Object.prototype.toString.call(process)?===?'[object?process]')?{
????//?For?node?use?HTTP?adapter
????adapter?=?require('./adapters/http');
??}
??return?adapter;
}
以上是適配器的判斷邏輯,通過偵測當(dāng)前環(huán)境的一些全局變量,決定使用哪個 adapter。其中對于 Node 環(huán)境的判斷邏輯在我們做 ssr 服務(wù)端渲染的時候,也可以復(fù)用。接下來我們來看一下 Axios 對于適配器的封裝。
Adapter xhr
定位到源碼文件 lib/adapters/xhr.js,先來看下整體結(jié)構(gòu),
module.exports?=?function?xhrAdapter(config)?{
??return?new?Promise(function?dispatchXhrRequest(resolve,?reject)?{
????//?...
??})
}
導(dǎo)出了一個函數(shù),接受一個配置參數(shù),返回一個 Promise。我們把關(guān)鍵的部分提取出來,
module.exports?=?function?xhrAdapter(config)?{
??return?new?Promise(function?dispatchXhrRequest(resolve,?reject)?{
????var?requestData?=?config.data;
????var?request?=?new?XMLHttpRequest();
????request.open(config.method.toUpperCase(),?buildURL(fullPath,?config.params,?config.paramsSerializer),?true);
????request.onreadystatechange?=?function?handleLoad()?{}
????request.onabort?=?function?handleAbort()?{}
????request.onerror?=?function?handleError()?{}
????request.ontimeout?=?function?handleTimeout()?{}
????request.send(requestData);
??});
};
是不是感覺很熟悉?沒錯,這就是 XMLHttpRequest 的使用姿勢呀,先創(chuàng)建了一個 xhr 然后 open 啟動請求,監(jiān)聽 xhr 狀態(tài),然后 send 發(fā)送請求。我們來展開看一下 Axios 對于 onreadystatechange 的處理,
request.onreadystatechange?=?function?handleLoad()?{
??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;
??}
??//?Prepare?the?response
??var?responseHeaders?=?'getAllResponseHeaders'?in?request???parseHeaders(request.getAllResponseHeaders())?:?null;
??var?responseData?=?!config.responseType?||?config.responseType?===?'text'???request.responseText?:?request.response;
??var?response?=?{
????data:?responseData,
????status:?request.status,
????statusText:?request.statusText,
????headers:?responseHeaders,
????config:?config,
????request:?request
??};
??settle(resolve,?reject,?response);
??//?Clean?up?request
??request?=?null;
};
首先對狀態(tài)進行過濾,只有當(dāng)請求完成時(readyState === 4)才往下處理。需要注意的是,如果 XMLHttpRequest 請求出錯,大部分的情況下我們可以通過監(jiān)聽 onerror 進行處理,但是也有一個例外:當(dāng)請求使用文件協(xié)議(file://)時,盡管請求成功了但是大部分瀏覽器也會返回 0 的狀態(tài)碼。
Axios 針對這個例外情況也做了處理。
請求完成后,就要處理響應(yīng)了。這里將響應(yīng)包裝成一個標(biāo)準(zhǔn)格式的對象,作為第三個參數(shù)傳遞給了 settle 方法,settle 在 lib/core/settle.js 中定義,
function?settle(resolve,?reject,?response)?{
??var?validateStatus?=?response.config.validateStatus;
??if?(!response.status?||?!validateStatus?||?validateStatus(response.status))?{
????resolve(response);
??}?else?{
????reject(createError(
??????'Request?failed?with?status?code?'?+?response.status,
??????response.config,
??????null,
??????response.request,
??????response
????));
??}
};
settle 對 Promise 的回調(diào)進行了簡單的封裝,確保調(diào)用按一定的格式返回。
以上就是 xhrAdapter 的主要邏輯,剩下的是對請求頭,支持的一些配置項以及超時,出錯,取消請求等回調(diào)的簡單處理,其中對于 XSRF 攻擊的防范是通過請求頭實現(xiàn)的。
我們先來簡單回顧下什么是 XSRF (也叫 CSRF,跨站請求偽造)。
CSRF
背景:用戶登錄后,需要存儲登錄憑證保持登錄態(tài),而不用每次請求都發(fā)送賬號密碼。
怎么樣保持登錄態(tài)呢?
目前比較常見的方式是,服務(wù)器在收到 HTTP請求后,在響應(yīng)頭里添加 Set-Cookie 選項,將憑證存儲在 Cookie 中,瀏覽器接受到響應(yīng)后會存儲 Cookie,根據(jù)瀏覽器的同源策略,下次向服務(wù)器發(fā)起請求時,會自動攜帶 Cookie 配合服務(wù)端驗證從而保持用戶的登錄態(tài)。
所以如果我們沒有判斷請求來源的合法性,在登錄后通過其他網(wǎng)站向服務(wù)器發(fā)送了偽造的請求,這時攜帶登錄憑證的 Cookie 就會隨著偽造請求發(fā)送給服務(wù)器,導(dǎo)致安全漏洞,這就是我們說的 CSRF,跨站請求偽造。
所以防范偽造請求的關(guān)鍵就是檢查請求來源,refferer 字段雖然可以標(biāo)識當(dāng)前站點,但是不夠可靠,現(xiàn)在業(yè)界比較通用的解決方案還是在每個請求上附帶一個 anti-CSRF token,這個的原理是攻擊者無法拿到 Cookie,所以我們可以通過對 Cookie 進行加密(比如對 sid 進行加密),然后配合服務(wù)端做一些簡單的驗證,就可以判斷當(dāng)前請求是不是偽造的。
Axios 簡單地實現(xiàn)了對特殊 csrf token 的支持,
//?Add?xsrf?header
//?This?is?only?done?if?running?in?a?standard?browser?environment.
//?Specifically?not?if?we're?in?a?web?worker,?or?react-native.
if?(utils.isStandardBrowserEnv())?{
??//?Add?xsrf?header
??var?xsrfValue?=?(config.withCredentials?||?isURLSameOrigin(fullPath))?&&?config.xsrfCookieName??
????cookies.read(config.xsrfCookieName)?:
????undefined;
??if?(xsrfValue)?{
????requestHeaders[config.xsrfHeaderName]?=?xsrfValue;
??}
}
Interceptor
攔截器是 Axios 的一個特色 Feature,我們先簡單回顧下使用方式,
//?攔截器可以攔截請求或響應(yīng)
//?攔截器的回調(diào)將在請求或響應(yīng)的?then?或?catch?回調(diào)前被調(diào)用
var?instance?=?axios.create(options);
var?requestInterceptor?=?axios.interceptors.request.use(
??(config)?=>?{
????//?do?something?before?request?is?sent
????return?config;
??},
??(err)?=>?{
????//?do?somthing?with?request?error
????return?Promise.reject(err);
??}
);
//?移除已設(shè)置的攔截器
axios.interceptors.request.eject(requestInterceptor)
那么攔截器是怎么實現(xiàn)的呢?
定位到源碼 lib/core/Axios.js 第 14 行,
function?Axios(instanceConfig)?{
??this.defaults?=?instanceConfig;
??this.interceptors?=?{
????request:?new?InterceptorManager(),
????response:?new?InterceptorManager()
??};
}
通過 Axios 的構(gòu)造函數(shù)可以看到,攔截器 interceptors 中的 request 和 response 兩者都是一個叫做 InterceptorManager 的實例,這個 InterceptorManager 是什么?
定位到源碼 lib/core/InterceptorManager.js,
function?InterceptorManager()?{
??this.handlers?=?[];
}
InterceptorManager.prototype.use?=?function?use(fulfilled,?rejected,?options)?{
??this.handlers.push({
????fulfilled:?fulfilled,
????rejected:?rejected,
????synchronous:?options???options.synchronous?:?false,
????runWhen:?options???options.runWhen?:?null
??});
??return?this.handlers.length?-?1;
};
InterceptorManager.prototype.eject?=?function?eject(id)?{
??if?(this.handlers[id])?{
????this.handlers[id]?=?null;
??}
};
InterceptorManager.prototype.forEach?=?function?forEach(fn)?{
??utils.forEach(this.handlers,?function?forEachHandler(h)?{
????if?(h?!==?null)?{
??????fn(h);
????}
??});
};
InterceptorManager 是一個簡單的事件管理器,實現(xiàn)了對攔截器的管理,
通過 handlers 存儲攔截器,然后提供了添加,移除,遍歷執(zhí)行攔截器的實例方法,存儲的每一個攔截器對象都包含了作為 Promise 中 resolve 和 reject 的回調(diào)以及兩個配置項。
值得一提的是,移除方法是通過直接將攔截器對象設(shè)置為 null 實現(xiàn)的,而不是 splice 剪切數(shù)組,遍歷方法中也增加了相應(yīng)的 null 值處理。這樣做一方面使得每一項ID保持為項的數(shù)組索引不變,另一方面也避免了重新剪切拼接數(shù)組的性能損失。
攔截器的回調(diào)會在請求或響應(yīng)的 then 或 catch 回調(diào)前被調(diào)用,這是怎么實現(xiàn)的呢?
回到源碼 lib/core/Axios.js 中第 27 行,Axios 實例對象的 request 方法,
我們提取其中的關(guān)鍵邏輯如下,
Axios.prototype.request?=?function?request(config)?{
??//?Get?merged?config
??//?Set?config.method
??//?...
??var?requestInterceptorChain?=?[];
??this.interceptors.request.forEach(function?unshiftRequestInterceptors(interceptor)?{
????requestInterceptorChain.unshift(interceptor.fulfilled,?interceptor.rejected);
??});
?var?responseInterceptorChain?=?[];
??this.interceptors.response.forEach(function?pushResponseInterceptors(interceptor)?{
????responseInterceptorChain.push(interceptor.fulfilled,?interceptor.rejected);
??});
??var?promise;
??var?chain?=?[dispatchRequest,?undefined];
??Array.prototype.unshift.apply(chain,?requestInterceptorChain);
??chain.concat(responseInterceptorChain);
??promise?=?Promise.resolve(config);
??while?(chain.length)?{
????promise?=?promise.then(chain.shift(),?chain.shift());
??}
??return?promise;
};
可以看到,當(dāng)執(zhí)行 request 時,實際的請求(dispatchRequest)和攔截器是通過一個叫 chain 的隊列來管理的。整個請求的邏輯如下,
首先初始化請求和響應(yīng)的攔截器隊列,將 resolve,reject 回調(diào)依次放入隊頭 然后初始化一個 Promise 用來執(zhí)行回調(diào),chain 用來存儲和管理實際請求和攔截器 將請求攔截器放入 chain 隊頭,響應(yīng)攔截器放入 chain 隊尾 隊列不為空時,通過 Promise.then 的鏈?zhǔn)秸{(diào)用,依次將請求攔截器,實際請求,響應(yīng)攔截器出隊 最后返回鏈?zhǔn)秸{(diào)用后的 Promise
這里的實際請求是對適配器的封裝,請求和響應(yīng)數(shù)據(jù)的轉(zhuǎn)換都在這里完成。
那么數(shù)據(jù)轉(zhuǎn)換是如何實現(xiàn)的呢?
Transform data
定位到源碼 lib/core/dispatchRequest.js,
function?dispatchRequest(config)?{
??throwIfCancellationRequested(config);
??//?Transform?request?data
??config.data?=?transformData(
????config.data,
????config.headers,
????config.transformRequest
??);
??
??var?adapter?=?config.adapter?||?defaults.adapter;
??return?adapter(config).then(function?onAdapterResolution(response)?{
????throwIfCancellationRequested(config);
????//?Transform?response?data
????response.data?=?transformData(
??????response.data,
??????response.headers,
??????config.transformResponse
????);
????return?response;
??},?function?onAdapterRejection(reason)?{
????if?(!isCancel(reason))?{
??????throwIfCancellationRequested(config);
??????//?Transform?response?data
??????if?(reason?&&?reason.response)?{
????????reason.response.data?=?transformData(
??????????reason.response.data,
??????????reason.response.headers,
??????????config.transformResponse
????????);
??????}
????}
????return?Promise.reject(reason);
??});
};
這里的 throwIfCancellationRequested 方法用于取消請求,關(guān)于取消請求稍后我們再討論,可以看到發(fā)送請求是通過調(diào)用適配器實現(xiàn)的,在調(diào)用前和調(diào)用后會對請求和響應(yīng)數(shù)據(jù)進行轉(zhuǎn)換。
轉(zhuǎn)換通過 transformData 函數(shù)實現(xiàn),它會遍歷調(diào)用設(shè)置的轉(zhuǎn)換函數(shù),轉(zhuǎn)換函數(shù)將 headers 作為第二個參數(shù),所以我們可以根據(jù) headers 中的信息來執(zhí)行一些不同的轉(zhuǎn)換操作,
//?源碼?core/transformData.js
function?transformData(data,?headers,?fns)?{
??utils.forEach(fns,?function?transform(fn)?{
????data?=?fn(data,?headers);
??});
??return?data;
};
Axios 也提供了兩個默認(rèn)的轉(zhuǎn)換函數(shù),用于對請求和響應(yīng)數(shù)據(jù)進行轉(zhuǎn)換。默認(rèn)情況下,
Axios 會對請求傳入的 data 做一些處理,比如請求數(shù)據(jù)如果是對象,會序列化為 JSON 字符串,響應(yīng)數(shù)據(jù)如果是 JSON 字符串,會嘗試轉(zhuǎn)換為 JavaScript 對象,這些都是非常實用的功能,
對應(yīng)的轉(zhuǎn)換器源碼可以在 lib/default.js 的第 31 行找到,
var?defaults?=?{
?//?Line?31
??transformRequest:?[function?transformRequest(data,?headers)?{
????normalizeHeaderName(headers,?'Accept');
????normalizeHeaderName(headers,?'Content-Type');
????if?(utils.isFormData(data)?||
??????utils.isArrayBuffer(data)?||
??????utils.isBuffer(data)?||
??????utils.isStream(data)?||
??????utils.isFile(data)?||
??????utils.isBlob(data)
????)?{
??????return?data;
????}
????if?(utils.isArrayBufferView(data))?{
??????return?data.buffer;
????}
????if?(utils.isURLSearchParams(data))?{
??????setContentTypeIfUnset(headers,?'application/x-www-form-urlencoded;charset=utf-8');
??????return?data.toString();
????}
????if?(utils.isObject(data))?{
??????setContentTypeIfUnset(headers,?'application/json;charset=utf-8');
??????return?JSON.stringify(data);
????}
????return?data;
??}],
??
??transformResponse:?[function?transformResponse(data)?{
????var?result?=?data;
????if?(utils.isString(result)?&&?result.length)?{
??????try?{
????????result?=?JSON.parse(result);
??????}?catch?(e)?{?/*?Ignore?*/?}
????}
????return?result;
??}],
}
我們說 Axios 是支持取消請求的,怎么個取消法呢?
CancelToken
其實不管是瀏覽器端的 xhr 或 Node.js 里 http 模塊的 request 對象,都提供了 abort 方法用于取消請求,所以我們只需要在合適的時機調(diào)用 abort 就可以實現(xiàn)取消請求了。
那么,什么是合適的時機呢?控制權(quán)交給用戶就合適了。所以這個合適的時機應(yīng)該由用戶決定,也就是說我們需要將取消請求的方法暴露出去,Axios 通過 CancelToken 實現(xiàn)取消請求,我們來一起看下它的姿勢。
首先 Axios 提供了兩種方式創(chuàng)建 cancel token,
const?CancelToken?=?axios.CancelToken;
const?source?=?CancelToken.source();
//?方式一,使用?CancelToken?實例提供的靜態(tài)屬性?source
axios.post("/user/12345",?{?name:?"monch"?},?{?cancelToken:?source.token?});
source.cancel();
//?方式二,使用?CancelToken?構(gòu)造函數(shù)自己實例化
let?cancel;
axios.post(
??"/user/12345",
??{?name:?"monch"?},
??{
????cancelToken:?new?CancelToken(function?executor(c)?{
??????cancel?=?c;
????}),
??}
);
cancel();
到底什么是 CancelToken?定位到源碼 lib/cancel/CancelToken.js 第 11 行,
function?CancelToken(executor)?{
??if?(typeof?executor?!==?"function")?{
????throw?new?TypeError("executor?must?be?a?function.");
??}
??var?resolvePromise;
??this.promise?=?new?Promise(function?promiseExecutor(resolve)?{
????resolvePromise?=?resolve;
??});
??var?token?=?this;
??executor(function?cancel(message)?{
????if?(token.reason)?{
??????//?Cancellation?has?already?been?requested
??????return;
????}
????token.reason?=?new?Cancel(message);
????resolvePromise(token.reason);
??});
}
CancelToken 就是一個由 promise 控制的極簡的狀態(tài)機,實例化時會在實例上掛載一個 promise,這個 promise 的 resolve 回調(diào)暴露給了外部方法 executor,這樣一來,我們從外部調(diào)用這個 executor方法后就會得到一個狀態(tài)變?yōu)?fulfilled 的 promise,那有了這個 promise 后我們?nèi)绾稳∠埱竽兀?/p>
是不是只要在請求時拿到這個 promise 實例,然后在 then 回調(diào)里取消請求就可以了?
定位到適配器的源碼 lib/adapters/xhr.js 第 158 行,
if?(config.cancelToken)?{
??//?Handle?cancellation
??config.cancelToken.promise.then(function?onCanceled(cancel)?{
????if?(!request)?{
??????return;
????}
????request.abort();
????reject(cancel);
????//?Clean?up?request
????request?=?null;
??});
}
以及源碼 lib/adaptors/http.js 第 291 行,
if?(config.cancelToken)?{
??//?Handle?cancellation
??config.cancelToken.promise.then(function?onCanceled(cancel)?{
????if?(req.aborted)?return;
????req.abort();
????reject(cancel);
??});
}
果然如此,在適配器里 CancelToken 實例的 promise 的 then 回調(diào)里調(diào)用了 xhr 或 http.request 的 abort 方法。試想一下,如果我們沒有從外部調(diào)用取消 CancelToken 的方法,是不是意味著 resolve 回調(diào)不會執(zhí)行,適配器里的 promise 的 then 回調(diào)也不會執(zhí)行,就不會調(diào)用 abort 取消請求了。
小結(jié)
Axios 通過適配器的封裝,使得它可以在保持同一套接口規(guī)范的前提下,同時用在瀏覽器和 node.js 中。源碼中大量使用 Promise 和閉包等特性,實現(xiàn)了一系列的狀態(tài)控制,其中對于攔截器,取消請求的實現(xiàn)體現(xiàn)了其極簡的封裝藝術(shù),值得學(xué)習(xí)和借鑒。
參考鏈接
Axios Docs - axios-http.com[1] Axios Github Source Code[2] 源碼拾遺 Axios —— 極簡封裝的藝術(shù)[3] Cross Site Request Forgery - Part III. Web Application Security[4] tc39/proposal-cancelable-promises[5]
寫在最后
本文首發(fā)于我的 博客[6],才疏學(xué)淺,難免有錯誤,文章有誤之處還望不吝指正!
如果有疑問或者發(fā)現(xiàn)錯誤,可以在相應(yīng)的 issues 進行提問或勘誤
如果喜歡或者有所啟發(fā),歡迎 star,對作者也是一種鼓勵
(完)
作者:campcc
https://github.com/campcc/blog/issues/23
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
???“分享、點贊、在看” 支持一波??
