preface

This article needs to have a basic understanding of JS, ES6, Webpack, network request and other basic knowledge

I’m sure you’ve all used Axios or some other library for web requests, ajax, fly.js, etc. I’ve only used seven or eight libraries, all of which are similar

This article is not intended to implement everything axios does word for word, it doesn’t make sense, but it will. The main point is to get a feel for the flow and structure, how the project is extensible, testable, readable, etc

Don’t talk nonsense, open do ~

Set up the project

The old rule is to create an empty directory, then open the command line to execute

yarn init -y

or

cnpm init -y

webpack

Yarn Global add webpack webpack- CLI – yarn Global add webpack webpack- CLI

Installing dependency packages

These packages are mainly needed to execute commands, help compile and debug, and Babel helps to be compatible with browsers as much as possible. After all, we must be full of ES6 code

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

Configuration webpack

Then create webpack.config.js in the root directory to configure webPack, and create a SRC directory to store the library code. The current directory will look like this

The first simple configuration, the follow-up needs to add, here directly on the code

~ 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'}; };Copy the code

In SRC, create an index.js file and write something like this

The terminal then executes the webpack command

Of course, it is incompatible now, or we can not use Babel in the beginning, we can try, such as I now index.js add a sentence

And then when you compile it, you see the result is still let, which is definitely not going to work

Okay, so now we’re going to configure Babel, there’s nothing to talk about, I’m just going to put the code here, there’s nothing to talk about, okay

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'],},},},],},}; };Copy the code

And then, you don’t want to manually go to webpack every time you make a change, right? Bring in 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,}}; };Copy the code

If you run webpack-dev-server in the terminal, it will automatically find the global module, which is not good, so… You know,

Add commands directly to 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"}}Copy the code

Then the yarn start

An HTML pops up

Of course, the default is to go to the root of index. HTML, which we don’t have, so create one under the root and import our axios.js


      
<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>
Copy the code

Refresh the page to see the SRC /index.js alert in effect

In addition, webpack-dev-server is also available, and the page will be refreshed automatically if the code is changed

Then, let’s match build

So without further ado, let’s go straight to the code

~ 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"}}Copy the code

~ 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,}}; };Copy the code

It’s ok to see it

Good to this I was the Webpack related things to build almost, the next is going to start busy ~

Axios project code

Let’s start by creating common.js, which holds common methods, with an assertion

~ /src/common.js

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

Then create another file (personal habit)/SRC /axios and put the main file here

Then let’s see if we can use axios({… }) or axios.get, etc

In index.js, write the result directly, write the expected usage, and then add the internal

~ 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,}});Copy the code

Then will think about it, we can directly write function, this is no problem, but it is too loose, people don’t like it, but also can be, so here I am in class, because the output into a class out certainly is an instance, since is the instance, then certainly will not run directly as a function () directly

Js class constructor can return something. If you are not familiar with this thing, it is recommended to see the js class first. Here is not much to say, mainly explain the idea

In simple terms, we can return a proxy object, so that we can write directly as a class, or call directly as a function ()

And let’s print it out first

~

If YOU look at the console of the page, you can see that AXIos is a proxy object, like this

You can also see an error because we are now returning a proxy object, not an instance class, and there is no get

If you’re listening to a proxy, it’s important to know what you’re using to create a proxy. If you’re listening to an object, That still can’t be called directly, but if you want to call it directly like a function, then you have to be listening to a function

Like this,

So let’s solve the problem that we can’t find a get function, so let’s add a method to the proxy, it’s very simple, you can add a get method to the proxy, and if someone comes to it, they’ll just return it from my class, and that’s it, but pay a little bit of attention to this, because if I write this, it’s going to refer to the 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;

Copy the code

If you look at this, no errors are reported, and get and POST console

At this point we can proceed to write the data request… ? Is far from enough

There’s a lot of variety in axios arguments, so let’s just kind of summarize

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

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

axios.post....

Copy the code

Wait a minute, this point, how can you unify the parameters of such a complex multi-source

The second thing is that AXIos can customize parameters very deeply, either globally or individually, interceptors, transfrom, etc., defaults, etc.


parameter

Let’s start with a default to define the default values, and let me just say that this x-request-by is sort of an unwritten specification, and it’s something that most Request libraries are willing to do, so that the backend can determine whether your Request is coming from Ajax or from or from a browser 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;

Copy the code

Of course, the diagram here is simple, simple to write a few parameters, you can like to add a lot of things, such as the default value of data and so on, enough for the first time, the follow-up is not enough to add


Default = _default, of course not, because our axios will eventually need multiple instances of axios. Create, then it will not work. Prototype, of course

Parse (json.stringify (_default)), which is the highest performance method, and then modify the code slightly

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();

Copy the code

Here we add a create method to both the prototype and the instance, because we can use axios.create() or axios() directly, and neither static method nor instance method will satisfy our needs

So let’s try it out. Let’s console axios.default

You’ll notice that undefined is already added here

Because at this point this axios is not an object, it’s a proxy, and we haven’t added a set method to the proxy yet, right

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();

Copy the code

If you look in your browser, you’ll see that default is already there

And let’s create two axios, change the parameters and try it out

The two instance parameters don’t matter, so that’s a nice NTH step, and we’re about a quarter of the way through AxiOS


We now have no effect between instances, but when we change parameters, we should not only change axios.default. XXX directly, we should also have parameters, like this

Here we can modify the axios.create method directly

~ axios.js

. Axios.create = Axios.prototype.create =function(options={}) {
  let axios = newAxios(); axios.default = { ... JSON.parse(JSON.stringify(_default)), ... options };returnaxios; }; .Copy the code

I can just expand it and replace it, right? But, really?

Suppose we pass an object directly, there is a word of headers is directly to our header the default parameters have to be replaced the ah, that this is not so good, of course, this also see we the library needs to himself, if we want to do, is to do not have what problem, the problem is as follows

So at this point, we can do it with a little recursion

~ axios.js

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

  letres = { ... 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;
  returnaxios; }; .Copy the code

Look at it at this point, there’s no problem


Code defragmenting

Next, let’s not rush to write the request parameters, let’s first plan this code a little planning, tidy up, after all, all put in a file, this later can not maintain

The current split can be divided into several points

  1. defaultIs it possible to use a separate file to install
  2. thismergeThe functions must be common, so we can put them in ourcommon.jsIn the
  3. thisrequestIt should also be put in a separate onejsTo define the

Without further ado, let’s get right to the code

~ request.js

export default function request() {}Copy the code

~ default.js

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

~ 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]; }}}Copy the code

~ 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();

  letres = { ... JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

Copy the code

It feels so much cleaner now, doesn’t it


Processing request parameters

Before writing, let’s take a look at what axios supports and then think about how to write it

Roughly speaking, except for the total axios({… }) are these three methods similar? Of course, there are too many things in Axios, so here is a simple implementation of these three, mainly to explain the problem, you can add your own, but only manual work

We can see that there are a lot of axios arguments, so we should just unify them. Whatever argument is passed, we should return axios({}), and finally unify them. Isn’t that convenient

So let’s just look at the first two cases

You’ll notice that the method in the first two cases is the same except for this one, so we can pull out a method to deal with it in a unified way

~ 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;
    }
    returnoptions; } 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();

  letres = { ... JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();
Copy the code

Then at this time, we in order to encapsulate a library perspective, certainly need to carry out a variety of parameters verification, type and so on, if not to give him a serious error, to help users to debug

We used assert in common.js

. 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',); }}}...Copy the code

The rules here correspond to the way axios is used as written above, which is pretty much the same. Now that these parameters are verified, we can write the specific personalized processing

By the way, this place of course can also be reused, but there is no need to make a pass actually did not reduce much code, and disorderly, also depends on the individual, we do not like to modify their own

Then let’s deal with the options and 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);
    returnoptions; } 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();

  letres = { ... JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

Copy the code

So let’s test that out

~ 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}});Copy the code

And you can see that it’s okay, right?

And then… Not forgetting, we also need to deal with the case of direct apply, when direct Axios() is called this way

No nonsense, go directly to the code, just like 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);
    returnoptions; } 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();

  letres = { ... JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

Copy the code

So let’s test that out

Method = undefined (default); method = undefined (default); method = undefined (default); Throw it to our request function

This method must be required for all requests, so let’s write a public method

This request method does four main things

  1. Merge with this.default
  2. Check whether the parameters are correct
  3. BaseUrl merge request
  4. Formally calling 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. Merge with this.default
    // 2. Check whether the parameters are correct
    // 3. BaseUrl merge request
    // 4. Formally call 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();

  letres = { ... JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

Copy the code

Merge this merge is very simple, we wrote earlier to use the merge function again, modify the code

. request(options) {console.log(options);
    // 1. Merge with this.default
    merge(options, this.default);

    console.log(options);
    // 2. Check whether the parameters are correct
    // 3. BaseUrl merge request
    // 4. Formally call request(options)}...Copy the code

In this case, we can see that the data before and after the merge already have, but in this case, we should not have all the headers. We should merge the corresponding header and common according to the method that is passed

request(options) {
    // 1. Merge with this.default
    let _headers = this.default.headers;
    delete this.default.headers;
    merge(options, this.default);
    this.default.headers = _headers;
    / / merge head
    // 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. Check whether the parameters are correct
    // 3. BaseUrl merge request
    // 4. Formally call request(options)
  }
Copy the code

It’s a little messy here, so let’s get this straight

We want to merge the headers, but there is a bit of a problem with merging them. We defined common, get… Also will be copied, if if if we use to judge options.header.com mon mon = = this.default.headers.com and then delete it, then you will find no, because we also know, If you write two objects directly, it is equivalent to new two objects directly, then the judgment must not be equal, so when did we copy

It’s in the merge that we encapsulate, and there are a lot of other places that have touched this thing

Then, we should find out when this thing is different, in fact, when we merge our request function for the first time

So here we play a little trick, because the common stuff is already done manually at the bottom, so there is no need for him to copy in, so we delete it first

Let him in before can not enter, delete after to take back, both ends do not delay, good ~

Finally, let’s assign headers to our options.headers

request(options) {
    // 1. Merge header
    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 merge request
    

    // 4. Formally call request(options)
  }
Copy the code

~ index.js

import Axios from './axios';

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

Test the results

As you can see, there is no problem

And then let’s look at the second step, in fact, this check we can write very, very many things worth checking, but this is just to illustrate the meaning, just write a few, you can add more if you are interested

. 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'); .Copy the code

The third step is also directly to the code

~ axios.js

options.url=options.baseUrl+options.url;
delete options.baseUrl; 
Copy the code

~ 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]; }}}}Copy the code

~ index.js

import Axios from './axios';

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

And when you test it again, you can see that it’s fine

Merge, merge, merge, merge, merge, merge, merge, merge, merge, merge, merge, merge, merge, merge, merge

Of course, there is a small matter that we need to deal with, if the person using your library is mentally ill (of course, it is not considered mentally ill), he wrote the path in this way

NodeJS has a url package that you can use directly, and WebPack will pack it for you. However, it is important to note that webPack can not pack everything, such as FS module, or even some low-level functions written in C and C ++ system package. But a URL is not a problem

~ 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. Merge header
    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. Check whether the parameters are correct
    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 merge request
    options.url = urlLib.resolve(options.baseUrl, options.url);
    delete options.baseUrl;

    // 4. Formally call 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();

  letres = { ... JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

Copy the code

Test again at this point

It’s ok

The merge problem

There is still a big problem with this merge, because there is a difference between what we originally wanted and what we have now

We are now forced to write an “if” instead of “check for a missing”, so everything that has priority should be in reverse order

What is our initial requirement? We want the dest to have the lowest priority, so that it can be overwritten by others. But now after we write this if, it becomes the highest priority, so this is not correct, but we can not remove it, and then the merge will have problems again

this.default
common.js

~ common.js

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

Copy the code

And let’s reverse the order a little bit and clone the data

Let’s test it out at this point

It will be found that the items in the header come back again. The reason is also simple, because we have cloned the whole default in the header, so let’s lift the delete part up

That’s when it’s ok


request

At this point, we should write the fourth step directly into the options of our request function, it can handle all the bits and pieces

Then modify 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);
}
Copy the code

Write a simple request for now, and then let’s test it out and see if it can be sent

First build a TXT, convenient for our test, I put in the data directory, like this

Then change index.js

~ index.js

import Axios from './axios';

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

Now, we can see that the header is added, and we’re returning the right thing

Of course, this thing is not finished, but also a matter of preventing users from making trouble

If the user gives you a header that looks like this, that’s not a good idea, so you might as well code it

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

Copy the code

So that’s fine if there’s a problem in the background, if there’s a colon, that’s the end of the problem

Then when we use it, we definitely need an async and await more, so we need to use Promise to package it

~ 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); }}}; }); }Copy the code

And at this time, we have many, many questions

  1. In fact, the 304 is also successful, so the packaging is not correct, and the user may have some customcodeHow do you configure this
  2. Our currentwebpackCan only be compatiblees6.asyncandawaitIt’s not compatible. How do I match this

Let’s solve the webpack problem first, this is actually very simple, we need to press another package yarn add @babel/polyfill

Then open webpack.config.js to modify entry

~ webpack.config.js

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

Note that this order cannot be reversed

Now that it’s compatible, let’s modify index.js

import Axios from './axios';

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

You can see that the result is undefined because we didn’t return our result at all

Modify axios.js at this point

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. Merge header
    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. Check whether the parameters are correct
    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 merge request
    options.url = urlLib.resolve(options.baseUrl, options.url);
    delete options.baseUrl;

    // 4. Formally call request(options)
    returnrequest(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();

  letres = { ... JSON.parse(JSON.stringify(_default)) };

  merge(res, options);

  axios.default = res;
  return axios;
};

export default Axios.create();

Copy the code

At this point, we will see if the result is ok. Of course, we can’t just return the original XML object, we will have to do all kinds of processing on the returned data


Process the data

Let’s start with a simple change to the axios.js request

Return another promise, and you can see the result without any problems

But it’s too messy to put all of our stuff in axios.js, so let’s remove it separately

Create two files: / SRC /response.js and/SRC /error.js

Then I’ll introduce axios.js here and hand it to them as I process it

Then return the value directly in response.js

But the headers a little special, it is necessary to separate a method XHR. The getAllResponseHeaders (), but the return is the original XHR head, this is definitely not line, so we need to cut it

~ the 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,
  };
}

Copy the code

That will do

transformRequest && transformResponse

This is not the end of the story, because we don’t have any data processing yet, so it’s always a string, and the user can customize the processing. Those of you who are familiar with Axios know that Axios has methods transformRequest and transformResponse

Let’s change the request method in axios.js

Now you need to process the request between steps 3 and 4

Print the parameters first, then modify the test demo of index.js

For the convenience of testing I changed 1.txt to 1.json, so that we can deal with JSON data later to see the effect

As you can see, this parameter is available, so it’s a little bit easier to just do it

So if you look at the request, the headers is added

Just for a second, why did I delete it? It doesn’t matter if I don’t delete it, but I want to keep my request clean

As for the custom return result, isn’t it even simpler

And you can look at the result, I didn’t pass in the transformResponse, so it looks like this

That’s it

Of course, we can now use very flexible, not only the single transmission of the JSON parameters can be configured, global configuration unified processing is also possible, let’s try

And between different instances


The interceptor

Interceptors are certainly essential in a request library, and it’s actually quite easy to add one to our library as we write it

~ 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); }) ();Copy the code

Then create a new interceptor.js

~ interceptor. Js

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

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

  list() {
    return this._list; }}export default Interceptor;
Copy the code

~ 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. Merge header
    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. Check whether the parameters are correct
    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 merge request
    options.url = urlLib.resolve(options.baseUrl, options.url);
    delete options.baseUrl;

    // 4. Change the request
    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. Formally call 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 => {
          leterr = 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();

Copy the code

As you can see, it’s basically the same thing, just passing in the parameters and calling it

But there are two small problems to deal with

  1. We’re giving the user a lot of openings now, and if he returnsconfigIt’s not true or it didn’t return and we should have sent it an error message, and then we’ll check it, and by this point you should have figured it out, I guessaxiosSo if you want to check these parameters and make a separate function, it’s not very technical, so I don’t want to talk about it here, but if you’re interested, you can try it out. Okay
  2. We’re giving the user a function, and we’re using thetaforEach“, which leads to a problem if the user gives you a bandasyncThat’s not going to work. Let’s add itasyncandawait, butasyncandawaitIt returns apromiseAgain very strange, this everybody is interested can try yourself, or leave a message in the comment section

This is the time to test the effect

As you can see, we’re almost done with the interceptor

conclusion

The final code has been uploaded to Github and linked to github.com/Mikey-9/axi…

Again, this article is not about implementing axios completely, but the idea of implementing such a library. Of course, there are a lot of problems. Please leave your comments in the comments section or add me to qq or wechat

It’s a little long. Thanks for watching

Thank You

qq:

WeChat: