Angular 記錄 - Rxjs 完整處理一個 Http 請求

路馬發表於2020-01-10

Angular 記錄 - Rxjs 完整處理一個 Http 請求

場景概述


專案中經常有輸入框輸入的時候,向後臺發起請求獲取列表或資料。這個簡單的業務場景在開發的時候需要考慮以下幾點:

  • 對使用者輸入的內容進行一些校驗
  • 控制請求傳送的頻率【防抖】
  • 當輸入框輸入長度為空時,恢復頁面資料至預設狀態
  • 響應使用者的鍵盤動作【如 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 就足夠了。

感謝您的閱讀~

Angular 記錄 - Rxjs 完整處理一個 Http 請求

相關文章