RxExample GitHubSignup 部分代码解读

FauS76 7年前
   <p>GitHubSignup 是一个注册例子的 Demo ,同时也是一个 MVVM 的 Demo 。但本节将重点介绍代码上 <strong>为什么这样写</strong> ,你可以从中了解到何时在代码中用 Rx 处理异步,如何合理的书写代码,以及如何优雅地处理网络请求状态。</p>    <p>事实上这个例子处理网络请求的方式是使用 using 操作符 hook 网络请求 Observable 的生命周期。</p>    <p>代码均在 RxExample 项目中,相关涉及文件如下:</p>    <ul>     <li>GitHubSignup 文件夹所有内容</li>     <li>ActivityIndicator.swift</li>    </ul>    <p>我们先来简单思考一下注册需要注意哪几个点,这里主要是表单验证问题:</p>    <ul>     <li>用户名不能重复,需要提交用户名到服务器验证</li>     <li>注册密码有等级限制,比如长度、带大小写字母</li>     <li>两次输入的密码相同</li>    </ul>    <p>从 Protocols.swift 文件入手,这个文件有两个枚举 ValidationResult 和 SignupState ,两个协议 GitHubAPI 和 GitHubValidationService 。</p>    <p>ValidationResult 包含了四个验证结果:</p>    <pre>  enumValidationResult{      case ok(message: String)      case empty      case validating      case failed(message: String)  }  </pre>    <p>分别是验证成功、验证为空、正在验证、验证失败。</p>    <p>在验证成功和验证失败两种情况中,会带上一个消息供展示。</p>    <p>SignupState 用于标记注册状态,表示是否已经注册,代码如下:</p>    <pre>  enumSignupState{      case signedUp(signedUp: Bool)  }  </pre>    <p>协议 GitHubAPI 和 GitHubValidationService 代码如下:</p>    <pre>  protocolGitHubAPI{      funcusernameAvailable(_username: String) -> Observable<Bool>      funcsignup(_username: String, password: String) -> Observable<Bool>  }    protocolGitHubValidationService{      funcvalidateUsername(_username: String) -> Observable<ValidationResult>      funcvalidatePassword(_password: String) -> ValidationResult      funcvalidateRepeatedPassword(_password: String, repeatedPassword: String) -> ValidationResult  }  </pre>    <p>在讨论这段代码的设计前,我们先思考一下哪些是异步场景:</p>    <ul>     <li>检查用户名是否可用</li>     <li>注册</li>    </ul>    <p>而验证密码和验证重复输入密码都可以同步地形式进行。</p>    <p>在设计到 <strong>检查用户名</strong> 和 <strong>注册</strong> 时,应当返回一个 Observable 代替 callback ,而密码的验证只需要在一个方法中返回验证结果即可。</p>    <p>所以上述两个协议中 usernameAvailable 、 signup 和 validateUsername 都是异步事件,都应当返回 Observable 。</p>    <p>DefaultImplementations.swift 文件给出了上述两个协议的实现,先来看 GitHubDefaultAPI :</p>    <pre>  classGitHubDefaultAPI:GitHubAPI{      let URLSession: Foundation.URLSession        static let sharedAPI = GitHubDefaultAPI(          URLSession: Foundation.URLSession.shared      )        init(URLSession: Foundation.URLSession) {          self.URLSession = URLSession      }        funcusernameAvailable(_username: String) -> Observable<Bool> {          // this is ofc just mock, but good enough            let url = URL(string: "https://github.com/\(username.URLEscaped)")!          let request = URLRequest(url: url)          return self.URLSession.rx.response(request: request)              .map { (response, _) in                  return response.statusCode == 404              }              .catchErrorJustReturn(false)      }        funcsignup(_username: String, password: String) -> Observable<Bool> {          // this is also just a mock          let signupResult = arc4random() % 5 == 0 ? false : true          return Observable.just(signupResult)              .concat(Observable.never())              .throttle(0.4, scheduler: MainScheduler.instance)              .take(1)      }  }  </pre>    <p>方法 usernameAvailable 验证了用户名是否可用,这里验证的方案是请求该用户名对应的主页,返回 404 说明没有该用户。</p>    <p>signup 是一个带延时的 mock 方法,对每一次的注册返回一个随机结果,并对该结果延迟 0.4s 。</p>    <p>你可能会问代码 .concat(Observable.never()) 存在的意义,回顾操作符 throttle ,当 <strong>接到 completed 时,立即传递 completed</strong> 。而 just 发射第一个值后立即发射 completed ,从而没有延时效果。当 concat 一个 never 时, Observable 永远不会发射 completed ,从而得到延时效果。</p>    <p>来看 GitHubDefaultValidationService , GitHubDefaultValidationService 提供了 <strong>用户名验证</strong> 、 <strong>密码验证</strong> 、 <strong>重复密码验证</strong> 三个功能。</p>    <p>我们只需关注方法 validateUsername :</p>    <pre>  funcvalidateUsername(_username: String) -> Observable<ValidationResult> {      if username.characters.count == 0 {          return .just(.empty)      }          // this obviously won't be      if username.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) != nil {          return .just(.failed(message: "Username can only contain numbers or digits"))      }        let loadingValue = ValidationResult.validating        return API          .usernameAvailable(username)          .map { available in              if available {                  return .ok(message: "Username available")              }              else {                  return .failed(message: "Username already taken")              }          }          .startWith(loadingValue)  }  </pre>    <p>首先验证输入的用户名是否为空,为空则直接返回 .just(.empty) ,再验证输入的用户名是否均为数字或父母,不是则直接返回 .just(.failed(message: "Username can only contain numbers or digits")) 。</p>    <p>当通过以上两种验证时,我们需要请求服务器验证用户名是否重复。 .startWith(loadingValue) 为我们请求数据时添加了 loading 状态。</p>    <h2>UsingVanillaObservables > 1</h2>    <p>本节示例在代码上使用 Observable 和 Driver 区别不大,以使用 Observable 代码为例。</p>    <p>GithubSignupViewModel1 是对应的ViewModel。</p>    <h2>ActivityIndicator</h2>    <h3>Using 操作符</h3>    <p>使用 using 操作符可以创建一个和 Observable 相同生命周期的实例对象·。</p>    <p>当 subscribe 时,创建该实例,当 dispose 时,调用该实例的dispose。</p>    <pre>  extensionObservablewhereElement{      public static funcusing<R: Disposable>(_resourceFactory: @escaping() throws -> R, observableFactory: @escaping (R) throws -> Observable<E>) -> Observable<E>  }  </pre>    <p>在 resourceFactory 中传入一个工厂方法,返回一个可以 dispose 的实例。</p>    <p>在 observableFactory 中同样传入一个工厂方法,这里的 R 是 resourceFactory 中返回的实例,返回一个 Observable ,这正是与 resource 对应生命周期的 Observable 。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/71962069e91927f0b1b36ba5f40e507f.png"></p>    <p>来看 ActivityIndicator 是如何使用 using 管理请求状态的。</p>    <pre>  extensionObservableConvertibleType{      public functrackActivity(_activityIndicator: ActivityIndicator) -> Observable<E> {          return activityIndicator.trackActivityOfObservable(self)      }  }  </pre>    <p>为 Observable 创建的扩展方法 trackActivity 中传入一个 ActivityIndicator 就可以跟踪加载状态了。</p>    <p>ActivityIndicator 服从协议 SharedSequenceConvertibleType ,直接调用 asObservable() 即可获取 loading 状态。</p>    <p>移除保证线程安全部分代码, ActivityIndicator 代码如下:</p>    <pre>  public classActivityIndicator:SharedSequenceConvertibleType{      public typealias E = Bool      public typealias SharingStrategy = DriverSharingStrategy        private let _variable = Variable(0)      private let _loading: SharedSequence<SharingStrategy, Bool>        public init() {          _loading = _variable.asDriver()              .map { $0 > 0 }              .distinctUntilChanged()      }        fileprivate functrackActivityOfObservable<O: ObservableConvertibleType>(_source: O) -> Observable<O.E> {          return Observable.using({ () -> ActivityToken<O.E> in              self.increment()              return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)          }) { t in              return t.asObservable()          }      }        private funcincrement() {          _variable.value = _variable.value + 1      }        private funcdecrement() {          _variable.value = _variable.value - 1      }        public funcasSharedSequence() -> SharedSequence<SharingStrategy, E> {          return _loading      }  }  </pre>    <p>我们通过 _variable 表示正在执行的 Observable ,当 _variable 中的值为 0 时, _loading 发射一个 false ,表示加载结束,当 _variable 中的值大于 0 时, _loading 会发射 true 。</p>    <p>方法 increment 和 decrement 处理的在执行的 Observable 的数量。</p>    <p>而在 trackActivityOfObservable 中使用了 using 将 increment 和 decrement 与 Observable 的生命周期绑定起来。</p>    <p>调用 using 的 resourceFactory 时,调用 increment 将资源数加1。</p>    <p>当 dispose 时,调用 ActivityToken 的 dispose 方法。</p>    <p>ActivityToken 代码如下:</p>    <pre>  private structActivityToken<E> :ObservableConvertibleType,Disposable{      private let _source: Observable<E>      private let _dispose: Cancelable        init(source: Observable<E>, disposeAction: @escaping () -> ()) {          _source = source          _dispose = Disposables.create(with: disposeAction)      }        funcdispose() {          _dispose.dispose()      }        funcasObservable() -> Observable<E> {          return _source      }  }  </pre>    <p>这就完成了对 Observable 的监听。</p>    <p> </p>    <p>来自:http://blog.dianqk.org/2017/03/03/rxexample-githubsignup/</p>    <p> </p>