Axios原始碼解析

大雄45發表於2021-09-22
導讀 宏觀的事聊完了,下面就詳細聊幾個核心細節吧:整個流程、請求/響應攔截器、dispatchRequest是個啥、請求/響應資料轉換器。
一、領悟思想

Axios是一個基於Promise的HTTP庫,根據官網介紹,有以下幾個特點:

  1. 在瀏覽器端會建立XMLHttpRequests
  1. 在Node端會建立HTTP請求
  1. 由於Axios是一個基於Promise的HTTP庫,所以其支援Promise API
  1. 支援請求和響應攔截器
  1. 支援請求和響應資料轉換
  1. 支援取消請求
  1. 自動轉換JSON資料
  1. 客戶端支援防禦XSRF攻擊

透過上述官網介紹的特點,我認為其突出的優點有三個:

  1. 支援Promise API,可以方便進行鏈式呼叫;
  1. 支援請求和響應攔截器,該攔截器將Node中中介軟體思想引入該庫,在請求傳送之前和響應接收之後可以對其進行處理。
  1. 支援資料轉換器,轉換器主要負責資料傳送前以及響應接收後對資料的處理。
二、把握設計

理解了該庫設計的特點,下面從原始碼目錄、抽象介面及核心設計原理三個層面對Axios進行整體的把握。

2.1 原始碼目錄

如下所示是Axios的原始碼目錄及各個檔案的作用

Axios原始碼解析Axios原始碼解析

2.2 抽象介面

對原始碼的目錄有了一定了解,下面利用UML類圖對該系統各個模組的依賴關係進一步瞭解,為後續原始碼分析打好基礎。(看該圖注意對著原始碼一起看)

Axios原始碼解析Axios原始碼解析

2.3 設計原理

首先看一段程式碼,這段程式碼的執行順序包含著Axios的核心原理。

axios.defaults.baseURL = '
 
// 請求攔截器一 
axios.interceptors.request.use( 
    config => { 
        console.log('請求攔截器一', config); 
        return config; 
    }, 
    error => { 
        console.log('request interceptor rejected1'); 
        return Promise.reject(error); 
    } 
); 
 
// 請求攔截器二 
axios.interceptors.request.use( 
    config => { 
        console.log('請求攔截器二', config); 
        return config; 
    }, 
    error => { 
        console.log('request interceptor rejected2'); 
        return Promise.reject(error); 
    } 
); 
 
// 響應攔截器一 
axios.interceptors.response.use( 
    response => { 
        console.log('響應攔截器一', response); 
        return response; 
    }, 
    error => { 
        console.log('response interceptor rejected1'); 
        return Promise.reject(error); 
    } 
); 
 
// 響應攔截器二 
axios.interceptors.response.use( 
    response => { 
        console.log('響應攔截器二', response); 
        return response; 
    }, 
    error => { 
        console.log('response interceptor rejected2'); 
        return Promise.reject(error); 
    } 
); 
 
axios('/', { 
    method: 'post', 
    headers: { 
        'Content-Type': 'application/json' 
    }, 
    data: { 
        test: 'test' 
    }, 
    // 請求轉換器 
    transformRequest: [(data, headers) => { 
        console.log('請求轉換器', data); 
        return JSON.stringify(data) 
    }], 
    // 響應轉換器 
    transformResponse: [(response, headers) => { 
        console.log('響應轉換器', response); 
        return response; 
    }] 
}) 
.then((response) => { 
    console.log(response.data) 
})

寫了這麼多程式碼,大家肯定對這段程式碼的執行結果很感興趣,為了滿足各位看客的好奇心,下面就直接丟擲來這段結果。

Axios原始碼解析Axios原始碼解析

不過單看執行結果也不能瞭解其核心設計原理呀,老鐵別急,其實小小程式碼就已經包含了Axios的整個執行過程,透過觀察結果及程式碼可以將整個過程簡化為下圖:

Axios原始碼解析Axios原始碼解析

其核心原理就是這個嗎?是的,你沒有看錯,這就是Axios的核心設計原理,透過一系列鏈式的處理就能夠得到所需要的結果。

三、體會細節

宏觀的事聊完了,下面就詳細聊幾個核心細節吧:整個流程、請求/響應攔截器、dispatchRequest是個啥、請求/響應資料轉換器。

3.1 整體執行流程

在第二章中闡述了該核心原理,老鐵們一定對該整體是如何運轉起來的很感興趣吧,下面就來解答各位老鐵的疑惑——Axios

function Axios(instanceConfig) { 
  this.defaults = instanceConfig; 
  // 攔截器例項化 
  this.interceptors = { 
    request: new InterceptorManager(), 
    response: new InterceptorManager() 
  }; 
} 
 
// 透過一系列的繼承繫結操作,該函式其實就是axios函式 
Axios.prototype.request = function request(config) { 
  // …… 
  config = mergeConfig(this.defaults, config); 
 
  // Set config.method 
  // …… 
 
  // ****核心**** 
  // 儲存該呼叫鏈的陣列 
  var chain = [dispatchRequest, undefined]; 
  var promise = Promise.resolve(config); 
 
  // 將請求攔截器的內容塞到陣列前面(注意用的unshift函式,這就很好的解釋了為什麼先呼叫的請求攔截器後執行) 
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { 
    chain.unshift(interceptor.fulfilled, interceptor.rejected); 
  }); 
  // 將響應攔截器的內容塞到陣列後面 
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { 
    chain.push(interceptor.fulfilled, interceptor.rejected); 
  }); 
   
  // 利用Promise將整個陣列中的內容串起來,這樣就可以按照順序鏈式執行了 
  while (chain.length) { 
    promise = promise.then(chain.shift(), chain.shift()); 
  } 
 
  return promise; 
};

是不是很巧妙?透過利用陣列先來儲存需要的內容,先處理的在陣列的前面(請求攔截器),後處理的在陣列的後面(響應攔截器),然後利用Promise將整個內容串起來,很好的處理網路請求屬於非同步的問題——Perfect。

3.2 請求/響應攔截器

透過觀察第二部分的執行結果我們已經瞭解了請求/響應攔截器,下面就做一下總結:

  1. 請求攔截器就是在傳送請求前執行的回撥函式,個人認為其最大功用就是對多個請求的配置進行統一修改
  1. 仔細觀察發現請求攔截器1先加入但是後執行,是不是與整體執行流程中的程式碼對上了。
  1. 響應攔截器就是在請求得到響應後執行的回撥函式,成功回撥的引數就是響應response,其可以對多個請求的響應進行統一修改。

先丟擲請求/響應攔截器的核心程式碼

function InterceptorManager() { 
  this.handlers = []; 
} 
 
// 註冊攔截器 
InterceptorManager.prototype.use = function use(fulfilled, rejected) { 
  this.handlers.push({ 
    fulfilled: fulfilled, 
    rejected: rejected 
  }); 
  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); 
    } 
  }); 
};

看看攔截器的核心原始碼,是不是發現與一種設計模式很像?對的,就是觀察者模式。當呼叫use方法的時候就會將回撥函式(成功、失敗)儲存至handlers屬性上,方便後期的呼叫;當呼叫eject方法的時候就會刪除對應索引位置回撥函式;當呼叫forEach方法的時候就會就會對handlers屬性(儲存的攔截器回撥)中的內容進行分發。

3.3 dispatchRequest是個啥

前面聊了整個請求的請求前(請求攔截器)和請求後(響應攔截器),是不是感覺少點東西,如何發請求,這就是我們本次要與大家一起嘮的dispatchRequest(config)。

module.exports = function dispatchRequest(config) { 
  // …… 
 
  //請求資料轉換 
  config.data = transformData( 
    config.data, 
    config.headers, 
    config.transformRequest 
  ); 
  // …… 
   
  // 獲取介面卡:自己配置了就選自己的,自己沒有設定就選預設的(瀏覽器端就選xhrAdapter、node端就選httpAdapter;這也就是為什麼Axios即支援瀏覽器又支援Node的原因) 
  var adapter = config.adapter || defaults.adapter; 
 
  return adapter(config).then(function onAdapterResolution(response) { 
    // …… 
 
    // 響應資料轉換器 
    response.data = transformData( 
      response.data, 
      response.headers, 
      config.transformResponse 
    ); 
 
    return response; 
  }, function onAdapterRejection(reason) { 
    if (!isCancel(reason)) { 
      // …… 
 
      // 響應資料轉換器 
      if (reason && reason.response) { 
        reason.response.data = transformData( 
          reason.response.data, 
          reason.response.headers, 
          config.transformResponse 
        ); 
      } 
    } 
 
    return Promise.reject(reason); 
  }); 
};

透過觀察整個請求流程中的中間環節——dispatchRequest,它一共做了三件事:

  1. 呼叫請求資料轉換器轉換請求資料
  1. 選擇合適的介面卡發起請求——自己配置了就選自己的,自己沒有配置就選預設的(瀏覽器端就選xhrAdapter、node端就選httpAdapter;這也就是為什麼Axios即支援瀏覽器又支援Node的原因)
  1. 當請求資料返回後,呼叫響應資料轉換器轉換響應資料
3.4 請求/響應資料轉換器

既然3.3中提到了請求/響應轉換器,本節就來聊一聊它倆。

// 核心原始碼 
module.exports = function transformData(data, headers, fns) { 
  utils.forEach(fns, function transform(fn) { 
    data = fn(data, headers); 
  }); 
 
  return data; 
};

請求資料轉換呼叫,實質上就是利用請求資料轉換器對請求頭和請求資料進行特定的處理(transformRequest為處理函式的陣列,defaults中包含預設的配置)

config.data = transformData( 
  config.data, 
  config.headers, 
  config.transformRequest 
);

響應資料轉換呼叫類似於請求資料轉換呼叫,對響應體進行一系列的處理(transformResponse為處理函式的陣列,defaults中包含預設的配置)

response.data = transformData( 
  response.data, 
  response.headers, 
  config.transformResponse 
);
四、結語

上述三章對Axios進行整體的分析,從Axios的特點、整體設計及關鍵環節三個方面進行了講述,透過閱讀原始碼學到了很多知識,也能夠更加熟練的使用Axios。為了保證各位老鐵的學習Axios原始碼的效果,對學習Axios原始碼的兩條建議:

  1. 邊閱讀本文邊看原始碼,能夠有更深入的理解。
  1. 不要糾結於具體的實現,從宏觀的角度去看原始碼,這樣能夠節省大量時間。

原文來自:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2793002/,如需轉載,請註明出處,否則將追究法律責任。

相關文章