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