新聞中心
在這種情況下,構(gòu)建更穩(wěn)健的前端應用,保證前端在長期迭代下的穩(wěn)健與可拓展性就變得非常重要。本文將重點介紹如何利用接口防腐策略避免或減少接口變更對前端的影響。

成都創(chuàng)新互聯(lián)公司是一家專業(yè)提供如東企業(yè)網(wǎng)站建設(shè),專注與成都網(wǎng)站建設(shè)、成都做網(wǎng)站、HTML5、小程序制作等業(yè)務(wù)。10年已為如東眾多企業(yè)、政府機構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)網(wǎng)站建設(shè)公司優(yōu)惠進行中。
一、困境與難題
為了更清晰解釋前端面臨的難題,我們以 ToB 業(yè)務(wù)中常見的儀表盤頁面為例,該頁面包含了可用內(nèi)存、已使用內(nèi)存和已使用的內(nèi)存占比三部分信息展示。
此時前端組件與接口之間的依賴關(guān)系如下圖所示。
當接口返回結(jié)構(gòu)調(diào)整時MemoryFree 組件對接口的調(diào)用方式需要調(diào)整。同樣的,MemoryUsage 與 MemoryUsagePercent 也要進行修改才能工作。
真實的 ToB 業(yè)務(wù)面臨的接口可能會有數(shù)百個,組件與接口的集成邏輯也遠比以上的例子要復雜。
經(jīng)過數(shù)年甚至更長時間的迭代后,接口會逐步產(chǎn)生多個版本,出于對界面穩(wěn)定性及用戶使用習慣的考量,前端往往會同時依賴接口的多個版本來構(gòu)建界面。當部分接口需要調(diào)整下線或發(fā)生變更時,前端需要重新理解業(yè)務(wù)邏輯,并做出大量代碼邏輯調(diào)整才能保證界面穩(wěn)定運行。
常見的對前端造成影響的接口變更包括但不限于:
- 返回字段調(diào)整
- 調(diào)用方式改變
- 多版本共存使用
當前端面對的是平臺型業(yè)務(wù)時,此類問題會變得更為棘手。平臺型產(chǎn)品會對一種或多種底層引擎進行封裝,例如機器學習平臺可能會基于 TensorFlow、Pytorch 等機器學習引擎搭建,實時計算平臺可能基于 Flink、Spark 等計算引擎搭建。
雖然平臺會對引擎的大部分接口進行上層封裝,但不可避免的仍然會有部分底層接口會直接被透傳到前端,在這個時候,前端不僅要應對平臺的接口變更,還會面臨著開源引擎接口的變更帶來的挑戰(zhàn)。
前端在面臨的困境是由獨特的前后端關(guān)系決定的。與其他領(lǐng)域不同,在 ToB 業(yè)務(wù)中,前端通常以下游客戶的身份接受后端供應商的供給,有些情況下會成為后端的跟隨者。
在客戶/供應商關(guān)系中,前端處于下游,而后端團隊處于上游,接口內(nèi)容與上線時間通常由后端團隊來決定。
在跟隨者關(guān)系中,上游的后端團隊不會去根據(jù)前端團隊的需求進行任何調(diào)整,前端只能去順應上游后端的模型。這種情況通常發(fā)生在前端無法對上游后端團隊施加影響的時刻,例如前端需要基于開源項目的接口設(shè)計界面,或者是后端團隊的模型已經(jīng)非常成熟且難以修改時。
《架構(gòu)整潔之道》的作者描述過這樣一個嵌入式架構(gòu)設(shè)計的難題,與上文我們描述的困境十分類似。
軟件應當是一種使用周期很長的東西,而固件會隨著硬件的演進而淘汰過時,但事實上的情況是,雖然軟件本身不會隨著時間推移而磨損,但硬件及其固件卻會隨時間推移而過時,隨即也需要對軟件做相應的改動。
無論是客戶/供應商關(guān)系,還是跟隨者關(guān)系,正如軟件無法決定硬件的發(fā)展與迭代一樣,前端也很難或者無法決定引擎與接口的設(shè)計,雖然前端本身不會隨著時間的推移而變得不可用,但技術(shù)引擎及相關(guān)接口卻會隨著時間推移而過時,前端代碼會跟隨技術(shù)引擎的迭代更換逐步腐爛,最終難逃被迫重寫的命運。
二、防腐層設(shè)計
早在 Windows 誕生之前,工程師為了解決上文中硬件、固件與軟件的可維護性問題,引入了 HAL(Hardware Abstraction Layer)的概念, HAL 為軟件提供服務(wù)并且屏蔽了硬件的實現(xiàn)細節(jié),使得軟件不必由于硬件或者固件的變更而頻繁修改。
HAL 的設(shè)計思想在領(lǐng)域驅(qū)動設(shè)計(DDD) 中又被稱為防腐層(Anticorruption Layer)。在 DDD 定義的多種上下文映射關(guān)系中,防腐層是最具有防御性的一種。它經(jīng)常被使用在下游團隊需要阻止外部技術(shù)偏好或者領(lǐng)域模型入侵的情況,可以幫助很好地隔離上游模型與下游模型。
我們可以在前端中引入防腐層的概念,降低或避免當前后端的上下文映射接口變更對前端代碼造成的影響。
在行業(yè)內(nèi)有很多種方式可以實現(xiàn)防腐層,無論是近幾年大火的 GraphQL 還是 BFF 都可以作為備選方案,但是技術(shù)選型同樣受限于業(yè)務(wù)場景。與 ToC 業(yè)務(wù)完全不同,在 ToB 業(yè)務(wù)中,前后端的關(guān)系通常為客戶/供應商或者跟隨者/被跟隨者的關(guān)系。在這種關(guān)系下,寄希望于后端配合前端對接口進行 GraphQL 改造已經(jīng)變得不太現(xiàn)實,而 BFF 的構(gòu)建一般需要額外的部署資源及運維成本。
在上述情況下,在瀏覽器端構(gòu)建防腐層是更為可行的方案,但是在瀏覽器中構(gòu)建防腐層同樣面臨挑戰(zhàn)。
無論是 React、Angular 還是 Vue 均有無數(shù)的數(shù)據(jù)層解決方案,從 Mobx、Redux、Vuex 等等,這些數(shù)據(jù)層方案對視圖層實際上都會有入侵,有沒有一種防腐層解決方案可以與視圖層徹底解耦呢?以 RxJS 為代表的 Observable 方案在這時可能是最好的選擇。
RxJS 是 ReactiveX 項目的 JavaScript 實現(xiàn),而 ReactiveX 最早是 LINQ 的一個擴展,由微軟的架構(gòu)師 Erik Meijer 領(lǐng)導的團隊開發(fā)。該項目目標是提供一致的編程接口,幫助開發(fā)者更方便的處理異步數(shù)據(jù)流。目前 RxJS 在開發(fā)中經(jīng)常被作為響應式編程開發(fā)工具使用,但是在構(gòu)建防腐層的場景中,RxJS 代表的 Observable 方案同樣可以發(fā)揮巨大作用。
我們選擇 RxJS 主要基于以下幾點考慮:
統(tǒng)一不同數(shù)據(jù)源的能力:RxJS 可以將 websocket、http 請求、甚至用戶操作、頁面點擊等轉(zhuǎn)換為統(tǒng)一的 Observable 對象。
統(tǒng)一不同類型數(shù)據(jù)的能力:RxJS 將異步數(shù)據(jù)和同步數(shù)據(jù)統(tǒng)一為 Observable 對象。
豐富的數(shù)據(jù)加工能力:RxJS 提供了豐富的 Operator 操作符,可以對 Observable 在訂閱前進行預先加工。
不入侵前端架構(gòu):RxJS 的 Observable 可以與 Promise 互相轉(zhuǎn)換,這意味著 RxJS 的所有概念可以被完整封裝在數(shù)據(jù)層,對視圖層可以只暴露 Promise。
當在引入 RxJS 將所有類型的接口轉(zhuǎn)換為 Observable 對象后,前端的視圖組件將僅依賴 Observable,并與接口實現(xiàn)的細節(jié)解耦,同時,Observable 可以與 Promise 相互轉(zhuǎn)換,在視圖層獲得的是單純的 Promise,可以與任意數(shù)據(jù)層方案和框架搭配使用。
除了轉(zhuǎn)換為 Promise 之外,開發(fā)者也可以與 RxJS 在渲染層的解決方案,例如 rxjs-hooks 混用,獲得更好的開發(fā)體驗。
三、防腐層實現(xiàn)
參照上文的防腐層設(shè)計,我們在開頭的儀表盤項目中實現(xiàn)以 RxJS Observable 為核心的防腐層代碼。
其中防腐層的核心代碼如下:
export function getMemoryFreeObservable(): Observable{
return fromFetch("/api/v1/memory/free").pipe(mergeMap((res) => res.json()));
}
export function getMemoryUsageObservable(): Observable{
return fromFetch("/api/v1/memory/usage").pipe(mergeMap((res) => res.json()));
}
export function getMemoryUsagePercent(): Promise{
return lastValueFrom(forkJoin([getMemoryFreeObservable(), getMemoryUsageObservable()]).pipe(
map(([usage, free]) => +((usage / (usage + free)) * 100).toFixed(2))
));
}
export function getMemoryFree(): Promise{
return lastValueFrom(getMemoryFreeObservable());
}
export function getMemoryUsage(): Promise{
return lastValueFrom(getMemoryUsageObservable());
}
MemoryUsagePercent 的實現(xiàn)代碼如下,此時該組件將不再依賴具體的接口,而直接依賴防腐層的實現(xiàn)。
function MemoryUsagePercent() {
const [usage, setUsage] = useState(0);
useEffect(() => {
(async () => {
const result = await getMemoryUsagePercent();
setUsage(result);
})();
}, []);
return Usage: {usage} %;
}
export default MemoryUsagePercent;
1.返回字段調(diào)整
返回字段變更時,防腐層可以有效攔截接口對組件的影響,當 /api/v2/quota/free 與 /api/v2/quota/usage 的返回數(shù)據(jù)變更為以下結(jié)構(gòu)時。
{
requestId: string;
data: number;
}
我們只需要調(diào)整防腐層的兩行代碼,注意此時我們的上層封裝的getMemoryUsagePercent 基于 Observable 構(gòu)建所以不需要進行任何改動。
export function getMemoryUsageObservable(): Observable{
return fromFetch("/api/v2/memory/free").pipe(
mergeMap((res) => res.json()),
+ map((data) => data.data)
);
}
export function getMemoryUsageObservable(): Observable{
return fromFetch("/api/v2/memory/usage").pipe(
mergeMap((res) => res.json()),
+ map((data) => data.data)
);
}
在 Observable 化的防腐層中,會存在高階 Observable 與 低階 Observable 兩種設(shè)計,在上文的例子中,F(xiàn)ree Observable 和 Usage Observable 為低階封裝,而 Percent Observable 利用 Free 和 Usage 的 Observable 進行了高階封裝,當?shù)碗A封裝改動時,由于 Observable 本身的特性,高階封裝經(jīng)常是不需要進行任何改動的,這也是防腐層給我們帶來的額外好處。
2.調(diào)用方式改變
當調(diào)用方式發(fā)生改變時,防腐層同樣可以發(fā)揮作用。/api/v3/memory 直接返回了 free 與 usage 的數(shù)據(jù),接口格式如下。
{
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(): Observable {
return getMemoryObservable().pipe(map((data) => data.free));
}
export function getMemoryUsageObservable(): Observable {
return getMemoryObservable().pipe(map((data) => data.usage));
}
export function getMemoryUsagePercent(): Promise {
return lastValue(getMemoryObservable().pipe(
map(({ usage, free }) => +((usage / (usage + free)) * 100).toFixed(2))
));
}
3.多版本共存使用
當前端代碼需要在多套環(huán)境下部署時,部分環(huán)境下 v3 的接口可用,而部分環(huán)境下只有 v2 的接口部署,此時我們依然可以在防腐層屏蔽環(huán)境的差異。
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(): Observable {
return getMemoryObservable().pipe(map((data) => data.free));
}
export function getMemoryUsageObservable(): Observable {
return getMemoryObservable().pipe(map((data) => data.usage));
}
export function getMemoryUsagePercent(): Promise {
return lastValue(getMemory().pipe(
map(({ usage, free }) => +((usage / (usage + free)) * 100).toFixed(2))
));
}
通過 race 操作符,當 v2 與 v3 任何一個版本的接口可用時,防腐層都可以正常工作,在組件層無需再關(guān)注接口受環(huán)境的影響。
四、額外應用
防腐層不僅僅是多了一層對接口的封裝與隔離,它還能起到以下作用。
1.概念映射
接口語義與前端需要數(shù)據(jù)的語義有時并不能完全對應,當在組件層直接調(diào)用接口時,所有開發(fā)者都需要對接口與界面的語義映射足夠了解。有了防腐層后,防腐層提供的調(diào)用方法包含了數(shù)據(jù)的真實語義,減少了開發(fā)者的二次理解成本。
2.格式適配
在很多情況下,接口返回的數(shù)據(jù)結(jié)構(gòu)與格式與前端需要的數(shù)據(jù)格式并不符合,通過在防腐層增加數(shù)據(jù)轉(zhuǎn)換邏輯,可以降低接口數(shù)據(jù)對業(yè)務(wù)代碼的入侵。在以上的案例里,我們封裝了 getMemoryUsagePercent 的數(shù)據(jù)返回,使得組件層可以直接使用百分比數(shù)據(jù),而不需要再次進行轉(zhuǎn)換。
3.接口緩存
對于多種業(yè)務(wù)依賴同一接口的情況,我們可以通過防腐層增加緩存邏輯,從而有效降低接口的調(diào)用壓力。
與格式適配類似,將緩存邏輯封裝在防腐層可以避免組件層對數(shù)據(jù)的二次緩存,并可以對緩存數(shù)據(jù)集中管理,降低代碼的復雜度,一個簡單的緩存示例如下。
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.穩(wěn)定性兜底
當接口穩(wěn)定性較差時,通常的做法是在組件層對 response error 的情況進行處理,這種兜底邏輯通常比較復雜,組件層的維護成本會很高。我們可以通過防腐層對穩(wěn)定性進行兜底,當接口出錯時可以返回兜底業(yè)務(wù)數(shù)據(jù),由于兜底數(shù)據(jù)統(tǒng)一維護在防腐層,后續(xù)的測試與修改也會更加方便。在上文中的多版本共存的防腐層中,增加以下代碼,此時即使 v2 和 v3 接口都無法返回數(shù)據(jù),前端仍然可以保持可用。
return race(getMemoryLegacy(), current).pipe(
+ catchError(() => of({ usage: '-', free: '-' }))
);
5.聯(lián)調(diào)與測試
接口和前端可能會存在并行開發(fā)的狀態(tài),此時,前端的開發(fā)并沒有真實的后端接口可用。與傳統(tǒng)的搭建 mock api 的方式相比,在防腐層直接對數(shù)據(jù)進行 mock 是更方便的方案。
export function getMemoryFree(): Observable{
return of(0.8);
}
export function getMemoryUsage(): Observable{
return of(1.2);
}
export function getMemoryUsagePercent(): Observable{
return forkJoin([getMemoryUsage(), getMemoryFree()]).pipe(
map(([usage, free]) => +((usage / (usage + free)) * 100).toFixed(2))
);
}
在防腐層對數(shù)據(jù)進行 mock 也可以用于對頁面的測試,例如 mock 大量數(shù)據(jù)對頁面性能影響。
export function getLargeList(): Observable{
const options = [];
for (let i = 0; i < 100000; i++) {
const value = `${i.toString(36)}${i}`;
options.push(value);
}
return of(options);
}
五、總結(jié)
在本文中我們介紹了以下內(nèi)容:
- 前端面對接口頻繁變動時的困境及原因如何
- 防腐層的設(shè)計思想與技術(shù)選型
- 使用 Observable 實現(xiàn)防腐層的代碼示例
- 防腐層的額外作用
請讀者注意,只在特定的場景下引入前端防腐層才是合理的,即前端處于跟隨者或供應商/客戶關(guān)系中,且面臨大量接口無法保障穩(wěn)定和兼容。如果在防腐層可以在后端 Gateway 構(gòu)建,或者接口數(shù)量較少時,引入防腐層帶來的額外成本會大于其帶來的好處。
RxJS 在防腐層構(gòu)建場景下提供的更多的是 Observable 化的能力,如果讀者不需要復雜的 operators 轉(zhuǎn)換工具,也可以自行構(gòu)建 Observable 構(gòu)建方案,事實上只需要 100 行的代碼就可以實現(xiàn) https://stackblitz.com/edit/mini-rxjs。
改造后的前端架構(gòu)將不再直接依賴接口實現(xiàn),不會入侵現(xiàn)有前端數(shù)據(jù)層設(shè)計,還可以承擔概念映射、格式適配、接口緩存、穩(wěn)定性兜底以及協(xié)助聯(lián)調(diào)測試等工作。文中所有的示例代碼都可以在倉庫 https://github.com/vthinkxie/rxjs-acl 獲得。
分享名稱:基于 Observable 構(gòu)建前端防腐策略
文章出自:http://www.5511xx.com/article/dhjjddc.html


咨詢
建站咨詢
