Observable 防腐層項目實戰
在 基於 Observable 構建前端防腐策略 發佈後,有一些讀者留言對文章中使用 Observable 構建防腐層的典型應用感到困惑,覺得代碼中的例子過於簡單,不少可以通過 Promise 解決,引入 Observable 會提高而非降低複雜度。
這種顧慮是完全有道理的,在 RxJS 中可以由 Promise 操作符來替代的場景還有很多,事實上,所有能由 Observable 實現的場景理論上都可以 Promise 來實現,畢竟 RxJS 是基於 JavaScript 構建,整個 Observable 的核心實現也不過只有 100 行代碼。
然而上文的例子只是用來說明防腐層的場景,而並非複雜到一定要使用防腐層的情況。既然我們需要構建防腐層,實際業務中的場景不可能只有 2 個接口,3 個組件這樣簡單。
在複雜的業務場景下,基於 Observable 構建的防腐層可以提升我們的代碼開發效率,更好的抽象和封裝底層接口。以下舉幾個項目中防腐層的實戰場景 ,每個場景均附加了在線示例。
1. 接口穩定提升
在線示例: https://stackblitz.com/edit/rxjs-stable-improvement
操作符: retry / retryWhen / delay
有些時候後端接口的成功率較低,但是前端爲了保證視圖層穩定,需要對這些接口的成功率進行增強。這裏,我們使用 Promise 模擬一個成功率只有 50% 的接口,代碼如下:
// 成功率 50% 的接口
function unstableAPI(): Promise<boolean> {
return new Promise((resolve, reject) => {
if (Math.random() < 0.5) {
resolve(true);
} else {
reject('error');
}
});
}
通過 RxJS 的 retry
操作符,我們可以很容易將 50% 成功率的接口組裝爲成功率 99.9% 的接口,即每次當接口失敗時,自動重試最多 10 次。
function stabilizedAPI(): Promise<boolean> {
return lastValueFrom(
from(defer(() => unstableAPI())).pipe(retry(10))
);
}
實際的業務中,以上代碼會導致代碼短時間內多次重試,可能會導致接口雪崩。在 RxJS 中我們可以輕鬆實現錯誤回退機制,以花費更多時間的代價來獲得更大的成功幾率。
我們將 stabilizedAPI
的代碼修改爲以下代碼,當發生錯誤時,等待 1s 後重新發起請求,最多發送 10 次。
function stabilizedAPI(): Promise<boolean> {
return lastValueFrom(
from(defer(() => unstableAPI())).pipe(
retryWhen((errors) => errors.pipe(delay(1000), take(10)))
)
);
}
2. 接口時序調整
在線示例: https://stackblitz.com/edit/rxjs-minimal-response-time
操作符: forkJoin / delay
絕大部分的前端應用都會有啓動屏幕,啓動屏幕中可能包含廣告、加載動畫或者應用 logo 信息等內容,應用啓動屏幕的展示時間通常由以下兩個因素決定:
-
網絡加載耗時 networkDelay: 應用加載所需的關鍵數據接口,例如用戶個人信息的最長返回時間
-
頁面最小展示時間 minimalDelay: 啓動屏幕包含的有效信息需要有一個最短展示時間,防止屏幕閃爍
啓動屏幕的展示時間應當由以下邏輯計算:當網絡加載耗時小於頁面最小展示時間時,將以頁面最小展示時間爲準,當大於頁面最小展示時間時,將網絡加載耗時爲準。簡化公式爲:
啓動屏幕展示時間 = Max(關鍵接口加載時間,最短加載時間)
我們使用 Promise 來模擬關鍵數據返回,其中網絡接口延時由 setTimeout 來模擬
function initData(): Promise<{ name: string }> {
return new Promise((resolve) => {
const networkDelay = Math.random() * 3000;
setTimeout(() => {
resolve({ name: 'lucy' });
}, networkDelay);
});
}
通過 forkJoin
delay
等 operator,我們可以組裝出給啓動屏幕使用的最短返回時間接口
// 初始化數據,返回時間必定大於 minimalDelay ms
function initDataWithMinimalDelay(minimalDelay: number): Promise<{ name: string }> {
return lastValueFrom(
forkJoin([
from(defer(() => initData())),
of(true).pipe(delay(minimalDelay)),
]).pipe(map(([data]) => data))
);
}
在以上代碼中,當 networkDelay 調用時間小於 minimalDelay 時,將以 minimalDelay 爲準,當大於 minimalDelay 時,將以 networkDelay 爲準。
3. 接口擇優使用
在線示例: https://stackblitz.com/edit/rxjs-race-query
操作符: raceWith
有時相同的數據可以從後端多個接口中獲取,我們使用 Promise 模擬快慢兩個接口
// 快速接口
function fastAPI(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve('fast data');
}, 1000);
});
}
// 慢速接口
function slowAPI(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve('slow data');
}, 3000);
});
}
在實際的使用中,我們無法提前知曉接口的網絡情況,通過 raceWith
操作符,我們可以對任意個接口進行封裝,自動獲取其中最快的那個
function getFasterOne(): Promise<string> {
return lastValueFrom(
from(defer(() => fastAPI())).pipe(raceWith(from(defer(() => slowAPI()))))
);
}
4. 接口競態處理
在線示例:https://stackblitz.com/edit/rxjs-race-condition
操作符:exhaustMap / switchMap / concatMap
接口的請求結果返回的順序不能保證一致,這就要求我們在業務中需要對接口的競態問題進行處理。Dan Abramov 在 useEffect 完整指南使用了布爾值來對數據進行處理。但是如果你使用 Observable 構建了防腐層,就會有更簡單的方法來處理競態問題。
我們使用 randomuser.me
的服務與 fromFetch
operator 構建一個簡單的數據層
function getData() {
return fromFetch('https://api.randomuser.me/?page=1&results=10').pipe(
map((data) => data.json())
);
}
4.1 以第一次請求爲準
由於防腐層 Observable 的特性,使用 Observable 與 exhaustMap
結合就可以獲得與 flag 標註相同的效果,即當前一次請求未返回時,下一次請求會被直接拋棄。
fromEvent(document.getElementById('button'), 'click')
.pipe(exhaustMap(() => getData()));
4.2 以最後一次請求爲準
我們也可以選擇以最後一次請求爲基準,將之前所有的請求都拋棄,在組件內直接使用 switchMap
operator 來保證請求順序與返回數據一致,fromFetch
中內置了 AbortController
可以將過期但仍未返回的接口置爲 canceled
狀態。
fromEvent(document.getElementById('button'), 'click')
.pipe(switchMap(() => getData()));
4.3 所有請求排隊處理
將所有發出的請求排隊處理,不丟棄任何一次請求,當上一次請求未返回時,下一次請求進入隊列排隊。
fromEvent(document.getElementById('button'), 'click')
.pipe(concatMap(() => getData()));
5. 高階數據組裝
在線示例:https://stackblitz.com/edit/rxjs-high-order-query
操作符:mergeMap / map / forkJoin
有些時候需要二次請求才能獲得視圖層的數據,例如下圖中的數據可能由 getList 與 getStatus 兩個接口才能完整獲取。當我們需要同步渲染這些數據時,在防腐層中抽象出 getListWithStatus 會是更好的選擇。
我們使用 Promise 模擬出兩個接口的內容
// 模擬獲取列表數據的接口
function getList(): Promise<
Array<{
name: string;
id: string;
}>
> {
return new Promise((resolve) => {
resolve([
{
name: 'John Brown',
id: '1',
},
{
name: 'Jim Green',
id: '2',
}
]);
});
}
// 模擬獲取狀態接口
function getStatus(id: string) {
return new Promise((resolve) => {
if (id === '2') {
resolve('old');
} else {
resolve('young');
}
});
}
通過 mergeMap
與 forkJoin
,我們可以將高階的請求直接打平爲一階數組,獲得含有 status 列表數據的接口抽象
// 抽象後含有 status 的列表數據
function getListWithStatus() {
const getList$ = from(defer(() => getList()));
const getStatus$ = (id: string) => from(defer(() => from(getStatus(id))));
const data$ = getList$.pipe(
mergeMap((list) => {
const queryList = list.map((item) =>
getStatus$(item.id).pipe(map((status) => ({ ...item, status })))
);
return forkJoin(queryList);
})
);
return lastValueFrom(data$);
}
調用 getListWithStatus 返回的數據爲
[
{
"name": "John Brown",
"id": "1",
"status": "young"
},
{
"name": "Jim Green",
"id": "2",
"status": "old"
}
]
總結
本文給出了 Observable 防腐層在實際項目中的一些相對複雜的例子,通過 Observable 防腐層的引入可以使用較少代碼來實現上述複雜功能。
複雜業務的有效設計對於簡單場景來說很可能是過度設計。不建議在看不到應用場景的時候強行引入 Observable,工程領域實踐中沒有銀彈,感謝大家的閱讀。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/g5PANaoZmm3fkxCiIZ-ZxA