Home Articles

RxSwift可观察错误会阻止链 - 使用Rx的Web服务,如何恢复?

Asked
Viewed 313 times
3

显然我是RxSwift的新手,虽然我消耗了很多文档和演讲,但我想我缺少一些基本概念 .

在我的应用程序中,我有一个RESTful 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对此很有用,但它似乎永远杀死了所有订阅者 .

我可以将订阅放在它自己的函数中并在Observer的错误处理程序中,我可以显示一个警告然后再次调用subscribe函数,如下所示:

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服务类,我想我也可以在那里实现相同的样式订阅函数 .

我读到有些人建议返回一个结果枚举,它有两种情况:实际结果(.success(结果:T))或错误(.error(错误:错误)) .

那么在Rx中处理错误Web服务错误的更好方法是什么?我无法解决这个问题,我试着用了2天才明白它 . 任何想法或建议?

Update

我刚想到我可以完全忽略来自Web服务调用的错误,而是将任何错误发布到全局“错误”变量,我的应用程序委托可以观察到这些变量以显示警报 . “错误”可以引用最初导致它的功能,因此可以进行重试 . 我仍然感到困惑,不知道该怎么做 . :/

Update 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)

诀窍是将来自cain的两个Web服务的调用移动到他们自己的变量中,以便我可以随时重新调用它 . 当我现在返回“userObservable”并在Web服务调用期间发生错误时,我可以在catchError中显示错误并返回相同的“userObservable”以进行下一次重试 .

目前,只有当它们出现在Web服务调用链中时才能正确处理错误,所以我认为我应该让按钮点击一个驱动程序 .

1 Answer

  • 0

    好的,对于每个来到这里的人来说,你可能对Rx世界应该如何运作缺乏了解或误解 . 我仍然觉得它有时令人困惑,但我发现了一种比我在原始问题中发布的更好的解决方案 .

    在Rx中,错误“杀死”或者更确切地说完成链中的所有观察者,这实际上是一件好事 . 如果Web服务调用中存在API错误等预期错误,您应该尝试在它们出现的地方处理它们,或者将它们视为预期值 .

    例如,您的观察者可以返回可选类型,订阅者可以过滤值的存在 . 如果API调用中发生错误,则返回nil . 其他“错误处理程序”可以过滤nil值以向用户显示错误消息 .

    同样可行的是返回带有两种情况的Result枚举:.success(value:T)和.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服务,如下所示:

    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已执行两次,这也不会发生 .

    我只是想知道是否有更好的方法来嵌套订阅者 .

Related