RxSwift可观察错误停止链 - 带Rx的Web服务,如何恢复?

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服务调用链,所以我认为我应该让按钮点击驱动程序。

+0

“问题是所有其他观察者都因为错误而死亡” - 这可能是一个问题,但它是Rx设计的方式。一个可观察的**可能有零个或多个OnNext,并且可能只有一个OnError或OnCompleted,此时观察结束,并且不能返回任何更多值**。 – Enigmativity

+0

您通常会使用带**重试**的** catch **运算符来捕获错误并重试observables。 – Enigmativity

+0

啊,我会看看并重试。谢谢。我知道这是设计,所以我想知道这个:我可以在我的Web服务中创建1个观察者。然后,我可以订阅观察员,转换其数据并仅在成功时将其发送给我的变量。然后,我可以创建第二个类似的转换,我将返回。只有在转换成功的情况下,变量才会被更新,而返回会将错误升级到我的UI,因此用户可以处理错误。这是一种有效的方法吗? – xxtesaxx

对于来到这里的每个人来说,好吧,您可能对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被执行了两次,这也不会发生。

我只是想知道是否有更好的方法来嵌套用户。