<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文讀懂Axios核心源碼思想

          共 12835字,需瀏覽 26分鐘

           ·

          2022-01-19 18:16

          大廠技術(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 的隊列來管理的。整個請求的邏輯如下,

          1. 首先初始化請求和響應(yīng)的攔截器隊列,將 resolve,reject 回調(diào)依次放入隊頭
          2. 然后初始化一個 Promise 用來執(zhí)行回調(diào),chain 用來存儲和管理實際請求和攔截器
          3. 將請求攔截器放入 chain 隊頭,響應(yīng)攔截器放入 chain 隊尾
          4. 隊列不為空時,通過 Promise.then 的鏈?zhǔn)秸{(diào)用,依次將請求攔截器,實際請求,響應(yīng)攔截器出隊
          5. 最后返回鏈?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 社群


          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。


          ???“分享、點贊在看” 支持一波??

          瀏覽 28
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  香蕉成人在线视频 | 豆花视频免费看 | 亚洲日韩AV电影 | 欧美性生活69 | 精品国产免费无码久久久 |