axios浏览器环境执行流程

486 阅读5分钟

axios源码学习

环境搭建:

  • github扒拉源码到本地

  • npm install

  • 点到入口文件,index.js,找到lib\axios.js就是入口文件了,node进入调试模式就可以进行调试了

首先看lib\axios.js文件调用createInstance函数创建一个axios实例。这边返回的是一个axios实例,又因为其使用bind方法将其指向当前实例的request方法,因此,这边instance返回的就是request函数

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  // 将request函数绑定到当前对象上,此处的bind方法其实就是对Function.bind方法的封装
  var instance = bind(Axios.prototype.request, context);

  // 继承Axios.prototype上的属性
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  // 适配axios可以直接使用axios.create(config)或者axios.get(url,config)的方式创建实例
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}
//使用默认配置项创建axios实例
var axios = createInstance(defaults);
//下面代码省略,总结来说是对实例对象的一些配置信息

此处,一定有人会问,为什么不直接使用new Axios创建实例呢?

摘自:axios执行原理了解一下!因为axios内部调用的都是Axios.prototype.request方法,Axios.prototype.request默认请求为get,为了让开发这可以直接调用axios()就可以发送请求,而不是axios.get()。如果直接new一个axios对象是无法实现这种简写的。

接下来看看createInstance到底做了什么:

var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
//主要函数,request函数
Axios.prototype.request = function request(configOrUrl, config) {
  // Allow for axios('example/url'[, config]) a la fetch API
   //支持直接配置url的方式或者是axios(url,{})方式。axios(url)或者axios(url,config)
  if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }
//合并配置项
  config = mergeConfig(this.defaults, config);

  // Set config.method
   //配置请求方法,如果配置了请求方法则使用自定义配置,否则使用默认配置,如果没穿则默认使用Get
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
//这个不知道是用来做什么的,不过不影响看整体流程
  var transitional = config.transitional;

  if (transitional !== undefined) {
    validator.assertOptions(transitional, {
      silentJSONParsing: validators.transitional(validators.boolean),
      forcedJSONParsing: validators.transitional(validators.boolean),
      clarifyTimeoutError: validators.transitional(validators.boolean)
    }, false);
  }

  // filter out skipped interceptors
   //我们常用的请求拦截器配置
  var requestInterceptorChain = [];
  var synchronousRequestInterceptors = true;
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }

    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
	//响应拦截器配置
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;
  var newConfig = config;
  try {
      //最终会调用dispatchRequest方法
    promise = dispatchRequest(newConfig);
  } catch (error) {
    return Promise.reject(error);
  }

  return promise;
};

看看dispatchRequest做了什么吧。顾名思义,派发请求,在此处会触发发起请求的方法。

用我自己简化版的代码来看吧

function dispatchRequest(config){
	if(!config) return new Error('没有请求配置')
	config.headers = config.headers || {};
	config.data = config.data;
	utils.forEach(
	    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
	    function(method) {
	      delete config.headers[method];
	    }
	  );
	  var adapter = config.adapter || defaults.adapter;
	  return adapter(config).then(function(response){
		  return response
	  },function(reason){
		  return Promise.reject(reason)
	  })
}

总的来说做了两件事情,

  • 配置请求头和请求数据信息

  • 调用adapter适配器

adapter是个什么东西呢?

溯源可以发现,在lib\defaults\index.js中发现,适配器是用来区分不用环境的代码

axios官网有些,其支持这两个特性:

适配器就是用来做不同环境区分用的,我们看的是浏览器环境的代码,Node环境流程和功能大致和浏览器端类似,后面有时间就继续更。

var defaults = {
    adapter:getDefaultAdapter()
}

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

既然最终会调用adapter函数,那么来看看require('../adapters/xhr')中做了什么事情:

总的来说适配器发起了请求,并且重写了各个请求的回调方法。请求结束后会回调onloadend方法,返回的是一个promise对象,然后一个请求的流程就结束了后面是对调用结果的处理。

三大步:

  • 创建请求var request = new XMLHttpRequest();
  • 发起请求 request.open()
  • 发送请求request.send(requestData)
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;
    var responseType = config.responseType;
    var onCanceled;
	//创建一个请求
    var request = new XMLHttpRequest();
    var fullPath = buildFullPath(config.baseURL, config.url);
    // 初始化一个请求,该方法只能在js代码中使用,原生代码中需要使用openRequest()方法
    var parsed = url.parse(fullPath);
    var protocol = utils.getProtocol(parsed.protocol);
	//request.open(method,url,async,user,password)
    request.open(
      config.method.toUpperCase(),
      buildURL(fullPath, config.params, config.paramsSerializer),
      true
    );

    // Set the request timeout in MS
    request.timeout = config.timeout;
	//loadend事件总是在一个资源的加载进度停止之后被触发 
    function onloadend() {
      if (!request) {
        return;
      }
      var responseData =
        !responseType || responseType === "text" || responseType === "json"
          ? request.responseText
          : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        config: config,
        request: request,
      };

      settle(
        function _resolve(value) {
          resolve(value);
        },
        function _reject(err) {
          reject(err);
        },
        response
      );

      // Clean up request
      request = null;
    }
    // 重写onload函数
    if ("onloadend" in request) {
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
      // 使用默认处理方法readystatechange改变时的回调函数
      // Listen for ready state to emulate onloadend
      request.onreadystatechange = function handleLoad() {
          //readyState == 4表示已经发送请求,服务器已完成返回相应,浏览器已完成了下载响应内容
        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;
        }
        // readystate handler is calling before onerror or ontimeout handlers,
        // so we should call onloadend on the next 'tick'
        setTimeout(onloadend);
      };
    }

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

      reject(
        //请求取消
      );

      // 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(
        //请求出错
      );

      // Clean up request
      request = null;
    };

    // Handle timeout
    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = config.timeout
        ? "timeout of " + config.timeout + "ms exceeded"
        : "timeout exceeded";
      var transitional = config.transitional || transitionalDefaults;
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(
        //请求超时
      );
      // Clean up request
      request = null;
    };
    if (!requestData) {
      requestData = null;
    }

    if (parsed.path === null) {
      reject(
        //请求路径有问题
      );
      return;
    }
    // 发送请求
    request.send(requestData);
  });
};

axios是怎么做到防止跨站请求伪造的?

让每一个请求都带一个从cookie中拿到的key,根据浏览器同源策略,假的网站拿不到cookie中的key.这样后台就可以轻松辨别出这个请求是否是用户在假的网站上误导。

当用户进行登录请求时,后端把包含xsrf字段的cookie保存在session中并返回给前端,前端需要获取到cookie中的值并且能放入ajax请求体或请求头中,后端把这个值与session中的相应值进行判断,根据跨域不可访问不同域的cookie,攻击者也很难猜测出xsrf的值。

axios获取到值后默认放入request header中的。

我自己仿照axios写的一个测试代码,里面有测试代码,直接扒下来就可以看,简易版的实现。

github