Axios原始碼分析

格砸發表於2019-01-13

目錄結構

│
├ lib/
│ ├ adapters/
│ ├ cancel/
│ ├ core/
│ ├ helpers/
│ ├ axios.js
│ ├ defaults.js     // 預設配置
│ └ utils.js
│
├ index.js // 入口

複製程式碼

從入口開始

從入口我們可以知道axios提供了些什麼

1. 從webpack.config得知入口檔案是index.js

// index.js

module.exports = require('./lib/axios');
複製程式碼

從這裡我們知道庫的入口檔案是lib/axios.js

2. lib/axios.js匯出了什麼

// lib/axios.js

module.exports = axios;

module.exports.default = axios;

複製程式碼

匯出了 "axios" 這個物件,並且還有相容import寫法

3. 匯出的axios是啥

var defaults = require('./defaults');

function createInstance(defaultConfig) {
  // ...
}

var axios = createInstance(defaults);
複製程式碼

這裡可以看出axios是一個例項,並且使用了預設引數。

4. 匯出的axios提供了啥

a. axios.Axios

// lib/axios.js
axios.Axios = Axios;
複製程式碼

這裡的Axios是該例項的類

b. axios.create

// lib/axios.js
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
複製程式碼

axios還提供了給使用者自己建立例項的方法,並且使用者傳入的config能覆蓋預設config(mergeConfig的邏輯)

c. axios.Cancelaxios.CancelTokenaxios.isCancel

// lib/axios.js
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
複製程式碼

axios提供了取消請求的方法

d. axios.all

// lib/axios.js
axios.all = function all(promises) {
  return Promise.all(promises);
};
複製程式碼

Promise.all的封裝

e. axios.spread

// lib/axios.js
axios.spread = require('./helpers/spread');
複製程式碼

引數解構的封裝,類似es6陣列的...操作符

Axios類

從入口來看,我們發現了重點方法axios.create,並且知道它是用方法createInstance建立例項的。

// lib/axios.js

var Axios = require('./core/Axios');

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  // ...
}
複製程式碼

這裡用到了Axios類,並且它是整個庫的核心,所以,接下來我們看看這個類是怎麼定義的。

1. 構造器

// core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
複製程式碼

構造器做了兩件事:i.初始化配置、ii.初始化攔截器。

2. 原型鏈方法

a. Axios.prototype.request

發起一個請求

傳送請求,以及對請求的處理,這部分我們放在下一節詳細分析。

b. Axios.prototype.getUri

獲取請求完整地址

c. Axios.prototype.get,Axios.prototype.post...

請求的別名

  • 語法糖,使得請求呼叫更加的語義化,更加的方便,其實現是基於Axios.prototype.request
  • 對於delete, get, head, options
function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
}
複製程式碼
  • 對於post, put, patch
function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
}
複製程式碼

3. Axios.prototype.request

這裡是axios的核心實現,包含了配置合併,請求傳送,攔截器加入

// core/Axios.js
Axios.prototype.request = function request(config) {
    if (typeof config === 'string') {
        config = arguments[1] || {};
        config.url = arguments[0];
    } else {
        config = config || {};
    }
    // ...
}
複製程式碼

對引數約定進行判斷,使得呼叫的時候可以更靈活。 比如:axios.request('api.example.com', config)axios.request(config)

// core/Axios.js
config = mergeConfig(this.defaults, config);
config.method = config.method ? config.method.toLowerCase() : 'get';
複製程式碼

合併配置,並且對方法有小寫處理,所以config傳入不用在意大小寫。

重點來了,這裡不僅處理了請求,還用了一個很巧妙的方法去處理請求和攔截器,以及攔截器之間的先後順序。

  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  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);
  });

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

  return promise;
複製程式碼

首先執行到迴圈前我們看看chain是啥結構

[請求攔截1成功,請求攔截1失敗,...,dispatchRequest, undefined,響應攔截1成功,響應攔截1失敗,...]
複製程式碼

入口通過Promise.resolve(config)將配置傳入,在經歷請求攔截器處理後,發起請求,請求成功獲得相應,再依次經歷響應攔截器處理。

dispatchRequest

發起請求的主要方法

處理URL
// core/dispatchRequest.js

if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
}
複製程式碼

區分相對路徑和絕對路徑

處理請求的data
// core/dispatchRequest.js

// Transform request data
config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
);
複製程式碼

處理PUT, POST, PATCH傳入的data

處理Headers
// core/dispatchRequest.js

// Flatten headers
config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
);
複製程式碼

這裡的config已經是合併以及處理過的,這裡分了三個headers

  1. 預設的通用headers
  2. 請求方法對應的headers
  3. 使用者傳入的headers

優先順序依次遞增,將這三個headers合併平鋪到config.headers

緊接著刪除config.headers下非http header的屬性

// core/dispatchRequest.js
utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
);
複製程式碼
★請求介面卡★

在不同環境下,發起請求的方式可能會不一樣,比如:在Node環境下發起請求可能是基於http模組,而在瀏覽器環境下發起請求又可能是基於XMLHttpRequest物件

但是介面卡的存在,建立了一個通用的介面,它有通用的輸入輸出。即不用管內部實現的差異,只需要按照約定輸入,以及處理約定的輸出即可。

預設介面卡:

// core/dispatchRequest.js
var defaults = require('../defaults');

var adapter = config.adapter || defaults.adapter;
複製程式碼
// defaults.js

function getDefaultAdapter() {
  var adapter;
  // Only Node.JS has a process variable that is of [[Class]] process
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

var defaults = {
    adapter: getDefaultAdapter()
    // ...
}
複製程式碼

axios內建了兩個介面卡adapters/http,adapters/xhr,在初始化預設配置時,它判斷了當前環境,並且應用了相應的介面卡。

當然你也可以傳入自己的介面卡,並且會被優先使用。介面卡可以應用於小程式開發等有自己的請求方法的場景。之後我們再來看看如何新建一個自定義介面卡。

接下來是處理請求介面卡的返回

// core/dispatchRequest.js

return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
}, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
});
複製程式碼

這裡就是簡單的處理了成功和失敗情況,格式化資料。

這裡的transformData做的事就是將使用者傳入的config.transformResponse全部執行一遍

throwIfCancellationRequested後面再細講。

4. InterceptorManager

攔截管理器,攔截器的增刪,遍歷

構造器

function InterceptorManager() {
  this.handlers = [];
}
複製程式碼

this.handlers存的就是所有攔截器

InterceptorManager.prototype.use

// core/InterceptorManager.js

InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
複製程式碼

新增一個攔截器並且返回攔截器序號(id)

InterceptorManager.prototype.eject

// core/InterceptorManager.js

InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
複製程式碼

根據序號(id)刪除一個攔截器

InterceptorManager.prototype.forEach

// core/InterceptorManager.js

InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};
複製程式碼

遍歷攔截器

使用

  1. Axios的建構函式中,初始化了兩個攔截管理器
// core/Axios.js

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
複製程式碼

對於每一個Axios例項,都可以訪問自己的兩個攔截管理器例項,對其進行增刪

  1. Axios.prototype.request方法中,將攔截器全部丟到佇列裡執行。

請求取消

首先我們舉個例子來看看如何取消請求

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();
複製程式碼

lib/axios.js

// lib/axios.js
axios.CancelToken = require('./cancel/CancelToken');
複製程式碼

接下來我們看看CancelToken類做了什麼

CancelToken類

1. 構造器

// cancel/CancelToken.js
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = 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);
  });
}
複製程式碼

構造器中用到了兩個屬性
cancelToken.promise
cancelToken.reason

構造器接收一個引數executor,並且在構造器最後執行,傳入一個cancel function作為引數

即在例子中傳入的config

{
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
}
複製程式碼

這裡的c引用的就是那個cancel function

這個cancel function可以隨時呼叫,並且在呼叫後,會將cancelToken.promisereslove掉,這有什麼用嗎你可能會問。

// adapters/xhr.js

var request = new XMLHttpRequest();

// ...

config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    // ...
});
複製程式碼

當呼叫cancel function的時候,會終止掉請求,這就是cancel的實現原理

2. CancelToken.source

// cancel/CancelToken.js

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};
複製程式碼

靜態方法source,自動新建cancelToken例項,並且將cancel function繫結返回

3. CancelToken.prototype.throwIfRequested

例項方法

// cancel/CancelToken.js

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};
複製程式碼

當取消時,拋錯

Axios.prototype.requestdispatchRequest

// core/dispathRequest.js

function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  // ...
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    // ...
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      // ...
    }
  });
}
複製程式碼

在請求前後都呼叫了throwIfCancellationRequested方法,在請求前取消不會發起請求,在請求後取消導致reject

Cancel類

1. 構造器

function Cancel(message) {
  this.message = message;
}
複製程式碼

2. 重寫了toString方法

3. Cancel.prototype.__CANCEL__

預設為true

4. 使用

// cancel/CancelToken.js

token.reason = new Cancel(message);
複製程式碼

token.reasonCancel的例項,它表示了cancelToken的狀態。而驗證狀態是由接下來這個方法實現的。

isCancel方法

module.exports = function isCancel(value) {
  return !!(value && value.__CANCEL__);
};
複製程式碼

xhrAdapter

瀏覽器端的請求介面卡

// adapters/xhr.js

module.exports = function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest() {})
})
複製程式碼

接收配置config作為引數,返回一個promise

dispatchXhrRequest就是ajax的封裝,來看看他是怎麼封裝的

  1. 首先,最基礎的一個請求建立,傳送流程
// adapters/xhr.js

var requestData = config.data;
var requestHeaders = config.headers;
var request = new XMLHttpRequest();
// ...
request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);
// ...
request.onreadystatechange = function handleLoad() {}
// ...
// 設定headers
if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // Remove Content-Type if data is undefined
          delete requestHeaders[key];
        } else {
          // Otherwise add header to the request
          request.setRequestHeader(key, val);
        }
      });
}
// ...
if (requestData === undefined) {
      requestData = null;
}
request.send(requestData);
複製程式碼
  1. 設定超時
// adapters/xhr.js

request.timeout = config.timeout;
// ...
request.ontimeout = function handleTimeout() {
      reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
};
// ...
複製程式碼
  1. 上傳下載進度
// adapters/xhr.js

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

if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
}
// ...
複製程式碼
  1. 取消請求,之前已經提及過
// adapters/xhr.js

if (config.cancelToken) {
    // ...
}
複製程式碼
  1. 處理錯誤
request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(createError('Network Error', config, null, request));

      // Clean up request
      request = null;
};
複製程式碼
  1. 處理中斷
request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      // Clean up request
      request = null;
};
複製程式碼

由於在手動觸發cancel時有reject,因此這裡判斷當沒有request的時候不重複reject

wxAdapter

在處理非預設環境時,可以自定義介面卡

import axios from 'axios'

const wxrequest = axios.create({
  adapter: function (config) {
    return new Promise(function (resolve, reject) {
      var response = {
        statusText: '',
        config: config
      }

      var request = wx.request({
        url: config.url,
        data: config.data,
        method: config.method.toUpperCase(),
        header: config.headers,
        responseType: config.responseType,
        success(res) {
          response.data = res.data
          response.status = res.statusCode
          response.headers = res.headers
          resolve(response)
        },
        fail(err) {
          reject(err)
          request = null
        }
      })

      response.request = request

      if (config.cancelToken) {
        config.cancelToken.promise.then(function onCanceled(cancel) {
          if (!request) {
            return
          }

          request.abort()
          reject(cancel)
          request = null
        })
      }
    })
  }
})

export default wxrequest

複製程式碼

在介面卡中加入取消功能,格式化返回資料

總結

攔截器和介面卡的設計,使得axios十分的靈活,更易擴充套件,其實現方式也值得學習使用。

相關文章