关于axios:Axios-源码解读-源码实现篇

38次阅读

共计 7306 个字符,预计需要花费 19 分钟才能阅读完成。

在上两期,咱们解说了 Axios 的源码:

  • Axios 源码解读 —— request 篇
  • 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 最根底的性能曾经实现了(尽管偷了个懒用了 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 语法调用。

当初,再运行一下咱们的代码,看看加上拦截器后的运行成果吧。(如下图)

从上图能够看出,返回的内容中,只剩下了 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 就曾经实现啦。

它能够用于在 Node 端实现网络申请,并反对一些根底配置,比方 baseURL、url、申请办法、拦截器、勾销申请 …

然而,还是有很多不够欠缺的中央,感兴趣的小伙伴能够找到上面的源码地址,持续往下续写。

源码地址,倡议练习

最初一件事

如果您曾经看到这里了,心愿您还是点个赞再走吧~

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

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

正文完
 0