网站平台设计 问题,小程序怎么开,重庆住房和城乡建设厅网站,济南seo网站建设简介#xff1a;To B 业务的生命周期与迭代通常会持续多年#xff0c;随着产品的迭代与演进#xff0c;以接口调用为核心的前后端关系会变得非常复杂。在多年迭代后#xff0c;接口的任何一处修改都可能给产品带来难以预计的问题。在这种情况下#xff0c;构建更稳健的前端…简介To B 业务的生命周期与迭代通常会持续多年随着产品的迭代与演进以接口调用为核心的前后端关系会变得非常复杂。在多年迭代后接口的任何一处修改都可能给产品带来难以预计的问题。在这种情况下构建更稳健的前端应用保证前端在长期迭代下的稳健与可拓展性就变得非常重要。本文将重点介绍如何利用接口防腐策略避免或减少接口变更对前端的影响。 作者 | 谢亚东 来源 | 阿里技术公众号
To B 业务的生命周期与迭代通常会持续多年随着产品的迭代与演进以接口调用为核心的前后端关系会变得非常复杂。在多年迭代后接口的任何一处修改都可能给产品带来难以预计的问题。在这种情况下构建更稳健的前端应用保证前端在长期迭代下的稳健与可拓展性就变得非常重要。本文将重点介绍如何利用接口防腐策略避免或减少接口变更对前端的影响。
一 困境与难题
为了更清晰解释前端面临的难题我们以 To B 业务中常见的仪表盘页面为例该页面包含了可用内存、已使用内存和已使用的内存占比三部分信息展示。 此时前端组件与接口之间的依赖关系如下图所示。 当接口返回结构调整时MemoryFree 组件对接口的调用方式需要调整。同样的MemoryUsage 与 MemoryUsagePercent 也要进行修改才能工作。 真实的 To B 业务面临的接口可能会有数百个组件与接口的集成逻辑也远比以上的例子要复杂。
经过数年甚至更长时间的迭代后接口会逐步产生多个版本出于对界面稳定性及用户使用习惯的考量前端往往会同时依赖接口的多个版本来构建界面。当部分接口需要调整下线或发生变更时前端需要重新理解业务逻辑并做出大量代码逻辑调整才能保证界面稳定运行。 常见的对前端造成影响的接口变更包括但不限于
返回字段调整调用方式改变多版本共存使用
当前端面对的是平台型业务时此类问题会变得更为棘手。平台型产品会对一种或多种底层引擎进行封装例如机器学习平台可能会基于 TensorFlow、Pytorch 等机器学习引擎搭建实时计算平台可能基于 Flink、Spark 等计算引擎搭建。
虽然平台会对引擎的大部分接口进行上层封装但不可避免的仍然会有部分底层接口会直接被透传到前端在这个时候前端不仅要应对平台的接口变更还会面临着开源引擎接口的变更带来的挑战。 前端在面临的困境是由独特的前后端关系决定的。与其他领域不同在 To B 业务中前端通常以下游客户的身份接受后端供应商的供给有些情况下会成为后端的跟随者。
在客户/供应商关系中前端处于下游而后端团队处于上游接口内容与上线时间通常由后端团队来决定。
在跟随者关系中上游的后端团队不会去根据前端团队的需求进行任何调整前端只能去顺应上游后端的模型。这种情况通常发生在前端无法对上游后端团队施加影响的时刻例如前端需要基于开源项目的接口设计界面或者是后端团队的模型已经非常成熟且难以修改时。 《架构整洁之道》的作者描述过这样一个嵌入式架构设计的难题与上文我们描述的困境十分类似。
软件应当是一种使用周期很长的东西而固件会随着硬件的演进而淘汰过时但事实上的情况是虽然软件本身不会随着时间推移而磨损但硬件及其固件却会随时间推移而过时随即也需要对软件做相应的改动。
无论是客户/供应商关系还是跟随者关系正如软件无法决定硬件的发展与迭代一样前端也很难或者无法决定引擎与接口的设计虽然前端本身不会随着时间的推移而变得不可用但技术引擎及相关接口却会随着时间推移而过时前端代码会跟随技术引擎的迭代更换逐步腐烂最终难逃被迫重写的命运。 二 防腐层设计
早在 Windows 诞生之前工程师为了解决上文中硬件、固件与软件的可维护性问题引入了 HALHardware Abstraction Layer的概念 HAL 为软件提供服务并且屏蔽了硬件的实现细节使得软件不必由于硬件或者固件的变更而频繁修改。 HAL 的设计思想在领域驱动设计DDD 中又被称为防腐层Anticorruption Layer。在 DDD 定义的多种上下文映射关系中防腐层是最具有防御性的一种。它经常被使用在下游团队需要阻止外部技术偏好或者领域模型入侵的情况可以帮助很好地隔离上游模型与下游模型。
我们可以在前端中引入防腐层的概念降低或避免当前后端的上下文映射接口变更对前端代码造成的影响。 在行业内有很多种方式可以实现防腐层无论是近几年大火的 GraphQL 还是 BFF 都可以作为备选方案但是技术选型同样受限于业务场景。与 To C 业务完全不同在 To B 业务中前后端的关系通常为客户/供应商或者跟随者/被跟随者的关系。在这种关系下寄希望于后端配合前端对接口进行 GraphQL 改造已经变得不太现实而 BFF 的构建一般需要额外的部署资源及运维成本。
在上述情况下在浏览器端构建防腐层是更为可行的方案但是在浏览器中构建防腐层同样面临挑战。
无论是 React、Angular 还是 Vue 均有无数的数据层解决方案从 Mobx、Redux、Vuex 等等这些数据层方案对视图层实际上都会有入侵有没有一种防腐层解决方案可以与视图层彻底解耦呢以 RxJS 为代表的 Observable 方案在这时可能是最好的选择。 RxJS 是 ReactiveX 项目的 JavaScript 实现而 ReactiveX 最早是 LINQ 的一个扩展由微软的架构师 Erik Meijer 领导的团队开发。该项目目标是提供一致的编程接口帮助开发者更方便的处理异步数据流。目前 RxJS 在开发中经常被作为响应式编程开发工具使用但是在构建防腐层的场景中RxJS 代表的 Observable 方案同样可以发挥巨大作用。 我们选择 RxJS 主要基于以下几点考虑
统一不同数据源的能力RxJS 可以将 websocket、http 请求、甚至用户操作、页面点击等转换为统一的 Observable 对象。统一不同类型数据的能力RxJS 将异步数据和同步数据统一为 Observable 对象。丰富的数据加工能力RxJS 提供了丰富的 Operator 操作符可以对 Observable 在订阅前进行预先加工。不入侵前端架构RxJS 的 Observable 可以与 Promise 互相转换这意味着 RxJS 的所有概念可以被完整封装在数据层对视图层可以只暴露 Promise。
当在引入 RxJS 将所有类型的接口转换为 Observable 对象后前端的视图组件将仅依赖 Observable并与接口实现的细节解耦同时Observable 可以与 Promise 相互转换在视图层获得的是单纯的 Promise可以与任意数据层方案和框架搭配使用。
除了转换为 Promise 之外开发者也可以与 RxJS 在渲染层的解决方案例如 rxjs-hooks 混用获得更好的开发体验。 三 防腐层实现
参照上文的防腐层设计我们在开头的仪表盘项目中实现以 RxJS Observable 为核心的防腐层代码。 其中防腐层的核心代码如下
export function getMemoryFreeObservable(): Observablenumber {return fromFetch(/api/v1/memory/free).pipe(mergeMap((res) res.json()));
}export function getMemoryUsageObservable(): Observablenumber {return fromFetch(/api/v1/memory/usage).pipe(mergeMap((res) res.json()));
}export function getMemoryUsagePercent(): Promisenumber {return lastValueFrom(forkJoin([getMemoryFreeObservable(), getMemoryUsageObservable()]).pipe(map(([usage, free]) ((usage / (usage free)) * 100).toFixed(2))));
}export function getMemoryFree(): Promisenumber {return lastValueFrom(getMemoryFreeObservable());
}export function getMemoryUsage(): Promisenumber {return lastValueFrom(getMemoryUsageObservable());
}
MemoryUsagePercent 的实现代码如下此时该组件将不再依赖具体的接口而直接依赖防腐层的实现。
function MemoryUsagePercent() {const [usage, setUsage] useStatenumber(0);useEffect(() {(async () {const result await getMemoryUsagePercent();setUsage(result);})();}, []);return divUsage: {usage} %/div;
}export default MemoryUsagePercent;
1 返回字段调整
返回字段变更时防腐层可以有效拦截接口对组件的影响当 /api/v2/quota/free 与 /api/v2/quota/usage 的返回数据变更为以下结构时
{requestId: string;data: number;
}
我们只需要调整防腐层的两行代码注意此时我们的上层封装的 getMemoryUsagePercent 基于 Observable 构建所以不需要进行任何改动。
export function getMemoryUsageObservable(): Observablenumber {return fromFetch(/api/v2/memory/free).pipe(mergeMap((res) res.json()),map((data) data.data));
}export function getMemoryUsageObservable(): Observablenumber {return fromFetch(/api/v2/memory/usage).pipe(mergeMap((res) res.json()),map((data) data.data));
}
在 Observable 化的防腐层中会存在高阶 Observable 与 低阶 Observable 两种设计在上文的例子中Free Observable 和 Usage Observable 为低阶封装而 Percent Observable 利用 Free 和 Usage 的 Observable 进行了高阶封装当低阶封装改动时由于 Observable 本身的特性高阶封装经常是不需要进行任何改动的这也是防腐层给我们带来的额外好处。 2 调用方式改变
当调用方式发生改变时防腐层同样可以发挥作用。/api/v3/memory 直接返回了 free 与 usage 的数据接口格式如下。
{requestId: string;data: {free: number;usage: number;}
}
防腐层代码只需要进行如下更新就可以保障组件层代码无需修改。
export function getMemoryObservable(): Observable{ free: number; usage: number } {return fromFetch(/api/v3/memory).pipe(mergeMap((res) res.json()),map((data) data.data));
}export function getMemoryFreeObservable(): Observablenumber {return getMemoryObservable().pipe(map((data) data.free));
}export function getMemoryUsageObservable(): Observablenumber {return getMemoryObservable().pipe(map((data) data.usage));
}export function getMemoryUsagePercent(): Promisenumber {return lastValue(getMemoryObservable().pipe(map(({ usage, free }) ((usage / (usage free)) * 100).toFixed(2))));
}
3 多版本共存使用
当前端代码需要在多套环境下部署时部分环境下 v3 的接口可用而部分环境下只有 v2 的接口部署此时我们依然可以在防腐层屏蔽环境的差异。 export function getMemoryLegacyObservable(): Observable{ free: number; usage: number } {const legacyUsage fromFetch(/api/v2/memory/usage).pipe(mergeMap((res) res.json()));const legacyFree fromFetch(/api/v2/memory/free).pipe(mergeMap((res) res.json()));return forkJoin([legacyUsage, legacyFree], (usage, free) ({free: free.data.free,usage: usage.data.usage,}));
}export function getMemoryObservable(): Observable{ free: number; usage: number } {const current fromFetch(/api/v3/memory).pipe(mergeMap((res) res.json()),map((data) data.data));return race(getMemoryLegacyObservable(), current);
}export function getMemoryFreeObservable(): Observablenumber {return getMemoryObservable().pipe(map((data) data.free));
}export function getMemoryUsageObservable(): Observablenumber {return getMemoryObservable().pipe(map((data) data.usage));
}export function getMemoryUsagePercent(): Promisenumber {return lastValue(getMemory().pipe(map(({ usage, free }) ((usage / (usage free)) * 100).toFixed(2))));
}
通过 race 操作符当 v2 与 v3 任何一个版本的接口可用时防腐层都可以正常工作在组件层无需再关注接口受环境的影响。
四 额外应用
防腐层不仅仅是多了一层对接口的封装与隔离它还能起到以下作用。
1 概念映射
接口语义与前端需要数据的语义有时并不能完全对应当在组件层直接调用接口时所有开发者都需要对接口与界面的语义映射足够了解。有了防腐层后防腐层提供的调用方法包含了数据的真实语义减少了开发者的二次理解成本。
2 格式适配
在很多情况下接口返回的数据结构与格式与前端需要的数据格式并不符合通过在防腐层增加数据转换逻辑可以降低接口数据对业务代码的入侵。在以上的案例里我们封装了 getMemoryUsagePercent 的数据返回使得组件层可以直接使用百分比数据而不需要再次进行转换。
3 接口缓存
对于多种业务依赖同一接口的情况我们可以通过防腐层增加缓存逻辑从而有效降低接口的调用压力。
与格式适配类似将缓存逻辑封装在防腐层可以避免组件层对数据的二次缓存并可以对缓存数据集中管理降低代码的复杂度一个简单的缓存示例如下。
class CacheService {private cache: { [key: string]: any } {};getData() {if (this.cache) {return of(this.cache);} else {return fromFetch(/api/v3/memory).pipe(mergeMap((res) res.json()),map((data) data.data),tap((data) {this.cache data;}));}}
}
4 稳定性兜底
当接口稳定性较差时通常的做法是在组件层对 response error 的情况进行处理这种兜底逻辑通常比较复杂组件层的维护成本会很高。我们可以通过防腐层对稳定性进行兜底当接口出错时可以返回兜底业务数据由于兜底数据统一维护在防腐层后续的测试与修改也会更加方便。在上文中的多版本共存的防腐层中增加以下代码此时即使 v2 和 v3 接口都无法返回数据前端仍然可以保持可用。 return race(getMemoryLegacy(), current).pipe(catchError(() of({ usage: -, free: - })));
5 联调与测试
接口和前端可能会存在并行开发的状态此时前端的开发并没有真实的后端接口可用。与传统的搭建 mock api 的方式相比在防腐层直接对数据进行 mock 是更方便的方案。
export function getMemoryFree(): Observablenumber {return of(0.8);
}export function getMemoryUsage(): Observablenumber {return of(1.2);
}export function getMemoryUsagePercent(): Observablenumber {return forkJoin([getMemoryUsage(), getMemoryFree()]).pipe(map(([usage, free]) ((usage / (usage free)) * 100).toFixed(2)));
}
在防腐层对数据进行 mock 也可以用于对页面的测试例如 mock 大量数据对页面性能影响。
export function getLargeList(): Observablestring[] {const options [];for (let i 0; i 100000; i) {const value ${i.toString(36)}${i};options.push(value);}return of(options);
}
五 总结
在本文中我们介绍了以下内容
前端面对接口频繁变动时的困境及原因如何防腐层的设计思想与技术选型使用 Observable 实现防腐层的代码示例防腐层的额外作用
请读者注意只在特定的场景下引入前端防腐层才是合理的即前端处于跟随者或供应商/客户关系中且面临大量接口无法保障稳定和兼容。如果在防腐层可以在后端 Gateway 构建或者接口数量较少时引入防腐层带来的额外成本会大于其带来的好处。
RxJS 在防腐层构建场景下提供的更多的是 Observable 化的能力如果读者不需要复杂的 operators 转换工具也可以自行构建 Observable 构建方案事实上只需要 100 行的代码就可以实现 mini-rxjs - StackBlitz。
改造后的前端架构将不再直接依赖接口实现不会入侵现有前端数据层设计还可以承担概念映射、格式适配、接口缓存、稳定性兜底以及协助联调测试等工作。文中所有的示例代码都可以在仓库 GitHub - vthinkxie/rxjs-acl: Anti Corruption Layer with RxJS 获得。
原文链接
本文为阿里云原创内容未经允许不得转载。