Angular應用架構設計-2:Data Service模式
這是有關Angular應用架構設計系列文章中的一篇,在這個系列當中,我會結合這近兩年中對Angular、Ionic、甚至Vuejs等框架的使用經驗,總結在應用設計和開發過程中遇到的問題、和總結的經驗,來說一下Angular應用的架構設計相關的一些問題,包括像元件設計、元件之間的資料互動與通訊、Ngrx Store的使用、Rxjs的使用與響應式程式設計思想。這些設計思想和方法,不僅適用於Angular,也適用於Vuejs、React等前端框架。
當然,設計沒有一個放之四海皆準的標準,他只能是根據具體情況具體分析。如果大家有更好的想法,歡迎交流。
,我們提到不同的元件直接傳遞資料和事件,我們可以用一個資料service來簡化資料和事件的傳遞。那麼, 如果我們有很多的業務Service,也有很多資料Service,那我們的元件和service的依賴關係會變成生麼樣呢?會這樣:
看到這個圖估計很多人就已經暈了,如果要維護這種設計下的應用,也是一件極其困難的事,哪個資料由誰維護,由誰獲取?一處的資料更新後,有哪些元件的資料需要更新?某個元件的一個資料改變了,到底是誰改的資料。形象一點說就是,這是誰的乳酪?我動了誰的乳酪?我的乳酪在哪兒?誰動了我的乳酪?
單向的資料流和事件流
要解決上面的複雜依賴的問題,我們先來看一下答案,然後再一步步分析為何這樣做以及如何實現。這個答案就是:單向的資料流和事件處理。這個答案其實也回答了哲學上的三個終極問題:
- 我是誰?
如果我是顯示元件?那我只能從我從上級那裡獲得資料,展示,如果需要執行什麼操作,就把要操作的事件傳送某個地方。我不能隨意篡改資料,也不能執行操作。
如果我是功能元件?那我就負責獲取資料,把資料傳遞給我下邊的顯示元件;如果要執行操作,那就由我來呼叫。 - 從哪裡來?
顯示元件的資料都來自上級;功能元件的資料都來自業務Service。 - 到哪裡去?
顯示元件會把要做的事情,也就是事件發給功能元件;功能元件透過呼叫業務Service來處理這個事件。
根據這個原則,我們可以試著把元件、service之間的關係定成這樣:
但是這樣的話,我們的顯示元件就會依賴業務服務,從業務服務獲取資料,這違背了之前說的顯示元件的規範。雖然說這種實現方式在某些情況下可能會比較方便,但是這樣就很難實現顯示元件的重用,而且很多列表顯示的元件,它的資料都是從父元件中獲得,我們不可能再在顯示元件裡再重新獲取資料。而且,當元件和服務之間的依賴越來越密切的時候,就違背了松耦合的開發原則,這會導致可維護性越來越差。
所以,我們稍微改進一下,用這樣方式實現:
在這種實現方式下,我們的顯示元件只依賴資料Service,而功能元件依賴資料Service去獲取更新事件,然後再依賴業務Service去處理事件、獲取資料。那麼上面的雜亂的元件圖就可以最佳化成這樣:
顯然,service和元件之間的耦合度還是太高,我們可以在data service裡面去掉用業務service去讀寫資料,這樣就能進一步減少元件和服務之間的耦合度。那麼元件和服務之間的資料、事件流就是這樣:
最終,我們的資料是從上往下的,也就是從根元件、功能元件一級一級傳遞到顯示元件。而事件的處理是自下而上的,顯示元件將事件以Data Service為通道發給功能元件。這就是單向的資料流,和單向的事件流。
可訂閱的資料服務
我們已經定義了我們的資料服務(data service)的功能,和它跟顯示元件、功能元件的互動方式,那麼我們怎麼保證這個資料流是單向的呢?在Angular中,元件中的資料繫結,可以使用單向繫結,也可以使用雙向繫結,我們為了實現資料的單向的流,就不能使用雙向繫結。單向繫結有很多好處,最大的好處就是減少資料的異常修改,從而也減少資料的修改檢查而得到效能提升。所以,我們不但要從元件、服務的設計上保證資料流的單向,也要用。這樣,我們的資料的修改就只能由data service 呼叫業務service來修改,資料一旦完成,那麼頁面的狀態也確定了。
既然這個資料是單向的,我的功能元件怎麼知道有新事件呢?處理完事件以後,怎麼知道這個資料已經更新了呢?這就要使用Rxjs了。在Angular中,大量使用了Rxjs,例如Http服務返回的結果是Observable
的,Angular中觸發事件的EventEmitter
是一個Subject
。所有這些都是可訂閱的,訂閱以後,就可以在有新資料的時候觸發訂閱方法。例如在上一篇文章中使用的簡單的Data Service:
@Injectable()
export class ProductSelectedService {
private _selected: BehaviorSubjectProduct> = new BehaviorSubject(null);
public selected$ = this._selected.asObservable();
select(product: Product) {
this._selected.next(product);
}
}
使用者每次選了一個商品的時候,就呼叫這個service的select()
方法,它會往裡面的Subject
物件寫一個新資料,然後在功能元件裡面訂閱這個物件:
this.productSelectedService.selected$.subscribe(product => this.selectProduct(product));
每當使用者選了一個商品後,這個subscribe
裡面的方法會被觸發。
所以,透過這種可訂閱的資料物件,我們的Data Service不需要反向的去檢查顯示元件的資料是否更改,功能元件也不需要回頭去Data Service去拿資料。因為所有的資料都是訂閱的。
不可變資料
有關單向事件流還有一個需要注意的就是,。舉個例子,還是京東的購物車,使用者在頁面上選了一個商品,如果在商品物件裡有一個欄位是selected
,代表是否勾選,如果我們在業務service裡直接修改了這個值,那麼在頁面上就會直接顯示相應的狀態。但是我們一直強調,資料的修改應該是在業務service修改了以後,由功能元件訂閱得到更新的資料,再傳遞給顯示元件。如果我們使用可變的資料物件,就會破壞單向事件流的規定,導致我們的資料沒法統一管理。
使用不可變資料,能夠規範我們的事件處理,就不會出現同一個資料在多個地方被使用和修改,從而能避免很多潛在的bug。更重要的是,使用不可變資料可以極大的改善應用的效能。因為,一個資料物件,它的內部資料不會被修改,如果要修改,只能新建一個物件,把原先的資料(或把原先的物件指標)複製過去,那麼Angular在檢查繫結的資料是否更改的時候,是需要看這個引用值是否變了,而不用檢查裡面的資料。
如果我們使用的都是不可變資料,那我們就可以在定義元件的時候,新增一個OnPush
配置:
@Component({
selector: 'CartItemComponent',
changeDetection: ChangeDetectionStrategy.OnPush,
template: ...
這樣就能減少很多檢查資料修改所帶來的開銷,從而提升效能。特別是資料物件越大,它帶來的效能提升越明顯。還有在ngfor
這樣的迴圈裡,也能減少很多迴圈遍歷的次數。如果使用了OnPush
,就只會遍歷一次,來顯示迴圈裡面的內容。如果沒有使用OnPush
,除了第一次遍歷顯示以外,還會再遍歷2,3次,來判斷裡面的資料是否修改。
可訂閱資料要注意的問題
當我們在Angular中使用Rxjs的Observable
的訂閱型別資料時,在設計上也有一些需要注意的地方。
模板中的重複訂閱
我們可以直接在模板中使用Observable
的資料,Angular框架會幫我們建立一個對這個資料的訂閱,並在頁面上繫結這個訂閱的資料。假設有一個訂單頁面,我們這樣使用:
div class="order-detail">
div>{{ (orderDetail$ | async)?.createdDate}}div>
div>{{ (orderDetail$ | async)?.status}}div>
div>{{ (orderDetail$ | async)?.product}}div>
div *ngFor="let prod of (orderDetail$ | async)?.products">商品列表div>
div>
在這個頁面對應的component裡,有一個變數orderDetail$,是一個Observable
的資料,是用http
服務從伺服器段返回訂單詳情的結果的訂閱。
orderDetail$ = this.orderService.getDetail(theId)
| async
是一個管道,他會對一個Observable
或Promise
物件進行訂閱,並返回最新的值,如果Observable
有新的值,就會更新改值,並在這個元件被銷燬的時候取消訂閱。但是,這個模板裡面多次使用| async
就會對這個可訂閱物件進行多次訂閱,而每次訂閱就會呼叫一下它的sunscribe()
方法。那麼對於上面的用法, getDetail
方法會被呼叫多次。
如果因為某些原因無法避免重複訂閱造成的重複呼叫,我們可以使用shareReplay
運算子,他就像一個cache一樣,第二次呼叫的時候就會從cache中返回值。
元件中訂閱時的取消訂閱問題
為了解決上面的問題,我們可以在組建中自行訂閱,並將訂閱後的值複製到元件中的變數中,並在模板中繫結這個變數進行顯示:
@Component({...})
export class OrderDetailComponent implements OnInit {
orderDetail: OrderDetail;
ngOnInit() {
this.orderService.getDetail(theId).subscribe(data => this.orderDetail = data)
}
}
但是,我們就必須在元件的ngOnDestroy
方法裡面去取消訂閱,Angular不會幫我們自動取消訂閱。這樣在元件銷燬的時候,由於這個訂閱還在,就會發生記憶體洩漏。也就是因為元件被銷燬,但是裡面的訂閱的引用還在被使用,就不會被銷燬。而且訂閱方法也會在有新資料的時候執行。
所以在使用這種方式的時候,一定要自己在銷燬方法裡面取消訂閱。
使用async as簡化
針對上述兩個問題,我們可以透過透過async as
來解決:
div *ngIf="orderDetail$ | async as orderDetail; else isLoading" class="order-detail">
div>{{ orderDetail?.createdDate}}div>
div>{{ orderDetail?.status}}div>
div>{{ orderDetail?.product}}div>
div *ngFor="let prod of orderDetail?.products">商品列表:{{prod.name}}div>
div>
ng-template #isLoading>
div>正在載入...div>
ng-template>
這樣既能解決訂閱的問題,也能解決自動取消訂閱的問題,而且還能在這個Observable
正在非同步獲取資料的時候,在模板上顯示正在載入的提示。
總結
所以,在這種模式下,我們使用可訂閱的、不可修改的資料物件,實現單向的資料流和事件流,它有諸多好處:
- 實現元件之間、元件和服務之間的解耦,讓系統容易維護、容易擴充套件。當我們的應用越來越大、越來越複雜,這個好處就會越發明顯。
- 使得應用更容易測試。由於頁面展示完全由data service裡面的資料確定,我們要測試各種業務邏輯,只需要測試我們的data service,也就是呼叫方法、檢查結果。由於不牽扯到頁面,測試用例就很容易編寫,執行效率也高。
- data service還能用做cache,這樣可以根據情況來判斷是要重新獲取資料,還是直接使用cache的資料,這樣就能減少很多無謂的資料請求。
- 使用可訂閱的資料,也可以有多個訂閱者,就很容易實現針對一個資料的多個響應和更新,或者是多個地方修改同一個資料。這樣就能很方便的實現複雜的頁面互動情況下的資料響應和更新。
歡迎關注課程
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1978/viewspace-2814072/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 總結 - 設計模式,企業應用架構模式,架構模式設計模式應用架構
- Angular應用架構設計-5:設計原則與總結Angular應用架構
- Unity應用架構設計(1)—— MVVM 模式的設計和實施(Part 2)Unity應用架構MVVM模式
- MVP應用架構模式MVP應用架構模式
- 應用架構圖的設計應用架構
- React應用架構設計指南React應用架構
- SaaS架構:應用服務、應用結構設計架構
- 架構設計思想-微服務架構設計模式架構微服務設計模式
- Unity應用架構設計(1)—— MVVM 模式的設計和實施(Part 1)Unity應用架構MVVM模式
- iOS應用架構談:架構設計的方法論iOS應用架構
- Unity應用架構設計(2)——使用中介者模式解耦ViewModel之間通訊Unity應用架構模式解耦View
- iOS應用架構談(一):架構設計的方法論iOS應用架構
- .NET應用架構設計—重新認識分層架構(現代企業級應用分層架構核心設計要素)應用架構
- 《Java應用架構設計:模組化模式與OSGi》讀書筆記Java應用架構模式筆記
- 用 VIPER 構建 iOS 應用架構(2)iOS應用架構
- React Native App應用架構設計React NativeAPP應用架構
- 論軟體架構設計及應用架構
- 百萬年薪架構師之路:談應用系統架構設計架構
- 架構師之路—理解設計模式架構設計模式
- 一文搞懂SaaS應用架構:應用服務、應用結構、應用互動設計應用架構
- 【Apollo】(2)--- Apollo架構設計架構
- 設計模式應用之Observer模式(2)設計模式Server
- 軟體架構設計原則和模式(上):分層架構設計架構模式
- .NET應用架構設計—表模組模式與事務指令碼模式的程式碼編寫應用架構模式指令碼
- .NET應用架構設計—適當使用活動記錄模式代替領域模型模式應用架構模式模型
- FMEA在架構設計中的應用分析架構
- Unity應用架構設計oC工廠理念先行Unity應用架構
- 貨拉拉應用多分組架構設計方案分享架構
- MicrosoftNet企業級應用架構設計(中)ROS應用架構
- 利用WMRouter 重新架構設計業務模式架構模式
- 軟體架構設計模式大全 - vikipediaaaa架構設計模式
- Unity應用架構設計(6)——設計動態資料集合ObservableListUnity應用架構
- 設計Android應用程式架構的基本指南:MVP:第2部分Android架構MVP
- 使用 Angular 打造微前端架構的 ToB 企業級應用Angular前端架構
- 【譯】開發大型 Angular 應用的12條架構清單Angular架構
- Unity應用架構設計(9)——構建統一的 RepositoryUnity應用架構
- 企業應用架構的基本模式之入口模式應用架構模式
- Unity應用架構設計(12)——AOP思想的實踐Unity應用架構