编写一个axios这样的库

16,832 阅读29分钟

前言

本文需对jses6webpack网络请求等基础知识有基本了解

相信axios大家都用过或者什么其他的网络请求相关的库,什么ajaxfly.js等等等等,光我用过的请求库就七八个,都大同小异

本文并不是要完完全全一个字不差的实现axios所有功能,没有意义,但是也会实现的七七八八,主要是感受一下这个流程和架子、以及 这个项目怎么才能易于拓展、是不是易于测试的、可读性怎么样等等等等

废话不多说,开搞~

搭建项目

老规矩先建一个空目录,然后打开命令行执行

yarn init -y

cnpm init -y

webpack

然后是引入webpack,虽然本节不主讲webpack,这里我稍微提一嘴,webpackwebpack-cli不光项目里要下载,全局也要下载,也就是 yarn global add webpack webpack-cli

安装依赖包

执行命令,主要就需要这几个包,帮忙编译和调试的,babel帮助尽量兼容浏览器的,毕竟咱们写代码肯定充满了es6

yarn add webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env

配置webpack

接下来再在根目录创建webpack.config.js来配置一下webpack,然后再建一个src目录,来存放咱们这个库的代码,现在的目录就会是这个样子

先简单配置一下,后续有需求在加,这里就直接上代码了

~ webpack.config.js

const path = require('path');

module.exports = function() {
  const dev = true;

  return {
    mode: dev ? 'development' : 'production',
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: dev ? 'axios.js' : 'axios.min.js',
      sourceMapFilename: dev ? 'axios.map' : 'axios.min.map',
      libraryTarget: 'umd',
    },
    devtool: 'source-map',
  };
};


这时候在src里面,建一个index.js,然后随便写点东西,像这样

然后终端执行webpack命令

当然了,现在肯定是不兼容的,要不咱们一开始也不用下babel了,咱们可以试试,比如我现在index.js加一句话

然后编译完可以看到结果也还是let,这肯定不行

好的,那么接下来就是配置babel,没什么可说的,这里直接放代码了,没什么可说的

const path = require('path');

module.exports = function() {
  const dev = true;

  return {
    mode: dev ? 'development' : 'production',
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: dev ? 'axios.js' : 'axios.min.js',
      sourceMapFilename: dev ? 'axios.map' : 'axios.min.map',
      libraryTarget: 'umd',
    },
    devtool: 'source-map',
    module: {
      rules: [
        {
          test: /\.js$/i,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        },
      ],
    },
  };
};

然后,大家肯定不希望每次修改手动的去webpack一下对吧?把webpack-dev-server引进来

~ webpack.config.js

const path = require('path');

module.exports = function() {
  const dev = true;

  return {
    mode: dev ? 'development' : 'production',
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: dev ? 'axios.js' : 'axios.min.js',
      sourceMapFilename: dev ? 'axios.map' : 'axios.min.map',
      libraryTarget: 'umd',
    },
    devtool: 'source-map',
    module: {
      rules: [
        {
          test: /\.js$/i,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        },
      ],
    },
    devServer: {
      port: 8000,
      open: true,
    },
  };
};

这时候,直接终端里运行webpack-dev-server的话其实他会自动去找全局的模块,这样不好,所以。。。你懂的

直接package.json里加上命令

~ package.json

{
  "name": "axios",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "webpack-dev-server"
  },
  "dependencies": {
    "@babel/core": "^7.7.7",
    "@babel/preset-env": "^7.7.7",
    "babel-loader": "^8.0.6",
    "webpack": "^4.41.5",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1"
  }
}

然后yarn start

这时候会弹出一个html

当然了,默认是去找根下的index.html,咱们没有,所以在根下建一个,然后引入咱们的axios.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>axios</title>
</head>
<body>
  <script src="/axios.js"></script>
</body>
</html>

刷新页面就会看到src/index.js里的alert生效了

并且 webpack-dev-server也是可以的,改了代码页面会自动刷新

然后,咱们就来配一下build

这里就不多废话了,直接上代码了

~ package.json

{
  "name": "axios",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "webpack-dev-server --env.dev",
    "build": "webpack --env.prod"
  },
  "dependencies": {
    "@babel/core": "^7.7.7",
    "@babel/preset-env": "^7.7.7",
    "babel-loader": "^8.0.6",
    "webpack": "^4.41.5",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1"
  }
}

~ webpack.config.json

const path = require('path');

module.exports = function(env={}) {
  const dev = env.dev;

  return {
    mode: dev ? 'development' : 'production',
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: dev ? 'axios.js' : 'axios.min.js',
      sourceMapFilename: dev ? 'axios.map' : 'axios.min.map',
      libraryTarget: 'umd',
    },
    devtool: 'source-map',
    module: {
      rules: [
        {
          test: /\.js$/i,
          use: {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        },
      ],
    },
    devServer: {
      port: 8000,
      open: true,
    },
  };
};

可以看到也是没问题的~

好的到这我本算是把webpack相关的东西搭的差不多了,接下来就要开始忙正事了~

Axios 项目代码

首先咱们先来建一个common.js,用来放公共的方法,先写一个断言

~ /src/common.js

export function assert(exp, msg = 'assert faild') {
  if (!exp) {
    throw new Error(msg);
  }
}

然后再建一个文件(个人习惯)/src/axios 主要的文件放在这

然后大家来看看axios正经怎么用,是不是可以直接 axios({...})或者axios.get等等

index.js引入直接写一下结果,把期望用法写上,然后来补充内部怎么写

~ index.js

import axios from './axios';

console.log(axios);
axios({ url: '1.txt', method: 'post' });
axios.get('1.txt', { headers: { aaa: 123 } });
axios.post(
  '1.txt',
  { data: 123 },
  {
    headers: {
      bbb: 123,
    },
  },
);

这时候就要考虑考虑了,咱们可以直接写函数,这个没问题,不过那样太散了,个人不喜欢,不过也是可以的,所以这里我就写成类了,由于改成类了那么输出出去的肯定得是一个实例,既然是实例的话,那么肯定也不能直接像函数一样直接()运行

没错,这时候就可以用到咱们的proxy了,jsclass里的constructor里是可以return东西的,如果对这东西不太熟,建议先去看看jsclass,这里就不多赘述,主要说明思想

简单来说,咱们可以return一个proxy对象,来代理咱们返回的结果,从而达到咱们既可以直接用class的方式写,用的时候也可以直接跟函数一样()调用

然后先来打印一下看看

这时候看页面的console,这时候可以看到axios就是一个proxy对象,像这样

这时候还能看到一个报错,因为咱们现在返回的是proxy对象,不是实例类了,没有get也是理所应当

可能有人会奇怪,为什么这个proxy监听的对象非要单独监听一个proxy函数呢,直接监听this不就行了么,注意,这其实是不行的,了解proxy的朋友应该知道,proxy你用什么监听创建的这事儿很重要,如果你监听的是一个对象,那还是不能直接调用,如果要是想直接像函数一样直接调用的话,那你监听的也必须是一个函数

像这样

然后咱们来解决一下get函数找不到的问题,来给proxy加一个方法, 很简单,可以给proxy加一个get方法,有人来找他,直接返回从我这个类上找,不就完了么,不过稍微要注意一下这个this,直接写this的话指向的是这个proxy

function request() {}

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      apply(fn, thisArg, args) {
        console.log(fn, thisArg, args);
      },
    });
  }

  get() {
    console.log('get');
  }

  post() {
    console.log('post');
  }

  delete() {}
}

let axios = new Axios();

export default axios;

这时候再看,就没有报错了,并且getpost也能console出来

这时候咱们就可以接着写数据请求了。。。。。吗? 远远不够

axios的参数有很多的多样性,咱们想来大概总结一下

axios('1.txt',{})
axios({
    url: '1.txt',
    method
})

axios.get('1.txt')
axios.get('1.txt',{})

axios.post....

等等吧,这一点,怎么才能把这么复杂多源的参数统一处理

第二就是axios可以非常深度的定制参数,可以全局也可以单独定制,拦截器,transfrom什么的,等等吧,默认值等等


参数

首先先来一个default,来定义一下这些默认值,这里稍微说一下这个X-Request-By算是一个不成文的规范,这也是普遍请求库都愿意做的事,方便后台去判断你这个请求是来自ajax还是from还是浏览器的url

function request() {}

const _default = {
  method: 'get',
  headers: {
    common: {
      'X-Request-By': 'XMLHttpRequest',
    },
    get: {},
    post: {},
    delete: {},
  },
};

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      apply(fn, thisArg, args) {
        console.log(fn, thisArg, args);
      },
    });
  }

  get() {
    console.log('get');
  }

  post() {
    console.log('post');
  }

  delete() {}
}

let axios = new Axios();

export default axios;

当然了,这里图简单,简单写着几个参数,自己喜欢可以再加很多东西,比如data的默认值等等,先够用,后续不够用再加


这时候,咱们来思考一下,这个default要加给谁,直接axios.default = _default么,当然不是,因为咱们这个axios到最后肯定是需要axios.create多个实例的,那么这时候就不行了,互相影响,prototype就更不用说了

其实也很简单,每次创建的时候从_default复制一份新的就可以了,直接JSON.parse(JSON.stringify(_default))包一下就可以了,这也是性能最高的方式,然后稍微来改造一下代码

function request() {}

const _default = {
  method: 'get',
  headers: {
    common: {
      'X-Request-By': 'XMLHttpRequest',
    },
    get: {},
    post: {},
    delete: {},
  },
};

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      apply(fn, thisArg, args) {
        console.log(fn, thisArg, args);
      },
    });
  }

  get() {
    console.log('get');
  }

  post() {
    console.log('post');
  }

  delete() {}
}

Axios.create = Axios.prototype.create = function() {
  let axios = new Axios();

  axios.default = JSON.parse(JSON.stringify(_default));

  return axios;
};

export default Axios.create();

这里是给原型和实例都加了一个create方法,因为咱们可能直接用axios.create()也可能直接用axios(),纯加静态方法或者实例方法都满足不了咱们的需求

这时候我们来实验一下,先来console一下axios.default

你会发现,undefined,这是为什么呢,在这里明明都已经添加了呀

因为这时候这个axios并不是一个对象,而是一个proxy,咱们还没给proxyset方法对不对,加什么都加不上,这时候来改造一下代码

function request() {}

const _default = {
  method: 'get',
  baseUrl: "",
  headers: {
    common: {
      'X-Request-By': 'XMLHttpRequest',
    },
    get: {},
    post: {},
    delete: {},
  },
};

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      set(data, name, val) {
        _this[name] = val;

        return true;
      },
      apply(fn, thisArg, args) {
        console.log(fn, thisArg, args);
      },
    });
  }

  get() {
    console.log('get');
  }

  post() {
    console.log('post');
  }

  delete() {}
}

Axios.create = Axios.prototype.create = function() {
  let axios = new Axios();

  axios.default = JSON.parse(JSON.stringify(_default));

  return axios;
};

export default Axios.create();

这时候再看浏览器,就会发现这个default就有了

以及咱们来create两个axios,改一下参数试一下

两个实例参数也不影响,这也是很好的第n步,这时候咱们就已经完成axios的四分之一了


咱们现在实例间是不影响了,不过咱们改改参数的时候绝不止直接axios.default.xxx这么改,咱们还应该有参数,像这样

这里咱们可以直接改造一下axios.create方法

~ axios.js

...
Axios.create = Axios.prototype.create = function(options={}) {
  let axios = new Axios();

  axios.default = {
    ...JSON.parse(JSON.stringify(_default)),
    ...options
  };

  return axios;
};

...

直接展开替换一下就行了对不对,但是,真的么?

假设咱们直接传了一个对象,里面有个headers的话是不是就直接给咱们的默认参数header整个就得替换了呀,那这个就不太好,当然了,这个也看咱们对自己这个库的需求,如果咱们就想这么做,也到是没啥毛病,问题如下

那么这时候,咱们就可以用一个小小的递归来搞定了

~ axios.js

...
Axios.create = Axios.prototype.create = function(options = {}) {
  let axios = new Axios();

  let res = { ...JSON.parse(JSON.stringify(_default)) };
  function merge(dest, src) {
      for (let name in src) {
        if (typeof src[name] == 'object') {
          if (!dest[name]) {
            dest[name] = {};
          }
    
          merge(dest[name], src[name]);
        } else {
          dest[name] = src[name];
        }
      }
    }

  merge(res, options);

  axios.default = res;
  return axios;
};
...

这时候再看,就没问题了


代码整理拆分

接下来,咱们先不急着去写请求的参数,咱们先把这个代码稍微规划规划,整理整理,毕竟全放在一个文件里,这个以后就没法维护了

目前拆分可以分几点

  1. default 是不是可以用一个单独的文件来装
  2. 这个merge函数肯定是公用的,可以放在咱们的common.js
  3. 这个request也应该单独放在一个js里来定义

废话不多说,直接上代码

~ request.js

export default function request() {
  
}

~ default.js

export default {
  method: 'get',
  baseUrl: '',
  headers: {
    common: {
      'X-Request-By': 'XMLHttpRequest',
    },
    get: {},
    post: {},
    delete: {},
  },
};

~ common.js

export function assert(exp, msg = 'assert faild') {
  if (!exp) {
    throw new Error(msg);
  }
}

export function merge(dest, src) {
  for (let name in src) {
    if (typeof src[name] == 'object') {
      if (!dest[name]) {
        dest[name] = {};
      }

      merge(dest[name], src[name]);
    } else {
      dest[name] = src[name];
    }
  }
}

~ axios.js

import _default from './default';
import { merge } from './common';
import request from './request';

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      set(data, name, val) {
        _this[name] = val;

        return true;
      },
      apply(fn, thisArg, args) {
        console.log(fn, thisArg, args);
      },
    });
  }

  get() {
    console.log('get');
  }

  post() {
    console.log('post');
  }

  delete() {}
}

Axios.create = Axios.prototype.create = function(options = {}) {
  let axios = new Axios();

  let res = { ...JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

这时候就感觉干净不少了,对不对


处理请求参数

在写之前,咱们先来看看axios都有哪些支持的写法,然后再去考虑怎么写

大概来看 除了那个总的axios({...})这三种方法是不是差不多呀,当然了,axios里东西太多了,这里就简单实现这三个,说明问题为主,大家有兴趣可以自己加,无非是体力活

当然,可以看到axios参数情况还是蛮多的,这时候咱们应该直接统一处理,不管传过来什么参数,咱们都返回一个axios({})这种,最终统一处理,这不是方便么

这里直接来先判断一下前两种情况

你会发现前两种情况除了这个method都是一样的,那这个咱们就可以抽出方法统一处理

~ axios.js

import _default from './default';
import { merge } from './common';
import request from './request';

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      set(data, name, val) {
        _this[name] = val;

        return true;
      },
      apply(fn, thisArg, args) {
        console.log(fn, thisArg, args);
      },
    });
  }

  _preprocessArgs(method, ...args) {
    let options;
    if (args.length == 1 && typeof args[0] == 'string') {
      options = { method, url: args[0] };
    } else if (args.length == 1 && args[0].constructor == Object) {
      options = {
        ...args[0],
        method,
      };
    } else {
      return undefined;
    }
    return options;
  }

  get(...args) {
    let options = this._preprocessArgs('get', args);

    if (!options) {
    }
  }

  post(...args) {
    let options = this._preprocessArgs('post', args);

    if (!options) {
    }
  }

  delete(...args) {
    let options = this._preprocessArgs('delete', args);

    if (!options) {
    }
  }
}

Axios.create = Axios.prototype.create = function(options = {}) {
  let axios = new Axios();

  let res = { ...JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

然后这时候,咱们以封装一个库的视角,肯定需要对参数进行各种各样的校验,类型等等,不对的话给他一个正经的报错,帮助使用者来调试

咱们之前在common.js里写的assert这时候就用上了

...
get(...args) {
    let options = this._preprocessArgs('get', args);

    if (!options) {
      assert(typeof args[0] == 'string', 'args[0] must is string');
      assert(
        typeof args[1] == 'object' && args[1] && args[1].constructor == Object,
        'args[1] must is JSON',
      );

      // ...
    }
  }

  post(...args) {
    let options = this._preprocessArgs('post', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
      } else if (args.length == 3) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[1] == 'object' &&
            args[1] &&
            args[1].constructor == Object,
          'args[1] must is JSON',
        );
      } else {
        assert(false, 'invaild argments');
      }
    }
  }

  delete(...args) {
    let options = this._preprocessArgs('delete', args);

    if (!options) {
      assert(typeof args[0] == 'string', 'args[0] must is string');
      assert(
        typeof args[1] == 'object' && args[1] && args[1].constructor == Object,
        'args[1] must is JSON',
      );
      
    }
  }
}
...

这里的规则对应着上面写的axios使用方式,大体来说也差不多,把这些参数都校验好了,接下来,咱们就可以写具体的个性化的处理了

顺便一说,这个地方当然也是可以重用的,不过没必要,搞了一通其实也没减少多少代码,并且贼乱,也看个人,大家不喜欢可以自己修改

然后咱们再来处理一下这个options,并且console一下

~ axios.js

import _default from './default';
import { merge, assert } from './common';
import request from './request';

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      set(data, name, val) {
        _this[name] = val;

        return true;
      },
      apply(fn, thisArg, args) {
        console.log(fn, thisArg, args);
      },
    });
  }

  _preprocessArgs(method, args) {
    let options;
    if (args.length == 1 && typeof args[0] == 'string') {
      options = { method, url: args[0] };
    } else if (args.length == 1 && args[0].constructor == Object) {
      options = {
        ...args[0],
        method,
      };
    } else {
      return undefined;
    }

    console.log(options);
    return options;
  }

  get(...args) {
    let options = this._preprocessArgs('get', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[1] == 'object' &&
            args[1] &&
            args[1].constructor == Object,
          'args[1] must is JSON',
        );

        options = {
          ...args[1],
          url: args[0],
          method: 'get',
        };
        console.log(options);
      } else {
        assert(false, 'invaild args');
      }
    }
  }

  post(...args) {
    let options = this._preprocessArgs('post', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        options = {
          url: args[0],
          data: args[1],
          method: 'post',
        };

        console.log(options);
      } else if (args.length == 3) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[2] == 'object' &&
            args[2] &&
            args[2].constructor == Object,
          'args[2] must is JSON',
        );
        options = {
          ...args[2],
          url: args[0],
          data: args[1],
          method: 'post',
        };
        console.log(options);
      } else {
        assert(false, 'invaild argments');
      }
    }
  }

  delete(...args) {
    let options = this._preprocessArgs('delete', args);

    if (!options) {
      assert(typeof args[0] == 'string', 'args[0] must is string');
      assert(
        typeof args[1] == 'object' && args[1] && args[1].constructor == Object,
        'args[1] must is JSON',
      );

      options = {
        ...args[1],
        url: args[0],
        method: 'get',
      };

      console.log(options);
    }
  }
}

Axios.create = Axios.prototype.create = function(options = {}) {
  let axios = new Axios();

  let res = { ...JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

这时候咱们来测试一下

~ index.js

import Axios from './axios';

Axios.get('1.json');
Axios.get('1.json', { headers: { a: 12 } });

Axios.post('1.php');
Axios.post('1.php', { a: 12, b: 5 });
Axios.post('1.php', [12, 5, 6]);

let form = new FormData();
Axios.post('1.txt', form);
Axios.post('1.txt', 'dw1ewdq');

Axios.post('1.json', form, { headers: { a: 213, b: 132 } });

Axios.delete('1.json');
Axios.delete('1.json', { parmas: { id: 1 } });

这时候可以看到,妥妥的对不对?

然后呢。。。还没忘,咱们还需要处理直接apply的情况,也就是直接Axios()这么调用的时候

不废话,直接上代码,跟get其实差不多

~ axios.js

import _default from './default';
import { merge, assert } from './common';
import request from './request';

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      set(data, name, val) {
        _this[name] = val;

        return true;
      },
      apply(fn, thisArg, args) {
        let options = _this._preprocessArgs(undefined, args);
        if (!options) {
          if (args.length == 2) {
            assert(typeof args[0] == 'string', 'args[0] must is string');
            assert(
              typeof args[1] == 'object' &&
                args[1] &&
                args[1].constructor == Object,
              'args[1] must is JSON',
            );

            options = {
              ...args[1],
              url: args[0],
            };
            console.log(options);
          } else {
            assert(false, 'invaild args');
          }
        }
      },
    });
  }

  _preprocessArgs(method, args) {
    let options;
    if (args.length == 1 && typeof args[0] == 'string') {
      options = { method, url: args[0] };
    } else if (args.length == 1 && args[0].constructor == Object) {
      options = {
        ...args[0],
        method,
      };
    } else {
      return undefined;
    }

    console.log(options);
    return options;
  }

  get(...args) {
    let options = this._preprocessArgs('get', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[1] == 'object' &&
            args[1] &&
            args[1].constructor == Object,
          'args[1] must is JSON',
        );

        options = {
          ...args[1],
          url: args[0],
          method: 'get',
        };
        console.log(options);
      } else {
        assert(false, 'invaild args');
      }
    }
  }

  post(...args) {
    let options = this._preprocessArgs('post', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        options = {
          url: args[0],
          data: args[1],
          method: 'post',
        };

        console.log(options);
      } else if (args.length == 3) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[2] == 'object' &&
            args[2] &&
            args[2].constructor == Object,
          'args[2] must is JSON',
        );
        options = {
          ...args[2],
          url: args[0],
          data: args[1],
          method: 'post',
        };
        console.log(options);
      } else {
        assert(false, 'invaild argments');
      }
    }
  }

  delete(...args) {
    let options = this._preprocessArgs('delete', args);

    if (!options) {
      assert(typeof args[0] == 'string', 'args[0] must is string');
      assert(
        typeof args[1] == 'object' && args[1] && args[1].constructor == Object,
        'args[1] must is JSON',
      );

      options = {
        ...args[1],
        url: args[0],
        method: 'get',
      };

      console.log(options);
    }
  }
}

Axios.create = Axios.prototype.create = function(options = {}) {
  let axios = new Axios();

  let res = { ...JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

然后来测试一下

没问题,当然为什么methodundefined,因为这时候还没走到咱们的default呢,咱们是在这决定默认请求值的,所以这里直接给个undefined就行,然后咱们应该把些options全都和defaulft处理完,丢给咱们的request函数去请求

而这个方法肯定所有请求都需要,所以咱们写一个公共方法

这个request方法主要干的事四件事

  1. 跟this.default进行合并
  2. 检测参数是否正确
  3. baseUrl 合并请求
  4. 正式调用request(options)
import _default from './default';
import { merge, assert } from './common';
import request from './request';

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      set(data, name, val) {
        _this[name] = val;

        return true;
      },
      apply(fn, thisArg, args) {
        let options = _this._preprocessArgs(undefined, args);
        if (!options) {
          if (args.length == 2) {
            assert(typeof args[0] == 'string', 'args[0] must is string');
            assert(
              typeof args[1] == 'object' &&
                args[1] &&
                args[1].constructor == Object,
              'args[1] must is JSON',
            );

            options = {
              ...args[1],
              url: args[0],
            };
            _this.request(options);
          } else {
            assert(false, 'invaild args');
          }
        }
      },
    });
  }

  _preprocessArgs(method, args) {
    let options;
    if (args.length == 1 && typeof args[0] == 'string') {
      options = { method, url: args[0] };
      this.request(options);
    } else if (args.length == 1 && args[0].constructor == Object) {
      options = {
        ...args[0],
        method,
      };
      this.request(options);
    } else {
      return undefined;
    }

    return options;
  }

  request(options) {
    console.log(options, 'request');
    // 1. 跟this.default进行合并
    // 2. 检测参数是否正确
    // 3. baseUrl 合并请求
    // 4. 正式调用request(options)
  }

  get(...args) {
    let options = this._preprocessArgs('get', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[1] == 'object' &&
            args[1] &&
            args[1].constructor == Object,
          'args[1] must is JSON',
        );

        options = {
          ...args[1],
          url: args[0],
          method: 'get',
        };
        this.request(options);
      } else {
        assert(false, 'invaild args');
      }
    }
  }

  post(...args) {
    let options = this._preprocessArgs('post', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        options = {
          url: args[0],
          data: args[1],
          method: 'post',
        };
        this.request(options);
      } else if (args.length == 3) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[2] == 'object' &&
            args[2] &&
            args[2].constructor == Object,
          'args[2] must is JSON',
        );
        options = {
          ...args[2],
          url: args[0],
          data: args[1],
          method: 'post',
        };
        this.request(options);
      } else {
        assert(false, 'invaild argments');
      }
    }
  }

  delete(...args) {
    let options = this._preprocessArgs('delete', args);

    if (!options) {
      assert(typeof args[0] == 'string', 'args[0] must is string');
      assert(
        typeof args[1] == 'object' && args[1] && args[1].constructor == Object,
        'args[1] must is JSON',
      );

      options = {
        ...args[1],
        url: args[0],
        method: 'get',
      };
      this.request(options);
    }
  }
}

Axios.create = Axios.prototype.create = function(options = {}) {
  let axios = new Axios();

  let res = { ...JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

这个合并很简单,咱们之前写的merge函数又派上用场了,修改一下代码

...
request(options) {
    console.log(options);
    // 1. 跟this.default进行合并
    merge(options, this.default);

    console.log(options);
    // 2. 检测参数是否正确
    // 3. baseUrl 合并请求
    // 4. 正式调用request(options)
  }
  ...

这时候可以看到,合并前和合并后数据就已经都有了,但是,这时候咱们的header就不应该是全部都有了,应该根据传过来的method是什么来把对应的方式的headercommon合并一下

request(options) {
    // 1. 跟this.default进行合并
    let _headers = this.default.headers;
    delete this.default.headers;
    merge(options, this.default);
    this.default.headers = _headers;
    //合并头
    // this.default.headers.common -> this.default.headers.get -> options
    let headers = {};
    merge(headers, this.default.headers.common);
    merge(headers, this.default.headers[options.method.toLowerCase()]);
    merge(headers, options.headers);

    console.log(headers);
    console.log(options);

    // 2. 检测参数是否正确
    // 3. baseUrl 合并请求
    // 4. 正式调用request(options)
  }

这里比较乱,所以咱们先来捋一捋

咱们目的是要让header合并,不过合并的话就会有点小问题,之前在们在default的里定义的commonget...也都会被复制过来,如果要是咱们用if判断 options.header.common == this.default.headers.common然后delete的话,这时候你会发现不行,因为咱们也知道,你直接写两个对象判断就相当于直接new了两个对象,那这时候判断肯定不相等,那么咱们是在什么时候复制的呢

就在咱们封装的merge里,以及还有很多地方都动过这个东西

然后,咱们应该找到这个东西到底在什么时候就不一样了,其实也就是咱们这个request函数里第一次merge的时候

所以咱们这里玩了一个小技巧,因为common这些东西在底下都已经手动搞了,所以这里不需要他复制进来,所以先delete了一下

让他在之前就进不去,delete之后再给拿回去,两头不耽误,真好~

最后,咱们把headers赋值到咱们的options.headers

request(options) {
    // 1. 合并头
    let _headers = this.default.headers;
    delete this.default.headers;
    merge(options, this.default);
    this.default.headers = _headers;

    // this.default.headers.common -> this.default.headers.get -> options
    let headers = {};
    merge(headers, this.default.headers.common);
    merge(headers, this.default.headers[options.method.toLowerCase()]);
    merge(headers, options.headers);

    options.headers = headers;
    console.log(options);

    // 3. baseUrl 合并请求
    

    // 4. 正式调用request(options)
  }

~ index.js

import Axios from './axios';

Axios('1.php');
Axios({
  url: '2.php',
  params: { a: 12, b: 3 },
  headers: {
    a: 12,
  },
});

测试一下结果

可以看到,没毛病~

然后咱们再来看一下第二步,其实这个校验咱们可以写的非常非常多值得校验的事儿,但是这里只说明意思,写几个就算,大家有兴趣可以多补一些

...
assert(options.method, 'no method');
assert(typeof options.method == 'string', 'method must be string');
assert(options.url, 'no url');
assert(typeof options.url == 'string', 'url must be string');
...

第三步也是直接上代码了

~ axios.js

options.url=options.baseUrl+options.url;
delete options.baseUrl; 

~ common.js

export function assert(exp, msg = 'assert faild') {
  if (!exp) {
    throw new Error(msg);
  }
}

export function merge(dest, src) {
  for (let name in src) {
    if (typeof src[name] == 'object') {
      if (!dest[name]) {
        dest[name] = {};
      }

      merge(dest[name], src[name]);
    } else {
      if (dest[name] === undefined) {
        dest[name] = src[name];
      }
    }
  }
}

~ index.js

import Axios from './axios';

Axios('1.php', {
  baseUrl: 'http://www.baidu.com/',
  headers: {
    a: 12,
  },
});
Ï

这时候再测试一下,可以看到,就没问题了

这里说明一下为什么要改一下merge,加了一个判断,因为咱们之前是直接替换掉,有没有都替换,这肯定不行,不加上的话会把咱们的baseUrl干了

当然了,还有个小事儿咱们需要处理一下,如果用你这个库的人脑子有病(当然,不考虑脑子有病的也可以),他写路径的时候是这么写的

你这个是不是又不行了呀,很简单,NodeJS里有一个url包,可以直接引过来用,webpack会帮你把他打包起来,不过有一点需要注意,webpack不是所有的东西都能打包的,比如fs模块,甚至一些底层功能用cc++写的系统包这肯定就不行,不过一个url问题不大

~ axios.js

import _default from './default';
import { merge, assert } from './common';
import request from './request';
const urlLib = require('url');

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      set(data, name, val) {
        _this[name] = val;

        return true;
      },
      apply(fn, thisArg, args) {
        let options = _this._preprocessArgs(undefined, args);
        if (!options) {
          if (args.length == 2) {
            assert(typeof args[0] == 'string', 'args[0] must is string');
            assert(
              typeof args[1] == 'object' &&
                args[1] &&
                args[1].constructor == Object,
              'args[1] must is JSON',
            );

            options = {
              ...args[1],
              url: args[0],
            };
            _this.request(options);
          } else {
            assert(false, 'invaild args');
          }
        }
      },
    });
  }

  _preprocessArgs(method, args) {
    let options;
    if (args.length == 1 && typeof args[0] == 'string') {
      options = { method, url: args[0] };
      this.request(options);
    } else if (args.length == 1 && args[0].constructor == Object) {
      options = {
        ...args[0],
        method,
      };
      this.request(options);
    } else {
      return undefined;
    }

    return options;
  }

  request(options) {
    // 1. 合并头
    let _headers = this.default.headers;
    delete this.default.headers;
    merge(options, this.default);
    this.default.headers = _headers;

    // this.default.headers.common -> this.default.headers.get -> options
    let headers = {};
    merge(headers, this.default.headers.common);
    merge(headers, this.default.headers[options.method.toLowerCase()]);
    merge(headers, options.headers);

    options.headers = headers;
    console.log(options);

    // 2. 检测参数是否正确
    assert(options.method, 'no method');
    assert(typeof options.method == 'string', 'method must be string');
    assert(options.url, 'no url');
    assert(typeof options.url == 'string', 'url must be string');

    // 3. baseUrl 合并请求
    options.url = urlLib.resolve(options.baseUrl, options.url);
    delete options.baseUrl;

    // 4. 正式调用request(options)
    request(options);
  }

  get(...args) {
    let options = this._preprocessArgs('get', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[1] == 'object' &&
            args[1] &&
            args[1].constructor == Object,
          'args[1] must is JSON',
        );

        options = {
          ...args[1],
          url: args[0],
          method: 'get',
        };
        this.request(options);
      } else {
        assert(false, 'invaild args');
      }
    }
  }

  post(...args) {
    let options = this._preprocessArgs('post', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        options = {
          url: args[0],
          data: args[1],
          method: 'post',
        };
        this.request(options);
      } else if (args.length == 3) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[2] == 'object' &&
            args[2] &&
            args[2].constructor == Object,
          'args[2] must is JSON',
        );
        options = {
          ...args[2],
          url: args[0],
          data: args[1],
          method: 'post',
        };
        this.request(options);
      } else {
        assert(false, 'invaild argments');
      }
    }
  }

  delete(...args) {
    let options = this._preprocessArgs('delete', args);

    if (!options) {
      assert(typeof args[0] == 'string', 'args[0] must is string');
      assert(
        typeof args[1] == 'object' && args[1] && args[1].constructor == Object,
        'args[1] must is JSON',
      );

      options = {
        ...args[1],
        url: args[0],
        method: 'get',
      };
      this.request(options);
    }
  }
}

Axios.create = Axios.prototype.create = function(options = {}) {
  let axios = new Axios();

  let res = { ...JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

这时候再测试一下

就没问题了

merge 问题

其实咱们这个merge现在还有很大的问题,因为咱们最开始的想要的功能和咱们现在的功能有差入

咱们现在是被迫写了一个if,改成了查漏补缺,那么其实后面有优先级的东西顺序应该都反过来

咱们最开始的需求是什么,是想让这个dest优先级最低,是可以被别人覆盖的,但是现在写了这个if之后,变成他优先级最高了,那这个就不对,但是还不能去掉,去掉了之后这个合并就又出问题了

其实应该怎么做,应该把这两个东西顺序颠倒一下,但是这时候又不对了,因为就导致这个this.default变了,这时候又需要复制一份,咱们来写一个公共的方法放到common.js

~ common.js

...

export function clone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

以及咱们这个顺序稍微颠倒一下,然后数据克隆一下

这个时候来测试一下

会发现是不是header里的这些东西又回来了呀,原因也很简单,因为咱们在上面整个的defaultclone下来了,所以咱们把delete这一块往上提提

这时候就没问题了


request

这个时候咱们就应该来写第四步了 直接把options给到咱们的request函数里就可以,零零碎碎的问题全都给它处理好了

然后来修改一下request.js

~ request.js

export default function request(options) {
  console.log(options);
  let xhr = new XMLHttpRequest();

  xhr.open(options.method, options.url, true);

  for (let name in options.headers) {
    xhr.setRequestHeader(name, options.headers[name]);
  }

  xhr.send(options.data);
}

暂时先写一个简单的请求,然后咱们来测试一下看看能不能发出去

首先先建一个txt,方便咱们测试,我就放到data目录里了,像这样

然后改一下index.js

~ index.js

import Axios from './axios';

Axios('/data/1.txt', {
  headers: {
    a: 12,
  },
});

这时候咱们可以看到,header是加上了的,并且返回的东西也对

当然这个东西还没完,也是一个防止用户捣乱的事

如果用户给你的header是这样的东西,那这个就不太好,所以最好还是给它来一个编码

~ request.js

export default function request(options) {
  console.log(options);
  let xhr = new XMLHttpRequest();

  xhr.open(options.method, options.url, true);

  for (let name in options.headers) {
    xhr.setRequestHeader(
      encodeURIComponent(name),
      encodeURIComponent(options.headers[name]),
    );
  }

  xhr.send(options.data);
}

这就没问题了万一后台有点问题,一碰见冒号就算结束这种问题就能避免了

然后咱们用的时候肯定更多的时候是需要一个asyncawait一下,所以咱们需要用Promise来包一下

~ axios.js

export default function request(options) {
  console.log(options);
  let xhr = new XMLHttpRequest();

  xhr.open(options.method, options.url, true);

  for (let name in options.headers) {
    xhr.setRequestHeader(
      encodeURIComponent(name),
      encodeURIComponent(options.headers[name]),
    );
  }

  xhr.send(options.data);

  return new Promise((resolve, reject) => {
    xhr.onreadystatechange = function() {
      if (xhr.readyState == 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          resolve(xhr);
        } else {
          reject(xhr);
        }
      }
    };
  });
}

以及这个时候,咱们还有很多很多问题

  1. 304其实也算成功,那这么封装也是就不对,而且用户可能有一些自定义的code,这个怎么做到配置
  2. 咱们目前的webpack只能兼容es6asyncawait是兼容不了的,这个该怎么配

咱们先来解决webpack的问题,这个其实很简单,咱们需要再按一个包 yarn add @babel/polyfill

然后打开webpack.config.js修改一下entry

~ webpack.config.js

...
entry: ['@babel/polyfill','./src/index.js'],
...

注意这个顺序是不能颠倒的

这样就可以兼容了,然后咱们再来修改一下index.js

import Axios from './axios';

(async () => {
  let res = await Axios('/data/1.txt', {
    headers: {
      a: 12,
      b: '321fho:fdsf vfds; : ',
    },
  });

  console.log(res);
})();

可以看到结果是undefined,这是因为咱们根本没有返回咱们的处理结果

这时候修改一下axios.js

import _default from './default';
import { merge, assert, clone } from './common';
import request from './request';
const urlLib = require('url');

class Axios {
  constructor() {
    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      set(data, name, val) {
        _this[name] = val;

        return true;
      },
      apply(fn, thisArg, args) {
        let options = _this._preprocessArgs(undefined, args);
        if (!options) {
          if (args.length == 2) {
            assert(typeof args[0] == 'string', 'args[0] must is string');
            assert(
              typeof args[1] == 'object' &&
                args[1] &&
                args[1].constructor == Object,
              'args[1] must is JSON',
            );

            options = {
              ...args[1],
              url: args[0],
            };
            return _this.request(options);
          } else {
            assert(false, 'invaild args');
          }
        }
      },
    });
  }

  _preprocessArgs(method, args) {
    let options;
    if (args.length == 1 && typeof args[0] == 'string') {
      options = { method, url: args[0] };
      this.request(options);
    } else if (args.length == 1 && args[0].constructor == Object) {
      options = {
        ...args[0],
        method,
      };
      this.request(options);
    } else {
      return undefined;
    }

    return options;
  }

  request(options) {
    // 1. 合并头
    let _headers = this.default.headers;
    delete this.default.headers;

    let result = clone(this.default);
    merge(result, this.default);
    merge(result, options);
    this.default.headers = _headers;

    options = result;

    // this.default.headers.common -> this.default.headers.get -> options
    let headers = {};
    merge(headers, this.default.headers.common);
    merge(headers, this.default.headers[options.method.toLowerCase()]);
    merge(headers, options.headers);

    options.headers = headers;

    // 2. 检测参数是否正确
    assert(options.method, 'no method');
    assert(typeof options.method == 'string', 'method must be string');
    assert(options.url, 'no url');
    assert(typeof options.url == 'string', 'url must be string');

    // 3. baseUrl 合并请求
    options.url = urlLib.resolve(options.baseUrl, options.url);
    delete options.baseUrl;

    // 4. 正式调用request(options)
    return request(options);
  }

  get(...args) {
    let options = this._preprocessArgs('get', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[1] == 'object' &&
            args[1] &&
            args[1].constructor == Object,
          'args[1] must is JSON',
        );

        options = {
          ...args[1],
          url: args[0],
          method: 'get',
        };
        return this.request(options);
      } else {
        assert(false, 'invaild args');
      }
    }
  }

  post(...args) {
    let options = this._preprocessArgs('post', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        options = {
          url: args[0],
          data: args[1],
          method: 'post',
        };
        return this.request(options);
      } else if (args.length == 3) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[2] == 'object' &&
            args[2] &&
            args[2].constructor == Object,
          'args[2] must is JSON',
        );
        options = {
          ...args[2],
          url: args[0],
          data: args[1],
          method: 'post',
        };
        return this.request(options);
      } else {
        assert(false, 'invaild argments');
      }
    }
  }

  delete(...args) {
    let options = this._preprocessArgs('delete', args);

    if (!options) {
      assert(typeof args[0] == 'string', 'args[0] must is string');
      assert(
        typeof args[1] == 'object' && args[1] && args[1].constructor == Object,
        'args[1] must is JSON',
      );

      options = {
        ...args[1],
        url: args[0],
        method: 'get',
      };
      return this.request(options);
    }
  }
}

Axios.create = Axios.prototype.create = function(options = {}) {
  let axios = new Axios();

  let res = { ...JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

这时候咱们再看结果,是不是就可以了,当然了,咱们肯定不能就直接把原始的xml对象返回回去,咱们还要对返回的数据进行各种处理


处理数据

咱们先来简单的改一下axios.js 下的request

返回又一个promise,这时候可以看到结果,一点毛病没有

但是咱们这个东西全放在axios.js里就太乱了,所以咱们也单独把它拆除去

建两个文件一个是/src/response.js一个是/src/error.js

然后这里axios.js引入一下,并且处理的时候分别交给他们

然后response.js里直接返回值就可以了

不过这个headers稍微有点特别,需要单独调一个方法xhr.getAllResponseHeaders(),不过这返回的是原始xhr的头,这肯定不行,所以咱们需要来切一下

~ response.js

export default function(xhr) {
  let arr = xhr.getAllResponseHeaders().split('\r\n');
  let headers = {};

  arr.forEach(str => {
    if (!str) return;
    let [name, val] = str.split(': ');
    headers[name] = val;
  });

  return {
    ok: true,
    status: xhr.status,
    statusText: xhr.statusText,
    data: xhr.response,
    headers,
    xhr,
  };
}

这样就行了

transformRequest && transformResponse

这时候当然还不算完,因为咱们现在的data还没有任何处理,所以肯定都是字符串,以及用户可能自定义这个处理方式,熟悉axios的朋友应该知道,axiostransformRequesttransformResponse方法

这时候咱们先来改一下之前axios.js里的request方法

现在需要在第三步和第四步之间来处理一下请求

先把参数打印一下,然后修改一下index.js的测试demo

为了测试方便我把1.txt改成1.json了,方便咱们一会处理json数据好看到效果

可以看到,这个参数是可以拿到的,那么接下来就比较简单了,直接来

这时候看请求,这个headers就加上了

这里稍微提一嘴,为什么我要delete掉,虽然不delete没什么关系,但是我希望我这个request保持干净

至于这个自定义返回结果,是不是就更简单了呀

这时候可以看眼结果,我没传transformResponse,结果是这样

这时候就可以了

当然了,咱们现在已经可以很灵活的运用了,不止单传这个json里可以配参数,全局配置统一处理一样是可以的,咱们来试试

以及不同的实例之间,都是可以的


拦截器

拦截器在一个请求库里肯定是必不可少的,其实咱们这个库写到现在,想加一个这玩意,其实是很容易的

~ index.js

import Axios from './axios';

Axios.interceptors.request.use(function(config) {
  config.headers.interceptors = 'true';
  return config;
});

(async () => {
  let res = await Axios('/data/1.json', {
    headers: {
      a: 12,
      b: '321fho:fdsf vfds; : ',
    },
  });

  console.log(res);
})();

然后新建一个interceptor.js

~interceptor.js

class Interceptor {
  constructor() {
    this._list = [];
  }

  use(fn) {
    this._list.push(fn);
  }

  list() {
    return this._list;
  }
}


export default Interceptor;

~ axios.js

import _default from './default';
import { merge, assert, clone } from './common';
import request from './request';
import createResponse from './response';
import createError from './error';
const urlLib = require('url');
import Interceptor from './interceptor';

class Axios {
  constructor() {
    this.interceptors = {
      request: new Interceptor(),
      response: new Interceptor(),
    };

    let _this = this;
    return new Proxy(request, {
      get(data, name) {
        return _this[name];
      },
      set(data, name, val) {
        _this[name] = val;

        return true;
      },
      apply(fn, thisArg, args) {
        let options = _this._preprocessArgs(undefined, args);
        if (!options) {
          if (args.length == 2) {
            assert(typeof args[0] == 'string', 'args[0] must is string');
            assert(
              typeof args[1] == 'object' &&
                args[1] &&
                args[1].constructor == Object,
              'args[1] must is JSON',
            );

            options = {
              ...args[1],
              url: args[0],
            };
            return _this.request(options);
          } else {
            assert(false, 'invaild args');
          }
        }
      },
    });
  }

  _preprocessArgs(method, args) {
    let options;
    if (args.length == 1 && typeof args[0] == 'string') {
      options = { method, url: args[0] };
      this.request(options);
    } else if (args.length == 1 && args[0].constructor == Object) {
      options = {
        ...args[0],
        method,
      };
      this.request(options);
    } else {
      return undefined;
    }

    return options;
  }

  request(options) {
    // 1. 合并头
    let _headers = this.default.headers;
    delete this.default.headers;

    let result = clone(this.default);
    merge(result, this.default);
    merge(result, options);
    this.default.headers = _headers;

    options = result;

    // this.default.headers.common -> this.default.headers.get -> options
    let headers = {};
    merge(headers, this.default.headers.common);
    merge(headers, this.default.headers[options.method.toLowerCase()]);
    merge(headers, options.headers);

    options.headers = headers;

    // 2. 检测参数是否正确
    assert(options.method, 'no method');
    assert(typeof options.method == 'string', 'method must be string');
    assert(options.url, 'no url');
    assert(typeof options.url == 'string', 'url must be string');

    // 3. baseUrl 合并请求
    options.url = urlLib.resolve(options.baseUrl, options.url);
    delete options.baseUrl;

    // 4. 变换一下请求
    const { transformRequest, transformResponse } = options;
    delete options.transformRequest;
    delete options.transformResponse;

    if (transformRequest) options = transformRequest(options);

    let list = this.interceptors.request.list();
    list.forEach(fn => {
      options = fn(options);
    });

    // 5. 正式调用request(options)
    return new Promise((resolve, reject) => {
      return request(options).then(
        xhr => {
          let res = createResponse(xhr);
          if (transformResponse) res = transformResponse(res);
          
          let list = this.interceptors.response.list();
          list.forEach(fn => {
            res = fn(res);
          });
          resolve(res);
        },
        xhr => {
          let err = createError(xhr);
          reject(err);
        },
      );
    });
  }

  get(...args) {
    let options = this._preprocessArgs('get', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[1] == 'object' &&
            args[1] &&
            args[1].constructor == Object,
          'args[1] must is JSON',
        );

        options = {
          ...args[1],
          url: args[0],
          method: 'get',
        };
        return this.request(options);
      } else {
        assert(false, 'invaild args');
      }
    }
  }

  post(...args) {
    let options = this._preprocessArgs('post', args);

    if (!options) {
      if (args.length == 2) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        options = {
          url: args[0],
          data: args[1],
          method: 'post',
        };
        return this.request(options);
      } else if (args.length == 3) {
        assert(typeof args[0] == 'string', 'args[0] must is string');
        assert(
          typeof args[2] == 'object' &&
            args[2] &&
            args[2].constructor == Object,
          'args[2] must is JSON',
        );
        options = {
          ...args[2],
          url: args[0],
          data: args[1],
          method: 'post',
        };
        return this.request(options);
      } else {
        assert(false, 'invaild argments');
      }
    }
  }

  delete(...args) {
    let options = this._preprocessArgs('delete', args);

    if (!options) {
      assert(typeof args[0] == 'string', 'args[0] must is string');
      assert(
        typeof args[1] == 'object' && args[1] && args[1].constructor == Object,
        'args[1] must is JSON',
      );

      options = {
        ...args[1],
        url: args[0],
        method: 'get',
      };
      return this.request(options);
    }
  }
}

Axios.create = Axios.prototype.create = function(options = {}) {
  let axios = new Axios();

  let res = clone(_default);

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

可以看到,基本上是一样的套路,主要就是把参数传过来然后调用一下而已

不过这里面有两个小小的问题需要处理

  1. 咱们现在给用户开了很多口,如果他要是返回的config不对或者没返回咱们理应给他返回错误信息,再校验一下,这时候大家应该能想到了,应该吧axios校验这些参数单独搞一个函数,没什么技术含量,这里就不多赘述,大家有兴趣可以试一下
  2. 咱们给用户的是函数,用的是forEach,这时候就会导致一个问题,如果用户给你的是一个带async的函数那就不行了,咱们也要加上asyncawait,不过asyncawait里面返回一个promise又很怪,这个大家有兴趣可以自己试试,或者评论区留言

这时候来试一下效果

可以看到咱们这个拦截器也算是完成了

总结

本篇最终代码已上传github,链接如下 github.com/Mikey-9/axi…

还是那句话,咱们本篇文章主要不是要完整的实现axios,而是实现一个这样的库的思想,当然其中也有很多问题,欢迎大家在评论区留言或者可以加我的qq或者微信一起交流

篇幅略长,感谢观看