Angular 真的需要狀態管理麼

前端在過去很多年壓根就沒有聽說過狀態管理這東西,即使在 Angular.js 火熱的那幾年也很少有人談前端的狀態管理,直到 React 的出現,各種狀態管理框架 Flux,Redux,Mobx, ... 層出不窮,讓人眼花繚亂。如果你是一個 Angular 的開發者,貌似沒有狀態管理框架也可以正常的組件化開發,並沒有發現缺什麼東西。那麼在 Angular 中如何優雅的管理前端狀態呢?

首先前端組件化開發已經變成了標準,對於他的好處和概念網上有很多文章介紹,目前三大框架都是遵循組件化開發的思想,而且組件之間的通信基本都是單向數據流,就是說父組件通過屬性綁定把數據傳遞給子組件,子組件想要修改傳入的數據必須通過事件回調和父組件通信,React 中如果組件的層級比較深,同時父組件與很遠的一個子組件之間需要共享數據,那就意味著數據會從父組件一層層往下傳遞,如果底層的組件需要修改數據,必須通過事件層層返回,這對於開發來說基本是災難,代碼變得難以維護,記得聽說過一句很有哲學的話:任何解決不了的問題都可以引入一個第三方去解決,沒錯, 引入一個第三方存放維護這些狀態,組件直接讀取第三方把需要的狀態展示在視圖上,那麼怎麼樣合理的設計這個第三方呢,那麼 Flux,Redux,Mobx 這些狀態管理類庫基本都是所謂的第三方。

那麼在 Angular 中為啥不是必須要狀態管理框架呢?

首先在 Angular 中有個 Service的概念,雖然 Angular 對於 Service 基本上什麼都沒有做,連一個基類 BaseService 都沒有提供,但是以下2個特性決定了在 Angular 中會很輕鬆的通過 Service 實現一個上述的第三方。

  1. Angular 中定義了一個 Service 後可以通過依賴注入很輕鬆的把這個服務注入到組件中,這樣組件就可以調用 Service 提供的各種方法;
  2. 我們可以把組件需要的狀態數據存儲在 Service 中,然後把注入的 Service 設成 public,這樣在模版中可以直接通過表達式綁定 Service 中的數據 。

基於以上 2 個特性,基本上在使用 Angular 開發應用時一旦遇到組件之間共享數據,都可以使用 Service 輕鬆應對(當然做一個 SPA 單頁應用,即使組件之間沒有共享數據,也建議 使用 Service 作為數據層,統一維護業務邏輯),官方提供的英雄編輯器示例 MessageService,就是直接公開服務在組件模版上綁定的,代碼如下,所以 Angular 不像 React 那樣必須完全依賴狀態管理框架才可以做組件之間的數據共享。:

export class MessageService {
messages: string[] = [];

add(message: string) {
this.messages.push(message);
}

clear() {
this.messages = [];
}
}
@Component({
selector: 'app-messages',
template: `

Messages



{{message}}


`
})
export class AppMessagesComponent implements OnInit {

constructor(public messageService: MessageService) { }

ngOnInit() {
}
}

那麼在 Angular 中使用 Service 做狀態管理會遇到哪些問題呢,如果只是很簡單的狀態通過 Service 直接管理肯定沒有任何問題,但是一旦 Service 存儲的狀態與每個組件需要展示的狀態不一致就很難處理了。比如下圖是我們經常遇到的場景,首先項目中會有很多自定義的視圖,默認只展示 2 個視圖,其餘的視圖在更多視圖中。

Angular 真的需要狀態管理麼

我們可以很簡單把所有的視圖列表存放在 ViewService中, 針對視圖的增刪改邏輯都移動到 ViewService中, 偽代碼如下,但是有個問題就是導航條組件和更多視圖組件兩個組件展示的視圖數據不一樣,需要把視圖列表進行分割,導航條只展示 2 個視圖,其餘的在更多視圖中。

class ViewService {
views: ViewInfo[];
addView(view: ViewInfo) {
// 調用 API
this.views.push(view);
}
updateView(view: ViewInfo) {
}
removeView(view: ViewInfo) {
}
}

此時要想解決這個問題怎麼辦?我能想到快速解決的有兩種方式

  1. 在 ViewService 除了存儲所有的 views 外單獨存儲導航條的 2 個視圖 toolbarShowViews 和更多視圖 moreViews,這麼做的缺點就是每次增刪改視圖後都需要重新計算這2個數組,Service 中的狀態會增多,如果有一天需求變了,所有的視圖直接顯示,顯示不下換行,那還得回過頭來修改 ViewSevice 中的代碼,這本來是應該是導航條和更多視圖組件的狀態,現在必須和全局的視圖狀態放在了一起,雖然可以解決問題,但是不完美;
  2. 還有一種更噁心的做法就是在導航條組件模版上循環所有視圖,根據 index 只取前 2 個展示,更多組件模版循環所有視圖只展示後面的視圖,這種做法缺點是把邏輯代碼放到了視圖中,如果有更復雜的場景通過模版表達式未必可以做到,其二是循環了一些不需要的數據或許在某些場景下有性能損耗,至於示例中的那幾個視圖肯定沒有性能問題。

那麼除了上述 2 中解決方式外還有更優雅更好的方式麼?答案就是 Observable( 可被訂閱的對象) ,當然 Angular 框架本身就是依賴 RxJS 的,官方提供的 HttpClient Router 提供的 API 返回的都是 Observable對象。

回到這個例子上來,我們可以把 ViewService 中的 views 改成BehaviorSubject,BehaviorSubject 對象既可以被訂閱,又可以廣播,同時還可以存儲最後一次的數據, 操作數據後通過 views$.next(newViews) 廣播出去,然後在導航條組件中訂閱 views$ 流只取前 2 個視圖,更多視圖菜單組件訂閱取後面的視圖,如果還有其他組件顯示所有的視圖可以直接訂閱視圖列表流 viewService.views$ | async 顯示所有視圖。

class ViewService {
views$ = new BehaviorSubject([]);
addView(view: ViewInfo) {
// 調用 API
const views = this.views$.getValue();

this.views$.next([...views, view]);
}
updateView(view: ViewInfo) {
}
removeView(view: ViewInfo) {
}
}
// component.ts
this.showViews$ = this.viewService.views$.pipe(
map((views) => {
return views.filter((view, index) => {
return index < 2;
});
})
);

所以在 Angular 中把狀態通過 BehaviorSubject保存在服務中,其他組件通過訂閱服務中的數據流可以處理各種複雜的場景,這樣的狀態流非常的清晰,簡單易維護,基本上不需要複雜的狀態管理框架。

其實前端狀態管理本質上處理無外乎只有 2 種方式;

  1. 不可變數據(類 Redux),函數式編程的一個特點;
  2. 響應式編程 Observable 。

通過 Service 去管理前端的狀態,需要共享的數據使用 Observable 足夠應付大部分應用場景。但是通過我們這麼長時間的實踐,我認為會有以下幾個問題:

  1. Service 比較靈活,可以存放普通的數據,也可以存放 Observable 對象, 一般建議 Service 做數據層,所有修改操作都要通過 Service 封裝的方法,但是數據是公開出去的,難免會不輕易間就在組件中直接操作 Service 中的數據了;什麼時候使用 Observable對象,
  2. 什麼時候用普通的數據對象,對開發人員來說不好把控,而且可能需求本來是不需要訂閱的,後來變了,就需要訂閱了,那就需要改很多地方。

上述的 2 個問題可能不是 Angular 的問題,但是怎麼樣通過引入一個簡單的狀態管理框架統一管理起來呢,同時讓開發人員更容易寫出一致的代碼,而且不容易出錯。

最近比較火的 mobx 它是通過裝飾器設置某個屬性是否是 Observable 的,這樣之後修改只需要加 @observable 就可以了,同時它提供了 @computed 計算屬性實現上面的更多視圖的問題,mobx 解決狀態管理的思路走的是 Observable,和在 Angular 中寫的那個 Service 解決思路類似,但是在 Angular 中我建議不要使用 mobox,原因如下 :

  1. mobox 還是有點複雜,概念比較多;
  2. 自己實現的 Observable,對於 Angular 應用來說有點多餘,和 Angular 配合總有點彆扭;
  3. 處理同步和異步的 Action 比較繁瑣。

其實和 Angular 匹配的狀態庫不多,你搜索下可能只能看到下面 2 個(雖然 Redux ,mobox 和框架無關,但是總感覺他們就是為 React 而生的):

  1. ngrx/platform 這個基本上是把 Redux 強行搬到 Angular 中,本來 Redux 就被吐槽不好用,看到各種 Switch 就高興不起來,並且繁瑣,寫起來費勁;
  2. ngxs/store 這個框架其實就是使用 RxJS 管理狀態,感覺比 ngrx 好用,使用裝飾器定義 State 和 Action,組件通過 store.dispatch(new AddTodo('title')) 調用對應的 Action 方法 , 充分利用了 Angular 和 TypeScript 的特質,推薦使用。

我們一開始是想選擇 ngxs/store 的,但是後來放棄了,放棄的原因如下:

  1. 它是單 Store 的,關於單 Store 和 多 Store 到底哪個好,仁者見仁智者見智,我覺得多 Store 更符合前端的場景,首先,單一 Store,意味著所有的操作都通過 Store.dispatch 觸發 Action,然後就會通過其他的方式分模塊處理不同的狀態,Redux 通過 Reducer 函數去處理不同的狀態,ngxs/store 通過定多個 State 類處理各種 Action,如果是多 Store,那就意味著 Store 的劃分就是按照業務模塊來的,小項目你可以把所有的狀態和操作 Action 都放入一個 Store,多餘複雜項目可以放在更多的 Store 去管理,完全交給用戶自己控制,另外一個就是我覺得狀態存在哪裡,操作狀態的 Action 應該和存儲的狀態放在一起,否則我要去多個地方去找,因為 Action 的操作就是操作狀態的;
  2. 既然是單 Store ,所有的操作都通過 Store.dispatch 觸發 Action,那麼這個 dispatch 函數方法就沒有類型檢查,你寫錯了也只能運行時通過調試得知,無法利用 TypeScript 的靜態類型檢查發現低級錯誤,當然 ngxs/store 比 Redux 會先進一點,它把 Action 的 type 和 payload 定義在一起,然後調用 dispatch 的時候示例化 Action 類做到類型檢查,但是定義 Action 的時候還是需要指定這個 Action 和 payload 參數一致,比如:
export class AddTodo {
static type = 'AddTodo';
constructor(public readonly payload: string) {}
}
@State({
name: 'todo',
defaults: []
})
export class TodoState {
@Selector()
static pandas(state: string[]) {
return state.filter(s => s.indexOf('panda') > -1);
}
@Action(AddTodo)
addTodo({ getState, setState }: StateContext, { payload }: AddTodo) {
setState([...getState(), payload]);
}
}

單 Store 還會帶來另外一個問題就是還需要統一管理所有的 Action,Action 類型不能重複,大型項目很多模塊還需要統一規劃 Actions。當然單 Store 也有它的優勢,可以循環調用其他 State 的 Action,統一使用 dispacth(action) 等等。

那麼在 Angular 中我們需要的東西其實和 ngxs/store 類似的理念,去除單 Store,換成多 Store 即可,所以我們內部自己封裝了一個超級簡單的狀態管理類庫。

Angular 真的需要狀態管理麼

  1. 每個 Store 對應一個狀態對象,狀態以 BehaviorSubject 的形式存在 Store 中;
  2. 每個 Store除了定義對應的狀態外還會定義各種 Action, Action 就是 Store 中的一個普通方法,通過裝飾器 @Action包裝一下即可,對於同步和異步沒有區分,異步的返回一個 Observable即可 ;
  3. 組件注入對應的 Store, 通過 Store 封裝的 select 方法訂閱當前組件需要的狀態,當然可以通過 store.snapshot獲取當前的狀態快照;
  4. 組件直接調用 Store 對應的 Action 進行狀態的增刪改。

上週末從我們的組件庫中提取出來,獨立成一個簡單的 Angular 狀態管理類庫 ngx-mini-store ,當然還有很多需要完善的地方。

// counter-store
import { Store, Action } from 'ngx-mini-store';
interface CounterStoreState {
count: number;
}
export class CounterStore extends Store {
constructor() {
super({
count: 0
});
}
@Action()
increase() {
this.snapshot.count++;
this.next(this.snapshot);
}
@Action()

decrement() {
this.snapshot.count--;
this.next(this.snapshot);
}
}
// counter-component.ts
import { CounterStore } from '../counter-store';
@Component({
selector: 'app-tasks',
templateUrl: './counter.component.html'
})
export class CounterComponent implements OnInit, OnDestroy {
count$: Observable;
constructor(public store: TaskListStore) {
this.count$ = this.store.select((state) => {
return state.count;
});
}
ngOnInit(): void {
}
increase() {
this.store.increase();
}
}

最後總結一下,在 Angular 中推薦使用 Service(或者 Store,本質上也是個服務)來做數據層的管理,那麼全局狀態或者組件之間共享的狀態存儲在 Service 中,使用 Observable 存儲數據是個推薦的方式,基於這個基礎上,你可以按照自己的喜歡封裝這一層實現狀態管理。我們按照這種方式做了之後,偶然發現 github 上也有一個項目 https://github.com/SebastianM/tinystate 和我們解決的思路一致,希望這篇文章可以給你帶來 Angular 狀態管理的一些思考。

https://zhuanlan.zhihu.com/p/45121775


分享到:


相關文章: