1. 程式人生 > >手把手教你用ngrx管理Angular狀態

手把手教你用ngrx管理Angular狀態

cli emit spl 工作 準備就緒 優雅 spa 現在 改字體

本文將與你一起探討如何用不可變數據儲存的方式進行Angular應用的狀態管理 :ngrx/store——Angular的響應式Redux。本文將會完成一個小型簡單的Angular應用,最終代碼可以在這裏下載。

Angular應用中的狀態管理

近幾年,大型復雜Angular/AngularJS項目的狀態管理一直是個讓人頭疼的問題。在AngularJS(1.x版本)中,狀態管理通常由服務,事件,$rootScope混合處理。在Angular中(2+版本),組件通信讓狀態管理變得清晰一些,但還是有點復雜,根據數據流向不同會用到很多方法。

註意:本文中,AngularJS特指1.x版本,Angular對應2.0版本及其以上。

有人用Redux來管理AngularJS或者Angular的狀態。Redux是JavaScript應用的可預測狀態容器,支持單一不可變數據儲存。Redux最有名的就是結合React的使用,當然它可以用於任意的視圖層框架。Egghead.io發布了一份非常優質的Redux免費視頻教程,視頻由Redux作者Dan Abramov本人講解。

初識ngrx/store

本文將采用ngrx/store管理我們的Angular應用。那麽,ngrx/store和Redux什麽關系呢?為什麽不用Redux呢?

與Redux的關系

ngrx/store的靈感來源於Redux,是一款集成RxJS的Angular狀態管理庫,由Angular的布道者Rob Wormald開發。它和Redux的核心思想相同,但使用RxJS實現觀察者模式。它遵循Redux核心原則,但專門為Angular而設計。

ngrx/store中的基本原則

  • State(狀態) 是指單一不可變數據

  • Action(行為) 描述狀態的變化

  • Reducer(歸約器/歸約函數) 根據先前狀態以及當前行為來計算出新的狀態

  • 狀態用State的可觀察對象,Action的觀察者——Store來訪問

我們會詳細解釋說明。先快速過一遍基礎,然後在實戰的過程中慢慢深入解釋。

Actions(行為)

Actions是信息的載體,它發送數據到reducer,然後reducer更新store。Actions是store能接受數據的唯一方式。

在ngrx/store裏,Action的接口是這樣的:

// actions包括行為類型和對應的數據載體
export interface Action {
type: string;
payload?: any;
}

type描述我們期待的狀態變化類型。比如,添加待辦‘ADD_TODO‘,增加‘DECREMENT‘等。payload是發送到待更新store中的數據。store派發action的代碼類似如下:

// 派發action,從而更新store
store.dispatch({
type: ‘ADD_TODO‘,
payload: ‘Buy milk‘
});

Reducers(歸約器)

Reducers規定了行為對應的具體狀態變化。它是純函數,通過接收前一個狀態和派發行為返回新對象作為下一個狀態的方式來改變狀態,新對象通常用Object.assign和擴展語法來實現。

// reducer定義了action被派發時state的具體改變方式
export const todoReducer = (state = [], action) => {
switch(action.type) {
case ‘ADD_TODO‘:
return [...state, action.payload];
default:
return state;
}
}

開發時特別要註意函數的純性。因為純函數:

  • 不會改變它作用域外的狀態

  • 輸出只決定於輸入

  • 相同輸入,總是得到相同輸出

關於函數的純性,可以點擊這裏進一步了解。開發時,要確保函數的純性和狀態不可變性,所以寫reducers的時候要多加小心。

Store(存儲)

store中儲存了應用中所有的不可變狀態。ngrx/store中的store是RxJS狀態的可觀察對象,以及行為的觀察者。

我們可以利用Store來派發行為。當然,我們也可以用Store的select()方法獲取可觀察對象,然後訂閱觀察,在狀態變化之後做出反應。

ngrx/store實戰:個性寵物標簽

目前我們熟悉了ngrx/store的基本工作原理,接下來我們來開發一個能讓用戶自定義寵物名稱標簽的應用。該應用將會有以下功能:

  • 用戶可以選擇標簽形狀,字體,文案,以及附加特性

  • 創建過程可以預覽標簽效果

  • 完成後,可以繼續創建

我們需要創建幾個組件來組合成標簽生成器和標簽預覽,還會添加登錄,創建標簽,完成創建的組件和路由。這個小應用的狀態將會用ngrx/store來管理。

完成後的個性寵物標簽app效果如下:

技術分享

讓我們開始吧!

Angular應用設置

安裝依賴

確保你已經安裝了NodeJS,推薦LTS版本。

用npm安裝Angular CLI包,方便一鍵生成項目手腳架。運行以下命令來全局安裝angular-cli。

$ npm install -g @angular/cli

創建項目

選好項目所在的文件夾,打開命令行,輸入以下命令來創建一個新的Angular 項目:

$ ng new pet-tags-ngrx

進入新創建的文件夾,安裝必要的包:

$ cd pet-tags-ngrx
$ npm install @ngrx/core @ngrx/store --save

一切準備就緒,可以開始開發了。

定制你的項目模板

讓我們根據這個項目的需求,稍微改造一下項目模板。

創建src/app/core文件夾

首先,創建文件夾src/app/core。應用的根組件和核心文件都會放在這個文件夾下。將所有的app.component.*文件移動到這裏。

註意:簡潔起見,該教程不會包含測試。我們會忽略所有的*.spec.ts文件。如果你想寫測試的話,可以自己寫。所以,文中不會再提到這些文件。同樣,出於清晰簡潔性的考慮,github倉庫的最終版代碼刪除了所有測試相關的文件。

更新App模塊

接著,打開src/app/app.module.ts文件,更新app.component 的路徑:

// src/app/app.module.ts
...
import { AppComponent } from ‘./core/app.component‘;
...

靜態資源整理

定位到src/assets文件夾。

在assets文件夾下新建一個images的文件夾,稍後我們會添加一些圖片。然後,將根目錄下的src/styles.css移動到src/assets下。

styles.css的移動需要我們修改.angular-cli.json的配置。打開這個文件,把styles屬性改成如下:

// .angular-cli.json
...
"styles": [
"assets/styles.css"
],
...

集成Bootstrap

最後,在index.html中添加Bootstrap樣式。在<link>標簽上加上CDN地址。這裏我們只用到樣式,不需要腳本文件。順便,更新一下標題,變成Custom Pet Tags:

<!-- index.html -->
...
<title>Custom Pet Tags</title>
...
<!-- Bootstrap CDN -->
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css"
integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ"
crossorigin="anonymous">
</head>

啟動服務

我們可以在本地起個服務,然後監聽文件變化實時更新:

$ ng serve

在瀏覽器中輸入http://localhost:4200,程序成功運行。

技術分享

App組件

現在開始創建新功能。從根組件app.component.*入手。不要擔心,變化很小。

刪除樣式文件

刪除app.component.css文件。該組件只用Bootstrap來定義樣式,所以不需要額外樣式。

根組件腳本

在app.component.ts文件中刪除對上述樣式文件的引用。我們也可以刪除AppComponent類中的title 屬性。

// src/app/core/app.component.ts
import { Component } from [email protected]/core‘;

@Component({
selector: ‘app-root‘,
templateUrl: ‘./app.component.html‘
})
export class AppComponent {

}

根組件模版

在app.component.html中添加一些內容,變成如下:

<!-- src/app/core/app.component.html -->
<div class="container">
<div class="row">
<div class="col-sm-12">
<h1 class="text-center">Custom Pet Tags</h1>
</div>
</div>
<router-outlet></router-outlet>
</div>

我們用Bootstrap來添加珊格系統和標題。然後添加一個<router-outlet>指令,這是當這個單頁面應用中添加路由後,視圖會渲染的地方。到現在為止,程序會報錯。等我們建好了路由和page組件的時候,就好了。

創建頁面組件

如上所述,應用會包含三個路由:登錄主頁,創建預覽頁,以及完成頁。

我們先創建好各頁面手腳架,以便搭建路由。然後再回來完善各個組件。

在根目錄下運行如下指令創建頁面組件:

$ ng g component pages/home
$ ng g component pages/create
$ ng g component pages/complete

ng g命令可以快速生成組件,指令,過濾器和服務,同時也會自動把生成的文件導入到app.module.ts中。現在,我們有三個頁面組件的腳手架,可以開始搭建路由了。

搭建路由

新建一個路由模塊,在src/app/core文件夾下創建一個app-routing.module.ts文件:

// src/app/core/routing-module.ts
import { NgModule } from [email protected]/core‘;
import { RouterModule } from [email protected]/router‘;

import { HomeComponent } from ‘../pages/home/home.component‘;
import { CreateComponent } from ‘./../pages/create/create.component‘;
import { CompleteComponent } from ‘./../pages/complete/complete.component‘;


@NgModule({
imports: [
RouterModule.forRoot([
{
path: ‘‘,
component: HomeComponent
},
{
path: ‘create‘,
component: CreateComponent
},
{
path: ‘complete‘,
component: CompleteComponent
},
{
path: ‘**‘,
redirectTo: ‘‘,
pathMatch: ‘full‘
}
])
],
providers: [],
exports: [
RouterModule
]
})
export class AppRoutingModule {}

現在有三個路由/,/create,/complete,未知路由會重定向到首頁。

打開根模塊文件app.module.ts,添加新增路由模塊AppRoutingModule至imports 屬性。

// src/app/app.module.ts
...
import { AppRoutingModule } from ‘./core/app-routing.module‘;

@NgModule({
...,
imports: [
...,
AppRoutingModule
],
...

到此,路由設置完畢。我們可以通過路由來訪問不同頁面,訪問首頁的時候,HomeComponent就會渲染在<router-outlet>所在的位置,如下圖所示:

技術分享

“Home”頁面組件

HomeComponent會有簡單的信息提示,以及登錄按鈕。點擊登錄按鈕,直接跳轉到/create頁面。

現在,讓我們添加提示信息和跳轉到/create頁面的按鈕。打開home.component.html,替換內容如下:

<!-- src/app/pages/home/home.component.html -->
<div class="row">
<div class="col-sm-12 text-center">
<p class="lead">
Please sign up or log in to create a custom name tag for your beloved pet!
</p>
<p>
<button
class="btn btn-lg btn-primary"
routerLink="/create">Log In</button>
</p>
</div>
</div>

現在,首頁效果如下:

技術分享

寵物標簽模型

開始實現個性寵物標簽生成器功能和狀態管理的工作了。首先,為我們的狀態創建一個數據模型,該模型描述了當前的寵物標簽。

新建文件src/app/core/pet-tag.model.ts:

// src/app/core/pet-tag.model.ts
export class PetTag {
constructor(
public shape: string,
public font: string,
public text: string,
public clip: boolean,
public gems: boolean,
public complete: boolean
) { }
}

export const initialTag: PetTag = {
shape: ‘‘,
font: ‘sans-serif‘,
text: ‘‘,
clip: false,
gems: false,
complete: false
};

PetTag類聲明了寵物標簽的屬性和類型,接著我們定義一個常量initialTag作為默認初始值,在初始化和重置狀態時需要用到。

寵物標簽行為

現在可以創建行為類型了。回顧之前說的,action被派發到reducer中,從而更新store。現在為我們想要的每種行為定義名字。

創建文件src/app/core/pet-tag.actions.ts

// src/app/core/pet-tag.actions.ts
export const SELECT_SHAPE = ‘SELECT_SHAPE‘;
export const SELECT_FONT = ‘SELECT_FONT‘;
export const ADD_TEXT = ‘ADD_TEXT‘;
export const TOGGLE_CLIP = ‘TOGGLE_CLIP‘;
export const TOGGLE_GEMS = ‘TOGGLE_GEMS‘;
export const COMPLETE = ‘COMPLETE‘;
export const RESET = ‘RESET‘;

將行為定義為常量。我們也可以構造可註入的行為類,就像ngrx/example-app中那樣。但我們這個例子很簡單,用這種方法反而會增加復雜度。

寵物標簽歸約器

現在可以創建我們的歸約函數了,這個函數接受action,更新store。

新建文件src/app/core/pet-tag.reducer.ts:

// src/app/core/pet-tag.reducer.ts
import { Action } from [email protected]/store‘;
import { PetTag, initialTag } from ‘./../core/pet-tag.model‘;
import { SELECT_SHAPE, SELECT_FONT, ADD_TEXT, TOGGLE_CLIP, TOGGLE_GEMS, COMPLETE, RESET } from ‘./pet-tag.actions‘;

export function petTagReducer(state: PetTag = initialTag, action: Action) {
switch (action.type) {
case SELECT_SHAPE:
return Object.assign({}, state, {
shape: action.payload
});
case SELECT_FONT:
return Object.assign({}, state, {
font: action.payload
});
case ADD_TEXT:
return Object.assign({}, state, {
text: action.payload
});
case TOGGLE_CLIP:
return Object.assign({}, state, {
clip: !state.clip
});
case TOGGLE_GEMS:
return Object.assign({}, state, {
gems: !state.gems
});
case COMPLETE:
return Object.assign({}, state, {
complete: action.payload
});
case RESET:
return Object.assign({}, state, initialTag);
default:
return state;
}
}

首先從ngrx/store導入Action。同時也需要PetTag數據模型以及它的初始狀態initialTag。還有上一步中創建的行為類型也需要導入。

然後創建petTagReducer()函數,該函數接收兩個參數:上一個狀態state和被派發的行為action。註意它是輸入決定輸出的純函數,函數不會改變全局的狀態。這就是說,從歸約器返回的數據要麽是新對象,要麽是未修改的輸入,比如default情況。

通常,我們可以借用Object.assign()從輸入數據中得到全新的對象。輸入數據是上一個狀態以及包含行為載體(payload)的對象。

TOGGLE_CLIP和TOGGLE_GEMS切換initialTag狀態中的布爾值,所以當我們派發這兩種行為的時候,不需要行為載體,我們只需要簡單取反即可。

COMPLETE行為需要一個載體,因為我們明確要將其設置為true,而且每個標簽只能操作一次。我們也可以切換布爾值,但明確起見,我們還是會派發一個具體的值作為行為載體。

註意:註意RESET行為用到導入的initialTag。因為它是個不變量,所以在這裏使用並不會違背歸約函數的純性。

根模塊導入Store

完成了行為和歸約函數的定義之後,我們要告訴應用程序有這些的存在。打開app.module.ts文件,更新如下:

// src/app/app.module.ts
...
import { StoreModule } from [email protected]/store‘;
import { petTagReducer } from ‘./core/pet-tag.reducer‘;

@NgModule({
...,
imports: [
...,
StoreModule.provideStore({ petTag: petTagReducer })
],
...

現在,我們可以用Store來實現狀態管理了。

創建“Create”頁面

之前創建的CreateComponent是個智能組件(Smart Component),它會有幾個木偶子組件(Dumb Component)。

智能組件/木偶組件

智能組件也稱容器組件,通常作為根級組件,包含業務邏輯,狀態管理,訂閱,處理事件。在這個例子中,就是那些可路由的頁面組件。CreateComponent是智能組件,它將為標簽生成器制定業務邏輯。同時,它會處理木偶子組件觸發的事件,而這些子組件是標簽生成器的一部分。

木偶組件又名展示組件,它只決定於父組件傳遞的數據。它可以觸發事件,然後在父組件中處理,但它不會直接影響訂閱或者store。木偶組件是可復用的模塊化組件。比如,我們會同時在Create頁面和Complete頁面使用標簽預覽這個木偶組件(CreateComponent和CompleteComponent是智能組件)。

“Create”頁面功能點

Create頁面將會有以下幾個功能: