文章出處:https://www.pluralsight.com/guides/front-end-javascript/ui-state-management-with-redux-in-angular-4
在此感謝 Hristo Georgiev 先生的原文許可
本文分上下兩節(jié)弯院,上節(jié)主要是指引讀者初始化一個 Angular 4(2) + Redux 的架構(gòu),并且用一個示例來方便讀者進行實踐;下節(jié)主要是一些功能的具體實現(xiàn)。
(感覺分兩節(jié)會有更多時間緩沖一點孕荠,下節(jié)對熟悉 ngb 的筒子也不si很有必要弯予,想看完整文章的童鞋點擊原文吧!)
? 盡管各種用于構(gòu)建 web 界面的先進技術(shù)在過去幾年相繼出現(xiàn),但 DOM 和應(yīng)用 UI 的管理仍然在很大程度上依賴 jQuery——一個已經(jīng)有歷盡10年滄桑的代碼庫。目前的對它的應(yīng)用已經(jīng)和它初生之時所肩負的使命有所不同瑟捣,盡管這不一定是一件不好的事情。但是從結(jié)果上來看踱蛀,如今的開發(fā)者們對 jQuery 的應(yīng)用方式已經(jīng)產(chǎn)生了一些問題锻梳。隨著大量低耦合甚至無耦合的組件寥掐、封裝后的視圖組件及其他各類元素集成在一起召耘,前端應(yīng)用變得越來越復(fù)雜。
? 在這篇文章里褐隆,我們將探索一個基于 Redux 的解決方案污它,用于目前在 anguar2 構(gòu)建的應(yīng)用中極具挑戰(zhàn)性的 UI 狀態(tài)管理。接下來衫贬,我們會通過理論和一個實例來學(xué)習(xí)如何通過使用 reducer 的方法來處理應(yīng)用 UI 狀態(tài)的邏輯。
掌控你的應(yīng)用界面布局( UI Layout )
? 自從 Redux 推出以來歇攻,前端應(yīng)用的狀態(tài)管理得到了革命性的進步固惯。我和我的團隊實例測試結(jié)果顯示,Redux 的集成對 Angular 2 應(yīng)用的生產(chǎn)效率有極大的提升缴守。
Redux 不僅僅是加快了數(shù)據(jù)的流速葬毫,它還通過將關(guān)鍵性的邏輯封裝在獨立的區(qū)域來從整體上提升了代碼的可維護性,并且為應(yīng)用的結(jié)構(gòu)測試提供了方便屡穗。
? 對 Redux 的沉迷讓我們希望用她來管理所有事務(wù)贴捡。在我們最近的工作中,其中一個項目對 UI 有著相當(dāng)強的依賴村砂,因而我們決定試驗基于這個需求來給予 Reducer 多一點的職責(zé)而不僅僅是管理數(shù)據(jù)烂斋。
使用 Redux 管理 UI 的三個要點
在切換路由的時候保持 UI 的狀態(tài),譬如保持 sidebar 的展開或者收起
-
在應(yīng)用的任何節(jié)點控制 UI 的狀態(tài),而不用考慮如何進行組件之間的通信或者通過具體的 service 注入來控制 UI
(意即不用通過組件間的通訊來控制組件的裝入和卸載源祈,而是在應(yīng)用生命周期的任何時刻進行靈活的控制)
將非 UI 行為的事件與 UI 的狀態(tài)改變關(guān)聯(lián)起來煎源,如路由的改變或者處理來自服務(wù)端的數(shù)據(jù)時
初始化工程
以下的示例是基于目前最流行的 bootstrap 4 風(fēng)格定制的 Angular 2 組件庫 “ng-bootstrap” 構(gòu)建的。你也可以在這個示例中實踐其他 UI 組件庫香缺,如聲名遠揚(譯者自加的)的 Material Design手销。敲代碼時通過遵循相同的設(shè)計原則和做一些局部的適配來讓當(dāng)前的組件庫能夠順利運行。
依賴庫
(譯者注:以下示例中用到的框架已由最新的 Angular 版本對應(yīng)實現(xiàn)图张,有些 api 已經(jīng)進行了調(diào)整——如 StoreModule.provideStore() 在 Angular4
的 @ngrx/store 版本中已經(jīng)更新為更為標準化的StoreModule.forRoot()
锋拖,因而請關(guān)注相關(guān)文檔避免兼容問題)
開始運行前你需要按順序安裝以下的依賴
Redux
- @ngrx/store + @ngrx/core
- @ngrx/effects
- reselect
- ngrx-store-logger
Bootstrap
- Bootstrap 4
- ng-bootstrap
安裝
為了有一個流暢的安裝過程,我們使用 Angular CLI 來初始化項目架構(gòu)祸轮。執(zhí)行前請確認你已經(jīng)全局安裝過它兽埃。
在你的終端中鍵入一下命令來初始化一個 Angular 2 項目
$ ng new redux-layout-tutorial-app
$ cd redux-layout-tutorial-app
$ yarn add bootstrap@4.0.0-alpha.6
//or by npm
$ npm i --save bootstrap@4.0.0-alpha.6
你需要在項目的根目錄下打開 angular-cli.json 來添加 Bootstrap 的資源庫
apps: [
{
//..
"styles": [ "../node_modules/bootstrap/dist/css/bootstrap.css"
],
//...
"environments": {
//...
"scripts": [
"../node_modules/jquery/dist/jquery.js",
"../node_modules/tether/dist/js/tether.js",
"../node_modules/bootstrap/dist/js/bootstrap.js"
]
}
這會讓你 angular-cli 從你的 Bootstrap 安裝目錄下確定 javascript 和 css 文件的位置,并在項目生成時加入他們(即形成依賴)
下一步适袜,安裝 ng-bootstrap
$ yarn add @ng-bootstrap/ng-bootstrap
// or by npm
$ npm i --save @ng-bootstrap/ng-bootstrap
然后在你的應(yīng)用的根目錄下的模塊文件中引入(即 app.module.ts 文件):
import {NgbModule} from "@ng-bootstrap/ng-bootstrap";
@NgModule({
//..
imports: [ NgbModule.forRoot() ],
//..
})
初始化應(yīng)用的 store 和基本的 reducer
接下來柄错,我們將構(gòu)建一個基礎(chǔ)的 Redux 架構(gòu),之后的操作中的用例都會基于這個架構(gòu)
從安裝 Redux 應(yīng)用 store 的核心依賴庫開始把苦酱!
$ yarn add @ngrx/core
$ yarn add @ngrx/store
// or by npm
$ npm i --save @ngrx/core
$ npm i --save @ngrx/store
對于異步事件售貌,譬如 pagination 或 loading bars 的控制,我們需要引入中間件來處理:
$ yarn add @ngrx/effects
// or by npm
$ npm i --save @ngrx/effects
我們使用reselect
來實現(xiàn)高效的state
存取操作疫萤。我們將使用reselect
的createSelector
方法來創(chuàng)建高效的選擇器颂跨,這個選擇器能被存儲且僅在參數(shù)更改的時候才會重構(gòu):
$ yarn add reselect
// or by npm
$ npm i --save reselect
為了讓開發(fā)更加方便并易于調(diào)試,我們添加能夠在控制臺記錄action
和state
的更新的store-logger
來幫助我們:
$ yarn add ngrx-store-logger
// or by npm
$ npm i --save ngrx-store-logger
我們將與 redux 相關(guān)聯(lián)的文件都存放在 src/app/common 下來使應(yīng)用架構(gòu)更加合理一些:
$ mkdir src/app/common
創(chuàng)建界面狀態(tài)
接著上面的步驟扯饶,創(chuàng)建 common/layout 目錄用于放置所有與界面狀態(tài)相關(guān)的action
恒削,effect
和 reducer
:
$ mkdir src/app/common/layout
$ cd src/app/common/layout
我們在這個目錄下創(chuàng)建三個與界面狀態(tài)相關(guān)的文件:
$ touch layout.actions.ts
layout.actions.ts
這些action
會在用戶行為發(fā)生(打開或關(guān)閉sidebar
,打開或關(guān)閉modal
元素或其他操作)或者一個相關(guān)的事件(頁面縮放)發(fā)生時被調(diào)用:
import {Action} from '@ngrx/store';
/*
Layout actions are defined here
*/
export const LayoutActionTypes = {};
/*
The action classes will be added here once they are defined
*/
export type LayoutActions = null;
layout.reducer.ts
$ touch layout.reducer.ts
負責(zé)界面狀態(tài)的 reducer 會在每次界面狀態(tài)的改變的時候更新 state
import * as layout from './layout.actions'
export interface State {
/*
界面狀態(tài)的描述符接口定義
*/
}
const initialState: State = {
/*
界面狀態(tài)在這里進行值的初始化
*/
};
/*
reducer 的主控函數(shù)尾序,在狀態(tài)改變的時候返回新的 state
*/
export function reducer(state = initialState, action: layout.LayoutActions): State {
switch(action.type) {
default: return state;
}
}
創(chuàng)建 reducer
界面狀態(tài)初始化配置完成后钓丰,最后一步就是添加 reducer,它會隨著@ngrx/store
提供的StoreModule
變化在操作流的末端進行更新
$ touch src/app/common/index.ts
index.ts
/*
引入之前提到的用于創(chuàng)建高效選擇器的工具 createSelector
*/
import { createSelector } from 'reselect';
/*
同理引入 store-logger
*/
import { storeLogger } from 'ngrx-store-logger';
/*
引入界面狀態(tài)
*/
import * as fromLayout from './layout/layout.reducer'
import { compose } from '@ngrx/core'
import { combineReducers } from '@ngrx/store'
export interface AppState {
layout: fromLayout.State
}
export const reducers = {
layout: fromLayout.reducer
}
const developmentReducer: Function = compose(storeLogger(), combineReducers)(reducers);
export function metaReducer(state: any, action: any) {
return developementReducer(state, action);
}
/*
創(chuàng)建界面狀態(tài)的選擇器
*/
export const getLayoutState = (state: AppState) => state.layout;
最后蹲诀,將metaReducer
注入到根模塊的imports
數(shù)組中的StoreModule
import { StoreModule } from '@ngrx/store'
import { metaReducer } from './common/index'
//...
@NgModule({
//...
imports: [
StoreModule.provideStore(metaReducer)
],
//...
})
export class AppModule { }
”機智的”容器與“啞巴”組件
如果你熟悉 Redux 的使用斑粱,你一定知道有兩種類型的組件——視覺組件(即 UI 組件)和容器組件
在實現(xiàn)整個界面狀態(tài)的時候弃揽,最好的實踐是將邏輯綁定在指令(directive)中脯爪,以保證邏輯的DRY原則。舉個例子矿微,你并不需要給每一個容器組件中的 sidebar 都重復(fù)一次相同的控制邏輯 痕慢。
另一種方式是將邏輯寫在組件內(nèi)部,當(dāng)然涌矢,只有特殊情況下才會將邏輯寫在表現(xiàn)界面元素的組件(即 UI 組件)中掖举。
在這個示例中,容器組件為 AppComponent
娜庇。我們將layout.actions
引入到根組件AppComponent
的imports
中塔次,以將狀態(tài)關(guān)聯(lián)到應(yīng)用內(nèi)部方篮,使其可以觸發(fā)action
。
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
/*
將初始狀態(tài)引入到組件中励负,以操作其中的各部分狀態(tài)
*/
import * as fromRoot from './common/index';
/*
引入界面狀態(tài)相關(guān)的 action 等待調(diào)用
*/
import * as layout from './common/layout/layout.actions';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(
private store: Store<fromRoot.AppState>
) { }
}
Modals(模態(tài))
實現(xiàn)一個模態(tài)框的最簡單方式是用一個常量來標記它藕溅。畢竟一個情景下界面只應(yīng)有一個模態(tài)框(除非你試著實踐一些黑魔法),每個模態(tài)框應(yīng)當(dāng)引用自一個唯一的modalName
继榆。
那我們就從定義action
開始吧巾表!
用戶應(yīng)該具有觸發(fā)和關(guān)閉模態(tài)框的能力,所有我們想這樣來定義模態(tài)框的action
:
Adding to the state
layout.actions.ts
export const LayoutActionTypes = {
OPEN_MODAL: '[Layout] Open modal',
CLOSE_MODAL: '[Layout] Close modal'
};
/*
模態(tài)框的 action
*/
export class OpenModalAction implements Action {
type = LayoutActionTypes.OPEN_MODAL;
constructor(
public payload:string
) { }
}
export class CloseModalAction implements Action {
type = LayoutActionTypes.CLOSE_MODAL;
constructor() { }
}
export type LayoutActions = CloseModalAction | OpenModalAction
我們繼續(xù)往下寫來實現(xiàn)action
的處理器reducer
:layout.reducer.ts
import * as layout from './layout.actions';
export interface State {
openedModalName: string;
}
const initialState: State = {
openedModalName: null
};
export function reducer(state = initialState, action: layout.LayoutActions): State {
switch(action.type) {
/*
模態(tài)框的 case
*/
case layout.LayoutActionTypes.OPEN_MODAL: {
const name = action.payload;
return Object.assign({}, state, {
openedModalName: name
}); // 此處用 Object.assign 的原因請?zhí)綄?redux 的狀態(tài)更新原理
}
case layout.LayoutActionTypes.CLOSE_MODAL: {
return Object.assign({}, state, {
openedModalName: null
})
}
default: return state;
}
}
export const getOpenedModalName = (state: State) => state.openedModalName;
當(dāng)前模態(tài)框的標識符(也就是它的名字)會被openedModalName
存儲下來略吨,然后根據(jù)調(diào)用的action
來變化集币。我們需要一個選擇器(getOpenedModalName
)來操作state
中的openedModalName
屬性。
index.ts
export const getLayoutState = (state: AppState) => state.layout;
//...
export const getLayoutOpenedModalName = createSelector(getLayoutState, fromLayout.getOpenedModalName);
使用
我們創(chuàng)建一個簡單的模態(tài)框來看一下它到底是如何運作的:
$ ng g component template-modal
template-modal.component.ts
import {
Component,
ChangeDelectionStrategy,
Output,
ViewChild,
EventEmiter,
Input,
ElementRef
} from '@angular/core'
import {
NgbModal,
NgbModalRef
} from '@ng-bootstrap/ng-bootstrap'
@Component({
selector: 'template-modal',
templateUrl: 'template-modal.component.html'
})
export class TemplateModalComponent {
private modalName: string = 'templateFormModal';
private modalRef: NgbMOdalRef;
@ViewChild('content') _templateModal: ElementRef;
@Input() set modalState(_modalState: any) {
if (_modalState == this.modalName) {
this.openModal()
} else if (this.modalRef) {
this.closeModal();
}
}
@Output() onCloseModal = new EventEmitter<any>();
constructor(private modalService: NgbModal) {}
openModal() {
this.modalRef = this.modalService.open(this._templateModal, {
backdrop: 'static',
keyboard: false,
size: 'sm'
})
}
closeModal() {
this.modalRef.close();
}
}
每當(dāng)用戶嘗試去關(guān)閉模態(tài)框的時候翠忠,onCloseModal
會被template出發(fā)并傳遞到容器組件(一個EventEmitter的原理)鞠苟。
在容器組件中需要一個處理器來處理openedModalName
的更替并調(diào)用action
來控制模態(tài)框:
app.component.ts
export class AppComponent {
public openedModalName$: Obeservable<any>;
constructor(
private store: Store<fromRoot.AppState>
) {
// 用選擇器直接操作開啟的模態(tài)框
this.openedModalName$ = store.select(fromRoot.getLayoutOpenedModalName);
}
// 調(diào)用 action 以開啟模態(tài)框
handleOpenModal(modalName: string) {
this.store.dispatch(new layout.OpenedModalAction(modalName));
}
// 調(diào)用 action 以關(guān)閉模態(tài)框() {
this.store.dispatch(new layout.CloseModalAction());
}
}
可以看到,我們能重用
handleOpenModal
和handleCloseModal
來控制無論多少的模態(tài)框秽之,只要這些模態(tài)框有唯一的標識符偶妖。
譯者:當(dāng)然,這是以一種非常'DRY'的方式來控制模態(tài)框政溃,我們也可以根據(jù)實際情況來改變這個架構(gòu)
app.component.html
<!-- 我們通過異步流的方式來響應(yīng)式的獲取組件中最新的相關(guān)值 -->
<template-modal [modalState]="this.openedModalName$ | async" (onCloseModal)="handleCloseModal()"></template-modal>
<button class="btn btn-outline-primary" (click)="handleOpenModal('templateFormModal')">
Open modal with template
</button>
<!-- 別忘了把這個寫上 -->
<template ngbModalContainer></template>
在這個示例中趾访,點擊按鈕來觸發(fā)了hanelOpenModal
,但這并不是唯一的觸發(fā)方式董虱,有了 Redux扼鞋,我們可以在任何地方調(diào)用 action 來執(zhí)行它,指令愤诱、service
或者effect
云头。這是沒有限制的。
Sidebar(s) 側(cè)邊欄
在一個應(yīng)用中淫半,其側(cè)邊欄最基本的視覺屬性便是它的顯示和隱藏溃槐。在全局的狀態(tài)中由一個布爾值屬性來決定側(cè)邊欄是opened
狀態(tài)還是closed
狀態(tài)。如果有兩個側(cè)邊欄(或者多個科吭,看你是怎么玩的>_<!!)昏滴,那就為每一個側(cè)邊欄提供一個狀態(tài)屬性。
當(dāng)用戶與側(cè)邊欄交互的時候对人,需要有一些類似于開關(guān)作用的action
:
layout.action.ts
export const LayoutActionTypes = {
//左側(cè)邊欄行為
OPEN_LEFT_SIDENAV: '[Layout] Open LeftSidenav',
CLOSE_LEFT_SIDENAV: '[Layout] Close LeftSidenav',
//右側(cè)邊欄行為
OPEN_RIGHT_SIDENAV: '[Layout] Open RightSidenav',
CLOSE_RIGHT_SIDENAV: '[Layout] Close RightSidenav', };
export class OpenLeftSidenavAction implements Action { type = LayoutActionTypes.OPEN_LEFT_SIDENAV; constructor() { } }
export class CloseLeftSidenavAction implements Action { type = LayoutActionTypes.CLOSE_LEFT_SIDENAV; constructor() { } }
export class OpenRightSidenavAction implements Action { type = LayoutActionTypes.OPEN_RIGHT_SIDENAV; constructor() { } }
export class CloseRightSidenavAction implements Action { type = LayoutActionTypes.CLOSE_RIGHT_SIDENAV; constructor() { } }
export type LayoutActions = CloseLeftSidenavAction | OpenLeftSidenavAction | CloseRightSidenavAction | OpenRightSidenavAction
根據(jù)之前提到的谣殊,側(cè)邊欄的狀態(tài)屬性值應(yīng)該是布爾類型的變量。在這個示例中牺弄,左側(cè)邊欄默認開啟姻几,但是會有一個根據(jù)屏幕尺寸來決定是否調(diào)用CloseLeftSidenavAction
來關(guān)閉它的邏輯。
layout.reducer.ts
import * as layout from './layout.actionis';
export interface State {
leftSidebarOpened: boolean;
rightSidebarOpened: boolean;
}
const initialState: State = {
leftSidebarOpened: true,
rightSidebarOpened: false
};
export function reducer(state = initialState, action: layout.LayoutActions): State {
switch(action.type) {
case layout.LayoutActionTypes.CLOSE_LEFT_SIDENAV: { return Object.assign({}, state, { leftSidebarOpened: false }); }
case layout.LayoutActionTypes.OPEN_LEFT_SIDENAV: { return Object.assign({}, state, { leftSidebarOpened: true }); }
case layout.LayoutActionTypes.CLOSE_RIGHT_SIDENAV: { return Object.assign({}, state, { rightSidebarOpened: false }); }
case layout.LayoutActionTypes.OPEN_RIGHT_SIDENAV: { return Object.assign({}, state, { rightSidebarOpened: true }); }
default:
return state;
}
}
export const getLeftSidenavState = (state:State) => state.leftSidebarOpened;
export const getRightSidenavState = (state:State) => state.rightSidebarOpened;
在index.ts中添加一些選擇器來訪問側(cè)邊欄的狀態(tài):
export const getLeftSidenavState = (state:State) => state.leftSidebarOpened;
export const getRightSidenavState = (state:State) => state.rightSidebarOpened;
使用
除了將邏輯綁定在組件自身外,還可以通過結(jié)合結(jié)構(gòu)型指令來關(guān)閉或開啟對應(yīng)的側(cè)邊欄蛇捌。
$ ng g directive sidebar-watch
sidebar-watch.directive.ts
import {
Directive,
ElementRef,
Renderer,
OnInit,
AfterViewInit,
AfterViewChecked
} from '@angular/core';
import {
Store
} from "@ngrx/store";
import * as fromRoot from "../common/index";
let $ = require('jquery');
@Directive({
selector: '[sidebarWatch]'
}) export class SidebarWatchDirective implements OnInit {
constructor(private el: ElementRef, private _store: Store < fromRoot.AppState > ) {} /* Doing the checks on ngOnInit makes sure the DOM is fully loaded and the elements are available to be selected */
ngOnInit() { /* 監(jiān)聽左側(cè)邊欄狀態(tài) */
this._store.select(fromRoot.getLayoutLeftSidenavState).subscribe((state) => {
if (this.el.nativeElement.className == 'left-sidebar') {
if (state) {
$("#main-content").css("margin-left", "300px");
$(this.el.nativeElement).css('width', '300px');
} else {
$("#main-content").css("margin-left", "0");
$(this.el.nativeElement).css('width', '0');
}
}
}); /* 監(jiān)聽右側(cè)邊欄狀態(tài) */
this._store.select(fromRoot.getLayoutRightSidenavState).subscribe((state) => { /* You can use classes (addClass/removeClass) instead of using jQuery css(), or you can go completely vanilla by using selectors such as windiw.getElementById(). . */
if (this.el.nativeElement.className == 'right-sidebar') {
console.log('test') if (state) {
$('#fade').addClass('fade-in');
$("#rightBar-body").css("opacity", "1");
$("body").css("overflow", "hidden");
$(this.el.nativeElement).css('width', '60%');
} else {
$('#fade').removeClass('fade-in');
$("#rightBar-body").css("opacity", "0");
$("body").css("overflow", "auto");
$(this.el.nativeElement).css('width', '0');
}
}
});
}
}
該指令控制ElementRef
的nativeElement
屬性,這個屬性用于訪問組件模板中DOM抚恒。當(dāng)指令知道了(也就是綁定成功)它控制哪個側(cè)邊欄以后,便確認其對應(yīng)的狀態(tài)是true
還是false
络拌。然后通過 jQuery 去操作視圖中對應(yīng)的元素柑爸。jQuery 的使用能高效的選定元素和改變其屬性,并使用原生的 JavaScript 去增刪元素的 class盒音。
類似的表鳍,我們可以創(chuàng)建一個用于控制側(cè)邊欄開關(guān)的指令
/** * Created by Centroida-2 on 1/22/2017. */
import {
Directive,
Input,
ElementRef,
Renderer,
HostListener
} from '@angular/core';
import {
Store
} from "@ngrx/store";
import * as fromRoot from "../common/index";
import * as layout from '../common/layout/layout.actions'
@Directive({
selector: '[sidebarToggle]'
}) export class SidebarToggleDirective {
public leftSidebarState: boolean;
public rightSidebarState: boolean;
@Input() sidebarToggle: string;
@HostListener('click', ['$event']) onClick(e) { /* 左側(cè)邊欄開關(guān) */
if (this.sidebarToggle == "left" && this.leftSidebarState) {
this._store.dispatch(new layout.CloseLeftSidenavAction());
} else if (this.sidebarToggle == "left" && !this.leftSidebarState) {
this._store.dispatch(new layout.OpenLeftSidenavAction())
} /* 右側(cè)邊欄開關(guān) */
if (this.sidebarToggle == "right" && this.rightSidebarState) {
this._store.dispatch(new layout.CloseRightSidenavAction());
} else if (this.sidebarToggle == "right" && !this.rightSidebarState) {
this._store.dispatch(new layout.OpenRightSidenavAction());
}
}
constructor(private el: ElementRef, private renderer: Renderer, private _store: Store < fromRoot.AppState > ) {
this._store.select(fromRoot.getLayoutLeftSidenavState).subscribe((state) => {
this.leftSidebarState = state;
});
this._store.select(fromRoot.getLayoutRightSidenavState).subscribe((state) => {
this.rightSidebarState = state;
});
}
}
該指令有一個輸入型屬性@Input sidebarToggle
,其值可以為left
或者right
祥诽,由它控制的側(cè)邊欄位置所決定譬圣。每當(dāng)用戶點擊元素觸發(fā)側(cè)邊欄行為的時候,@HostListener('click')
會捕獲這個點擊事件并且檢查該側(cè)邊欄的全局狀態(tài)雄坪,并調(diào)用對應(yīng)的 action厘熟。
我們創(chuàng)建兩個側(cè)邊欄來驗證以上的實現(xiàn):
$ ng g component left-sidebar
left-sidebar-component.ts
import {
Component
} from '@angular/core';
@Component({
selector: 'left-sidebar',
templateUrl: 'left-sidebar.component.html',
styleUrls: ['./sidebar.styles.css']
}) export class LeftSidebarComponent {
constructor() {}
}
left-sidebar.component.html
<section sidebarWatch class="left-sidebar">
</section>
接著創(chuàng)建另一個側(cè)邊欄
$ ng g component right-sidebar
right-sidebar.component.ts
import {
Component
} from '@angular/core';
@Component({
selector: 'right-sidebar',
templateUrl: 'right-sidebar.component.html',
styleUrls: ['./sidebar.styles.css']
}) export class RightSidebarComponent {
constructor() {}
}
right-sidebar.component.html
<section sidebarWatch class="right-sidebar">
<button class="btn btn-primary" sidebarToggle="right">Close Right Sidebar</button>
</section>
sidebarWatch
的使用方式很直觀。只需要將它放置在側(cè)邊欄組件的頂層元素中维哈。
sidebarToggle
需要放置在控制側(cè)邊欄開關(guān)的元素中(在這里以 button 為例)绳姨,并且需要將left
或者right
賦值給這個指令,讓它知道自己控制的是哪個側(cè)邊欄阔挠。
我們需要一些樣式來讓這些元素看起來更像側(cè)邊欄:
$ touch src/app/components/sidebar.styles.css
sidebar.styles.css
.left-sidebar,
.right-sidebar {
transition: width 0.3s;
height: 100%;
position: fixed;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.left-sidebar {
background: #909090;
}
.right-sidebar {
overflow-y: auto !important;
overflow-x: hidden !important;
right: 0;
z-index: 999 !important;
background: #212121;
}
在應(yīng)用的根組件中飘庄,將側(cè)邊欄放置在 class 為main-content
的 div 元素的上方:
app.component.html
<div id="fade" class="fade-in"></div>
<left-sidebar></left-sidebar>
<right-sidebar></right-sidebar>
<div id="main-content"> <button class="btn btn-primary" sidebarToggle="left">Toggle Left Sidebar</button> <button class="btn btn-primary" sidebarToggle="right">Toggle Right Sidebar</button>
<!-- ... -->
</div>
<!-- ... -->
這個 id 為fade
的 div 元素用于在右側(cè)邊欄開啟的時候?qū)崿F(xiàn) fade 過渡效果。為其添加一些樣式:
.fade-in {
postition: absolute;
min-height: 100%!important;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0,0,0,0.5);
width: 100%;
transition: top 0.3s, right 0.3s, bottom 0.3s, left 0.3s;
}
在這樣的設(shè)定中购撼,側(cè)邊欄完全是無關(guān)其容器的跪削。任何元素都可以通過指令在視圖中的任何位置來實現(xiàn)和側(cè)邊欄一樣的效果。并且可以靈活的根據(jù)需求去添加底欄或者頂欄組件
Dismissable Alerts (可消除的呼出提示)
控制 alert 在應(yīng)用中何時何地呼出迂求。因為 alert 要么是服務(wù)端控制呼出碾盐,要么是通過用戶行為呈現(xiàn),因此它理所應(yīng)當(dāng)由應(yīng)用狀態(tài)進行控制揩局。
與本文的其他示例不同毫玖,“reduxifying”的 alert 相比起來要容易一些——它們能根據(jù)狀態(tài)中響應(yīng)的 alert 項集合的增刪進行直接的渲染。
通常情況下凌盯,一個 alert 應(yīng)當(dāng)由兩個屬性:message
和type
付枫。一下是一個 alert 的屬性模型:
export class Alert {
message: string;
type: string;
}
首先,我們添加一些 action 來控制 alert 的增刪:
layout.actions.ts
export const LayoutActionTypes = {
ADD_ALERT: '[Layout] add alert',
REMOVE_ALERT: '[Layout] remove alert'
};
export class AddAlertAction implements Action {
type = LayoutActionTypes.ADD_ALERT;
constructor(public payload: Object) {}
}
export class RemoveAlertAction implements Action {
type = LayoutActionTypes.REMOVE_ALERT;
constructor(public payload: Object) {}
}
export type LayoutActions = AddAlertAction | RemoveAlertAction
接著十气,我們在視圖狀態(tài)中創(chuàng)建 alert 的片段:
layout.reducer.ts
import * as layout from './layout.actions';
export interface State {
alerts: Array < Object > ;
}
const initialState: State = {
alerts: [],
};
export function reducer(state = initialState, action: layout.LayoutActions): State {
switch (action.type) {
case layout.LayoutActionTypes.ADD_ALERT:
{
return Object.assign({}, state, {
alerts: [...state.alerts, action.payload]
});
}
case layout.LayoutActionTypes.REMOVE_ALERT:
{
return Object.assign({}, state, {
/* Alerts are filtered by message content, but for real-world usage, an 'id' field would be more suitable. */
alerts: state.alerts.filter(alert => alert['message'] !== action.payload['message'])
});
}
default:
return state;
}
}
/* If you add more attributes to the alerts such as 'position' or 'modelType', there can be more selectors added that can filter the collection and allow only certain to be displayed in designated places in the application. */
export const getAlerts = (state: State) => state.alerts;
最后励背,我們在index.ts
中為 alert 添加一個選擇器:
//..
export const getLayoutAlertsState = createSelector(getLayoutState, fromLayout.getAlerts);
這就是全部了。現(xiàn)在 alert 是應(yīng)用狀態(tài)的一部分了砸西。但如和使用呢?我們繼續(xù)前進。
使用
通過一些工具的使用芹枷,構(gòu)建 alert 只需要非常小段的代碼衅疙,因為 ng-bootstrap 已經(jīng)提供了的實現(xiàn)。因此鸳慈,我們只需要在任何有相應(yīng)需求的地方復(fù)用這個組件:
$ touch src/app/alerts-list.component.ts
alerts.component.ts
import {
Component,
Input,
EventEmitter,
Output
} from '@angular/core';
@Component({
selector: 'alerts-list',
templateUrl: 'alerts-list.component.html',
}) export class AlertsListComponent {
@Input() alerts: any;
@Output() closeAlert = new EventEmitter();
constructor() {}
}
這個組件接收一個包含一些 alert 對象的數(shù)組饱溢,然后想要關(guān)閉的 alert 對應(yīng)的事件響應(yīng)四瘫。
$ touch src/app/alerts-list.component.html
alerts.component.html
<p *ngFor="let alert of alerts">
<ngb-alert [type]="alert.type" (close)="closeAlert.emit(alert)">{{ alert.message }}</ngb-alert>
</p>
不要忘記將組件導(dǎo)入到根模塊:
app.module.ts
import {
AlertsListComponent
} from "./components/alerts-list.component";
@NgModule({
declarations: [
AlertsListComponent,
//...
],
}) export class AppModule {}
接下來尸红,容器組件需要實現(xiàn)一個選定 alert 和調(diào)起事件的邏輯。
app.component.ts
import {
Component,
OnInit
} from '@angular/core';
import {
Store
} from "@ngrx/store";
import {
Observable
} from "rxjs";
import * as fromRoot from './common/index';
import * as layout from './common/layout/layout.actions';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
}) export class AppComponent implements OnInit {
public alerts$: Observable < any > ;
constructor(private store: Store < fromRoot.AppState > ) {
this.alerts$ = store.select(fromRoot.getLayoutAlertsState);
}
addAlert(alert) {
this.store.dispatch(new layout.AddAlertAction(alert))
}
onCloseAlert(alert: Object) {
this.store.dispatch(new layout.RemoveAlertAction(alert))
}
}
我們用兩個呼出不同類型 alert 的按鈕來查看 alert 的效果:
app.component.html
<div id="fade" class="fade-in"></div>
<left-sidebar></left-sidebar>
<right-sidebar></right-sidebar>
<div id="main-content">
<!-- List of alerts goes here -->
<alerts-list [alerts]="alerts$ | async (closeAlert)=" onCloseAlert($event)
"></alerts-list> <!-- Buttons for creating alerts --> <button class="btn btn-danger " (click)="addAlert({type: 'danger', message: 'This is a danger alert'})
">Add a danger alert</button> <button class="btn btn-success " (click)="addAlert({type: 'success', message: 'This is a success alert'}) ">Add a success alert</button> </div>
在實際的開發(fā)方案中萍启,alert 可以在服務(wù)端返回一個確切的結(jié)果的時候進行呼出翁逞。比如肋杖,在如下的片段中,當(dāng)應(yīng)用處理來自服務(wù)端的請求會調(diào)用AddAlertAction
來呼出 alert挖函。
@Effect() deleteStudent = this._actions.ofType(student.ActionTypes.DELETE_STUDENT).switchMap((action) => this._service.delete(action.payload)).mergeMap(() => {
return Observable.from([new DeleteStudentSuccessAction(), /* Chain actions - once the server successfully deletes some model, create an alert from it. */ new layout.AddAlertAction({
type: 'success',
message: 'Student successfully deleted!')]).catch(() => {
new layout.AddAlertAction({
type: 'danger',
message: 'An error ocurred.'
}) return Observable.of(new DeleteStudentFailureAction()
}));
});
Window size (窗口尺寸)
在應(yīng)用狀態(tài)中存儲一個監(jiān)聽窗口尺寸的屬性能讓 Redux 在許多方面相當(dāng)有用状植,尤其是在實現(xiàn)響應(yīng)式的視圖、設(shè)備相關(guān)的行為或者樣式(通過 NgClass 或者 NgStyle 等方式)的動態(tài)更換的時候怨喘。
我們要在窗口尺寸發(fā)生改變的時候同時改變應(yīng)用狀態(tài)中對應(yīng)的屬性值來讓它發(fā)揮作用津畸。我們?yōu)樗砑右粋€ action:
layout.actions.ts
import {
Action
} from '@ngrx/store';
export const LayoutActionTypes = {
// 添加對應(yīng)窗口拉伸行為的 action
RESIZE_WINDOW: '[Layout] Resize window'
};
export class ResizeWndowAction implements Action {
type = LayoutActionTypes.RESIZE_WINDOW;
constructor(public payload: Object) {}
}
export type LayoutActions = ResizeWndowAction
我們需要windowWidth
和windowHeight
兩個屬性來實現(xiàn)窗口尺寸的存儲:
layout.reducer.ts
import * as layout from './layout.actions';
export interface State {
windowHeight: number;
windowWidth: number;
}
const initialState: State = {
windowHeight: window.screen.height,
windowWidth: window.screen.width
};
export function reducer(state = initialState, action: layout.LayoutActions): State {
switch (action.type) { /* Window resize case */
case layout.LayoutActionTypes.RESIZE_WINDOW:
{
const height: number = action.payload['height'];
const width: number = action.payload['width'];
return Object.assign({}, state, {
windowHeight: height,
windowWidth: width
});
}
default:
return state;
}
}
export const getWindowWidth = (state: State) => state.windowWidth;
export const getWindowHeight = (state: State) => state.windowHeight;
我們直接用window.screen.height
和window.screen.width
的值來作為初始化的狀態(tài)值。WindowResizeAction
附帶一個包含窗口高寬值的對象:{width: number, height: number
必怜。
有許多種方式可以監(jiān)聽窗口的拉伸肉拓,但或許最方便也最常用的方式是給根組件修飾器的host
屬性添加一個監(jiān)聽。如此一來梳庆,無論用戶在應(yīng)用的何處帝簇,只要發(fā)生了窗口拉伸,ResizeWindowAction
都會被調(diào)用靠益。
app.component.ts
import {
Component,
OnInit
} from '@angular/core';
import {
Store
} from "@ngrx/store";
import {
Observable
} from "rxjs";
import * as fromRoot from './common/index';
import * as layout from './common/layout/layout.actions';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
/* Add this to your AppComponent to listen for window resize events */
host: {
'(window:resize)': 'onWindowResize($event)'
}
}) export class AppComponent implements OnInit {
constructor(private store: Store < fromRoot.AppState > ) {}
ngOnInit() {}
onWindowResize(event) {
this.store.dispatch(new layout.ResizeWndowAction({
width: event.target.innerWidth,
height: event.target.innerHeight
}))
}
}
host
將會監(jiān)聽窗口拉伸并調(diào)用onWindowResize
方法丧肴,并傳入事件對象。這個方法通過event.target
獲取新的窗口尺寸胧后,然后將其作為參數(shù)來調(diào)用ResizeWindowAction
以更新狀態(tài)中的值芋浮。
使用
最普遍的窗口尺寸需求就是響應(yīng)式設(shè)計。比如壳快,當(dāng)屏幕像素寬小于 768px(iPad)的時候希望左側(cè)邊欄初始狀態(tài)為收起纸巷。用 Redux 實現(xiàn)這個需求非常簡單——只需要添加一個 if 語句進行對應(yīng)操作:
layout.reducers.ts
export function reducer(state = initialState, action: layout.LayoutActions): State {
switch (action.type) {
case layout.LayoutActionTypes.RESIZE_WINDOW:
{
const height: number = action.payload['height'];
const width: number = action.payload['width'];
const leftSidebarState = width < 768 ? false : state.leftSidebarOpened;
return Object.assign({}, state, {
windowHeight: height,
windowWidth: width,
leftSidebarOpened: leftSidebarState
});
}
}
}
如果是使用 jQuery,相同的實現(xiàn)過程是不那么盡如人意的眶痰。然而瘤旨,有了 Redux,一個三目表達式就可以滿足你的需要竖伯。
使用 Redux 的時候存哲,所有的邏輯會在一個狀態(tài)中心隔離開來因宇,并且調(diào)試和測試你所有的實現(xiàn)是非常容易。
Server-side Pagination (服務(wù)端分頁)
(譯者注:其實 Server-side 在這里的含義并不是指由服務(wù)端完成分頁祟偷,而是強調(diào)應(yīng)用狀態(tài)在客戶端和服務(wù)端的聯(lián)動性)
用 Redux 管理應(yīng)用分頁可以提高應(yīng)用狀態(tài)的利用率察滑,并用盡可能少的代碼來提高靈活性。
GiantBomb API
我們用 GiantBomb API 來作為數(shù)據(jù)源來演示 Redux pagination 是如何運作的修肠。我們將請求到 GiantBomb 數(shù)據(jù)庫中的游戲數(shù)據(jù)贺辰,然后對結(jié)果進行分頁。分頁由應(yīng)用狀態(tài)管理嵌施。
首先饲化,創(chuàng)建games
文件夾:
$ mkdir src/app/common/games
$ touch src/app/common/games.actions.ts
games.actions.ts
import {
type
} from "../util";
import {
Action
} from "@ngrx/store";
export const GameActionTypes = { /* Because the games collection is asynchronous, there need to be actions to handle each of the stages of the request. */
LOAD: '[Games] load games',
LOAD_SUCCESS: '[Games] successfully loaded games',
LOAD_FAILURE: '[Games] failed to load games',
};
export class LoadGamesAction implements Action {
type = GameActionTypes.LOAD;
constructor(public payload: any) {}
}
export class LoadGamesFailedAction implements Action {
type = GameActionTypes.LOAD_FAILURE;
constructor() {}
}
export class LoadGamesSuccessAction implements Action {
type = GameActionTypes.LOAD_SUCCESS;
constructor(public payload: any) {}
}
export type GameActions = LoadGamesAction | LoadGamesFailedAction | LoadGamesSuccessAction
Redux 中有一個加載異步數(shù)據(jù)的規(guī)則,由LOAD
吗伤,LOAD_SUCCESS
和LOAD_FAILURE
三個 action 實現(xiàn)吃靠。后兩個會在 middleware (Redux 中間件)處理服務(wù)端響應(yīng)的時候調(diào)起。
我們梳理一下實現(xiàn)一個分頁功能的所需的組成部分牲芋,以明確如何來構(gòu)造這個games
的分頁狀態(tài):
- 當(dāng)前頁碼
- 數(shù)據(jù)的總量
- 當(dāng)前展示數(shù)據(jù)的集合
- (可選)每個分頁展示的數(shù)據(jù)量
有了如上思路撩笆,那么分頁狀態(tài)的接口應(yīng)當(dāng)如下所示:
export interface State {
loaded: boolean;
loading: boolean;
entities: Array<any>;
count: number;
page: number;
}
我們看看整個功能代碼是什么樣的:
$ touch src/app/common/games.reducer.ts
games.reducer.ts
import {
createSelector
} from 'reselect';
import * as games from './games.actions';
export interface State {
loaded: boolean;
loading: boolean;
entities: Array < any > ;
count: number;
page: number;
};
const initialState: State = {
loaded: false,
loading: false,
entities: [],
count: 0,
page: 1
};
export function reducer(state = initialState, action: games.GameActions): State {
switch (action.type) {
case games.GameActionTypes.LOAD:
{
const page = action.payload;
return Object.assign({}, state, {
loading: true,
/* If there is no page selected, use the page from the initial state */ page: page == null ? state.page : page
});
}
case games.GameActionTypes.LOAD_SUCCESS:
{
const games = action.payload['results'];
const gamesCount = action.payload['number_of_total_results'];
return Object.assign({}, state, {
loaded: true,
loading: false,
entities: games,
count: gamesCount
});
}
case games.GameActionTypes.LOAD_FAILURE:
{
return Object.assign({}, state, {
loaded: true,
loading: false,
entities: [],
count: 0
});
}
default:
return state;
}
} /* Selectors for the state that will be later used in the games-list component */
export const getEntities = (state: State) => state.entities;
export const getPage = (state: State) => state.page;
export const getCount = (state: State) => state.count;
export const getLoadingState = (state: State) => state.loading;
每當(dāng)GamesAction
調(diào)起LOAD
的時候,都會將頁碼傳遞到 reducer 并賦值給當(dāng)前狀態(tài)缸浦。剩下的工作就是用當(dāng)前分頁狀態(tài)的page
去請求服務(wù)器夕冲。我們需要將這個狀態(tài)集成到全局中去來實現(xiàn)這個功能。
index.ts
import * as fromGames from "./games/games.reducer"
export interface AppState {
layout: fromLayout.State;
games: fromGames.State
}
export const reducers = {
layout: fromLayout.reducer,
games: fromGames.reducer
};
export const getGamesState = (state: AppState) => state.games;
export const getGamesEntities = createSelector(getGamesState, fromGames.getEntities);
export const getGamesCount = createSelector(getGamesState, fromGames.getCount);
export const getGamesPage = createSelector(getGamesState, fromGames.getPage);
export const getGamesLoadingState = createSelector(getGamesState, fromGames.getLoadingState);
getGamesPage
用于取得當(dāng)前頁碼并將其作為參數(shù)來請求服務(wù)端數(shù)據(jù)裂逐。
$ touch src/app/common/games.service.ts
games.service.ts
import {
Injectable,
Inject
} from '@angular/core';
import {
Response,
Http,
Headers,
RequestOptions,
Jsonp
} from "@angular/http";
import {
Store
} from "@ngrx/store";
import * as fromRoot from "../index"
@Injectable() export class GamesService {
public page: number;
constructor(private jsonp: Jsonp, private store: Store < fromRoot.AppState > ) { /* Get the page from the games state */
store.select(fromRoot.getGamesPage).subscribe((page) => {
this.page = page;
});
} /* Get the list of games. GiantBomb requires a jsnop request with a token. You can use this token as a present from me, the author, and use it in moderation! */
query() {
let pagination = this.paginate(this.page);
let url = `http://www.giantbomb.com/api/games/?api_key=b89a6126dc90f68a87a6fe1394e64d7312b242da&?&offset=${pagination.offset}&limit=${pagination.limit}&format=jsonp&json_callback=JSONP_CALLBACK`;
return this.jsonp.request(url, {
method: 'Get'
}).map((res) => {
return res['_body']
});
} /** * This function converts a page to a pagination * query. * * @param page * * @returns {{offset: number, limit: number}} */
paginate(page: number, ) {
let beginItem: number;
let endItem: number;
let itemsPerPage: number = 10;
if (page == 1) {
beginItem = 0;
} else {
beginItem = (page - 1) * itemsPerPage;
}
return {
offset: beginItem,
limit: itemsPerPage
}
}
}
當(dāng)前的頁碼信息是從狀態(tài)中獲取的歹鱼,并且由paginate
進行傳遞。paginate
是一個輔助函數(shù)卜高,用于將當(dāng)前頁碼信息轉(zhuǎn)換成符合 GiantBomb API 規(guī)則的offset
和limit
參數(shù)弥姻。
接下來,我們實現(xiàn)一個用于調(diào)用 service 和調(diào)起SUCCESS
或FAILURE
action 的中間件掺涛。
$ touch src/app/common/games.effects.ts
games.effects.ts
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/switchMap';
import {
Observable
} from 'rxjs/Observable';
import {
Injectable
} from "@angular/core";
import * as games from "./games.actions";
import {
Actions,
Effect
} from "@ngrx/effects";
import {
GamesService
} from "./games.service";
import {
LoadGamesSuccessAction
} from "./games.actions";
import {
LoadGamesFailedAction
} from "./games.actions";
@Injectable() export class GameEffects {
constructor(private _actions: Actions, private _service: GamesService) {}
@Effect() loadGames$ = this._actions.ofType(games.GameActionTypes.LOAD).switchMap(() => this._service.query().map((games) => {
return new LoadGamesSuccessAction(games)
})).catch(() => Observable.of(new LoadGamesFailedAction()));
}
最后庭敦,從@ngrx/effects
模塊中導(dǎo)入EffectsModule
并運行該 effect,再將GamesService
以 provider 的形式導(dǎo)入:
app.module.ts
import {
EffectsModule
} from "@ngrx/effects";
import {
GameEffects
} from "./common/games/games.effects";
import {
GamesService
} from "./common/games/games.service";
@NgModule({
imports: [EffectsModule.run(GameEffects)],
providers: [GamesService],
bootstrap: [AppComponent]
}) export class AppModule {}
這個實現(xiàn)為分頁功能提供了極大的便利——該應(yīng)用的狀態(tài)同時用于客戶端的數(shù)據(jù)呈現(xiàn)和服務(wù)端的數(shù)據(jù)查詢薪缆。
使用
我們構(gòu)建一個games-list
組件來確認這個分頁實現(xiàn)在開發(fā)可重用分頁功能時的可用性秧廉。
之前提到的,分頁的實現(xiàn)需要狀態(tài)中有4個“片段”:
- 數(shù)據(jù)實例的集合
- 數(shù)據(jù)總量
- 當(dāng)前頁碼
- Loading/Loaded 狀態(tài)
我們先創(chuàng)建一個game-list
的模板:
$ ng g component games-list
games-list.component.ts
Component,
OnInit,
Input,
EventEmitter,
Output
} from '@angular/core';
@Component({
selector: 'games-list',
templateUrl: 'games-list.component.html',
}) export class GamesListComponent { /* The minimim required inputs of a list component using redux */
@Input() games: any;
@Input() count: number;
@Input() page: number;
@Input() loading: boolean; /* Emit and event when the user clicks on another page */
@Output() onPageChanged = new EventEmitter < number > ();
constructor() {}
}
games-list.component.html
<div class="container" *ngIf="games">
<table class="table table-hover">
<thead class="thead-inverse">
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let game of games">
<td>{{game?.name}}</td>
</tr>
</tbody>
</table>
<ngb-pagination [collectionSize]="count" [(page)]="page" (pageChange)="onPageChanged.emit($event)" [maxSize]="10" [disabled]="loading"></ngb-pagination>
</div>
在根模塊中聲明這個組件:
import {
GamesListComponent
} from "./components/games-list.component";
@NgModule({
declarations: [GamesListComponent, ]
})
GamesListComponent
使用 ng-bootstrap 中的 ngbPagination 組件來輔助構(gòu)建拣帽。這個組件需要通過輸入(@Input
)屬性來渲染一個分頁疼电,而pageChange
事件會觸發(fā)輸出(@Output
)函數(shù)onPageChanged
來將動作傳遞到容器組件。
接下來减拭,我們對容器組件進行完善(在這個示例中蔽豺,即AppComponent
)。
容器組件需要做一些工作來使分頁功能運行起來:
- 提供相應(yīng)的狀態(tài)信息作為
GamesListComponent
組件的輸入 - 有一個方法來處理
onPageChanged
事件
app.component.ts
import * as games from './common/games/games.actions';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
}) export class AppComponent implements OnInit {
public games$: Observable < any > ;
public gamesCount$: Observable < number > ;
public gamesPage$: Observable < number > ;
public gamesLoading$: Observable < boolean > ;
constructor(private store: Store < fromRoot.AppState > ) { /* Select all the parts of the state needed for the GamesListComponent */
this.games$ = store.select(fromRoot.getGamesEntities);
this.gamesCount$ = store.select(fromRoot.getGamesCount);
this.gamesPage$ = store.select(fromRoot.getGamesPage);
this.gamesLoading$ = store.select(fromRoot.getGamesLoadingState);
} /* When the component initializes, render the first page ofresults */
ngOnInit() {
this.store.dispatch(new games.LoadGamesAction(1));
}
onGamesPageChanged(page: number) {
this.store.dispatch(new games.LoadGamesAction(page))
}
}
最后拧粪,將GamesListComponent
的選擇器添加到AppComponent
的模板修陡。
app.component.html
<div id="main-content"> <!-- ... -->
<games-list [games]="games$ | async" [count]="gamesCount$ | async" [page]="gamesPage$ | async" [loading]="gamesLoading$ | async" (onPageChanged)="onGamesPageChanged($event)"></games-list>
</div>
async
管道在這里會提取對應(yīng) observable 變量最近一次的值沧侥,同時監(jiān)聽狀態(tài)的更新,然后作為輸入屬性傳遞到組件中去濒析。
以下是分頁功能在觸發(fā) action 時的運行圖示:
結(jié)論
這些示例代表了許多你在使用 Angular 2/4 + Redux 構(gòu)建應(yīng)用時可能遇到的需求正什。它們在很大程度上提供了一個樣板示例供更明確的需求實現(xiàn)啥纸,在此也希望提供更多的 idea 來實現(xiàn)其他需求号杏。
Redux 在控制視圖狀態(tài)上表現(xiàn)得優(yōu)秀嗎?在我看來斯棒,這是毫無疑問的盾致。在開發(fā)的時候它可能需要比較多一點的代碼量,但是當(dāng)應(yīng)用的代碼基礎(chǔ)不斷增強以及邏輯的可復(fù)用性越來越強的時候荣暮,Redux 的光輝庭惜,誰也掩蓋不了。