
一 困境與難題




-
返回欄位調整 -
呼叫方式改變
-
多版本共存使用



二 防腐層設計




-
統一不同資料來源的能力:RxJS 可以將 websocket、http 請求、甚至使用者操作、頁面點選等轉換為統一的 Observable 物件。
-
統一不同型別資料的能力:RxJS 將非同步資料和同步資料統一為 Observable 物件。
-
豐富的資料加工能力:RxJS 提供了豐富的 Operator 運算子,可以對 Observable 在訂閱前進行預先加工。
-
不入侵前端架構:RxJS 的 Observable 可以與 Promise 互相轉換,這意味著 RxJS 的所有概念可以被完整封裝在資料層,對檢視層可以只暴露 Promise。

三 防腐層實現

exportfunctiongetMemoryFreeObservable(): Observable<number> {
return fromFetch("/api/v1/memory/free").pipe(mergeMap((res) => res.json()));
}
exportfunctiongetMemoryUsageObservable(): Observable<number> {
return fromFetch("/api/v1/memory/usage").pipe(mergeMap((res) => res.json()));
}
exportfunctiongetMemoryUsagePercent(): Promise<number> {
return lastValueFrom(forkJoin([getMemoryFreeObservable(), getMemoryUsageObservable()]).pipe(
map(([usage, free]) => +((usage / (usage + free)) * 100).toFixed(2))
));
}
exportfunctiongetMemoryFree(): Promise<number> {
return lastValueFrom(getMemoryFreeObservable());
}
exportfunctiongetMemoryUsage(): Promise<number> {
return lastValueFrom(getMemoryUsageObservable());
}
functionMemoryUsagePercent() {
const [usage, setUsage] = useState<number>(0);
useEffect(() => {
(async () => {
const result = await getMemoryUsagePercent();
setUsage(result);
})();
}, []);
return <div>Usage: {usage} %</div>;
}
exportdefaultMemoryUsagePercent;
1 返回欄位調整
{
requestId: string;
data: number;
}
exportfunctiongetMemoryUsageObservable(): Observable<number> {
return fromFetch("/api/v2/memory/free").pipe(
mergeMap((res) => res.json()),
+ map((data) => data.data)
);
}
exportfunctiongetMemoryUsageObservable(): Observable<number> {
return fromFetch("/api/v2/memory/usage").pipe(
mergeMap((res) => res.json()),
+ map((data) => data.data)
);
}

2 呼叫方式改變
當呼叫方式發生改變時,防腐層同樣可以發揮作用。/api/v3/memory 直接返回了 free 與 usage 的資料,介面格式如下。
{
requestId: string;
data: {
free: number;
usage: number;
}
}
防腐層程式碼只需要進行如下更新,就可以保障元件層程式碼無需修改。
exportfunctiongetMemoryObservable(): Observable<{ free: number; usage: number }> {
return fromFetch("/api/v3/memory").pipe(
mergeMap((res) => res.json()),
map((data) => data.data)
);
}
exportfunctiongetMemoryFreeObservable(): Observable<number> {
return getMemoryObservable().pipe(map((data) => data.free));
}
exportfunctiongetMemoryUsageObservable(): Observable<number> {
return getMemoryObservable().pipe(map((data) => data.usage));
}
exportfunctiongetMemoryUsagePercent(): Promise<number> {
return lastValue(getMemoryObservable().pipe(
map(({ usage, free }) => +((usage / (usage + free)) * 100).toFixed(2))
));
}
3 多版本共存使用
當前端程式碼需要在多套環境下部署時,部分環境下 v3 的介面可用,而部分環境下只有 v2 的介面部署,此時我們依然可以在防腐層遮蔽環境的差異。

exportfunctiongetMemoryLegacyObservable(): 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,
}));
}
exportfunctiongetMemoryObservable(): Observable<{ free: number; usage: number }> {
const current = fromFetch("/api/v3/memory").pipe(
mergeMap((res) => res.json()),
map((data) => data.data)
);
return race(getMemoryLegacyObservable(), current);
}
exportfunctiongetMemoryFreeObservable(): Observable<number> {
return getMemoryObservable().pipe(map((data) => data.free));
}
exportfunctiongetMemoryUsageObservable(): Observable<number> {
return getMemoryObservable().pipe(map((data) => data.usage));
}
exportfunctiongetMemoryUsagePercent(): Promise<number> {
return lastValue(getMemory().pipe(
map(({ usage, free }) => +((usage / (usage + free)) * 100).toFixed(2))
));
}
透過 race 運算子,當 v2 與 v3 任何一個版本的介面可用時,防腐層都可以正常工作,在元件層無需再關注介面受環境的影響。
四 額外應用
防腐層不僅僅是多了一層對介面的封裝與隔離,它還能起到以下作用。
1 概念對映
介面語義與前端需要資料的語義有時並不能完全對應,當在元件層直接呼叫介面時,所有開發者都需要對介面與介面的語義對映足夠了解。有了防腐層後,防腐層提供的呼叫方法包含了資料的真實語義,減少了開發者的二次理解成本。
2 格式適配
在很多情況下,介面返回的資料結構與格式與前端需要的資料格式並不符合,透過在防腐層增加資料轉換邏輯,可以降低介面資料對業務程式碼的入侵。在以上的案例裡,我們封裝了 getMemoryUsagePercent 的資料返回,使得元件層可以直接使用百分比資料,而不需要再次進行轉換。
3 介面快取
對於多種業務依賴同一介面的情況,我們可以透過防腐層增加快取邏輯,從而有效降低介面的呼叫壓力。
與格式適配類似,將快取邏輯封裝在防腐層可以避免元件層對資料的二次快取,並可以對快取資料集中管理,降低程式碼的複雜度,一個簡單的快取示例如下。
classCacheService{
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 是更方便的方案。
exportfunctiongetMemoryFree(): Observable<number> {
returnof(0.8);
}
exportfunctiongetMemoryUsage(): Observable<number> {
returnof(1.2);
}
exportfunctiongetMemoryUsagePercent(): Observable<number> {
return forkJoin([getMemoryUsage(), getMemoryFree()]).pipe(
map(([usage, free]) => +((usage / (usage + free)) * 100).toFixed(2))
);
}
在防腐層對資料進行 mock 也可以用於對頁面的測試,例如 mock 大量資料對頁面效能影響。
exportfunctiongetLargeList(): Observable<string[]> {
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 行的程式碼就可以實現 https://stackblitz.com/edit/mini-rxjs。
改造後的前端架構將不再直接依賴介面實現,不會入侵現有前端資料層設計,還可以承擔概念對映、格式適配、介面快取、穩定性兜底以及協助聯調測試等工作。文中所有的示例程式碼都可以在倉庫 https://github.com/vthinkxie/rxjs-acl 獲得。
求職特訓營火熱來襲!阿里專家教你製作專業簡歷!
如何在眾多簡歷中吸引HR關注?如何描述過往經歷突出亮點?如何增加簡歷加分項?阿里專家從面試官角度告訴你一份高質量簡歷應該長什麼樣!
阿里雲開發者社群舉辦“阿里專家五堂課教你製作專業簡歷”訓練營,邀請四位阿里專家傾情傳授簡歷秘籍,深挖簡歷承載的價值,從面試官的角度切入講解簡歷必含模組,更有資深專家直播線上答疑幫你修改簡歷!金三銀四黃金求職季,阿里專家助力你的求職之路!還在等什麼?立即點選閱讀原文免費報名參加!