如何设计一个架构良好的前端请求库?

作者:sorryhc日期:2025/10/12

一、写在前面(抛出思考题)

过去的时间,你刷遍了面试题,在公司中工作了很多年,基于axios二次封装单项目级别的请求文件手到擒来。

你有没有想过?你是一个前端团队的资深老人,随着公司业务不断发展,各种各样的前端项目用着不同的请求技术方案。

A项目比较老,用的xhr

B项目用了axios,拥抱开源;

C项目因为小王拥抱原生的理念,使用了fetch

现在团队需要技术标准化,统一前端请求方案,将所有的请求集成到一个包里,如何设计技术方案,可以让这个包很健壮,未来很好维护?

这其实和大公司业务面广的背景很相似,比如美团外卖业务,在微信小程序端,你需要使用wx.request;在其他小程序,你需要使用不同的DSL去请求;在H5,你需要用到fetch/xhr/axios;在APP,你需要用到端上bridge能力,发出请求是计算机网络统一标准成熟的事情,如何基于不同的客户端环境来解耦并集成统一的前端解决方案?

二、方案设计

比较快速地实现是拆两层,直接在请求库中判断传入的配置,如axiosfetchxhr,去执行不同的函数。

image.png

这样很简单,也能实现,但是会有很严重的问题:

“请求”这件事不变的点和变的点,耦合在了一起,比如埋点、异常上报、拦截器等共性的功能全部都耦合在了请求库本身中,随着接入能力越来越多,代码会变得更加混乱。

那不耦合在一起呢?放在对应的执行请求文件里?重复代码会很多,相同的“事情”会在多个文件中重复多次。

那有什么比较好的方案呢?

有,开源项目中比较多的技术方案,如axios、umi、openai,通常会引入client请求器+adapter适配器概念,什么意思呢?

  1. 首先整个请求库需要设定标准统一的入参和出参,如入参就是url、options.....,出参就是success、data、code......
  2. client负责处理所有的通用逻辑,如埋点、异常上报、拦截器;
  3. adapter负责保存特定的请求能力,如wx.request、fetch、xhr,并且基于透传过来的标准化入参先做一层transform,比如微信小程序的入参命名叫params,那这里就需要将标准化的入参转成这个命名,响应也同理,将不同请求方案返回的结构体标准化处理再回传给client,最后client进行埋点、响应拦截、异常上报等处理,再透传给业务侧;

用图来说即为三层架构:

image.png

一句话总结:

在用户和发出请求之间增加一层适配层,负责对用户定制一套标准统一的入参类型、一套统一标准的出参类型,但只需要这一套参数类型标准即可满足所有请求方案。

而前面提到的“耦合”问题,直接在client层执行即可,因此client层做的是 执行请求 + 执行通用事件;adapter层做的是参数转换 + 实际请求逻辑处理。

说到这里光看两张图的前后对比,可能感知没那么明显,接下来我基于xhr + fetch适配器+client来手写一个请求库感受一下这个架构设计吧。

三、代码实现

3.1、类型设计

入参标准化:

1{
2  url: string,
3  method: 'GET' | 'POST' | ...,
4  headers: Record<string,string>,
5  body?: any, // 自动 JSON.stringify 或 FormData 等
6}
7

出参标准化:

1{
2  status: number,
3  statusText: string,
4  headers: Record<string,string>,
5  data: any, // 已解析完成的响应数据
6  rawResponse: any // 原始 Response 或 XHR 对象
7}
8

这样在用户调用层 -> 请求库Client层核心的类型就设计好了。

3.2、核心代码设计

1// Adapter 抽象基类
2class Adapter {
3  request(config) {
4    throw new Error("Adapter.request must be implemented");
5  }
6
7  // 入参标准化(可在基类内定义通用处理)
8  normalizeConfig(config) {
9    return {
10      url: config.url,
11      method: (config.method || 'GET').toUpperCase(),
12      headers: config.headers || {},
13      body: config.body || null
14    };
15  }
16
17  // 出参标准化
18  normalizeResponse({ status, statusText, headers, data, rawResponse }) {
19    return {
20      status,
21      statusText,
22      headers,
23      data,
24      rawResponse
25    };
26  }
27}
28
29// Fetch Adapter
30class FetchAdapter extends Adapter {
31  async request(config) {
32    const cfg = this.normalizeConfig(config);
33
34    const response = await fetch(cfg.url, {
35      method: cfg.method,
36      headers: cfg.headers,
37      body: cfg.body ? JSON.stringify(cfg.body) : null
38    });
39
40    // 解析 headers
41    const headersObj = {};
42    response.headers.forEach((v, k) => { headersObj[k] = v; });
43
44    // 自动解析 json / text
45    let data;
46    const contentType = response.headers.get('content-type') || '';
47    if (contentType.includes('application/json')) {
48      data = await response.json();
49    } else {
50      data = await response.text();
51    }
52
53    return this.normalizeResponse({
54      status: response.status,
55      statusText: response.statusText,
56      headers: headersObj,
57      data,
58      rawResponse: response
59    });
60  }
61}
62
63// XHR Adapter
64class XHRAdapter extends Adapter {
65  request(config) {
66    const cfg = this.normalizeConfig(config);
67
68    return new Promise((resolve, reject) => {
69      const xhr = new XMLHttpRequest();
70      xhr.open(cfg.method, cfg.url, true);
71
72      // 设置 headers
73      Object.entries(cfg.headers).forEach(([key, value]) => {
74        xhr.setRequestHeader(key, value);
75      });
76
77      xhr.onreadystatechange = () => {
78        if (xhr.readyState === 4) {
79          const headersObj = this.parseHeaders(xhr.getAllResponseHeaders());
80          let data;
81          try {
82            const contentType = headersObj['content-type'] || '';
83            if (contentType.includes('application/json')) {
84              data = JSON.parse(xhr.responseText);
85            } else {
86              data = xhr.responseText;
87            }
88          } catch (err) {
89            reject(err);
90            return;
91          }
92
93          resolve(this.normalizeResponse({
94            status: xhr.status,
95            statusText: xhr.statusText,
96            headers: headersObj,
97            data,
98            rawResponse: xhr
99          }));
100        }
101      };
102
103      xhr.onerror = () => {
104        reject(new Error('XHR network error'));
105      };
106
107      xhr.send(cfg.body ? JSON.stringify(cfg.body) : null);
108    });
109  }
110
111  // 辅助方法解析 headers 字符串
112  parseHeaders(rawHeaders) {
113    const headers = {};
114    rawHeaders.trim().split(/[\r\n]+/).forEach(line => {
115      const parts = line.split(': ');
116      const header = parts.shift();
117      const value = parts.join(': ');
118      if (header) headers[header.toLowerCase()] = value;
119    });
120    return headers;
121  }
122}
123
124// Client 类
125class Client {
126  constructor(adapter, options = {}) {
127    this.adapter = adapter;
128    this.baseURL = options.baseURL || '';
129    this.defaultHeaders = options.defaultHeaders || {};
130  }
131
132  async request(config) {
133    try {
134      const finalConfig = {
135        ...config,
136        url: this.baseURL + config.url,
137        headers: { ...this.defaultHeaders, ...(config.headers || {}) }
138      };
139      console.log(`-----请求开始${finalConfig}-----`);
140      const res = await this.adapter.request(finalConfig);
141      console.log(`-----请求成功${res}-----`);
142    } catch(e) {
143      console.log(`-----请求失败${e}-----`);
144      throw(e);
145    }
146  }
147
148  get(url, headers) {
149    return this.request({ url, method: 'GET', headers });
150  }
151
152  post(url, body, headers) {
153    return this.request({ url, method: 'POST', body, headers });
154  }
155}
156
157// 使用示例
158const fetchClient = new Client(new FetchAdapter(), {
159  baseURL: 'https://jsonplaceholder.typicode.com',
160  defaultHeaders: { 'Content-Type': 'application/json' }
161});
162
163const xhrClient = new Client(new XHRAdapter(), {
164  baseURL: 'https://jsonplaceholder.typicode.com',
165  defaultHeaders: { 'Content-Type': 'application/json' }
166});
167
168fetchClient.get('/todos/1').then(res => console.log('FetchAdapter normalized:', res));
169xhrClient.get('/todos/2').then(res => console.log('XHRAdapter normalized:', res));
170
171

162行代码,核心的代码就实现完毕了(实际开发可以拆分文件处理)

主要干了这几件事情:

  1. 定义BaseAdapter适配器抽象类,负责初始化请求、入参统一、出参统一三个核心能力;
  2. 定义XHRAdapterFetchAdapter,实现各自的请求函数,基于原生不同的参数标准化处理响应结构,返回给Client层;
  3. 定义Client请求实例,调用传入的Adapter,透传对应配置,中转请求发出和响应;

这样 在每个 Adapter 内部,对 入参出参 做一层 标准化处理,这样无论使用什么请求方式,Client 都能拿到统一结构的返回值,同时向 Adapter 传入的 config 也会有统一的字段,这也是很多通用库(axios 等)会做的事。

结尾

基于这个思路和模板,你非常容易的扩展很多能力,当你的公司业务需要接入京东小程序,请求相关的迭代很简单,新建一个适配器,把京东相关的DSL开发一下,把请求库更新个版本就搞定了。

请求监控、稳定性,你都不用管,因为这些早就成熟稳定地在多个项目跑了很久了。(自从你的请求库投产后)。

很多开源项目都是基于这个思路来工作的,包括你最熟悉的axios

如果这篇文章对你有帮助,欢迎和我一起讨论。


如何设计一个架构良好的前端请求库?》 是转载文章,点击查看原文


相关推荐


C/C++黑客帝国代码雨
Want5952025/10/10

写在前面 数字雨,又被称为“黑客帝国雨”,是一种经典的视觉效果,常用于表现科幻、科技感十足的场景。这种效果最初在电影《黑客帝国》中出现,以绿色字符从屏幕顶端不断下落的方式,营造出一种神秘而充满未来感的氛围。本文将介绍如何使用C语言在Windows控制台中实现一个简易的数字雨效果。通过这篇文章,你不仅能了解如何利用控制台API进行绘图操作,还能体会到字符动画背后的技术逻辑与美感。 系列文章 序号直达链接1C/C++李峋同款跳动的爱心2C/C++跳动的爱心3C/C++经典爱心4C/C++满


深入浅出 Compose 测量机制
Pika2025/10/9

自从换了新工作后,好久没有写博客了,今天终于能有时间写点东西,Compose作为Android新一代UI框架,已经得到了很多公司的认可,未来市场对Compose的要求也逐步提高。如果大家对Compose有兴趣,也欢迎后台私信我,字节移动OS招聘Compose框架的二次定制开发的Android小伙伴,一起把Compose做大做强吧! UI框架的测量流程 对于UI框架来说,测量布局与绘制可谓是非常重要的三个话题,对于Compose来说也不例外,本章我们将从着Compose的原理出发,来聊一下最重要


大数据毕业设计选题推荐-基于大数据的全球产品库存数据分析与可视化系统-大数据-Spark-Hadoop-Bigdata
IT研究室2025/10/8

✨作者主页:IT研究室✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python项目 安卓项目 微信小程序项目 文章目录 一、前言二、开发环境三、系统界面展示四、代码参考五、系统视频结语 一、前言 系统介绍 本系统是一个基于大数据技术的全球产品库存数据分析与可视化系统,采用Hado


【Linux】线程的互斥
羚羊角uou2025/10/6

因为线程是共享地址空间的,就会共享大部分资源,这种共享资源就是公共资源,当多执行流访问公共资源的时候,就会出现各种情况的数据不一致问题。为了解决这种问题,我们就需要学习线程的同步与互斥,本篇将介绍线程的互斥。 1.相关概念 临界资源:多线程执⾏流被保护的共享资源就叫做临界资源 临界区:每个线程内部,访问临界资源的代码,就叫做临界区 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤ 原⼦性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,


MySQL Performance Schema详解与实战应用
IT橘子皮2025/10/5

Performance Schema是MySQL内置的性能监控系统,自5.5版本引入以来已成为数据库性能分析与优化的核心工具。本文将全面解析其架构原理、配置方法及典型应用场景,帮助您掌握这一强大的性能诊断利器。 一、Performance Schema核心架构 Performance Schema采用插桩-消费者模型构建,通过轻量级的内存表存储性能数据,对数据库性能影响通常控制在5%以内。其核心组件包括: ​插桩点(Instruments)​​:嵌入MySQL代码的探测点,按层级命名如wai


【Unity笔记】Unity XR 模式下 Point Light 不生效的原因与解决方法
EQ-雪梨蛋花汤2025/10/4

Unity XR 模式下 Point Light 不生效的原因与解决方法 在 Unity 中开发 VR 应用时,经常会遇到一个让人疑惑的现象: 在 编辑器 Game 模式下,场景中的 Point Light(点光源) 可以正常照亮物体。但当启用 Initialize XR on Startup 并通过 VR 设备运行时,Point Light 不再生效,只有 Directional Light(平行光) 仍然有效。 这让很多开发者误以为“材质只支持 Directional Light,而不支持


XYplorer(多标签文件管理器) 多语便携版
东风西巷2025/10/2

XYplorer中文版是一款多标签文件管理器及增强资源管理器的工具,XYplorer文件管理器支持多标签页栏,管理文件时跟使用Chrome之类浏览器一样,从浏览方便性,和切换滑顺程度,要比Windows系统自带的Explorer资源管理器便捷得多.可以大部分程度上替代系统自带的文件管理器.同时,有浏览器快捷键和鼠标快捷. 软件功能 双窗口浏览:支持双窗口浏览,可以同时浏览两个文件夹,方便文件的复制、移动和比较。 高级搜索:支持高级搜索功能,可以根据文件名、大小、日期、属性等多种条件进


什么是 Apache Ignite?
悟能不能悟2025/10/2

首先需要明确一点:“Ignite”这个名字在技术领域可能指代不同的事物,但最著名和广泛使用的是 ​Apache Ignite。它是一个功能强大的、分布式内存计算平台。除此之外,还有例如 ​Couchbase Ignite​(一个会议)等。本文将重点介绍 ​Apache Ignite。 什么是 Apache Ignite? Apache Ignite 是一个以内存为中心的分布式数据库、缓存和处理平台,设计用于在横向扩展的架构上提供极高的性能和吞吐量。你可以把它理解为一个“内存数据网格”,但其


Python零基础入门:30分钟掌握核心语法与实战应用
做运维的阿瑞2025/10/2

Python基础入门指南 5分钟掌握核心概念,15分钟上手实战项目 你将学到什么 核心技能实际应用学习时间🔢 数据类型处理文本、数字、列表10分钟🔄 控制流程循环、判断、函数15分钟📊 数据处理文件操作、数据分析20分钟🎮 实战项目猜数字游戏30分钟 适合人群 零基础新手 | 转语言开发者 | 在校学生 | 职场提升 快速开始 三个核心场景 数据处理 # 处理学生成绩 scores = [85, 92, 78, 96, 88, 76, 94, 82] #


软件工程实践团队作业——团队组建与实践选题
Funny Valentine-js10/1/2025

吴彦组。

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0