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 已完成且不能返回任何值 您通常会使用带有 retrycatch 运算符来捕获错误并重试 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 服务,如何恢复?的主要内容,如果未能解决你的问题,请参考以下文章

RxSwift:返回一个带有错误的新可观察对象

在 ViewController 中可观察到的单元测试 RxSwift

为啥带有多个可观察对象的 RxSwift concat 似乎不起作用?

RxSwift:嵌套可观察对象上的 FlatMap

RxSwift:将可完成映射到单个可观察?

RxSwift: 链 Completable 到 Observable