基于原生fetch封装一个带有拦截器功能的fetch,类似axios的拦截器

11,985 阅读8分钟

背景

为什么要给fetch增加拦截器?浏览器提供的fetch方法不够用吗?

是的。原生fetch确实不够用。

在项目中,如果想要在所有的网络请求之前往header中加入权限信息(比如:authorization=xxxx)。用原生fetch的话,你只能在每个fetch请求的时候,header配置中写上authorization=xxxx。或者在请求结果返回来之后,对响应结果做一些特殊处理,在原生fetch中,我们只能在每个请求的结果回来之后,都写一遍特殊处理。这样做起来一点都不优雅,一点都不高级。相信每一个程序员都是一个懒人,能用少量代码实现出更优雅,更健壮程序的话,绝对不用大量代码实现一个很脆弱的程序。

用过axios的人,都知道axios有拦截器功能(axios.interceptors)。遇到需要在所有ajax请求之前或者请求完成之后做一些事情的话,我们就可以在拦截器中写。拦截器可以在发起请求之前拦截请求,对请求做一些处理,然后,再继续请求;也可以在请求完成之后拦截请求,对响应结果做一些处理,然后再把结果返回。

这样做的好处是我们不需要在每个请求的时候,都去写相同的这些代码,可以把全局生效/通用的处理放在拦截器中来实现。可以省去很多代码,也避免了我们在开发时,不小心某一个请求漏掉了做全局处理。

拦截器设计思考

  1. 目的是在原生fetch基础之上增加拦截请求的功能。
  2. 封装后,最终暴露在外面的接口应该于原生fetch的使用方法一致,同时增加拦截器功能
  3. 拦截器的API设计参考axios的拦截器(个人比较喜欢axios的拦截器设计)。

拦截器API使用方法

你可以在then和catch之前拦截请求和响应。

// 添加一个请求拦截器
c_fetch.interceptors.request.use(function (config) {
    // Do something before request is sent
    return config;
  });

// 添加一个响应拦截器
c_fetch.interceptors.response.use(function (response) {
    // Do something with response data
    return response;
  });

拦截器实现方案

  1. 为了保证封装后的fetch暴露在最外面的api跟原生fetch保持一致。所以,我们要暴露到最外面的api应该是个跟fetch接收相同参数的函数。
    我们看下原生fetch方法语法解释和接收参数解释。
语法: 
Promise<Response> fetch(input[, init]); 
参数:
?input
定义要获取的资源。这可能是:
一个 USVString 字符串,包含要获取资源的 URL。一些浏览器会接受 blob: 和 data: 作为 schemes.
一个 Request 对象。

init 可选
一个配置项对象,包括所有对请求的设置。可选的参数有:
method: 请求使用的方法,如 GET、POST。
headers: 请求的头信息,形式为 Headers 的对象或包含 ByteString 值的对象字面量。
body: 请求的 body 信息:可能是一个 Blob、BufferSource、FormData、URLSearchParams 或者 USVString 对象。注意 GET 或 HEAD 方法的请求不能包含 body 信息。
mode: 请求的模式,如 cors、 no-cors 或者 same-origin。
credentials: 请求的 credentials,如 omit、same-origin 或者 include。为了在当前域名内自动发送 cookie , 必须提供这个选项, 从 Chrome 50 开始, 这个属性也可以接受 FederatedCredential 实例或是一个 PasswordCredential 实例。
cache:  请求的 cache 模式: default 、 no-store 、 reload 、 no-cache 、 force-cache 或者 only-if-cached 。
redirect: 可用的 redirect 模式: follow (自动重定向), error (如果产生重定向将自动终止并且抛出一个错误), 或者 manual (手动处理重定向). 在Chrome中,Chrome 47之前的默认值是 follow,从 Chrome 47开始是 manual。
referrer: 一个 USVString 可以是 no-referrer、client或一个 URL。默认是 client。
referrerPolicy: Specifies the value of the referer HTTP header. May be one of no-referrer、 no-referrer-when-downgrade、 origin、 origin-when-cross-origin、 unsafe-url 。
integrity: 包括请求的  subresource integrity 值 ( 例如: sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=)。

原生fetch的详细文档请移步MDN查阅:
developer.mozilla.org/zh-CN/docs/…

从上面的fetch的语法解释和参数解释,我们可以知道fetch是个函数;接收两个参数,第一个参数定义要获取的资源,第二个参数为可选项,一个配置项对象,包括所有对请求的设置;fetch函数分返回结果是一个promise对象。

  1. 我们先来实现封装后,暴露出来的c_fetch函数。
  function c_fetch (input, init = {}) {
    //fetch默认请求方式设为GET
    if(!init.method){
      init.method = 'GET'
    }
    
    //interceptors_req是拦截请求的拦截处理函数集合
    //后面会讲解interceptors_req的定义与实现
    interceptors_req.forEach(interceptors => {
      init = interceptors(init);
    })
    
    //在原生fetch外面封装一个promise,为了在promise里面可以对fetch请求的结果做拦截处理。
    //同时,保证c_fetch函数返回的结果是个promise对象。
    return new Promise(function (resolve, reject) {
      //发起fetch请求,fetch请求的形参是接收上层函数的形参
      fetch(input, init).then(res => {
        //interceptors_res是拦截响应结果的拦截处理函数集合
        //后面会讲解interceptors_res的定义与实现
        interceptors_res.forEach(interceptors => {
          //拦截器对响应结果做处理,把处理后的结果返回给响应结果。
          res = interceptors(res);
        })
        //将拦截器处理后的响应结果resolve出去
        resolve(res)
      }).catch(err => {
        reject(err);
      })
    })

  }
  1. c_fetch 函数实现完了,我们就差拦截器的实现了。我们在 c_fetch 函数的基础上增加interceptors,用来注册拦截器。
  //定义用来存储拦截请求和拦截响应结果的处理函数集合
  let interceptors_req = [], interceptors_res = [];

  function c_fetch (input, init = {}) {
    //fetch默认请求方式设为GET
    if(!init.method){
      init.method = 'GET'
    }
    
    //interceptors_req是拦截请求的拦截处理函数集合
    interceptors_req.forEach(interceptors => {
      init = interceptors(init);
    })
    
    //在原生fetch外面封装一个promise,为了在promise里面可以对fetch请求的结果做拦截处理。
    //同时,保证c_fetch函数返回的结果是个promise对象。
    return new Promise(function (resolve, reject) {
      //发起fetch请求,fetch请求的形参是接收上层函数的形参
      fetch(input, init).then(res => {
        //interceptors_res是拦截响应结果的拦截处理函数集合
        interceptors_res.forEach(interceptors => {
          //拦截器对响应结果做处理,把处理后的结果返回给响应结果。
          res = interceptors(res);
        })
        //将拦截器处理后的响应结果resolve出去
        resolve(res)
      }).catch(err => {
        reject(err);
      })
    })

  }
  
  //在c_fetch函数上面增加拦截器interceptors,拦截器提供request和response两种拦截器功能。
  //可以通过request和response的use方法来绑定两种拦截器的处理函数。
  //use方法接收一个参数,参数为一个callback函数,callback函数用来作为拦截器的处理函数;
  //request.use方法会把callback放在interceptors_req中,等待执行。
  //response.use方法会把callback放在interceptors_res中,等待执行。
  //拦截器的处理函数callback接收一个参数。
  //request拦截器的callback接收的是请求发起前的config;
  //response拦截器的callback接收的是网络请求的response结果。
  c_fetch.interceptors = {
    request: {
      use: function (callback) {
        interceptors_req.push(callback);
      }
    },
    response: {
      use: function (callback) {
        interceptors_res.push(callback);
      }
    }
  }
  1. 最后将整个封装之后,含有拦截器功能的fetch包装为一个插件暴露给开发者使用。
/**
 * c_fetch
 * 基于原生fetch封装了拦截器功能,暴露出来的c_fetch跟原生fetch用法一致,只是增加了拦截器功能。拦截器用法参考axios的拦截器用法。
 * 拦截器: c_fetch.interceptors
 * 注意: 拦截器不拦截reject类型的response结果
 */

  //定义用来存储拦截请求和拦截响应结果的处理函数集合
  let interceptors_req = [], interceptors_res = [];

  function c_fetch (input, init = {}) {
    //fetch默认请求方式设为GET
    if(!init.method){
      init.method = 'GET'
    }
    
    //interceptors_req是拦截请求的拦截处理函数集合
    interceptors_req.forEach(interceptors => {
      init = interceptors(init);
    })
    
    //在原生fetch外面封装一个promise,为了在promise里面可以对fetch请求的结果做拦截处理。
    //同时,保证c_fetch函数返回的结果是个promise对象。
    return new Promise(function (resolve, reject) {
      //发起fetch请求,fetch请求的形参是接收上层函数的形参
      fetch(input, init).then(res => {
        //interceptors_res是拦截响应结果的拦截处理函数集合
        interceptors_res.forEach(interceptors => {
          //拦截器对响应结果做处理,把处理后的结果返回给响应结果。
          res = interceptors(res);
        })
        //将拦截器处理后的响应结果resolve出去
        resolve(res)
      }).catch(err => {
        reject(err);
      })
    })

  }
  
  //在c_fetch函数上面增加拦截器interceptors,拦截器提供request和response两种拦截器功能。
  //可以通过request和response的use方法来绑定两种拦截器的处理函数。
  //use方法接收一个参数,参数为一个callback函数,callback函数用来作为拦截器的处理函数;
  //request.use方法会把callback放在interceptors_req中,等待执行。
  //response.use方法会把callback放在interceptors_res中,等待执行。
  //拦截器的处理函数callback接收一个参数。
  //request拦截器的callback接收的是请求发起前的config;
  //response拦截器的callback接收的是网络请求的response结果。
  c_fetch.interceptors = {
    request: {
      use: function (callback) {
        interceptors_req.push(callback);
      }
    },
    response: {
      use: function (callback) {
        interceptors_res.push(callback);
      }
    }
  }

  export default c_fetch;

总结

本篇文章只是实现了一个最基本的拦截器功能,文章字数有限,没有深入讲解更加成熟的拦截器实现方式。有兴趣的朋友可以阅读下axios的拦截器实现,很有意思的。axios的拦截器会更加完善。

我这里针对fetch的封装也只是最基本的封装,目的是讲解拦截器的实现,没有过于复杂化。上文中暴露出来的c_fetch其实可以在封装一层cc_fetch,用bind方法把c_fetch的方法绑定在cc_fetch上,最后暴露出来的是cc_fetch。这样做的好处是保护了c_fetch的方法不会被外部所影响,篡改等。当然了,这只是我个人的一些看法,不代表所有人。

最后谢谢各位能够坚持阅读到最后,希望您阅读本篇文章能够有所收获。谢谢🙏~