关闭前端统一请求库设计与落地

前言

对于一个前端工程师而言,每天都在面对的较多的需求场景就是调用后端的接口,但是因为众所周知的原因,前端目前已经有无数种调用接口的方式,例如:之前有基于 XHR、Axios、Fetch 进行封装的工具,大家都试图在统一接口的调用方式,但是他们看起来最后都需要再进行改造。于是,我们试图在 B 站开发一套能够综合上述工具之长处,并结合 B 站事实需要的工具, 推出一个具有统一错误处理、减少代码冗余、抹平风格差异、降低文档负担、优化代码提示等功能的统一请求库。

背景

为什么需要统一的请求库

作为一名研发,我们会面临各种各样的业务需求和技术场景,这导致我们不得不对大量的接口调用做差异化的设计和封装,再混合开发人员的风格的差异和历史问题,会导致各种各样问题的产生。

以下是比较典型的几个问题:

  1. 代码冗余或维护成本过大:受历史因素和业务需求影响,各团队和仓库中存在多版本请求库,比如为 SSR 和 CSR 定制的处理逻辑、基于 Vue2 和 Vue3 的封装,以及端内或 Web 的请求兼容处理等。这些库之间的模块相似但不同,导致维护和扩展性复杂;

  2. 存在性能问题: 公司之前的请求库存在功能堆砌导致代码过多、可能存在体积过大等问题而影响页面性能;

  3. 前后端无法协同进化:目前公司后端的基础/通用能力已经遵循统一的标准,因此每当后端提供一种基础能力需要前端接入时,前端由于是零散的并且没有一个统一的标准,带来的后果是不同的前端项目对应一种通用的后端能力需要各自开发,迭代成本高,这也是最严重的一个问题,其阻碍了前后端协同进化;

为了解决这些问题,我们设计了能够解决上述问题的统一请求库,并通过一些中间件模式的设计来尽可能减少包体积。

现状以及能力的对比

我们调研了社区已经有的一些耳熟能详的技术方案,看看是否已经符合我们的需求,同时剖析他们的优缺点以得出是否存在更佳方案的结论。

尽管市场上存在如 Axios 这样的成熟请求库,它们通过拦截器等机制提供了一定程度的扩展性,但在多端适配、中间件管理、动态配置等方面仍显不足。例如,Axios 虽然在 Web 开发中广受欢迎,但其在原生App内嵌H5页面的兼容处理、以及跨平台的灵活性方面存在局限。对比之下,我们新开发的基于中间件模式的统一请求库能够提供更为灵活的配置和扩展机制,不仅能够动态管理请求流程中的各个环节,还能更好地适应不同平台和应用场景的需求。

图片

(client-server中请求库示意图)

我们再来看看浏览器原生提供的 XHR 和 fetch,XHR 的历史无需赘述,但 fetch 作为“下一代Ajax”标准,我们相信它能走的更远。但同时fetch目前只具备发起请求的核心能力,而在请求的前后处理方面,它并不直接负责这些部分,因为我们可以将其作为框架的一部分,即标准之一,因为这样才能让更多的团队适应以及接受这个约定。

社区似乎并不能完美覆盖我们的场景,不如我们先尝试定义一套协议,由于我们已经对 KOA 等服务框架比较了解,其设计模式启发了我们,因此我们决定以中间件模式作为我们的基础通用型协议来实现我们的“统一请求库”。

目标

首先,我们先为统一请求库下一个定义:

⭐️ 一个 标准、灵活、强大 的 服务调用工具集 ⭐️

我们主要从以下几个方面来看,统一请求库能够解决的问题:

1.  标准化和统一:统一不同前端仓库中的接口调用方式,从而降低因使用不同请求库而引发的行为差异和问题;

2.  完成场景优化:多平台代理优化( App 端 内嵌H5 场景的请求库) 彻底解决体积过大等历史问题;

3.  实现一键全局能力注册;

4.  集成基础设施:统一请求库与全团队的基础技术生态进行集成,形成生态;

设计思路与实现

模式选型思路

为了解决B站复杂的业务场景,我们先将应用拆成各个单一的场景。在各个单一的场景下,我们希望一个单例对应一个场景,并且我们考虑到业务的灵活多变性,需要提供集成插件式逻辑的能力。

我们首先确定的是需要基于面向对象开发。其次,我们将工厂集成方案放在一边,暂时因为这个在B站内部的历史方案的不足,已经得到了一定的验证。我们必须肯定的是 Axios 在前端领域网络请求库的翘楚地位,参考 Axios 中我们认为有价值的拦截器部分,以及 Koa 带来的中间件形式的启发,取二者做结合,这便有了前端领域的中间件模式。根据一开始的需求场景关系,有这么一张图可以理清工作原理。

图片

(需求场景关系图)

这就是请求库基于面向对象设计,提供灵活插件能力,同时根据场景也能保持统一逻辑形式的原理。

中间件模式

在统一请求库中,中间件为我们提供了插件式粒度的集成能力。中间件模式通过将请求处理流程分解为一系列独立的功能单元,每个单元负责处理请求的一个特定方面,如日志记录、错误处理、数据转换等。这些中间件按照预定的顺序组成一个处理链,请求和响应数据在链中依次通过每个中间件,每个中间件可以对数据进行处理、修改或者直接终止请求。这种模式的灵感来源于 Koa 框架的洋葱模型,其中请求和响应像穿过洋葱的每一层一样,经过每个中间件的处理。这不仅增强了请求处理的灵活性和可扩展性,而且使得每个中间件都可以独立开发和测试,大大提高了代码的可维护性。它们在一定的秩序下这些中间件组合成一个请求逻辑,这个秩序,我们同样选择了洋葱模式。在开始我们的请求库示例之前,我们先介绍一下“洋葱模型”。

什么是洋葱模型?

想象一下,一个洋葱的结构——由许多层组成,每剥开一层就能看到下一层。在洋葱模式中,每个中间件都像是洋葱的一层,请求从最外层开始传递,依次穿过每一层中间件,直到达到核心处理逻辑,然后再逐层返回,每一层都有机会对请求和响应进行处理。

图片

Koa的洋葱模型

我们的统一请求库也基于洋葱模型有自己的调用链:

图片

请求库的洋葱模型

洋葱模式同时是一种责任链模式(Chain of Responsibility Pattern),这种设计模式相较于传统的大工厂模式优点十分明显,它能最大程度地降低耦合度,增强了每个节点的灵活性,并且职责极其明确。

图片

当然,这种模式也有一定的缺点,例如调试较困难以及链的管理等问题,需要好的团队风格规范去约束。

因此我们需要约定请求库中间件的处理原则来尽量规避这些问题,例如我们原则上规定允许编辑上下文中的 request 对象,而不是 config 对象。

中间件的扩展性

按照职责的划分,我们有了明确的独立处理者。于是在职责明确前,做好基础协议的扩展也是必不可少的环节。为了实现统一的请求库,我们就要让它的各个部分做到可增、可减、可替换以及可拓展。

基础模型

提供一个中间件基础抽象类,作为所有插件的原型,这是面向对象开发中最重要的一步,同时也提供了类型检查以提示开发者实现必须的接口。

覆盖预设行为

从内部内置的中间件开始,我们就设计成可通过同名方式索引替换目标内置中间件,用户不仅可以通过自定义的方式创建同名中间件,也可以通过继承或者直接调用这些中间件的静态方法来快速实现内置能力的拓展。

另外,我们区分了 Fetch 中间件和其他所有中间件,因为从根源上这两类是有区别的。其他所有中间件在顺序上,按照先全局后临时的方式排列,最后的,也是作为洋葱中心的就是 Fetch 中间件,因此在覆盖这些预设行为上,我们也做了不同的接口区分。

例如实例化的时候,我们设计的接口默认是传入中间件列表,是因为修改 Fetch 是一个低频的行为,并且在传入对象配置时,可以让用户清晰的将二者分开。

 ...
    /**
     * 初始化
     * @param initConfig 初始配置,参考interface
     */
    (initConfig?: IHttpServiceInit | IHttpSvcMiddleware[]);   
    ...
}
    
// interface
export interface IHttpServiceInit {
  baseURL?: FetchBaseURL
  fetch?: IHttpSvcMiddleware
  middlewares?: IHttpSvcMiddleware[]
}

范围制定

我们提出了“全局”与“临时”这样的范围概念。全局中间件的作用范围在实例化的时候就已经确定,至于这个中间件逻辑是否真正作用于该实例发起的每一个请求,其实还是掌握在用户手中。通过基于基础抽象类去派生中间件,这种模式允许用户自行定义子类的更多行为,这从某种意义上也是扩展性的一部分。

例如,用户可以真正实现 Provide/Inject,即先注入,后按需使用,这完全取决于你在撰写中间件内部逻辑时是否默认开启功能,从而在后续调用请求时,是否允许通过一个激活的行为,将逻辑开关打开。

const globalMeta = (ctx, next, config) => {
    if (config?.payload?.active) {
        ctx.request.params["meta"] = {
            platform: "web",
            device: ""
        }
    }
    await next()
}
class GlobalMeta extends Middleware {
    name = "GLOBA_META"
    constructor() {
        super(globalMeta)
    }
}

const httpSvc = new HttpService([new GlobalMeta()])
// 默认该能力注册,但是没有激活标识不会工作。

httpSvc.with("GLOBAL_META", { active: true })
// 通过指定激活全局注册过的中间件名来使其工作(使活跃)

从这些目标出发,可以找到一一对应的关系:

  1. 索引键——中间件名称

  2. 全局注册方式——Register API

  3. 临时携带处理者及状态——With API

  4. 通过索引中间件名称的方式,临时禁用全局能力——Disable API

Package的分离

我们将 middleware 作为一个独立的包,而不是作为请求库这个框架的子导出成员,考虑这样做的原因是可以减轻用户制作中间件的理解成本,并且允许这种方式的一个重要条件是中间件的基础形态已经确认。

控制器

我们为请求库设计了三个控制模块:

1.  ConfigCtrl,配置控制器

2.  AssembleCtrl,装配控制器

3.  RequestCtrl,请求控制器

如若用一句话串联起这三个模块:在使用请求库发起请求时,调用装配控制器收集全部的临时中间件与 Payload(载荷),同时将 Payload 存入配置控制器中,收集完毕后通过组合中间件逻辑产出请求方法,同时配置控制器将产出中间件配置上下文,将这些物料一齐汇向请求控制器,创建请求上下文,发起请求。

通过依赖注入的方式,将三个职责分明的模块相互关联起来:

整体设计图

装配控制器(AssembleCtrl)

顾名思义,装配控制器负责装配各种物件,具体物件包括:

1.  中间件

2.  临时 Payload

它既可以为已经注册过的全局中间件装配 Payload,也可以携带临时中间件一同挂载 Payload;除了增,也能减;我们也提供了禁用功能,可以禁用那些已经全局注册的中间件;

这一增一减就能够灵活控制用于发起请求的所有中间件的活跃态;并且临时 Payload 的设计相较于传统的一个大 Config 对象模式更直观清晰,这有效地避免了配置成员无限增加这种不可维护的趋势,同时相较于大对象内置逻辑的这种方式,我们的中间件逻辑都是可热拔插的。

配置控制器(ConfigCtrl)

配置控制器为每次请求生成中间件上下文,所有的配置都记录在此,组装时参与各种设置配置,运行时为中间件注入配置。

请求控制器(RequestCtrl)

在装配控制器为核心的模式下,准备好一切条件,发起请求时调用请求控制器,请求控制器用于将组装好的请求函数(RequestFunction),结合中间件配置上下文以及初始请求配置生成 Context 后,执行请求。

小结

装配控制器作为链式模式最重要的控制器,也为 HTTP Service 提供直接的 API,实现它循环链式调用能力的核心需要实现一个 Dispatcher(调度器)。

通过这个调度器,三个控制器在一次请求调度发起时的工作流程如下:

调度流程图

实现组合排列

通过调度器的调度,无论是添加 Payload 还是设置禁用,我们都要有一套明确的组织规则去将调度后得到的资源进行有序整合。上文提到,我们确定了先全局、后临时的基调,这在直觉上也是符合规律的。

全局注册的时机相对是靠前的,这个时机远远早于临时携带的部分,所以才会有全局注册能力在前这一说法。当然,想到这里,我们也考虑到用户也可能有想操作全局已经固定位置的中间件顺序,因此我们可以继续向上扩展,于是有了全局中间件提权的逻辑。

图片

全局提权行为

成果

直接收益

各个团队通过接入统一请求库,首先在接入公司级通用能力时,仅需通过安装 NPM 包即可快速集成,所需开发资源大大降低。在适配多种平台方面,我们提供了平替老 SDK 的专用 Fetch 中间件包,解放了受困于旧的封装形式,稳固了中间件拓展集成的风格,而且还为构建一个健康、可持续发展的前端技术生态打下了坚实的基础。

图片

价值模型

横向对比

假设我们调用一个API,要实现:

1.  对入参进行 encode

2.  请求需要上报状态(成功or失败)

3.  需要获取 headers 里的某标记

4.  该接口返回的是非 JSON 格式(如纯文本)

5.  服务端渲染请求时需要透传 headers(user-agent,ip 等 kv)

传统方式

// 传统 API 形式
// http 内部需要实现 encode,responseType 等逻辑
http.request({
    url: '/xxx',
    params: { id: 1 },
    encode: true,
    report: true,
    responseType: 'text',
    responseAll: true,
    headers: {
        ...(typeof window === 'undefined' ? context.headers : {})
    },
})

每需要增加一个 config key,都要深入http内核里去增加一个 if case,内核会随着迭代越来越大直至耦合到难以拆分的地步。

统一请求库方式

// 请求库
httpSvc
    .with(encodeHandler),
    .with(reportHandler),
    .with('RES_DATA', { type: 'text' }) // 对应 responseType
    .disable('RES_EXTRACT') // 负责从 response 对象中取得data数据,我们此举会禁用该中间件从而实现获取到整个响应对象,对应上面的responseAll的输入
    .with('SERVER_SIDE', { headers: context.headers || {} })
    .request({
        url: '/xxx',
        params: { id: 1 },
    })

对比传统方式的每增加一个功能就要增加一个配置键这种形式,请求库通过链式方式顺序一一调用指定中间件的方式,直观清晰,并且充分解耦,好维护。

结语

随着 HttpService 的不断成熟和完善,它已成为我们处理前端网络请求不可或缺的基础能力之一。它的灵活性和扩展性极大地简化了前端开发工作,使我们能够更加专注于创造出色的用户体验。随着技术的不断进步,我们相信,基于中间件模式的请求库将继续引领前端请求处理的创新,为开发者带来更多可能性。

通过上面的介绍,相信你已经理解如何使用请求库的几项基本能力了,如果想要更多案例,请前往我们的 Github(https://github.com/bilibili/http-service)站点,在那您将获得:

  1. 内置中间件定义及说明

  2. 社区已发布的公共中间件

  3. 更全的设计介绍,方案对比

  4. 玩坏中间件的N种方式

  5. 更多精彩等待您的发现与加入

图片

-End-

作者丨Josper

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/598611.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

有没有电脑桌面监控软件|十大电脑屏幕监控软件超全盘点!

当然,目前市场上有许多电脑桌面监控软件可供选择,它们各有特色,旨在满足不同企业和个人对于远程监控、安全管理、提高工作效率等方面的需求。以下是根据近期资料整理的十大电脑屏幕监控软件盘点,包括它们的一些特点和优势&#xf…

Web3:下一代互联网的科技进化

随着科技的不断演进,互联网已经成为了我们生活中不可或缺的一部分。而在Web3时代,我们将会见证互联网进化的下一个阶段。本文将探讨Web3作为下一代互联网的科技进化,以及它所带来的重要变革和影响。 传统互联网的局限性 传统互联网存在诸多…

如何从零开始学习数据结构?

在开始前我有一些资料,是我根据网友给的问题精心整理了一份「数据结构的资料从专业入门到高级教程」, 点个关注在评论区回复“888”之后私信回复“888”,全部无偿共享给大家!!!数据结构 算法=程…

MySQL日志机制【undo log、redo log、binlog 】

前言 SQL执行流程图文分析:从连接到执行的全貌_一条 sql 执行的全流程?-CSDN博客文章浏览阅读1.1k次,点赞20次,收藏12次。本文探讨 MySQL 执行一条 SQL 查询语句的详细流程,从连接器开始,逐步介绍了查询缓存、解析 S…

xmind的13个快捷方式

1.新建导图 CtrlshiftN 2.编辑文字 空格键 3.插入图片 Ctrli 4. 插入主题 Enter键 5. 插入主题之前 ShiftEnter键 6. 插入子主题 Tab键 7. 放大导图 “Ctrl”“” 8. 缩小导图 “Ctrl”“-” 9. 复制 CtrlInsert 10. 粘贴 Shift Insert 11. 剪切 ShiftDelete 12. 截图 F7 13. 保…

【Pytorch】5.DataLoder的使用

什么是DataLoader 个人理解是,如果Dataset的所有数据相当于一副扑克牌,DataLoader就相当于从扑克牌中抽取几张,我们可以规定一次抽取的张数,或者以什么规则进行抽取 DataLoader的使用 查阅官网的文档,主要有这几个参数…

Unity Shader中获取像素点深度信息

1.顶点着色器中对深度进行计算 v2f vert(appdata v) {v2f o;o.pos UnityObjectToClipPos(v.vertex);o.uv TRANSFORM_TEX(v.uv, _MainTex);o.depth (o.pos.z / o.pos.w 1.0) * 0.5; // Normalize depth to [0, 1]return o; }但是达不到预期,最后返回的值一直大于…

SpringMVC响应数据

三、SpringMVC响应数据 3.1 handler方法分析 理解handler方法的作用和组成: /*** TODO: 一个controller的方法是控制层的一个处理器,我们称为handler* TODO: handler需要使用RequestMapping/GetMapping系列,声明路径,在HandlerMapping中注册,供DS查找!* TODO: ha…

数据挖掘实战-基于深度学习RNN+CNN的能源价格预测模型

🤵‍♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞&#x1f4…

java JMH 学习

JMH 是什么? JMH(Java Microbenchmark Harness)是一款专用于代码微基准测试的工具集,其主要聚焦于方法层面的基准测试,精度可达纳秒级别。此工具由 Oracle 内部负责实现 JIT 的杰出人士编写,他们对 JIT 及…

翻译《The Old New Thing》 - What is the deal with the ES_OEMCONVERT flag?

What is the deal with the ES_OEMCONVERT flag? - The Old New Thinghttps://devblogs.microsoft.com/oldnewthing/20050719-12/?p34893 Raymond Chen 在 2005年07月19日 ES_OEMCONVERT 标志是怎么回事? 简要 文章讨论了 ES_OEMCONVERT 编辑控件风格的起源和用途…

java将图片转为pdf

效果图 直接上代码 1.引入jar <dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.24</version></dependency> 2.测试类 package pers.wwz.study.img2pdf20240507;import org.a…

MetaCRM upload 任意文件上传漏洞

文章目录 漏洞描述漏洞原理漏洞复现修复建议 漏洞描述 北京美特软件技术有限公司&#xff08;以下简称“美特软件”&#xff09;是一家专业的客户关系管理软件提供商。 美特软件MetaCrm存在文件上传漏洞&#xff0c;攻击者可利用该漏洞上传任意恶意文件。 漏洞原理 在系统u…

什么牌子的骨传导耳机质量好?五大宝藏热门机型测评对比!

我作为一名音乐发烧友&#xff0c;对各类耳机产品都有深入的了解&#xff0c;最近也经常被人问及骨传导耳机哪个牌子好。通过交流&#xff0c;我发现很多人在选择骨传导耳机的时候&#xff0c;都有出现踩坑的情况&#xff0c;这也难怪&#xff0c;随着骨传导耳机热度逐渐增加&a…

护眼灯排名前十的品牌有哪些?护眼灯品牌排行前十名推荐

近视在儿童中愈发普遍&#xff0c;许多家长开始认识到&#xff0c;除了学业成绩之外&#xff0c;孩子的视力健康同样重要。毕竟&#xff0c;学业的落后可以逐渐弥补&#xff0c;而一旦孩子近视&#xff0c;眼镜便可能成为长期伴随。因此&#xff0c;专业的护眼台灯对于每个家庭…

220V转18V500mA非隔离恒压WT5113

220V转18V500mA非隔离恒压WT5113 亲爱的朋友们&#xff0c;你们是否在为如何提高电源方案而烦恼呢&#xff1f;今天我给大家带来了一款芯片&#xff0c;WT5113宽输出范围非隔离交直流转换芯片&#xff0c;它可是电源方案中的得力助手哦&#xff01; 这款芯片拥有220V降12V、2…

docker-compose 运行jenkins 并一键发布springboot项目

文档时间&#xff1a; 2024-05-07 jenkins一键发布原理个人理解&#xff1a; 本质就是通过jenkins 配置git地址&#xff0c;然后通过地址拉取代码到服务器上&#xff0c; 然后jenkins 再通过maven把拉取下来的进行打包。 再通过jenkins 配置的shell命令&#xff0c;在服务…

ubuntu20.04搭建Fabric教程

本章节环境配置 ubuntu: 20.04 go&#xff1a;1.16.3 docker: 20.10.6 docker-compose: 1.27.2 fabric&#xff1a;2.2.0 fabric-ca: 1.4.9 一 搭建通道 新建工作目录 mkdir fabric && cd fabric配置go代理 go env -w GO111MODULEon ​ #更新下载包的镜像 go env …

Python中GDAL批量将多个遥感影像各波段数值缩小10000倍的方法

本文介绍基于Python中的gdal模块&#xff0c;批量读取大量多波段遥感影像文件&#xff0c;分别对各波段数据加以数值处理&#xff0c;并将所得处理后数据保存为新的遥感影像文件的方法。 首先&#xff0c;看一下本文的具体需求。我们现有一个文件夹&#xff0c;其中含有大量.ti…

数据结构:环形链表的实战指南

✨✨小新课堂开课了&#xff0c;欢迎欢迎~✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;http://t.csdnimg.cn/oHJAK&#xff08;数据结构与算法&#xff09; 小新的主页&#xff1a;编程版小新-CSDN博客 …
最新文章