這篇我們要來看 Effect 中如何做到 batch request ,但為什麼我們會需要 batch request 呢?我之前正好有寫過類似的文章,有興趣的話可以去看看,這邊簡單的說一下,在平常寫程式中,我們可能會寫出像這樣的程式碼
function fetchData(id: number): Effect.Effect<unknown> {
// 取得 data 的邏輯
}
function processData(data: unknown) {
// 處理資料
}
const listOfDataID = [1, 3, 4, 7];
pipe(
listOfDataID.map((id) =>
pipe(
fetchData(id),
Effect.map((data) => processData(data))
)
),
Effect.allWith({ concurrency: "unbounded" })
);
像這樣,取得某個列表中的每個元素的詳細資料,但有時候我們都忘了, fetchData
需要呼叫後端的 API 耶,如果資料一多是不是會因此送出太多的 request 而造成後端的負擔呢?這時我們可能會開始善用之前學到的限制一下 concurrency
數,但若後端可以配合的話,我們是不是可以只用一個 request 就查到我們需要的資料並請後端回傳呢?
function batchFetchDatum(id: number[]) {
// 這邊使用某種特別的 API ,可以傳入一組 id ,並且一次回傳所有資料
}
pipe(
batchFetchDatum(listOfDataID),
// 但這邊我們也要跟著配合調整我們是如何處理資料的
Effect.map((datum) => datum.map((data) => processData(data)))
);
但有時候,我們的資料處理可能不是那邊容易就可以改寫的,例如裡面可能還牽扯到了其它的非同步的操作等等,如果有什麼神奇的方法讓我們可以像是一個一個各別取得,但又同時可以在背後自動轉換成 batch request 的 API 該有多好,這時候就是 Effect 的 Request
與 RequestResolver
登場的時候了
Request
與 RequestResolver
在 Effect 中的 Request
是代表你需要跟某個外部的資源互動,並且這個互動會交由 RequestResolver
來完成
這跟自己取得資料不同的地方在於,你將這個請求交給的 Effect 的 runtime 執行,這使得 Effect 的 runtime 有機會在平行處理你的請求時,幫你調度具有 batch request 能力的 resolver 來處理 request ,這樣說可能有點抽像,我們實際看個例子吧
在開始前,我們要先來定義我們的 request
// 假設我們的資料長成這樣
interface Data {
id: number
content: string
}
// 這是我們的 request ,三個 type 參數分別代表:成功回傳的資料,失敗時的 error ,輸入的參數
class GetDataRequest extends Request.TaggedClass('data')<Data, never, {id: number}> {}
像這樣,我們就定義好了我們的請求,接下來就是要來定義 resolver
這邊我們先來定義一個一般的 resolver ,並且等下我們可以來實驗看看一般的 resolver 跟 batch resolver 的差別
const GetDataResolver = RequestResolver.fromEffect((request: GetDataRequest) => {
console.log("GetDataResolver", request)
// 這邊可以回傳 Effect ,假裝這就是我們取的資料的邏輯
return Effect.succeed({
id: request.id,
content: `Data ${request.id}`
})
})
像這樣我們就定義好了一個 resolver ,再來我們來定義一個 batch 的 resolver
定義一個 batch resolver 會比較複雜,因為它接收的是 Effect runtime 收集到的多個請求(以 Array.NonEmptyArray
形式,保證至少有一個元素,這是 makeBatched
的特性)。你需要自己處理這些批次請求,並將結果正確地對應回每個原始的 request
// 這邊的參數是 Effect 幫你收集來的 request
// Array.NonEmptyArray 是 Effect 中定義的,保證至少有一個元素的 array
const BatchGetDataResolver = RequestResolver.makeBatched((requests: Array.NonEmptyArray<GetDataRequest>) => {
console.log('BatchGetDataResolver', requests)
return Effect.forEach(requests, (request) =>
// 透過這個 function 可以完成對應的 request
Request.completeEffect(
request,
Effect.succeed({
id: request.id,
content: `Data ${request.id}`
})
))
})
看完上面的定義,你可能會想,奇怪了, RequestResolver 除了 type 以外,又沒有傳入 Request 的任何東西, Effect 是要怎麼知道哪個 request 對應的是哪個 resolver ,畢竟我們都知道 TypeScript 的 type 到了執行時就不存在了。答案是:不知道,因為你在使用時需要兩個一起給 Effect
function getData(id: number) {
// 在將 request 交給 Effect 的同時,你需要將 resolver 也一起傳過去
return Effect.request(new GetDataRequest({ id }), GetDataResolver)
}
function getDataBatch(id: number) {
return Effect.request(new GetDataRequest({ id }), BatchGetDataResolver)
}
通常會定義像這樣的 helper function,將 Request
和 RequestResolver
封裝起來,以簡化 Effect.request
的呼叫並提高可讀性
接下來我們就來實際的使用看看吧
const listOfDataID = [1, 1, 2, 3, 5, 8, 11, 13]
pipe(
listOfDataID,
// 這邊可以試看看換成 getData
Array.map((id) => getDataBatch(id)),
// 這邊可以試看看關掉 batching
Effect.allWith({ batching: true }),
Effect.tap((result) => {
console.log(result)
}),
Effect.runPromise
)
上面的 code 可以自行實驗看看,不論是換成 getData
或是關掉 batching
都會讓 batch 的效果消失
在這篇裡面我們認識是怎麼使用 Effect 來 batch request ,下一篇會再來分享一個實際的經驗,怎麼取得與處理像看板這種大量且複雜的資料