浅谈古茗内部的Request请求库

点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群

引言

在现代Web开发中,前端请求库的重要性日益凸显。它们不仅简化了客户端与服务器之间的数据交互,还提高了开发效率和应用性能。前端请求库的主要作用是封装HTTP请求的复杂性,提供简洁、一致的API来发送请求和处理响应,从而使得开发者可以更加专注于业务逻辑的实现。在加入古茗之前,我主要依赖XMLHttpRequest、Fetch API和Axios来处理这些交互。这些技术都有其独特的优势和适用场景,下面我将探讨这些技术,并分享我的实践经验。

常见的几种请求处理方法

XMLHttpRequest——传统的异步通信方式

XMLHttpRequest(XHR)是最早的JavaScript异步通信技术之一,它允许网页在不重新加载整个页面的情况下与服务器交换数据,并更新部分网页。在刚接触前端发送请求时,我是使用XHR来实现数据的异步加载。例如,我可能会这样发送一个GET请求。

const xhr = new XMLHttpRequest();
const url = 'https://api.example.com/data';
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
  if (xhr.readyState == 4 && xhr.status == 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();
  • 优点:支持所有浏览器,包括老旧版本。

  • 缺点:API繁琐,不支持Promise,代码可读性和维护性较差。

Fetch API——原生异步Web API

Fetch API是JavaScript原生提供的一个异步Web API,是基于Promise的新一代网络请求API,它提供了一个更简洁和强大的方式来发起网络请求。在实践中,Fetch API的使用非常直接,例如发起一个GET请求。

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));
  • 优点:基于Promise,使用简洁;支持流式传输,可以处理大文件。

  • 缺点:需要手动处理HTTP状态码和JSON序列化;不支持请求取消。

Axios——功能丰富的HTTP请求库

Axios是一个基于Promise的HTTP请求库,用于浏览器和node.js。它提供了一个简洁的API和自动转换JSON数据的功能。这也是我经常使用的一个请求库,在我的项目中,Axios的使用非常广泛。以下是如何使用Axios发送一个GET请求。

import axios from 'axios';

axios.get('https://api.example.com/data')
  .then(response => console.log(response.data))
  .catch(error => console.error(error));
  • 优点:API简洁,自动处理JSON,支持请求和响应拦截,支持请求取消。

  • 缺点:需要额外安装库,相比于Fetch API,它增加了项目的依赖。

Request——古茗内部的标准化请求库

在来到古茗后,我开始使用Request请求库,这是专为简化和标准化前端数据请求流程而设计的内部库。Request请求库的使用与Axios类似,但它更注重团队内部的标准化和统一性。以下是如何使用Request发送一个GET请求。

import { request } from '@guming/request';

const http = request.create();
http.get('https://api.example.com/data').then(res => console.log(res.data));
  • 优点:标准化API,无外部依赖,体积小,功能全面。

  • 缺点:社区支持相对较小,因为是内部库,可能不如Axios那样广泛使用。

以上是通过使用各种请求库来发送一个GET请求的方式,除了较为原始的XHR方式外,我们感觉到好像各种请求库的请求方式都差不多,但实际上当在处理复杂场景时,它们的表现和适用性却大相径庭。在展开讲讲这些请求库在处理复杂场景之前的我们先了解一下什么是Request请求库。

什么是Request请求库

Request库是古茗内部开发的一款标准化的HTTP请求库,专为简化和标准化前端数据请求流程而设计。它的核心优势在于其标准化的API设计、多环境兼容性、以及强大的内置功能。Request请求库特别强调“规范”和“统一”。它基于业务场景和团队约定,提供了一套标准化的请求处理流程,帮助团队保持代码的一致性和可维护性。这种设计不仅提升了开发效率,还确保了代码质量,使得团队能够更加专注于业务逻辑的实现,而不是被底层的请求细节所困扰。

核心特性
  • 标准化API及配置,Request库提供了一套统一的API和配置标准,使得在不同的运行环境中(浏览器环境、各小程序端以及Node环境)都能保持一致的使用体验。

  • 多环境兼容,在浏览器环境和小程序端,Request库基于XMLHttpRequest进行封装,而在Node端则基于axios 1.x实现,确保了在不同平台的兼容性和可用性。

  • Request库内置了多种通用能力,如请求失败重试、相同请求复用、请求缓存等,这些功能帮助开发者在面对网络请求的不确定性时能够更加从容应对。

  • Request库的API设计简洁直观,易于理解和使用,降低了学习成本,使得开发者可以快速上手。

  • 无外部依赖,体积小,Request库不依赖于任何外部库,使得其体积保持轻量,便于集成和部署。

请求流程

  1. 初始化请求阶段。流程从浏览器端发起一个HTTP请求开始。在请求发出之前,系统会合并并应用所有相关的配置项,如设置请求头、超时参数等。

  2. 应用请求拦截器。请求通过请求拦截器,在这里可以对请求进行预处理,比如添加认证Token。

  3. 查询缓存阶段。系统检查是否存在有效的缓存数据。如果缓存命中,将直接返回缓存数据,绕过实际的网络请求。

  4. 实际发送请求阶段。如果缓存未命中,请求将被发送到服务器。

  5. 错误检测阶段。请求发送后,系统会检测是否有错误发生,如网络异常或服务器错误。

  6. 重试机制阶段。如果请求失败,系统会根据设定的重试策略进行重试,直到超过最大重试次数。

  7. 错误处理阶段。如果请求在重试后仍然失败,将进入错误处理流程,可能会记录错误日志或触发错误回调。

  8. 应用响应拦截器。请求成功后,响应会通过响应拦截器,在这里可以对响应数据进行预处理。

  9. 缓存响应阶段。如果响应数据需要被缓存,系统会将其存储起来,以便未来快速访问。

  10. 错误响应检查阶段。系统会检查响应是否表示一个错误。如HTTP状态码4xx或5xx。

  11. 响应转换阶段。如果响应没有错误,可能会经过一个转换过程,以适配应用的数据格式或结构。

  12. 请求完成阶段。处理完的响应数据被返回给调用者,请求流程结束。

以下为Request请求库的请求流程图。

eac61ca3824cac612638ecf9595f0dbe.png
request-progress

与其他请求库的比较

正如我们上面所说在平时一些简单的请求中我们使用这些请求库并不会发现有什么特别大的不同。但其实Fetch API、Axios和Request库各有千秋,下面我们将对这三种请求技术进行比较。

Fetch API

Fetch API是JavaScript原生提供的异步Web API,它的优势在于无需引入任何第三方包,直接使用浏览器提供的接口。Fetch返回一个Promise对象,使得异步操作更加直观。然而,Fetch API的缺点在于它不自动处理JSON数据的序列化和反序列化,需要开发者手动调用JSON.stringifyres.json()。此外,Fetch API的错误处理也较为特殊,它不会在HTTP错误状态码时拒绝Promise,这可能会导致开发者在错误处理上的混淆。

基础用法

调用fetch后,fetch方法返回一个Promise对象,需要对其在进行一次手动处理,则再进行一次res.json(),才能将响应对象转化为json格式,这也是一个异步操作,所以它本身返回还是一个Promise。

请求头和请求参数

fetch通过body属性进行传参,body必须接收一个JSON字符串,不能直接接收JSON对象,因为fetch不会自动对body参数进行stringify,因此需要手动对body参数进行一次JSON.stringify,才可正确传参。

const handleSendFetch = async () => {
  const res = awaitfetch('https://api.example.com/data', {
    method: 'POST',
    body: JSON.stringify({ page: { pageSize: 10, pageNo: 1 } }),
    headers: {
      'content-type': 'application/json',
      Authorization:
        'Bearer eyJhbGciOiJSUzI1xxxx',
    },
  });
  const data = await res.json();
  console.log(data);
};
错误处理

当接收到一个代表错误的HTTP状态码时,调用fetch()后返回的Promise不会被标记为reject,即使响应的HTTP状态码是404或者500也不会被标记为reject。相反,他会将Promise状态标记为resolve,但是如果响应的HTTP状态码不是2XX(即200~299范围内的),则设置resolve返回值的ok属性为false,仅当网络故障时或者请求被阻止时fecth才将返回的Promise标记为reject

fetch通过返回的res.ok属性来判断请求是否成功,如果res.ok不为true则代表请求失败,则可进行错误处理,并且可以通过res.status拿到当前请求的状态码。

const handleSendFetch = async () => {
  const res = awaitfetch('https://api.example.com/data', {
    method: 'POST',
    body: JSON.stringify({ page: { pageSize: 10, pageNo: 1 } }),
    headers: {
      'content-type': 'application/json',
      Authorization:
        'Bearer eyJhbGciOiJSUzI1xxxx',
    },
  });
  console.log(res);

  if (!res.ok) {
    thrownewError(`HTTP error! status: ${res.status}`);
  }
  const data = await res.json();
  console.log(data);
};
同步请求

FecthAPI可以通过借助Promise来进行同步请求,通过Promise.all可以使得这段代码能够高效地并行地处理多个HTTP请求。

const handleSendSync = async () => {
  Promise.all([
    fetch('https://api.example.com/data1'),
    fetch('https://api.example.com/data2'),
    fetch('https://api.example.com/data3'),
  ]).then(async ([res1, res2, res3]) => {
    const data1 = await res1.json();
    const data2 = await res2.json();
    const data3 = await res3.json();
    console.log(data1);
    console.log(data2);
    console.log(data3);
  });
};
超时处理

由于Fetch API没有提供timeout属性来处理超时,所以可以通过AbortController来进行超时处理。将fetch的配置对象的signalAbortControllersignal进行关联,设置超时时长,这里设置当时间超过5秒时调用controller.abort()来中断请求从而实现超时处理。

// 超时处理
// 创建了一个 AbortController 的实例。AbortController 接口允许你通过一个信号(signal)来取消一个或多个相关的 fetch 请求
const controller = newAbortController();
// 定义请求的配置对象 除了请求方法和携带的参数外还要将signa与controller.signal进行关联
constoptions: RequestInit = {
  method: 'POST',
  signal: controller.signal,
  body: JSON.stringify({ page: { pageSize: 10, pageNo: 1 } }),
};
const promise = fetch('https://api.example.com/data', options);
// 超过五秒执行controller.abort()中断请求
setTimeout(() => controller.abort(), 5000);

promise
  .then((res) => res.json())
  .then((data) =>console.log(data))
  .catch((err) =>console.dir(err));
请求拦截

Fetch API没有通过请求拦截的方法。

请求取消

与超时处理类似,都是通过将fecth上signal与AbortController上的signal相关联,然后通过controller.abort()来实现请求的取消。

Axios

Axios是一个基于Promise的HTTP请求库,可同时在Node.js和浏览器中使用。在Node.js服务器端基于Node的http模块来实现,在浏览器端则使用XMLHttpRequest来实现。Axios的错误处理更加灵活,它可以配置超时处理和错误处理,提供了更丰富的功能和更简单的错误处理机制。它以其简洁的API、自动的JSON处理、丰富的配置选项和拦截器功能而受到开发者的青睐。

核心特性(来自官方)
  • 从浏览器创建XMLHttpRequests

  • 从 node.js 创建http 请求

  • 支持Promise API

  • 拦截请求和响应

  • 转换请求和响应数据

  • 取消请求

  • 超时处理

  • 查询参数序列化支持嵌套项处理

  • 自动将请求体序列化为:

    • JSON (application/json)

    • Multipart / FormData (multipart/form-data)

    • URL encoded form (application/x-www-form-urlencoded)

  • 将 HTML Form 转换成 JSON 进行请求

  • 自动转换JSON数据

  • 获取浏览器和 node.js 的请求进度,并提供额外的信息(速度、剩余时间)

  • 为 node.js 设置带宽限制

  • 兼容符合规范的 FormData 和 Blob(包括 node.js)

  • 客户端支持防御XSRF

基础用法

和Fetch API不同,axios每个HTTP方法都提供了一份独立的一份function,可以通过如下不同方法进行请求,例如axios.get()axios.post()axios.put()等。

Axios会自动将返回的promise的响应对象转化为json格式,所以无需我们自己像使用Fetch API一样手动进行res.json()的转化。

请求头和请求参数

Axios可以通过params属性来传递get的请求参数,通过data属性来传递post的请求参数,不像Fetch API还需要通过body传递并且需要将数据进行JSON反序列化JSON.stringify(),Axios会自动JSON.stringify请求body,headers的处理则和Fetch API的处理类型,需要通过headers属性来传递headers的信息。

错误处理

因为Axios直接返回一个Promise对象,所以我们可以直接通过Promise.catch方法捕获错误并进行处理。

axios.get('https://api.example.com/data').catch(function (error) {
  if (error.response) {
    // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
    console.log(error.response.data);
    console.log(error.response.status);
    console.log(error.response.headers);
  } elseif (error.request) {
    // 请求已经成功发起,但没有收到响应
    // `error.request` 在浏览器中是 XMLHttpRequest 的实例,
    // 而在node.js中是 http.ClientRequest 的实例
    console.log(error.request);
  } else {
    // 发送请求时出了点问题
    console.log('Error', error.message);
  }
  console.log(error.config);
});
同步请求

Axios提供给了axios.all()来进行同步请求,用法与Promise.all()类似,当然也可以时使用Promise.all()来实现同步请求。

超时处理

Axios可以通过配置timeout属性来实现超时处理,代码示例如下。

axios({
  url: 'https://api.example.com/data',
  method: 'GET',
  timeout: 5000,
});
请求/响应拦截

请求/响应拦截是Axios最重要的特性之一。在axios可以添加对应的拦截器来对请求和响应进行一系列的拦截。

// 添加请求拦截器
axios.interceptors.request.use(
  function (config) {
    // 在发送请求之前做些什么
    return config;
  },
  function (error) {
    // 对请求错误做些什么
    returnPromise.reject(error);
  }
);

// 添加响应拦截器
axios.interceptors.response.use(
  function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
  },
  function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    returnPromise.reject(error);
  }
);
请求取消

v0.22.0开始,Axios 支持像以Fetch API的方式,通过AbortController来取消请求,用法与在Fetch API的用法一致,都是新创建一个AbortController实例,将实例的signal与Axios项的signal进行关联,然后通过controller.abort()来实现取消请求。

const controller = new AbortController();

axios.get('https://api.example.com/datar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// 取消请求
controller.abort()

Request

核心特性
  • 标准化 API 及配置,兼容多种运行环境,可扩展设计

  • 内置通用能力: 请求失败重试、相同请求复用、请求缓存等

  • 支持 presets 预设逻辑

  • 简单易用,API 友好

  • 无任何外部依赖,体积小

基础用法

request可以通过create进行创建于一个request的实例,在实例中的配置项中直接配置url,request每个HTTP方法也和axios一样都提供了一份独立的一份function,可以通过如下不同方法进行请求,例如http.get(),http.post(),http.put()等。。,

import { request } from '@guming/request';

// 创建 request 实例
const http = request.create();

// 发送请求
http({ url: '/api/example/1' }).then((res) => {
  console.log(res);
});

// 发送 POST 请求
http.post('/api/example/2', { foo: 'bar' }).then((res) => {
  console.log(res);
});
请求头

HTTP 请求规定请求头名称不区分大小写,为了规范配置项,会自动进行标准化转换。例如content-type (全小写), 默认添加请求头content-type: application/json。并且会将请求头值转换成字符串,开发者可以直接通过headers请求头配置。

配置合并策略
  1. headersparamsmeta 会将对象进行浅合并。

  2. 如果合并双方配置项的data 均为一个普通对象,这会将对象浅合并。

  3. 在其他场景,后者则会覆盖前者。

标准化处理

在发送请求前,request 会做一系列标准化配置处理,以便于适配器拿到的都是一致的请求配置,具体如下。

  1. method 转换为全大写,且如果为空默认设置为'GET'

  2. 标准化处理urlparams 配置,处理后会将url 携带的 search 参数合并至params 对象里(params 的优先级更高)

  3. headers 值转换成字符串;并始终转换请求头名称content-type (全小写)

  4. 非法timeout 将被设置为0

  5. 暴露只读属性rawURL,表示调用请求传入的原始url(不可枚举)

  6. paramsheaders 一定为一个对象

http({
  url: '/a?x=1&y=2',
  params: { x: 3 },
  method: 'post',
  headers: {
    'x-custom': 999,
    'content-type': 'application/json',
  },
  timeout: -1,
});

// 上述配置,在发送请求前将被转换成:
const normalizedConfig = {
  url: '/a',
  rawURL: '/a?x=1&y=2', // 只读,不可枚举属性
  params: { x: '3', y: '2' },
  method: 'POST',
  headers: {
    'x-custom': '999',
    'content-type': 'application/json',
  },
  timeout: 0,
};
请求/响应拦截

使用拦截器可以对每次请求前、响应数据、报错等进行环节拦截并加入自定义的逻辑处理。

import { request } from'@guming/request';

const http = request.create();

http
  .tap('request', (config) => {
    config.url = `https://example.com${config.url}`;
    // something...
  })
  .tap('response', (response) => {
    // something...
  })
  .tap('error', (error) => {
    // something...
  });

// 实际请求 url 为: https://example.com/api/1
http.get('/api/1');

看到这大家会发现request好像和axios没啥区别啊,很多使用方法上的居然出奇的一致。别着急请继续往下看。

取消请求

request中通过扩展标准Promise对象,对 http 请求返回的Promise对象绑定静态方法abort()实现。可以直接通过实例请求返回的Promise对象,调用其上的abort()方法来终止已发送的请求。这使得我们无需再去实例AbortController,然后再去将signal建立联系等等这一系列繁琐的操作。

import { request } from '@guming/request';

const http = request.create();

const promise = http.get('/example');

promise.then((res) => {
  console.log(res);
});

setTimeout(() => {
  promise.abort(); // 1s 后取消请求
}, 1000);
错误处理
  1. Request调用请求方法会返回一个Promise对象,可以直接通过catch()方法来捕获错误,这一点和Axios的用法类似。

  2. 在Request中也可以通过tap('error', ()=>{})来拦截请求过程中的所有错误。

    import { request, RequestError } from '@guming/request';
    
    const http = request.create();
    
    http.tap('error', (error) => {
      error.code = RequestError.ERR_NETWORK;
    });
  3. 在Request中可以使用errorHandler来做最终的错误处理,例如通过UI展示错误信息等等。

    import { request } from '@guming/request';
    
    const http = request.create({
      // 为实例设置默认的错误处理器
      errorHandler: (error) => {
        console.error(error.message); // 最终的错误处理,通常用于桥接 UI,在这里不应该再次抛错
      },
    });
    
    // 对某个请求设置 errorHandler: null, 将不会进入到实例的默认错误处理逻辑
    http.get('/example', { errorHandler: null });
标准service请求

与Axios不同的是,为了保证团队内的请求service风格统一,并且避免在service定义层做过度封装,Request建议使用ServiceHelper辅助类来创建团队规范的service请求。这也是得Requset相对较Axios显得不那么灵活了,而是使得风格更加地统一。

import { ServiceHelper, request } from'@guming/request';

// 创建 http 实例
const http = request.create();

// 创建 serviceHelper 实例
const serviceHelper = newServiceHelper({ http });

// 定义 service
const service = {
  commit: serviceHelper.define({
    url: '/example/commit',
    method: 'post',
  }),
};

// 发起请求
service.commit({ user: 'Alice', age: 18 }).then((res) => {
  console.log(res);
});

// 中断请求(如果支持)
const task = service.commit();
setTimeout(() => {
  task.abort();
}, 1000);

// 监听事件(如果支持)
task.on('upload-progress', (evt) => {
  console.log(evt);
});
监听事件

Reques也允许监听和响应请求过程中的特点事件。在请求过程中给可能会发送一些事件,例如上传进度等等,我们可以通过.on().off()方法来进行监听和取消监听。

import { request } from'@guming/request';

const http = request.create();

// 监听
http
  .get('/example')
  .on('downloadProgress', (p) => {
    console.log(p);
  })
  .then((res) => {
    console.log(res);
  });

// 取消监听
http
  .get('/example')
  .off('downloadProgress', (p) => {
    console.log(p);
  })
  .then((res) => {
    console.log(res);
  });

比较

89a470f2c59ab89f443473b836a1aa32.png
画板

对比总结

Fetch API适合简单的HTTP请求,Axios提供了更多的灵活性和配置选项,而Request库在易用性和功能支持上提供了良好的平衡,并且更加注重团队内的请求service风格的统一。Axios和Request库提供了更丰富的API和更简单的错误处理,而Fetch API则需要更多的手动操作。Fetch API作为原生API,社区支持最为广泛。Axios由于其流行度,也有着庞大的社区支持。Request库由于是内部使用的请求库,社区可能相对较小,但使得团队的请求service风格更加统一。

在实际开发中,我们可以根据项目需求、团队习惯以及对库的熟悉程度来选择适合的HTTP请求库。对于需要快速开发和轻量级应用,Fetch API可能是一个好选择。对于需要更多配置和更复杂请求逻辑的应用,Axios可能更加合适。而对于需要轻量级、功能全面且希望减少外部依赖的项目以及团队风格规范统一,Request库可能是更好的选择。

为什么选择Request?

说到这里或许有人会说,“既然Request请求库的使用方法和Axios那么类似,那为什么还要重复造轮子呢,不直接使用Axios呢?”

首先是标准化需求,在很多业务场景中,我们并不需要Axios提供的高度可定制性,而是更需要一种标准化、规范化的请求处理方式。Request库通过“收”掉一些非必要的配置项和API,使得开发者在使用时能够遵循统一的标准,减少了因为过度定制而带来的混乱。

其次是易用性的提升,Axios提供了接近30个配置项,这在一定程度上增加了使用的复杂性。Request库则精简到只有13个必要的配置项,并且对于每个配置项只提供一种处理方式,这样做简化了API的使用,使得开发者更容易上手和记忆。

然后是业务逻辑的收敛,Request库通过preset(预设)功能,允许我们将通用的请求逻辑封装到一个npm包中,便于在多个项目之间复用。这种方式有助于实现业务层面的规范统一。

自研的Request库可以更加精细地控制库的体积,去除不必要的功能,从而减少应用的最终打包大小。同时,针对特定的业务场景进行优化,可以提高库的性能。再者自研的库可以根据公司的技术栈和业务需求进行定制,使得库更加符合内部使用习惯,同时也便于长期的维护和更新。

虽然Axios已经非常成熟,但是要完全适应特定的业务场景可能需要进行大量的定制和改造。自研Request库则可以在设计初期就考虑到这些因素,从而降低后续的改造成本。

总的来说,Axios和Request库在设计上有不同的侧重点。Axios更注重扩展性和灵活性,而Request库则更侧重于规范性和统一性。基于这些考虑,我们选择了开发Request库,以更好地适应我们的业务需求和技术生态。

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞、在看” 支持一波👍
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值