背景
在當前angular專案中嘗試使用狀態管理。下面先來了解狀態管理。
什麼是狀態管理
狀態管理是一個十分廣泛的概念,因為狀態無處不在。服務端也有狀態,Spring 等框架會管理狀態,手機 App 也會把資料儲存到手機記憶體裡。
在前端技術中, 狀態管理可以幫助你管理“全域性”狀態 - 那些應用程式的許多部分都需要的狀態。比如元件之間共享的資料、頁面需要及時更新的資料等等。
為什麼需要狀態管理
我們先來看一個 facebook
的例子。
在 Facebook 沒有 Flux 這種狀態管理架構以前,就有很多狀態未同步的 bug:
在 Facebook 逛著逛著,突然來了幾條通知,但是點進去之後竟然沒有了,過了一會兒,通知又來了,點進去看到了之前的訊息,但是新的還是沒有來。
這種問題就跟他們在 Flux 介紹 中所描述的一樣:
為了更好地描述Flux,我們將其與經典的MVC結構比較。在客戶端MVC應用程式中,資料流大概如下
- 使用者互動控制器,
- 控制器更新model,
- 檢視讀取model的新資料,更新並顯示給使用者
但是在多個控制器,模型,父子元件等新增下,依賴變得越來越複雜。
控制器不僅需要更新當前元件,也需要更新父子元件中的共享資料。
如下圖,僅新增三個檢視,一個控制器和一個模型,就已經很難跟蹤依賴圖。
這時,facebook 提出了 Flux架構 ,它的重要思想是:
單一資料來源和單向資料流
單一資料來源要求: 客戶端應用的關鍵資料都要從同一個地方獲取
單向資料流要求:應用內狀態管理的參與者都要按照一條流向來獲取資料和發出動作,不允許雙向交換資料
舉個例子:
一個頁面中渲染了資料列表 books,這個 books 變數是透過 store 獲取的,符合單向資料流。
但是這時候要新增一本書,我們呼叫了一個 api 請求 createBook.
通常有人為了方便,會在元件裡面透過 http 呼叫請求後直接把新增的 book 加到列表裡面。這就違反了單一資料來源和單向資料流
假設這時另一個元件也在渲染資料列表 books, 這時候我們就需要手動更改這個元件中的資料。或者再次請求一次後臺。
當這樣的情況一多,意味著應用內的狀態重新變成七零八落的情況,重歸混沌,同步失效,容易bug從生。
在同一時期,Facebook 的 React 跟 Flux 一起設計出來,React 開始逐漸流行,同時在 Flux 的啟發下,狀態管理也正式走進了眾多開發者的眼前,狀態管理開始規範化地走進了人們的眼前。
狀態模式的模式和庫
Vue與React都有較為簡單的全域性狀態管理策略,比如簡單的store模式,以及第三方狀態管理庫,比如Rudux,Vuex等
Store
最簡單的處理就是把狀態存到一個外部變數裡面,當然也可以是一個全域性變數
var store = {
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
this.state.message = newValue
},
clearMessageAction () {
this.state.message = ''
}
}
但是當你認真觀察上面的程式碼的時候,你會發現:你可以直接修改 store 裡的 state.
萬一元件瞎胡修改,不透過 action,那我們也沒法跟蹤這些修改是怎麼發生的。
所以就需要規定一下,元件不允許直接修改屬於 store 例項的 state,
也就是說,元件裡面應該執行 action 來分發 (dispatch) 事件通知 store 去改變。
這樣進化了一下,一個簡單的 Flux 架構就實現了。
Flux 架構
Flux其實是一種思想,就像MVC,MVVM之類的。
Flux 把一個應用分成四部分:
- View:檢視層
- Action:動作,即資料改變的訊息物件。
- Dispatcher:派發器,接收 Actions ,發給所有的 Store
- Store:資料層,存放應用狀態與更新狀態的方法,一旦發生變動,就提醒 Views 更新頁面
這樣定義之後,我們修改store裡的資料只能透過 Action , 並讓 Dispatcher 進行派發。
可以發現,Flux的最大特點就是資料都是單向流動的。
同時也有一個特點 : 多Store。
Redux
受 Facebook 的 Flux 啟發, Redux 狀態管理庫演變出來,Dan Abramov 與 Andrew Clark於2015年建立。
React 的狀態管理庫中,Redux基本佔據了最主流位置。
它和 Flux 有什麼不同呢?
Redux 相比 Flux 有以下幾個不同的點:
- Redux是 單Store, 整個應用的 State 儲存在單個 Store 的物件樹中。
- 沒有 Dispatcher
Redux 的組成:
- Store
- Action
- Reducer
store
Redux 裡面只有一個 Store,整個應用的資料都在這個大 Store 裡面。
import { createStore } from 'redux';
const store = createStore(fn);
Reducer
Redux 沒有 Dispatcher 的概念,因為 Store 裡面已經整合了 dispatch 方法。
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
那 Reducer 負責什麼呢?
負責計算新的 State
這個計算的過程就叫 Reducer。
它需要兩個引數:
- PreviousState,舊的State
- action,行為
工作流如下
簡單走一下工作流:
1.使用者透過 View 發出 Action:store.dispatch(action);
2.然後 Store 自動呼叫 Reducer,並且傳入兩個引數:當前 State 和收到的 Action。Reducer 會返回新的 State 。
let nextState = xxxReducer(previousState, action);
3.Store 返回新的 State 給元件
// 或者使用訂閱的模式
let newState = store.getState();
component.setState(newState);
總結
Flux | Redux | |
---|---|---|
store | 單store | 多store |
dispatcher | 有 | 無,依賴reducer來替代事件處理器,store和dispatcher合併 |
這兩種模式剛好對應著 angular 匹配的兩種狀態管理庫: @ngxs/store , @ngrx/platform
下面會提到。
其他的模式和庫
還有一些其他的庫, 比如 Vue 用的 Vuex , Mobx等。這裡就不一一介紹了。
Angular 需要狀態管理嗎
如果你是一個 Angular 的開發者,貌似沒有狀態管理框架也可以正常開發,並沒有發現缺什麼東西。那麼在 Angular 中如何管理前端狀態呢?
首先在 Angular 中有個 Service的概念。
Angular 區別於其他框架的一個最大的不同就是:基於 DI 的 service
。也就是,我們基本上,不實用任何狀態管理工具,也能實現類似的功能。
比如,如下程式碼就很簡單地實現資料的管理:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { User } from './type';
export interface UserState {
users: User[];
}
@Injectable({
providedIn: 'root'
})
export class UserStateService {
userState$: Observable<UserState>;
private _userState$ = new BehaviorSubject<UserState>(null);
constructor() {
this.userState$ = this._userState$.asObservable();
}
get userState(): UserState {
return this._userState$.getValue();
}
addUser(user: User): void {
const users = [
...this.userState.users,
user
];
this._userState$.next({
users
})
}
}
這樣的狀態流其實也很清晰,簡單易維護,基本上不需要複雜的狀態管理框架。
假如我們作為大型專案的 angular 開發者
如果我們想引入一個簡單的第三方狀態管理框架統一管理起來呢,應該選什麼庫?
Redux
是React社群提供的。angular 能用,但是總感覺是為了Redux而生的。Vuex
是Vue 特有的狀態管理庫。MobX
在大型專案中,程式碼難以維護
實際上,我們搜尋與 angular 匹配的狀態庫,大多數都是這兩種:
其中, @ngrx/platform 是Angular 的Redux 執行, 也就是說,和 Redux 很像。
而 @ngxs/store 更像是 Flux 。
為了進一步瞭解兩個狀態管理庫,下面我簡單對這兩個管理庫,嘗試寫了一些demo。
@ngrx/platform
NgRx 是在 Angular 中使用 RxJS 的 狀態管理庫,靈感來自 Redux(React 陣營被應用最廣泛的狀態管理工具),官方文件
實際上, Ngrx 比原生的 Redux 概念更多一些,比如 effect等。
工作流如下:
下面我們透過寫一個demo,理解一下工作流。
假設有以下使用場景:
假設我們把 民族 這個欄位單獨拿出來建立一個表,而這個表交給使用者維護,可以點 + 號來彈窗新增民族。
此時我們希望:彈窗新增後的民族,能夠實時的更新到 民族列表 元件中。
示例
1.定義實體
export interface Nation {
id: number;
name: string;
}
2.定義actions
// 定義獲取 nation 列表 的三種行為,獲取,獲取成功,獲取失敗
export const GET_NATIONS = '[Nation] GET_NATIONS';
export const GET_NATIONS_SUCCESS = '[Nation] GET_NATIONS_SUCCESS';
export const GET_NATIONS_ERROR = '[Nation] GET_NATIONS_ERROR';
/**
* get All
*/
export class GetAllNations implements Action {
readonly type = GET_NATIONS;
}
export class GetAllNationsSuccess implements Action {
readonly type = GET_NATIONS_SUCCESS;
constructor(public payload: Nation[]) {
}
}
export class GetAllNationsError implements Action {
readonly type = GET_NATIONS_ERROR;
constructor(public payload: any) {
}
}
3.定義Reducer
當收到 上面定義的 三種 actions 時,透過 swith case, 分別進行不同的處理, 更新State
// 定義 State介面
export interface State {
data: Nation[];
selected: Nation;
loading: boolean;
error: string;
}
// 初始化資料
const initialState: State = {
data: [],
selected: {} as Nation,
loading: false,
error: ''
};
export function reducer(state = initialState, action: AppAction ): State {
switch (action.type) {
case actions.GET_NATIONS:
return {
...state,
loading: true
};
case actions.GET_NATIONS_SUCCESS:
return {
...state,
data: action.payload,
loading: true
};
case actions.GET_NATIONS_ERROR:
return {
...state,
loading: true,
error: action.payload
};
}
return state;
}
/*************************
* SELECTORS
************************/
export const getElementsState = createFeatureSelector<ElementsState>('elements');
const getNationsState = createSelector(getElementsState, (state: ElementsState) => state.nation);
export const getAllNations = createSelector(getNationsState, (state: State) => {
return state.loading ? state.data : [];
}
);
export const getNation = createSelector(getNationsState, (state: State) => {
return state.loading ? state.selected : {};
});
4.定義service
@Injectable()
export class NationService {
protected URL = 'http://localhost:8080/api/nation';
constructor(protected http: HttpClient) {
}
// 獲取列表
public findAll(params?: any): Observable<Nation[]>
return this.http.get<Nation[]>(this.URL, {params});
}
// 新增
public save(data: Nation): Observable<Nation> {
let headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json; charset=utf-8');
return this.http.post<Nation>(this.URL, data, {headers});
}
}
5.定義Effect,用於偵聽 ation, 並呼叫api
@Injectable()
export class NationEffect {
constructor(private actions$: Actions,
private nationsService: NationService) {
}
// 偵聽 GET_NATIONS, 並呼叫獲取列表的service方法
getAllNations$ = createEffect(() => {
return this.actions$.pipe(
tap((data) => console.log(data)),
ofType(actions.GET_NATIONS),
switchMap(() => this.nationsService.findAll()),
map(response => new GetAllNationsSuccess(response)),
catchError((err) => [new GetAllNationsError(err)])
);
});
}
// 偵聽 CREATE_NATION, 並呼叫新增的service方法
createNations$ = createEffect(() => {
return this.actions$.pipe(
ofType(actions.CREATE_NATION),
map((action: AddNation) => action.payload),
switchMap(newNation => this.nationsService.save(newNation)),
map((response) => new AddNationSuccess(response.id)),
catchError((err) => [new AddNationError(err)])
);
});
- 元件使用
@Component({
selector: 'app-index',
templateUrl: './index.component.html',
styleUrls: ['./index.component.css']
})
export class IndexComponent implements OnInit {
nations: Nation[] | undefined;
show = false;
model = {} as Nation;
constructor(private store: Store<ElementsState>) {
}
ngOnInit(): void {
// 初始化,執行 GetAllNations aciton,並分發
this.store.dispatch(new GetAllNations());
// 透過selector,獲取 state中的資料
this.store.select(getAllNations).subscribe((data) => {
this.nations = data;
});
}
save(): void {
if (confirm('確認要新增嗎?')) {
const nation = this.model;
// 執行新增 action
this.store.dispatch(new AddNation(nation));
this.show = false;
this.model = {} as Nation;
}
}
}
全域性的Store只有一個, 那我們怎麼知道去哪個 State中獲取呢?
答案是透過 selector進行選擇。
7.定義selector
/**
* nation的selector, 獲取nation的 state
*/
export const getElementsState = createFeatureSelector<ElementsState>('elements');
const getNationsState = createSelector(getElementsState, (state: ElementsState) => state.nation);
export const getAllNations = createSelector(getNationsState, (state: State) => {
return state.loading ? state.data : [];
}
);
export const getNation = createSelector(getNationsState, (state: State) => {
return state.loading ? state.selected : {};
});
走一遍工作流
1.元件執行 GetAllNations 的action,獲取資料列表
ngOnInit(): void {
this.store.dispatch(new GetAllNations());
}
2.reducer 獲取到 action, 返回 State, 這時候 State剛初始化,資料為空。
switch (action.type) {
case actions.GET_NATIONS:
return {
...state,
loading: true,
};
}
3.同時 Effect 也獲取到該 action
向service請求,獲取後臺資料後,執行 GetAllNationsSuccess action,表示成功獲取到後臺資料。
getAllNations$ = createEffect(() => {
return this.actions$.pipe(
ofType(actions.GET_NATIONS),
switchMap(() => this.nationsService.findAll()),
map(response => new GetAllNationsSuccess(response)),
catchError((err) => [new GetAllNationsError(err)])
);
});
4.reducer 獲取到 GetAllNationsSuccess action
將後臺返回的資料更新到 State中。
case actions.GET_NATIONS_SUCCESS:
return {
...state,
data: action.payload,
loading: true,
};
5.元件利用 selector 獲取 State 中的資料
this.store.select(getAllNations).subscribe((data) => {
this.nations = data;
});
這時候,就完成了對 State 的訂閱,資料的更新能夠及時地收到。
從而實現了單一資料流和單向資料流。
新增操作也是類似的原理,可檢視我寫的 demo: https://stackblitz.com/edit/node-zpcxsq?file=src/app/nation/i...
我的感受
參考了幾個谷歌上的demo寫的。 實際使用起來感覺很臃腫。幾點感覺:
1.對於一種aciton,比如獲取資料列表,需要定義三種aciton, 分別是獲取、獲取成功、獲取失敗。
2.reducer 裡大量使用了switch case, 頁面程式碼很長。
3.需要定義Effect, 資料流多了一層,感覺不便。
4.程式碼需要有一定理解,新手上手會有點懵。
當然也有可能因為我沒有對比多個狀態管理庫。下面我再嘗試一下ngxs/Stroe.
ngxs/Stroe
待更新。。。。
參考:
https://zhuanlan.zhihu.com/p/140073055
https://cloud.tencent.com/developer/article/1815469
http://fluxxor.com/what-is-flux.html
https://zhuanlan.zhihu.com/p/45121775
https://juejin.cn/post/6890909726775885832