源碼學習01 Axios

Axios是一個基于Promise的HTTP請求庫英妓,可以用在瀏覽器和Node.js中云头。平時在Vue項目中,經(jīng)常使用它來實現(xiàn)HTTP請求量愧。

它的使用簡便钾菊、靈活帅矗,并且有interceptors、數(shù)據(jù)轉(zhuǎn)換器等強大的功能结缚,以前用的時候并沒有仔細研究過這些功能是如何實現(xiàn)的损晤,正好在知乎的大前端專欄看到一篇文章對Axios的源碼進行了解讀。借著這篇文章的幫助红竭,我開始了自己閱讀源碼的道路尤勋。

以后要多多的讀源碼,更多的獨立完成茵宪,向大神們學習最冰。

Axios的目錄結(jié)構(gòu)

Axios的目錄結(jié)構(gòu)相對還是比較簡單的

image

目錄里面adapters/目錄下定義的是如何發(fā)出一個HTTP請求,這也就是為什么Axios技能應用在瀏覽器中(XHR)又能用在Node.js中(http.request)稀火,core/Axios.js是Axios的核心主類暖哨,axios.js是整個Axios的入口。

Axios的實現(xiàn)流程

graph TB
引入axios-->Axios構(gòu)造函數(shù)實例化
Axios構(gòu)造函數(shù)實例化-->Interceptors請求攔截器
Interceptors請求攔截器-->dispatchRequest方法
dispatchRequest方法-->請求轉(zhuǎn)換器transformRequest
請求轉(zhuǎn)換器transformRequest-->http請求適配器adapter
http請求適配器adapter-->響應轉(zhuǎn)換器transformResponse
響應轉(zhuǎn)換器transformResponse-->Interceptors響應攔截器

工具函數(shù)的學習

forEach

這個forEach與原生的數(shù)組的forEach并不相同凰狞,它可以遍歷對象篇裁,也可以遍歷數(shù)組,還可以遍歷基本值:

function forEach(obj, fn) {
  // 如果是空值就返回
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // 如果是基本類型赡若,則放到數(shù)組里面進行遍歷
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  if (isArray(obj)) {
    // 遍歷數(shù)組
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // 遍歷對象
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

mergedeepMerge

用來合并對象达布,二者的區(qū)別只是對于嵌套的深層的對象,deepMerge也會進行深層的拷貝逾冬,而不是指針的改變

function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (typeof result[key] === 'object' && typeof val === 'object') {
      result[key] = merge(result[key], val);
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

function deepMerge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (typeof result[key] === 'object' && typeof val === 'object') {
      result[key] = deepMerge(result[key], val);
    } else if (typeof val === 'object') {
      result[key] = deepMerge({}, val);
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

isStandardBrowserEnv

用來判斷是否是標準的瀏覽器環(huán)境黍聂,對于Web Workers,

typeof window -> undefined
typeof document -> undefined

對于RN和NativeScript

react-native:
navigator.product -> 'ReactNative'

nativescript
navigator.product -> 'NativeScript' or 'NS'

所以有:

/**
 * Determine if we're running in a standard browser environment
 *
 * This allows axios to run in a web worker, and react-native.
 * Both environments support XMLHttpRequest, but not fully standard globals.
 *
 * web workers:
 *  typeof window -> undefined
 *  typeof document -> undefined
 *
 * react-native:
 *  navigator.product -> 'ReactNative'
 * nativescript
 *  navigator.product -> 'NativeScript' or 'NS'
 */
function isStandardBrowserEnv() {
  if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' ||
                                           navigator.product === 'NativeScript' ||
                                           navigator.product === 'NS')) {
    return false;
  }
  return (
    typeof window !== 'undefined' &&
    typeof document !== 'undefined'
  );
}

Axios的多種使用方式

Axios有多種使用方式:

import axios from 'axios';

// 第一種 axios(option)
axios({ url, method, headers });

// 第二種 axios(url[, option]);
axios(url, { method, headers })

// 第三種(對于 get/delete 等方法) axios.[method](url[, option])
axios.get(url, { headers })

// 第四種(對于 post/put等方法)axios[.method](url[, data[, option]])
axios.post(url, data, { headers })

// 第五種 axios.request(option)
axios.request({ url, method, headers })

下面從入口文件axios.js來分析這些使用方式都是如何實現(xiàn)的

// axios.js

// 用來創(chuàng)建一個 axios 的實例
function createInstance(defaultConfig) {
  // 通過默認配置新建一個 axios 實例
  var context = new Axios(defaultConfig);
  
  // 通過 bind 方法獲取到 instance身腻,并且綁定 this 上下文
  // instance 是一個函數(shù)产还,實際上就是 Axios.prototype.request.bind(context),其上下文指向 context
  // 所以可以通過 instance(options)的方法調(diào)用
  var instance = bind(Axios.prototype.request, context);

  // 將 Axios 原型上的屬性和方法復制到 instance 上嘀趟,作為靜態(tài)屬性和靜態(tài)方法
  // Axios.prototype 上定義了 get/delete/post 等方法脐区,所以可以直接使用 instance.get 這種形式調(diào)用
  utils.extend(instance, Axios.prototype, context);

  // 將 context 實例的屬性和方法復制到 instance 上,作為靜態(tài)屬性和靜態(tài)方法
  // context.request 指向 Axios.prototype.request她按,所以可以通過instance.request 這種形式調(diào)用
  utils.extend(instance, context);

  // 返回 request 方法牛隅,它與 context 的差別僅僅在于它本身是一個函數(shù),可以直接調(diào)用
  return instance;
}

// 接受默認配置項作為參數(shù)尤溜,創(chuàng)建一個 request 方法,具有 Axios 的各種實例屬性汗唱、方法以及原型屬性和方法
// 可以認為導出的就是 Axios 的實例
var axios = createInstance(defaults);

// 暴露 Axios 類宫莱,用于繼承
axios.Axios = Axios;

// 實例上定義的 create 方法,可以創(chuàng)建新的 axios 實例
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// 暴露出 Cancel 相關(guān)的類
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// 暴露出 all 方法哩罪,實際上利用的就是 Promise.all方法
axios.all = function all(promises) {
  return Promise.all(promises);
};

// 導出一個 apply 的語法糖授霸,用來將多參數(shù)作為輸入傳入函數(shù)巡验,功能和 rest 參數(shù)相同
axios.spread = require('./helpers/spread');

// 導出實例
module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;

有個疑問,為什么createInstance函數(shù)需要繞那么大一個彎碘耳,而不是直接導出new Axios的實例呢显设?我的理解是如果直接導出new Axios是沒有辦法使用axios(option)axios(url, option)這兩種方式來實現(xiàn)調(diào)用。

createInstance最終返回的是一個函數(shù)辛辨,它指向Axios.prototype.request捕捂,并且綁定了new Axios實例作為上下文對象,同時這個導出的函數(shù)還有Axios.prototype以及new Axios實例的各個方法和屬性作為其靜態(tài)屬性和方法斗搞,這些方法的上下文都指向new Axios這同一個對象指攒。

上面的代碼解釋了除了第二種Axios的調(diào)用方法之外的調(diào)用方法,第二種調(diào)用方法是在Axios.prototype.request中對第一個參數(shù)的數(shù)據(jù)類型進行判斷來實現(xiàn)的僻焚,后面在學習Axios.prototype.request代碼時會提到允悦。

Axios

Axios.js是Axios的核心,它聲明了Axios這個類虑啤,并在原型添加了一些方法隙弛,其中最核心的就是request方法,上面提到的各種調(diào)用方法都是通過調(diào)用request方法實現(xiàn)的狞山。

首先來看Axios類的聲明

function Axios(instanceConfig) {
  // 將傳入的配置保存到實例屬性上
  this.defaults = instanceConfig;
  
  // 聲明 interceptors 屬性全闷,用來保存請求攔截器和響應攔截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios類只聲明了兩個實例屬性,攔截器屬性都是InterceptorManager的實例铣墨。InterceptorManager這個類位于core/InterceptorManager.js室埋,并不復雜,定義了一個實例屬性來存放攔截器伊约,定義了一些原型方法來對隊列中攔截器進行添加姚淆、移除和遍歷的操作

// 定義實例屬性 handlers 來存放攔截器
function InterceptorManager() {
  this.handlers = [];
}

// 添加攔截器到隊列中,接受兩個參數(shù)屡律,分別對應 Promise的 resolve 和 reject
// 返回攔截器的 ID腌逢, 用來移除它
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;
  }
};

// 用來遍歷執(zhí)行隊列中的攔截器
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

實際上Axios的實例屬性interceptors.request用來存放請求攔截器,interceptors.response用來存放響應攔截器超埋,

然后來看核心的request方法的代碼:

Axios.prototype.request = function request(config) {
  // 通過參數(shù)的類型判斷實現(xiàn) axios(url[, option]) 的調(diào)用方式
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // 合并配置項
  config = mergeConfig(this.defaults, config);
  config.method = config.method ? config.method.toLowerCase() : 'get';

  // 添加攔截器
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 將請求攔截器添加到 chain 前面
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // 將響應攔截器添加到 chain 后面
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 對 chain 進行循環(huán)
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

個人感覺上面的代碼十分巧妙搏讶,首先生命了chain這個數(shù)組,填了兩個成員dispatchRequestundefined霍殴,然后定義了一個立刻resolve的Promise對象promise媒惕,它返回的是config對象。

我們在添加請求攔截器時:

// 添加請求攔截器
const myRequestInterceptor = axios.interceptors.request.use(config => {
  // 在發(fā)送http請求之前做些什么
  return config; // 有且必須有一個config對象被返回
}, error => {
    // 對請求錯誤做些什么
    return Promise.reject(error);
});

使用了use方法来庭,將請求攔截器添加到了this.interceptors.request.handlers對列中妒蔚,然后通過forEach方法,對這個隊列進行遍歷,要注意請求攔截器使用的是unshift方法肴盏,添加到dispatchRequest前面科盛,而響應攔截器使用push方法添加到undefined后面,所以對于請求攔截器來說菜皂,先添加的攔截器會后執(zhí)行贞绵,對于響應攔截器來說,先添加的攔截器會先執(zhí)行:

axios.interceptors.request.use(fn1, fn1_1);
axios.interceptors.request.use(fn2, fn2_1);
axios.interceptors.response.use(fn3, fn3_1);
axios.interceptors.response.use(fn4, fn4_1);

按照上面的順序添加的攔截器恍飘,存儲到chain數(shù)組中是這樣的:

[fn2, fn2_1, fn1, fn1_1, dispatchRequest, undefined, fn3, fn3_1, fn4, fn4_1]

InterceptorManager.prototype.use方法接受兩個參數(shù)分別是Promise成功和失敗的回調(diào)函數(shù)榨崩,如果之傳入一個函數(shù),那么默認失敗的情況對應的就是undefined常侣。所以chain數(shù)組中是兩個成員為一組的蜡饵,分別對應一次Promise狀態(tài)改變的兩個回調(diào)函數(shù)。

然后對chain進行循環(huán):

while (chain.length) {
  promise = promise.then(chain.shift(), chain.shift());
 }

在循環(huán)過程中對promise重新復制胳施,then方法中的兩個參數(shù)分別是chain.shift()溯祸,chain.shift()的作用有二:

  1. 減小chain的長度
  2. 將剪切得到的兩個chain成員作為then方法的兩個參數(shù)執(zhí)行。

Axios規(guī)定舞肆,在使用攔截器時焦辅,請求攔截器必須返回config對象,響應攔截器必須返回response對象椿胯,這樣才能實現(xiàn)promise的鏈式調(diào)用

在請求攔截器中進行鏈式調(diào)用時筷登,將config對象作為Promise的結(jié)果進行傳遞,使得所有請求攔截器共享config對象哩盲,直到真正發(fā)出請求的dispatchRequest接收到config對象并發(fā)出請求后將接收到的response作為結(jié)果返回給后續(xù)的響應攔截器前方,并繼續(xù)傳遞。

chain數(shù)組中的undefined是作為dispatchRequest一組的then方法的第二個回調(diào)函數(shù)廉油,它的作用是兜住最后一個響應攔截器的錯誤對象惠险,不會破壞chain兩個回調(diào)函數(shù)一組的匹配順序。

Axios.js中還有一些其他的代碼抒线,主要的作用是為Axios.prototype添加了get班巩、post等方法,實際上都是調(diào)用Axios.prototype.request方法

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  // 添加到原型對象
  Axios.prototype[method] = function(url, config) {
    // 調(diào)用 Axios.prototype.request 方法
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  // 添加到原型對象
  Axios.prototype[method] = function(url, data, config) {
   // 調(diào)用 Axios.prototype.request 方法
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

dispathReqeust

上面提到的嘶炭,真正發(fā)出HTTP請求的是dispathReqeust方法抱慌,dispathReqeust主要完成了三件事:

  1. 拿到config對象,進行處理眨猎、合并抑进,傳遞給HTTP請求適配器
  2. HTTP請求適配器根據(jù)config對象發(fā)起HTTP請求
  3. 請求完成后,根據(jù)數(shù)據(jù)轉(zhuǎn)換器對得到的數(shù)據(jù)進行二次處理睡陪,返回response
// 如果取消了請求寺渗,則拋出原因
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  // 如果執(zhí)行了 cancelToken 的回調(diào)函數(shù)則終端流程夕凝, throw 出回調(diào)函數(shù)傳入的 reason
  // 是否執(zhí)行 cancelToken 的回調(diào)函數(shù)就是通過是否有 reason 來判斷的
  throwIfCancellationRequested(config);

  // 如果傳入的不是絕對地址,并且配置了 baseUrl户秤,則將 baseUrl 與 config.url 組合
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

  // 確保 config.headers 是一個對象
  config.headers = config.headers || {};

  // 如果定義了 config.transformRequest,則據(jù)此對 data 和 headers 進行處理
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // 處理 headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  // 刪除各種請求方法下的特定的header
  // 因為上面都已經(jīng)合并到 config.headers 中逮矛,所以沒用了
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // 指定 HTTP 請求適配器鸡号,一般來說默認的適配器就可以滿足需要
  // 默認的適配器會根據(jù)環(huán)境自動選擇 XHR 或者 Node 的 http.request 方法發(fā)送網(wǎng)絡請求
  // 手動指定適配器需要返回一個 Promise 對象,一般可以用來攔截請求须鼎,返回 mock 數(shù)據(jù)
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    // 請求成功
    // 再次判斷是否取消
    throwIfCancellationRequested(config);

    // 根據(jù) config.transformResponse 對響應數(shù)據(jù)和響應頭進行處理
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    // 請求失敗
    // 判斷是否是手動取消導致的失敗
    if (!isCancel(reason)) {
      // 如果早晚要取消鲸伴,不妨再次拋出取消的原因
      throwIfCancellationRequested(config);

      // 根據(jù) config.transformResponse 對響應數(shù)據(jù)和響應頭進行處理
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }
    
    // 返回一個 reject 的 Promise
    // 沒有直接 return reason,是因為只有返回 reject 的 Promise晋控,才能走到響應攔截器的第二個參數(shù)(處理異常的函數(shù))
    // 否則就會走到第一個參數(shù)(處理正常的函數(shù))
    return Promise.reject(reason);
  });
};

dispathReqeust方法返回一個Promise汞窗,攜帶著成功求情后的響應數(shù)據(jù),或者是失敗后的錯誤對象赡译。用戶就可以在調(diào)用axios()方法后的then或者catch中進行業(yè)務處理了仲吏。

Adapter

上面代碼的注釋里面提到了,在dispatchRequest中通過config.adapter或者defaults.adapter指定HTTP請求適配器蝌焚,一般來說默認的適配器就可以滿足需要裹唆,默認的適配器會根據(jù)環(huán)境自動選擇XHR或者Node的http.request方法發(fā)送網(wǎng)絡請求

defaults.js中的adatper方法完成的就是根據(jù)環(huán)境選擇HTTP適配器

function getDefaultAdapter() {
  var adapter;
  // 通過判斷 process 是否存在以及類型(toString)來判斷是否是 Node 環(huán)境
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // Node 環(huán)境使用 http 適配器
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // 瀏覽器環(huán)境使用 XHR 
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

Axios是基于Promise的,而HTTP請求的實現(xiàn)又是通過傳統(tǒng)的Ajax實現(xiàn)的只洒,所以adapter/xhr.js的主要功能就是面試時經(jīng)常遇到的一道題许帐,將Ajax改為Promise的形式。來學習一下Axios是如何實現(xiàn)的毕谴。

// xhr.js

module.exports = function xhrAdapter(config) {
  // 返回一個 Promise 對象
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;
    
    // 如果 data 是 FromData 成畦,則刪除 Content-Type 的 Header
    if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }
    
    // 新建 xhr 對象
    var request = new XMLHttpRequest();

    // HTTP 認證信息
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password || '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }
    
    // 新建一個 AJAX 連接,注意 HTTP 方法都應該是大寫的
    // 請求的 url 是通過 buildURL 方法創(chuàng)建的涝开,它會將查詢參數(shù)進行序列化并安全編碼循帐,附在 url 后
    request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);

    // 設定超時時間,單位是毫秒
    request.timeout = config.timeout;

    // 監(jiān)聽 ready state 的改變
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }

      // 如果請求出錯忠寻,并且沒有收到響應惧浴,這種情況會被 onerror 時間捕獲到
      // 只有一種例外情況:請求使用的是 file 協(xié)議,大多數(shù)瀏覽器會返回的 status 為 0
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }

      // getAllResponseHeaders 方法可以返回所有的響應頭
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      //  提取響應結(jié)果
      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, // 將 config 對象傳遞給 response 對象
        request: request
      };
      
      // 根據(jù) response.status 來 resolve 或者 reject 當前的 Promise
      settle(resolve, reject, response);

      // 重置 request
      request = null;
    };

    // 處理瀏覽器的終止事件奕剃,返回 reject 的 Promise
    // 當一個請求被終止時衷旅,request 的 readyState 屬性被置為 0
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }
      
      // 返回 reject 的 Promise,內(nèi)容是定制的 Error 對象
      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      request = null;
    };

    // 處理網(wǎng)絡錯誤導致的資源加載失敗
    request.onerror = function handleError() {
      // 真正的錯誤被瀏覽器攔截了纵朋,只有當網(wǎng)絡錯誤時才會觸發(fā) onerror 事件
      reject(createError('Network Error', config, null, request));

      // Clean up request
      request = null;
    };

    // 處理超時的情況
    request.ontimeout = function handleTimeout() {
      reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
    };

    // 添加 xsrf 的請求頭柿顶,這只適用于在瀏覽器環(huán)境,不適用于 Web Worker 和 RN 無效
    if (utils.isStandardBrowserEnv()) {
      var cookies = require('./../helpers/cookies');

      var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    // 使用 setRequestHeader 方法來添加請求頭
    // 此方法必須在 open() 和 send() 之間調(diào)用操软。
    // 如果多次對同一個請求頭賦值嘁锯,只會生成一個合并了多個值的請求頭。
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // 如果沒有 data  屬性,移除 Content-Type 屬性
          delete requestHeaders[key];
        } else {
          request.setRequestHeader(key, val);
        }
      });
    }

    // 添加 withCredentials 屬性
    if (config.withCredentials) {
      request.withCredentials = true;
    }

    // 添加 request.responseType 
    // 需要確保服務器所返回的類型和所設置的返回值類型是兼容的家乘。
    // 如果兩者類型不兼容蝗羊,服務器返回的數(shù)據(jù)變成了null,即使服務器返回了數(shù)據(jù)仁锯。
    if (config.responseType) {
      try {
        request.responseType = config.responseType;
      } catch (e) {
        if (config.responseType !== 'json') {
          throw e;
        }
      }
    }

    // Handle progress if needed
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Not all browsers support upload events
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }
    
    // 處理 cancelToken耀找,后面會單獨介紹
    if (config.cancelToken) {
      // 也是一個 Promise 對象
      config.cancelToken.promise.then(function onCanceled(cancel) {
        // 如果 cancel 時 request 已經(jīng)完成,那就返回
        if (!request) {
          return;
        }
        
        // 使用了 abort 事件
        request.abort();
        reject(cancel);
        
        request = null;
      });
    }
    
    // 根據(jù)標準业崖,如果是 GET 或者 HEAD 請求野芒,應將請求主體設置為 null
    if (requestData === undefined) {
      requestData = null;
    }

    // 發(fā)送請求
    request.send(requestData);
  });
};

xhrAdapter的XHR發(fā)送請求成功后會執(zhí)行Promise對象的resolve方法,將請求的數(shù)據(jù)傳遞出去双炕,如果請求失斈(超時、網(wǎng)絡出錯妇斤、終止)則執(zhí)行reject方法摇锋,并將自定義的錯誤信息傳遞出去。

Settle

xhrAdapter中將改變Promise狀態(tài)的功能抽離成為單獨的settle方法

// settle.js

module.exports = function settle(resolve, reject, response) {
  // 獲得配置的 validateStatus 方法
  var validateStatus = response.config.validateStatus;
  
  // 如果不存在 validateStatus 或者驗證通過站超,resolve
  if (!validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(createError(
      'Request failed with status code ' + response.status,
      response.config,
      null,
      response.request,
      response
    ));
  }
};

validateStatus接受response.stastus作為參數(shù)乱投,對于給定的HTTP狀態(tài)碼確定其成功失敗狀態(tài),比如:

validateStatus: function (status) {
  return status >= 200 && status < 300; // 默認的
}

數(shù)據(jù)轉(zhuǎn)換器

前面也提到了數(shù)據(jù)轉(zhuǎn)換器顷编,可以對請求對象和響應和數(shù)據(jù)進行轉(zhuǎn)換腿宰,可以全局使用:

// 往現(xiàn)有的請求轉(zhuǎn)換器里增加轉(zhuǎn)換方法
axios.defaults.transformRequest.push((data, headers) => {
  // ...處理data
  return data;
});

// 往現(xiàn)有的響應轉(zhuǎn)換器里增加轉(zhuǎn)換方法
axios.defaults.transformResponse.push((data, headers) => {
  // ...處理data
  return data;
});

也可以在單次請求中使用:

// 往已經(jīng)存在的轉(zhuǎn)換器里增加轉(zhuǎn)換方法
axios.get(url, {
  // ...
  transformRequest: [
    ...axios.defaults.transformRequest, // 去掉這行代碼就等于重寫請求轉(zhuǎn)換器了
    (data, headers) => {
      // ...處理data
      return data;
    }
  ],
  transformResponse: [
    ...axios.defaults.transformResponse, // 去掉這行代碼就等于重寫響應轉(zhuǎn)換器了
    (data, headers) => {
      // ...處理data
      return data;
    }
  ],
})

defaults配置項中已經(jīng)默認自定義了一個請求轉(zhuǎn)換器和響應轉(zhuǎn)換器

// 請求轉(zhuǎn)換器
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;
  }
],

// 響應轉(zhuǎn)換器
transformResponse: [
  function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }
],

默認的請求轉(zhuǎn)換器對請求數(shù)據(jù)和請求頭進行標準化處理扬跋,默認的響應轉(zhuǎn)換器用來自動將字符串解析為JSON對象哮洽。

使用的時候是通過transformData這個方法纹磺,對數(shù)組中的轉(zhuǎn)換器進行遍歷:

// transformData.js
module.exports = function transformData(data, headers, fns) {
  /*eslint no-param-reassign:0*/
  utils.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });

  return data;
};

轉(zhuǎn)換器和攔截器都可以對請求和響應的數(shù)據(jù)進行攔截處理,但是一般情況下钮惠,攔截器主要負責攔截修改config配置項茅糜,數(shù)據(jù)轉(zhuǎn)換器主要用來負責攔截轉(zhuǎn)換請求主體和響應數(shù)據(jù)。

Cancel

Axios提供了取消請求的功能素挽,有兩種使用方法:

// 第一種取消方法
axios.get(url, {
  cancelToken: new axios.CancelToken(cancel => {
    if (/* 取消條件 */) {
      cancel('取消日志');
    }
  })
});

// 第二種取消方法
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get(url, {
  cancelToken: source.token
});
source.cancel('取消日志');

Axios用了三個模塊來實現(xiàn)這個功能蔑赘,首先是Cancel這個類:

// ./cancel/Cancel.js
function Cancel(message) {
  this.message = message;
}

Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;

module.exports = Cancel;

主要是定義了Cancelmessage實例屬性,和原型上的內(nèi)部用的__CANCEL__屬性预明,還定義了一個toString方法

isCancel返回布爾值缩赛,根據(jù)是否傳入了value以及是否有__CANCEL__屬性,判斷是否是Cancel實例

核心的代碼在CancelToken.js中:

// ./cancel/CancelToken.js

function CancelToken(executor) {
  // executor 必須是函數(shù)
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  // 通過閉包撰糠,將 this.promise 的控制權(quán)導出到 resolvePromise 變量上酥馍,此時 promise 狀態(tài)為 pending
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  // 傳入一個函數(shù)作為 executor 的參數(shù),將 promise 狀態(tài)的控制權(quán)導出到 executor 函數(shù)中
  // 當 executor 的參數(shù)執(zhí)行時阅酪,為 this.reason 賦值旨袒,并且改變 this.promise 狀態(tài)為 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);
  });
}

// 由于 this.reason 是在 exxcutor 的方法執(zhí)行后才賦值的汁针,所以據(jù)此判斷是否已經(jīng)執(zhí)行了取消操作
// 如果已經(jīng)執(zhí)行取消操作,則拋出 this.reason
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

// 對應第二種使用方法砚尽,相當于默認構(gòu)建了一個 executor 函數(shù)
// 導出 token 傳入 config施无,導出 cancel 函數(shù)來執(zhí)行取消操作
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

總結(jié)

閱讀Axios的過程,還是學到了很多東西:

  1. Promise的串聯(lián)操作
  2. 攔截器的添加和執(zhí)行原理
  3. 將Promise的控制權(quán)導出必孤,讓外界決定Promise的狀態(tài)

還有很繁瑣也很重要的一部分沒有涉及帆精,就是針對HTTP請求的標準化處理,比如Heder的處理等隧魄,這也是大大方便開發(fā)者的功能之一,我們不用再擔心這些細節(jié)的處理隘蝎,只需要關(guān)注核心邏輯的實現(xiàn)购啄。這也是優(yōu)秀的組件和庫的標準之一,暴露出簡單嘱么、直接的接口讓使用者調(diào)用狮含,復雜、瑣碎的邏輯隱藏在內(nèi)部曼振。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末几迄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子冰评,更是在濱河造成了極大的恐慌映胁,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甲雅,死亡現(xiàn)場離奇詭異解孙,居然都是意外死亡,警方通過查閱死者的電腦和手機抛人,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門弛姜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人妖枚,你說我怎么就攤上這事廷臼。” “怎么了绝页?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵荠商,是天一觀的道長。 經(jīng)常有香客問我续誉,道長结啼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任屈芜,我火速辦了婚禮郊愧,結(jié)果婚禮上朴译,老公的妹妹穿的比我還像新娘。我一直安慰自己属铁,他們只是感情好眠寿,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著焦蘑,像睡著了一般盯拱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上例嘱,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天狡逢,我揣著相機與錄音,去河邊找鬼拼卵。 笑死奢浑,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的腋腮。 我是一名探鬼主播雀彼,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼即寡!你這毒婦竟也來了徊哑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤聪富,失蹤者是張志新(化名)和其女友劉穎莺丑,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體墩蔓,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡窒盐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了钢拧。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蟹漓。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖源内,靈堂內(nèi)的尸體忽然破棺而出葡粒,到底是詐尸還是另有隱情,我是刑警寧澤膜钓,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布嗽交,位于F島的核電站,受9級特大地震影響颂斜,放射性物質(zhì)發(fā)生泄漏夫壁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一沃疮、第九天 我趴在偏房一處隱蔽的房頂上張望盒让。 院中可真熱鬧梅肤,春花似錦、人聲如沸邑茄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肺缕。三九已至左医,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間同木,已是汗流浹背浮梢。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留彤路,地道東北人秕硝。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像斩萌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子屏轰,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

推薦閱讀更多精彩內(nèi)容

  • Axios是近幾年非臣绽桑火的HTTP請求庫,官網(wǎng)上介紹Axios 是一個基于 promise 的 HTTP 庫霎苗,可以...
    milletmi閱讀 3,498評論 0 9
  • 概述 在前端開發(fā)過程中姆吭,我們經(jīng)常會遇到需要發(fā)送異步請求的情況。而使用一個功能齊全唁盏,接口完善的HTTP請求庫内狸,能夠在...
    grain先森閱讀 1,569評論 0 4
  • Vue項目越做越多,Axios一直作為請求發(fā)送的基礎工程厘擂,這里就深究一下Axios的攔截器相關(guān)的一些邏輯和對應一個...
    RandyZhang閱讀 32,443評論 9 31
  • axios 基于 Promise 的 HTTP 請求客戶端昆淡,可同時在瀏覽器和 node.js 中使用 功能特性 在...
    Yanghc閱讀 3,640評論 0 7
  • 一、安裝 1刽严、 利用npm安裝npm install axios --save 2昂灵、 利用bower安裝bower...
    kiddings閱讀 1,755評論 0 3