当前位置: 首页 > Web前端 > HTML

基于Observable构建前端防腐策略

时间:2023-03-28 13:11:04 HTML

介绍:ToB业务的生命周期和迭代通常持续多年。随着产品的迭代演进,以接口调用为中心的前后端关系会变得非常复杂。经过多年的迭代,界面的任何修改都可能给产品带来不可预知的问题。在这种情况下,构建一个更加健壮的前端应用,保证前端在长期迭代下的稳定性和扩展性就变得非常重要。本文将重点介绍如何使用接口防腐策略来避免或减少接口变更对前端的影响。作者|谢亚东来源|阿里科技公众号ToB业务的生命周期和迭代通常会持续很多年。随着产品的迭代和演进,以接口调用为中心的前后端关系会变得非常复杂。经过多年的迭代,界面的任何修改都可能给产品带来不可预知的问题。在这种情况下,构建一个更加健壮的前端应用,保证前端在长期迭代下的稳定性和扩展性就变得非常重要。本文将重点介绍如何使用接口防腐策略来避免或减少接口变更对前端的影响。1、困难与难点为了更清楚的说明前端面临的困难,我们以ToB业务中常见的dashboard页面为例。该页面包括三个部分的信息显示:可用内存、已用内存和已用内存比例。此时前端组件与接口的依赖关系如下图所示。调整接口返回结构时,需要调整MemoryFree组件对接口的调用方式。同样,必须修改MemoryUsage和MemoryUsagePercent才能工作。一个真正的ToB业务面对的接口可能有上百个,组件和接口的集成逻辑远比上面的例子复杂。经过几年甚至更长时间的迭代,界面会逐渐产生多个版本。出于对界面稳定性和用户习惯的考虑,前端往往依赖多个版本的界面同时构建界面。当一些接口需要离线调整或更改时,前端需要重新理解业务逻辑,并进行大量的代码逻辑调整,以保证接口的稳定运行。常见的影响前端的接口变更包括但不限于:返回字段调整调用方法变更、多版本共存等,当前端面对平台化业务时,此类问题会变得更加棘手。平台产品会封装一个或多个底层引擎。例如,机器学习平台可以基于TensorFlow、Pytorch等机器学习引擎构建,实时计算平台可以基于Flink、Spark等计算引擎构建。虽然平台会在上层封装引擎的大部分接口,但难免会有部分底层接口直接透传给前端。界面变化带来的挑战。前端面临的困境是由独特的前后端关系决定的。与其他领域不同,在ToB业务中,前端通常作为下游客户接受后端供应商的供货,在某些情况下成为后端的跟随者。在客户/供应商关系中,前端在下游,后端团队在上游,界面内容和上线时间通常由后端团队决定。在follower关系中,上游后端团队不会根据前端团队的需求做任何调整,前端只能顺应上游后端的模型。这种情况一般出现在前端无法对上游后端团队施加影响的时候,比如前端需要基于开源项目设计界面,或者后端团队的模型非常成熟,很难修改。《架构整洁之道》的作者描述了这样一个嵌入式架构设计的难题,和我们上面描述的困境非常相似。软件应该是长寿的东西,固件会随着硬件的发展而过时,但现实是,虽然软件本身不会随着时间的推移而磨损,但硬件及其固件会。随着时间的推移过时,需要对软件进行相应的更改。不管是customer/supplier关系还是follower关系,就像软件不能决定硬件的发展和迭代一样,前端也很难或不可能决定引擎和界面的设计,虽然前端-end本身不会随时间改变。不可用,但技术引擎和相关接口会随着时间的推移而过时,前端代码会随着技术引擎的迭代更替而逐渐腐烂,最终难逃被迫重写的命运。两个防腐层设计早在Windows诞生之前,工程师们就引入了HAL(HardwareAbstractionLayer)的概念,以解决上述硬件、固件和软件的可维护性问题。HAL为软件提供服务,屏蔽硬件的实现细节。无需因硬件或固件更改而频繁修改软件。HAL的设计思想在领域驱动设计(DDD)中也被称为AnticorruptionLayer。在DDD定义的各种上下文映射关系中,防腐层是防御性最强的。它经常在下游团队需要防止外部技术偏好或领域模型入侵时使用,并且可以帮助隔离上游模型与下游模型。我们可以在前端引入防腐层的概念,以减少或避免当前后端上下文映射接口变化对前端代码的影响。工业上有多种实施防腐层的方法。GraphQL和近几年流行的BFF都可以作为替代方案,但技术选型也受限于业务场景。与ToC业务完全不同,在ToB业务中,前后端关系通常是customer/supplier或follower/follower关系。在这种关系下,希望后端配合前端用GraphQL改造接口已经变得不现实,而BFF的建设一般需要额外的部署资源和运维成本。以上案例中,在浏览器端构建防腐层是一个比较可行的方案,但是在浏览器端构建防腐层也面临着挑战。无论是React、Angular还是Vue,数据层的解决方案数不胜数,从Mobx、Redux、Vuex等等,这些数据层的解决方案实际上都会侵入视图层。有没有可以和视图层结合的防腐层方案?完全脱钩呢?以RxJS为代表的Observable方案可能是此时最好的选择。RxJS是ReactiveX项目的JavaScript实现,它最初是由微软的架构师ErikMeijer领导的团队开发的LINQ的扩展。本项目的目标是提供一致的编程接口,帮助开发者更方便地处理异步数据流。目前在开发中经常使用RxJS作为响应式编程开发工具,但是在构建防腐层的场景中,以RxJS为代表的Observable方案也可以发挥巨大的作用。我们选择RxJS主要基于以下考虑:能够统一不同的数据源:RxJS可以将websocket、http请求,甚至用户操作、页面点击等转化为一个统一的Observable对象。统一不同类型数据的能力:RxJS将异步和同步数据统一到Observable对象中。丰富的数据处理能力:RxJS提供了丰富的Operator操作符,可以在订阅前对Observables进行预处理。不侵入前端架构:RxJSObservable和Promise可以相互转换,也就是说RxJS的所有概念都可以完全封装在数据层,只有Promise可以暴露给视图层。引入RxJS将所有类型的接口都转化为Observable对象后,前端视图组件将只依赖Observable,与接口实现细节解耦。同时,Observable可以转化为Promise,在视图层Promises中得到什么,可以与任何数据层解决方案和框架一起使用。除了转为Promise,开发者还可以在渲染层混入RxJS方案,比如rxjs-hooks,以获得更好的开发体验。三层防腐层的实现参考上面的防腐层设计,我们一开始在dashboard项目中实现了以RxJSObservable为核心的防腐层代码。防腐层核心代码如下()));}导出函数getMemoryUsageObservable():Observable{returnfromFetch("/api/v1/memory/usage").pipe(mergeMap((res)=>res.json()));}导出函数getMemoryUsagePercent():Promise{returnlastValueFrom(forkJoin([getMemoryFreeObservable(),getMemoryUsageObservable()]).pipe(map(([usage,free])=>+((usage/(usage+free))*100).toFixed(2))));}导出函数getMemoryFree():Promise{returnlastValueFrom(getMemoryFreeObservable());}导出函数getMemoryUsage():Promise{returnlastValueFrom(getMemoryUsageObservable());}MemoryUsagePercent实现代码如下。此时组件不再依赖于具体的接口,而是直接依赖于防腐层的实现。functionMemoryUsagePercent(){const[usage,setUsage]=useState(0);useEffect(()=>{(async()=>{constresult=awaitgetMemoryUsagePercent();setUsage(result);})();},[]);return

Usage:{usage}%
;}exportdefaultMemoryUsagePercent;1调整返回字段和更改返回字段时,防腐层可以有效拦截接口对组件的影响.当/当api/v2/quota/free和/api/v2/quota/usage的返回数据变为如下结构时`{requestId:string;data:number;}`,我们只需要调整防腐层的两行代码即可,注意这个我们上层封装的getMemoryUsagePercent是建立在Observable上的所以不需要改动。导出函数getMemoryUsageObservable():Observable{returnfromFetch("/api/v2/memory/free").pipe(mergeMap((res)=>res.json()),+map((data)=>data.data));}导出函数getMemoryUsageObservable():Observable{returnfromFetch("/api/v2/memory/usage").pipe(mergeMap((res)=>res.json()),+map((data)=>data.data));}在Observable防腐层中,有高阶Observable和低阶Observable两种设计。上面的例子中,FreeObservable和UsageObservable是低级封装,PercentObservable使用Free和Usage的Observable进行高层封装。当底层封装发生变化时,由于Observable本身的特性,高层封装往往不需要做任何改动。这就是防腐层带给我们的。额外的好处。2调用方式的改变当调用方式改变时,防腐层也能发挥作用。/api/v3/memory直接返回free和usage的数据,接口格式如下。{requestId:字符串;数据:{免费:数字;用法:数字;}}防腐层代码只需要更新如下,保证组件层代码不需要修改。导出函数getMemoryObservable():Observable<{free:number;用法:数字}>{returnfromFetch("/api/v3/memory").pipe(mergeMap((res)=>res.json()),map((data)=>data.data));}export函数getMemoryFreeObservable():Observable{returngetMemoryObservable().pipe(map((data)=>data.free));}exportfunctiongetMemoryUsageObservable():Observable{returngetMemoryObservable().pipe(map((data)=>data.usage));}导出函数getMemoryUsagePercent():Promise{returnlastValue(getMemoryObservable().pipe(map(({usage,free})=>+((usage/(usage+free))*100).toFixed(2))));}3多版本共存当前端代码需要部署到多个环境时,部分环境可以使用v3的接口,但只有接口v2部署在某些环境中。这时,我们仍然可以在防腐层中屏蔽环境的差异。导出函数getMemoryLegacyObservable():Observable<{free:number;usage:number}>{constlegacyUsage=fromFetch("/api/v2/memory/usage").pipe(mergeMap((res)=>res.json()));constlegacyFree=fromFetch("/api/v2/memory/free").pipe(mergeMap((res)=>res.json()));returnforkJoin([legacyUsage,legacyFree],(usage,free)=>({free:free.data.free,usage:usage.data.usage,}));}导出函数getMemoryObservable():Observable<{free:数字;用法:数字}>{constcurrent=fromFetch("/api/v3/memory").pipe(mergeMap((res)=>res.json()),map((data)=>data.data));returnrace(getMemoryLegacyObservable(),current);}导出函数getMemoryFreeObservable():Observable{returngetMemoryObservable().pipe(map((data)=>data.free));}导出函数getMemoryUsageObservable():Observable{返回getMemoryObservable().pipe(map((data)=>data.usage));}导出函数getMemoryUsagePercent():Promise{returnlastValue(getMemory().pipe(map(({usage,free})=>+((usage/(usage+free))*100).toFixed(2))));}通过race算子,当v2和v3任意版本的接口可用时,防腐层可以正常工作,没有需要注意组件层接口受环境的影响防腐层的四层附加应用不仅是接口的封装和隔离的附加层,它还可以起到以下作用。1概念映射界面的语义和前端需要的数据的语义有时并不完全对应。在组件层直接调用接口时,需要所有开发人员对接口与接口之间的语义映射有足够的了解。有了防腐层,防腐层提供的调用方法就包含了数据的真实语义,降低了开发者的二次理解成本。2格式适配很多时候,接口返回的数据结构和格式与前端需要的数据格式不匹配。通过在防腐层增加数据转换逻辑,可以减少接口数据对业务代码的侵入。在上面的案例中,我们对getMemoryUsagePercent返回的数据进行了封装,使得组件层可以直接使用百分比数据,无需再次转换。3接口缓存针对多个服务依赖同一个接口的情况,我们可以通过防腐层增加缓存逻辑,从而有效降低调用接口的压力。与格式适配类似,将缓存逻辑封装在防腐层可以避免组件层对数据进行二次缓存,并且可以集中管理缓存的数据,降低代码的复杂度。一个简单的缓存示例如下。classCacheService{私有缓存:{[key:string]:any}={};getData(){如果(this.cache){返回(this.cache);}else{returnfromFetch("/api/v3/memory").pipe(mergeMap((res)=>res.json()),map((data)=>data.data),tap((data)=>{this.cache=数据;}));}}}4稳定性覆盖当接口的稳定性较差时,通常的做法是在组件层处理响应错误。这种自下而上的逻辑通常比较复杂,组件层的维护成本也会很高。我们可以通过防腐层检查稳定性。当接口失效时,我们可以返回自下而上的业务数据。由于自下而上的数据统一维护在防腐层中,后续的测试和修改会更加方便。在上面的多版本共存防腐层中,添加如下代码,即使v2和v3接口无法返回数据,前端依然可以保持可用。returnrace(getMemoryLegacy(),current).pipe(+catchError(()=>of({usage:'-',free:'-'})));5联调联测接口可能存在并行以及前端的发展状态。此时,还没有真正的后端接口可供前端开发使用。与传统的构建mockAPI的方式相比,直接在防腐层mock数据是一种更方便的方案。导出函数getMemoryFree():Observable{returnof(0.8);}exportfunctiongetMemoryUsage():Observable{returnof(1.2);}exportfunctiongetMemoryUsagePercent():Observable{returnforkJoin([getMemoryUsage(),getMemoryFree()]).pipe(map(([usage,free])=>+((usage/(usage+free))*100).toFixed(2)));}在反腐蚀层Mocking数据也可以用来测试页面,比如mock大量数据对页面性能的影响。导出函数getLargeList():Observable{constoptions=[];for(leti=0;i<100000;i++){constvalue=`${i.toString(36)}${i}`;options.push(值);}returnof(options);}五小结在本文中,我们介绍了以下内容:界面频繁变化时前端的困境及原因,防腐层如何设计及选择使用技术Observable实现防腐层的代码示例。防腐层的附加作用。请注意,仅在特定场景引入前端防腐层是合理的,即前端处于跟随者或供应商/客户关系,并面临大量无法保证的接口。稳定兼容。如果可以在后端Gateway上构建防腐层,或者接口数量少,引入防腐层的额外成本将得不偿失。RxJS在防腐层构建场景中提供了更多的Observable能力。如果读者不需要复杂的算子转换工具,也可以自己搭建Observable构建方案。其实只需要100行代码就可以实现https://stackblitz.com/edit/m...。修改后的前端架构将不再直接依赖接口实现,不会侵入现有前端数据层设计,还可以承担概念映射、格式适配、接口缓存、稳定性保障、协助联调测试等工作.本文中的所有示例代码都可以在存储库https://github.com/vthinkxie/…中找到。原文链接本文为阿里云原创内容,未经许可不得转载。