十分钟手把手带你实现一个 mini-axios

科技   2024-07-17 18:03   广东  

今天我们来一起欣赏一下 axios 这个前端请求库的设计之美。我们将主要从以下几个方面来一起看一下:

  1. 准备基础的测试环境
  2. 开发入口模块
  3. axios 多环境适配的思想
  4. axios 拦截器思想的设计
  5. axios 取消请求的实现方案
  6. ts 中模板模式实现通用请求处理的方案(扩展)

这几个角度一起来探讨一下,并且我们采用的形式是自己实现一个最微型的 axios。

准备基础的测试环境

1. 基于 Koa 准备一个最简单的服务程序:

js 代码解读复制代码import Koa from 'koa';
const app = new Koa();

// 一个简单的路由处理函数
app.use(async ctx => {
  ctx.body = 'Hello, World!';
});

// 启动服务器
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

因为我们需要在浏览器中测试请求,所以服务端还需要支持浏览器跨域,所以我们添加一个支持跨域的中间件:

js 代码解读复制代码
app.use(async (ctx, next) => {
    ctx.set('Access-Control-Allow-Origin''*'); // 允许所有来源
    ctx.set('Access-Control-Allow-Methods''GET, POST, PUT, DELETE, OPTIONS');
    ctx.set('Access-Control-Allow-Headers''Content-Type, Authorization');
    
    if (ctx.request.method === 'OPTIONS') {
      ctx.status = 200;
      return;
    }
    
    await next();
  });

2. 准备浏览器和node端测试环境:

我们初始化基础的测试 html文件以及 node 文件:

html 代码解读复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="./index.js"></script>
</body>
</html>
js 代码解读复制代码
console.log('基础的浏览器测试环境')

基础的 node 测试环境比较简单,就是一个普通的 js 文件,只要我们上述的 js 文件不包含浏览器端的宿主 api 那么也可以直接在 node 端进行测试。 整个结构搭建完成之后应该就是下面的文件格式:

image.png

开发 axios 的入口模块:

我们在测试文件夹下面新建一个 axios.js 的文件,入口内容开发比较简单,我们就不再过多赘述了,主要就是开发一个 Axios 类,初始化 axios 工厂函数以及导出 axios 实例:

js 代码解读复制代码
// util.js

/**
 * 
 * @param {Object} config1 
 * @param {Object} config2 
 */


export const mergeConfig = (config1, config2) => {
  return Object.assign(config1, config2)
}
js 代码解读复制代码
import { mergeConfig } from './utils.js'

class Axios {

 constructor(defaultConfig) {
    this.defaultConfig = defaultConfig
 }

/**
 * 
 * @param {string} url 
 * @param {Object} options 
 */

  requiest(url, options) {
    try {
      this._requiest(url, options)
    } catch (error) {

    }
  }
  /**
 * 
 * @param {string} url 
 * @param {Object} options 
 */

  _requiest(url, options) {
    console.log('开始发送请求', url, options)
  }
}

/**
 * 
 * @param {Object} defaultConfig  axios 的基础配置
 */


function createInstance(defaultConfig{
  // 初始化 axios 实例
  const context = new Axios(defaultConfig)
  const instance = Axios.prototype.requiest.bind(context)
  // 实例上挂手动挂载一个 create 方法
  instance.create = function create(instanceConfig{
    // 将用户传入的配置和默认配置进行合并
    return createInstance(mergeConfig(defaultConfig, instanceConfig))
  };
  return instance
}

// 基于默认配置,利用工厂函数创建出一个默认的 axios 实例

const axios = createInstance({
  // 默认的网络延迟时间
  timeout0,
  // adapter: 默认的适配器配置
  adapter: ["xhr""http""fetch"],
  // 基础路径
  beseURL"",
  headers: {}
});

// 给 axios 添加一系列其他配置

axios.Axios = Axios;
axios.default = axios;

export default axios

axios 入口核心代码其实比较简单,最核心的就是利用工厂函数创建出一个最基础的 request 请求方法。如果我们不需要进行额外的自定义配置,那么 axios 本身就已经可以开箱即用了。如果我们调用 create,本质上就是合并用户自定义的 axios 配置然后重新产生一个 requiest 方法。 开发完毕之后,我们就可以在之前已经准备好的测试文件中导入 axios 实例来进行测试了:

js 代码解读复制代码
import axios from './axios.js'

axios('http://localhost:3000/')
image.png

浏览器中最基础的测试已经可以了。

我们看一下node环境:

image.png

至此基础的开发和测试就完毕了。

利用参数归一化的技巧处理  _requiest 的参数问题:

参数归一化是 js 这种弱类型语言中一种常见的统一函数入参的方法,好处就是减少主干函数中对于参数校验的判断逻辑,统一函数参数的类型,让主干函数的代码更加清爽:

js 代码解读复制代码
  /**
   *  参数归一化的辅助函数
   * @param {string} url
   * @param {Object} options
   */


  requestHelper(url, options) {
    let config = {}
    if (typeof url === 'string') {
        config = options || {};
        config.url = url;
      } else if (typeof url === 'object') {
        config = url;
      }
      return config
  }
  
    /**
   *
   * @param {string} url
   * @param {Object} options
   */

  _requiest(url, options) {
    // 首先进行参数归一化,将所有的参数全部统一为一个配置对象
    const config = this.requestHelper(url, options)
    console.log('config', config)
  }

我们来测试一下输出:

image.png

我们可以看到参数就已经统一成为一个对象了。 在统一完毕 _requiest 这个函数的参数之后,因为现在 Axios 这个类中存在一个 defaultConfig 的默认配置,而 _requiest 本身又可以接收一个配置对象,所以我们可以将将这两个配置进行简单的合并:

js 代码解读复制代码
  /**
   *
   * @param {string} url
   * @param {Object} options
   */

  _requiest(url, options) {
    // 首先进行参数归一化,将所有的参数全部统一为一个配置对象
    const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
    console.log('最终的配置', config)
  }
image.png

多环境请求发送的问题处理:

浏览器环境中两个发送请求的方案:

  1. xhr
  2. fetch

但是如果是比较旧的node环境的话这两种请求方案都不支持,node环境中原生支持的请求库是 http 以及 https 模块。 axios作为一个通用的http请求库必须要解决这个问题。为此,axios 提出了适配器的概念,axios中所有的请求的发送都是基于这个适配器来进行发送的,源码中专门有一个模块来处理请求适配的问题:

image.png

适配器的思想其实很简单,就是判断哪一套请求 api 存在,那么就使用那一套请求 api。这个和 vue 内部 nextTick 异步模块的处理方案是一致的。 我们来简单实现一下适配器的核心逻辑:

我们先新建一个 dispatchRequest 模块,并且在这个模块中统一发送 axios 请求:

js 代码解读复制代码

/**
 * 
 * axios 统一进行 http 请求发送的模块
 * 
 * @param {Object} config 
 */

export default function dispatchRequest(config{
  console.log('开始请求发送', config)
}

我们将这个模块导入到 axios 主模块中,并且在 _request 函数中进行调用:

js 代码解读复制代码
import dispatchRequest from './dispatchRequest.js'

  /**
   *
   * @param {string} url
   * @param {Object} options
   */

  _requiest(url, options) {
    // 首先进行参数归一化,将所有的参数全部统一为一个配置对象
    const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
    // 调用 dispatchRequest 方法进行请求的发送和处理,并且将合并之后的配置传入
    dispatchRequest(config)
  }

看一下控制台我们就可以发现请求方法被成功调用了:

image.png

接着我们就需要在 dispatchRequest 方法中处理多环境请求适配的问题了。

首先我们要明白一个点,axios中的请求器是允许用户自定义的,用户只需要在配置中指定 adapter 配置,传入一个 () => Promise 类型的函数就可以了。因此我们新建一个 Adapte.js 的文件:

js 代码解读复制代码
export default {
    /**
     * 获取请求适配器的方法
     * @param {Function | Function[]} adapters 
     */

    getAdapter(adapters) {

    }
}

这里的参数类型需要同时支持支持一个函数或者函数数组,我们同样可以进行参数归一化:

js 代码解读复制代码
// getAdapte 参数归一化

/**
 * 获取请求适配器的方法
 * @param {Function | Function[]} adapters
 */


const getAdapteHandlers = (adapters) => {
    return  Array.isArray(adapters) ? adapters : [adapters]
}

export default {
  /**
   * 获取请求适配器的方法
   * @param {Function | Function[] | string[]} adapters
   */

  getAdapter(adapters) {
    // 参数归一化
    adapters = getAdapteHandlers(adapters)
  },
}

axios内部默认的适配器配置是这样指定的:

用户只需要将这个配置替换掉:

那么原本默认的适配器就会被替换:

image.png

然后我们将 adapter 配置传入到 adapate.getAdapter 函数中:

js 代码解读复制代码
export default function dispatchRequest(config{
  // 利用适配器获取当前环境的请求 api
  adapate.getAdapter(config.adapter)
}

此时我们就可以在 adapate.getAdapter 函数中获取到最终的适配器的配置了: 因为参数归一的缘故,已经被统一为一个数组结构了:

image.png

我们目前先不测试自定义适配器的情况,所以我们先将基础配置复原:

image.png

那么我们拿到的适配器配置就是基础的配置:

image.png

我们先针对基础配置进行处理,我们先梳理一下适配器中需要处理的一些基础问题:

  1. 首先需要提供出 xhr, http, fetch 这三种请求方案对应的请求方法。
  2. 查找当前环境支持的第一个请求类型,使用基于该请求类型封装的请求方法来作为请求方案。

我们在这里就不把所有的请求方案都写出来了,我们先以 xhr 为例子来封装一个请求模块: 我们新建一个 xhr 模块,先添加上如下代码:

js 代码解读复制代码
// 首先判断当前环境是否存在 xhr 模块

const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'

这个判定就是为了判断当前宿主环境是否支持 XMLHttpRequest 这个构造函数

紧接着如果支持的话,那么我们就可以返回一个Promise函数,在函数中封装 xhr 的逻辑:

js 代码解读复制代码
export default isXHRAdapterSupported && function (config{
   return Promise((resplved, rejected) => {
   
   })
}

至于里面细枝末节的内容我就不过多赘述了,大家感兴趣可以去查阅代码,总体上比较简单。 封装好了 xhr 模块之后,我们就在 adapate 适配器中导入该模块,并且进行简单配置:

js 代码解读复制代码
import xhr from './xhr.js'

// 假如目前的浏览器比较古老,只支持 xhr

const adapteConfig = {
    xhr,
    fetchundefined,
    httpundefined
}

紧接着,我们需要来编写查找当前环境支持的第一个请求方法的逻辑:

js 代码解读复制代码
  /**
   * 获取请求适配器的方法
   * @param {Function | Function[] | string[]} adapters
   */

  getAdapter(adapters) {
    // 参数归一化
    adapters = getAdapteHandlers(adapters)
    let handler = null
    for (const adapter of adapters) {
        handler = adapteConfig[adapter]
        if (handler) {
            // 当前已经找到合适的适配器,那么不需要继续查找了
            break
        }
    }
    return handler
  },

至此我们就处理完毕默认处理器的情况了,但如果是用户自定义了处理器呢,我们还需要进一步适配这种情况,其实总体很简单:

js 代码解读复制代码
  /**
   * 获取请求适配器的方法
   * @param {Function | Function[] | string[]} adapters
   */

  getAdapter(adapters) {
    // 参数归一化
    adapters = getAdapteHandlers(adapters)
    let handler = null
    for (const adapter of adapters) {
        if (typeof adapter === 'function') {
            // 支持用户自定义处理器的情况
            handler = adapter
        } else {
            handler = adapteConfig[adapter]
        }
        if (handler) {
            // 当前已经找到合适的适配器,那么不需要继续查找了
            break
        }
    }
    return handler
  },

至此我们就把 axios 中的适配器的实现方案的核心逻辑探讨完毕了。

kotlin 代码解读复制代码## axios 拦截器实现方案:

axios 拦截器实现方案其实并没有太多特别的地方,和大部分开源库中实现异步任务调度是类似的方案,总体上是以下的思路:

1. 实现拦截器的注册逻辑:


```js

class InterceptorManager {
  constructor() {
    this.handlers = [];
  }
  use(fulfilled, rejected, options) {
    this.handlers.push({
      fulfilled,
      rejected,
      synchronous: options ? options.synchronous : false,
      runWhen: options ? options.runWhen : null
    })
    return this.handlers.length - 1
  }
}

export default InterceptorManager

在 Axios 类中导入该模块,初始化拦截器存储容器

js 代码解读复制代码
class Axios {
  /**
   *
   * @param {Object} defaultConfig
   */


  constructor(defaultConfig) {
    this.defaultConfig = defaultConfig;
    // 初始化拦截器容器
    this.interceptors = {
      requestnew InterceptorManager(),
      responsenew InterceptorManager()
    };
  }
  }
  
  function createInstance(defaultConfig{
  // 初始化 axios 实例
  const context = new Axios(defaultConfig);
  const instance = Axios.prototype.requiest.bind(context);
  // 实例上挂手动挂载一个 create 方法
  instance.create = function create(instanceConfig{
    // 将用户传入的配置和默认配置进行合并
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };
  // 获取到当前 axios 对象上的拦截器
  instance.interceptors = context.interceptors;
  return instance;
}

核心就是在 Axios 实例上挂载了拦截器对象。

然后在使用的过程中注册请求和响应拦截器

js 代码解读复制代码
const instance = axios.create({
  uri'http://localhost:3000/',
  // adapter: () => new Promise((resolve, reject) => {
  //   resolve('自定义内容')
  // })

})

// console.log('instance', instance.interceptors)
// 添加请求拦截器
instance.interceptors.request.use(
  function (config{
    // 在发送请求之前做些什么,例如添加 token 到请求头
    config.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`
    return config;
  },
  function (error{
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)

// 添加响应拦截器
instance.interceptors.response.use(
  function (response{
    // 对响应数据做点什么
    return response
  },
  function (error{
    // 对响应错误做点什么,例如处理 401 错误
    if (error.response && error.response.status === 401) {
      // 清除本地存储的 token,并跳转到登录页面
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)
  

紧接着我们开始在 _request 中处理请求和响应拦截器的内容: 我们先将请求和响应拦截器打印出来:

js 代码解读复制代码
  /**
   *
   * @param {string} url
   * @param {Object} options
   */

  _requiest(url, options) {
    // 首先进行参数归一化,将所有的参数全部统一为一个配置对象
    const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
    // 开始处理请求和响应拦截器的内容
    console.log('获取到的请求和响应拦截器'this.interceptors)
    // 调用 dispatchRequest 方法进行请求的发送和处理,并且将合并之后的配置传入
    dispatchRequest(config)
  }

image.png

我们可以看到请求和响应拦截器注册进来了。

  1. 开始进行拦截器和请求方法的任务编排:

所谓任务编排其实很简单,就是在底层维护一个任务队列来处理一系列任务,队列类似于下面这样:

js 代码解读复制代码
[
  请求拦截器1成功方法,
  请求拦截器1失败方法,
  请求方法,
  undefined,
  响应拦截器1成功方法,
  响应拦截器1失败方法
]

然后从头到尾循环这个队列,每一次循环都取出当前队列的头两位,并且使用 Promise.then 将其注册为当前任务阶段成功和失败的处理函数。然后浏览器主线程任务执行完毕之后会依次执行 Promise.then 注册的微任务。 理解了思路,代码实现就非常简单了:

js 代码解读复制代码
  /**
   *
   * @param {string} url
   * @param {Object} options
   */

  _requiest(url, options) {
    // 首先进行参数归一化,将所有的参数全部统一为一个配置对象
    const config = mergeConfig(this.defaultConfig, this.requestHelper(url, options))
    // 开始处理请求和响应拦截器的内容
    console.log('获取到的请求和响应拦截器'this.interceptors)
    // 调用 dispatchRequest 方法进行请求的发送和处理,并且将合并之后的配置传入
    const chain = [dispatchRequest, undefined]
    // 开始进行任务编排
    this.interceptors.request.handlers.forEach(function unshiftRequestInterceptors(interceptor{
      chain.unshift(interceptor.fulfilled, interceptor.rejected);
    })
    this.interceptors.response.handlers.forEach(function pushResponseInterceptors(interceptor{
      chain.push(interceptor.fulfilled, interceptor.rejected)
    })
    // 开始注册异步任务, 注意这里很重要,将 config 配置对象按照 Promise 链式调用的参数一直传递给后续的任务去处理
    let promise = Promise.resolve(config)
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift())
    }
  }

调整 dispatchRequest 函数,调用适配器发送请求:

js 代码解读复制代码

import adapate from './Adapte.js'

/**
 * 
 * axios 统一进行 http 请求发送的模块
 * 
 * @param {Object} config 
 */

export default function dispatchRequest(config{
  // 利用适配器获取当前环境的请求 api
  const adapter = adapate.getAdapter(config.adapter)
  return adapter(config)
}

调整 xhr 适配器,简单返回一个结果:

js 代码解读复制代码
// 首先判断当前环境是否存在 xhr 模块

const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined'

export default isXHRAdapterSupported && function (config{
    if (!isXHRAdapterSupported) {
        throw new Error('XMLHttpRequest is not supported');
    }
    return new Promise((resolved, rejected) => {
        const xhr = new XMLHttpRequest()
        console.log('config.url', config)
        xhr.open(config.method || 'GET', config.uri, true)
        xhr.onreadystatechange = () => {
          if (xhr.readyState === 4) {
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
              resolved(xhr.responseText);
            } else {
              rejected(new Error(`Request failed with status ${xhr.status}`));
            }
          }
        };
        xhr.onerror = () => {
          rejected(new Error('Network error'));
        };
        if (config.headers) {
          Object.keys(config.headers).forEach(key => {
            xhr.setRequestHeader(key, config.headers[key]);
          });
        }
        xhr.send(config.data || null);
    })
}

我们在测试代码中加入一些测试log:

js 代码解读复制代码
instance.interceptors.request.use(
  function (config{
    // 在发送请求之前做些什么,例如添加 token 到请求头
    config.headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`
    console.log('请求拦截器', config)
    return config;
  },
  function (error{
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)

// 添加响应拦截器
instance.interceptors.response.use(
  function (response{
    console.log('响应拦截器', response)
    // 对响应数据做点什么
    return response
  },
  function (error{
    // 对响应错误做点什么,例如处理 401 错误
    if (error.response && error.response.status === 401) {
      // 清除本地存储的 token,并跳转到登录页面
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

image.png

可以看到整个流程按照预期的执行了

至此我们就搞清楚了 axios 拦截器的设计哲学了。

axios 取消请求的实现方案

axios 取消请求的实现我们在这里就不一行一行的写代码了,我们直接去源码中一探究竟:

  1. 首先我们简单梳理一下 axios 的源码结构:

axios 的核心代码就分模块存在放 lib 目录下面:

image.png

lib目录下面几个核心文件夹的作用分别是:

js 代码解读复制代码
adapters:适配器相关逻辑
cancel: 请求取消相关功能
core: axios 核心代码
   Axios.js Axios 核心类
   dispatchRequest: 请求发送的核心模块,和我们手写的核心逻辑类似
helpers: 工具方法

其他几个模块的核心设计原则,我们在前面已经大体上了解过来,接下来我们要把目光聚焦到 cancel 模块中。

  1. 回顾 axios 请求取消的使用方式(以 ### CancelToken 为例,其他方式大家自行查阅):
js 代码解读复制代码
import axios from 'axios';
 const source = axios.CancelToken.source(); 
 axios.get('/api/data', {
    cancelToken: source.token
    })
    .catch(thrown => { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // handle error } }); // 取消请求 
    
    source.cancel('取消请求的原因');

核心其实就是三步:

  1. 创建 CancelToken.source 对象
  2. 将该对象中的 token 配置到 cancelToken 上
  3. 在指定的时候调用 source.cancel 方法

我们先来看一下 CancelToken 这个的核心逻辑:

js 代码解读复制代码

class CancelToken 
   
   construcoer(sxsc) {
    // 缓存改变 promise 状态的方法
    let resolvePromise;
    this.promise = new Promise(function promiseExecutor(resolve{
      resolvePromise = resolve;
    });
    // 注册异步微任务
    this.promise.then(cancel => {
      // 没有订阅任务,那么直接退出
      if (!token._listeners) return;
      let i = token._listeners.length;
      // 依次执行订阅任务
      while (i-- > 0) {
        token._listeners[i](cancel);
      }
      token._listeners = null;
    })
    
    // 注册任务
    
  subscribe(listener) {
    if (this.reason) {
      listener(this.reason);
      return;
    }

    if (this._listeners) {
      this._listeners.push(listener);
    } else {
      this._listeners = [listener];
    }
  }
    
    // 执行回调方法
    
    executor(function cancel(message, config, request{
      if (token.reason) {
        // Cancellation has already been requested
        return;
      }

      token.reason = new CanceledError(message, config, request);
      resolvePromise(token.reason);
    });
    
   }
   
    static source() {
    let cancel;
    const token = new CancelToken(function executor(c{
      cancel = c;
    });
    return {
      token,
      cancel
    };
}

从以上的核心代码我们可以看出

  1. cancelToken 本质上就是一个 CancelToken 的实例,因此我们可以调用 CancelToken 原型上的 subscribe 方法来注册一个异步任务。
  2. CancelToke 内部通过 Promise 微任务的方式来管理了一个任务队列,任何通过 subscribe 注册的任务都会在该 Promise 对象的状态完成之后得到执行。
  3. cancel 方法做的最核心的一件事情就是 resolvePromise(token.reason) 将上面聊到的 Promise 对象的状态变成已完成,从而将 subscribe 注册的任务全部推入微任务队列去进行执行。
  4. CancelToken 底层实际上就是实现了一个典型的发布订阅模式,外部的模块可以通过 subscribe 方法来注册一系列时间,然后通过调用 cancel 执行这些事件。

梳理了 CancelToken 模块的实现逻辑,我们再来看请求取消的实现方案就很好理解了, 我们在创建 axios 请求任务的时候传入了 cancelToken: source.token 实际上就是把 CancelToken 对象传入到了配置中,然后 axios 内部会在某一个模块中通过 cancelToken.subscribe 的方法注册一个请求取消的事件,最后我们在需要的地方调用cannal方法触发事件的调用从而取消请求。

实际上我们查看 xhr 模块就可以看到在模块中 axios 通过 cancelToken.subscrib 注册了请求取消事件:

image.png

另外如果我们查看 dispatchReques 源码,我们可以看到,如果我们进行了请求取消相关配置,在请求发布的每一个阶段,axios都会调用 throwIfCancellationRequested 方法来检查请求的取消状态,如果发现某一个阶段请求和已经取消了,那么这个阶段以及以后的额任务都不会继续执行了:

image.png
image.png

最后我们在扩展一个点,就是在浏览环境,我们可以使用 controller.signal; 方式来配置请求取消,这也是新版 axios 更加推荐的一种方案,controller.signal是浏览器提供的通用网络信号管理接口,不过它不能直接和xhr通信,只能和fetch直接通信,不过新版的axios已经直接支持了fetch请求,这种方式将更加方便高效。node环境目前 http 模块暂时还没有提供直接取消已经发出的 http 请求的接口,所以目前无法终止已经发送的http请求。

至此我们就了解完毕了 axios 中最核心的设计原理。

ts 中模板模式实现通用请求处理的方案(扩展):

在企业中我们目前一般使用 ts 来编写底层的库,而 ts 为我们提供了接口和类型更加强大的抽象类,基于这些高级语法,我们可以使用模板模式更好的实现跨平台的通用请求逻辑。 总体实现方案如下:

比如我们现在需要封装一个通用的业务请求库,这个库底层的依赖的基础请求库可能是 axios,也可能是传统的 fetch,也可能是针对 Vue 框架的 VueRequest,也可能是针对react的 useSWR 等等,我们可能需要根据不同的业务场景进行灵活的切换。但是在切换的过程中,我们可能希望做到在使用层面无感,使用层面的 api 统一。比如在基于 axios 来进行请求的时候我们这样用:

js 代码解读复制代码
import { request } from 'my-request'

request('xxx', {
  ...xxx
})

在基于 vueRequest 时候我们也希望可以直接这样使用:

js 代码解读复制代码
import { request } from 'my-request'

request('xxx', {
  ...xxx
})

也就是这个库底层需要对用户完全屏蔽各种底层请求库存的差异,不管是基于什么样的底层请求库,api还是照样的调用,代码还是照样的写。为了实现这一点,我们就可以这样去进行设计:

  1. 暴露一个标准的 request 方法,不论在怎样的情况下,发送请求都是基于这一个统一的请求方法,并且存在统一侧参数类型。
  2. 暴露一个标准的 interface RequestHandler 接口,任何需要注册到请求库中的模块都需要按照标准统一的接口进行设计。
  3. 按照 RequestHandler 接口的约束来封装对应的请求模块。
  4. request上暴露一个对外的 use 方法,用户调用这个方法可以将指定的请求模块进行注册。
  5. 用户需要将默认的请求库 AxiosFetch替换为指定的请求库使用指定的 请求库 VueRequestFetch的时候,只需要导入基于统一的接口封装的 VueRequestFetch 模块,然后调用 use 函数将该模块进行注册就可以了,其他的业务代码完全不需要替换。

request 库最核心的代码类似于下面这样:

ts 代码解读复制代码
import AxiosFetch from '@/xxx/AxiosFetch'

// 定义接口

export interface RequestHandler {
   get(xxx) {
     
   },
   post(xxx) {
     
   }
}

let useHandler: RequestHandler = AxiosFetch

export interface RequestOptions {
....
}

export const request(options: RequestOptions) {
    // 利用 useHandler 处理请求
  useHandler.get()
}

request.use = (handler: RequestHandler) {
  // 注册
   useHandler = handler
}

在使用的时候极其简单

js 代码解读复制代码
import { request } from '@xxx/core'
import VueRequestFetch from '@xxx/VueRequestFetch'

// 注册自定义请求库

const { use } = request

use(VueRequestFetch)

request('xxx', {})

request.get('xxx', {})

作者:文学与代码 

链接:https://juejin.cn/post/7388316163578363916


加我微信,拉你进前端进阶、面试交流群,互相监督学习进步等!

❤️ 看完三件事

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 关注我的博客 https://github.com/qappleh/Interview,让我们成为长期关系

  3. 关注公众号「深圳湾码农」,持续为你推送精选好文,回复「加群」加入面试互助交流群

点一下,代码无 Bug

深圳湾码农
分享大前端最新技术、BAT大厂面试题、程序员轶事、职场成长等,你想要的这里都有.
 最新文章