剛出鍋的 Axios 網(wǎng)絡(luò)請(qǐng)求源碼閱讀筆記,你會(huì)嗎?
項(xiàng)目中一直都有用到 Axios 作為網(wǎng)絡(luò)請(qǐng)求工具,用它更要懂它,因此為了更好地發(fā)揮 Axios 在項(xiàng)目的價(jià)值,以及日后能夠得心應(yīng)手地使用它,筆者決定從源碼層面好好欣賞一下它的美貌!
Axios是一款基于 Promise 并可用于瀏覽器和 Node.js 的網(wǎng)絡(luò)請(qǐng)求庫。
- Github:https://github.com/axios/axios
- NPM:https://www.npmjs.com/package/axios
- Docs:https://axios-http.com/docs/intro
最近,Axios 官方文檔終于變好看了,支持多語言切換,閱讀更清晰,使用起來也更加舒適!作為一款受全球歡迎的網(wǎng)絡(luò)請(qǐng)求庫,有必要偷學(xué)一下其中的架構(gòu)設(shè)計(jì)、編碼方式。
本篇文章從源碼層面主要分析 Axios 的功能實(shí)現(xiàn)、設(shè)計(jì)模式、以及分享 Axios 中一些筆者認(rèn)為比較“精彩”的地方!
本文主要內(nèi)容結(jié)構(gòu)如下,大家按需食用:
一、Axios 項(xiàng)目概況
本次分析的 Axios 版本是:v0.24.0
通過簡(jiǎn)單的瀏覽 package.json、文件及目錄,可以得知 axios 工程采用了如下三方依賴:
名稱 | 說明 |
---|---|
Grunt[1] | JavaScript 任務(wù)運(yùn)行器 |
dtslint[2] | TypeScript 類型聲明&樣式校驗(yàn)工具 |
TypeScript[3] | 支持TS環(huán)境下開發(fā) |
Webpack[4] | JavaScript 模塊打包工具 |
karma[5] | 測(cè)試用例檢查器 |
mocha[6] | 多功能的 JavaScript 測(cè)試框架 |
sinojs[7] | 提供spies, stub, mock,推薦文章《Sinon 入門,看這篇文章就夠了[8]》 |
follow-redirects[9] | http(s)重定向,NodeJS模塊 |
這里省略了對(duì)一些工具介紹,但可以發(fā)現(xiàn),Axios 開發(fā)項(xiàng)目的主功能依賴并不多,換句話說是只有 follow-redirects作為了“使用依賴”,其他都是編譯、測(cè)試、框架層面的東西,可以看出官方團(tuán)隊(duì)在對(duì)于 Axios 有多么注質(zhì)量和穩(wěn)定性,畢竟是全球都在用的工具。
Axios 中相關(guān)代碼都在 lib/ 目錄下(建議逐行閱讀):
- .
- ├── adapters // 網(wǎng)絡(luò)請(qǐng)求,NodeJS 環(huán)境使用 NodeJS 的 http 模塊,瀏覽器使用 XHR
- │ ├── README.md
- │ ├── http.js // Node.js 環(huán)境使用
- │ └── xhr.js // 瀏覽器環(huán)境使用
- ├── helpers // 一些功能輔助工具函數(shù),看文件名可基本知道干啥的
- │ ├── README.md
- │ ├── bind.js
- │ ├── buildURL.js
- │ ├── combineURLs.js
- │ ├── cookies.js
- │ ├── deprecatedMethod.js
- │ ├── isAbsoluteURL.js
- │ ├── isAxiosError.js
- │ ├── isURLSameOrigin.js
- │ ├── normalizeHeaderName.js
- │ ├── parseHeaders.js
- │ ├── spread.js
- │ └── validator.js
- ├── cancel // 取消網(wǎng)絡(luò)請(qǐng)求的處理
- │ ├── Cancel.js // 取消請(qǐng)求
- │ ├── CancelToken.js // 取消 Token
- │ └── isCancel.js // 判斷是否取消請(qǐng)求的函數(shù)方法
- ├── core // 核心功能
- │ ├── Axios.js // Axios 對(duì)象
- │ ├── InterceptorManager.js // 攔截器管理
- │ ├── README.md
- │ ├── buildFullPath.js // 構(gòu)造完成的請(qǐng)求 URL
- │ ├── createError.js // 創(chuàng)建錯(cuò)誤,拋出異常
- │ ├── dispatchRequest.js // 請(qǐng)求分發(fā),用于區(qū)分調(diào)用 http 還是 xhr
- │ ├── enhanceError.js // 增強(qiáng)錯(cuò)誤???????????????
- │ ├── mergeConfig.js // 合并配置參數(shù)
- │ ├── settle.js // 根據(jù)請(qǐng)求響應(yīng)狀態(tài),改變 Promise 狀態(tài)
- │ └── transformData.js // 數(shù)據(jù)格式轉(zhuǎn)換
- ├── env // 無關(guān)緊要,沒啥用,與發(fā)包版本有關(guān)
- │ ├── README.md
- │ └── data.js
- ├── defaults.js // 默認(rèn)參數(shù)/初始化參數(shù)配置
- ├── utils.js // 提供簡(jiǎn)單的通用的工具函數(shù)
- └── axios.js // 入口文件,初始化并導(dǎo)出 axios 對(duì)象
有了一個(gè)簡(jiǎn)單的代碼功能組織架構(gòu)熟悉后,對(duì)于串聯(lián) Axios 的功能很有好處,另外,從上述文件和文件夾的命名,很容易讓人意識(shí)到這是一個(gè)什么功能的文件。
“高內(nèi)聚、低耦合”的真言,在 Axios 中應(yīng)該算是一個(gè)運(yùn)用得很好的例子。
二、Axios 網(wǎng)絡(luò)請(qǐng)求流程圖
梳理了一張 Axios 發(fā)起請(qǐng)求、響應(yīng)請(qǐng)求的執(zhí)行流程圖,希望可以給大家一個(gè)完整流程的概念,便于理解后續(xù)的源碼分析。
Axios 網(wǎng)絡(luò)請(qǐng)求流程圖
三、Axios API 設(shè)計(jì)
我們?cè)谑褂?Axios 的時(shí)候,會(huì)覺得 Axios 的使用特別方便,其原因就是 Axios 中針對(duì)同一功能實(shí)現(xiàn)了不同的 API,便于大家在各種場(chǎng)景下的變通擴(kuò)展使用。
例如,發(fā)起一個(gè) GET 請(qǐng)求的寫法有:
- // 第一種
- axios('https://xxx.com/api/userInfo?uid=1')
- // 第二種
- axios.get('https://xxx.com/api/userInfo?uid=1')
- // 第三種
- axios({
- method: 'GET',
- url: 'https://xxx.com/api/userInfo?uid=1'
- })
Axios 請(qǐng)求的核心方法僅兩種:
- axios(config)
- // or
- axios(url[, config])
我們知道一個(gè)網(wǎng)絡(luò)請(qǐng)求的方式會(huì)有 GET、POST、PUT、DELETE 等,為了使用更加語義化,Axios 對(duì)外暴露了別名 API:
- axios.request(config)
- axios.get(url[, config])
- axios.delete(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]])
通過遍歷擴(kuò)展axios對(duì)象原型鏈上的方法:
- // Provide aliases for supported request methods
- utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
- /*eslint func-names:0*/
- Axios.prototype[method] = function(url, config) {
- return this.request(mergeConfig(config || {}, {
- method: method,
- url: url,
- data: (config || {}).data
- }));
- };
- });
- utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
- /*eslint func-names:0*/
- Axios.prototype[method] = function(url, data, config) {
- return this.request(mergeConfig(config || {}, {
- method: method,
- url: url,
- data: data
- }));
- };
- });
能夠如上的直接循環(huán)列表賦值,得益于 Axios 將核心的請(qǐng)求功能單獨(dú)放到了 Axios.prototype.request 方法中,該方法的 TS 定義為:
- Axios.request(config: any, ...args: any[]): any
在其方法(Axios.request())內(nèi)會(huì)對(duì)外部傳參數(shù)類型做判斷,并選擇組裝正確的請(qǐng)求參數(shù):
- // 生成規(guī)范的 config,抹平 API(函數(shù)入?yún)ⅲ┎町?nbsp;
- if (typeof config === 'string') {
- // 處理了第一個(gè)參數(shù)是 url 字符串的情況 request(url[, config])
- config = arguments[1] || {};
- config.url = arguments[0];
- } else {
- config = config || {};
- }
- // 合并默認(rèn)配置
- config = mergeConfig(this.defaults, config);
- // 將請(qǐng)求方法轉(zhuǎn)小寫字母,默認(rèn)為 get 方法
- if (config.method) {
- config.method = config.method.toLowerCase();
- } else if (this.defaults.method) {
- config.method = this.defaults.method.toLowerCase();
- } else {
- config.method = 'get';
- }
以此來抹平了各種類型請(qǐng)求以及所需傳入?yún)?shù)之間的差異性!
四、Axios 工廠模式創(chuàng)建實(shí)例
默認(rèn) Axios 導(dǎo)出了一個(gè)單例,導(dǎo)出了一個(gè)實(shí)例化后的單例,所以我們可以直接引入后就可以調(diào)用 Axios 的方法。
在某些場(chǎng)景下,我們的項(xiàng)目中可能對(duì)接了多個(gè)業(yè)務(wù)方,那么請(qǐng)求中的 base URL 就不一樣,因此有沒有辦法創(chuàng)建多個(gè) Axios 實(shí)例?
那就是使用 axios.create([config]) 方法創(chuàng)建多個(gè)實(shí)例。
考慮到多實(shí)例這樣的實(shí)際需求,Axios 對(duì)外暴露了 create() 方法,在 Axios 內(nèi)部中,往導(dǎo)出的 axios 實(shí)例上綁定了用于創(chuàng)建本身實(shí)例的工廠方法:
- /**
- * Create an instance of Axios
- *
- * @param {Object} defaultConfig The default config for the instance
- * @return {Axios} A new instance of Axios
- */
- function createInstance(defaultConfig) {
- var context = new Axios(defaultConfig);
- var instance = bind(Axios.prototype.request, context);
- // Copy axios.prototype to instance
- utils.extend(instance, Axios.prototype, context);
- // Copy context to instance
- utils.extend(instance, context);
- // Factory for creating new instances
- instance.create = function create(instanceConfig) {
- return createInstance(mergeConfig(defaultConfig, instanceConfig));
- };
- return instance;
- }
這里的實(shí)現(xiàn)值得一說的地方在于:
- instance.create = function create(instanceConfig) {
- return createInstance(mergeConfig(defaultConfig, instanceConfig));
- };
在創(chuàng)建 axios 實(shí)例的工廠方法內(nèi),綁定工廠方法到實(shí)例的 create 屬性上。為什么不是在工廠方法外綁定吶?這是我們可能的習(xí)慣做法,Axios 之前確實(shí)也是這么做的。
為什么挪到了內(nèi)部?可以看看這條 PR: Allow axios.create(options) to be used recursively[10]
原因簡(jiǎn)單來說就是,用戶自己創(chuàng)建的實(shí)例依然可以調(diào)用 create 方法創(chuàng)建新的實(shí)例,例如:
- const axios = require('axios');
- const jsonClient = axios.create({
- responseType: 'json' // 該項(xiàng)配置可以在后續(xù)創(chuàng)建的實(shí)例中復(fù)用,而不必重復(fù)編碼
- });
- const serviceOne = jsonClient.create({
- baseURL: 'https://service.one/'
- });
- const serviceTwo = jsonClient.create({
- baseURL: 'https://service.two/'
- });
這樣有助于復(fù)用實(shí)例的公共參數(shù)復(fù)用,減少重復(fù)編碼。
五、網(wǎng)絡(luò)請(qǐng)求適配器
在文件 ./defaults.js 中生成了默認(rèn)完整的 Request Config 參數(shù)。
其中 config.adapter 字段表明當(dāng)前應(yīng)該使用 ./adapters/目錄下的 http.js 還是 xhr.js 模塊。
- // 根據(jù)當(dāng)前使用環(huán)境,選擇使用的網(wǎng)絡(luò)請(qǐng)求適配器
- 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;
- }
這里使用了設(shè)計(jì)模式中的適配器模式,通過判斷不同環(huán)境下是否支持方法的方式,選擇正確的網(wǎng)絡(luò)請(qǐng)求模塊,便可以實(shí)現(xiàn)官網(wǎng)所說的支持 NodeJS 和瀏覽器環(huán)境。
六、轉(zhuǎn)換請(qǐng)求體和響應(yīng)體數(shù)據(jù)
這是 Axios 貼在官網(wǎng)的核心功能之一,且提到了可以自動(dòng)轉(zhuǎn)換響應(yīng)體內(nèi)容為 JSON 數(shù)據(jù)。
默認(rèn)請(qǐng)求配置中初始化的請(qǐng)求/響應(yīng)轉(zhuǎn)換器數(shù)組。
自動(dòng)嘗試轉(zhuǎn)換響應(yīng)數(shù)據(jù)為 JSON 格式
transformRequest 和 transformResponse 字段是一個(gè)數(shù)組類型,因此我們還可以向其中增加自定義的轉(zhuǎn)換器。
一般來講我們只會(huì)通過復(fù)寫 transitional 字段來控制響應(yīng)數(shù)據(jù)的轉(zhuǎn)換與否,但可以作為擴(kuò)展 Axios 的一個(gè)點(diǎn),留了口子,這一點(diǎn)考慮得也很到位。
七、請(qǐng)求攔截器&響應(yīng)攔截器
可以通過攔截器來提前處理請(qǐng)求前和收到響應(yīng)前的一些處理方法。
7.1 攔截器的使用
攔截器用于在 .then() 和 .catch() 前注入并執(zhí)行的一些方法。
- // 通過 use 方法,添加一個(gè)請(qǐng)求攔截器
- axios.interceptors.request.use(function (config) {
- // 在發(fā)送請(qǐng)求前干點(diǎn)啥,.then() 處理之前,比如修改 request config
- return config;
- }, function (error) {
- // 在發(fā)起請(qǐng)求發(fā)生錯(cuò)誤后,.catch() 處理之前干點(diǎn)啥
- return Promise.reject(error);
- });
- // 通過 use 方法,添加一個(gè)響應(yīng)攔截器
- axios.interceptors.response.use(function (response) {
- // 只要響應(yīng)網(wǎng)絡(luò)狀態(tài)碼是 2xx 的都會(huì)觸發(fā)
- // 干點(diǎn)啥
- return response;
- }, function (error) {
- // 狀態(tài)碼不是 2xx 的會(huì)觸發(fā)
- // 發(fā)生錯(cuò)誤了,干點(diǎn)啥
- return Promise.reject(error);
- });
7.2 攔截管理器
Axios 將請(qǐng)求和響應(yīng)的過程包裝成了 Promise,那么 Axios 是如何實(shí)現(xiàn)攔截器在 .then() 和 .catch() 執(zhí)行前執(zhí)行吶?
可以很容易猜到通過組裝一條 Promise 執(zhí)行鏈即可!
來看看 Axios 在請(qǐng)求函數(shù)中如何實(shí)現(xiàn):
首先是 Axios 對(duì)象中初始化了 攔截管理器:
- function Axios(instanceConfig) {
- this.defaults = instanceConfig;
- this.interceptors = {
- request: new InterceptorManager(),
- response: new InterceptorManager()
- };
- }
來到 ./lib/core/InterceptorManager.js 文件下,對(duì)于攔截管理器:
- // 攔截管理器對(duì)象
- function InterceptorManager() {
- this.handlers = [];
- }
- /**
- * 添加新的管理器,定義了 use 方法
- *
- * @param {Function} fulfilled 處理 `Promise` 執(zhí)行 `then` 的函數(shù)方法
- * @param {Function} rejected 處理 `Promise` 執(zhí)行 `reject` 的函數(shù)方法
- *
- * @return {Number} 返回一個(gè) ID 值用于移除攔截器
- */
- InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
- this.handlers.push({
- fulfilled: fulfilled,
- rejected: rejected,
- // 默認(rèn)不同步
- synchronous: options ? options.synchronous : false,
- // 定義是否執(zhí)行當(dāng)前攔截器的函數(shù)或布爾值
- runWhen: options ? options.runWhen : null
- });
- return this.handlers.length - 1; // ID 值實(shí)際就是當(dāng)前攔截器的數(shù)組索引
- };
- /**
- * 從棧中移除指定 id 的攔截器
- *
- * @param {Number} id use 方法返回的 id 值
- */
- InterceptorManager.prototype.eject = function eject(id) {
- if (this.handlers[id]) {
- this.handlers[id] = null; // 刪除攔截器,但索引會(huì)保留
- }
- };
- /**
- * 迭代所有注冊(cè)的攔截器
- * 該方法會(huì)跳過因攔截器被刪除而值為 null 的索引
- *
- * @param {Function} 調(diào)用每個(gè)有效攔截器的函數(shù)
- */
- InterceptorManager.prototype.forEach = function forEach(fn) {
- utils.forEach(this.handlers, function forEachHandler(h) {
- if (h !== null) {
- fn(h);
- }
- });
- };
迭代所有注冊(cè)的攔截器是一個(gè) FIFS(first come first served,先到先服務(wù))隊(duì)列執(zhí)行順序的方法。
7.3 組裝攔截器與請(qǐng)求執(zhí)行鏈
在 ./lib/core/Axios.js 文件中,Axios 對(duì)象定義了 request 方法,其中將網(wǎng)絡(luò)請(qǐng)求、請(qǐng)求攔截器和響應(yīng)攔截器組裝。
默認(rèn)返回一個(gè)還未執(zhí)行網(wǎng)絡(luò)請(qǐng)求的 Promise 執(zhí)行鏈,如果設(shè)置了同步,則會(huì)立即執(zhí)行請(qǐng)求過程,并返回請(qǐng)求結(jié)果的 Promise 對(duì)象,也就是官方文檔中提到的 Axios 還支持 Promise API。
函數(shù)詳細(xì)的分析,都已經(jīng)注釋在如下代碼中:
- /**
- * Dispatch a request
- *
- * @param {Object} config 傳入的用戶自定義配置,并和默認(rèn)配置 merge
- */
- Axios.prototype.request = function request(config) {
- // 省略 ...
- // 請(qǐng)求攔截器執(zhí)行鏈
- var requestInterceptorChain = [];
- // 同步請(qǐng)求攔截器
- var synchronousRequestInterceptors = true;
- // 遍歷請(qǐng)求攔截器
- this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
- // 判斷 runWhen 如果是函數(shù),則執(zhí)行函數(shù),結(jié)果若為 false,則不執(zhí)行當(dāng)前攔截器
- if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
- return;
- }
- // 判斷當(dāng)前攔截器是否同步
- synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
- // 插入 requestInterceptorChain 數(shù)組首位
- // 效果:[interceptor.fulfilled, interceptor.rejected, ...]
- requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
- });
- // 響應(yīng)攔截器執(zhí)行鏈
- var responseInterceptorChain = [];
- // 遍歷所有的響應(yīng)攔截器
- this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
- // 插入 responseInterceptorChain 尾部
- // 效果:[ ..., interceptor.fulfilled, interceptor.rejected]
- responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
- });
- var promise;
- // 如果非同步
- // 一般大家在使用 axios.interceptors.request.use 都沒有傳遞第三個(gè)配置參數(shù)
- // 所以一般情況下會(huì)走這個(gè)邏輯
- if (!synchronousRequestInterceptors) {
- var chain = [dispatchRequest, undefined];
- // 將請(qǐng)求攔截器執(zhí)行鏈放到 chain 數(shù)組頭部
- Array.prototype.unshift.apply(chain, requestInterceptorChain);
- // 將響應(yīng)攔截器執(zhí)行鏈放到 chain 數(shù)組末尾
- chain = chain.concat(responseInterceptorChain);
- // 給 promise 賦值 Promise 對(duì)象,并注入 request config
- promise = Promise.resolve(config);
- // 循環(huán) chain 數(shù)組,組合成 Promise 執(zhí)行鏈
- while (chain.length) {
- // 正好 resolve 和 reject 對(duì)應(yīng)方法,兩兩一組
- promise = promise.then(chain.shift(), chain.shift());
- }
- // 返回 Promise 執(zhí)行鏈
- return promise;
- }
- // 同步方式
- var newConfig = config;
- // 循環(huán)并執(zhí)行所有請(qǐng)求攔截器
- while (requestInterceptorChain.length) {
- var onFulfilled = requestInterceptorChain.shift();
- var onRejected = requestInterceptorChain.shift();
- try {
- // 執(zhí)行定義請(qǐng)求前的“請(qǐng)求攔截器” then 處理方法
- newConfig = onFulfilled(newConfig);
- } catch (error) {
- // 執(zhí)行定義請(qǐng)求前的“請(qǐng)求攔截器” catch 處理方法
- onRejected(error);
- break;
- }
- }
- try {
- // 執(zhí)行網(wǎng)絡(luò)請(qǐng)求
- promise = dispatchRequest(newConfig);
- } catch (error) {
- return Promise.reject(error);
- }
- // 循環(huán)并執(zhí)行所有響應(yīng)攔截器
- while (responseInterceptorChain.length) {
- promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
- }
- // 返回 Promise 對(duì)象
- return promise;
- };
可以看到由于請(qǐng)求攔截器和響應(yīng)攔截器使用了 unshift 和 push,那么 use 攔截器的先后順序就有變動(dòng)。
通過如上代碼的分析,可以得知若有多個(gè)攔截器的執(zhí)行順序規(guī)則是:
- 請(qǐng)求攔截器:先 use,后執(zhí)行
- 響應(yīng)攔截器:先 use,先執(zhí)行
關(guān)于攔截器執(zhí)行這部分,涉及到一個(gè) PR改動(dòng): Requests unexpectedly delayed due to an axios internal promise[11],推薦大家閱讀一下,有助于熟悉微任務(wù)和宏任務(wù)。
改動(dòng)的原因:如果請(qǐng)求攔截器中存在一些長(zhǎng)時(shí)間的任務(wù),會(huì)使得使用 axios 的網(wǎng)絡(luò)請(qǐng)相較于不使用 axios 的網(wǎng)絡(luò)請(qǐng)求會(huì)延后,為此,通過為攔截管理器增加 synchronous 和 runWhen 字段,來實(shí)現(xiàn)同步執(zhí)行請(qǐng)求方法。
八、取消網(wǎng)絡(luò)請(qǐng)求
在網(wǎng)絡(luò)請(qǐng)求中,會(huì)遇到許多非預(yù)期的請(qǐng)求取消,當(dāng)然也有主動(dòng)取消請(qǐng)求的時(shí)候,例如,用戶獲取 id=1 的新聞數(shù)據(jù),需要耗時(shí) 30s,用戶等不及了,就返回查看 id=2 的新聞詳情,此時(shí)我們可以在代碼中主動(dòng)取消 id=1 的網(wǎng)絡(luò)請(qǐng)求,節(jié)省網(wǎng)絡(luò)資源。
8.1 如何取消 Axios 請(qǐng)求
通過 CancleToken.source() 工廠方法創(chuàng)建取消請(qǐng)求的實(shí)例 source
在發(fā)起請(qǐng)求的 request Config 中設(shè)置 cancelToken 值為 source.token
在需要主動(dòng)取消請(qǐng)求的地方調(diào)用:source.cancle()
- const CancelToken = axios.CancelToken;
- const source = CancelToken.source();
- axios.get('/user/12345', {
- cancelToken: source.token
- }).catch(function (thrown) {
- if (axios.isCancel(thrown)) {
- console.log('Request canceled', thrown.message);
- } else {
- // handle error
- }
- });
- axios.post('/user/12345', {
- name: 'new name'
- }, {
- cancelToken: source.token
- })
- // 主動(dòng)取消請(qǐng)求 (提示信息是可選的參數(shù))
- source.cancel('Operation canceled by the user.');
同一個(gè) source 實(shí)例調(diào)用取消 cancle() 方法時(shí),會(huì)取消所有含有當(dāng)前實(shí)例 source.token 的請(qǐng)求。
8.2 取消請(qǐng)求功能的原理
想必大家也很好奇是怎么實(shí)現(xiàn)取消網(wǎng)絡(luò)請(qǐng)求功能的,實(shí)際上有了上述的基礎(chǔ),把 Axios 的請(qǐng)求想象成為一條事件執(zhí)行鏈,執(zhí)行鏈中任意一處發(fā)生了異常,都會(huì)中斷整個(gè)請(qǐng)求。
整個(gè)請(qǐng)求執(zhí)行鏈中的設(shè)計(jì)了,首先來看:axios.CancelToken.source():
- /**
- * Returns an object that contains a new `CancelToken` and a function that, when called,
- * cancels the `CancelToken`.
- */
- CancelToken.source = function source() {
- var cancel;
- var token = new CancelToken(function executor(c) {
- cancel = c;
- });
- return {
- token: token,
- cancel: cancel
- };
- };
該工廠方法返回了一個(gè)對(duì)象,該對(duì)象包含了一個(gè) token(取消令牌,CancleToken 對(duì)象的實(shí)例),以及一個(gè)取消與 token 映射綁定的取消請(qǐng)求方法 cancle()。
其中 new CancelToken() 會(huì)創(chuàng)建 CancleToken 的單例,通過傳入函數(shù)方式,拿到了取消請(qǐng)求的回調(diào)函數(shù),該函數(shù)內(nèi)會(huì)構(gòu)造 token 取消的原因,并通過執(zhí)行 resolvePromise(),主動(dòng) reslove。
同樣是一個(gè)微任務(wù),當(dāng)主動(dòng)調(diào)用 cancle() 方法后,會(huì)調(diào)用 resolvePromise(reason),此時(shí)就會(huì)給當(dāng)前 cancleToken 實(shí)例的 reason 字段賦值“請(qǐng)求取消的原因”:
- function CancelToken(executor) {
- if (typeof executor !== 'function') {
- throw new TypeError('executor must be a function.');
- }
- // 初始化一個(gè) promise 屬性,resolvePromise 變量指向 resolve
- var resolvePromise;
- this.promise = new Promise(function promiseExecutor(resolve) {
- resolvePromise = resolve;
- });
- // 賦值 token 為當(dāng)前對(duì)象的實(shí)例
- var token = this;
- // 省略...
- // 執(zhí)行外部傳入的初始化方法,將取消請(qǐng)求的方法,賦值給返回對(duì)象的 cancel 屬性
- executor(function cancel(message) {
- if (token.reason) {
- // Cancellation has already been requested
- return;
- }
- token.reason = new Cancel(message);
- resolvePromise(token.reason);
- });
- }
在 ./lib/core/dispatchRequest.js 文件中:
- function throwIfCancellationRequested(config) {
- // 當(dāng) request config 中有實(shí)例化 cancelToken 時(shí)
- // 執(zhí)行 throwIfRequested() 方法
- // throwIfRequested() 方法在 cancleToken 實(shí)例的 reason 字段有值時(shí)
- // 拋出異常
- if (config.cancelToken) {
- config.cancelToken.throwIfRequested();
- }
- // 判斷 config.signal.aborted 值為真的時(shí)候拋出異常
- // 該值時(shí)通過 new AbortController().signal,不過目前暫時(shí)未用到
- // 官方文檔上暫也暫未更新相關(guān)內(nèi)容
- if (config.signal && config.signal.aborted) {
- throw new Cancel('canceled');
- }
- }
- module.exports = function dispatchRequest(config) {
- // 準(zhǔn)備發(fā)起請(qǐng)求前檢查
- throwIfCancellationRequested(config);
- // 省略...
- var adapter = config.adapter || defaults.adapter;
- return adapter(config).then(function onAdapterResolution(response) {
- // 請(qǐng)求成功后檢查
- throwIfCancellationRequested(config);
- // 省略...
- return response;
- }, function onAdapterRejection(reason) {
- if (!isCancel(reason)) {
- // 請(qǐng)求發(fā)生錯(cuò)誤時(shí)候檢查
- throwIfCancellationRequested(config);
- // 省略...
- }
- // 省略...
- return Promise.reject(reason);
- });
- }
在文章前邊分析攔截器的時(shí)候講到了 dispatchRequest() 在請(qǐng)求攔截器之后執(zhí)行。
在請(qǐng)求前,請(qǐng)求成功、失敗后三個(gè)時(shí)機(jī)點(diǎn),都會(huì)通過 throwIfCancellationRequested() 函數(shù)檢查是否取消了請(qǐng)求,throwIfCancellationRequested() 函數(shù)判斷了 cancleToken.reason 是否有值,如果有則拋出異常并中斷請(qǐng)求 Promise 執(zhí)行鏈。
九、CSRF 防御
Axios 支持防御 CSRF(Cross-site request forgery,跨站請(qǐng)求偽造)攻擊,而防御 CSRF 攻擊的最簡(jiǎn)單方式就是加 Token。
CSRF 的攻擊可以簡(jiǎn)述為:服務(wù)器錯(cuò)把攻擊者的請(qǐng)求當(dāng)成了正常用戶的請(qǐng)求。
加一個(gè) Token 為什么就能解決吶?首先 Token 是服務(wù)端隨用戶每次請(qǐng)求動(dòng)態(tài)生成下發(fā)的,用戶在提交表單、查詢數(shù)據(jù)等行為的時(shí)候,需要在網(wǎng)絡(luò)請(qǐng)求體加上這個(gè)臨時(shí)性的 Token 值,攻擊者無法在三方網(wǎng)站中獲取當(dāng)前 Token,因此服務(wù)端就可以通過驗(yàn)證 Token 來區(qū)分是否是正常用戶的請(qǐng)求。
Axios 在請(qǐng)求配置中提供了兩個(gè)字段:
- // cookie 中攜帶的 Token 名稱,通過該名稱可以從 cookie 中拿到 Token 值
- xsrfCookieName: 'XSRF-TOKEN',
- // 請(qǐng)求 Header 中攜帶的 Token 名稱,通過該成名可從 Header 中拿到 Token 值
- xsrfHeaderName: 'X-XSRF-TOKEN',
用于附加驗(yàn)證防御 CSRF 攻擊的 Token。
十、值得一說的自定義工具庫
在 Axios 內(nèi),沒有引入其他例如 lodash 的工具函數(shù)依賴,都在自己內(nèi)部按需實(shí)現(xiàn)了工具函數(shù),提供給整個(gè)項(xiàng)目使用。
個(gè)人非常喜歡這種做法,尤其是在一個(gè) ES5 的工具庫下,這樣做不僅代碼易讀,與此同時(shí)還顯得非常得純粹、干凈、清晰!
如果團(tuán)隊(duì)內(nèi)有這種訴求,建議可以寫一個(gè) ESM 模塊的工具庫,這樣做以后,在打包 Tree Shaking 時(shí),打包的結(jié)果應(yīng)該能更加干凈。
總結(jié)
總體來說,Axios 涉及到的設(shè)計(jì)模式就有:?jiǎn)卫J健⒐S模式、職責(zé)鏈模式、適配器模式,因此絕對(duì)是值得學(xué)習(xí)的一個(gè)工具庫,梳理之后不僅利于我們靈活使用其 API,更有助于根據(jù)業(yè)務(wù)去自定義擴(kuò)展封裝網(wǎng)絡(luò)請(qǐng)求,將網(wǎng)絡(luò)請(qǐng)求統(tǒng)一收口。
與此同時(shí),Axios 絕對(duì)是一個(gè)可以作為軟件工程編碼的學(xué)習(xí)范本,其中的文件夾結(jié)構(gòu),功能設(shè)計(jì),功能解耦,按需封裝工具類,以及靈活運(yùn)用設(shè)計(jì)模式都是值得揣度回味。