RxSwift可观察错误停止链 - 带Rx的Web服务,如何恢复?
显然我是RxSwift的新手,虽然我消耗了大量文档和演讲,但我想我缺少一些基本概念。RxSwift可观察错误停止链 - 带Rx的Web服务,如何恢复?
在我的应用程序中,我有一个REST风格的Web服务来加载各种资源,但Web服务的基础URL在构建/开始时未知。相反,我有一个“URL解析器”Web服务,我可以使用我的应用程序包,版本和可能的环境(“生产”,“调试”或在应用程序调试设置中输入的任何自定义字符串)调用以获取基础URL,然后使用为实际的服务。
我的想法是,我会创建2个服务,一个用于URL解析器,另一个用于实际的Web服务,它为我提供了资源。 URL解析器将有一个Variable和一个Observable。我使用该变量来表示需要通过对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的唯一的其他观察者是我的其他网络服务类,我想我可以实现同样的风格认购功能也在那里。
我读过一些人建议返回一个Result枚举,它有两种情况:实际结果(.success(result:T))或错误(.error(error:Error))。
那么,在Rx中处理错误Web服务错误的更好方法是什么?我不能把这个问题包裹起来,我想了解2天。任何想法或建议?
更新
它只是来到我的脑海,我可以忽略来自Web服务的错误调用的任何错误完全,而是张贴到一个全球性的“错误”的变量,我的应用程序代理可以观察到显示警报。 “错误”可以引用最初导致它的功能,因此可以进行重试。我仍然感到困惑,不知道该怎么做。 :/
更新2
我想我可能找到了一个工作方案。由于我还是Rx和RxSwift的初学者,我很乐意接受改进建议。当我写实际的代码,我分裂我的调用链分为两个部分:
- ,我让Web服务调用
- ,我点击一个按钮,处理Web服务的结果的一部分,部分,无论是错误还是成功
在我单击按钮并处理结果的部分中,我使用catchError并按照注释中的建议重试。代码如下所示:
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服务了该隐到自己的变量,所以我可以在任何时候重新调用它。当我现在返回“userObservable”并在Web服务调用期间发生错误时,我可以在catchError中显示错误,并为下一次重试返回相同的“userObservable”。
目前这只能正确处理错误,当他们发生在Web服务调用链,所以我认为我应该让按钮点击驱动程序。
对于来到这里的每个人来说,好吧,您可能对Rx世界应该如何工作缺乏理解或误解。我仍然发现它有时会让人困惑,但我发现比我在原始问题中发布的更好的解决方案。
在Rx中,错误“杀死”或者完成链中的所有观察者,这实际上是一件好事。如果Web服务调用中存在预期的错误(如API错误),则应尝试在发生它们的位置处理它们,或将它们视为期望值。
例如,您的观察者可以返回一个可选类型,并且订阅者可以筛选值的存在。如果发生API调用错误,则返回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视图模型进行调用到Web serivces像这样:
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被执行了两次,这也不会发生。
我只是想知道是否有更好的方法来嵌套用户。
“问题是所有其他观察者都因为错误而死亡” - 这可能是一个问题,但它是Rx设计的方式。一个可观察的**可能有零个或多个OnNext,并且可能只有一个OnError或OnCompleted,此时观察结束,并且不能返回任何更多值**。 – Enigmativity
您通常会使用带**重试**的** catch **运算符来捕获错误并重试observables。 – Enigmativity
啊,我会看看并重试。谢谢。我知道这是设计,所以我想知道这个:我可以在我的Web服务中创建1个观察者。然后,我可以订阅观察员,转换其数据并仅在成功时将其发送给我的变量。然后,我可以创建第二个类似的转换,我将返回。只有在转换成功的情况下,变量才会被更新,而返回会将错误升级到我的UI,因此用户可以处理错误。这是一种有效的方法吗? – xxtesaxx