剛出鍋的 Axios 網(wǎng)絡請求源碼閱讀筆記

項目中一直都有用到 Axios 作為網(wǎng)絡請求工具,用它更要懂它,因此為了更好地發(fā)揮 Axios 在項目的價值,以及日后能夠得心應手地使用它,筆者決定從源碼層面好好欣賞一下它的美貌!

image

Axios是一款基于 Promise 并可用于瀏覽器和 Node.js 的網(wǎng)絡請求庫。

最近,Axios 官方文檔終于變好看了,支持多語言切換,閱讀更清晰,使用起來也更加舒適!作為一款受全球歡迎的網(wǎng)絡請求庫,有必要偷學一下其中的架構設計編碼方式。

本篇文章從源碼層面主要分析 Axios 的功能實現(xiàn)、設計模式、以及分享 Axios 中一些筆者認為比較“精彩”的地方!

本文主要內(nèi)容結構如下,大家按需食用

image

一、Axios 項目概況

本次分析的 Axios 版本是:v0.24.0

通過簡單的瀏覽 package.json、文件及目錄,可以得知 axios 工程采用了如下三方依賴:

名稱 說明
Grunt JavaScript 任務運行器
dtslint TypeScript 類型聲明&樣式校驗工具
TypeScript 支持TS環(huán)境下開發(fā)
Webpack JavaScript 模塊打包工具
karma 測試用例檢查器
mocha 多功能的 JavaScript 測試框架
sinojs 提供spies, stub, mock,推薦文章《Sinon 入門,看這篇文章就夠了
follow-redirects http(s)重定向,NodeJS模塊

這里省略了對一些工具介紹,但可以發(fā)現(xiàn),Axios 開發(fā)項目的主功能依賴并不多,換句話說是只有 follow-redirects作為了“使用依賴”,其他都是編譯、測試、框架層面的東西,可以看出官方團隊在對于 Axios 有多么注質(zhì)量和穩(wěn)定性,畢竟是全球都在用的工具。

Axios 中相關代碼都在 lib/ 目錄下(建議逐行閱讀):

.
├── adapters  // 網(wǎ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)絡請求的處理
│   ├── Cancel.js  // 取消請求
│   ├── CancelToken.js  // 取消 Token
│   └── isCancel.js  // 判斷是否取消請求的函數(shù)方法
├── core  // 核心功能
│   ├── Axios.js  // Axios 對象
│   ├── InterceptorManager.js  // 攔截器管理
│   ├── README.md
│   ├── buildFullPath.js  // 構造完成的請求 URL
│   ├── createError.js  // 創(chuàng)建錯誤,拋出異常
│   ├── dispatchRequest.js  // 請求分發(fā),用于區(qū)分調(diào)用 http 還是 xhr
│   ├── enhanceError.js
│   ├── mergeConfig.js  // 合并配置參數(shù)
│   ├── settle.js  // 根據(jù)請求響應狀態(tài),改變 Promise 狀態(tài)
│   └── transformData.js  // 數(shù)據(jù)格式轉(zhuǎn)換
├── env  // 無關緊要,沒啥用,與發(fā)包版本有關
│   ├── README.md
│   └── data.js
├── defaults.js  // 默認參數(shù)/初始化參數(shù)配置
├── utils.js  // 提供簡單的通用的工具函數(shù)
└── axios.js  // 入口文件,初始化并導出 axios 對象

有了一個簡單的代碼功能組織架構熟悉后,對于串聯(lián) Axios 的功能很有好處,另外,從上述文件和文件夾的命名,很容易讓人意識到這是一個什么功能的文件。

高內(nèi)聚、低耦合”的真言,在 Axios 中應該算是一個運用得很好的例子。

二、Axios 網(wǎng)絡請求流程圖

梳理了一張 Axios 發(fā)起請求、響應請求的執(zhí)行流程圖,希望可以給大家一個完整流程的概念,便于理解后續(xù)的源碼分析。

Axios 網(wǎng)絡請求流程圖

三、Axios API 設計

我們在使用 Axios 的時候,會覺得 Axios 的使用特別方便,其原因就是 Axios 中針對同一功能實現(xiàn)了不同的 API,便于大家在各種場景下的變通擴展使用。

例如,發(fā)起一個 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])

我們知道一個網(wǎng)絡請求的方式會有 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
    }));
  };
});

能夠如上的直接循環(huán)列表賦值,得益于 Axios 將核心的請求功能單獨放到了 Axios.prototype.request 方法中,該方法的 TS 定義為:

Axios.request(config: any, ...args: any[]): any

在其方法(Axios.request())內(nèi)會對外部傳參數(shù)類型做判斷,并選擇組裝正確的請求參數(shù):

// 生成規(guī)范的 config,抹平 API(函數(shù)入?yún)ⅲ┎町?if (typeof config === 'string') {
  // 處理了第一個參數(shù)是 url 字符串的情況 request(url[, config])
  config = arguments[1] || {};
  config.url = arguments[0];
} else {
  config = config || {};
}

// 合并默認配置
config = mergeConfig(this.defaults, config);

// 將請求方法轉(zhuǎ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';
}

以此來抹平了各種類型請求以及所需傳入?yún)?shù)之間的差異性!

四、Axios 工廠模式創(chuàng)建實例

默認 Axios 導出了一個單例,導出了一個實例化后的單例,所以我們可以直接引入后就可以調(diào)用 Axios 的方法。

在某些場景下,我們的項目中可能對接了多個業(yè)務方,那么請求中的 base URL 就不一樣,因此有沒有辦法創(chuàng)建多個 Axios 實例?

那就是使用 axios.create([config]) 方法創(chuàng)建多個實例。

考慮到多實例這樣的實際需求,Axios 對外暴露了 create() 方法,在 Axios 內(nèi)部中,往導出的 axios 實例上綁定了用于創(chuàng)建本身實例的工廠方法:

/**
 * 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;
}

這里的實現(xiàn)值得一說的地方在于:

instance.create = function create(instanceConfig) {
  return createInstance(mergeConfig(defaultConfig, instanceConfig));
};

在創(chuàng)建 axios 實例的工廠方法內(nèi),綁定工廠方法到實例的 create 屬性上。為什么不是在工廠方法外綁定吶?這是我們可能的習慣做法,Axios 之前確實也是這么做的。

為什么挪到了內(nèi)部?可以看看這條 PR: Allow axios.create(options) to be used recursively

原因簡單來說就是,用戶自己創(chuàng)建的實例依然可以調(diào)用 create 方法創(chuàng)建新的實例,例如:

const axios = require('axios');

const jsonClient = axios.create({
  responseType: 'json' // 該項配置可以在后續(xù)創(chuàng)建的實例中復用,而不必重復編碼
});

const serviceOne = jsonClient.create({
  baseURL: 'https://service.one/'
});

const serviceTwo = jsonClient.create({
  baseURL: 'https://service.two/'
});

這樣有助于復用實例的公共參數(shù),減少重復編碼。

五、網(wǎng)絡請求適配器

在文件 ./defaults.js 中生成了默認完整的 Request Config 參數(shù)。

其中 config.adapter 字段表明當前應該使用 ./adapters/目錄下的 http.js 還是 xhr.js 模塊

// 根據(jù)當前使用環(huán)境,選擇使用的網(wǎ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;
}

這里使用了設計模式中的適配器模式,通過判斷不同環(huán)境下是否支持方法的方式,選擇正確的網(wǎng)絡請求模塊,便可以實現(xiàn)官網(wǎng)所說的支持 NodeJS 和瀏覽器環(huán)境。

六、轉(zhuǎn)換請求體和響應體數(shù)據(jù)

這是 Axios 貼在官網(wǎng)的核心功能之一,且提到了可以自動轉(zhuǎn)換響應體內(nèi)容為 JSON 數(shù)據(jù)

默認請求配置中初始化的請求/響應轉(zhuǎn)換器數(shù)組
自動嘗試轉(zhuǎn)換響應數(shù)據(jù)為 JSON 格式

transformRequesttransformResponse 字段是一個數(shù)組類型,因此我們還可以向其中增加自定義的轉(zhuǎn)換器。

一般來講我們只會通過復寫 transitional 字段來控制響應數(shù)據(jù)的轉(zhuǎn)換與否,但可以作為擴展 Axios 的一個點,留了口子,這一點考慮得也很到位。

七、請求攔截器&響應攔截器

可以通過攔截器來提前處理請求前和收到響應前的一些處理方法。

7.1 攔截器的使用

攔截器用于在 .then().catch() 前注入并執(zhí)行的一些方法。

// 通過 use 方法,添加一個請求攔截器
axios.interceptors.request.use(function (config) {
    // 在發(fā)送請求前干點啥,.then() 處理之前,比如修改 request config
    return config;
  }, function (error) {
    // 在發(fā)起請求發(fā)生錯誤后,.catch() 處理之前干點啥
    return Promise.reject(error);
  });

// 通過 use 方法,添加一個響應攔截器
axios.interceptors.response.use(function (response) {
    // 只要響應網(wǎng)絡狀態(tài)碼是 2xx 的都會觸發(fā)
    // 干點啥
    return response;
  }, function (error) {
    // 狀態(tài)碼不是 2xx 的會觸發(fā)
    // 發(fā)生錯誤了,干點啥
    return Promise.reject(error);
  });

7.2 攔截管理器

Axios 將請求和響應的過程包裝成了 Promise,那么 Axios 是如何實現(xiàn)攔截器在 .then().catch() 執(zhí)行前執(zhí)行吶?

可以很容易猜到通過組裝一條 Promise 執(zhí)行鏈即可!

來看看 Axios 在請求函數(shù)中如何實現(xiàn):

首先是 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` 執(zhí)行 `then` 的函數(shù)方法
 * @param {Function} rejected 處理 `Promise` 執(zhí)行 `reject` 的函數(shù)方法
 *
 * @return {Number} 返回一個 ID 值用于移除攔截器
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    // 默認不同步
    synchronous: options ? options.synchronous : false,
    // 定義是否執(zhí)行當前攔截器的函數(shù)或布爾值
    runWhen: options ? options.runWhen : null 
  });
  return this.handlers.length - 1; // ID 值實際就是當前攔截器的數(shù)組索引
};

/**
 * 從棧中移除指定 id 的攔截器
 *
 * @param {Number} id use 方法返回的 id 值
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null; // 刪除攔截器,但索引會保留
  }
};

/**
 * 迭代所有注冊的攔截器
 * 該方法會跳過因攔截器被刪除而值為 null 的索引
 *
 * @param {Function} 調(diào)用每個有效攔截器的函數(shù)
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

迭代所有注冊的攔截器是一個 FIFS(first come first served,先到先服務)隊列執(zhí)行順序的方法。

7.3 組裝攔截器與請求執(zhí)行鏈

./lib/core/Axios.js 文件中,Axios 對象定義了 request 方法,其中將網(wǎng)絡請求、請求攔截器和響應攔截器組裝。

默認返回一個還未執(zhí)行網(wǎng)絡請求的 Promise 執(zhí)行鏈,如果設置了同步,則會立即執(zhí)行請求過程,并返回請求結果的 Promise 對象,也就是官方文檔中提到的 Axios 還支持 Promise API。

函數(shù)詳細的分析,都已經(jīng)注釋在如下代碼中:

/**
 * Dispatch a request
 *
 * @param {Object} config 傳入的用戶自定義配置,并和默認配置 merge
 */
Axios.prototype.request = function request(config) {
  // 省略 ...

  // 請求攔截器執(zhí)行鏈
  var requestInterceptorChain = [];
  // 同步請求攔截器
  var synchronousRequestInterceptors = true;
  // 遍歷請求攔截器
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 判斷 runWhen 如果是函數(shù),則執(zhí)行函數(shù),結果若為 false,則不執(zhí)行當前攔截器
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }
    // 判斷當前攔截器是否同步
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    // 插入 requestInterceptorChain 數(shù)組首位
    // 效果:[interceptor.fulfilled, interceptor.rejected, ...]
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // 響應攔截器執(zhí)行鏈
  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 都沒有傳遞第三個配置參數(shù)
  // 所以一般情況下會走這個邏輯
  if (!synchronousRequestInterceptors) {
    var chain = [dispatchRequest, undefined];
    // 將請求攔截器執(zhí)行鏈放到 chain 數(shù)組頭部
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    // 將響應攔截器執(zhí)行鏈放到 chain 數(shù)組末尾
    chain = chain.concat(responseInterceptorChain);
    // 給 promise 賦值 Promise 對象,并注入 request config
    promise = Promise.resolve(config);
    // 循環(huán) chain 數(shù)組,組合成 Promise 執(zhí)行鏈
    while (chain.length) {
      // 正好 resolve 和 reject 對應方法,兩兩一組
      promise = promise.then(chain.shift(), chain.shift());
    }
    // 返回 Promise 執(zhí)行鏈
    return promise;
  }

  // 同步方式
  var newConfig = config;
  // 循環(huán)并執(zhí)行所有請求攔截器
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    try {
      // 執(zhí)行定義請求前的“請求攔截器” then 處理方法
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      // 執(zhí)行定義請求前的“請求攔截器” catch 處理方法
      onRejected(error);
      break;
    }
  }

  try {
    // 執(zhí)行網(wǎng)絡請求
    promise = dispatchRequest(newConfig);
  } catch (error) {
    return Promise.reject(error);
  }

  // 循環(huán)并執(zhí)行所有響應攔截器
  while (responseInterceptorChain.length) {
    promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
  }
  // 返回 Promise 對象
  return promise;
};

可以看到由于請求攔截器和響應攔截器使用了 unshiftpush,那么 use 攔截器的先后順序就有變動。

通過如上代碼的分析,可以得知若有多個攔截器的執(zhí)行順序規(guī)則是:

  • 請求攔截器:先 use,后執(zhí)行
  • 響應攔截器:先 use,先執(zhí)行

關于攔截器執(zhí)行這部分,涉及到一個 PR改動: Requests unexpectedly delayed due to an axios internal promise,推薦大家閱讀一下,有助于熟悉微任務宏任務。

改動的原因:如果請求攔截器中存在一些長時間的任務,會使得使用 axios 的網(wǎng)絡請相較于不使用 axios 的網(wǎng)絡請求會延后,為此,通過為攔截管理器增加 synchronousrunWhen 字段,來實現(xiàn)同步執(zhí)行請求方法。

八、取消網(wǎng)絡請求

在網(wǎng)絡請求中,會遇到許多非預期的請求取消,當然也有主動取消請求的時候,例如,用戶獲取 id=1 的新聞數(shù)據(jù),需要耗時 30s,用戶等不及了,就返回查看 id=2 的新聞詳情,此時我們可以在代碼中主動取消 id=1 的網(wǎng)絡請求,節(jié)省網(wǎng)絡資源。

8.1 如何取消 Axios 請求

通過 CancleToken.source() 工廠方法創(chuàng)建取消請求的實例 source

在發(fā)起請求的 request Config 中設置 cancelToken 值為 source.token

在需要主動取消請求的地方調(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
})

// 主動取消請求 (提示信息是可選的參數(shù))
source.cancel('Operation canceled by the user.');

同一個 source 實例調(diào)用取消 cancle() 方法時,會取消所有含有當前實例 source.token 的請求

8.2 取消請求功能的原理

想必大家也很好奇是怎么實現(xiàn)取消網(wǎng)絡請求功能的,實際上有了上述的基礎,把 Axios 的請求想象成為一條事件執(zhí)行鏈,執(zhí)行鏈中任意一處發(fā)生了異常,都會中斷整個請求。

整個請求執(zhí)行鏈中的設計了,首先來看: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() 會創(chuàng)建 CancleToken 的單例,通過傳入函數(shù)方式,拿到了取消請求的回調(diào)函數(shù),該函數(shù)內(nèi)會構造 token 取消的原因,并通過執(zhí)行 resolvePromise(),主動 reslove。

同樣是一個微任務,當主動調(diào)用 cancle() 方法后,會調(diào)用 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;

  // 省略...

  // 執(zhí)行外部傳入的初始化方法,將取消請求的方法,賦值給返回對象的 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 時
  // 執(zhí)行 throwIfRequested() 方法
  // throwIfRequested() 方法在 cancleToken 實例的 reason 字段有值時
  // 拋出異常
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
  // 判斷 config.signal.aborted 值為真的時候拋出異常
  // 該值時通過 new AbortController().signal,不過目前暫時未用到
  // 官方文檔上暫也暫未更新相關內(nèi)容
  if (config.signal && config.signal.aborted) {
    throw new Cancel('canceled');
  }
}

module.exports = function dispatchRequest(config) {
  // 準備發(fā)起請求前檢查
  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)) {
      // 請求發(fā)生錯誤時候檢查
      throwIfCancellationRequested(config);
      // 省略...
    }
    // 省略...

    return Promise.reject(reason);
  });
}

在文章前邊分析攔截器的時候講到了 dispatchRequest() 在請求攔截器之后執(zhí)行。

在請求前,請求成功、失敗后三個時機點,都會通過 throwIfCancellationRequested() 函數(shù)檢查是否取消了請求,throwIfCancellationRequested() 函數(shù)判斷了 cancleToken.reason 是否有值,如果有則拋出異常并中斷請求 Promise 執(zhí)行鏈。

九、CSRF 防御

Axios 支持防御 CSRF(Cross-site request forgery,跨站請求偽造)攻擊,而防御 CSRF 攻擊的最簡單方式就是加 Token。

CSRF 的攻擊可以簡述為:服務器錯把攻擊者的請求當成了正常用戶的請求。

加一個 Token 為什么就能解決吶?首先 Token 是服務端隨用戶每次請求動態(tài)生成下發(fā)的,用戶在提交表單、查詢數(shù)據(jù)等行為的時候,需要在網(wǎng)絡請求體加上這個臨時性的 Token 值,攻擊者無法在三方網(wǎng)站中獲取當前 Token,因此服務端就可以通過驗證 Token 來區(qū)分是否是正常用戶的請求。

Axios 在請求配置中提供了兩個字段:

// cookie 中攜帶的 Token 名稱,通過該名稱可以從 cookie 中拿到 Token 值
xsrfCookieName: 'XSRF-TOKEN',
// 請求 Header 中攜帶的 Token 名稱,通過該成名可從 Header 中拿到 Token 值
xsrfHeaderName: 'X-XSRF-TOKEN',

用于附加驗證防御 CSRF 攻擊的 Token。

十、值得一說的自定義工具庫

在 Axios 內(nèi),沒有引入其他例如 lodash 的工具函數(shù)依賴,都在自己內(nèi)部按需實現(xiàn)了工具函數(shù),提供給整個項目使用。

個人非常喜歡這種做法,尤其是在一個 ES5 的工具庫下,這樣做不僅代碼易讀,與此同時還顯得非常得純粹、干凈、清晰!

如果團隊內(nèi)有這種訴求,建議可以寫一個 ESM 模塊的工具庫,這樣做以后,在打包 Tree Shaking 時,打包的結果應該能更加干凈。

總結

總體來說,Axios 涉及到的設計模式就有:單例模式、工廠模式、職責鏈模式、適配器模式,因此絕對是值得學習的一個工具庫,梳理之后不僅利于我們靈活使用其 API,更有助于根據(jù)業(yè)務去自定義擴展封裝網(wǎng)絡請求,將網(wǎng)絡請求統(tǒng)一收口。

與此同時,Axios 絕對是一個可以作為軟件工程編碼的學習范本,其中的文件夾結構,功能設計,功能解耦,按需封裝工具類,以及靈活運用設計模式都是值得揣度回味。

相信能看到文末的你,一定收獲不小,不妨動動小手,點個關注?

image
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容