【问题标题】:How to debounce async validator in Angular 4 with RxJS observable?如何在 Angular 4 中使用 RxJS observable 去抖动异步验证器?
【发布时间】:2017-10-26 07:19:45
【问题描述】:

我正在使用带有 Angular 4 响应式表单的自定义异步验证器来检查是否已通过调用后端获取电子邮件地址。

但是,Angular 会调用验证器,该验证器会针对每个输入的字符向服务器发出请求。这会给服务器带来不必要的压力。

是否可以使用 RxJS observable 优雅地去抖动异步调用?

import {Observable} from 'rxjs/Observable';

import {AbstractControl, ValidationErrors} from '@angular/forms';
import {Injectable} from '@angular/core';

import {UsersRepository} from '../repositories/users.repository';


@Injectable()
export class DuplicateEmailValidator {

  constructor (private usersRepository: UsersRepository) {
  }

  validate (control: AbstractControl): Observable<ValidationErrors> {
    const email = control.value;
    return this.usersRepository
      .emailExists(email)
      .map(result => (result ? { duplicateEmail: true } : null))
    ;
  }

}

【问题讨论】:

标签: angular validation asynchronous rxjs


【解决方案1】:

在研究了一些使用 Observables 提供的解决方案后,我发现它们过于复杂,因此决定使用带有承诺和超时的解决方案。虽然直率,但这个解决方案更容易理解:

import 'rxjs/add/operator/toPromise';

import {AbstractControl, ValidationErrors} from '@angular/forms';
import {Injectable} from '@angular/core';

import {UsersRepository} from '../repositories/users.repository';


@Injectable()
export class DuplicateEmailValidatorFactory {

  debounceTime = 500;


  constructor (private usersRepository: UsersRepository) {
  }

  create () {

    let timer;

    return (control: AbstractControl): Promise<ValidationErrors> => {

      const email = control.value;

      if (timer) {
        clearTimeout(timer);
      }

      return new Promise(resolve => {
        timer = setTimeout(() => {
          return this.usersRepository
            .emailExists(email)
            .map(result => (result ? { duplicateEmail: true } : null))
            .toPromise()
            .then(resolve)
          ;
        }, this.debounceTime);
      });

    }

  }

}

在这里,我使用 RxJS 的 toPromise() 运算符将现有的 observable 转换为 promise。使用工厂函数是因为我们需要为每个控件设置一个单独的计时器。


请考虑这是一种解决方法。非常欢迎其他实际使用 RxJS 的解决方案!

【讨论】:

  • 使用 observables 更容易:Observable.timer(this.debounceTime).switchMap(()=&gt;this.usersRepository.emailExists(email).map(result =&gt; (result ? { duplicateEmail: true } : null))),因为如果值更改,它会被取消订阅,如果新值弹出,则永远不会调用 http 部分...
【解决方案2】:

如果你想使用 RxJs 来实现它,你可以显式地监听 valueChanges 并在其上应用异步验证器。 例如,考虑到您可以参考您的 abstractControl 的引用,

ref.valueChanges.debounceTime(500).subscribe(//value is new value of control
 value=>{this.duplicateValidator.validate(value)//duplicateValidator is ref to validator
                                .then(d => console.log(d))
                                .catch(d=>console.log(d))
        })

【讨论】:

    【解决方案3】:

    虽然@Slava 的回答是正确的。使用 Observable 更容易:

    return (control: AbstractControl): Observable<ValidationErrors> => {
          return Observable.timer(this.debounceTime).switchMap(()=>{
            return this.usersRepository
                .emailExists(control.value)
                .map(result => (result ? { duplicateEmail: true } : null));
          });
    }
    

    使用现代 RxJS 更新:

    return (control: AbstractControl): Observable<ValidationErrors> => {
        return timer(this.debounceTime).pipe(
            switchMap(()=>this.usersRepository.emailExists(control.value)),
            map(result => (result ? { duplicateEmail: true } : null))
        );
    }
    

    注意事项:

    • Angular 会自动取消订阅返回的Observable
    • timer() 带有一个参数只会发出一项
    • 因为timer 只发出一个值,所以我们使用switchMapflatMap 并不重要
    • 您应该考虑使用catchError,以防服务器调用失败
    • 角度文档:async-validation

    【讨论】:

    • 使用此代码时出现错误:“Observable.timer 不是函数”。有什么想法吗?
    • import 'rxjs/add/observable/timer';@Tim
    • 您的代码运行良好。我仍然不确定我是否完全理解它。 “去抖动”运算符不是专门为此设计的,但您没有使用它吗?
    • 你不能使用 debounce 操作符,因为每次一个新的值到达来检查一个新的 observable 被生成并且旧的被取消订阅。
    • 我不明白:如果去抖动不起作用,那switchMap怎么会起作用? switchMap 如何在不同的、未连接的 observables 之间切换?我认为每次Angular调用该函数时,都会创建一个新的计时器和一个新的switchMap。
    【解决方案4】:

    我认为您的方法只是延迟,而不是去抖动,然后找到示例方法来存档此结果。

    import { debounce } from 'lodash';
    
    ...
    
    constructor() {
       this.debounceValidate = debounce(this.debounceValidate.bind(this), 1000);
    }
    
    debounceValidate(control, resolve) {
       ...//your validator
    }
    
    validate (control: AbstractControl): Promise {
      return new Promise(resolve => {
        this.debounceValidate(control, resolve);
      })
    }
    

    【讨论】:

      【解决方案5】:

      更新 RxJS 6.0.0:

      import {of, timer} from 'rxjs';
      import {map, switchMap} from 'rxjs/operators';
      
      
      return (control: AbstractControl): Observable<ValidationErrors> => {
        return timer(500).pipe(
          switchMap(() => {
            if (!control.value) {
              return of(null)
            }
                            
            return this.usersRepository.emailExists(control.value).pipe(
              map(result => (result ? { duplicateEmail: true } : null))
            );
          })
        )
      }

      *RxJS 5.5.0

      对于所有使用 RxJS ^5.5.0 以获得更好的树摇动和可管道操作符的人

      import {of} from 'rxjs/observable/of';
      import {map, switchMap} from 'rxjs/operators';
      import {TimerObservable} from 'rxjs/observable/TimerObservable';
      
      
      return (control: AbstractControl): Observable<ValidationErrors> => {
        return TimerObservable(500).pipe(
          switchMap(() => {
            if (!control.value) {
              return of(null)
            }
                            
            return this.usersRepository.emailExists(control.value).pipe(
              map(result => (result ? { duplicateEmail: true } : null))
            );
          })
        )
      }

      【讨论】:

      • 这是目前最好的解决方案。我已经为此努力了 4 个小时,您的解决方案立即奏效。谢谢
      • 几个小时过去了……我来了。我想在一段时间内这将成为最新版本/答案。谢谢。
      猜你喜欢
      • 2019-03-15
      • 2019-03-23
      • 1970-01-01
      • 2018-08-17
      • 2021-02-20
      • 2020-02-22
      • 2023-03-12
      • 2016-08-23
      相关资源
      最近更新 更多