学习 Axios 源码并实现一个简易版的请求库

更好的阅读体验可查看我的博客原文

本文在参考 Axios(v1.3.3) 源码的基础上实现一个简易版的请求库(仅用于学习),将包含以下功能

  • 基本请求
  • 适配器
  • 拦截器
  • 请求中断
  • 超时中断

Demo 地址

前置知识

适配器

通过适配器来使 Axios 在 Browser/Node 平台使用不同的 API 来发送请求,Axios Browser 使用的是 ajax,Node 端用的是 http,本文在 Browser 将用 fetch 来替代,Node 端暂时不实现。下面代码中的 adapter 其实最后返回的就是 fetch 函数了。

import xhr from './xhr';
import http from './http';

const knownAdapters = {
    
    
  http,
  xhr
};

export default async function adapters(config) {
    
    
  let adapter;
  for (const key in knownAdapters) {
    
    
    if (knownAdapters[key]) {
    
    
      adapter = knownAdapters[key];
    }
  }

  return adapter(config);
}
const isXHRAdapterSupported = typeof fetch !== 'undefined';

export default isXHRAdapterSupported &&
  async function xhrAdapter(config) {
    
    
    return fetch(config.url, config);
  };
const isHttpAdapterSupported = typeof process !== 'undefined';

export default isHttpAdapterSupported &&
  function httpAdapter(config) {
    
    
    // TODO: 暂未实现
    return config;
  };

dispatchRequest

该部分主要用于发送请求并将 fetch 接口进行 json() 处理

import adapters from './adapters/index.js';

export default function dispatchRequest(config) {
    
    
  const adapter = adapters(config);

  return adapter.then(
    async function onAdapterResolution(response) {
    
    
      try {
    
    
        const res = await response.json();
        return {
    
     ...res, config };
      } catch (error) {
    
    
        return Promise.reject(response);
      }
    },
    function onAdapterRejection(reason) {
    
    
      return Promise.reject(reason);
    }
  );
}

基本请求

我们先实现一个简单的 axios.get(url, [config])

import adapters from './adapters';

export default class Axios {
    
    
  constructor(config) {
    
    
    this.defaultConfig = config;
  }

  request(config) {
    
    
    // 相当于 let promise = fetch(config.url, { ...this.defaultConfig, ...config });
    let promise = dispatchRequest({
    
     ...this.defaultConfig, ...config });
    return promise;
  }

  get(url, config) {
    
    
    const mergedConfig = {
    
     ...config, url };
    return this.request(mergedConfig);
  }
}

到这一步我们基本就实现了一个非常简易的请求库了,无非就是将fetch封装成axios api 的样子

拦截器

拦截器是我们平常用的比较多的一个功能,

使用拦截器我们可以实现以下功能

  • request 拦截器:修改 header 如添加 token 之类的,处理接口缓存等等…
  • response 拦截器:处理 code 码(301 重定向,404 转到 notFound 页面),针对失败请求收集错误日志,mock 数据,统一处理数据返回格式,记录请求耗时等等…

示例

在实现之前先看下示例代码明确要实现的功能以及 api 长什么样

const axios = new Axios();

axios.interceptors.request.use(
  function requestInterceptorFulfilled(config) {
    
    
    config.headers = {
    
    
      token: 'add by request interceptors'
    };
    config.metadata = {
    
     startTime: Date.now() };
    return config;
  },
  function requestInterceptorRejected(error) {
    
    
    return Promise.reject(error);
  }
);

axios.interceptors.response.use(
  function responseInterceptorFulfilled(response) {
    
    
    response.extraData = 'add by response interceptors';
    // 接口请求耗时
    response.duration = Date.now() - response.config.metadata.startTime;
    return response;
  },
  function responseInterceptorRejected(error) {
    
    
    if (error.status === 404) {
    
    
      alert('返回状态码为404,重定向到404页面');
    }
    return Promise.reject(error);
  }
);

上面的实例中我们在请求的参数上加上header.tokenmetadata,在请求返回体上加上了 extraDataduration(记录请求耗时) 以及处理 404 状态码。

具体实现

InterceptorManager 内部维护着一个数组,对拦截器进行增加(use)和删除(eject)操作

export default class InterceptorManager {
    
    
  constructor() {
    
    
    this.handlers = [];
  }

  use(fulfilled, rejected) {
    
    
    this.handlers.push({
    
    
      fulfilled,
      rejected
    });

    // 返回当前的 handler 在数组中的 index,用于eject
    return this.handlers.length - 1;
  }

  eject(index) {
    
    
    if (this.handlers[index]) {
    
    
      this.handlers[index] = null;
    }
  }
}

Axios this.interceptors.requestthis.interceptors.response 各自添加InterceptorManager对象

constructor(config) {
    
    
  this.defaultConfig = config;
  // highlight-add-start
  this.interceptors = {
    
    
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
  // highlight-add-end
}

接下来是最核心的 request 方法编写

request(config) {
    
    
  let promise;
  let i = 0;
  const requestInterceptorChain = [];
  const responseInterceptorChain = [];

  this.interceptors.request.handlers.forEach(({
     
      fulfilled, rejected }) => {
    
    
    requestInterceptorChain.unshift(fulfilled, rejected);
  });
  this.interceptors.response.handlers.forEach(({
     
      fulfilled, rejected }) => {
    
    
    responseInterceptorChain.push(fulfilled, rejected);
  });

  // chain为成对出现的fulfilled, rejected
  const chain = [
    ...requestInterceptorChain,
    dispatchRequest,
    undefined,
    ...responseInterceptorChain
  ];

  let len = chain.length;

  // 将config转化成promise对象以方便后面组成 Promise 链
  promise = Promise.resolve({
    
    ...this.defaultConfig, ...config});

  while (i < len) {
    
    
    promise = promise.then(chain[i++], chain[i++]);
  }

  return promise;
}

这里有一个特别的地方就是,在加入 chain 的操作中,request 是使用 unshift 为 response 是使用 push,也就是说先加入的 request 后调用,而先加入的 response 先调用

针对前面的示例最终会转化成以下核心代码

// undefined 用来占位的,和 dispatchRequest 组成一对 fulfilled, rejected
const chain = [
  requestInterceptorFulfilled,
  requestInterceptorRejected,
  dispatchRequest,
  undefined,
  responseInterceptorFulfilled,
  responseInterceptorRejected
];
let len = chain.length;

// 将config转化成promise对象以组成 Promise 链
promise = Promise.resolve({
    
     ...this.defaultConfig, ...config });

while (i < len) {
    
    
  promise = promise.then(chain[i++], chain[i++]);
}

总结

当我们运行 axios.get('https://run.mocky.io/v3/0a4e2970-39b4-4bb5-9a12-06e47408e2a3') 时,Axios 内部其实做了下面这些事情

// 将 config 转化成 promise 对象以方便后面组成 Promise 链
Promise.resolve({
    
    
  url: 'https://run.mocky.io/v3/0a4e2970-39b4-4bb5-9a12-06e47408e2a3'
})
  .then(
    // request 拦截器
    config => {
    
    
      config.headers = {
    
    
        token: 'add by request interceptors'
      };
      config.metadata = {
    
     startTime: Date.now() };
      return config;
    },
    error => Promise.reject(error)
  )
  .then(config => {
    
    
    console.log('request 拦截器处理后的参数:', config);
    // 请求函数 dispatchRequest
    return fetch(config.url, config).then(async response => {
    
    
      const json = await response.json();
      return {
    
     ...json, config };
    });
    // undefined 占位
  }, undefined)
  .then(
    // response 拦截器
    response => {
    
    
      console.log('fetch 返回的数据 + config:', JSON.stringify(response));
      response.extraData = 'add by response interceptors';
      // 接口请求耗时
      response.duration = Date.now() - response.config.metadata.startTime;
      return response;
    },
    error => {
    
    
      if (error.status === 404) {
    
    
        alert('返回状态码为404,重定向到404页面');
      }
      return Promise.reject(error);
    }
  )
  .then(res => console.log('最终获得的数据:', res));

可以将上方代码复制到控制台查看输出结果

请求中断

请求中断不需要我们做其他操作,只需要在使用时传入 signal 参数即可

const abortController = new AbortController();
const signal = abortController.signal;

axios.get(`https://run.mocky.io/v3/0a4e2970-39b4-4bb5-9a12-06e47408e2a3?mocky-delay=10000ms`, {
    
    
  signal
});

// 中断请求
abortController.abort();

超时中断

在请求时添加一个计时器来调用 abort() 即可

const isXHRAdapterSupported = typeof fetch !== 'undefined';

export default isXHRAdapterSupported &&
  async function xhrAdapter(config) {
    
    
    // highlight-add-start
    if (config.timeout) {
    
    
      const controller = new AbortController();
      const abortId = setTimeout(() => {
    
    
        controller.abort();
      }, config.timeout);

      const res = await fetch(config.url, {
    
     ...config, signal: controller.signal });

      clearTimeout(abortId);

      return res;
    }
    // highlight-add-end

    return fetch(config.url, config);
  };

猜你喜欢

转载自blog.csdn.net/qq_36925843/article/details/129100708