2024-09-18 JS wretch


关于 JS HTTP request client library,现阶段我推荐 wretch。

我的技术栈变化:wretch <- fetch <- node-fetch <- axios

我推荐 wretch 的理由:

  1. 适当的封装,不需要编写大量业务无关的 boilerplate code
  2. 统一实现的请求格式,如:请求 application/json, multipart/form-data, application/x-www-form-urlencoded
  3. 开箱即用的高级功能,如:retry, delay, abort 等等

技术栈变化简述

  • axios: ES 提出 fetch() API 前,只记得 axios,XMLHttpRequest 我已经不记得了。
  • node-fetch: ES 提出 fetch() API 后,但是 Node.js 没有实现出来
  • fetch: Node.js 18 发布 fetch() API 后
  • wretch: 写 fetch() 太疲劳了,寻找简单的工具

1. 什么是“不需要编写大量业务无关的 boilerplate code”

fetch 需要手动配置 http methods, headers and body

fetch("endpoint", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ "hello": "world" })
}).then(response => /* ... */)

fetch() 需要手动处理 status,堆叠 if-else

// 😮‍💨
fetch("anything")
  .then(response => {
    if(!response.ok) {
      if(response.status === 404) throw new Error("Not found")
      else if(response.status === 401) throw new Error("Unauthorized")
      else if(response.status === 418) throw new Error("I'm a teapot !")
      else throw new Error("Other error")
    }
    else // ...
  })
  .then(data => /* ... */)
  .catch(error => { /* ... */ })
而这些都是业务无关的代码,仅仅是为了配合 fetch() API 的使用。

wretch 封装了这些业务无关的代码,让你专注于业务逻辑。

wretch("endpoint")
  .post({ "hello": "world" })
  .notFound(error => { /* ... */ })
  .unauthorized(error => { /* ... */ })
  .error(418, error => { /* ... */ })
  .res(response => /* ... */)
  .catch(error => { /* uncaught errors */ })

2. 什么是“统一实现的请求格式”

wretch 无论是发送 json 或 form-data 亦或 x-www-form-urlencoded 都可以直接调用 JS object 对象。

So Sweet 🥰
const form = { a: 1, b: { c: 2 } };

// 📤 application/json
wretch("...").post(form)

// 📤 multipart/form-data
import FormDataAddon from "wretch/addons/formData"
wretch("...").addon(FormDataAddon).formData(form).post();

// 📤 application/x-www-form-urlencoded
import FormUrlAddon from "wretch/addons/formUrl"
wretch("...").addon(FormUrlAddon).formUrl(form).post();

fetch()

  1. 发送 json 需要 JSON.stringify();
  2. 发送 form-data 需要构建 new FormData();
  3. 发送 x-www-form-urlencoded 需要 new URLSearchParams();
Why? 😵‍💫
const data = { a: 1, b: { c: 2 } }

// 📤 application/json
fetch("endpoint", {
  method: "POST",
  headers: { "Content-Type": "application/json" }, // 1️⃣
  body: JSON.stringify(data) // 2️⃣
}).then(response => /* ... */)

// 📤 multipar/form-data
const formData = new FormData(); // 1️⃣
formData.append('a', 1);
formData.append('b', JSON.stringify({ c: 2})); // 2️⃣

fetch('https://example.com/submit', {
    method: 'POST',
    body: formData
}).then(response => /* ... */)

// 📤 application/x-www-form-urlencoded
const data = new URLSearchParams(); // 1️⃣
data.append('a', 1);
data.append('b', JSON.stringify({ c: 2})); // 2️⃣

fetch('https://example.com/submit', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded' // 3️⃣
    },
    body: data.toString()() // 4️⃣
}).then(response => /* ... */)

3. wretch 提供的“开箱即用的高级功能”

wretch 实现 retry 功能 So Easy

import { retry } from "wretch/middlewares"

const w = wretch().middlewares([retry()])

fetch() 实现 retry 功能,So Hard

// generated by ChatGPT
function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
    return fetch(url, options)
        .then(response => {
            if (!response.ok) {
                if (retries > 0) {
                    console.log(`Retrying... ${retries} attempts left.`);
                    return new Promise((resolve) => {
                        setTimeout(() => resolve(fetchWithRetry(url, options, retries - 1, delay)), delay);
                    });
                } else {
                    throw new Error('Max retries reached, fetch failed.');
                }
            }
            return response.json(); // 处理返回的响应体(假设是 JSON 格式)
        })
        .catch(error => {
            if (retries > 0) {
                console.log(`Retrying due to error: ${error.message}, ${retries} attempts left.`);
                return new Promise((resolve) => {
                    setTimeout(() => resolve(fetchWithRetry(url, options, retries - 1, delay)), delay);
                });
            } else {
                throw new Error('Max retries reached, fetch failed: ' + error.message);
            }
        });
}

// 调用
fetchWithRetry('https://example.com/api', { method: 'GET' })
    .then(data => console.log('Success:', data))
    .catch(error => console.error('Error:', error));

更不用提 delay abort 等高级功能了。

我发现的 wretch 的一点点不足

  1. progress 特性仅支持 download https://github.com/elbywan/wretch/issues/225#issuecomment-2105633417
  2. 没有开箱即用 Stream handler

refs