Axios 源码解读 —— 源码实现篇

在上两期,我们讲解了 Axios 的源码:

今天,我们将实现一个简易的 Axios,用于在 Node 端实现网络请求,并支持一些基础配置,比如 baseURL、url、请求方法、拦截器、取消请求...

本次实现所有的源码都放在 这里,感兴趣的可以看看。

Axios 实例

本次我们将使用 typescript + node 来实现相关代码,这样对大家理解代码也会比较清晰。

这里,先来实现一个 Axios 类吧。

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;

在上面,我们主要是实现了 Axios 类,使用 defaults 存储默认配置,同时声明了 createInstance 方法。该方法会创建一个新的 Axios 实例,并且会继承上一个 Axios 实例的配置。

请求方法

接下来,我们将对 https://mbd.baidu.com/newspage/api/getpcvoicelist 发起一个网络请求,将响应返回的数据输出在控制台。

我们发起请求的语法如下:

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 方法

我们先来给我们的 Axios 类加上一个 requestget 方法吧。

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

这里 request 方法的实现和 axios 自带的方法差异性不大。

现在,我们来编辑发起真实请求的 dispatchRequest 方法吧。

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

axios 一样,调用了配置中的 adapter 来发起网络请求,而我们在 defaultConfig 中配置了默认的 adapter。(如下)

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

adapter 方法

接下来,我们重点来看看我们的 adapter 实现即可。

// 这里偷个懒,直接用一个 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;

在这里,我们的实现相对来说比较简陋。简单来说就是几步

  1. 组装 url
  2. 发起请求
  3. 组装响应数据

看看效果

现在来控制台运行一下我们的代码,也就是下面这,看看控制台输出吧。

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

Axios 源码解读 —— 源码实现篇_第1张图片

从上图可以看出,我们的 axios 最基础的功能已经实现了(虽然偷了个懒用了 fetch)。

接下来,我们来完善一下它的能力吧。

拦截器

现在,我想要让我的 axios 拥有添加拦截器的能力。

  1. 我将在请求处添加一个拦截器,在每次请求前加上一些自定义 headers
  2. 我将在响应处添加一个拦截器,直接取出响应的数据主体(data)和配置信息(config),去除多余的信息。

代码实现如下:

// 添加请求拦截器
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 }));

改造 Axios 类,添加 interceptors

我们先来创建一个 InterceptorManager 类,用于管理我们的拦截器。(如下)

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;

定义好了拦截器后,我们需要在 Axios 类中加上拦截器 —— interceptors,如下:

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

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

接下来,我们在 request 方法中处理一下这些拦截器的调用吧。(如下)

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

这里主要是将拦截器和真实的请求合并成一个数组,然后再使用 Promise 进行串联。

这里发现了一个自己还不知道的 Promise 知识点,在 Promise.resolve 中,不需要显式返回一个 Promise 对象, Promise 内部会将返回的值包装成一个 Promise 对象,支持 .then 语法调用。

现在,再运行一下我们的代码,看看加上拦截器后的运行效果吧。(如下图)

Axios 源码解读 —— 源码实现篇_第2张图片

从上图可以看出,返回的内容中,只剩下了 dataconfig 字段(响应拦截器)。并且在 config 字段中也可以看到我们在 请求拦截器 中添加的自定义 headers 也起作用啦!

取消请求

最后,我们来实现 CancelToken 类,用于取消 axios 请求。

在实际应用中,我经常会使用 CancelToken 来自动检测重复请求(来源于频繁点击),然后取消掉更早的请求,仅使用最后一次请求作为有效请求。

所以,CancelToken 对我而言其实是个非常喜爱的功能,它本身的实现并不复杂,我们下面就开始来实现它吧。

我们先看看调用方式吧,下面我将在发起请求后 10ms 后(利用 setTimeout),将请求取消。也就是说,只有 10ms 内完成的请求才能成功。

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

我们先来理一理思路。

首先,我们使用了 CancelToken.source() 获取了一个 cancelToken,并传给了对应的请求函数。

接下来,我们应该是使用这个 token 进行查询,查询该请求是否被取消,如果被取消则抛出错误,结束这次请求。

CancelToken

ok,思路已经清晰了,接下来就开始实现吧,先从 CancelToken 开始吧。

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 基本上完成了,它的主要功能就是使用一个 CancelToken 实例对应一个需要做处理的请求,然后在已取消的请求中抛出了一个 CancelError 类型的抛错。

处理 CancelError

接下来,我们需要在对应的请求处(dispatchRequest)添加取消请求检测,最后再加上一个对应的响应拦截器处理对应错误即可。

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

由于我们的拦截器实现的太粗糙,并没有添加失败响应拦截器(本应在这里处理),所以我这里直接将整个请求包裹在 try ... catch 中处理。

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

接下来,我们运行我们的程序,看看控制台输出吧!(如下图)

Axios 源码解读 —— 源码实现篇_第3张图片

大功告成!

小结

到这里,我们的简易版 axios 就已经完成啦。

它可以用于在 Node 端实现网络请求,并支持一些基础配置,比如 baseURL、url、请求方法、拦截器、取消请求...

但是,还是有很多不够完善的地方,感兴趣的小伙伴可以找到下面的源码地址,继续往下续写。

源码地址,建议练习

最后一件事

如果您已经看到这里了,希望您还是点个赞再走吧~

您的点赞是对作者的最大鼓励,也可以让更多人看到本篇文章!

如果觉得本文对您有帮助,请帮忙在 github 上点亮 star 鼓励一下吧!

你可能感兴趣的:(axios前端)