主頁 > 移動端開發 > axios 原始碼精讀

axios 原始碼精讀

2021-03-05 11:21:57 移動端開發

前言

閱讀框架原始碼的好處在于提升編程水平,以及了解框架的設計思想,配合其官方檔案,讓我們對它的使用變得更加得心應手,

axios 是一個小而精的框架,不像 vue 那般龐大而復雜到讓人望而生畏,閱讀也起來相對容易,在閱讀原始碼前,建議掌握 call、apply、bind,Promise,閉包,this 指向,原型鏈等知識,以及一些設計模式,

axios 版本:0.18.1

1.目錄結構

axios 的原始碼在 lib 目錄,其結構如下
在這里插入圖片描述

在正式介紹原始碼前,先回顧一下 axios 的常見的使用姿勢,以便更好的理解原始碼,

2.axios 的使用姿勢

看原始碼前必須熟練使用 axios,以下就列舉了 axios 的使用方式以及如何封裝 axios,

使用方式

方式一:axios(config)

axios({
  method: "post",
  url: "/user/12345",
  data: {
    firstName: "Fred",
    lastName: "Flintstone",
  },
});

方式二:axios(url[, config])

axios("/user/12345", {
  /* config */
});

方式三:axios[method](url[, config])

axios.get("/user/12345", {
  /* config */
});

這種方式針對 get、delete、head、options 方法

方式四:axios[method](url[, data[, config]])

axios.post(
  "/user/12345",
  {
    firstName: "Fred",
    lastName: "Flintstone",
  },
  {
    /* config */
  }
);

這種方式針對 post、put、patch 方法

方式五:axios.request(config)

axios.request({
  method: "post",
  url: "/user/12345",
  data: {
    firstName: "Fred",
    lastName: "Flintstone",
  },
});

是否會好奇 axios 如何實作支持這五種方式的?后續將會解答,

封裝 axios

有時我們更傾向于創建一個 Axios 實體,然后設定攔截器,

import axios from "axios";
import { BASE_URL } from "./http";

// create an axios instance
const service = axios.create({
  baseURL: BASE_URL, // url = base url + request url
  withCredentials: true, // send cookies when cross-domain requests(是否支持跨域)
  timeout: 5000, // request timeout(超時時間)
});

// request interceptor(請求攔截器)
service.interceptors.request.use(
  (config) => {
    // do something before request is sent
    return config;
  },
  (error) => {
    // do something with request error
    // console.log(error) // for debug
    return Promise.reject(error);
  }
);

// response interceptor(回應攔截器)
service.interceptors.response.use(
  (response) => {
    const res = response.data;
    return res;
  },
  (error) => {
    if (error.response) {
      // console.log('err' + error) // for debug
      switch (error.response.status) {
        // 不同狀態碼下執行不同操作
        case 401:
          break;
        case 404:
          break;
        case 500:
          break;
        default:
      }
    }
    return Promise.reject(error);
  }
);

export default service;

攔截器是 axios 最核心的一個功能,通過閱讀原始碼將了解其攔截器是如何實作的,

3.從 axios.js 開始

axios.js 中主要包含以下部分:

  • 第一部分:bind、utils 工具方法;
  • 第二部分:axios、axios.Axios、axios.create;
  • 第三部分:取消相關的方法(Cancel、CancelToken、isCancel)、all、spread 處理并發請求的助手函式(這里暫不介紹),

這里著重介紹第二部分的實作,

axios.js 原始碼如下:

"use strict";

var utils = require("./utils");
var bind = require("./helpers/bind");
var Axios = require("./core/Axios");
var defaults = require("./defaults");

/**
 * 創建一個 Axios 實體
 *
 * @param {Object} defaultConfig 實體的配置
 * @return {Axios} 一個 Axios 實體
 */
function createInstance(defaultConfig) {
  // 創建一個 Axios 實體
  var context = new Axios(defaultConfig);

  // bind 回傳一個函式實體(這里是 wrap 方法)
  // 修改 Axios.prototype.request 的 this 指向為 context
  // 執行 axios(config) 相當于執行 Axios.prototype.request(config)
  var instance = bind(Axios.prototype.request, context);

  // 把 Axios.prototype 的所有屬性/方法都復制到 instance 中, context 作為 bind 的作用域
  // Axios.prototype 上有 request, get, post 等 8 個方法
  utils.extend(instance, Axios.prototype, context);

  // 把 context 的所有屬性都復制到 instance 中
  // context 包含 defaults 默認配置 和 interceptors 攔截器
  utils.extend(instance, context);

  return instance;
}

// 創建要匯出的默認實體
var axios = createInstance(defaults);

// 暴露 Axios 類以允許類繼承
axios.Axios = Axios;

// 暴露創建 Axios 實體的工廠方法 【工廠模式】
axios.create = function create(instanceConfig) {
  // merge: 將用戶傳入的配置與默認配置進行合并
  return createInstance(utils.merge(defaults, instanceConfig));
};

// 暴露取消 request 相關的方法
axios.Cancel = require("./cancel/Cancel");
axios.CancelToken = require("./cancel/CancelToken");
axios.isCancel = require("./cancel/isCancel");

// 暴露 all/spread (處理并發請求的助手函式)
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require("./helpers/spread");

module.exports = axios;
// 允許在 TypeScript 中使用默認匯入語法
module.exports.default = axios;

注:代碼中的 bind、utils 后面有介紹,

從這個檔案中可以看到其對外暴露了一個 Axios 實體 axios,axios 是通過 createInstance() 方法創建的,

這里不妨列印一下 axios(直接在 VScode 中執行當前檔案),其結果如下:

function wrap() {
  var args = new Array(arguments.length);
  for (var i = 0; i < args.length; i++) {
    args[i] = arguments[i];
  }
  return fn.apply(thisArg, args);
}

可以看到,axios 其實就是一個 wrap() 方法,結合上面的原始碼知道,fn 為 Axios.prototype.request,thisArg 為 context(Axios 實體),所以執行 axios(config) 就相當于執行 Axios.prototype.request(config),

如果好奇 context 長什么模樣也可列印一下,其結果如下:

Axios {
  defaults: {
    adapter: [Function: httpAdapter],
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    validateStatus: [Function: validateStatus],
    headers: {
      common: [Object],
      delete: {},
      get: {},
      head: {},
      post: [Object],
      put: [Object],
      patch: [Object]
    }
  },
  interceptors: {
    request: InterceptorManager { handlers: [] },
    response: InterceptorManager { handlers: [] }
  }
}

注:這里的方括號‘[]’并不一定是陣列,比如 adapter: [Function: httpAdapter]

可以看到,列印的是一個 Axios 類的實體物件,包含 defaults(默認配置)和 interceptors(攔截器),至于具體細節放在后面再介紹,

再看 utils.extend(instance, Axios.prototype, context); 代碼,

這步操作是在將 Axios.prototype 的所有屬性/方法都復制到 instance 中, context 作為 bind 時的作用域(見 utils.entend 內部實作),這里的 instance 其實就是 axios;context 上面已經列印了,是 Axios 的默認實體,Axios.prototype 又包含啥呢?列印結果如下:

Axios {
  request: [Function: request],
  delete: [Function],
  get: [Function],
  head: [Function],
  options: [Function],
  post: [Function],
  put: [Function],
  patch: [Function]
}

所以經過 utils.extend 后 instance 就并不是一個簡單的 wrap 方法,其上還包括了 Axios.prototype 中的 8 個方法,因此,這就解釋了為什么可以通過 axios.get 等方式(文章開頭時 axios 的使用方式 三、四、五)進行發送請求,

最后 utils.extend(instance, context);

這里將 context 中的 defaults、interceptors(攔截器)擴展到了 instance 中,所以,在回頭看看在 封裝 axios 中為什么能通過 service.interceptors 設定攔截器就不感到奇怪了吧,

看到這兒,你如果感覺有些暈,說明你還對 axios 整體結構還不熟悉,不妨繼續往下,之后再回頭看一遍,

4.第一部分:bind、utils 工具方法

bind 方法

"use strict";
// 手寫實作一個 bind 方法,兼容低版本瀏覽器
module.exports = function bind(fn, thisArg) {
  return function wrap() {
    // 將 arguments 轉為陣列
    // 注: 這里是否多余?apply 支持 arguments 類陣列物件
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    // 執行 fn 并傳入作用域和引數
    return fn.apply(thisArg, args);
  };
};

utils 中的方法有很多,這里只介紹下面三個,

utils.forEach

/**
 * forEach 方法的實作,并支持物件的遍歷,【迭代器模式】
 * 遍歷陣列和物件,為每一項呼叫一個函式 fn
 *
 * @param {Object|Array} obj 迭代的物件
 * @param {Function} fn 為每一項執行的回呼函式
 */
function forEach(obj, fn) {
  // 不是一個有效值時,退出
  if (obj === null || typeof obj === "undefined") {
    return;
  }

  // 不是一個可迭代的值時
  if (typeof obj !== "object") {
    obj = [obj];
  }

  if (isArray(obj)) {
    // 遍歷并覆寫陣列值
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // 遍歷并覆寫物件值
    // for in 會遍歷原型上的屬性, 這里用 hasOwnProperty() 過濾掉原型上的屬性
    for (var key in obj) {
      // 判斷鍵值是否存在
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

utils.merge

/**
 * 物件屬性進行合并,后面物件屬性優先級高于前面物件屬性
 *
 * 例如:
 * ```js
 * var result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // 輸出 456
 * ```
 *
 * @param {Object} obj1 合并物件
 * @returns {Object} 所有合并屬性的結果
 */
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;
}

utils.extend

/**
 * 將 b 的屬性復制到 a 上,屬性名相同則覆寫
 *
 * @param {Object} a 要擴展的物件
 * @param {Object} b 要復制屬性的物件
 * @param {Object} thisArg 要系結功能的物件
 * @return {Object} 物件 a 的結果值
 */
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === "function") {
      // 是函式
      a[key] = bind(val, thisArg);
    } else {
      // 不是函式
      a[key] = val;
    }
  });
  return a;
}

5.第二部分:axios、axios.Axios、axios.create

axios、axios.create 上面已經介紹了,下面主要介紹 Axios 類的實作,

Axios.js

這個檔案宣告了一個 Axios 構造器,然后在其原型上添加了 request、delete、get、head、options、post、put、patch 8 個方法,

"use strict";

var defaults = require("./../defaults");
var utils = require("./../utils");
var InterceptorManager = require("./InterceptorManager");
var dispatchRequest = require("./dispatchRequest");

/**
 * Axios 構造器
 *
 * @param {Object} instanceConfig 實體的配置引數
 */
function Axios(instanceConfig) {
  // 設定請求配置
  this.defaults = instanceConfig;
  // 為請求和回應添加 interceptors (攔截器)
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager(),
  };
}

/**
 * 發送請求
 *
 * @param {Object} config 用戶傳入的配置 (將其合并到默認配置 defaults 上)
 */
Axios.prototype.request = function request(config) {
  // 如果 config 是一個字串,則將其當作 url
  if (typeof config === "string") {
    config = utils.merge(
      {
        url: arguments[0],
      },
      arguments[1]
    );
  }

  // 將配置進行合并
  // defaults: Axios 默認配置
  // {method: 'get'}: 默認是 get 方法
  // this.defaults: Axios 構造器中的配置屬性
  // config: 用戶傳入的配置
  config = utils.merge(defaults, { method: "get" }, this.defaults, config);
  config.method = config.method.toLowerCase(); // 方法名轉小寫

  // 連接攔截器中間件
  // undefined: 后面需要兩兩取出, 用于占位
  var chain = [dispatchRequest, undefined];
  // 將 config 轉換為 promise, 同時后續 config 將作為引數進行傳遞
  var promise = Promise.resolve(config);

  // 將 request 攔截器逐一插入到 鏈表的頭部
  // 注:這里的 forEach 是 InterceptorManager.prototype.forEach
  this.interceptors.request.forEach(function unshiftRequestInterceptors(
    interceptor
  ) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // 將 response 攔截器逐一插入到 鏈表的尾部
  this.interceptors.response.forEach(function pushResponseInterceptors(
    interceptor
  ) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 通過上面的步驟, 我們就形成了如下鏈表:
  // request 攔截器(interceptors.request) + 發起請求的方法(dispatchRequest) + response 攔截器(interceptors.response)

  while (chain.length) {
    // 從鏈表中從頭連續取出2個元素,第一個作為 promise 的 resolve handler, 第二個作為 reject handler
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

// 提供支持的請求方法的別名
// 注意這四個方法都不需要 請求負載(request payload)
utils.forEach(
  ["delete", "get", "head", "options"],
  function forEachMethodNoData(method) {
    Axios.prototype[method] = function (url, config) {
      // this.request: 呼叫 Axios.prototype.request
      // 將引數合并為一個, config 非必填
      return this.request(
        utils.merge(config || {}, {
          method: method,
          url: url,
        })
      );
    };
  }
);

// 注意這四個方法都有 請求負載(request payload)
utils.forEach(["post", "put", "patch"], function forEachMethodWithData(method) {
  Axios.prototype[method] = function (url, data, config) {
    // this.request: 呼叫 Axios.prototype.request
    // 將引數合并為一個, config 非必填
    return this.request(
      utils.merge(config || {}, {
        method: method,
        url: url,
        data: data,
      })
    );
  };
});

module.exports = Axios;

注:請求負載(request payload),就是傳遞的 data 資料,

通過上面的代碼可以發現,不管什么方法,其最后都是執行 request 方法,

request 方法接收一個 config 引數,若 config 是字串,則將 arguments[0] 當作 url,arguments[1] 當作其他配置引數,所以,這就是為什么即能夠通過 axios(config) 傳參方式也能夠通過 axios(url[, config]) 的傳參方式進行訪問的原因,當然,還有一種傳參方式 axios[method](url[, data[, config]])(只支持 post、put、patch),通過其 post、put、patch 的實作就能看明白原因,

接著往下,開始合并 config,然后將 config.method 轉換為小寫,

然后,最重要的部分到了,定義一個鏈表 chain,然后通過 Promise.resolve() 將 config 轉換為 promise,再將請求與回應攔截器添加到鏈表頭尾,最終,回傳的是一個 promise,

由于這里涉及攔截器,所以先看看 InterceptorManager.js,

InterceptorManager.js

InterceptorManager.js 中宣告了一個 InterceptorManager 攔截器管理類,然后在其原型上添加了 use、eject、forEach 方法,

"use strict";

var utils = require("./../utils");

/**
 * 宣告一個攔截器管理的類
 */
function InterceptorManager() {
  // 用一個陣列存放所有攔截器
  this.handlers = [];
}

/**
 * 添加一個攔截器
 *
 * @param {Function} fulfilled 成功的回呼
 * @param {Function} rejected 失敗的回呼
 * @return {Number} 回傳添加的攔截器的下標, 用于以后洗掉
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
  });
  return this.handlers.length - 1;
};

/**
 * 洗掉一個攔截器
 *
 * @param {Number} id 通過 use 方法回傳的 id
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

/**
 * 遍歷所有非 null 的 handler, 并呼叫
 *
 * @param {Function} fn 呼叫每個攔截器的函式
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    // 這里的 h 就是 handlers 中的值
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

結合最開始的 封裝 axios 部分代碼,簡寫如下示例代碼:

// -示例代碼-
// 創建一個 axios 實體
const service = axios.create({
  baseURL: "baseurl",
  withCredentials: true,
  timeout: 5000,
});
// 添加請求攔截器
service.interceptors.request.use(
  (config) => config, // fn1
  (error) => Promise.reject(error) // fn2
);
// 添加回應攔截器
service.interceptors.response.use(
  (response) => response.data, // fn3
  (error) => Promise.reject(error) // fn4
);
// 發送請求
service({
  url: "/api",
  method: "POST",
  data: {
    name: "Bob",
  },
})
  .then((res) => {
    // 拿到結果
  })
  .catch((err) => {
    // 錯誤資訊
  });

結合上面的示例代碼、InterceptorManager.js 、Axios.js 就更容易理解攔截器的實作與運行流程,

這里執行 service(config) 就是在執行 Axios.prototype.request(config),最終 chain 鏈表如下:

[
  [Function], // fn1
  [Function], // fn2
  [(Function: dispatchRequest)],
  undefined,
  [Function], // fn3
  [Function], // fn4
];

其執行程序為:fn1,fn2(request 攔截器) => dispatchRequest(發起請求的方法) => fn3,fn4(response 攔截器),

接下來就開始查看 dispatchRequest 的實作了,

dispatchRequest.js

"use strict";

var utils = require("./../utils");
var transformData = require("./transformData");
var isCancel = require("../cancel/isCancel");
var defaults = require("../defaults");
var isAbsoluteURL = require("./../helpers/isAbsoluteURL");
var combineURLs = require("./../helpers/combineURLs");

/**
 * 如果請求已取消,則拋出“取消”,
 */
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

/**
 * 使用配置的配接器向服務器發送請求,
 *
 * @param {object} config 用于請求的配置
 * @returns {Promise} 回傳 fulfilled(已成功) 狀態的 Promise
 */
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // 如果配置了 baseURL, 且 config.url 也不是絕對路徑, 則拼接
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

  // 確保 headers 存在
  config.headers = config.headers || {};

  // 對即將發起的請求的 資料 和 header 做預處理
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // 合并不同配置的 header
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  // 洗掉 headers 中的 method
  utils.forEach(
    ["delete", "get", "head", "post", "put", "patch", "common"],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // 配接器
  var adapter = config.adapter || defaults.adapter;

  // 使用配接器發起請求
  return adapter(config).then(
    function onAdapterResolution(response) {
      // 請求成功

      throwIfCancellationRequested(config);

      // 轉換回應資料
      response.data = transformData(
        response.data,
        response.headers,
        config.transformResponse
      );

      // 回傳回應資料
      return response;
    },
    function onAdapterRejection(reason) {
      // 請求失敗
      if (!isCancel(reason)) {
        throwIfCancellationRequested(config);

        // 如果請求錯誤且有資料
        // 轉換回應資料
        if (reason && reason.response) {
          reason.response.data = transformData(
            reason.response.data,
            reason.response.headers,
            config.transformResponse
          );
        }
      }

      // 回傳錯誤資訊,通過 catch 捕獲
      return Promise.reject(reason);
    }
  );
};

dispatchRequest(config) 回傳的是 adapter(config) 執行的結果

接著需要看 adapter 的實作,這里的 adapter 一般是從 defaults 中獲取的,所以先看看 defaults,

defaults.js

"use strict";

var utils = require("./utils");
var normalizeHeaderName = require("./helpers/normalizeHeaderName");

// 默認 Content-Type
var DEFAULT_CONTENT_TYPE = {
  "Content-Type": "application/x-www-form-urlencoded",
};

// 如果 headers 中 Content-Type 不存在,則設定其值
function setContentTypeIfUnset(headers, value) {
  if (
    !utils.isUndefined(headers) &&
    utils.isUndefined(headers["Content-Type"])
  ) {
    headers["Content-Type"] = value;
  }
}

// 不同環境下,獲取默認的配接器
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== "undefined") {
    // 瀏覽器環境使用 XHR
    adapter = require("./adapters/xhr");
  } else if (typeof process !== "undefined") {
    // node 環境使用 HTTP
    adapter = require("./adapters/http");
  }
  return adapter;
}

var defaults = {
  // 默認請求方式
  adapter: getDefaultAdapter(),

  // 格式化 requestData
  transformRequest: [
    function transformRequest(data, headers) {
      // 將 headers 中不標準的屬性名,格式化為 Content-Type 屬性名
      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;
      }
      // 如果是 URLSearchParams 物件, 設定 Content-Type
      if (utils.isURLSearchParams(data)) {
        setContentTypeIfUnset(
          headers,
          "application/x-www-form-urlencoded;charset=utf-8"
        );
        return data.toString();
      }
      // 如果是 物件, 設定 Content-Type
      if (utils.isObject(data)) {
        setContentTypeIfUnset(headers, "application/json;charset=utf-8");
        return JSON.stringify(data);
      }
      return data;
    },
  ],

  // 格式化 responseData, 將其決議為物件
  transformResponse: [
    function transformResponse(data) {
      if (typeof data === "string") {
        try {
          data = JSON.parse(data);
        } catch (e) {
          /* Ignore */
        } // 這里并未對 JSON.parse() 的錯誤做處理
      }
      return data;
    },
  ],

  // 設定請求超時時間, 默認為 0. 超時將終止請求, 0 意味著沒有超時
  timeout: 0,

  // CSRF/XSRF (跨站請求偽造)
  // `xsrfCookieName` 是要用作 xsrf 令牌的值的 cookie 的名稱
  // `xsrfHeaderName` 是攜帶 xsrf 令牌值的 http 頭的名稱
  xsrfCookieName: "XSRF-TOKEN",
  xsrfHeaderName: "X-XSRF-TOKEN",

  // 定義允許的回應內容的最大尺寸
  maxContentLength: -1,

  // 驗證請求狀態
  // `validateStatus` 定義對于給定的 HTTP 回應狀態碼是 resolve 或 reject 的 promise ,
  // 如果 `validateStatus` 回傳 `true` (或者設定為 `null` 或 `undefined`),
  // promise 將被 resolve; 否則, promise 將被 reject
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  },
};

defaults.headers = {
  // 通用的 HTTP 欄位
  common: {
    Accept: "application/json, text/plain, */*",
  },
};

// 添加 請求方法
utils.forEach(["delete", "get", "head"], function forEachMethodNoData(method) {
  defaults.headers[method] = {}; // 沒有 Content-Type
});

// 設定默認的 Content-Type
utils.forEach(["post", "put", "patch"], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;

defaults 中包含的內容整理如下圖:

在這里插入圖片描述

adapter 這里只介紹 xhr,

xhr.js

"use strict";

var utils = require("./../utils");
var settle = require("./../core/settle");
var buildURL = require("./../helpers/buildURL");
var parseHeaders = require("./../helpers/parseHeaders");
var isURLSameOrigin = require("./../helpers/isURLSameOrigin");
var createError = require("../core/createError");

// 瀏覽器環境下使用的方法
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;

    // 如果是 FormData 物件, 洗掉 headers 中的 Content-Type 欄位, 讓瀏覽器自動生成
    if (utils.isFormData(requestData)) {
      delete requestHeaders["Content-Type"];
    }

    // 創建一個 xhr 物件
    // 參考: https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
    var request = new XMLHttpRequest();

    // 配置 HTTP 請求頭的 Authentication [HTTP身份驗證]
    // 參考: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Authentication
    if (config.auth) {
      var username = config.auth.username || "";
      var password = config.auth.password || "";
      // 使用 btoa 創建一個 base-64 編碼的字串
      requestHeaders.Authorization = "Basic " + btoa(username + ":" + password);
    }

    // open(請求方法, 請求URL, 是否支持異步): 初始化一個請求
    request.open(
      config.method.toUpperCase(),
      buildURL(config.url, config.params, config.paramsSerializer),
      true
    );

    // 請求的最大請求時間, 超出則終止請求
    request.timeout = config.timeout;

    // 監聽 readyState 屬性變化, 其值為 4 時表示請求成功
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }

      // 在請求完成前, status 的值為 0,如果 XMLHttpRequest 出錯, 瀏覽器回傳的 status 也為 0,
      // file 協議除外, status 為 0 時也是一個成功的請求
      // responseURL: 回傳經過序列化(serialized)的回應 URL
      if (
        request.status === 0 &&
        !(request.responseURL && request.responseURL.indexOf("file:") === 0)
      ) {
        return;
      }

      // 準備回應
      // getAllResponseHeaders 回傳所有回應頭
      // responseText 回傳的文本資料
      var responseHeaders =
        "getAllResponseHeaders" in request
          ? parseHeaders(request.getAllResponseHeaders())
          : null;
      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,
        request: request,
      };

      // status >= 200 && status < 300 resolve
      // 否則 reject
      settle(resolve, reject, response);

      // 清除 request
      request = null;
    };

    // 請求出錯
    request.onerror = function handleError() {
      // 拋出網路錯誤
      reject(createError("Network Error", config, null, request));

      // 清除 request
      request = null;
    };

    // 請求超時
    request.ontimeout = function handleTimeout() {
      // 拋出超時錯誤
      reject(
        createError(
          "timeout of " + config.timeout + "ms exceeded",
          config,
          "ECONNABORTED",
          request
        )
      );

      // 清除 request
      request = null;
    };

    // 增加 xsrf header
    // 僅在標準瀏覽器環境中運行時才能執行此操作,
    // xsrf header 是用來防御 CSRF 攻擊
    if (utils.isStandardBrowserEnv()) {
      var cookies = require("./../helpers/cookies");

      // 增加 xsrf header
      var xsrfValue =
        (config.withCredentials || isURLSameOrigin(config.url)) &&
        config.xsrfCookieName
          ? cookies.read(config.xsrfCookieName)
          : undefined;

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

    // 將 config 中配置的 requestHeaders, 回圈設定到請求頭上
    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 header
          request.setRequestHeader(key, val);
        }
      });
    }

    // `withCredentials` 表示跨域請求時是否需要使用憑證
    if (config.withCredentials) {
      request.withCredentials = true;
    }

    // `responseType` 表示服務器回應的資料型別,
    // 可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
    if (config.responseType) {
      try {
        request.responseType = config.responseType;
      } catch (e) {
        // DOMException 拋出瀏覽器不兼容 XMLHttpRequest Level 2.
        // 但是,對于 “json” 型別,可不拋出錯誤,因為可以通過默認的 transformResponse 方法對其進行決議,
        if (config.responseType !== "json") {
          throw e;
        }
      }
    }

    // `onDownloadProgress` 允許為下載處理進度事件
    if (typeof config.onDownloadProgress === "function") {
      request.addEventListener("progress", config.onDownloadProgress);
    }

    // `onUploadProgress` 允許為上傳處理進度事件
    if (typeof config.onUploadProgress === "function" && request.upload) {
      request.upload.addEventListener("progress", config.onUploadProgress);
    }

    if (config.cancelToken) {
      // 取消操作
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // 清除 request
        request = null;
      });
    }

    if (requestData === undefined) {
      requestData = null;
    }

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

xhrAdapter 回傳一個 Promise,執行 adapter(config) 就相當于執行 dispatchXhrRequest,因此,可以得到如下執行流程:

在這里插入圖片描述

結尾

到這兒其實我們就對 axios 核心原始碼部分有了解了,其中涉及多種涉及模式,比如工廠模式、迭代器模式、配接器模式、中間件等,其中較為精華的部分便是其使用中間件實作攔截器,這部分內容不妨多看兩遍,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/266398.html

標籤:其他

上一篇:一文幫你搞懂 Android 檔案描述符

下一篇:Shell 一鍵安裝命令

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more