RxSwift 可观察到的错误停止链 - 带有 Rx 的 Web 服务,如何恢复?
Posted
技术标签:
【中文标题】RxSwift 可观察到的错误停止链 - 带有 Rx 的 Web 服务,如何恢复?【英文标题】:RxSwift observable error stops chain - Web services with Rx, how to recover? 【发布时间】:2017-10-28 09:09:43 【问题描述】:显然我是 RxSwift 的新手,虽然我阅读了大量的文档和演讲,但我认为我缺少一些基本概念。
在我的应用程序中,我有一个 RESTful Web 服务来加载各种资源,但 Web 服务的基本 URL 在构建/启动时是未知的。相反,我有一个“URL 解析器”Web 服务,我可以使用我的应用程序包、版本和可能的环境(“生产”、“调试”或在应用程序调试设置中输入的任何自定义字符串)调用它来获取我然后使用的基本 url为实际服务。
我的想法是我会创建 2 个服务,一个用于 URL 解析器,另一个用于为我提供资源的实际 Web 服务。 URL 解析器将有一个变量和一个可观察对象。我使用该变量来表示需要通过对 URL 解析器的 Web 服务调用来刷新基本 URL。我通过观察变量并仅过滤真实值来做到这一点。服务类中的一个函数将变量值设置为 true(最初为 false),在过滤变量的观察者中,我在另一个 Observable 中进行 Web 服务调用(此示例使用虚拟 JSON Web 服务):
import Foundation
import RxSwift
import Alamofire
struct BaseURL: Codable
let title: String
struct URLService
private static var counter = 0
private static let urlVariable: Variable<Bool> = Variable(false)
static let urlObservable: Observable<BaseURL> = urlVariable.asObservable()
.filter counter += 1; return $0
.flatMap _ in
return Observable.create observer in
let url = counter < 5 ? "https://jsonplaceholder.typicode.com/posts" : ""
let requestReference = Alamofire.request(url).responseJSON response in
do
let items = try JSONDecoder().decode([BaseURL].self, from: response.data!)
observer.onNext(items[0])
catch
observer.onError(error)
return Disposables.create()
requestReference.cancel()
static func getBaseUrl()
urlVariable.value = true;
static func reset()
counter = 0;
现在的问题是,有时 Web 服务调用可能会失败,我需要向用户显示错误,以便可以重试。我认为 onError 对此很有用,但它似乎永远杀死了所有订阅者。
我可以将订阅放在它自己的函数和观察者的错误处理程序中,我可以显示一个警报,然后再次调用订阅函数,如下所示:
func subscribe()
URLService.urlObservable.subscribe(onNext: (baseURL) in
let alert = UIAlertController(title: "Success in Web Service", message: "Base URL is \(baseURL.title)", preferredStyle: .alert)
let actionYes = UIAlertAction(title: "Try again!", style: .default, handler: action in
URLService.getBaseUrl()
)
alert.addAction(actionYes)
DispatchQueue.main.async
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1;
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
, onError: error in
let alert = UIAlertController(title: "Error in Web Service", message: "Something went wrong: \(error.localizedDescription)", preferredStyle: .alert)
let actionYes = UIAlertAction(title: "Yes", style: .default, handler: action in
URLService.reset()
self.subscribe()
)
alert.addAction(actionYes)
DispatchQueue.main.async
VesselService.reset()
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1;
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
).disposed(by: disposeBag)
然后在我的 AppDelegate 中调用
subscribe()
URLService.getBaseUrl()
问题是所有其他观察者也会因错误而被杀死,但由于 URLService.urlObservable 上唯一的其他观察者是我的另一个 Web 服务类,我想我也可以在那里实现相同样式的订阅函数.
我读到有人建议返回一个 Result 枚举,它有 2 种情况:实际结果 (.success(result: T)) 或错误 (.error(error: Error))。
那么在 Rx 中处理错误 Web 服务错误的更好方法是什么?我无法解决这个问题,我正在尝试 2 天来理解它。有什么想法或建议吗?
更新
我突然想到,我可以完全忽略来自 Web 服务调用的错误,而是将任何错误发布到我的应用程序委托可以观察到的全局“错误”变量以显示警报。 “错误”可以引用最初导致它的函数,因此可以进行重试。我仍然很困惑,不知道我应该做什么。 :/
更新 2
我想我可能会找到一个可行的解决方案。由于我还是 Rx 和 RxSwift 的初学者,我很乐意接受改进建议。在编写实际代码时,我将调用链分为两部分:
我调用 Web 服务的部分 单击按钮并处理 Web 服务结果的部分,无论是错误还是成功在我单击按钮并处理结果的部分,我使用 catchError 并按照 cmets 中的建议重试。代码如下所示:
let userObservable = URLService
.getBaseUrl(environment: UserDefaults.standard.environment) //Get base url from web service 1
.flatMap( [unowned self] baseURL -> Observable<User> in
UserService.getUser(baseURL: baseURL,
email: self.usernameTextField.text!,
password: self.passwordTextField.text!) //Get user from web service 2 using the base url from webservice 1
)
signInButton
.rx
.tap
.throttle(0.5, scheduler: MainScheduler.instance)
.flatMap( [unowned self] () -> Observable<()> in
Observable.create observable in
let hud = MBProgressHUD.present(withTitle: "Signing in...");
self.hud = hud
observable.onNext(())
return Disposables.create
hud?.dismiss()
)
.flatMap( () -> Observable<User> in
return userObservable
)
.catchError( [unowned self] error -> Observable<User> in
self.hud?.dismiss()
self.handleError(error)
return userObservable
)
.retry()
.subscribe(onNext: [unowned self] (user) in
UserDefaults.standard.accessToken = user.accessToken
UserDefaults.standard.tokenType = user.tokenType
self.hud?.dismiss()
)
.disposed(by: disposeBag)
诀窍是将对两个 Web 服务的调用从 cain 转移到它们自己的变量中,这样我就可以随时重新调用它。当我现在返回“userObservable”并且在 Web 服务调用期间发生错误时,我可以在 catchError 中显示错误并返回相同的“userObservable”以进行下一次重试。
目前,这仅在 Web 服务调用链中发生错误时才能正确处理,因此我认为我应该让按钮点击驱动程序。
【问题讨论】:
“问题是所有其他观察者都会因错误而被杀死”——这可能是个问题,但这是 Rx 的设计方式。一个 observable 可能有零个或多个 OnNext,并且后面可能只有一个 OnError 或 OnCompleted,此时 observable 已完成且不能返回任何值。 您通常会使用带有 retry 的 catch 运算符来捕获错误并重试 observables。 啊,我看看 catch 和 retry。谢谢。我知道这是设计,所以我想知道:我可以在我的 Web 服务中创建 1 个观察者。然后我可以订阅观察者,转换它的数据,并只有在它成功时才向我的变量发出信号。然后我可以创建第二个类似的转换,然后返回。该变量只会在转换成功时更新,而返回会将错误升级到我的 UI,以便用户可以处理错误。这是一种有效的方法吗? 几乎总有一种方法可以在单个 observable 中干净利落地完成。除非您已经用尽所有其他方法,否则不要介绍主题和副作用。这就像良好的编码一样 - 尝试将错误处理封装在代码块中,以便可观察对象永远不必响应错误。 @xxtesaxx 也许你可以使用这里描述的 .switchLatest() - ***.com/a/47040856/856588 【参考方案1】:好的,所以对于来到这里的每个人来说,您可能对 Rx 世界应该如何运作缺乏理解或误解。我仍然觉得它有时令人困惑,但我找到了比我在原始问题中发布的更好的解决方案。
在 Rx 中,一个错误“杀死”或者更确切地说完成了链中的所有观察者,这实际上是一件好事。如果在 Web 服务调用中出现 API 错误等预期错误,您应该尝试在它们发生的地方处理它们,或者将它们视为预期值。
例如,您的观察者可以返回一个可选类型,而订阅者可以过滤值的存在。如果 API 调用发生错误,则返回 nil。其他“错误处理程序”可以过滤 nil 值以向用户显示错误消息。
同样可行的是返回具有两种情况的 Result 枚举:.success(value: T) 和 .error(error: Error)。您将错误视为可接受的结果,观察者负责检查它是否应该显示错误消息或成功结果值。
还有另一种选择,它肯定不是最好的,但它可以将您期望失败的呼叫简单地嵌套在呼叫的订户中,该呼叫的订户不得受到影响。在我的情况下,这是一个按钮点击,它会导致调用 Web 服务。
我原来帖子的“更新 2”将变为:
signInButton.rx.tap.throttle(0.5, scheduler: MainScheduler.instance)
.subscribe(onNext: [unowned self] () in
log.debug("Trying to sign user in. Presenting HUD")
self.hud = MBProgressHUD.present(withTitle: "Signing in...");
self.viewModel.signIn()
.subscribe(onNext: [unowned self] user in
log.debug("User signed in successfully. Dismissing HUD")
self.hud?.dismiss()
, onError: [unowned self] error in
log.error("Failed to sign user in. Dismissing HUD and presenting error: \(error)")
self.hud?.dismiss()
self.handleError(error)
).disposed(by: self.disposeBag)
).disposed(by: self.disposeBag)
MVVM 视图模型像这样调用网络服务:
func signIn() -> Observable<User>
log.debug("HUD presented. Loading BaseURL to sign in User")
return URLService.getBaseUrl(environment: UserDefaults.standard.environment)
.flatMap [unowned self] baseURL -> Observable<BaseURL> in
log.debug("BaseURL loaded. Checking if special env is used.")
if let specialEnv = baseURL.users[self.username.value]
log.debug("Special env is used. Reloading BaseURL")
UserDefaults.standard.environment = specialEnv
return URLService.getBaseUrl(environment: specialEnv)
else
log.debug("Current env is used. Returning BaseURL")
return Observable.just(baseURL)
.flatMap [unowned self] baseURL -> Observable<User> in
log.debug("BaseURL to use is: \(baseURL.url). Now signing in User.")
let getUser = UserService.getUser(baseURL: baseURL.url, email: self.username.value, password: self.password.value).share()
getUser.subscribe(onError: error in
UserDefaults.standard.environment = nil
).disposed(by: self.disposeBag)
return getUser
.map user in
UserDefaults.standard.accessToken = user.accessToken
UserDefaults.standard.tokenType = user.tokenType
return user
首先我想在按下按钮时只调用视图模型的 signIn() 函数,但由于视图模型中不应该有 UI 代码,我认为呈现和关闭 HUD 是 ViewController 的责任。
我认为这个设计现在非常可靠。按钮观察者永远不会完成,并且可以永远继续发送事件。早些时候,如果出现第二个错误,可能会发生按钮观察者死亡,而我的日志显示 userObservable 执行了两次,这也一定不会发生。
我只是想知道是否有比嵌套订阅者更好的方法。
【讨论】:
非常感谢,我正在研究如何连接 VC->VM->API 层。你有什么新的建议吗?以上是关于RxSwift 可观察到的错误停止链 - 带有 Rx 的 Web 服务,如何恢复?的主要内容,如果未能解决你的问题,请参考以下文章
在 ViewController 中可观察到的单元测试 RxSwift