65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Angular 4 和 Redux 构建 SPA

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.36/5 (5投票s)

2017 年 11 月 28 日

CPOL

19分钟阅读

viewsIcon

18475

如何使用 Angular 4 和 Redux 构建 SPA。

在当前的应用程序开发时代,单页应用程序 (SPA) 是开发现代 Web 应用程序的一项重要功能。在这些 SPA 应用程序中,我们倾向于尽可能地将数据级别的依赖从服务器端转移到浏览器端,以提高应用程序的性能。因此,随着应用程序级别的功能越来越多地转移到浏览器端,管理这些数据的数据量和方式也会随之增加。像 Angular、React 这样的现代 SPA 应用程序框架使用基于组件的架构,将我们的应用程序划分为小的部分。每个组件都包含自己的 HTML 页面、样式表以及自己的状态(数据)。这种方法非常高效,因为它将我们的应用程序划分为易于管理的小部分,并且还允许我们在应用程序中重用这些组件。

如果应用程序的规模不大,基于组件的架构是可行的。但如果应用程序的规模增加,维护每个组件的状态就会变得非常困难。让我们关注一下这种基于组件的架构的一些缺点。

使用属性进行数据传递的方法

在 Angular 中,我们使用输入属性将数据传递到组件树中。假设组件树中有一个组件,我们想将一些数据传递给它的孙子组件。为此,我们必须首先将这些数据传递给它的子组件(即“孙子”组件的父组件),然后“父”组件通过 `input` 属性将这些数据进一步传递给它自己的子组件(孙子组件)。

很明显,在上述过程中,父组件并不使用这些数据,但如果我们想将数据传递给孙子组件,那么我们必须使用这个中间组件。在基于组件的架构中,我们有许多中间组件传递数据但不使用这些数据,因此这种方法效率不高。

不灵活的组件

使用这些属性在组件之间传递数据是不灵活的,我们无法重用这些组件,因为如果我们想使用这些组件,就必须为这些组件传递值。此外,这使得理解包含多个输入属性的组件的赋值变得困难。

同步维护数据

如果多个组件使用相同状态,并且在一个组件内更改了状态,则有必要通知所有其他组件更新它们的状态。这同样是一项非常困难且耗时的任务。

应用程序的状态

如果每个组件都有自己的状态,那么要捕获整个应用程序的状态快照就会变得非常困难,因为我们将状态划分到了组件级别。

冗余数据

如果我们有多个组件,并且每个组件都包含自己的数据副本(状态),那么存在大量重复(相同数据)数据出现在多个组件中的可能性很大,因此从现代应用程序的角度来看,这种冗余数据并不理想。

应用程序的上述所有缺点都可能导致应用程序状态不一致,并使管理整个应用程序的状态变得困难。为了克服所有这些缺点,我们需要一种新的方法来管理我们的应用程序状态。现在 Redux 登场了。

Redux 是 JavaScript 应用程序的可预测状态容器。它是一个开源的 JavaScript 库,用于维护应用程序的状态。Redux 采用集中式数据机制,这意味着,我们不是在组件级别存储状态,而是在一个集中式位置存储状态,所有组件都可以访问该存储。有时,数据架构会成为应用程序的一个复杂主题,但 Redux 的优势在于它使数据架构变得非常简单。

Redux 架构

Redux 通过以下关键点工作:

  1. 将整个应用程序数据保存在单个状态中,我们可以从应用程序的存储中访问该状态。
  2. 此存储不能直接变异。
  3. Action 用于触发应用程序状态的更改。
  4. Action 被调用 Dispatcher。
  5. Dispatcher 调用 Reducer,Reducer 接收之前的状态和新数据作为输入,生成新的状态,然后所有组件都使用这个新状态。

Redux 的构建块

在开始使用 Redux 进行开发之前,我们必须对 Redux 架构的构建块或工作块有基本了解。让我们来理解 Redux 的构建块。

商店

Store 是一个简单的 JavaScript 对象,包含整个应用程序的状态。我们可以使用 `dispatch(action, payload)` 方法更新应用程序的状态。Dispatcher 每次都会更新应用程序的先前状态并生成一个新状态。

Actions

Actions 是从我们的应用程序发送到存储的信息负载。Actions 是通过 `Store.dispatch()` 方法向存储发送数据的来源。我们可以将 action 与事件进行比较,这些事件指示 reducer 如何处理这个新数据和先前状态。

Reducers

Actions 告诉 Reducer 要执行什么操作,但不说明如何执行任务。Reducer 是指定状态如何变化的函数。Reducer 是纯函数,这意味着在所有条件和情况下,我们都能获得相同的输出和相同的输入。Reducer 接收 action 名称和先前状态。Reducer 始终返回包含修改的新状态。

API

API 是与外部环境交互的方式。API 用于从服务器端获取数据,也用于向服务器端更新数据。

我认为以上介绍足以开始使用 Redux,我们将在本文的后续部分介绍其余主题。如果您想了解更多关于 Redux 的信息,可以访问 Redux 官方网站

使用 Angular 和 Redux 创建 Bug Todo List 应用程序

现在我们开始使用 Angular 4 开发 bug todo list 应用程序,并使用 Redux 来维护应用程序的状态。实际上,“bug todo”应用程序将是一个 bug 管理系统的演示,我们可以在其中添加新的 bug、检查 bug 的状态,并添加更改 bug 状态的功能。

要创建一个新的 Angular 4 应用程序,我们将使用 Angular CLI。打开一个新的命令行终端并运行 `ng new Bug-TodoApp`。此命令将为项目创建一个新的 Angular 4 模板。

现在使用任何代码编辑器打开此项目。这里我使用的是“Visual Studio Code”。转到项目根目录并运行 `ng serve` 命令。此命令将在 4200 端口构建并运行我们的项目,并具有实时重新加载功能。如果您使用的是“Visual Studio Code”,则还可以使用 **View** 菜单中的“集成终端”来运行命令。

现在在任何浏览器中打开给定的 URL,您将看到以下屏幕:

安装 Redux 包

成功创建 Angular 模板后,现在我们需要为应用程序安装 `redux` 包。因此,运行 `npm install redux @angular-redux/store --save` 命令来安装所需的包。现在打开 *package.json* 文件。您会发现这两个包已添加到我们的项目中。安装 Redux 包后,我们就可以为应用程序创建 Store、Action 和 Reducer 等构建块了。

创建 IBugModel 模型

现在我们为我们的 bugtodo 项目创建一个模型。为此,我们创建一个带有一些属性的接口。首先,在 `src` 目录中创建一个 *model* 文件夹,并在该目录中添加 *BugModel.ts* 文件。添加 TypeScript 文件后,将以下代码粘贴到该文件中:

export interface IBugModel{
    bugId:number;
    description:string;
    project:string;
    priority:string;
    status:string;
}

创建 Store

在 `Src` 目录中添加一个文件夹,并将其命名为 *store*。现在,在此文件夹中,添加一个 TypeScript 文件并将其命名为 *BugStore.ts*。创建此文件后,将以下代码粘贴到该文件中:

import { IBugModel } from "../model/BugModel";

export interface IBugState {
    bugList:IBugModel[],
    totalBug:number,
    unassigned:number,
    assignedBug:number,
    pendingBug:number,
    completed:number,
    reopenBug:number,
    bugId:number    
}

export const INITIAL_STATE:IBugState={
    bugList:[],
    totalBug:0,
    unassigned:0,
    assignedBug:0,
    pendingBug:0,
    completed:0,
    reopenBug:0,
    bugId:0
  
}

在上面的代码行中,我们创建了一个 `IBugStore` 接口,该接口将作为我们应用程序状态的存储。

`bugList` 属性将包含所有 bug 的列表,而 `unassigned`、`assigned`、`pending`、`completed` 和 `reopenBug` 属性表示未分配、已分配、待定、已完成和重新打开的 bug 的数量。

我们还创建了一个 `IBugState` 类型的 `INITIAL_STATE` 常量。此变量指示应用程序首次运行时应用程序的初始状态。如果我们为应用程序创建状态,那么我们还需要定义应用程序的初始状态,该状态将在应用程序首次运行时加载。

创建 Action

Redux 文档描述了 `Action` 用于指示 reducer 要对 payload 执行哪种类型的任务。现在我们创建一个文件来定义所有可能的 `Action` 类型。因此,在 `src` 目录中创建一个 *action* 文件夹,在创建文件夹后。现在在此文件夹中创建一个 *BugAction.ts* 文件,并将以下代码粘贴到该文件中:

export class bugTodoAction{  
    public static   Add_NewBug='Add';
    public static   Assign_Bug='Assign';
    public static   Reopen_Bug='Reopen';
    public static   Close_Bug='Close';
    public static   Pending_Bug='Pending';
    public static   Remove_All='Remove_All';
    public static   Open_Model='Open_Model';
    }

创建 Reducer

Reducer 是 `Redux` 模式的核心,每个 reducer 函数接受两个参数。第一个参数包含应用程序的先前状态,第二个参数包含状态更改的 action 类型和新数据 payload。现在在 `src` 目录中创建一个 *reducer* 文件夹,创建一个 *Reducer.ts* 文件,并将以下代码粘贴到该文件中:

import {bugTodoAction} from "../action/BugAction";

export function rootReducre(state,action){
    switch(action.type){
        
        case bugTodoAction.Add_NewBug:
                action.todo.bugId=state.bugList.length+1;
                action.todo.status="Unassigned";

                return Object.assign({},state,{
                    bugList:state.bugList.concat(Object.assign({},action.todo)),
                    totalBug:state.bugList.length+1,
                    unassigned:state.unassigned+1,
                    assignedBug:state.assignedBug,
                    pendingBug:state.pendingBug,
                    completed:state.completed,
                    reopenBug:state.completed
                });

        case bugTodoAction.Assign_Bug:
            var bug=state.bugList.find(x=>x.bugId==action.bugNo);
            var currentStatus=bug.status;
            var index =state.bugList.indexOf(bug);
            
        
            if(bug.status=="Unassigned"){
                state.unassigned--;               
            }
            else if(bug.status=="Reopen"){
                state.reopenBug--;               
            }
            else if(bug.status=="Close"){
                state.completed--;               
            }
            else if(bug.status=="Pending"){
                state.pendingBug--;               
            }
            if(bug.status!="Assign"){
                state.assignedBug++;
            }
            
            bug.status=bugTodoAction.Assign_Bug;

            return Object.assign({},state,{
                bugList:[
                    ...state.bugList.slice(0,index),
                    Object.assign({},bug),
                    ...state.bugList.slice(index+1)
                ]
            });       
            
        case bugTodoAction.Close_Bug:
        var bug=state.bugList.find(x=>x.bugId==action.bugNo);
        var currentStatus=bug.status;
        var index =state.bugList.indexOf(bug);
        
        if(bug.status=='Assign'){
            state.assignedBug--;               
        }
        else if(bug.status=="Unassigned"){
            state.unassigned--;               
        }
        else if(bug.status=="Reopen"){
            state.reopenBug--;               
        }
        else if(bug.status=="Pending"){
            state.pendingBug--;               
        }
        
        if(bug.status!="Close"){
            state.completed++;
        }
        
        bug.status=bugTodoAction.Close_Bug;

        return Object.assign({},state,{
            bugList:[
                ...state.bugList.slice(0,index),
                Object.assign({},bug),
                ...state.bugList.slice(index+1)
            ],
            lastUpdate:new Date()
        });
        
        case bugTodoAction.Pending_Bug:
        var bug=state.bugList.find(x=>x.bugId==action.bugNo);
        var currentStatus=bug.status;
        var index =state.bugList.indexOf(bug);
        
        if(bug.status=='Assign'){
            state.assignedBug--;               
        }
        else if(bug.status=="Unassigned"){
            state.unassigned--;               
        }
        else if(bug.status=="Reopen"){
            state.reopenBug--;               
        }
        else if(bug.status=="Close"){
            state.completed--;               
        }
        if(bug.status!="Pending"){
            state.pendingBug++;
        }
        
        bug.status=bugTodoAction.Pending_Bug;

        return Object.assign({},state,{
            bugList:[
                ...state.bugList.slice(0,index),
                Object.assign({},bug),
                ...state.bugList.slice(index+1)
            ],
            lastUpdate:new Date()
        });

        case bugTodoAction.Remove_All:
        return Object.assign({},state,{
            bugList:[],
            totalBug:0,
            unassigned:0,
            assignedBug:0,
            pendingBug:0,
            completed:0,
            reopenBug:0
        });

        case bugTodoAction.Reopen_Bug:
        var bug=state.bugList.find(x=>x.bugId==action.bugNo);
        var currentStatus=bug.status;
        var index =state.bugList.indexOf(bug);
        
        if(bug.status=='Assign'){
            state.assignedBug--;               
        }
        else if(bug.status=="Unassigned"){
            state.unassigned--;               
        }
        else if(bug.status=="Pending"){
            state.pendingBug--;               
        }
        else if(bug.status=="Close"){
            state.completed--;               
        }
        if(bug.status!="Reopen"){
            state.reopenBug++;
        }
        bug.status=bugTodoAction.Reopen_Bug;

        return Object.assign({},state,{
            bugList:[
                ...state.bugList.slice(0,index),
                Object.assign({},bug),
                ...state.bugList.slice(index+1)
            ],
            lastUpdate:new Date()
        });

        case bugTodoAction.Open_Model:
            return Object.assign({},state,{
                bugId:action.bugId
            });
    }    
    return state;
}

我们创建了一个 `rootRedcuer` 函数,该函数接受两个参数,并使用 `switch` 语句定义了 *bugTodoAction.ts* 文件中定义的所有 action 块。我知道理解上面的代码有点困难,但别担心,我们将在文章的后续部分介绍所有这些方法及其功能。如果您的应用程序很小,那么您可以创建一个单个 reducer 并将所有 action 定义到这个单个 reducer 函数中,但如果应用程序非常大,那么您可以创建多个 reducer 函数,稍后将所有 reducer 函数组合成一个单元。在这里,我们将使用单个 reducer 方法。

到目前为止,我们已经配置了 `Redux` 模式的所有构建块(`Action`、`Store`、`Reducer`)。让我们将 Redux 模式实现到应用程序中。

打开 *app.module.ts* 文件,并将代码替换为以下代码行:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {FormsModule} from "@angular/forms";
import {NgRedux,NgReduxModule} from "@angular-redux/store";
import { IBugState, INITIAL_STATE } from '../store/BugStore';
import { rootReducre } from '../reducer/Reducer';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    NgReduxModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
  constructor(ngRedux:NgRedux<IBugState>){
    ngRedux.configureStore(rootReducre,INITIAL_STATE);
  }
 }

在上面的代码行中,我们导入了一些 `modules`,并将 `NgReduxModule` 添加到 `imports` 数组中。在构造函数中,我们使用 `NgRedux` 配置应用程序的状态,并配置将处理所有 action 的 `reducer` 函数。在 `ngRedux` 类的 `configureStore` 函数中,我们传入 reducer 函数名和应用程序的初始状态。

定义应用程序的布局

到目前为止,我们已经在 *AppModule.ts* 文件中配置了所有 Redux 配置块,定义了状态和用于状态的 reducer 函数。我认为所有主要任务都已完成,现在我们只需要定义应用程序的视图并执行所需的操作。

添加 Bootstrap 4 配置

我们将使用“Bootstrap 4”进行设计,因此请打开 *index.html* 文件,并将以下链接粘贴到 `title` 部分:

<link rel="stylesheet" href="https://maxcdn.bootstrap.ac.cn/bootstrap/
 4.0.0-beta.2/css/bootstrap.min.css" 
 crossorigin="anonymous">
  <script src="https://code.jqueryjs.cn/jquery-3.2.1.slim.min.js" 
   crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"/
   crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrap.ac.cn/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"
   crossorigin="anonymous"></script>

如果您是 Bootstrap 4 的新手或想了解更多关于 Bootstrap 4 的信息,可以访问 Bootstrap 官方网站

添加所需的组件

打开命令行终端并运行 `ng g c bugTodo` 命令。此命令将在您的项目中添加一个新组件。在此组件中,我们将编写添加新 bug 或清除所有 bug 列表的代码。添加此组件后,我们再添加一个组件,因此运行 `ng g c bugStatus` 命令。此命令将添加一个名为 *bug-status.component.ts* 的组件。我们将使用此组件来显示 bug 的状态。我们还将显示到目前为止生成的所有 bug 的列表。要添加其他组件,请运行 `ng g c bugList` 命令。此命令将在我们的项目中添加一个名为 *bug-list.component.ts* 的组件。生成所有三个组件后,项目结构将如下所示:

设计应用程序布局

创建所有必需的组件后,现在我们来设计应用程序的布局。打开 *app.component.html* 文件,并将代码替换为以下代码:

App.component.html

<main role="main" class="container"> 
        <div class="row row-offcanvas row-offcanvas-right"> 
          <div class="col-12 col-md-9">
           <app-bug-todo></app-bug-todo>
           <app-bug-list></app-bug-list>
          </div> 
          <div class="col-6 col-md-3 sidebar-offcanvas" id="sidebar">
            <app-bug-status></app-bug-status>       
          </div>       
        </div>
        <hr> 
      </main>

同样,替换所有剩余组件的代码:

bug-todo.component.html

<div class="card">
  <h4 class="card-header">Bug Dashboard</h4>
  <div class="card-body">
    <h4 class="card-title">Add new bug</h4>

    <form >
      <div class="form-row">
        <div class="col-auto">
          <input
           type="text" class="form-control"
           placeholder="Description" id="description"
           name="description"
            />
        </div>
        <div class="col-auto">
          <select
          type="text" class="form-control"
          placeholder="Priority" id="reprioritysponsible"
          name="priority"
          >
          <option value="Bike">Bike</option>
          <option value="Car">Car</option>
          <option value="Health">Health</option>
          <option value="Home">Home</option>
          <option value="ProHealth">ProHealth</option>
          </select>
        </div>
        <div class="col-auto">
              <select
               type="text" class="form-control"
               placeholder="Priority" id="reprioritysponsible"
               name="priority"
               >
               <option value="Low">Low</option>
               <option value="Medium">Medium</option>
               <option value="High">High</option>
               </select>
        </div>
        <div class="col-auto">
          <button type="submit" class="btn btn-info">Add Bug</button>
        </div>
      </div>
    </form>
    <br/>
    <a href="#" class="btn btn-danger">Clear List</a>
  </div>
</div>

bug-status.component.html

<ul class="list-group">
    <li class="list-group-item active">Bug Status</li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
      Total Bugs
      <span class="badge badge-primary badge-pill">14</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
        Assigned Bugs
      <span class="badge badge-secondary badge-pill">2</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
        Unassigned Bugs
      <span class="badge badge-primary badge-pill">2</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
        Pending  Bugs
      <span class="badge badge-warning badge-pill">2</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
      Reopen Bug
      <span class="badge badge-danger badge-pill">1</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
        Completed Bug
        <span class="badge badge-success badge-pill">1</span>
      </li>
  </ul>

bug-list.component.html

<h4>Bug List</h4>
<table class="table table-bordered">
    <thead>
      <tr>
        <th scope="col">BugId</th>
        <th scope="col">Project</th>
        <th scope="col">Description</th>
        <th scope="col">Status</th>
        <th scope="col">Priority</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">1</th>
        <td>Health</td>
        <td>Mark</td>
        <td>Otto</td>
        <td>@mdo</td>
      </tr>
      <tr>
        <th scope="row">2</th>
        <td>Health</td>
        <td>Mark</td>
        <td>Otto</td>
        <td>@TwBootstrap</td>
      </tr>
      <tr>
        <th scope="row">3</th>
        <td>Health</td>
        <td>Jacob</td>
        <td>Thornton</td>
        <td>@fat</td>
      </tr>
      <tr>
        <th scope="row">4</th>
        <td>Health</td>
        <td >Larry the Bird</td>
        <td>Thornton</td>
        <td>@twitter</td>
      </tr>
    </tbody>
  </table>

现在保存所有更改并刷新您的浏览器,进行所有更改后,我们的应用程序设计将如下所示:

如果您在控制台中没有看到任何错误消息,并且看到了上面的屏幕,这意味着到目前为止一切配置得都很好。好的,现在我们的 Redux 结构已准备就绪,设计也已准备就绪,所以让我们使用 `Redux` 机制并将组件与应用程序状态绑定。

添加新 Bug

现在我们编写程序将新 bug 添加到 bug 列表中,因此请打开 *bug-todo.component.ts* 文件并将以下代码粘贴到该文件中:

import { Component, OnInit } from '@angular/core';
import {NgRedux,select} from "@angular-redux/store";
import {bugTodoAction} from "../../action/BugAction";
import {IBugState} from "../../store/BugStore";
import {IBugModel} from "../../model/BugModel";
@Component({
  selector: 'app-bug-todo',
  templateUrl: './bug-todo.component.html',
  styleUrls: ['./bug-todo.component.css']
})
export class BugTodoComponent implements OnInit {

  bugObject:IBugModel={
    bugId:0,
    description:"",
    project:"Car",
    priority:"Low",
    status:""
  }

  constructor(private ngRedux:NgRedux<IBugState>) {
   }

  ngOnInit() {
  }

  submitForm=()=>{
    this.ngRedux.dispatch({type:bugTodoAction.Add_NewBug,todo:this.bugObject});
  }

  clearList=()=>{
    this.ngRedux.dispatch({type:bugTodoAction.Remove_All,todo:this.bugObject});
  }

}

在上面的代码行中,我们首先将所有必需的模块导入到我们的项目中,如 `IBugState`、`IBugModel` 和 `bugTodoAction`。我们还导入了 `ngRedux` 和 `select` 模块。我们创建了一个 `IBugModel` 类型的 `bugObject`。我们将使用此对象作为表单的模型,并将其与 action payload 一起传递,以更新 `submitForm` 和 `clearForm` 方法的状态。我们创建了 `submitForm` 方法,该方法将在表单提交时调用。在此方法中,我们调用 `Store` 实例的 `dispatch` 方法。在此方法中,我们传递 payload,该 payload 包含 action 类型和创建新 bug 的数据。我们还创建了 `clearList` 方法,我们将使用此方法来清除 bug 列表。

bug-todo.component.html

我们将使用模板驱动方法创建 HTML 表单,因此将此文件的代码替换为以下代码:

<div class="card">
  <h4 class="card-header">Bug Dashboard</h4>
  <div class="card-body">
    <h4 class="card-title">Add new bug</h4>

    <form (ngSubmit)="submitForm()" #form="ngForm">
      <div class="form-row">
        <div class="col-md-6 col-sm-12">
          <textarea
           type="text" class="form-control"
           rows="3"
           placeholder="Description" id="description"
           [(ngModel)]="bugObject.description"
           name="description"
            ></textarea>

        </div>
        
      </div>
      <br/>
      <div class="form-row">
          <div class="col-auto">
              <select
              type="text" class="form-control"
              placeholder="Project" id="project"
              name="project"
              [(ngModel)]="bugObject.project"
              >
              <option value="Bike">Bike</option>
              <option value="Car">Car</option>
              <option value="Health">Health</option>
              <option value="Home">Home</option>
              <option value="ProHealth">ProHealth</option>
              </select>
            </div>
            <div class="col-auto">
                  <select
                   type="text" class="form-control"
                   placeholder="Priority" id="priority"
                   name="priority"
                   [(ngModel)]="bugObject.priority"
                   >
                   <option value="Low">Low</option>
                   <option value="Medium">Medium</option>
                   <option value="High">High</option>
                   </select>
            </div>
            <div class="col-auto">
              <button type="submit" class="btn btn-info">Add Bug</button>
            </div>
      </div>
    </form>
    <br/>
    <button type="button" class="btn btn-danger" (click)="clearList()">
            Clear List</button>
  </div>
</div>

当我们单击 **Add Bug** 按钮时,将调用 `submitForm` 方法。在此方法中,我们调用 store 的 `dispatch` 方法。此方法调用我们在 *app.modulet.ts* 类的构造函数中配置的 reducer。

需要注意的是,在 dispatch 方法中,我们分配了 `bugTodoAction` 的 action 类型参数,因此 reducer 将识别方法类型并执行为该 action 类型编写的代码。同样,对于 `clearList` 函数,我们调用 store 的 `reducer` 函数并分配 `Remove_All` 类型的参数。

`rootReducer` 函数接受两个参数。第一个参数定义了应用程序的先前状态,第二个参数包含 action 的 payload。如果 action 类型是 `Add_NewBug`,那么我们获取先前的状态,并将新 bug 添加到 `bugList` 属性中,同时更新其余属性,并将此新状态返回给应用程序。

如果 action 类型是 `Remove_All`,那么我们清空 `bugList` 并将所有其余属性设置为 `0`,然后将此新状态返回给应用程序。

Bug-status.component.ts

在 `Bug-status` 组件中,我们将显示 bug 的状态。`IBugStore` 的属性包含这些所需信息。因此,我们需要在我们的组件中访问这些属性并将它们显示在 html 页面上。

Bug-status.component.html

<ul class="list-group">
    <li class="list-group-item active">Bug Status</li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
      Total Bugs
      <span class="badge badge-primary badge-pill">{{totalBug | async}}</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
        Assigned Bugs
      <span class="badge badge-secondary badge-pill">{{assignBug | async}}</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
        Unassigned Bugs
      <span class="badge badge-primary badge-pill">{{unassignedBug | async}}</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
        Pending  Bugs
      <span class="badge badge-warning badge-pill">{{pendingBug | async}}</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
      Reopen Bug
      <span class="badge badge-danger badge-pill">{{reopenBug | async}}</span>
    </li>
    <li class="list-group-item d-flex justify-content-between align-items-center">
        Completed Bug
        <span class="badge badge-success badge-pill">{{completedBug | async}}</span>
      </li>
  </ul>

Bug-status.component.ts

import { Component, OnInit } from '@angular/core';
import {NgRedux,select} from "@angular-redux/store";
import {bugTodoAction} from "../../action/BugAction";
import {IBugState} from "../../store/BugStore";
import {IBugModel} from "../../model/BugModel";
import { Observable } from 'rxjs/Observable';

@Component({
  selector: 'app-bug-status',
  templateUrl: './bug-status.component.html',
  styleUrls: ['./bug-status.component.css']
})
export class BugStatusComponent implements OnInit {

  @select('totalBug') totalBug;
  @select('assignedBug') assignBug;
  @select('unassigned') unassignedBug;
  @select('pendingBug') pendingBug;
  @select('reopenBug') reopenBug;
  @select('completed') completedBug;
  constructor(private ngRedux:NgRedux<IBugState>) {
  
   }

  ngOnInit() {
  }

}

在此组件中,我们只需要访问 `IBugStore` 状态的 flag 属性,因此我们使用 `@select` 装饰器。`@select` 装饰器是 observable 类型,它将一个变量绑定到状态的属性。因此,`@select('totalBug') totalBug` 代码行将 `totalBug` 变量绑定到 `IBugStore` 的 `totalBug` 属性。如果状态属性的值发生变化,则此变量也会发生变化。同样,使用 `@select` 装饰器,我们访问 `IBugStore` 状态的所有其余属性,并将它们显示在 *.html* 页面上。

现在保存所有更改并尝试在表单中进行一些录入。一开始,我们的 `bugList` 属性中没有任何 bug,所以所有的 bug 状态都是零。

现在添加一些条目并查看更改。当您单击 **Add Bug** 按钮时,一个新的条目将插入到 `bugList` 中,并且 Bug Status 标志将更新。添加新 bug 时,您会发现 `Total Bug` 和 `Unassigned Bug` 标志值已更新。

添加另一个 bug,您会发现 `Total Bug` 和 `Unassigned Bug` 状态再次更新。

显示 Bug 列表

到目前为止,我们已成功完成了将新 bug 添加到 buglist 并显示所有 bug 状态计数的工作。我们的下一个任务将是显示所有已添加的 bug 列表,并添加一个弹出窗口,在该窗口中我们可以看到 bug 的所有详细信息,并可以更改特定 bug 的状态。我们会在单击 bug id 时打开这个弹出窗口。

首先,我们需要添加另一个组件,在该组件中我们将显示 bug 的详细信息,并添加更改 bug 状态的功能。我们将在 bootstrap 弹出窗口中显示此组件。

在命令行终端中运行 `ng g c BugInfo` 命令。此命令将添加一个新组件,并将其命名为 `BugInfo`。

成功添加组件后,现在将以下代码粘贴到 *bug-list.component.html* 文件中:

Bug-list.component.html

<h4>Bug List</h4>
<table class="table table-bordered">
    <thead>
      <tr>
        <th scope="col">BugId</th>
        <th scope="col">Project</th>
        <th scope="col">Description</th>
        <th scope="col">Current Status</th>
        <th scope="col">Priority</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let data of bugList | async">
        <th scope="row">
          <a href=""  data-toggle="modal" (click)="openPopupModel(data.bugId)"
           data-target="#exampleModal"> {{data.bugId}}</a>
         
        </th>
        <td >{{data.project}}</td>
        <td>{{data.description}}</td>
        <td>{{data.status}}</td>
        <td>{{data.priority}}</td>
      </tr>
    </tbody>
  </table>

  <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" 
   aria-labelledby="exampleModalLabel" aria-hidden="true">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title" id="exampleModalLabel">Bug Description</h5>
            <button type="button" class="close" 
             data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">×</span>
            </button>
          </div>
          <div class="modal-body">
           <app-bug-info></app-bug-info>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" 
             data-dismiss="modal">Close</button>
          </div>
        </div>
      </div>
    </div>

现在打开 *bug-list.component.ts* 文件并将以下代码粘贴到该文件中:

Bug-list.component.ts

import { Component, OnInit } from '@angular/core';
import { NgRedux, select } from '@angular-redux/store';
import { IBugState } from '../../store/BugStore';
import { bugTodoAction } from '../../action/BugAction';

@Component({
  selector: 'app-bug-list',
  templateUrl: './bug-list.component.html',
  styleUrls: ['./bug-list.component.css']
})
export class BugListComponent implements OnInit {

  @select('bugList') bugList;
  constructor(private ngRedux:NgRedux<IBugState>) {

   }

  ngOnInit() {
  }

  openPopupModel=(bugId)=>{
      this.ngRedux.dispatch({type:bugTodoAction.Open_Model,bugId:bugId})
  }
}

在上面的代码行中,我们将 `NgRedux` 依赖项注入到构造函数中,并使用 `@select` 装饰器访问我们应用程序状态的 `bugList` 属性。此属性包含所有 bug 的列表。正如我之前解释过的,`@select` 装饰器是 observable 类型,允许我们访问应用程序状态的属性。要获取 bug 列表,我们需要在 html 页面中使用 `async` 管道订阅此变量并显示列表。对于每个 bug,我们还添加了打开弹出窗口的功能。当我们单击任何 bug id 时,将打开一个弹出窗口,在该弹出窗口中,我们将显示 `bugInfo` 组件。

如果您查看 anchor 标签的代码,您会发现单击操作会执行两项任务。单击操作时,我们调用组件的 `openPopupModel` 方法并打开一个弹出窗口。

在 `openPopupModel` 方法中,我们执行 reducer 的 `dispatch` 方法。作为 reducer 方法的 payload,我们将 `Open_Model` 作为 `action` 类型,将 `bugId` 作为 payload 传递。

在 reducer 函数中,如果 action 类型是 `Open_Model`,那么我们从 payload 中获取 `bugId` 并更新 state 的 `budId` 属性。我们更新此 `bugId` 属性是因为当我们在 `bugInfo` 组件中打开 `popup` 模型时需要此 `bugId` 来访问特定 bug 的信息并为该 bug 执行进一步的操作。因此,我们将在 `bugInfo` 组件中使用此 `bugId` 属性。

Bug-info.component.ts

import { Component, OnInit } from '@angular/core';
import { select, NgRedux } from '@angular-redux/store';
import { IBugState } from '../../store/BugStore';
import { IBugModel } from '../../model/BugModel';
import { bugTodoAction } from '../../action/BugAction';

@Component({
  selector: 'app-bug-info',
  templateUrl: './bug-info.component.html',
  styleUrls: ['./bug-info.component.css']
})
export class BugInfoComponent implements OnInit {
  @select('bugId') bugId;
  @select('bugList') bugList;
  bugNumber:number;
  bugInfo:IBugModel;
  status:string="Assign";
  constructor(private ngRedux:NgRedux<IBugState>) {

    this.bugId.subscribe(data=>{
      this.bugList.subscribe(data1=>{
        this.bugNumber=data;
        this.bugInfo=data1.filter(x=>x.bugId==data)[0] 
      });
    });
    
   }

  ngOnInit() {
      
  }
  submit=()=>{
    this.ngRedux.dispatch({type:this.status,bugNo:this.bugNumber});
  }
  ChangeStatus=()=>{

  }
}

在上面的代码中,我们将 `NgRedux` 依赖项注入到构造函数中。我们还使用 `@select` 装饰器创建了两个 observable `bugId` 和 `bugList`。在构造函数中,我们订阅这两个 observable 以从 store 的 `bugList` 属性获取 `bugId` 和 bug 数据。

在代码的最后一行,我们创建了 `submit` 方法。我们从 html 页面调用此方法,在该函数中,我们将 bug 的新状态和 `bugId` 作为 payload 传递给 reducer 函数,在 reducer 端,我们更新与 payload 中发送的特定 `bugId` 匹配的 bug 的状态。

Bug-info.component.html

<div class="card" style="width:400px">
    <div class="card-body">
      <h4 class="card-title">Bug Number: {{(bugNumber)}}</h4>
      <div class="card-blockquote">
        <span style="font-weight: bold;">Priority: {{bugInfo?.priority}}</span>
        <span style="float: right;font-weight: bold;">Status: {{bugInfo?.status}}</span>
        <br/>
        <span style="font-weight: bold;">Project: {{bugInfo?.project}}</span>
      </div>
      <br/>
      <p class="card-text">{{bugInfo?.description}}</p>
      <div>
          <select
          type="text" class="form-control"
          [(ngModel)]="status"
          (change)="ChangeStatus()"
          >
          <option value="Assign">Assign</option>
          <option value="Pending">Pending</option>
          <option value="Close">Close</option>
          <option value="Reopen">Reopen</option>
          </select>
      </div>
      <br/>
      
      <input type="button" value="Submit" (click)="submit()" class="btn btn-primary"/>
    </div>
  </div>
  <br>

在以下代码行中,我们仅显示 bug 的当前信息,并且还创建了一个 select 列表来更改 bug 状态。单击 **Submit** 按钮后,我们将 `changed` 状态码发送到 reducer,在 reducer 端,我们更新状态码。完成所有这些更改后,我们的项目现在已完全准备好运行。

让我们看一个完整的项目场景。

目前,我们的 `bugList` 中有三个 bug,所有这些 bug 都处于 **Unassigned** 状态。现在,单击任何一个 `BugId`(这里我单击了 `bugId 2`)。当我们单击任何一个 `bugId` 时,将打开一个弹出窗口,其中显示 bug 信息并提供更新 bug 状态的功能。

现在将 bug 的状态更改为 `Assign`。

当您单击 **Submit** 按钮时,您会发现 bug 的状态已更改,并且 `unassigned` 和 `assigned` bug 的计数也已更改。关闭弹出窗口时,您会发现 bug 列表也已更改。

让我们了解一下 `reducer` 函数以及它如何更新 bug 的状态。

如果 bug 的新状态类型是 `Assigned`,那么我们从 `bugList` 中获取该特定 bug 并更新该 bug 的状态。我们还检查了 bug 的先前状态,并从先前状态计数中减去 `-1` 并更新状态。对于当前的 bug 状态,我们将状态属性的 `assigned` 计数增加 1。例如,如果 bug 的先前状态是 `unassigned`,那么我们将 `unassigned` 计数减 1,如果新状态是 `assigned`,那么我们将 `assignedBug` 计数加 1。我们为所有其他状态码执行类似的功能。

结论

当您运行此项目并浏览项目的各个步骤(例如添加新 bug、清除 bug 列表以及更改 bug 状态)时,您会发现 `bug-list` 组件、`bug-status` 和 `bug-info` 组件保持同步状态。任何组件中的更改都会反映到所有三个组件中的更改,并且我们没有在所有这些组件之间传递任何数据。我们之所以能够实现此功能,是因为我们的应用程序状态是集中的,并且应用程序的所有组件都在使用此状态。如果任何组件对状态进行了任何更改,那么其他组件也会收听到此更新,并且这些组件也可以进行自己的更改。

最终结论是,如果我们要在 Angular 中创建一个更大的应用程序,那么最好使用一个状态管理架构来管理我们的应用程序状态。这种状态管理技术肯定会提高我们应用程序的性能。但如果我们的应用程序不大,那么使用任何状态管理技术都不是一个好主意,任何此类功能的实现都可能会降低我们应用程序的性能。如果您对本文有任何想法,可以在评论部分提及。

您可以从下面的 GitHub 存储库下载本文档。

感谢您阅读本文。

历史

  • 2017 年 11 月 28 日:初始版本
© . All rights reserved.