RxSwift_v1.0筆記——14 Error Handling in Practice

RxSwift_v1.0筆記——14 Error Handling in Practice

錯誤在所難免,我們需要知道如何優(yōu)雅和高效的處理錯誤。這章,你講學習如何處理錯誤,如何通過重審來管理錯誤恢復(how to manage error recovery through retries)。or just surrender yourself to the universe and let the errors go。

開始 269

這個應用是第12章的延續(xù)。在這個版本的應用中,不但你能夠檢索用戶當前的位置并查詢這個位置的天氣,而且也請求城市名并查看那個位置的天氣。這個應用app也有activity indicator用來做視覺反饋。

像之前一樣在ApiController.swift,中替換你的key,pod install

let apiKey = BehaviorSubject(value: "[YOUR KEY]")

運行程序確保當你所說城市時能夠檢索天氣。

管理錯誤 269

任何應用都無法避免錯誤。不幸的是,沒有人能保證應用絕不會出錯,因此你需要某種類型d的錯誤處理機制。

應用中大部分普遍的錯誤有:

  • 沒有網(wǎng)絡連接:這十分普遍。如果應用需要網(wǎng)絡連接檢索和處理數(shù)據(jù),要是設備掉線了,你需要能夠適當?shù)臋z測并做出響應。
  • 無效輸入:有時你需要一個確定格式的輸入,但是用戶輸入的可能完全不同。在你的應用中可能有一個電話號碼字段(field),但是用戶不理睬需求并輸入了字母。
  • API錯誤或HTTP錯誤:API的錯誤可能有很大差異。他們可能是標準的HTTP錯誤(響應代碼從400到500),或作為響應中的錯誤,例如在JSON響應中使用狀態(tài)字段。

在RxSwift,錯誤處理是框架的一部分并能夠以兩種方式處理:

  • Catch:使用默認值從錯誤中恢復。

  • Retry:重試有限(或無限)次.

本章的開始項目沒有任何真正的錯誤處理。所有的錯誤用 catchErrorJustReturn捕獲返回一個虛擬的版本。這聽起來像是一個處理方案,但在RxSwift中有更好的處理方式,并且可以在任何一流的應用程序中保持一致和有益的錯誤處理方式。

拋出錯誤 270

一個好的開始是處理RxCocoa錯誤,它封裝了由蘋果底層框架返回的系統(tǒng)錯誤。RxCocoa錯誤提供了你遇到的更詳細類型的錯誤,并且也讓你的錯誤代碼更容易寫。

來看看RxCocoa封裝在底層(under the hood)是如果工作的,在Pods/RxCocoa/URLSession+Rx.swift.搜索下面方法:

public func data(request: URLRequest) -> Observable<Data> {...}

這個方法給定NSURLRequest,返回了一個Data類型的observable。

重要的部分是返回錯誤的代碼:

if 200 ..< 300 ~= response.statusCode {
    return data
}
else {
    throw RxCocoaURLError.httpRequestFailed(response: response, data: data)
}

這是一個用來說明observable如何能夠發(fā)射一個錯誤的完美例子——具體來說,是一個定制(custom-tailored)錯誤,后續(xù)章節(jié)將會說明。

注意在這個閉包中沒有為錯誤寫返回。當你想在flatMap操作中輸出錯誤,你應該像常規(guī)的Swift代碼一樣使用throw。這是一個很好的例子,用來說明RxSwift如何讓您在必要時編寫符合習慣的Swift代碼,并在適當?shù)臅r候使用RxSwift類型的錯誤處理。

用catch處理錯誤 271

解釋了如何拋出錯誤,是時候看看怎樣處理錯誤了。大部分基本的方式是使用catch。catch操作與普通Swift中的do-try-catch流程非常相似。執(zhí)行一個observable,如果有錯誤產(chǎn)生,返回一個封裝了錯誤的事件。

在RxSwift,有兩個主要的捕獲錯誤的操作。第一個:

func catchError(_ handler:) -> RxSwift.Observable<Self.E>

這是常規(guī)的操作;它接受一個閉包作為參數(shù),并給出機會返回一個完全不同的observable。如果你還不清楚在哪里選擇使用這個,考慮一個捕獲策略,如果observable輸出錯誤就返回一個先前的緩存值。你能夠用這個機制來實現(xiàn)以下流程:

在這種情況下,catchError返回先前可用的值,而且由于某種原因,該值不再可用。

第二個是:

func catchErrorJustReturn(_ element:) -> RxSwift.Observable<Self.E>

在前兩章你已經(jīng)使用過它——它忽略錯誤并僅僅返回一個預先定義的值。這個操作比上一個受到更多限制,它不可能返回給定類型錯誤的值——不管錯誤是什么,對于任何錯誤它都返回同樣的值。

一個常見的陷阱 271

錯誤通過observable鏈傳播,因此如果在事發(fā)現(xiàn)場沒有進行任何處理,在observable鏈開始發(fā)生的錯誤將被轉(zhuǎn)發(fā)到(be forwarded to)最終的訂閱者。

這是什么意思呢?當一個observable錯誤發(fā)出時,錯誤的訂閱者被通知,然后所有的訂閱者被銷毀。因此當一個observable錯誤發(fā)出時,這個observable必須終止,且跟隨錯誤之后的任何事件將被忽略。這是observable 約定的規(guī)則。

你能夠看到它被繪制到下面的時間線上。一旦網(wǎng)絡產(chǎn)生一個錯誤,observable序列錯誤輸出,訂閱更新UI的工作將停止,實際上阻止了將來的更新:

為了將這個轉(zhuǎn)換到實際的應用中,移除在textSearch observable中的.catchErrorJustReturn(ApiController.Weather.empty)行,啟動應用,在城市搜索字段隨機輸入字符直到API 回應了404錯誤。在你的控制臺中你應該看到以下相似的信息:

"http://api.openweathermap.org/data/2.5/weather?
q=goierjgioerjgioej&appid=[API-KEY]&units=metric" -i -v
Failure (207ms): Status 404

當響應后(這意味著它是一個無效的城市名),這個應用停止了工作,并且搜索在那之后不再工作。不完美的用戶體驗,不是嗎?

捕獲錯誤 272

現(xiàn)在你已經(jīng)了解了一些原理,你可以繼續(xù)更新當前項目。一旦你完成了,這個應用將通過返回一個空的Weather類型來從錯誤中恢復,因此這個應用的流程不會被中斷。

這次,工作流包含了錯誤處理,將看起來像下圖這樣:

這很好,但如果app可以返回緩存數(shù)據(jù),如果有得話,那將更完美。

打開ViewController.swift,創(chuàng)建一個簡單的字典來緩存天氣數(shù)據(jù),增加它作為視圖控制器的屬性:

var cache = [String: Weather]()

這將臨時的存儲緩存數(shù)據(jù)。滾動到 viewDidLoad()內(nèi),找到你創(chuàng)建textSearch observable的行。現(xiàn)在通過添加 do(onNext:)更改textSearch observable來填充緩存:

let textSearch = searchInput.flatMap { text in
  return ApiController.shared.currentWeather(city: text ?? "Error")
    .do(onNext: { data in
      if let text = text {
        self.cache[text] = data
      }
    })
    .catchErrorJustReturn(ApiController.Weather.empty)
}

這樣每個有效的天氣響應將被存儲在字典例?,F(xiàn)在——怎么重用緩存結(jié)果呢?

在錯誤事件返回一個緩存值,替換 .catchErrorJustReturn(ApiController.Weather.empty)用:

.catchError { error in
  if let text = text, let cachedData = self.cache[text] {
    return Observable.just(cachedData)
  } else {
    return Observable.just(ApiController.Weather.empty)
  }
}

為了測試這個,輸入3~4個城市,例如“London”, “New York”, “Amsterdam”并加載這些城市的天氣。接著,斷開網(wǎng)絡搜索一個不同的城市,例如“Barcelona”;你應該接受到一個錯誤。保持斷網(wǎng)并搜索一個你已經(jīng)檢索數(shù)據(jù)的城市,這個應用將返回緩存的版本。

這是catch的普通用法。你一定可以擴展它,使其成為一個通用和強大的緩存解決方案。

Retry錯誤 274

在RxSwift捕獲錯誤僅僅是錯誤處理的一種方式。你也能用retry處理錯誤。

當使用retry操作并且一個observable錯誤輸出時,observable將重復它自己。重要的是要記住,retry意味著重復在observable內(nèi)的整個任務

這是建議避免在observable內(nèi)部更改用戶界面以免產(chǎn)生副作用(side effect)的主要原因之一,因為您無法控制誰將重試它!

Retry操作 274

retry操作有三種類型。第一個是最基礎的:

func retry() -> RxSwift.Observable<Self.E>

這將無限次的重復observable直到他返回成功。例如,如果沒有網(wǎng)絡連接,他講持續(xù)retry直到連接有效。這聽起來像是一個粗魯?shù)闹饕猓芎馁Y源,如無必要,很少會推薦retry無限次。

為了測試這個操作,注釋掉complete catchError塊:

//.catchError { error in
//  if let text = text, let cachedData = self.cache[text] {
//      return Observable.just(cachedData)
//  } else {
//      return Observable.just(ApiController.Weather.empty)
//  }
//}

在這個位置簡單的插入retry()。運行你的app,取消網(wǎng)絡連接并試著搜索。你將看很多的輸出在控制臺,它代表了應用正試著做出請求。過一會重新連接網(wǎng)絡,一旦應用成功完成請求,你將看到顯示結(jié)果。

第二個操作讓你改變重復的次數(shù)

func retry(_ maxAttemptCount:) -> Observable<E>

這個observable會重復指定的次數(shù)。嘗試一下內(nèi)容:

  • 移除剛增加的retry()
  • 取消先前注釋的代碼塊
  • 在 catchError前插入 retry(3)

完成后的代碼塊顯示如下:

return ApiController.shared.currentWeather(city: text ?? "Error")
  .do(onNext: { data in
    if let text = text {
      self.cache[text] = data
    }
  })
  .retry(3)
  .catchError { error in
    if let text = text, let cachedData = self.cache[text] {
      return Observable.just(cachedData)
    } else {
      return Observable.just(ApiController.Weather.empty)
    }
  }

如果observable產(chǎn)生錯誤,它將連續(xù)重復三次,在第四次時,錯誤將不被處理并將執(zhí)行 catchError操作。

高級retries 276

最后一個操作, retryWhen,適用于高級retry的情況。這個錯誤處理算子被認為是最強大的一個:

func retryWhen(_ notificationHandler:) -> Observable<E>

notificationHandler是 TriggerObservable.類型。觸發(fā)observable既是普通的observable或subject又被用來觸發(fā)retry任意次數(shù)。

在你的應用中你將做以下操作,如果互聯(lián)網(wǎng)連接不可用,或者API發(fā)生錯誤,請使用智能手法重試。

如果搜索出錯,這個目標是執(zhí)行一個遞增的回退(back-off)策略。設計結(jié)果如下:

subscription -> error
delay and retry after 1 second
subscription -> error
delay and retry after 3 seconds
subscription -> error
delay and retry after 5 seconds
subscription -> error
delay and retry after 10 seconds

他是一個聰明而復雜的解決方案。在正常的命令式代碼中,這意味著創(chuàng)建一些抽象,可能將任務封裝在NSOperation中,或者圍繞Grand Central Dispatch創(chuàng)建一個定制的封裝 - 但是使用RxSwift,解決方案是一小段代碼。

創(chuàng)建最終結(jié)果之前,考慮到(taking in consideration)該類型可以被忽略,并且觸發(fā)可以是任意類型,思考下observable(觸發(fā))內(nèi)部應該返回什么。

目標是用一個給定的延時序列retry四次。首先在 ViewController.swift內(nèi), 訂閱ApiController.shared.currentWeather序列之前,在 retryWhen操作前定義最大嘗試數(shù),它將用于序列內(nèi)部:

let maxAttempts = 4

重試這多次后,應該轉(zhuǎn)發(fā)(forward on)錯誤。接著替換 .retry(3):

.retryWhen { e in
  // flatMap source errors
}

這個observable必須與源observable返回錯誤的那個組合。因此當一個錯誤作為事件到達,這些observable的組合也將接收事件當前的索引。

你能夠和你的朋友, flatMapWithIndex操作,來實現(xiàn)這個。替換注釋“ // flatMap source errors”:

e.flatMapWithIndex { (error, attempt) -> Observable<Int> in
  // attempt few times
}

現(xiàn)在原始error observable與定義的重試之前多長延時被結(jié)合。

用一個定時器與那段代碼組合,產(chǎn)生第一個延時時間。按如下調(diào)整代碼:

e.flatMapWithIndex { (error, attempt) -> Observable<Int> in
  if attempt >= maxAttempts - 1 {
    return Observable.error(error)
  }
  return Observable<Int>.timer(Double(attempt + 1), scheduler:
    MainScheduler.instance).take(1)
}

包含retryWhen的完整代碼塊如下:

.retryWhen { e in
  return e.flatMapWithIndex { (error, attempt) -> Observable<Int> in
    if attempt >= maxAttempts - 1 {
      return Observable.error(error)
    }
    return Observable<Int>.timer(Double(attempt + 1), scheduler:
      MainScheduler.instance).take(1)
  }
}

當新的retry觸發(fā)時,在 flatMapWithIndex的第二個return前增加如下代碼輸出日志

print("== retrying after \(attempt + 1) seconds ==")

現(xiàn)在運行程序,取消網(wǎng)絡連接并執(zhí)行搜索。你應該看到下面日志:

== retrying after 1 seconds ==
... network ...
== retrying after 2 seconds ==
... network ...
== retrying after 3 seconds ==
... network ...

下圖顯示了處理的過程:

觸發(fā)器可以接受源錯誤observable完成十分復雜的回退(back-off)策略。這展示了你怎樣僅用數(shù)行RxSwift代碼來創(chuàng)建復雜的錯誤處理策略。

自定義錯誤 278

創(chuàng)建自定義錯誤遵循了一般Swift原則,因此,沒有什么時好的Swift開發(fā)者不知道的,但是看看如何處理錯誤和創(chuàng)建自定義操作任然是有益的。

創(chuàng)建自定義錯誤 278

來至RxCocoa返回的錯誤十分通用,因此HTTP 404錯誤(頁面沒發(fā)現(xiàn))幾乎被視為502(無效網(wǎng)關)。這是兩個完全不同的錯誤,所以能夠以不同的方式處理它們是最好的。

如果你深入ApiController.swift,你將看到已經(jīng)包含了有兩個錯誤情況,你能夠用來處理不同HTTP響應的錯誤:

enum ApiError: Error {
  case cityNotFound
  case serverFailure
}

你將在buildRequest(...)中使用這個錯誤類型。那個方法的最后一行返回一個數(shù)據(jù)的observable,然后隱射到JSON結(jié)構(gòu)的對象。這是你必須注入檢查并返回你創(chuàng)建的自定義錯誤的地方。RxCocoa的.data方便已經(jīng)處理了創(chuàng)建自定義錯誤對象。

替換在 buildRequest(…)中最后flatMap快內(nèi)的代碼:

return session.rx.response(request: request).map() { response, data in
  if 200 ..< 300 ~= response.statusCode {
    return JSON(data: data)
  } else if 400 ..< 500 ~= response.statusCode {
    throw ApiError.cityNotFound
  } else {
    throw ApiError.serverFailure
  }
}

使用這個方法,你能創(chuàng)建自定義錯誤和更多高級邏輯的事件,例如當API提供了一個在JSON內(nèi)部的響應信息,你能夠得到JSON數(shù)據(jù),處理message字段并將其封裝到錯誤中拋出。在Swift中Errors是十分強大的,而在RxSwift中更強大。

使用自定義錯誤 279

現(xiàn)在返回你的自定義error,你可以做些建設性的事情。

返回ViewController.swift,注釋掉retryWhen {…}操作。你希望error通過鏈并由observable串起來。

有一個便利的叫做InfoView的視圖,它在app底部閃現(xiàn)一個小的視圖用來給出錯誤信息。使用很簡單,只用一行代碼(現(xiàn)在不需要輸入這行):

InfoView.showIn(viewController: self, message: "An error occurred")

Errors 通常用retry或捕獲操作處理,但是如果你想要實現(xiàn)副作用并在用戶界面顯示消息呢?為了實現(xiàn)這個,用do操作。在同樣的訂閱中,你注釋retryWhen的地方,你已經(jīng)使用了一個do來執(zhí)行捕獲:

.do(onNext: { data in
  if let text = text {
    self.cache[text] = data
  }

將另一個參數(shù)(onError)添加到.do中,以便在發(fā)生錯誤事件時執(zhí)行副作用。完整的塊如下:

.do(onNext: { data in
  if let text = text {
    self.cache[text] = data
  }
}, onError: { [weak self] e in
  guard let strongSelf = self else { return }
  DispatchQueue.main.async {
    InfoView.showIn(viewController: strongSelf, message:
      "An error occurred")
  }
})

調(diào)度是必須的,因為這個序列在后臺線程被觀察;如果不這樣,UIKit將給出UI通過后臺線程被修改的警告。運行app,試著搜索一個隨機的字符串,錯誤將會出現(xiàn)(show up)。

很好,錯誤是相當?shù)钠胀?。但是你能夠很容易的在那里注入一些信息。RxSwift處理這個就像Swift,因此你能檢查錯誤情況并顯示不同信息。讓代碼更加清晰,增加下面新方法到視圖控制器類:

func showError(error e: Error) {
  if let e = e as? ApiController.ApiError {
    switch (e) {
    case .cityNotFound:
      InfoView.showIn(viewController: self, message: "City Name is invalid")
    case .serverFailure:
      InfoView.showIn(viewController: self, message: "Server error")
    }
  } else {
    InfoView.showIn(viewController: self, message: "An error occurred")
  }
}

然后返回到 do(onNext:onError:),替換 InfoView.showIn(...)這行,用:

strongSelf.showError(error: e)

這將提供更多的錯誤的上下文給用戶。

高級錯誤處理 281

高級錯誤的情況可能難以實現(xiàn)。 當API返回錯誤時,除了向用戶顯示消息外,還沒有一般的規(guī)則。假設你想增加認證到當前app。用戶必須經(jīng)過身份驗證并被授權(quán)才能請求天氣狀況。這意味著一個會話的創(chuàng)建將確保用戶登錄并正確的授權(quán)。但是假如會話失效該做什么呢?返回一個錯誤或返回一個空值與一個消息字符串?

在這種情況下沒有新技術(shù)(silver bullet)。這兩種解決方案都適用于此,但是了解有關錯誤的更多信息總是有用的,因此您將會走上這條路線。

在這種情況下,推薦的方式是執(zhí)行一個副作用并在會話正確創(chuàng)建之后立即重試。

你能夠使用名為apiKey的subject并包含你的API key來模擬這個行為。

這個API key subject 能夠在retryWhen closure內(nèi)部被用來觸發(fā)重試。缺少API key是一個明確的錯誤,因此在ApiError enum中增加下面的額外的錯誤情況:

case invalidKey

當服務器返回401編碼時,這個錯誤必須被拋出。在 builderRequest(...) function函數(shù)中拋出該錯誤,緊跟在第一個if if 200 ..< 300:

else if response.statusCode == 401 {
  throw ApiError.invalidKey
}

新的錯誤請求也有一個新的處理。回到ViewController.swift,升級在 showError(error:)方法中的switch包含新的case:

case .invalidKey:
  InfoView.showIn(viewController: self, message: "Key is invalid")

現(xiàn)在你能夠返回 viewDidLoad()并重新實現(xiàn)錯誤處理代碼。由于您已經(jīng)注釋掉當前的 retryWhen {...}代碼,您可以重新構(gòu)建您的錯誤處理。

上面的訂閱 searchInput創(chuàng)建了一個專門的閉包,在觀察者鏈外部,它將作為錯誤處理服務:

let retryHandler: (Observable<Error>) -> Observable<Int> = { e in
  return e.flatMapWithIndex { (error, attempt) -> Observable<Int> in
    //error handling
  }
}

你將復制你之前使用過的代碼到新的錯誤處理閉包中。替換//error處理注釋用:

if attempt >= maxAttempts - 1 {
  return Observable.error(error)
} else if let casted = error as? ApiController.ApiError, casted == .invalidKey {
  return ApiController.shared.apiKey
    .filter {$0 != ""}
    .map { _ in return 1 }
}
print("== retrying after \(attempt + 1) seconds ==")
return Observable<Int>.timer(Double(attempt + 1), scheduler: MainScheduler.instance)
  .take(1)

在 invalidKey case里返回類型不重要,但是你必須保持一致。之前,它是 Observable<Int>,因此你應該堅持返回那個類型。為此,你應該使用 { _ in return 1 }。

現(xiàn)在,滾動到被注釋的 retryWhen {…}并替換它用:

.retryWhen(retryHandler)

最后一步是使用API key的subject。 ViewController.swift中已經(jīng)有一個名為requestKey()的方法,它打開一個帶有文本框的alert視圖。 然后,用戶可以鍵入密鑰(或?qū)⑵湔迟N到其中)來模擬登錄功能。(您可以在此進行測試;在現(xiàn)實生活的應用程序中,用戶將輸入憑據(jù),從服務器獲取密鑰。)

切換到ApiController.swift。刪除apiKey主題中的API key并將其設置為一個空字符串(您可能希望將密鑰復制到某個地方,方便您再次使用它),如下所示:

let apiKey = BehaviorSubject(value: "")

運行程序,試著執(zhí)行搜索,你將接收到一個錯誤:

點擊在右下角的key按鈕:

應用將打開一個alert請求輸入API key:

粘貼API key到文本框點擊OK。app將重復整個observable序列,如果輸入有效,將返回正確的信息。如果輸入無效,將在不同的錯誤路徑上結(jié)束(end up)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容