Angular 记录 - Rxjs 完整处理一个 Http 请求

2,474 阅读2分钟

场景概述


项目中经常有输入框输入的时候,向后台发起请求获取列表或数据。这个简单的业务场景在开发的时候需要考虑以下几点:

  • 对用户输入的内容进行一些校验
  • 控制请求发送的频率【防抖】
  • 当输入框输入长度为空时,恢复页面数据至默认状态
  • 响应用户的键盘动作【如 enter 进行查询,esc 进行清空】
  • 确保返回的数据是根据最后输入的参数进行查询的

代码实现


了解了业务需求后,我们结合 rxjs 操作符来控制 input 框实现上述功能

模板视图 test.component.html 代码如下:

<div class="l-widget-notice-alarmCode">
   <input 
        nz-input 
        placeholder="输入告警编码" 
        #noticeInput
    />
</div>

接下来,我们使用 @ViewChild 属性装饰器,从模板视图中获取匹配的元素后, 通过 fromEvent 将一个该元素上的事件转化为一个Observable:

export class NoticeOverviewComponent implements OnInit, OnDestroy, AfterViewInit  {
    @ViewChild('noticeInput', {static: true}) noticeInput: ElementRef;
    
    // Angular 视图查询在 ngAfterViewInit 钩子函数调用前完成
    ngAfterViewInit() {
        this.bindNoticeInputEvent();
    }
    
    private bindNoticeInputEvent(): void {
        const noticeInputEvent$ = fromEvent(this.noticeInput.nativeElement, 'keyup');
    }
}

接下来,我们通过 Pipe 管道操作符来操作事件流:

export class NoticeOverviewComponent implements OnInit, OnDestroy, AfterViewInit  {
    @ViewChild('noticeInput', {static: true}) noticeInput: ElementRef;
    
    // Angular 视图查询在 ngAfterViewInit 钩子函数调用前完成
    ngAfterViewInit() {
        this.bindNoticeInputEvent();
    }
    
    private bindNoticeInputEvent(): void {
        const noticeInputEvent$ = fromEvent(
            this.noticeInput.nativeElement, 
            'keyup'
        );
        
        noticeInputEvent$.pipe(
            debounceTime(300),
            filter((event: KeyboardEvent) => 
                !(event.keyCode >= 37 && event.keyCode <= 40)
            ),
            pluck('target', 'value'),
        ).subscribe(this.loadData);
    }
    
    public loadData(value: string): void {
        // todo => fetch data
        ...
    }
}

上面的代码中,我们在 pipe 管道中,使用 debounceTime 操作符,舍弃掉在两次输出之间小于指定时间的发出值来完成防抖处理, 通过 filter 操作符过滤符合业务需求的发出值。

最后,我们通过 pluck 操作符来取得发出对象嵌套属性,即 event.value 属性来获取用户的输入值。

由于 Observable 是惰性的,我们需要主动去触发这个函数来获取这个值。 关于 Observable 的介绍可以 参考 Angular - Observable 概述

代码优化


为了避免订阅操作可能会导致的内存泄漏,我们的请求方法还需要做取消订阅的处理。

由于 Observable 也是一种基于发布、订阅模式的推送体系,在某个时间点,我们需要执行取消订阅操作来释放系统的内存。否则,应用程序可能会出现内存泄露的情况。

Observable 订阅之后会返回一个 subscription 对象,通过调用 subscription 的 unsubscribe 方法来取消当前 Observer 的订阅,关于取消订阅,可以使用标准的模式来取消订阅:

export class NoticeOverviewComponent implements OnInit, OnDestroy, AfterViewInit  {
    @ViewChild('noticeInput', {static: true}) noticeInput: ElementRef;
    noticeInputSubscription: Subscription;
    
    // Angular 视图查询在 ngAfterViewInit 钩子函数调用前完成
    ngAfterViewInit() {
        this.bindNoticeInputEvent();
    }
    
    // 通常我们在组件销毁时,去取消订阅。
    OnDestroy() {
        this.noticeInputSubscription.unsubscribe();
    }
    
    private bindNoticeInputEvent(): void {
        const noticeInputEvent$ = fromEvent(
            this.noticeInput.nativeElement, 
            'keyup'
        );
        
        this.noticeInputSubscription = noticeInputEvent$.pipe(
            debounceTime(300),
            filter((event: KeyboardEvent) => 
                !(event.keyCode >= 37 && event.keyCode <= 40)
            ),
            pluck('target', 'value'),
        ).subscribe(this.loadData);
    }
    
    public loadData(value: string): void {
        // todo => fetch data
        ...
    }
}

但是这种做法过于麻烦,且一个 Subscription 对应一个 subscribe。

我们可以通过 使用 takeUntil 操作符来实现 observable 的自动取消订阅:

export class NoticeOverviewComponent implements OnInit, OnDestroy, AfterViewInit  {
    @ViewChild('noticeInput', {static: true}) noticeInput: ElementRef;
    // 创建一个在整个组件中使用的订阅对象 Subject 
    private unsubscribe: Subject<void> = new Subject<void>();
    
    // Angular 视图查询在 ngAfterViewInit 钩子函数调用前完成
    ngAfterViewInit() {
        this.bindNoticeInputEvent();
    }
    
    // 通常我们在组件销毁时,去取消订阅。
    OnDestroy() {
        this.unsubscribe.next();
        this.unsubscribe.complete();
    }
    
    private bindNoticeInputEvent(): void {
        const noticeInputEvent$ = fromEvent(
            this.noticeInput.nativeElement, 
            'keyup'
        );
        noticeInputEvent$.pipe(
            takeUntil(this.unsubscribe),  
            debounceTime(300),
            filter((event: KeyboardEvent) => 
                !(event.keyCode >= 37 && event.keyCode <= 40)
            ),
            pluck('target', 'value'),
        ).subscribe(this.loadData);
    }
    
    public loadData(value: string): void {
        // todo => fetch data
        ...
    }

takeUntil 接受一个 observable ,当接受的 observable 发出值的时候,源 observable 便自动完成了,利用这种机制不仅可以对单个订阅进行取消,整个组件中都可以利用同一个 unsubscribe: Subject<void> 对象来取消订阅,因为我们使用了 Subject,这是一种 多播 的模式

这种机制也是 Angular 中组件销毁时采用的取消订阅模式的基础

温馨提示


大多数时候,我们可以在 Pipe 最上层来设置 takeUntil 来处理订阅,但是在部分 高阶流 中,订阅者所订阅的 observable 可能是由其他流返回,这个过程也是惰性的,因此如果此时在最上方设置 takeUntil 也极有可能导致内容泄漏的问题。

takeUntil 在一些其他场景中,也有可能会引发一些问题,可以通过 配置 rxjs-tslint-rules 中的 rxjs-no-unsafe-takeuntil 规则来确保 takeUntil 的位置放置正确。在这个业务中,我们在最上方设置 takeUntil 就足够了。

感谢您的阅读~