【JavaScript】fetch的模拟封装(基于promise封装的ajax方法)

近期使用Worker线程,发现在其内部无法调用fetch方法,但可以使用原生的XMLHttpRequest对象、Headers对象、Request对象、Response对象、URLSearchParams对象,所以就模拟封装了一个可在Worker线程使用的 ajax 函数。

一、源码

/**
 * [基于 promise 封装的 ajax 方法]
 * @param {string} url - [路径]
 * @param {object} init - [配置对象,还不是很完善]
 * return - 若成功则调用 resolve 并向 then() 传递结果。若失败则调用 reject 并传递错误对象
 * 
 * init 可配置属性:
 *  method - 请求方法,默认 'GET',不区分大小写
 *  body - 传递参数,'GET / HEAD' 请求不许有参数
 *  headers - 头信息,该怎么写就这么写
 * 
 * Tips:
 *  1. 基于 promise 的简单封装,所以可以使用 then() 方法
 *  2. 可以在 worker 线程使用(将该函数放在 worker 线程再调用)
 */
function _fetch(url, init) {
  return new Promise((resolve, reject) => {
    // 若空参则直接返回 reject()
    if (!url && !init) {
      // 判断当前线程
      let thread = '';
      if (/window/gi.test(self.constructor.name)) {
        thread = 'Window';
      } else if (/worker/gi.test(self.constructor.name)) {
        thread = 'Worker';
      }

      // 创建错误对象
      const err = new TypeError(`Failed to execute '_fetch' on '${thread}': 1 argument required, but only 0 present.`);

      // 返回 reject()
      return reject(err);
    }

    // 初始化请求方法
    let method = !!init && !!init.method ? init.method : 'GET';
    // 初始化传递参数
    let params = null;
    
    // 创建请求实例
    const xhr = new XMLHttpRequest();

    // 初始化请求
    xhr.open(method, url, true);

    // 设置头信息
    if (!!init && !!init.headers) {
      // 遍历添加头信息
      for (let key in init.headers) {
        // 过滤非自身属性
        if (init.headers.hasOwnProperty(key)) {
          xhr.setRequestHeader(key, init.headers[key]);
        }
      }
    }

    // 设置请求得到的数据类型
    // 选blob,因为 Response 实例有 text()/json()/blob() 等方法进行转换
    xhr.responseType = 'blob';

    // 响应
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        // 请求成功
        if ( (xhr.status >= 200 && xhr.status < 300)
        || (xhr.status === 304) ) {
          // 创建 Response 实例
          const response = new Response(this.response);

          // 返回 resolve() 并传递 Response 实例给 then()
          return resolve(response);
          
        // 请求失败
        } else {
          // 错误提示字符串内容
          const err = new TypeError('Failed to _fetch');

          // 返回 reject() 并传递 Error 实例
          return reject(err);
        }
      }
    };

    // GET / HEAD 请求不能发送 data
    if (!!init && !!init.body
    && method.toUpperCase() !== 'GET'
    && method.toUpperCase() !== 'HEAD'){
      // 处理json格式与字符串两种参数
      if (({}).toString.call(init.body) === '[object Object]') {
        params = new URLSearchParams(init.body);
      } else {
        params = init.body;
      }
    }

    // 发送参数
    xhr.send(params);
  });
}

// ----------------- 示例 -----------------

// 1. 获取 blob
// _fetch('./demo.jpg')
// .then(res => res.blob())
// .then(res => conole.log(res));

// 2. 获取 json
// _fetch('./demo.json')
// .then(res => res.json())
// .then(res => console.log(res));

// 3. 获取 string
// _fetch('./demo.txt')
// .then(res => res.text())
// .then(res => console.log(res));

二、使用方法

1. 在主线程中使用

在这里插入图片描述

<!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>
  <div class="demo">
    <div class="txt"></div>
    <div class="pic"></div>
  </div>
  <script>
    // 获取 json 数据
    // _fetch('https://api.btstu.cn/yan/api.php?charset=utf-8&encode=json')
    // .then(res => res.json())
    // .then(json => {
    //   console.log(json);
    //   document.querySelector('.txt').innerHTML = json.text;
    // });

    // 获取 blob 数据
    // _fetch('https://unsplash.it/1600/900')
    // .then(res => res.blob())
    // .then(blob => {
    //   console.log(blob);
    //   const img = document.createElement('img');
    //   img.src = URL.createObjectURL(blob);  // 生成 URL 实例
    //   img.onload = function () {
    //     this.width = 500;
    //     document.querySelector('.pic').appendChild(this);
    //     URL.revokeObjectURL(this.src);  	// 释放 URL 实例
    //   }
    // });

    
    // 使用 async / await 语法改写,使得语义更清晰
    (async function() {
      // json
      const response1 = await _fetch('https://api.btstu.cn/yan/api.php?charset=utf-8&encode=json');
      const json = await response1.json();
      document.querySelector('.txt').innerHTML = json.text;
      
      // blob
      const response2 = await _fetch('https://unsplash.it/1600/900');
      const blob = await response2.blob();
      const img = document.createElement('img');
      img.src = URL.createObjectURL(blob);  // 生成 URL 实例
      img.onload = function () {
        this.width = 500;
        document.querySelector('.pic').appendChild(this);
        URL.revokeObjectURL(this.src);  	// 释放 URL 实例
      }
    })();


    // _fetch 封装
    function _fetch(url, init) {
      return new Promise((resolve, reject) => {
        // 若空参则直接返回 reject()
        if (!url && !init) {
          // 判断当前线程
          let thread = '';
          if (/window/gi.test(self.constructor.name)) {
            thread = 'Window';
          } else if (/worker/gi.test(self.constructor.name)) {
            thread = 'Worker';
          }

          // 创建错误对象
          const err = new TypeError(`Failed to execute '_fetch' on '${thread}': 1 argument required, but only 0 present.`);

          // 返回 reject()
          return reject(err);
        }

        // 初始化请求方法
        let method = !!init && !!init.method ? init.method : 'GET';
        // 初始化传递参数
        let params = null;
        
        // 创建请求实例
        const xhr = new XMLHttpRequest();

        // 初始化请求
        xhr.open(method, url, true);

        // 设置头信息
        if (!!init && !!init.headers) {
          // 遍历添加头信息
          for (let key in init.headers) {
            // 过滤非自身属性
            if (init.headers.hasOwnProperty(key)) {
              xhr.setRequestHeader(key, init.headers[key]);
            }
          }
        }

        // 设置请求得到的数据类型
        // 选blob,因为 Response 实例有 text()/json()/blob() 等方法进行转换
        xhr.responseType = 'blob';

        // 响应
        xhr.onreadystatechange = function () {
          if (xhr.readyState === 4) {
            // 请求成功
            if ( (xhr.status >= 200 && xhr.status < 300)
            || (xhr.status === 304) ) {
              // 创建 Response 实例
              const response = new Response(this.response);

              // 返回 resolve() 并传递 Response 实例给 then()
              return resolve(response);
              
            // 请求失败
            } else {
              // 错误提示字符串内容
              const err = new TypeError('Failed to _fetch');

              // 返回 reject() 并传递 Error 实例
              return reject(err);
            }
          }
        };

        // GET / HEAD 请求不能发送 data
        if (!!init && !!init.body
        && method.toUpperCase() !== 'GET'
        && method.toUpperCase() !== 'HEAD'){
          // 处理json格式与字符串两种参数
          if (({}).toString.call(init.body) === '[object Object]') {
            params = new URLSearchParams(init.body);
          } else {
            params = init.body;
          }
        }

        // 发送参数
        xhr.send(params);
      });
    }
  </script>
</body>
</html>

2. 在 Worker 线程中使用

在这里插入图片描述

  • index.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>
  <div class="demo">
    <div class="txt"></div>
    <div class="pic"></div>
  </div>
  <script>
    const worker = new Worker('work.js');
    worker.onmessage = function (e) {
      console.log(e.data[0], e.data[1]);
      // txt
      document.querySelector('.txt').innerHTML = e.data[0].text;

      // pic
      const img = document.createElement('img');
      img.src = URL.createObjectURL(e.data[1]);  // 生成 URL 实例
      img.onload = function () {
        this.width = 500;
        document.querySelector('.pic').appendChild(this);
        URL.revokeObjectURL(this.src);  	// 释放 URL 实例
      }
    }
  </script>
</body>
</html>
  • work.js
// 使用 async / await 语法改写,使得语义更清晰
(async function() {
  // json
  const response1 = await _fetch('https://api.btstu.cn/yan/api.php?charset=utf-8&encode=json');
  const json = await response1.json();
  
  // blob
  const response2 = await _fetch('https://unsplash.it/1600/900');
  const blob = await response2.blob();

  // 方便主线程读取
  const data = [json, blob];

  self.postMessage(data);
})();


// _fetch 封装
function _fetch(url, init) {
  return new Promise((resolve, reject) => {
    // 若空参则直接返回 reject()
    if (!url && !init) {
      // 判断当前线程
      let thread = '';
      if (/window/gi.test(self.constructor.name)) {
        thread = 'Window';
      } else if (/worker/gi.test(self.constructor.name)) {
        thread = 'Worker';
      }

      // 创建错误对象
      const err = new TypeError(`Failed to execute '_fetch' on '${thread}': 1 argument required, but only 0 present.`);

      // 返回 reject()
      return reject(err);
    }

    // 初始化请求方法
    let method = !!init && !!init.method ? init.method : 'GET';
    // 初始化传递参数
    let params = null;
    
    // 创建请求实例
    const xhr = new XMLHttpRequest();

    // 初始化请求
    xhr.open(method, url, true);

    // 设置头信息
    if (!!init && !!init.headers) {
      // 遍历添加头信息
      for (let key in init.headers) {
        // 过滤非自身属性
        if (init.headers.hasOwnProperty(key)) {
          xhr.setRequestHeader(key, init.headers[key]);
        }
      }
    }

    // 设置请求得到的数据类型
    // 选blob,因为 Response 实例有 text()/json()/blob() 等方法进行转换
    xhr.responseType = 'blob';

    // 响应
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        // 请求成功
        if ( (xhr.status >= 200 && xhr.status < 300)
        || (xhr.status === 304) ) {
          // 创建 Response 实例
          const response = new Response(this.response);

          // 返回 resolve() 并传递 Response 实例给 then()
          return resolve(response);
          
        // 请求失败
        } else {
          // 错误提示字符串内容
          const err = new TypeError('Failed to _fetch');

          // 返回 reject() 并传递 Error 实例
          return reject(err);
        }
      }
    };

    // GET / HEAD 请求不能发送 data
    if (!!init && !!init.body
    && method.toUpperCase() !== 'GET'
    && method.toUpperCase() !== 'HEAD'){
      // 处理json格式与字符串两种参数
      if (({}).toString.call(init.body) === '[object Object]') {
        params = new URLSearchParams(init.body);
      } else {
        params = init.body;
      }
    }

    // 发送参数
    xhr.send(params);
  });
}

三、Some Tips

  1. Worker 线程无法读取 window 对象,所以无法使用原生 fetch,但可以使用 XMLHttpRequest 对象(还有与AJAX相关的对象:HeadersRequestResponse…),所以可以进行 ajax 操作,在 Worker 线程封装一个 fetch();
  2. Worker 线程无法使用 DOM 对象,所以无法进行 DOM 操作,而需要将数据发送给主线程,再由主线程进行相关 DOM 操作;
  3. 为了尽可能的去还原 fetch,所以在封装里使用了 Response 对象,并将其实例传递给 then(),即,fetch().then() 里可以使用 response.json() 读取数据,在 _fetch().then() 中也是同样的操作(连报错提示也尽可能的去还原了);
    在这里插入图片描述

四、获取 XML 数据

虽然原生的 AJAX(XMLHttpRequest 对象)可以通过 xhr.responseType = 'document' 来规定返回响应数据的类型为 ‘document’ 结构,读取 XML/HTML 数据,但 fetch() 和 _fetch()并没有响应的方法调用,去读取 XML/HTML(是的,就是要尽可能的去还原)。其主要原因是 Response 对象 并未提供对应转换方法。

所以,得借助 DOMParser 对象 去实现获取。

【关于 xhr.responseType,在 _fetch() 源码中设置的是 ‘blob’ 类型,如果更改,将无法正常调用相关读取方法,如:text()、json()、blob()等】

在这里插入图片描述
在这里插入图片描述

XML 部分

  • demo.xml
<?xml version="1.0" encoding="utf-8" ?>
<student>
	<member>
		<name>张三</name>
		<age>14</age>
		<gender></gender>
	</member>
	<member>
		<name>李四</name>
		<age>16</age>
		<gender></gender>
	</member>
	<member>
		<name>王五</name>
		<age>17</age>
		<gender></gender>
	</member>
	<member>
		<name>赵六</name>
		<age>13</age>
		<gender></gender>
	</member>
	<member>
		<name>田七</name>
		<age>15</age>
		<gender></gender>
	</member>
</student>

1. 主线程读取 XML 数据

<!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>
  <div class="xml"></div>
  <script>
    // _fetch('demo.xml')
    // .then(res => res.text())
    // .then(str => {
    //   // 创建解析对象
    //   const parser = new DOMParser();
    //   // 解析 XML 字符串,并传递给下一个 then()
    //   return parser.parseFromString(str, 'application/xml');
    // })
    // .then(data => {
    //   console.log(data.firstChild);
    //   // 获取 XML 第一个子节点,并添加给一个 DOM 元素
    //   document.querySelector('.xml').appendChild(data.firstChild);
    // });


    // 使用 async / await 语法改写,使得语义更清晰
    (async function() {
      // 发送请求
      const response = await _fetch('demo.xml');

      // 获取字符串
      const str = await response.text();

      // 创建解析对象
      const parser = new DOMParser();
      // 解析 XML 字符串,并得到 XML Document
      const xmlDocument = parser.parseFromString(str, 'application/xml');

      // 将 XML 第一个字节点添加给 DOM 元素
      document.querySelector('.xml').appendChild(xmlDocument.firstChild);
    })();


    // _fetch 封装
    function _fetch(url, init) {
      return new Promise((resolve, reject) => {
        // 若空参则直接返回 reject()
        if (!url && !init) {
          // 判断当前线程
          let thread = '';
          if (/window/gi.test(self.constructor.name)) {
            thread = 'Window';
          } else if (/worker/gi.test(self.constructor.name)) {
            thread = 'Worker';
          }

          // 创建错误对象
          const err = new TypeError(`Failed to execute '_fetch' on '${thread}': 1 argument required, but only 0 present.`);

          // 返回 reject()
          return reject(err);
        }

        // 初始化请求方法
        let method = !!init && !!init.method ? init.method : 'GET';
        // 初始化传递参数
        let params = null;
        
        // 创建请求实例
        const xhr = new XMLHttpRequest();

        // 初始化请求
        xhr.open(method, url, true);

        // 设置头信息
        if (!!init && !!init.headers) {
          // 遍历添加头信息
          for (let key in init.headers) {
            // 过滤非自身属性
            if (init.headers.hasOwnProperty(key)) {
              xhr.setRequestHeader(key, init.headers[key]);
            }
          }
        }

        // 设置请求得到的数据类型
        // 选blob,因为 Response 实例有 text()/json()/blob() 等方法进行转换
        xhr.responseType = 'blob';

        // 响应
        xhr.onreadystatechange = function () {
          if (xhr.readyState === 4) {
            // 请求成功
            if ( (xhr.status >= 200 && xhr.status < 300)
            || (xhr.status === 304) ) {
              // 创建 Response 实例
              const response = new Response(this.response);

              // 返回 resolve() 并传递 Response 实例给 then()
              return resolve(response);
              
            // 请求失败
            } else {
              // 错误提示字符串内容
              const err = new TypeError('Failed to _fetch');

              // 返回 reject() 并传递 Error 实例
              return reject(err);
            }
          }
        };

        // GET / HEAD 请求不能发送 data
        if (!!init && !!init.body
        && method.toUpperCase() !== 'GET'
        && method.toUpperCase() !== 'HEAD'){
          // 处理json格式与字符串两种参数
          if (({}).toString.call(init.body) === '[object Object]') {
            params = new URLSearchParams(init.body);
          } else {
            params = init.body;
          }
        }

        // 发送参数
        xhr.send(params);
      });
    }
  </script>
</body>
</html>

2. Worker 线程读取 XML 数据

  • index.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>
  <div class="xml"></div>
  <script>
    const worker = new Worker('work.js');
    worker.onmessage = function (e) {
      // 创建解析对象
      const parser = new DOMParser();
      // 解析 XML 字符串,并得到 XML Document
      const xmlDocument = parser.parseFromString(e.data, 'application/xml');
      // 将 XML 第一个字节点添加给 DOM 元素
      document.querySelector('.xml').appendChild(xmlDocument.firstChild);
    }
  </script>
</body>
</html>
  • work.js
// 使用 async / await 语法改写,使得语义更清晰
(async function() {
  // 发送请求
  const response = await _fetch('demo.xml');

  // 获取字符串
  const str = await response.text();

  // Worker 无法访问 window 对象,自然无法使用 DOMParser()
  self.postMessage(str);
})();


// _fetch 封装
function _fetch(url, init) {
  return new Promise((resolve, reject) => {
    // 若空参则直接返回 reject()
    if (!url && !init) {
      // 判断当前线程
      let thread = '';
      if (/window/gi.test(self.constructor.name)) {
        thread = 'Window';
      } else if (/worker/gi.test(self.constructor.name)) {
        thread = 'Worker';
      }

      // 创建错误对象
      const err = new TypeError(`Failed to execute '_fetch' on '${thread}': 1 argument required, but only 0 present.`);

      // 返回 reject()
      return reject(err);
    }

    // 初始化请求方法
    let method = !!init && !!init.method ? init.method : 'GET';
    // 初始化传递参数
    let params = null;
    
    // 创建请求实例
    const xhr = new XMLHttpRequest();

    // 初始化请求
    xhr.open(method, url, true);

    // 设置头信息
    if (!!init && !!init.headers) {
      // 遍历添加头信息
      for (let key in init.headers) {
        // 过滤非自身属性
        if (init.headers.hasOwnProperty(key)) {
          xhr.setRequestHeader(key, init.headers[key]);
        }
      }
    }

    // 设置请求得到的数据类型
    // 选blob,因为 Response 实例有 text()/json()/blob() 等方法进行转换
    xhr.responseType = 'blob';

    // 响应
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        // 请求成功
        if ( (xhr.status >= 200 && xhr.status < 300)
        || (xhr.status === 304) ) {
          // 创建 Response 实例
          const response = new Response(this.response);

          // 返回 resolve() 并传递 Response 实例给 then()
          return resolve(response);
          
        // 请求失败
        } else {
          // 错误提示字符串内容
          const err = new TypeError('Failed to _fetch');

          // 返回 reject() 并传递 Error 实例
          return reject(err);
        }
      }
    };

    // GET / HEAD 请求不能发送 data
    if (!!init && !!init.body
    && method.toUpperCase() !== 'GET'
    && method.toUpperCase() !== 'HEAD'){
      // 处理json格式与字符串两种参数
      if (({}).toString.call(init.body) === '[object Object]') {
        params = new URLSearchParams(init.body);
      } else {
        params = init.body;
      }
    }

    // 发送参数
    xhr.send(params);
  });
}

五、关于报错

1. 无参调用(还原了 fetch)

  • 主线程调用
    在这里插入图片描述
  • Worker 线程调用
    在这里插入图片描述

2. 请求失败(路径正确,但服务器未返回有效数据)

在这里插入图片描述
在这里插入图片描述

3. 路径错误

在这里插入图片描述


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值