使用Angular與TypeScript構建Electron應用(六)

Witt發表於2019-02-16

這一小節我們只做幾件小事,它們在專案整體中顯得微不足道。特別是new-feed中,我們並沒有設計出完善的業務邏輯,現在我們甚至可以結束教程,但我希望能夠藉助這幾個專案中細節實現來傳達整體的構建思想與程式設計思維方式,如果你認為學習它有些困難,可以跳過這些方法轉而使用一些不太優雅的解決方案。

news-feed如今還不能正確的瀏覽文章,至少缺少返回列表與列表翻頁。Angular中它們有很多完全不同的解決方案,你需要爭對不同場景選擇合理的實現方式,下文裡我們嘗試幾種不同的方案來解決這些問題。

實現返回按鈕

元件方式

文章在詳情瀏覽時需要一個返回至列表頁的按鈕,它僅僅只做路由上的回退,換言之,這個按鈕無需接受任何的引數,也不會受環境變化影響。返回按鈕始終是一個固定的功能元件,我們先使用元件的方式完成它:

  1. src/app/shared裡新建component資料夾並建立back元件。

  2. 為元件新增html模板與樣式。

  3. 為元件新增邏輯。

  4. 將back元件連結至shared.module.ts中,並匯出它。

import {Component, OnInit} from `@angular/core`
import {Location} from `@angular/common`

@Component({
    selector: `app-back`,
    templateUrl: `./back.component.html`,
    styleUrls: [`./back.component.scss`]
})
export class BackComponent implements OnInit {

    constructor (
        private location: Location
    ){}

    goBack ():void{
        this.location.back()
    }

    ngOnInit (){
    }

}

現在back元件已經能夠正常工作,但還需要注意一個小問題,在快速點選按鈕時可能觸發多次的goBack ()導致路由返回到上一層或登入頁,我們需要用一些小技巧來解決它:

  1. 命名一個具備閥門功能的布林變數(private returnOnlyOnce: boolean = true),每次執行函式時設定一次變數。這是一個不錯的實現方式。

  2. 或者你可以嘗試在goBack ()函式中傳入一個事件物件,然後利用RxfromEvent來將事件轉化為Observable,訂閱Observable時我們僅僅需要使用動態操作符即可遮蔽連續的點選事件。

指令方式

back元件是共享的,它能夠在任何需要的地方被引入使用,但這又引申出一個值得注意的問題:每當我們需要改動back元件的樣式時,就不得不為元件巢狀一層模板或加入一些新的介面,隨著業務越來越複雜,back元件會俞加臃腫,直到有一天,它看起來和React元件一樣渾身被打滿屬性瘡口。很明顯,這不是我們期望的結果。

簡單的說,你僅僅只需要做一些邏輯/屬性上的改變而模板不會多次複用時,你需要儘量避免元件,轉而使用屬性型指令,這是Angular與React的不同之處。
在介紹屬性型指令之前,我們先看看React最明顯的問題:在構建一個模板與樣式會變化的元件時,React總是需要傳達屬性或style,這樣的程式碼很多時候會顯得臃腫而且富含黏性。簡單的說,它是基於view思考問題的。在Angular中遇到類似問題時我們首先要做的並非建立元件,轉而考慮改變dom的行為,從邏輯上來說,這是行得通而且非常具有形象意義的。

開始建立back指令:

import {Directive, HostListener} from `@angular/core`
import {Location} from `@angular/common`

@Directive({
    selector: `[routeBack]`
})
export class BackDirective {

    constructor (
        private location: Location
    ){
    }

    @HostListener(`click`)
    goBack (){
        this.location.back()
    }

}

@HostListener裝飾器可以標註DOM宿主的動作,它避免我們直接操作DOM元素(如果你想,當然也可以),在編寫Angular程式碼的大部分時間,我們都不必考慮DOM的問題,也很少直接參與DOM事件的登出。
現在,只需要將指令檔案匯入至src/app/shared中並匯出,我們就可以在任何的模板中輕鬆使用它:

<div routeBack>返回</div>

這是一個不錯的開始,讓我們體會到Angular的不同尋常之處,現在開始編寫相對複雜一些的載入列表按鈕。

實現載入列表

使用變數控制

在類似於計數器、時間控制、購物車等等業務邏輯之處,你都可以通過暫存一個變數來解決數量的快取與顯示,所有的操作邏輯被看作Action,用來觸發快取的變化。它們實現起來非常簡單,特別是只需要載入更多的單一翻頁功能時:

  1. 建立一個介面用來繼承或宣告屬性:interface Pagination {page: number}

  2. 建立變數 pagination,初始值為1

  3. 為點選事件繫結函式loadMore (),並在其中發起一次service檔案中的getList,並使變數 pagination+1。

這裡的Pagination介面非常簡陋,但實際業務中肯定遠遠不止這些。翻頁需要考慮到一共有多少頁碼數量,在最後一頁時需要對下一頁或載入更多隱藏,返回上一頁時也需要請求介面,由於列表使用的服務是公共的getList,你可能還要集齊每頁數量、排序方式、篩選條件、搜尋條件等等引數,每一個引數發聲變化時都需要重複計算整個邏輯,並重新請求一次介面,當然還需要對這些引數進行儲存,以便於下一頁繼續使用。
它們太複雜了,特別像電商網站這樣的複雜的篩選引數時,你需要為此付出巨大精力,而且程式碼也未必有足夠的擴充套件性,甚至於其他開發人員也很難理解。面對這類功能時,我們可以嘗試使用一次rxjs。

通過可觀察物件

在使用rxjs之前,有一點值得我們關注:在實現翻頁功能時,我們需要訂閱的並非是來自於翻頁按鈕的事件,而使基於頁碼本身的Observable。頁面中可能有多個位置會觸發翻頁函式,但操作的始終只是隨著時間推進而變化的單個值。即便是在未來,我們需要關注也只是一個稍稍複雜的物件而已。

private pagination:Subject<number> = new Subject<number>()
private paginationSub: Subscription
this.paginationSub = this.pagination
            .filter(page => page > 0)
            .switchMap(page => this.listService.getList(page))
            .subscribe(
                list => this.list.push(...list),
                err => Observable.of<any>([])
            )

Subject是rxjs中一種特殊的Observable,它允許值被多播到多個觀察者。我們可以通過呼叫this.pagination.next(1)為觀察者發射一個新的值,那麼它的觀察者會再次執行filter與switch直到處理訂閱函式。程式碼中的filter用來幫助驗證和過濾一些不合理的值,它在未來會有其他用處,通過switchMap切換至新的流並取消原來流的訂閱。最後我們訂閱的是listService返回的流,並將資料更新至list中。

在未來業務邏輯變化的更復雜時,我們可以為這些列表篩選與排序產生的值建立多個可觀察物件,再使用combineLatest將它們合併起來:

this.paginationSub = Observable.combineLatest(
     this.pagination,
      this.sort,
      this.filter,
            // ...
    )

無論邏輯怎樣複雜,我們始終僅僅只維護這些可觀察物件,在合理的時間為它們發射新的值即可。理所應當的,任何過濾,驗證操作都可以使用rxjs的操作符完成,甚至你可以自己建立一些操作符來過濾、合併這些流。相比於前一種實現方式,rxjs使程式碼具備了高度可讀性與可擴充套件性,這是難能可貴的。

不知道你是否注意到,這段程式碼還存在一個問題,如果我們需要對this.pagination進行維護,如果你不能夠從http服務返回的流中獲取這個值就需要自己維護所謂的當前值。這類情況下我們更需要Subject的一個變種BehaviorSubjectBehaviorSubject儲存著最新發射的值,可以通過getValue()獲取:

private pagination: BehaviorSubject<number> = new BehaviorSubject<number>(1)

// ...

loadMore (nextNumber: number):void{
      this.pagination.next(this.pagination.getValue() + nextNumber)
}

對於排序、篩選或其他任何邏輯都是相同的,未來我們永遠只把注意力放在可觀察物件上,通過少量的高可讀性的程式碼來解決邏輯問題。對於剛剛解除Angular或Rxjs的開發者來說,這需要一些學習時間,可參考 github記錄理解這一節。在下一小節中,我將會預設大家已經掌握這些技能,開始著手完成剩餘的使用者模組。

相關文章