axios 源码分析一:初识 axios

150 阅读3分钟

从事前端开发的同学,相信对网络请求库 axios 不陌生;那么在面向文档编程的同时,是否有思考过该库是如何被设计的?又是如何实现的呢?作为一名已经使用该库多年的前端开发者,接下来会花一段时间来研究其实现原理,并将所学到的记录下来,作为学习笔记。

本文将从 axios 目录结构和构建流程谈起,目的是从整体上认识 axios,分析的库版本为:0.26.1

目录结构

想要研究开源库的源码实现,需要对其目录结构有整体上的认识,并且了解一级目录及根目录下文件的作用,从而了解其模糊划分和功能。那么,axios 的目录结构如下:

.
├── dist                    // 构建打包产物
├── examples                // 提供的例子
├── lib                     // axios 代码实现都在该目录下
├── node_modules            // 安装依赖
├── sandbox                 // 官方提供的 serve
├── test                    // 测试用例
├── CHANGELOG.md            // changelog
├── CODE_OF_CONDUCT.md      // 贡献者契约行为准则
├── COLLABORATOR_GUIDE.md   // 合作指南
├── CONTRIBUTING.md         // 参与开源指南
├── COOKBOOK.md             // 常用功能指南
├── ECOSYSTEM.md            // axios 相关库和资源列表
├── Gruntfile.js            // grunt 配置文件
├── LICENSE                 // 所使用的开源协议描述
├── README.md               // 使用指南
├── SECURITY.md             // 查看该文件可得知提交安全漏洞方式
├── UPGRADE_GUIDE.md        // 升级指南
├── bower.json              // bower 配置文件 
├── index.d.ts              // typescript 类型声明文件
├── index.js                // 入口文件
├── karma.conf.js           // 单元测试框架 karma 配置文件
├── package-lock.json       // 依赖库锁定版本文件
├── package.json            // package.json
├── tsconfig.json           // typescript 配置文件
├── tslint.json             // tslint 配置文件
└── webpack.config.js       // webpack 配置文件

构建流程

在分析 axios 构建流程前,先简单地介绍构建工具 GruntGrunt 是一个自动化、借助任务运行器调度任务执行的 JavaScript 构建工具。它依赖于 Gruntfile.js 或者 Gruntfile.coffee 配置文件,用来配置或者定义任务,并加载插件;也就是说,当执行 Grunt 命令,Grunt 先加载 Gruntfile.js 配置文件,然后执行其定义的任务,最终输出构建产物。至于 Grunt 更详情的信息,可参考官网

平时我们在使用 axios 时,要么通过 npm 方式安装,要么通过链接引入等方式,那么是否有思考过其产物是如何生成的?也就是说,通过输入源代码,经过构建,最终输出构建产物。那么,整个构建流程又是如何实现的吗?

先来看下 package.json 文件:

{
  "scripts": {
    "build": "cross-env NODE_ENV=production grunt build",
  }
}

当在控制台执行命令 npm run build 时,通过 package.json 文件可知,最终会执行命令 grunt build,进而会执行 Gruntfile.js 文件定义的任务以及加载插件,那么来看下其是如何定义的?

// eslint-disable-next-line strict
module.exports = function(grunt) {
  require('load-grunt-tasks')(grunt);

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    meta: {
      banner: '/* <%= pkg.name %> v<%= pkg.version %> | (c) <%= grunt.template.today("yyyy") %> by Matt Zabriskie */\n'
    },

    clean: {
      dist: 'dist/**'
    },

    package2bower: {
      all: {
        fields: [
          'name',
          'description',
          'version',
          'homepage',
          'license',
          'keywords'
        ]
      }
    },

    package2env: {
      all: {}
    },

    usebanner: {
      all: {
        options: {
          banner: '<%= meta.banner %>',
          linebreak: false
        },
        files: {
          src: ['dist/*.js']
        }
      }
    },

    eslint: {
      target: ['lib/**/*.js']
    },

    karma: {
      options: {
        configFile: 'karma.conf.js'
      },
      single: {
        singleRun: true
      },
      continuous: {
        singleRun: false
      }
    },

    mochaTest: {
      test: {
        src: ['test/unit/**/*.js']
      },
      options: {
        timeout: 30000
      }
    },

    watch: {
      build: {
        files: ['lib/**/*.js'],
        tasks: ['build']
      },
      test: {
        files: ['lib/**/*.js', 'test/**/*.js', '!test/typescript/axios.js', '!test/typescript/out.js'],
        tasks: ['test']
      }
    },

    webpack: require('./webpack.config.js')
  });

  grunt.registerMultiTask('package2bower', 'Sync package.json to bower.json', function() {
    var npm = grunt.file.readJSON('package.json');
    var bower = grunt.file.readJSON('bower.json');
    var fields = this.data.fields || [];

    for (var i = 0, l = fields.length; i < l; i++) {
      var field = fields[i];
      bower[field] = npm[field];
    }

    grunt.file.write('bower.json', JSON.stringify(bower, null, 2));
  });

  grunt.registerMultiTask('package2env', 'Sync package.json to env.json', function() {
    var npm = grunt.file.readJSON('package.json');
    grunt.file.write('./lib/env/data.js', [
      'module.exports = ',
      JSON.stringify({
        version: npm.version
      }, null, 2),
      ';'].join(''));
  });

  grunt.registerTask('test', 'Run the jasmine and mocha tests', ['eslint', 'mochaTest', 'karma:single']);
  grunt.registerTask('build', 'Run webpack and bundle the source', ['clean', 'webpack']);
  grunt.registerTask('version', 'Sync version info for a release', ['usebanner', 'package2bower', 'package2env']);
};

首先,grunt 需要调用方法 initCofnig 做初始化操作,比如定义任务、加载插件等。那么,从配置文件可看出,定义的任务有 cleanpackage2bowerpackage2envusebannereslintkarmamochaTestwatchwebpack等。

接着,需要注册已经定义的任务,可调用方法 registerMultiTask 或者 registerTask 注册任务。

这里对任务 webpack 做进一步说明,在 Grunt 中借助插件 grunt-webpack 使用 webpack 来执行构建打包,那么我们来看下 webpack 配置文件 webpack.config.js 是如何定义的?

const TerserPlugin = require('terser-webpack-plugin');
var webpack = require('webpack');
var config = {};

function generateConfig(name) {
  var compress = name.indexOf('min') > -1;
  var config = {
    entry: './index.js',
    output: {
      path: __dirname + '/dist/',
      filename: name + '.js',
      sourceMapFilename: name + '.map',
      library: 'axios',
      libraryTarget: 'umd',
      globalObject: 'this'
    },
    node: {
      process: false
    },
    devtool: 'source-map',
    mode: compress ? 'production' : 'development'
  };
  return config;
}

['axios', 'axios.min'].forEach(function (key) {
  config[key] = generateConfig(key);
});

module.exports = config;

可以看出配置文件逻辑挺简单的,由于最终要构建出 axios.jsaxios.min.js,于是为它们各自生成配置存储在变量 config,将其导出并作为参数传给 GruntGrunt 根据这份配置,并且借助插件 grunt-webpack 执行构建打包,最终构建出产物。