聊聊RxJS中的錯誤重試

BlackHole1發表於2019-01-12

前言

最近工作中有一個需求是:如果這個請求超時,則進行重試,且重試次數可配置。

首先我們發請求使用的庫為:Axios,其處理請求的位置,是在 redux-observable 中的 epic 裡。

那麼如果要完成重試機制的話,有兩種辦法:

  • 在對 Axios 封裝的函式裡新增重試程式碼
  • epic 裡,使用 RxJS 操作符進行重試。

關於 Axios 重試的,其實比較麻煩的,而且需要在原有封裝好的函式裡,繼續新增重試程式碼,總感覺不太好。且維護起來也不太方便。於是那就使用 RxJS 操作符進行重試吧。本文程式碼將不會套用專案程式碼,而是重新寫一個 Demo,方便理解。

RxJS 錯誤重試操作符

RxJS 中,提供了兩個操作符 retryretryWhen

需要注意的是:重試時,這兩個操作符都會重試整個序列

retryretryWhen 只捕獲 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 函式。

執行結果如圖:

Imgur

首先發出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)
});
複製程式碼

線上執行

執行結果如圖:

Imgur

其傳送邏輯和上面差不多,只是處理的時候不同了。

我們使用 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)
});
複製程式碼

線上執行

結果如圖:

Imgur

傳送邏輯沒有變化,但是出現了新的操作符: 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)
});
複製程式碼

線上執行

執行結果如圖:

Imgur

可以看到使用了一個新的操作符 takeWhile。這個操作符接受一個函式,如果這個函式返回了 true,則繼續把值交給下面的操作符,一旦函式返回 false,則會觸發 subscribe 中的 complete,也就是說這個序列已經完成。這樣看的話,你就明白上面的程式碼的意圖了。

解決Promise問題

上文也說了 retryretryWhen 是不支援 Promise.reject() 的,其實這裡的表達不太準確,應該說是 Promise沒有重試的API,當重試的時候Promise 已經在執行中了,所以無法再次呼叫該方法。也就造成了 retryretryWhen 不能對 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

相關文章