Read axios source code in one article

勤学如春起之苗,不见其增,日有所长;
辍学如磨刀之石,不见其损,日有所亏。

image.png

This article says axios源码that after reading this article, the following questions will be easily solved,

  • What is the adapter principle of Axios?
  • How does Axios implement request and response interception?
  • How does Axios cancel the request?
  • What is the principle of CSRF? How does Axios protect against client-side CSRF attacks?
  • How is request and response data conversion implemented?

The full text is about 2,000 words, and it takes about 6 minutes to read it.Axios 版本为 0.21.1

Let's use features as an entry point to answer the above questions and experience the art of minimal encapsulation of Axios source code.

Features

  • Create XMLHttpRequest from browser
  • Create HTTP requests from Node.js
  • Support Promise API
  • Intercept requests and responses
  • cancel request
  • Automatically convert JSON data
  • Support for client-side XSRF attacks

The first two features explain that 为什么 Axios 可以同时用于浏览器和 Node.js 的原因, in simple terms, it is to determine whether to use XMLHttpRequestor Node.js 的 HTTPto create a request by judging whether it is a server or a browser environment. This compatible logic is called 适配器, and the corresponding source code is in lib/defaults.js,

// defaults.js
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;
}

The above is the judgment logic of the adapter, passed 侦测当前环境的一些全局变量,决定使用哪个 adapter.
Among them, the judgment logic for the Node environment can also be reused when we do ssr server-side rendering. Next, let's take a look at Axios's encapsulation of the adapter.

Adapter xhr

Locate the source code file lib/adapters/xhr.js, first look at the overall structure,

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

Exports a function that accepts a configuration parameter and returns a Promise.

We extract the key parts,

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

    var request = new XMLHttpRequest();

    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    request.onreadystatechange = function handleLoad() {
    
    }
    request.onabort = function handleAbort() {
    
    }
    request.onerror = function handleError() {
    
    }
    request.ontimeout = function handleTimeout() {
    
    }

    request.send(requestData);
  });
};

Does it feel familiar? That's right, this is XMLHttpRequestthe usage posture of , first create an xhr and then open to start the request, monitor the xhr status, and then send to send the request.

Let's take a look at Axios' handling of onreadystatechange.

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

  settle(resolve, reject, response);

  // Clean up request
  request = null;
};

First , only proceed 对状态进行过滤when the request is complete ( ).readyState === 4

It should be noted that if the XMLHttpRequest request is wrong, we can handle it by monitoring onerror in most cases, but also: 有一个例when the request is used 文件协议(file://), most browsers will return a status code of 0 even though the request is successful.

Axios also handles this exception.

Once the request is complete, it's time to process the response. Here 将响应包装成一个标准格式的对象, as the third parameter passed to the settle method, settle is lib/core/settle.jsdefined in,

function settle(resolve, reject, response) {
    
    
  var validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    
    
    resolve(response);
  } else {
    
    
    reject(createError(
      'Request failed with status code ' + response.status,
      response.config,
      null,
      response.request,
      response
    ));
  }
};

settle simply encapsulates the callback of Promise, 确保调用按一定的格式返回.

The above is the main logic xhrAdapterof , and the rest is the simple processing of the request header, some supported configuration items, and callbacks such as timeout, error, and cancellation of the request 对于 XSRF 攻击的防范是通过请求头实现.

Let's briefly review what is XSRF (also called CSRF, 跨站请求伪造).

CSRF

Background: After the user logs in, the login credentials need to be stored to keep the login status, instead of sending the account password for each request.

How to stay logged in?

At present, the more common method is that after the server receives the HTTP request, it adds Set-Cookieoptions in the response header and stores the credentials in the cookie. After the browser receives the response, it will store the cookie. According to the same-origin policy of the browser, the When the server initiates a request, it will automatically carry cookies to cooperate with server-side verification to maintain the user's login status.

So if we don't 判断请求来源的合法性, after logging in 通过其他网站向服务器发送了伪造的请求, the carry 登录凭证的 Cookiewill be sent to the server with the forged request at this time, causing a security hole, which is what we said CSRF,跨站请求伪造.

Therefore 防范伪造请求的关键就是检查请求来源, reffereralthough the field can identify the current site, it is not reliable enough. Now the common solution in the industry is to attach one to each request. The anti-CSRF tokenprinciple of this is that the attacker cannot get the cookie, so we can encrypt the cookie (such as Encrypt the sid), and then cooperate with the server to do some simple verification to determine whether the current request is forged.

Axios simply implements support for special csrf tokens,

// 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.
if (utils.isStandardBrowserEnv()) {
    
    
  // Add xsrf header
  var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
    cookies.read(config.xsrfCookieName) :
    undefined;

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

Interceptor

Interceptor is a characteristic feature of Axios. Let's briefly review the usage method first.

// 拦截器可以拦截请求或响应
// 拦截器的回调将在请求或响应的 then 或 catch 回调前被调用
var instance = axios.create(options);

var requestInterceptor = axios.interceptors.request.use(
  (config) => {
    // do something before request is sent
    return config;
  },
  (err) => {
    // do somthing with request error
    return Promise.reject(err);
  }
);

// 移除已设置的拦截器
axios.interceptors.request.eject(requestInterceptor)

So how is the interceptor implemented?

Go to lib/core/Axios.jsline ,

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Through the constructor of Axios, we can see that both request and response in interceptors are an instance InterceptorManagercalled , what is this InterceptorManager?

Locate to the source code lib/core/InterceptorManager.js,

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

InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null
  });
  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);
    }
  });
};

InterceptorManager 是一个简单的事件管理器, to realize the management of interceptors,

The interceptor is stored through handlers, and then the instance method of the interceptor is provided 添加,移除,遍历执行. Each interceptor object stored contains callbacks as resolve and reject in Promise and two configuration items.

It is worth mentioning that 移除方法是通过直接将拦截器对象设置为 null 实现的instead of splice cutting the array, the corresponding null value processing is also added in the traversal method.

In doing so, on the one hand, the ID of each item remains unchanged as the array index of the item, and on the other hand, it also avoids the performance loss of re-cutting and splicing the array.

The callback of the interceptor will be called before the then or catch callback of the request or response. How is this achieved?

Go back to lib/core/Axios.jsline 27 of the source code, the request method of the Axios instance object,

The key logic we extract is as follows,

Axios.prototype.request = function request(config) {
    
    
  // Get merged config
  // Set config.method
  // ...
  var requestInterceptorChain = [];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    
    
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

	var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    
    
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;

  var chain = [dispatchRequest, undefined];

  Array.prototype.unshift.apply(chain, requestInterceptorChain);

  chain.concat(responseInterceptorChain);

  promise = Promise.resolve(config);

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

  return promise;
};

It can be seen that when the request is executed, the actual request (dispatchRequest) and the interceptor are managed through a queue called chain. The logic of the whole request is as follows,

  1. First initialize the request and response interceptor queue, and put the resolve and reject callbacks at the head of the queue in turn
  2. Then initialize a Promise to execute the callback, and the chain is used to store and manage the actual request and interceptor
  3. Put the request interceptor at the head of the chain queue, and the response interceptor at the end of the chain queue
  4. When the queue is not empty, through the chain call of Promise.then, the request interceptor, the actual request, and the response interceptor are dequeued in turn, and
    finally the Promise after the chain call is returned

The actual request here is the encapsulation of the adapter, and the conversion of request and response data is done here.

So how is data conversion achieved?

Transform data

Locate to the source code lib/core/dispatchRequest.js,

function dispatchRequest(config) {
    
    
  throwIfCancellationRequested(config);

  // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
  
  var adapter = config.adapter || defaults.adapter;

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

Here throwIfCancellationRequested 方法用于取消请求, we will discuss about canceling the request later. We can see that the sending request is implemented by calling the adapter, and the request and response data will be converted before and after the call.

The transformation is implemented through the transformData function, which traverses the transformation function set by the call. The transformation function takes headers as the second parameter, so we can perform some different transformation operations according to the information in the headers.

// 源码 core/transformData.js
function transformData(data, headers, fns) {
    
    
  utils.forEach(fns, function transform(fn) {
    
    
    data = fn(data, headers);
  });

  return data;
};

Axios also provides two default conversion functions for converting request and response data. by default,

Axios will do some processing on the data passed in by the request. For example, if the request data is an object, it will be serialized into a JSON string, and if the response data is a JSON string, it will try to convert it into a JavaScript object. These are very useful functions.

The corresponding converter source code can lib/default.jsbe found on line 31 of

var defaults = {
    
    
	// Line 31
  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;
  }],
  
  transformResponse: [function transformResponse(data) {
    
    
    var result = data;
    if (utils.isString(result) && result.length) {
    
    
      try {
    
    
        result = JSON.parse(result);
      } catch (e) {
    
     /* Ignore */ }
    }
    return result;
  }],
}

We said that Axios supports cancellation requests. How about a cancellation method?

CancelToken

In fact, whether it is the xhr on the browser side or the request object of the http module in Node.js, the abort method is provided to cancel the request, so we only need to call abort at the right time to cancel the request.

So, what is the right time? It is appropriate to give control to the user. So the right timing should be decided by the user, that is to say, we need to expose the method of canceling the request. Axios implements the canceling request through CancelToken. Let's take a look at its posture.

First of all, Axios provides two ways to create a cancel token,

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 方式一,使用 CancelToken 实例提供的静态属性 source
axios.post("/user/12345", {
    
     name: "monch" }, {
    
     cancelToken: source.token });
source.cancel();

// 方式二,使用 CancelToken 构造函数自己实例化
let cancel;

axios.post(
  "/user/12345",
  {
    
     name: "monch" },
  {
    
    
    cancelToken: new CancelToken(function executor(c) {
    
    
      cancel = c;
    }),
  }
);

cancel();

What exactly is CancelToken? lib/cancel/CancelToken.jsGo to line 11 of the source code ,

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

CancelTokenIt is one 由 promise 控制的极简的状态机. When instantiated, a promise will be mounted on the instance. The resolve callback of this promise is exposed to the external method executor. In this way, after we call the executor method from the outside, we will get a promise whose state becomes fulfilled. So how do we cancel the request with this promise?

Is it enough to just get the promise instance when requesting, and then cancel the request in the then callback?

Locate to lib/adapters/xhr.jsline 158 of the source code of the adapter,

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

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

And lib/adaptors/http.jsline 291 of the source code,

if (config.cancelToken) {
    
    
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    
    
    if (req.aborted) return;

    req.abort();
    reject(cancel);
  });
}

Sure enough, in the then callback of the promise of the CancelToken instance in the adapter 调用了 xhr 或 http.request 的 abort 方法. Just imagine, if we don't call the method of canceling the CancelToken from the outside, does it mean that the resolve callback will not be executed, and the then callback of the promise in the adapter will not be executed, and the abort will not be called to cancel the request.

reference

Reprinted from the original
Axios Github Source Code

recommended reading

Vue source code learning directory

Vue source code learning complete directory

Connect the dots - the road to front-end growth

Connect the dots - the road to front-end growth


谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强

Guess you like

Origin blog.csdn.net/weixin_42752574/article/details/122376017