前言
最近工作中有一個需求是:如果這個請求超時,則進行重試,且重試次數可配置。
首先我們發請求使用的庫為:Axios
,其處理請求的位置,是在 redux-observable
中的 epic
裡。
那麼如果要完成重試機制的話,有兩種辦法:
- 在對
Axios
封裝的函式裡新增重試程式碼 - 在
epic
裡,使用RxJS
操作符進行重試。
關於 Axios
重試的,其實比較麻煩的,而且需要在原有封裝好的函式裡,繼續新增重試程式碼,總感覺不太好。且維護起來也不太方便。於是那就使用 RxJS
操作符進行重試吧。本文程式碼將不會套用專案程式碼,而是重新寫一個 Demo
,方便理解。
RxJS 錯誤重試操作符
在 RxJS
中,提供了兩個操作符 retry
和 retryWhen
。
需要注意的是:重試時,這兩個操作符都會重試整個序列。
且 retry
和 retryWhen
只捕獲 Error
,但是對 Promise
有點無能為,解決方案文中會說明。
retry
retry
操作符是用來指定重試次數,比如遇到錯誤了,將會重試n次。以下是 Demo
:
const source = Rx.Observable.interval(1000)
const example = source.map(val => {
if (val === 2) {
throw Error('error');
}
return val;
}).retry(1)
example.subscribe({
next: val => console.log(val),
error: val => console.log(val.message)
});
複製程式碼
上面的程式碼,會每隔1秒鐘發出一次數字序列,當使用 subscribe
訂閱後,一秒鐘後會發出0,第二秒發出1,以此類推。
然後每次的數字序列都會到到達 map
操作符裡,在 map
操作符中,我們可以看到當數字序列等於2時,則會丟擲錯誤。不等於2時 ,則原封不動的返回,最終到達 subscribe
中的 next
函式。
執行結果如圖:
首先發出0和1,沒有問題,當val為2時,丟擲錯誤。被 retry
捕獲到,重新走一遍整個 RxJS
序列。於是會發現又發了一次0和1,這個時候又到2了,於是繼續報錯,但是 retry
的重試次數已經用完,則 retry
就不會再管了,直接跳過。於是被 subscribe
中的 error
函式捕獲到。列印出 error
。
retryWhen
上面的 retry
操作符,只能用來設定重試次數,我們有時想做成:重試時,列印日誌,或者其他操作。那麼這個時候 retry
就不太適合了。所以我們需要 retryWhen
來操作。
程式碼如下:
const source = Rx.Observable.interval(1000)
const example = source.map(val => {
if (val === 2) {
throw Error('error')
}
return val;
}).retryWhen(err => {
return err
.do(() => console.log('正在重試'))
.delay(2000)
})
example.subscribe({
next: val => console.log(val),
error: val => console.log(val.message)
});
複製程式碼
執行結果如圖:
其傳送邏輯和上面差不多,只是處理的時候不同了。
我們使用 retryWhen
操作符來控制重試的邏輯,我們先使用 do
操作符,在控制檯列印字串,再使用 delay
來延遲2秒進行重試。
但是這裡會一直重試,沒有設定重試次數的地方,解決方案在下一章節。
retry + retryWhen
這個時候,我們發現 retry
可以設定重試次數,retryWhen
可以設定重試邏輯。
但是我們想設定重試次數,又想設定重試邏輯,那應該怎麼辦呢?
OK,先讓我們看看 retryWhen
操作符。這個操作符如果內部觸發了 Error
或者 Completed
,那麼就會停止重試,將會把內部觸發的 Error
或者 Completed
交給 subscribe
的訂閱操作符。可能這樣說,比較麻煩,我們先上 Demo
,按照 Demo
來說,會有助於理解:
const source = Rx.Observable.interval(1000)
const example = source.map(val => {
if (val === 2) {
throw Error('error')
}
return val;
}).retryWhen(err => {
return err
.scan((acc, curr) => {
if (acc > 2) {
throw curr
}
return acc + 1
}, 1)
})
example.subscribe({
next: val => console.log(val),
error: val => console.log(val.message)
});
複製程式碼
結果如圖:
傳送邏輯沒有變化,但是出現了新的操作符: scan
,那麼這個操作符是做什麼用的呢?
可以把 scan
理解為 javascript
中的 reduce
函式,這個操作符,具有兩個引數,第一個是回撥函式,第二個是預設值。就比如上面的程式碼,預設值是1,acc第一次是1,第二次重試時,acc就是2,第三次重試時,acc為3,已經大於2了,那麼 if
表示式則會true,直接使用 throw
丟擲 curr
,這裡的 curr
其實就是上面的錯誤原文。上文也說道了,如果在 scan
內初觸發了 Error
則會停止重試,交給下面的 subscribe
,然後觸發了訂閱的 error
函式,列印出 error
。
其實滿足重試次數後,把錯誤再丟擲去,是比較正常的操作,讓後面的操作符,對錯誤進行處理。但是可能有些人的業務需求是需要返回 Completed
,那麼可以參考下面的程式碼:
const source = Rx.Observable.interval(200)
const example = source.map(val => {
if (val === 2) {
throw Error('error')
}
return val;
}).retryWhen(err => {
return err
.scan((acc, curr) => {
return acc + 1
}, 0)
.takeWhile(v => v <= 2)
})
example.subscribe({
complete: () => console.log('Completed'),
next: val => console.log(val),
error: val => console.log(val.message)
});
複製程式碼
執行結果如圖:
可以看到使用了一個新的操作符 takeWhile
。這個操作符接受一個函式,如果這個函式返回了 true
,則繼續把值交給下面的操作符,一旦函式返回 false
,則會觸發 subscribe
中的 complete
,也就是說這個序列已經完成。這樣看的話,你就明白上面的程式碼的意圖了。
解決Promise問題
上文也說了 retry
和 retryWhen
是不支援 Promise.reject()
的,其實這裡的表達不太準確,應該說是 Promise沒有重試的API,當重試的時候Promise
已經在執行中了,所以無法再次呼叫該方法。也就造成了 retry
和 retryWhen
不能對 Promise
進行重試。那麼解決方案也很簡單了。
我們可以使用 defer
操作符,現在來簡單說明下這個操作符的用處。
defer
接受一個函式引數,其函式不會執行,只有你使用 subscribe
去訂閱的時候,才會去執行函式。並且執行函式,都是在獨立的執行空間內,也就說,即使我們使用 Promise
,也不會造成無法重試的情況,因為它不是複用之前的結果,而是重新開啟一個新的記憶體空間,去執行函式,返回函式結果。
那麼我們就可以把程式碼寫成下面這樣:
const getInfo: AxiosPromise = axios.get('http://xxx.com')
const exp = defer(() => getInfo)
.retryWhen(err => {
return err.scan((acc, curr) => {
if (acc > 2) {
throw curr
}
return acc + 1
}, 1)
})
example.subscribe({
next: val => console.log(val),
error: val => console.log(val.message)
});
複製程式碼
作者資訊
Black-Hole: 158blackhole@gmail.com
Blog: www.bugs.cc
Github: github.com/BlackHole1