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

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

看之前需要?

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

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

比如:

  1. axios既可以像函数一样执行,又类似对象一样,可以访问属性
  1. // axios作为函数执行
  2. axios({
  3. method: 'post',
  4. url: '/user/12345',
  5. data: {
  6. firstName: 'Fred',
  7. lastName: 'Flintstone'
  8. }
  9. });
  10. // axios类似对象一样访问属性 去执行
  11. axios.get('/user?ID=12345')
  12. .then(function (response) {
  13. console.log(response);
  14. })
  15. .catch(function (error) {
  16. console.log(error);
  17. });
  1. 可以使用自定义配置新建一个 axios 实例
  1. var instance = axios.create({
  2. baseURL: 'https://some-domain.com/api/',
  3. timeout: 1000,
  4. headers: {'X-Custom-Header': 'foobar'}
  5. });
  6. // 了解配置项
  7. {
  8. // `url` 是用于请求的服务器 URL
  9. url: '/user',
  10. // `method` 是创建请求时使用的方法
  11. method: 'get', // 默认是 get
  12. // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  13. // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  14. baseURL: 'https://some-domain.com/api/',
  15. // `transformRequest` 允许在向服务器发送前,修改请求数据
  16. // 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  17. // 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
  18. transformRequest: [function (data) {
  19. // 对 data 进行任意转换处理
  20. return data;
  21. }],
  22. ...
  23. ...
  24. ...
  25. ...
  26. ...
  27. }
  1. 拦截器
  1. // 添加请求拦截器
  2. axios.interceptors.request.use(function (config) {
  3. // 在发送请求之前做些什么
  4. return config;
  5. }, function (error) {
  6. // 对请求错误做些什么
  7. return Promise.reject(error);
  8. });
  9. // 添加响应拦截器
  10. axios.interceptors.response.use(function (response) {
  11. // 对响应数据做点什么
  12. return response;
  13. }, function (error) {
  14. // 对响应错误做点什么
  15. return Promise.reject(error);
  16. });

开始:先解析目录结构

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

拆解一下主要几个流程

Axiso源码目录及核心功能的梳理 - 图1Axios系统学习流程图.png

主要讲解:

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

1. 创建实例的流程?

  1. function Axios(instanceConfig) {
  2. this.defaults = instanceConfig;
  3. this.interceptors = {
  4. request: new InterceptorManager(),
  5. response: new InterceptorManager()
  6. };
  7. }
  8. Axios.prototype.request = function request(config) {...}
  9. // Axios.prototype.xx 省略很多方法
  10. function createInstance(defaultConfig) {
  11. var context = new Axios(defaultConfig);
  12. var instance = bind(Axios.prototype.request, context); // 注意实例是一个函数
  13. // 此处的.extend类似 Object.assign(), 把Axios.prototype和context的属性 方法,copy的实例instance身上去。 此时实例拥有类似对象的属性和方法(之前只是一个函数)
  14. utils.extend(instance, Axios.prototype, context);
  15. // 让实例instance的this指向context
  16. utils.extend(instance, context);
  17. // 用户可以自定义实例,配置项通过mergeConfig(defaultConfig, instanceConfig) 合并
  18. instance.create = function create(instanceConfig) {
  19. return createInstance(mergeConfig(defaultConfig, instanceConfig));
  20. };
  21. return instance;
  22. }
  23. 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) 合并
  1. var instance = axios.create({
  2. baseURL: 'https://some-domain.com/api/',
  3. timeout: 1000,
  4. headers: {'X-Custom-Header': 'foobar'}
  5. });

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模块发请求
  1. function getDefaultAdapter() {
  2. var adapter;
  3. if (typeof XMLHttpRequest !== 'undefined') {
  4. // For browsers use XHR adapter
  5. adapter = require('./adapters/xhr');
  6. } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
  7. // For node use HTTP adapter
  8. adapter = require('./adapters/http');
  9. }
  10. return adapter;
  11. }

也可以支持用户自定义请求处理,在自定义实例的配置项里设置adapter(此功能可以写mock,axios-mock-adapter库就是基于这个公共)

  1. var adapter = config.adapter || defaults.adapter;
  1. 多个拦截器的执行顺序?
    多个拦截器的话,是先 外到里,在里到外。
  1. 比如按顺序写的,
  2. 请求拦截器1
  3. 请求拦截器2
  4. 响应拦截器1
  5. 响应拦截器2
  6. 执行顺序是 请求拦截器2 - 请求拦截器1 - 发起请求/等待结果 - 响应拦截器1 - 响应拦截器2
  7. // 因为请求拦截器是unshift插入数组的,响应拦截器是按顺序push进数组的
  8. if (!synchronousRequestInterceptors) {
  9. var chain = [dispatchRequest, undefined];
  10. Array.prototype.unshift.apply(chain, requestInterceptorChain); // 请求拦截器是unshift插入数组的
  11. chain = chain.concat(responseInterceptorChain); // 响应拦截器是按顺序push进数组的
  12. promise = Promise.resolve(config);
  13. while (chain.length) {
  14. promise = promise.then(chain.shift(), chain.shift());
  15. }
  16. return promise;
  17. }
  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() 等等 可以自行看代码
  1. var defaults = {
  2. ...
  3. transformRequest: [function transformRequest(data, headers) {
  4. normalizeHeaderName(headers, 'Accept');
  5. normalizeHeaderName(headers, 'Content-Type');
  6. if (utils.isFormData(data) ||
  7. utils.isArrayBuffer(data) ||
  8. utils.isBuffer(data) ||
  9. utils.isStream(data) ||
  10. utils.isFile(data) ||
  11. utils.isBlob(data)
  12. ) {
  13. return data;
  14. }
  15. if (utils.isArrayBufferView(data)) {
  16. return data.buffer;
  17. }
  18. if (utils.isURLSearchParams(data)) {
  19. setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
  20. return data.toString();
  21. }
  22. if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
  23. setContentTypeIfUnset(headers, 'application/json');
  24. return stringifySafely(data);
  25. }
  26. return data;
  27. }],
  28. transformResponse: [function transformResponse(data) {
  29. var transitional = this.transitional || defaults.transitional;
  30. var silentJSONParsing = transitional && transitional.silentJSONParsing;
  31. var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
  32. var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';
  33. if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
  34. try {
  35. return JSON.parse(data);
  36. } catch (e) {
  37. if (strictJSONParsing) {
  38. if (e.name === 'SyntaxError') {
  39. throw enhanceError(e, this, 'E_JSON_PARSE');
  40. }
  41. throw e;
  42. }
  43. }
  44. }
  45. return data;
  46. }],
  47. ...
  48. };

3. 取消请求

首先看一个使用案例:

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

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

  • 主要的方法是:XMLHttpRequest对象的 abort 方法,才能取消请求 (node内的http模块也复用了abort这个名字)
  1. if (config.cancelToken || config.signal) {
  2. // Handle cancellation
  3. // eslint-disable-next-line func-names
  4. onCanceled = function(cancel) {
  5. if (!request) {
  6. return;
  7. }
  8. reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
  9. request.abort();
  10. request = null;
  11. };
  12. config.cancelToken && config.cancelToken.subscribe(onCanceled);
  13. if (config.signal) {
  14. config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
  15. }
  16. }

最终返回响应结果

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

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

  1. request.onreadystatechange = function handleLoad() {
  2. if (!request || request.readyState !== 4) {
  3. return;
  4. }
  5. // 请求出错,没有得到响应,会由 onerror 处理,通过promise的 reject 抛出去
  6. // 有一个例外:请求使用 file: 协议,大多数浏览器 即使请求成功,也会返回状态为 0
  7. if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
  8. return;
  9. }
  10. // readystate handler is calling before onerror or ontimeout handlers,
  11. // so we should call onloadend on the next 'tick'
  12. setTimeout(onloadend);
  13. }
  14. function onloadend() {
  15. if (!request) {
  16. return;
  17. }
  18. // Prepare the response
  19. var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
  20. var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
  21. request.responseText : request.response;
  22. var response = {
  23. data: responseData,
  24. status: request.status,
  25. statusText: request.statusText,
  26. headers: responseHeaders,
  27. config: config,
  28. request: request
  29. };
  30. settle(function _resolve(value) {
  31. resolve(value);
  32. done();
  33. }, function _reject(err) {
  34. reject(err);
  35. done();
  36. }, response);
  37. // Clean up request
  38. request = null;
  39. }
  40. // 最终会由settle,对结果进行一下校验,在resolve或reject,对外返回
  41. function settle(resolve, reject, response) {
  42. var validateStatus = response.config.validateStatus;
  43. if (!response.status || !validateStatus || validateStatus(response.status)) {
  44. resolve(response);
  45. } else {
  46. reject(createError(
  47. 'Request failed with status code ' + response.status,
  48. response.config,
  49. null,
  50. response.request,
  51. response
  52. ));
  53. }
  54. };

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


码字不易,点赞鼓励!