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

前言

对于一个前端工程师而言,每天都在面对的较多的需求场景就是调用后端的接口,但是因为众所周知的原因,前端目前已经有无数种调用接口的方式,例如:之前有基于 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?: FetchBaseURLfetch?: IHttpSvcMiddlewaremiddlewares?: 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.mzph.cn/diannao/7479.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2-qt之信号与槽-简单实例讲解

前言、因实践课程讲解需求,简单介绍下qt的信号与槽。 一、了解信号与槽 怎样使用信号与槽? 概览 还记得 X-Window 上老旧的回调函数系统吗?通常它不是类型安全的并且很复杂。(使用)它(会)有很多…

prometheus+grafana的安装与部署及优点

一、Prometheus 的优点 1、非常少的外部依赖,安装使用超简单; 2、已经有非常多的系统集成 例如:docker HAProxy Nginx JMX等等; 3、服务自动化发现; 4、直接集成到代码; 5、设计思想是按照分布式、微服…

YashanDB与帆软信创商业智能软件完成兼容互认证

近日,深圳计算科学研究院崖山数据库系统YashanDB与帆软信创商业智能软件(V6.0)顺利完成兼容性互认证,经严格测试,双方产品能够相互兼容,稳定运行。 崖山数据库系统YashanDB是深圳计算科学研究院自主研发设计…

构建第一个ArkTS应用之@LocalStorage:页面级UI状态存储

LocalStorage是页面级的UI状态存储,通过Entry装饰器接收的参数可以在页面内共享同一个LocalStorage实例。LocalStorage也可以在UIAbility实例内,在页面间共享状态。 本文仅介绍LocalStorage使用场景和相关的装饰器:LocalStorageProp和LocalS…

[开发|安卓] Android Studio 开发环境配置

Android Studio下载 Android Studio下载地址 下载SDK依赖 1.点击左上角菜单 2.选择工具 3.打开SDK管理中心 4.下载项目目标Android版本的SDK 配置安卓虚拟机 1.打开右上角的设备管理 2.选择合适的手机规格 3.下载并选择项目目标Android系统 4.点击完成配置 …

国内如何下载TikTOK,手机刷机教程

最近很多玩家都来问怎么刷机?手机环境怎么搭建?这里给大家整理了苹果IOS刷机教程 1.iOS下载教程 : 步骤一:手机调试 苹果手机系统配置推荐:iPhone6S以上,16G。 注意:如果是选择购入二手手机…

指针进阶(三)

嘿嘿,uu们,今天呢我们来剖析指针进阶的剩下部分,好啦,废话不多讲,开干! 1:回调函数 概念:回调函数是指一个通过函数指针调用的函数,如果将函数的地址作为参数传递给另外一个函数,当这个指针被用来调用所指向的函数时,那么这个被调用的函数就是回调函数.回调函数不是由该函数的…

2024年中国AI大模型产业发展报告,洞见下一个智能时代!

人民网财经研究院、至顶科技联合发布的《开启智能新时代:2024年中国AI大模型产业发展报告》,全面梳理了我国AI大模型产业的发展背景、现状、应用案例、面临的挑战以及未来趋势。报告指出,AI大模型是全球科技竞争的新高地、未来产业的新赛道、经济发展的新引擎,在我国…

如何高效封装App?小猪APP分发平台一站式解决方案

在移动应用开发领域,App封装(App Packaging)是一个至关重要的环节,它不仅关乎应用的安全性,还直接影响到最终用户体验和市场推广策略。本文旨在通过实战指南,揭示如何高效完成App封装,并介绍如何…

微信报名活动链接怎么做

在数字营销日新月异的今天,微信作为拥有数亿用户的社交平台,早已成为品牌宣传的重要阵地。而微信报名活动链接,更是品牌吸引用户参与、提升活动影响力的关键工具。今天,就让我们一起探讨如何制作一个引人入胜的微信报名活动链接&a…

信创 | 2023年中国信创产业深度研究报告(完整版)

信创产业研究报告 免责声明:本文资料来源于“第一新声”,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将在第一时间协商版权问题或删除内容! 获取文中相关的PPT资料,请关注文末公众号“程序员…

【JVM】内存结构

内存结构 Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。 线程私有…

Baidu Comate 智能编码助手:编程新伙伴,效率新飞跃

作者简介:一名云计算网络运维人员、每天分享网络与运维的技术与干货。 公众号:网络豆云计算学堂 座右铭:低头赶路,敬事如仪 个人主页: 网络豆的主页​​​​​ 目录 写在前面 一、Baidu Comate智能编码助手简介…

STL——deque

双端队列,可以对头端进行插入删除操作 记录一个常遇到的问题 deque subscript out of range 使用了还未定义的空间,大概率是没有初始化就使用了下标或者其他方式进行数据访问。 与vector区别 内部实现方式:deque采用了分段连续存储的方式&…

Java代码基础算法练习-年龄问题-2024.05.07

数学家维纳智力早熟,11岁就上了大学。一次,他参加某个重要会议,年轻的脸孔引人注目。于是有人询问他的年龄,他回答说:“我年龄的立方是个4位数。我年龄的4次方是个6位数。这10 个数字正好包含了从0到9这10个数字&#…

mac安装linux的centos strem9 虚拟机解压rar文件报错

背景:解压rar文件需要再linux上安装unrar工具 yum install unrar直接安装的后解压报错,如图 解决办法: 下载:wget https://www.rarlab.com/rar/rarlinux-x64-6.0.2.tar.gz 安装: tar -zxvf rarlinux-x64-6.0.2.tar.gz cd rar …

分销体系搭建sop,如何快速建立分销体系

坐标:厦门,我是易创客运营肖琳 深耕社交新零售行业10年,主要提供新零售系统工具及顶层商业模式设计、全案策划运营陪跑等。 今天为大家介绍分销体系搭建 SOP。 分销是什么? 分销是一种店铺利用客户推广带来流量与销量的营销工具。分销商通过…

Puppeteer的基本使用及多目标同时访问

文章目录 一、安装 puppeteer 并更改默认缓存路径1、更改 Puppeteer 用于安装浏览器的默认缓存目录2、安装 puppeteer3、项目结构目录 二、基本使用1、启动浏览器并访问目标网站2、生成截图3、生成 PDF 文件4、获取目标网站 html 结构并解析5、拦截请求6、执行 JavaScript7、同…

大模型最新消息

最新消息如下: 大语言模型服务的多样化:互联网上出现了许多免费的大语言模型服务,如OpenAI的ChatGPT、Google的Gemini、Anthropic的Claude、Meta的Llama等。这些服务的推出使得大语言模型的应用更加广泛和便捷。软银和苹果的AI新动向&#x…