Libon

fetch API 是如何实现的

#JavaScript
学习浏览器内置 fetch API 是如何实现的

ToC

前置

在学习之前 fetch 之前需要提前了解一下以下 API

core

在来到这个章节以前,默认你已经了解过上面的 API 了。那么首先我们知道 fetch 函数接收两个参数并返回一个 Promise,我们先定义一下:

function fetch(input: RequestInfo | URL, init?: RequestInit) {
  return new Promise(function(resolve, reject) {
    const request = new Request(input, init)

    // 如果这个请求已经被终止了则不继续
    if (request.signal && request.signal.aborted) {
      return reject(new DOMException('Aborted', 'AbortError'))
    }

    // 创建一个 xhr 请求对象
    const xhr = new XMLHttpRequest()

    // 当请求完成时
    xhr.onload = function () {
      const headers = new Headers(init?.headers || {})
      const options = {
        headers,
        status: xhr.status,
        statusText: xhr.statusText,
        url: 'responseURL' in xhr ? xhr.responseURL : headers.get('X-Request-URL')
      }

      const body = xhr.response || xhr.responseText

      // 如果状态正常, 并且是以 file:// 开头的则表示它可能是在请求本地文件, 正常返回即可
      if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
        options.status = 200;
      }

      // 请求是属于宏任务, 因此这里包装一层成宏任务, 下面同理
      setTimeout(() => {
        resolve(new Response(body, options))
      }, 0)
    }

    // 当请求出现错误时
    xhr.onerror = function () {
      setTimeout(function() {
        reject(new TypeError('Network request failed'))
      }, 0)
    }

    // 当请求出现超时时
    xhr.ontimeout = function () {
      setTimeout(function() {
        reject(new TypeError('Network request timed out'))
      }, 0)
    }

    // 当请求被手动或在内部终止时
    xhr.onabort = function () {
      setTimeout(function() {
        reject(new DOMException('Aborted', 'AbortError'))
      }, 0)
    }

    function abortXhr() {
      xhr.abort()
    }

    function fixUrl(url: string): string {
      try {
        return url === '' && window.location.href ? window.location.href : url
      } catch (e) {
        return url
      }
    }

    // 打开一个请求
    xhr.open(request.method, fixUrl(request.url), true)

    // 它的作用是决定是否允许跨域
    if (request.credentials === 'include') {
      xhr.withCredentials = true
    } else if (request.credentials === 'omit') {
      xhr.withCredentials = false
    }

    // 可能返回的是并不是字符串,而是二进制数据
    if ('responseType' in xhr) {
      // 如果是可读的二进制数据则设置
      if ('FileReader' in window && 'Blob' in window) {
        xhr.responseType = 'blob'
      } else if('ArrayBuffer' in window) {
        xhr.responseType = 'arraybuffer'
      }
    }

    // 如果传入了请求头则设置
    if (init && ({}).toString.call(init.headers) === '[object Object]') {
      const names: string[] = [];
      Object.getOwnPropertyNames(init.headers).forEach(function(name) {
        names.push(name)
        xhr.setRequestHeader(name, init.headers![name])
      })
      request.headers.forEach(function(value, name) {
        if (names.indexOf(name) === -1) {
          xhr.setRequestHeader(name, value)
        }
      })
    } else if (Array.isArray(init?.headers)) {
      // 设置数组格式的请求头
      request.headers.forEach(function(value, name) {
        xhr.setRequestHeader(name, value)
      })
    }

    // 如果设置了 signal 则监听 abort 事件来终止请求
    if (request.signal) {
      request.signal.addEventListener('abort', abortXhr)

      xhr.onreadystatechange = function() {
        // DONE (success or failure)
        if (xhr.readyState === 4) {
          // 如果响应体正常结束则移除事件监听
          request.signal.removeEventListener('abort', abortXhr)
        }
      }
    }

    // 发送请求
    xhr.send(init!.body as XMLHttpRequestBodyInit)
  })
}

这样我们就得到了一个可用的自定义 fetch 方法了,不过这里面还缺失了一部分功能,比如:redirectcachekeepalive 功能,但这并不影响我们对其原理实现的学习,在多数情况下,我们只需要了解其核心功能到底做了什么即可,如果要细究,那需要参照 W3C 的规范文档及搭配引擎的内部实现来学习才能达到最好的效果。

以上。


CD ..