axios没有fetch适配器?那就自己写一个呗!

前言

用过axios或者了解过axios源码的同学们,或多或少都知道,axios在浏览器中的请求,底层使用的是XMLHttpRequest发起。

那我如果想要使用fetch,那么不好意思,并没有哦!

既然如此,何不自己开动双手,给axios写一个fetch适配器。

2 axios的dispatchRequest

要写fetch适配器,肯定要先知道axios是在哪里使用的适配器,是如何使用的。

axios源码的dispatchRequest.js文件中,有如下代码段:

js 复制代码
export default function dispatchRequest (config) {
  
  // .....do something

  // 获取适配器,如果是浏览器环境获取xhr,如果是Node环境,获取http
  const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

  // 这里的adapter就是我们要重写的方法
  return adapter(config).then(function onAdapterResolution (response) {
     // ...... do success 
  }, function onAdapterRejection (reason) {
     // ...... do error
  });
}

可以看出来,adapter是一个函数,参数是config,也就是我们调用axios请求时传入的。

另外adapter返回值是一个promise

3 axios的xhr适配器

既然要写fetch适配器,那么少不了要参考xhr适配器是怎么写的,那么让我们先看一下代码:

js 复制代码
// 判断是否支持XMLHttpRequest
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';

// 适配器的请求参数是config
export default isXHRAdapterSupported && function (config) {
  // 返回Promise
  return new Promise(function dispatchXhrRequest (resolve, reject) {
    // 请求体
    let requestData = config.data;
    // 请求头
    const requestHeaders = AxiosHeaders.from(config.headers).normalize();
    // ....省略

    // xhr请求
    let request = new XMLHttpRequest();
    // 开启请求
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    // 监听请求完成
    request.onreadystatechange = function handleLoad () {
        //....省略
    };
    // 处理浏览器请求取消事件
    request.onabort = function handleAbort () {
      //....省略
    };
    // 处理低级的网络错误
    request.onerror = function handleError () {
     //....省略
    };
    // 处理超时
    request.ontimeout = function handleTimeout () {
      //....省略
    };
    // 增加下载过程的监听函数
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', progressEventReducer(config.onDownloadProgress, true));
    }
    // 增加上传过程的监听函数
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
    }

    // 请求过程中取消的监听事件
    if (config.cancelToken || config.signal) {
      onCanceled = cancel => {
        if (!request) {
          return;
        }
        reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
        request.abort();
        request = null;
      };

      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }
    // 发送请求
    request.send(requestData || null);
  });
}

可以看出来,xhr适配器主要做了5件事:

  1. config传入的信息转为request属性
  2. 对请求取消事件的监听
  3. 对请求超时时间的监听
  4. 监听上传和下载进度事件
  5. 发送请求

由于fetch不能监听上传进度,所以我们就先监听实现下载进度就行了,其他的就按照这个思路来写。

config传入request的操作,很多代码都可以复用。

4 手写fetch适配器

接下来看看我写的fetch适配器吧。

lib->adapters下新增fetch.js文件,内容如下:

想尝试的小伙伴可以直接复制粘贴使用。

js 复制代码
'use strict';

import utils from '../utils.js';
import settle from '../core/settle.js';
import cookies from '../helpers/cookies.js';
import buildURL from '../helpers/buildURL.js';
import buildFullPath from '../core/buildFullPath.js';
import isURLSameOrigin from '../helpers/isURLSameOrigin.js';
import transitionalDefaults from '../defaults/transitional.js';
import AxiosError from '../core/AxiosError.js';
import CanceledError from '../cancel/CanceledError.js';
import parseProtocol from '../helpers/parseProtocol.js';
import platform from '../platform/index.js';
import AxiosHeaders from '../core/AxiosHeaders.js';

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

// 适配器的请求参数是config
export default isFetchAdapterSupported &&
  function (config) {
    // 返回Promise
    return new Promise(function dispatchFetchRequest(resolve, reject) {
      // 这行代码非常重要,代表进入我们的适配器了O(∩_∩)O哈哈~
      console.log('进入了fetch适配器。。。');
      // 请求体
      let requestData = config.data;
      // 请求头
      const requestHeaders = AxiosHeaders.from(config.headers).normalize();
      // 返回数据类型
      const responseType = config.responseType;
      // 接下来先把config转为fetch的请求参数,
      // 大多都是从xhr复用

      // 自动帮我们设置contentType,
      // 这就是为什么我们使用的时候都不需要
      // 特别设置contentType的原因了
      if (utils.isFormData(requestData)) {
        if (
          platform.isStandardBrowserEnv ||
          platform.isStandardBrowserWebWorkerEnv
        ) {
          // 浏览器环境让浏览器设置
          requestHeaders.setContentType(false);
        } else {
          requestHeaders.setContentType('multipart/form-data;', false);
        }
      }

      // 设置auth,帮我们转码好了
      if (config.auth) {
        const username = config.auth.username || '';
        const password = config.auth.password
          ? unescape(encodeURIComponent(config.auth.password))
          : '';
        requestHeaders.set(
          'Authorization',
          'Basic ' + btoa(username + ':' + password)
        );
      }

      // 拼接完整URL路径
      const fullPath = buildFullPath(config.baseURL, config.url);

      // Add xsrf header
      if (platform.isStandardBrowserEnv) {
        const xsrfValue =
          (config.withCredentials || isURLSameOrigin(fullPath)) &&
          config.xsrfCookieName &&
          cookies.read(config.xsrfCookieName);
        if (xsrfValue) {
          requestHeaders.set(config.xsrfHeaderName, xsrfValue);
        }
      }

      requestData === undefined && requestHeaders.setContentType(null);

      const headers = new Headers();
      // 构造headers
      utils.forEach(
        requestHeaders.toJSON(),
        function setRequestHeader(val, key) {
          headers.append(key, val);
        }
      );
      
      // 移除取消事件订阅
      function done() {
        if (config.cancelToken) {
          config.cancelToken.unsubscribe(onCanceled);
        }
      }

      
      // 获取请求协议,比如https这样的
      const protocol = parseProtocol(fullPath);
      // 判断当前环境是否支持该协议
      if (protocol && platform.protocols.indexOf(protocol) === -1) {
        reject(
          new AxiosError(
            'Unsupported protocol ' + protocol + ':',
            AxiosError.ERR_BAD_REQUEST,
            config
          )
        );
        return;
      }
       
      // 这里已经初步构造出了请求参数,接下来的才是重点
      const requestParams = {
        method: config.method.toUpperCase(),
        headers,
        body: requestData,
        mode: 'cors',
        credentials: config.withCredentials ? 'include' : 'omit'
        // signal,responseType在下面补充
      };

      let onCanceled;
      // 如果用户传入了signal,那么直接用用户传入的
      // 如果用户传入了cancelToken,则构造一个signal,
      // 以便fetch适配器可以兼容cancelToken
      let signal;
      if (!utils.isUndefined(config.signal)) {
        // 直接使用signal
        signal = config.signal;
      } else if (!utils.isUndefined(config.cancelToken)) {
        // 自己构造一个signal
        const controller = new AbortController();
        signal = controller.signal;
        // 取消回调函数,内部也是调用
        // controller.abort()
        onCanceled = (cancel) => {
          reject(
            !cancel || cancel.type
              ? new CanceledError(null, config, requestParams)
              : cancel
          );
          controller.abort();
        };
        // cancelToken订阅回调
        config.cancelToken.subscribe(onCanceled);
      }

        // 设置signal
      if (!utils.isUndefined(signal)) {
        requestParams.signal = signal;
      }
      // 设置responseType
      if (responseType && responseType !== 'json') {
        requestParams.responseType = config.responseType;
      }

      const url = buildURL(fullPath, config.params, config.paramsSerializer);

      // 由于fetch没有现成的timeout属性
      // 此处是开启另一个超时Promise进行竞赛
      // 来实现超时控制
      Promise.race([
        fetch(url, requestParams),

        new Promise((_, innerReject) => {
          setTimeout(
            () => {
              let timeoutErrorMessage = config.timeout
                ? 'timeout of ' + config.timeout + 'ms exceeded'
                : 'timeout exceeded';
              const transitional = config.transitional || transitionalDefaults;
              if (config.timeoutErrorMessage) {
                timeoutErrorMessage = config.timeoutErrorMessage;
              }
              innerReject(
                new AxiosError(
                  timeoutErrorMessage,
                  transitional.clarifyTimeoutError
                    ? AxiosError.ETIMEDOUT
                    : AxiosError.ECONNABORTED,
                  config,
                  requestParams
                )
              );
              // 如果timeout是设置为0,说明永不超时
              // 就不需要用Pormise.race,此处为了方便,我将
              // 超时时间设置为99999999,当做永不超时
            },
            config.timeout > 0 ? config.timeout : 99999999
          );
        }),
      ])
        .then(async (res) => {
          //判断返回类型
          let responseData = '';
          // 如果配置了监听下载进度回调,自己来读取数据
          // 至于监听上传进度,目前fetch好像还做不到
          if (typeof config.onDownloadProgress === 'function') {
            const total = +res.headers.get('content-length');
            const reader = res.body.getReader();
            let loaded = 0;

            // 此处仅写了返回文本的情况
            const decoder = new TextDecoder();
            while (true) {
              const { value, done } = await reader.read();
              if (done) {
                break;
              }
              // 每一次读取都累加起来
              loaded += value.length;
              // 每一次读取都对数据解码并拼接起来
              responseData += decoder.decode(value);
              // 执行回调,此处简单写了,只回传loaded和total,基本也够用了
              config.onDownloadProgress({
                loaded,
                total,
              });
            }
            // 没配置的情况就直接用现成的方法解析
          } else if (!responseType || responseType === 'text') {
            responseData = await res.text();
          } else if (responseType === 'json') {
            responseData = await res.json();
          } else if (responseType === 'blob') {
            responseData = await res.blob();
          }
          // 获取返回头信息
          const responseHeaders = AxiosHeaders.from(res.headers);
          // 构造axios的返回体
          const response = {
            data: responseData,
            status: res.status,
            statusText: res.statusText,
            headers: responseHeaders,
            config,
            request: requestParams,
          };
          // 内部会再次判断请求是否成功
          settle(
            function _resolve(value) {
              resolve(value);
              done();
            },
            function _reject(err) {
              reject(err);
              done();
            },
            response
          );
        })
        .catch((error) => {
          if (error.isAxiosError) {
            // 说明是超时的错误,直接抛出
            reject(error);
          }
          // 判断error的类型,
          if (error && error.type === 'abort') {
            reject(
              new AxiosError(
                'Request aborted',
                AxiosError.ECONNABORTED,
                config,
                requestParams
              )
            );
          }
          // 如果是网络错误
          if (error && error.type === 'error') {
            reject(
              new AxiosError(
                'Network Error',
                AxiosError.ERR_NETWORK,
                config,
                requestParams
              )
            );
          }
          // 其他错误直接抛出
          reject(error);
        });
    });
  };

总结一下:

  1. config转为请求参数,主要还是参考了xhr的原代码
  2. 实现超时控制,使用Promise.race()
  3. 实现请求取消,使用的是AbortController,还做了兼容CancelToken的使用,底层也是用的AbortController
  4. 实现下载进度的监听,采用的方法是自己读取文件流,每读取一次,调用一次回调事件。

文件写完了之后我们还需要改动一下axios浏览器默认适配器为fetch,这样才能调用到我们写的代码。

首先是在lib->defaults->index.js

js 复制代码
const defaults = {
  // ...........省略
  // 此处改为fetch
  adapter: platform.isNode ? 'http' : 'fetch'
  // ...........省略
  }

另外在lib->adapters->adapters.js中引入fetch,这样axios就可以获取到fetch了。

js 复制代码
// 新增
import fetchAdapter from './fetch.js';

const knownAdapters = {
  http: httpAdapter,
  xhr: xhrAdapter,
  // 新增
  fetch: fetchAdapter,
};

最后执行npm run build,我们的包就打出来了。

看看我们的成果:

5 测试fetch是否可行

首先新建一个index.html文件进行测试,当然自己要准备一些可调用的接口,这边就不介绍了。

5.1 测试基本的请求

js 复制代码
<html>
<script type="module" >
    import axios from '../dist/esm/axios.js'
    // 测试get请求
    axios({
        url:'http://127.0.0.1:5556/auth/login?username=admin&password=admin',
        method:'get',
    }).then(res=>{
        console.log(res);
    }).catch(err=>{
        console.log(err);
    })
</script>

可以看到请求已经变成fetch了,并且打印出了我们的关键日志,结果也正常返回。

5.2 测试AbortController取消

js 复制代码
<html>
<script type="module" >
    import axios from '../dist/esm/axios.js'
    // 测试signal请求
    const controller = new AbortController();
    const signal = controller.signal;
    axios({
        url:'http://127.0.0.1:5556/auth/login?username=admin&password=admin',
        method:'get',
        signal
    }).then(res=>{
        console.log(res);
    }).catch(err=>{
        console.log(err);
    })
    // 一秒后取消,我这边是在后端打断点,模拟
    setTimeout(() => {
        controller.abort()
    }, 1000);
</script>

可以看到,抛出了请求取消的错误

5.3 测试cancelToken取消

返回结果同上,我就不贴了,测试代码如下:

js 复制代码
<html>
<script type="module" >
    import axios from '../dist/esm/axios.js'
    // 测试cancelToken请求
    const {token,cancel} = axios.CancelToken.source();
    axios({
        url:'http://127.0.0.1:5556/auth/login?username=admin&password=admin',
        method:'get',
        cancelToken:token,
    }).then(res=>{
        console.log(res);
    }).catch(err=>{
        console.log(err);
    })
    // 一秒后取消,我这边是在后端打断点,模拟
    setTimeout(() => {
        cancel()
    }, 1000);
</script>

5.4 测试超时

js 复制代码
<html>
<script type="module" >
    import axios from '../dist/esm/axios.js'
    axios({
        url:'http://127.0.0.1:5556/auth/login?username=admin&password=admin',
        method:'get',
        //多了超时设置
        timeout:1000
    }).then(res=>{
        console.log(res);
    }).catch(err=>{
        console.log(err);
    })
</script>

返回的错误:

5.5 测试下载进度

js 复制代码
<html>
<script type="module" >
    import axios from '../dist/esm/axios.js'
    
    axios({
        url: 'http://127.0.0.1:5556/auth/test-file',
        method: 'get',
        // 监听下载进度
        onDownloadProgress: ({ loaded, total }) => {
            // 这里只做打印
            console.log({loaded,total});
        },
        })
        .then((res) => {
            console.log(res);
        })
        .catch((err) => {
            console.log(err);
        });
</script>

6 结束

至此,fetch适配器就完成了,如果小伙伴们有疑问欢迎评论区沟通。

如果觉得本文对您有所帮助,可以给个小小的赞吗?

如果你想对axios有更深的了解,可以阅读我的文章:

用了这么久的axios,没想到源码居然这么简单!

五分钟!让你彻底搞懂axios的请求取消原理!附源码分析

相关推荐
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光7 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   7 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   7 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web8 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery