Axiso源码目录及核心功能的梳理

507 阅读8分钟

Axiso源码目录及核心功能的梳理

前言:此文是个人对axios源码的一个整理,没有细入到每个api功能,主要是把文件目录结构及对应的功能,和几个核心功能的原理解析了一遍。目的是领入源码的入门,以及了解核心流程

看之前需要?

需要充分了解promise相关用法,比如.then()接收2个参数都是函数,分别作用是什么?这2个函数的返回值对后面的链式调用有什么影响?如果函数返回值也是个promise 后面会怎么执行?

需要充分了解axios的基本使用和配置: www.kancloud.cn/yunye/axios…

比如:

  1. axios既可以像函数一样执行,又类似对象一样,可以访问属性
// axios作为函数执行
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

// axios类似对象一样访问属性 去执行
axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });
  1. 可以使用自定义配置新建一个 axios 实例
var instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});


// 了解配置项
{
  // `url` 是用于请求的服务器 URL
  url: '/user',

  // `method` 是创建请求时使用的方法
  method: 'get', // 默认是 get

  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://some-domain.com/api/',

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
  transformRequest: [function (data) {
    // 对 data 进行任意转换处理

    return data;
  }],
  
  ...
  ...
  ...
  ...
  ...
}
  1. 拦截器
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

开始:先解析目录结构

  1. 打开package.json找到main字段,就是源代码dev模式的入口(打包后的文件另算)
"main": "index.js",
  1. 我们主要关注lib目录,axios核心功能代码都在这里面(源码的目录结构和功能拆分的非常清晰,非常有美感,很值得学习)
./lib
├── adapters     适配器(当前环境是 浏览器则用xhr发请求,node环境则用http模块发请求)
│   ├── README.md
│   ├── http.js
│   └── xhr.js
├── axios.js     主入口(聚合所有的功能)
├── cancel       取消请求 相关的
│   ├── Cancel.js
│   ├── CancelToken.js
│   └── isCancel.js
├── core 
│   ├── Axios.js     初始化axios实例(主要是请求相关的功能,单独拆出来的,利于维护。属于axios.js的子集)
│   ├── InterceptorManager.js    拦截器管理
│   ├── README.md
│   ├── buildFullPath.js         合并成完整的path,比如baseURL: 'https://some-domain.com/api/',   请求path是 someModule/getList。 结果合成https://some-domain.com/api/someModule/getList
│   ├── createError.js           封装报错的功能
│   ├── dispatchRequest.js       管理适配器adapters,类似小老板 代理管 适配器工人(适配器工人有3种,用户自定义,浏览器的xhr,node的http模块)
│   ├── enhanceError.js          增强错误提示,更具体
│   ├── mergeConfig.js           合并配置,axios有默认配置,用户可以axios.create自定义实例,用户自定义的配置,和 axios的默认配置的合并
│   ├── settle.js                promise处理的一个代理。根据响应状态解析或拒绝 Promise。 
│   └── transformData.js         负责执行转换数据的函数
├── defaults.js       默认配置
├── env               当前axios版本
│   ├── README.md
│   └── data.js       当前axios版本
├── helpers           工具函数,几乎见名知意
│   ├── README.md
│   ├── bind.js       和现在的bind一样,此处是兼容老浏览器
│   ├── buildURL.js   往url后面添加参数,比如 xxx?id=1&page=2
│   ├── combineURLs.js    通过组合指定的 URL 创建一个新的 URL
│   ├── cookies.js        操作cookies
│   ├── deprecatedMethod.js    废弃的写法报错提示
│   ├── isAbsoluteURL.js       判断url是否是绝对路径
│   ├── isAxiosError.js        判断是否是axios内部错误
│   ├── isURLSameOrigin.js     判断url是否同源
│   ├── normalizeHeaderName.js 标准化请求头字段名(兼容用户的不规范写法)
│   ├── parseHeaders.js        解析http头 成js对象
│   ├── spread.js              类似apply的功能(兼容老浏览器)
│   └── validator.js           校验(如一些配置项等等)
└── utils.js

拆解一下主要几个流程

Axios系统学习流程图.png

主要讲解:

  1. 创建实例的流程?
  2. 执行请求的流程?
    1. 适配器是什么?
    2. 多个拦截器的执行顺序是什么?
    3. 如何转换请求数据?
  3. 如何取消请求?
  4. 最终返回响应结果

1. 创建实例的流程?

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Axios.prototype.request = function request(config) {...}
// Axios.prototype.xx  省略很多方法

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context); // 注意实例是一个函数

  // 此处的.extend类似 Object.assign(), 把Axios.prototype和context的属性 方法,copy的实例instance身上去。  此时实例拥有类似对象的属性和方法(之前只是一个函数)
  utils.extend(instance, Axios.prototype, context);

  // 让实例instance的this指向context
  utils.extend(instance, context);

  // 用户可以自定义实例,配置项通过mergeConfig(defaultConfig, instanceConfig) 合并
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

var axios = createInstance(defaults);

总结:

  1. 实例instance最开始是一个函数,后面通过类似 Object.assign(),把Axios.prototype和context的属性 方法,copy的实例instance身上去。 此时实例拥有类似对象的属性和方法。可以完成axios({method: 'get'}) 也可以 axios.get('')2种灵活调用
  2. 可以用默认axios配置,也可以自定义,自定义用axios.create,可以自定义一些配置。配置项通过mergeConfig(defaultConfig, instanceConfig) 合并
var instance = axios.create({ 
    baseURL: 'https://some-domain.com/api/', 
    timeout: 1000, 
    headers: {'X-Custom-Header': 'foobar'}
});

2. 执行请求的流程?

流程: Axios.prototype.request -> 请求拦截器 -> dispatchRequest(处理请求参数,调用适配器,小老板 代理) -> adapter适配器 发起请求 -> 报错/取消请求 -> 响应拦截器 -> 返回结果

从axios官网上copy下来 特性展示:

  1. 从浏览器中创建 XMLHttpRequests
  2. 从 node.js 创建 http 请求
  3. 支持 Promise API
  4. 拦截请求和响应
  5. 转换请求数据和响应数据
  6. 取消请求
  7. 自动转换 JSON 数据
  8. 客户端支持防御 XSRF

  1. 首先理解适配器,针对特性1,2
  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求 axios默认会根据当前环境,是否存在 XMLHttpRequest,确定是浏览器环境还是node环境,浏览器环境用XHR对象发请求,node环境用http模块发请求
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(此功能可以写mock,axios-mock-adapter库就是基于这个公共)

var adapter = config.adapter || defaults.adapter;
  1. 多个拦截器的执行顺序? 多个拦截器的话,是先 外到里,在里到外。
比如按顺序写的,
请求拦截器1
请求拦截器2
响应拦截器1
响应拦截器2

执行顺序是  请求拦截器2 - 请求拦截器1 - 发起请求/等待结果 - 响应拦截器1 - 响应拦截器2


// 因为请求拦截器是unshift插入数组的,响应拦截器是按顺序push进数组的
if (!synchronousRequestInterceptors) {
    var chain = [dispatchRequest, undefined];

    Array.prototype.unshift.apply(chain, requestInterceptorChain); // 请求拦截器是unshift插入数组的
    chain = chain.concat(responseInterceptorChain); // 响应拦截器是按顺序push进数组的

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

    return promise;
}
  1. 如何转换请求数据? 在 dispatchRequest 阶段执行的
  • dispatchRequest阶段:处理请求参数,调用适配器,类似小老板 代理

以下就是有一些默认配置,比如

  1. 数据是 ArrayBuffer,Blob,File,stream,Formdata等就直接return
  2. 数据是json格式则JSON.stringify转成字符串
  3. 如果content-type是application/x-www-form-urlencoded,则return data.toString() 等等 可以自行看代码
var defaults = {
  ...
  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) || (headers && headers['Content-Type'] === 'application/json')) {
      setContentTypeIfUnset(headers, 'application/json');
      return stringifySafely(data);
    }
    return data;
  }],

  transformResponse: [function transformResponse(data) {
    var transitional = this.transitional || defaults.transitional;
    var silentJSONParsing = transitional && transitional.silentJSONParsing;
    var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
    var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';

    if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
      try {
        return JSON.parse(data);
      } catch (e) {
        if (strictJSONParsing) {
          if (e.name === 'SyntaxError') {
            throw enhanceError(e, this, 'E_JSON_PARSE');
          }
          throw e;
        }
      }
    }

    return data;
  }],
  ...
};

3. 取消请求

首先看一个使用案例:

  1. 配置 cancelToken 对象
  2. 缓存用于取消请求的 cancel 函数,在后面特定时机调用 cancel 函数取消请求
  3. 在错误回调中判断如果 error 是 cancel, 做相应处理
  4. 实现功能 点击按钮, 取消某个正在请求中的请求(主动触发的)
 <script>
   //获取按钮
   const btns = document.querySelectorAll('button');
   //2.声明全局变量
   let cancel = null;
   //发送请求
   btns[0].onclick = function () {
     //检测上一次的请求是否已经完成
     if (cancel !== null) {
       //取消上一次的请求
       cancel();
     }
     axios({
       method: 'GET',
       url: 'http://localhost:3000/posts',
       //1. 添加配置对象的属性
       cancelToken: new axios.CancelToken(function (c) {
         //3. 将 c 的值赋值给 cancel
         cancel = c;
       })
     }).then(response => {
       console.log(response);
       //将 cancel 的值初始化
       cancel = null;
     })
   }

   //绑定第二个事件取消请求
   btns[1].onclick = function () {cancel(); }
 </script>

源码细节在cancel文件内 和 xhr.js内

  • 主要的方法是:XMLHttpRequest对象的 abort 方法,才能取消请求 (node内的http模块也复用了abort这个名字)
if (config.cancelToken || config.signal) {
  // Handle cancellation
  // eslint-disable-next-line func-names
  onCanceled = function(cancel) {
    if (!request) {
      return;
    }
    reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
    request.abort();
    request = null;
  };

  config.cancelToken && config.cancelToken.subscribe(onCanceled);
  if (config.signal) {
    config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
  }
}

最终返回响应结果

监听xhr的onreadystatechange方法,当readyState === 4 时,才正常得到响应结果

最终会由settle,对结果进行一下校验,在resolve或reject对外返回响应结果

request.onreadystatechange = function handleLoad() {
  if (!request || request.readyState !== 4) {
    return;
  }

   // 请求出错,没有得到响应,会由 onerror 处理,通过promise的 reject 抛出去
   // 有一个例外:请求使用 file: 协议,大多数浏览器 即使请求成功,也会返回状态为 0
  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);
}

function onloadend() {
  if (!request) {
    return;
  }
  // Prepare the response
  var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
  var responseData = !responseType || responseType === 'text' ||  responseType === 'json' ?
    request.responseText : request.response;
  var response = {
    data: responseData,
    status: request.status,
    statusText: request.statusText,
    headers: responseHeaders,
    config: config,
    request: request
  };

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

  // Clean up request
  request = null;
}

// 最终会由settle,对结果进行一下校验,在resolve或reject,对外返回
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
    ));
  }
};

感受: 源码的目录结构和功能拆分的非常清晰,非常有美感,很值得学习!


码字不易,点赞鼓励!