1
头图

In the last two issues, we explained the source code Axios

Today, we will implement a simple Axios , for Node implement network requests end, and to support some basic configuration, such baseURL, url, request method, interceptors, cancellation request ...

All the source code of this implementation is placed in here , you can take a look if you are interested.

Axios instance

This time we will use typescript + node to implement the relevant code, so that it will be clearer for everyone to understand the code.

Here, let's implement a Axios class first.

type AxiosConfig = {
  url: string;
  method: string;
  baseURL: string;
  headers: {[key: string]: string};
  params: {};
  data: {};
  adapter: Function;
  cancelToken?: number;
}

class Axios {
  public defaults: AxiosConfig;
  public createInstance!: Function;

  constructor(config: AxiosConfig) {
    this.defaults = config;
    this.createInstance = (cfg: AxiosConfig) => {
      return new Axios({ ...config, ...cfg });
    };
  }
}

const defaultAxios = new Axios(defaultConfig);

export default defaultAxios;

Above, we mainly implement the Axios class, use defaults store the default configuration, and declare the createInstance method. This method creates a new Axios instance and inherits the configuration of the Axios

request method

Next, we will https://mbd.baidu.com/newspage/api/getpcvoicelist output the data returned by the response to the console.

The syntax for our request is as follows:

import axios from './Axios';

const service = axios.createInstance({
  baseURL: 'https://mbd.baidu.com'
});

(async () => {
  const reply = await service.get('/newspage/api/getpcvoicelist');
  console.log(reply);
})();

request method

Let's first add a request and get method Axios

import { dispatchRequest } from './request';

class Axios {
  //...

  public request(configOrUrl: AxiosConfig | string, config?: AxiosConfig) {
    if (typeof configOrUrl === 'string') {
      config!.url = configOrUrl;
    } else {
      config = configOrUrl;
    }
    
    const cfg = { ...this.defaults, ...config };
    return dispatchRequest(cfg);
  }

  public get(configOrUrl: AxiosConfig | string, config?: AxiosConfig) {
    return this.request(configOrUrl, {...(config || {} as any), method: 'get'});
  }
}

The implementation of the request method here is not very different from the method that comes with axios

Now, let's edit the 061eb8f5cc5a6d method dispatchRequest

export const dispatchRequest = (config: AxiosConfig) => {
  const { adapter } = config;
  return adapter(config);
};

And axios like to call the configuration adapter to initiate network requests, and we defaultConfig configured with a default of adapter . (as follows)

const defaultConfig: AxiosConfig = {
  url: '',
  method: 'get',
  baseURL: '',
  headers: {},
  params: {},
  data: {},
  adapter: getAdapter()
};

adapter method

Next, let's focus on our adapter implementation.

// 这里偷个懒,直接用一个 fetch 库
import fetch from 'isomorphic-fetch';
import { AxiosConfig } from './defaults';

// 检测是否为超链接
const getEffectiveUrl = (config: AxiosConfig) => /^https?/.test(config.url) ? config.url : config.baseURL + config.url;

// 获取 query 字符串
const getQueryStr = (config: AxiosConfig) => {
  const { params } = config;
  if (!Object.keys(params).length) return '';

  let queryStr = '';
  for (const key in params) {
    queryStr += `&${key}=${(params as any)[key]}`;
  }

  return config.url.indexOf('?') > -1 
    ? queryStr
    : '?' + queryStr.slice(1);
};

const getAdapter = () => async (config: AxiosConfig) => {
  const { method, headers, data } = config;
  let url = getEffectiveUrl(config);
  url += getQueryStr(config);

  const response = await fetch(url, {
    method,
    // 非 GET 方法才发送 body
    body: method !== 'get' ? JSON.stringify(data) : null,
    headers
  });

  // 组装响应数据
  const reply = {
    data: await response.json(),
    status: response.status,
    statusText: response.statusText,
    headers: response.headers,
    config: config,
  };
  return reply;
};

export default getAdapter;

Here, our implementation is relatively rudimentary. Just a few steps

  1. assembly url
  2. make a request
  3. Assemble response data

see the effect

Now come to the console to run our code, which is the following, and look at the console output.

import axios from './Axios';

const service = axios.createInstance({
  baseURL: 'https://mbd.baidu.com'
});

(async () => {
  const reply = await service.get('/newspage/api/getpcvoicelist');
  console.log(reply);
})();

image

As can be seen from the above figure, the axios have been implemented (although I stole it and used fetch ).

Next, let's improve its capabilities.

interceptor

Now, I want to have the axios add interceptors to my 061eb8f5cc5c00.

  1. I'm going to add an interceptor at the request with some custom headers before each request.
  2. I will add an interceptor to the response to directly take out the data body ( data ) and configuration information ( config ) of the response to remove redundant information.

The code is implemented as follows:

// 添加请求拦截器
service.interceptors.request.use((config: AxiosConfig) => {
  config.headers.test = 'A';
  config.headers.check = 'B';
  return config;
});

// 添加响应拦截器
service.interceptors.response.use((response: any) => ({ data: response.data, config: response.config }));

Modify the Axios class and add interceptors

Let's start by creating a InterceptorManager class to manage our interceptors. (as follows)

class InterceptorManager {
  private handlers: any[] = [];

  // 注册拦截器
  public use(handler: Function): number {
    this.handlers.push(handler);
    return this.handlers.length - 1;
  }

  // 移除拦截器
  public eject(id: number) {
    this.handlers[id] = null;
  }

  // 获取所有拦截器
  public getAll() {
    return this.handlers.filter(h => h);
  }
}

export default InterceptorManager;

After defining the interceptor, we need to add the interceptor - interceptors Axios class, as follows:

class Axios {
  public interceptors: {
    request: InterceptorManager;
    response: InterceptorManager;
  }

  constructor(config: AxiosConfig) {
    // ...
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    }
  }
}

Next, let's handle these interceptor calls request (as follows)

public async request(configOrUrl: AxiosConfig | string, config?: AxiosConfig) {
  if (typeof configOrUrl === 'string') {
    config!.url = configOrUrl;
  } else {
    config = configOrUrl;
  }

  const cfg = { ...this.defaults, ...config };
  // 将拦截器与真实请求合并在一个数组内
  const requestInterceptors = this.interceptors.request.getAll();
  const responseInterceptors = this.interceptors.response.getAll();
  const handlers = [...requestInterceptors, dispatchRequest, ...responseInterceptors];

  // 使用 Promise 将数组串联调用
  let promise = Promise.resolve(cfg);
  while (handlers.length) {
    promise = promise.then(handlers.shift() as any);
  }

  return promise;
}

The main thing here is to combine the interceptor and the real request into an array, and then use Promise for concatenation.

Promise knowledge point that I don't know yet Promise.resolve , there is no need to explicitly return a Promise object. Promise internally wraps the returned value into a Promise object, which supports .then syntax calls.

Now, run our code again to see the effect of adding the interceptor. (As shown below)

image

As can be seen from the above figure, only the data and config fields (response interceptors) are left in the returned content. And config field, we can also see in request to add the interceptor custom headers also play it!

cancel the request

Finally, let's implement the CancelToken class, which is used to cancel the axios request.

In practical applications, I often use CancelToken to automatically detect duplicate requests (from frequent clicks), then cancel earlier requests and use only the last request as a valid request.

Therefore, CancelToken is actually a very favorite function for me. Its implementation is not complicated. Let's start to implement it below.

Let's take a look at the calling method first. Next, I will cancel the setTimeout That is, only requests completed within 10ms can succeed.

import axios, { CancelToken } from './Axios';

// ...
(async () => {
  const source = CancelToken.source();
  // 10ms 后,取消请求
  setTimeout(() => {
    source.cancel('Operation canceled by the user.');
  }, 10);
  
  const reply = await service.get('/newspage/api/getpcvoicelist', { cancelToken: source.token });
  console.log(reply);
})();

Let's start with a rationale.

First, we used CancelToken.source() get a cancelToken and passed it to the corresponding request function.

Next, we should use this token to query to see if the request has been canceled. If it is canceled, an error will be thrown to end the request.

CancelToken

ok, the idea is clear, let's start implementing it, CancelToken start with 061eb8f5cc5e5b.

class CancelError extends Error {
  constructor(...options: any) {
    super(...options);
    this.name = 'CancelError';
  }
}

class CancelToken {
  private static list: any[] = [];

  // 每次返回一个 CancelToken 实例,用于取消请求
  public static source(): CancelToken {
    const cancelToken = new CancelToken();
    CancelToken.list.push(cancelToken);
    return cancelToken;
  }

  // 通过检测是否有 message 字段来确定该请求是否被取消
  public static checkIsCancel(token: number | null) {
    if (typeof token !== 'number') return false;
    
    const cancelToken: CancelToken = CancelToken.list[token];
    if (!cancelToken.message) return false;

    // 抛出 CancelError 类型,在后续请求中处理该类型错误
    throw new CancelError(cancelToken.message);
  }

  public token = 0;
  private message: string = '';
  constructor() {
    // 使用列表长度作为 token id
    this.token = CancelToken.list.length;
  }

  // 取消请求,写入 message
  public cancel(message: string) {
    this.message = message;
  }
}

export default CancelToken;

CancelToken is basically completed. Its main function is to use an CancelToken to correspond to a request that needs to be processed, and then throw an CancelError type 061eb8f5cc5ec1 in the canceled request.

Handling CancelError

Next, we need to dispatchRequest ), and finally add a corresponding response interceptor to handle the corresponding error.

export const dispatchRequest = (config: AxiosConfig) => {
  // 在发起请求前,检测是否取消请求
  CancelToken.checkIsCancel(config.cancelToken ?? null);
  const { adapter } = config;
  return adapter(config).then((response: any) => {
    // 在请求成功响应后,检测是否取消请求
    CancelToken.checkIsCancel(config.cancelToken ?? null);
    return response;
  });
};

Since our interceptor is too rough to add a failure response interceptor (which should be handled here), I directly wrap the entire request in try ... catch for processing.

try {
  const reply = await service.get('/newspage/api/getpcvoicelist', { cancelToken: source.token });
  console.log(reply);
} catch(e) {
  if (e.name === 'CancelError') {
    // 如果请求被取消,则不抛出错误,只在控制台输出提示
    console.log(`请求被取消了, 取消原因: ${e.message}`);
    return;
  }
  throw e;
}

Next, let's run our program and see the console output! (As shown below)

image

You're done!

summary

At this point, our simple version axios has been completed.

It can be used Node side, and supports some basic configurations, such as baseURL, url, request method, interceptor, cancel request...

However, there are still many imperfections. Interested partners can find the source code address below and continue to write.

source code address, it is recommended to practice

one last thing

If you have seen this, I hope you will give a like and go~

Your likes are the greatest encouragement to the author, and can also allow more people to see this article!

If you think this article is helpful to you, please help to light up star github to encourage it!


晒兜斯
1.8k 声望534 粉丝