阅读笔记 - 你可能并不需要 axios

99 阅读2分钟

通过使用 Fetch API,我们可以轻松地发起 HTTP 请求并处理响应,而无需引入额外的第三方库。

本文为一些常见的 HTTP 请求场景提供了 Fetch 示例。

Get JSON

fetch('http://example.com/api/v1/users/sb')
  .then((res) => res.json())
  .then((data) => {
    console.log(data) // 输出 `res.json()` 的结果
  })
  .catch((err) => console.error(err))

自定义标头

fetch('http://example.com/api/v1/users/sb', {
  headers: new Headers({
    'User-agent': 'Mozilla/4.0 Custom User Agent',
  }),
})
  .then((res) => res.json())
  .then((data) => console.log(data))
  .catch((err) => console.error(err))

HTTP 错误处理

fetch('http://example.com/api/v1/users/sb')
  .then((res) => (res.ok ? res.json() : Promise.reject(new Error('Fail to load'))))
  .then((data) => console.log(data))
  .catch((err) => console.error(err))

CORS

使用 credentials 选项来控制是否自动包含 cookie。

它有三个可选值:

  • omit:默认值,不包含凭据
  • same-origin:同源请求时包含凭据
  • include:所有请求都包含凭据
fetch('http://example.com/api/v1/users/sb', {
  credentials: 'include',
})
  .then((res) => res.json())
  .then(console.log)
  .catch(console.error)

Post JSON

function postRequest(url, data) {
  return fetch(url, {
    credentials: 'same-origin',
    method: 'POST', // 'GET', 'PUT', 'DELETE' 等
    body: JSON.stringify(data), // 匹配 'Content-Type'
    headers: { 'Content-Type': 'application/json' },
  })
    .then((res) => res.json())
    .catch((err) => console.error(err))
}

postRequest('http://example.com/api/v1/users', { user: 'sb' }).then(console.log)

Post <form>

function postForm(url, formSelector) {
  const formData = new FormData(document.querySelector(formSelector))

  return fetch(url, {
    methods: 'POST',
    body: formData,
  })
    .then((res) => res.json())
    .catch((err) => console.error(err))
}

postForm('http://example.com/api/v1/users', '#user-form').then(console.log)

Form encoded data

使用 URLSearchParams 像查询字符串一样构建表单编码数据。

例如 new URLSearchParams({ a: '1', b: '2' }) 将返回 a=1&b=2

function postFormData(url, data) {
  return fetch(url, {
    methods: 'POST',
    body: new URLSearchParams(data),
    headers: new Headers({
      'Content-Type': 'application/x-www-form-urlencoded',
    }),
  })
    .then((res) => res.json())
    .catch((err) => console.error(err))
}

postFormData('http://example.com/api/v1/users', { user: 'sb' }).then(console.log)

上传文件

function postFile(url, fileSelector) {
  const formData = new FormData()
  const fileField = document.querySelector(fileSelector)

  formData.append('username', 'sb')
  formData.append('avatar', fileField.files[0])

  return fetch(url, {
    method: 'POST',
    body: formData,
  })
}

postFile('http://example.com/api/v1/users', 'input[type="file"].avatar').then(console.log)

上传多个文件

HTML 中可以设置 mutiple 属性来允许用户选择多个文件。

<input type="file" name="files" class="files" multiple />
function postFile(url, fileSelector) {
  const formData = new FormData()
  const fileField = document.querySelector(fileSelector)

  // 返回一个类数组对象,所以使用 Array.prototype.forEach 遍历
  Array.prototype.forEach.call(fileField.files, (file) => formData.append('files', file))

  return fetch(url, {
    method: 'POST',
    body: formData,
  })
    .then((res) => res.json())
    .catch(console.error)
}

超时

function promiseTimeout(ms) {
  return (promise) => {
    const timeout = new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('Timeout')), ms)
    })
    return Promise.race([promise, timeout])
  }
}

promiseTimeout(3000)(fetch('http://example.com/api/v1/users/sb'))
  .then((res) => res.json())
  .then(console.log)
  .catch(console.error)

// 或者
function fetchTimeout(ms, ...args) {
  function raceTimeout(promise) {
    const timeout = new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('Timeout')), ms)
    })
    return Promise.race([promise, timeout])
  }

  return raceTimeout(fetch(...args))
}

更复杂的示例,具有跟踪标志 __timeout

function promiseTimeout(ms) {
  return (promise) => {
    let isDone = false
    promise.then(() => (isDone = true))
    const timeout = new Promise((resolve, reject) => {
      setTimeout(() => {
        if (!isDone) {
          promise.__timeout = true
          reject(new Error('Timeout'))
        }
      }, ms)
    })
    return Promise.race([promise, timeout])
  }
}

下载进度

function progressHelper(onProgress) {
  return (response) => {
    if (!response.body) return response

    const contentLength = response.headers.get('content-length')
    const total = contentLength ? parseInt(contentLength, 10) : -1
    let loaded = 0

    return new Response(
      new ReadableStream({
        start(controller) {
          const reader = response.body.getReader()
          return read()

          function read() {
            return reader
              .read()
              .then(({ done, value }) => {
                if (done) return void controller.close()
                loaded += value.byteLength
                onProgress({ loaded, total })
                controller.enqueue(value)
                return read()
              })
              .catch((error) => {
                console.error(error)
                controller.error(error)
              })
          }
        },
      })
    )
  }
}

使用示例,更多可查看 fetch-progress-indicators

fetch('https://fetch-progress.anthum.com/20kbps/images/sunrise-progressive.jpg')
  .then(progressHelper(console.log))
  .then((response) => response.blob())
  .then((blob) => {
    // 你可以在这里使用 blob
    const img = document.createElement('img')
    img.src = URL.createObjectURL(blob)
    document.body.appendChild(img)
  })

递归重试

function retryPromise(fn, retriesLeft = 5) {
  return fn().catch((err) => (retriesLeft > 0 ? retryPromise(fn, retriesLeft - 1) : Promise.reject(err)))
}

const getJson = (url) => fetch(url).then((res) => res.json())

retry(() => getJson('http://example.com/api/v1/users/sb'))
  .then(console.log)
  .catch(console.error)

// 或者
function retryCurry(fn, retriesLeft = 5) {
  const retryFn = (...args) => fn(...args)
    .catch(err => retriesLeft > 0
      ? retryFn(fn, retriesLeft - 1)
      : Promise.reject(err)
    })
  return retryFn
}

处理重定向

const checkForRedirect = (response) => {
  // 307 temporary redirect
  // 308 permanent redirect
  if (response.status === 307 || response.status === 308) {
    const location = response.headers.get('location')
    if (!location) return Promise.reject(new Error('Invalid redirect'))
    return fetch(location).then(checkForRedirect)
  }
  return response
}

fetch('http://example.com/api/v1/users/sb')
  .then(checkForRedirect)
  .then((res) => res.json())
  .then(console.log)
  .catch(console.error)