Shane Jix

Implementation Axios

create:August 17, 2020  update:April 12, 2022  ☕️☕️ 9 min read

同步链接: https://www.shanejix.com/posts/Implementation Axios/

曾经想过实现一个 mini 版的 axios,终于达成目标了!

mini axios

const xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
  if (request.readyState === 4) {
    if (request.status === 200) {
      return success(request.responseText);
    } else {
      return fail(request.status);
    }
  } else {
  }
};

xhr.open(method, url, true);

xhr.send(body);

也许你会捂不住嘴直呼这 tm 不是 Ajax(Async JavaScript And XML)吗~,跟 axios 有毛关系。

当然,如果没看过 axios 源码,确实很难让 axios 的浏览器实现和 Ajax 扯上联系,axios 不仅包装了 XMLHttpRequest,而且还很彻底,多彻底呢?

XMLHttpRequest 的属性:

  • onreadystatechange
  • readyState
  • response
  • responseText
  • responseType
  • responseURL
  • responseXML
  • status
  • statusText
  • timeout
  • upload
  • withCredentials

XMLHttpRequest 的方法:

  • abort()
  • getAllResponseHeaders()
  • getResponseHeader()
  • open()
  • openRequest()
  • overrideMimeType()
  • send()
  • setRequestHeader()

能用上的几乎都用上了!一览无余,是否有似曾相识的感觉!在没有 axios 的时代,可是手撸 http 请求的呢(得瑟)。怎么,还是还是觉得太空旷难以和 axios 的使用或实现建立联系?别慌!慢慢来,让我们从 axios 的使用和功能慢慢回忆。

Axios

首先需要知道 axios 是一个基于 Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生 XHR 的封装,只不过它是 Promise 的实现版本,有以下特点:

  • 在浏览器端使用 XMLHttpRequest 对象通讯
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 支持请求和响应的拦截器
  • 支持请求数据和响应数据的转换
  • 支持请求的取消
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

看了一大堆不如 demo 来得直接:

超级简单的

axios("/user?ID=12345").then((res) => {
  console.log(res);
});

几乎涵盖所有所有用法

// Set config defaults when creating the instance
const instance = axios.create(config);

// Append interceptors with instance
const myInterceptor = instance.interceptors.request.use(function () {/*...*/ });
instance.interceptors.request.eject(myInterceptor);

// Alter defaults after instance has been created
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;

// Alter properties after alert defaults in request
instance.get('/longRequest', {
  timeout: 5000,
  ...
}).then((res: response) => { });
// config
const config = {
  url: "/xxx",
  method: "get", // default
  baseURL: "https://some-domain.com/api/",
  transformRequest: [
    function (data, headers) {
      return data;
    },
  ],
  transformResponse: [
    function (data) {
      return data;
    },
  ],
  headers: { "X-Requested-With": "XMLHttpRequest" },
  params: {},
  paramsSerializer: function (params) {
    return Qs.stringify(params, { arrayFormat: "brackets" });
  },
  data: {},
  timeout: 1000,
  withCredentials: false, // default
  adapter: function (config) {},
  auth: {},
  responseType: "json", // default
  responseEncoding: "utf8", // default
  xsrfCookieName: "XSRF-TOKEN", // default
  xsrfHeaderName: "X-XSRF-TOKEN", // default
  onUploadProgress: function (progressEvent) {},
  onDownloadProgress: function (progressEvent) {},
  maxContentLength: 2000,
  validateStatus: function (status) {
    return status >= 200 && status < 300; // default
  },
  maxRedirects: 5, // default
  socketPath: null, // default
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),
  proxy: {},
  cancelToken: new CancelToken(function (cancel) {}),
};

// response model
interface response {
  data: {};
  status: 200;
  statusText: "OK";
  headers: {};
  config: {};
  request: {};
}

上述 instance 实例对于 axios 同样适用,有没有发现很多属性和方法 XMLHttpRequest 中有 axios 中同样也有呢?

没错 axios 中大部分的核心功能就是基于此的,下面看看是怎么实现 axios 的核心功能的吧!

核心实现

首先,用 create-react-app 创建了个简单的 demo 用于模拟查看 axios 的具体调用逻辑

useEffect(() => {
  debugger;
  Axios.get("www.biying.com").then((res) => {
    console.log(res);
  });
}, []);

demo 中 get 请求的调用栈如下图

大致可以分为三个阶段:

merge config

左边的小红框

transform

转换各种 data

request

依据 adapter 发送真实请求

当然这只是宏观上的认识,具体实现还得从源码入手

Index

https://github.com/axios/axios/blob/master/index.js

入口直接 require 到 lib 目录下

module.exports = require("./lib/axios");

axios

https://github.com/axios/axios/blob/master/lib/axios.js

直接导出 axios

module.exports = axios;

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

并且,导出的 axios 是默认配置 defaults 的实例对象

// Create the default instance to be exported
var axios = createInstance(defaults);

然后对 axios 对象做了扩展,create 方法,Axios 类,CancelToken 等等

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose Cancel & CancelToken
axios.Cancel = require("./cancel/Cancel");
axios.CancelToken = require("./cancel/CancelToken");
axios.isCancel = require("./cancel/isCancel");

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};

createInstance:

创建 axios 实例,并绑定 context 上下文

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

  return instance;
}

defaults:

https://github.com/axios/axios/blob/master/lib/defaults.js

默认配置中包含适配器(根据环境决定用什么发送请求),发送请求前对 data 和 headers 的转换函数,接受请求后对 data 的转换函数等

var defaults = {
  adapter: getDefaultAdapter(),

  transformRequest: [function transformRequest(data, headers) {}],

  transformResponse: [function transformResponse(data) {}],

  timeout: 0,

  xsrfCookieName: "XSRF-TOKEN",
  xsrfHeaderName: "X-XSRF-TOKEN",

  maxContentLength: -1,
  maxBodyLength: -1,

  validateStatus: function validateStatus(status) {},
};

defaults.headers = {
  common: {},
};

Axios

https://github.com/axios/axios/blob/master/lib/core/Axios.js

Axios():

构造函数初始化 defauls 属性和请求响应拦截器

/**
 * Create a new instance of Axios
 *
 * @param {Object} instanceConfig The default config for the instance
 */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager(),
  };
}

request(核心):

request() 方法实际执行 dispatchRequest 时会将请求拦截和响应拦截中加入 chain 队列的两端,从而实现一个promise 调用链

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  ...

  // Set config.method
  ...

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

InterceptorManager

(请求和响应)拦截器其实就是基于队列,实现的一个发布订阅模型

function InterceptorManager() {
  this.handlers = [];
}

订阅:入栈的对象的两个 key 所对应的 value 分别对应 promise 中的 resoveleFn 和 rejectedFn 回调

/**
 * Add a new interceptor to the stack
 *
 * @param {Function} fulfilled The function to handle `then` for a `Promise`
 * @param {Function} rejected The function to handle `reject` for a `Promise`
 *
 * @return {Number} An ID used to remove interceptor later
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
  });
  return this.handlers.length - 1;
};

发布:遍历 handlers 数组中个每个 item,在 request()中会加入 chain 的两端

/**
 * Iterate over all the registered interceptors
 *
 * This method is particularly useful for skipping over any
 * interceptors that may have become `null` calling `eject`.
 *
 * @param {Function} fn The function to call for each interceptor
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

取消:对应位置设置为 null(不是直接删除这个位置的元素),chain 链中不会执行

/**
 * Remove an interceptor from the stack
 *
 * @param {Number} id The ID that was returned by `use`
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

dispatchRequest:

https://github.com/axios/axios/blob/master/lib/core/dispatchRequest.js

核心逻辑就是,调用 adapter 执行正真的请求

/**
 * Dispatch a request to the server using the configured adapter.
 *
 * @param {object} config The config that is to be used for the request
 * @returns {Promise} The Promise to be fulfilled
 */
module.exports = function dispatchRequest(config) {

  // Ensure headers exist
  ...
  // Transform request data
  ...
  // Flatten headers
  ...

  var adapter = config.adapter || defaults.adapter;

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

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

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

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

getDefaultAdapter()

简单直接,对浏览器端和 node 判断

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

xhrAdapter:

https://github.com/axios/axios/blob/master/lib/adapters/xhr.js

返回一个 promise,核心逻辑还是对 XMLHttpRequest 的运用,是不是和开篇殊途同归呢

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    ...

    var request = new XMLHttpRequest();

    // HTTP basic authentication
    ...

    // Set the request timeout in MS
    request.timeout = config.timeout;

    // Listen for ready state
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }

      // The request errored out and we didn't get a response, this will be
      // handled by onerror instead
      // With one exception: request that using file: protocol, most browsers
      // will return status as 0 even though it's a successful request
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }

      // Prepare the response
      ...
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };

      settle(resolve, reject, response);

      // Clean up request
      request = null;
    };

    // Handle browser request cancellation (as opposed to a manual cancellation)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

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

      // Clean up request
      request = null;
    };

    // Handle low level network errors
    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;
    };

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

      // Clean up request
      request = null;
    };

    // Add xsrf header
    // This is only done if running in a standard browser environment.
    // Specifically not if we're in a web worker, or react-native.
    ...

    // Add headers to the request
    if ('setRequestHeader' in request) {
      ...
    }

    // Add withCredentials to request if needed
    ...

    // Add responseType to request if needed
    ...
    // Handle progress if needed
    ...

    // Not all browsers support upload events
    ...

    // Send the request
    request.send(requestData);
  });
};

总结

沿着本文的思路顺便画了张图

references

作者:shanejix 出处:https://www.shanejix.com/posts/Implementation Axios/ 版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。 声明:转载请注明出处!

Edit on GitHubDiscuss on GitHub


Shane Jix

Personal blog by Shane Jix. I explain with words and code.

LinksTools
© 2019 - 2022, Built withGatsby