Ngrx和Angular入门指南

Beginner's Guide to ngrx and Angular

介绍

状态管理是一个在处理应用程序数据结构时始终会想到的术语。

大型软件系统开发和维护中的最大问题是复杂性-大型系统难以理解。

反应式编程是指我们对随着时间流逝而流向我们的数据作出反应。

在本文中,我们将介绍ngrx的基础知识及其在Angular中的应用。

源代码

可以在此GitHub存储库中找到本文中实现的源代码。

什么是ngrx?

ngrx是Angular的一组RxJS驱动的状态管理库,其灵感来自Redux,Redux是一种流行且可预测的JavaScript应用程序状态管理容器。 它是由Angular Developer Advocate在2013年开发的。

以下是ngrx给我们带来的一些好处:

  • ngrx旨在将反应性扩展引入Angular。

  • @ ngrx / store将所有应用程序状态的类似Redux的单个存储区带到Angular。

  • ngrx / store是Redux的实现,它是使用RxJS开发的,同时保留了Redux的核心概念和API。

    受Redux启发的ngrx与它共享相同的原理,并使用RxJS对其进行增压。

    我们将在以下各节中介绍@ ngrx / store的内部工作原理。

    什么是RxJS?

    RxJS是用于反应式编程的JavaScript库,允许您使用异步或基于回调的数据流。

    谈论流-流是时间上的一系列值。 实时的事件和数据流(我们称为Observable s)是一种处理异步代码的绝妙方法。

    使用RxJS,我们将编写如下内容:

    1
    2
    3
    4
    5
    6
    7
    var button = document.querySelector('button');

    Rx.Observable.fromEvent(button, 'click')
        .subscribe(() => console.log('Clicked!'));

    var arr = Rx.Observable.of(90, 80)
        .subscribe((v) => console.log('Value:', v));

    您会发现,使用RxJS,我们只需很少的代码就可以实现很多目标。

    什么是Redux?

    如前所述,Redux是JavaScript应用程序的状态管理库。 尽管它最初是为React社区开发的,但也可以在原始JavaScript或任何其他JavaScript框架中使用。

    Redux是一个实现Flux思想的库。 Flux是一种流行的单向数据流(单向流)的设计模式,最早由Facebook提出。

    Redux中应用程序的状态保留在商店中。 使用传输到称为reducers的纯函数的动作来更新状态。 减速器将状态和操作作为参数,并对状态执行不可变的操作并返回新状态。

    核心概念

    在深入探讨@ ngrx / store的工作方式/原因之前,让我们看一下核心概念。 使用@ ngrx / store开发的应用程序必须处理Store,Reducers,State和Action。

    商店

    简而言之,存储是我们应用程序的"数据库"。 它包含在我们的应用程序中定义的不同状态。 因此,状态是不可变的,只能通过动作来改变。

    该商店将整个应用程序状态组合到一个实体中,充当Web应用程序的数据库。 就像传统的数据库一样,它代表了应用程序的记录点,您的商店可以视为客户端的"单一事实来源"。

    减速器

    如果商店是应用程序的数据库,那么约简器就是表。 约简器是一个纯函数,它接受两个参数-一个动作和一个具有与事件关联的类型和可选数据的先前状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    export function reducer(state = initialState, action: articles.Actions):State {
        switch(action.type) {
            case 'ADD_ARTICLE':
                return {
                    articles: [...state.articles,action.payload]
                }
            default:
                return state;
        }
    }

    状态是单个不变的数据结构。 状态是组成商店的原因。 如前所述,化简器就像表一样,因此state是表中的字段。

    动作

    Store包含了应用程序的状态,Reducer获得了存储的状态片或状态部分,但是当需要时我们如何更新Store? 这就是行动的作用。 动作表示从应用程序分发到商店的信息有效负载,通常由用户交互触发。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Action interface
    export interface Action {
        type: string,
        payload?: any
    }

    // Action with payload
    dispatch({type: 'ADD_ARTICLE', payload: {link: 'github.com/philipszdavido', points:90}})

    // Action without payload
    dispatch({type:'LOAD_LINKS'})

    调度动作后,Reducer会根据动作类型采用它并应用有效负载,并输出新状态。

    总结一下:存储包括整个状态,reduce返回状态的片段,而动作是预定义的用户触发的事件,用于传达给定状态框架应如何更改。

    店铺优势

    我们已经看到@ ngrx / store在管理应用程序中的状态方面多么有效和有用。 但是在继续展示它在Angular中的应用之前,我们将先看一下它的优势。

    该商店的主要优势是集中状态,测试,性能和DevTools。

  • 集中状态:商店中的状态保存在一个目录中。 它使预测商店的更新或更改以及跟踪问题变得更加容易

  • 测试:为纯函数编写测试很容易。 由于商店由减速器组成-它们是纯函数,仅使用其输入来产生其输出而没有副作用。 只需输入,然后声明输出即可。

  • 性能:反应性导致的单向数据流状态变化使其变得非常快速和高效。

  • DevTools:已经创建了一些很棒的工具来帮助开发人员。 一个示例是ngrx / store-devtool,它可以帮助开发人员在开发过程中进行"时间旅行"。 它还具有一些不错的功能,可帮助提供操作和状态更改的历史记录。

  • ngrx / store:幕后花絮

    @ ngrx / store是用RxJS的宗旨构建的。 BehaviorSubjectSubjectObservable是构成@ ngrx / store引擎的RxJS核心类型。 首先让我们了解这些概念,然后我们才能有效地使用该库。

    要很好地理解一个概念,您必须查看源代码。 @ ngrx / store对我来说一直是一个谜,直到我从其Git存储库下载该项目并深入到源代码中时,我才得到图片。 我看到了图书馆是如何建造的。 这样,我真的很熟悉这些东西。

    查看代码,您将看到@ ngrx / store有四个核心类StoreStateActionsSubjectReducerManager,它们在库中起主要作用。

  • Store是所有内容的起点,它实例化其他类。 它扩展了Observable类,以便我们可以订阅它以获得最新状态。

  • ActionsSubject处理将动作分配到Store中。

  • State保留最后发出的状态值。

  • ReducerManager保留reducer函数,并使用State中的状态值和ActionsSubject类中的操作调用reducer函数。

  • ngrx在实践中

    现在,我们来展示一下如何在Angular中使用@ ngrx / store。 为了演示@ ngrx / store的功能,我们将构建一个简单的"在线存储",该文件将允许用户执行以下操作:

  • 查看产品清单

  • 查看特定产品

  • 用户可以将产品添加到他们的购物车

  • 用户可以从购物车中删除产品

  • 样品申请

    我们将使用angular/cli设置我们的项目,您可以通过运行以下命令进行安装:

    1
    $ npm install angular/cli -g

    在这里,我们全局安装了angular/cli,以便可以在系统中的任何目录中使用它。

    建立

    现在设置好了。 我们将项目文件夹称为"在线存储"。 要搭建项目,请运行以下命令:

    1
    $ ng new online-store --minimal

    请注意使用minimal标志,该标志用于创建准系统Angular应用程序。 它生成"规范",HTML和CSS文件。 一切都会内联(在*.component.ts文件内部)。

    现在,我们的目录结构将如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    ├── online-store
      ├── src
        ├── app
          ├── app.component.ts
          └── app.module.ts
        ├── assets
          └── .gitkeep
        ├── environment
          ├── environment.prod.ts
          └── environment.ts
        ├── index.html
        ├── main.ts
        ├── polyfills.ts
        ├── style.css
        ├── tsconfig.app.json
        └── typings.d.ts
      ├── .angular-cli.json
      ├── .gitignore
      ├── package.json
      └── tsconfig.json

    现在,我们将安装Bootstrap以使我们的应用程序具有响应性和美观的外观:

    1
    $ npm install bootstrap -S

    将" style.css"重新显示为" style.scss",然后打开" style.scss"并添加以下行:

    1
    @import"~bootstrap/scss/bootstrap.scss"

    接下来,我们引入@ ngrx / store库:

    1
    $ npm install @ngrx/store @ngrx/core -S

    我们的应用程序将包含三个组件:

  • products.component:这将显示产品及其价格的列表。

  • cart.component:此组件显示我们添加到购物车中的所有物品。

  • product.component:此组件将显示所选产品的名称和详细信息。

  • 要搭建上述组件,请运行以下命令:

    1
    2
    3
    $ ng g c products --inline-style=true --spec=false
    $ ng g c cart --inline-style=true --spec=false
    $ ng g c product --inline-style=true --spec=false

    注意我们传递给ng g c命令的选项--inline-style=true --spec=falseng实用程序有很多选项,可以在Angular中使用以满足您的需求。

    在这里,传递--inline-style=true告诉Angular在ts文件中生成组件的样式。 --spec=true跳过生成测试*.spec.ts文件。

    接下来,我们将路由添加到我们的应用程序。 我们将创建三个路线:

  • / products:这将是我们的索引路径。 它将激活products.component

  • /购物车:这将激活cart.component以显示用户的购物车

  • / product /:id:此路由具有id参数,该参数将用于显示特定产品。

  • 要在我们的应用中启用路由,我们必须从app.module.ts中的@angular/router导入RouterModuleRoutes

    1
    2
    3
    4
    5
    6
    7
    // app.module.ts

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    ...
    import { Routes, RouterModule } from '@angular/router'
    ...

    接下来,我们定义类型为routes的变量routes。 它将包含我们的路线数组:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // app.module.ts

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    ...
    import { Routes, RouterModule } from '@angular/router'
    ...
    const routes: Routes = [
        {
            path: '',
            redirectTo: '/products',
            pathMatch: 'full'
        },
        {
            path: 'products',
            component: ProductsComponent
        },
        {
            path: 'cart',
            component: CartComponent
        },
        {
            path: 'product/:id',
            component: ProductComponent
        },
        {
            path: '**',
            redirectTo: '',
            pathMatch: 'full'
        }
    ];
    ...

    这代表了我们的应用可以进入的所有可能的路由器状态。

    如前所述,我们的应用程序具有三种途径:"产品","产品/:id"和"购物车"。 我们在此处添加了一些其他配置:

  • path: '':由于/ products是我们的索引页,因此将重定向到/ products。 实际上,我们只需添加component属性并将其分配给products.component即可创建索引页面。这取决于您。

  • path:'**':如果没有路由与用户请求相匹配,这将重定向回到/products页面。

  • 现在,要激活应用程序中的路由系统,我们在imports数组中调用RouterModuleforRoot方法,并将routes变量作为参数传递。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    // app.module.ts

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';

    import { AppComponent } from './app.component';
    import { ProductsComponent } from './products/products.component';
    import { CartComponent } from './cart/cart.component';
    import { ProductComponent } from './product/product.component';
    import { Routes, RouterModule } from '@angular/router';

    const routes: Routes = [
        {
            path: '',
            redirectTo: '/products',
            pathMatch: 'full'
        },
        {
            path: 'products',
            component: ProductsComponent
        },
        {
            path: 'cart',
            component: CartComponent
        },
        {
            path: 'product/:id',
            component: ProductComponent
        },
        {
            path: '**',
            redirectTo: '',
            pathMatch: 'full'
        }
    ];

    @NgModule({
      declarations: [
        AppComponent,
        ProductsComponent,
        CartComponent,
        ProductComponent
      ],
      imports: [
        BrowserModule,
        RouterModule.forRoot(routes)
      ],
      providers: [],
      bootstrap: [AppComponent]
    });

    export class AppModule { }

    最后,我们需要告诉Angular Router在哪里可以将我们的应用程序路由配置放置在DOM中。

    我们将元素添加到AppComponent的模板中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // app.component.ts

    import { Component } from '@angular/core';

    @Component({
      selector: 'app-root',
      template: `
      <p>
        app works!
        <router-outlet></router-outlet>
      </p>
      `,
      styles: []
    });

    export class AppComponent {
      title = 'app';
    };

    元素告诉Angular Router在DOM中插入匹配组件的位置。

    定义我们的减速器

    现在,我们将定义我们的商店,并开始在其上添加减速器。 让我们为所有与商店相关的文件创建一个名为" store"的中央文件夹:

    1
    $ mkdir src/app/store

    OK,现在让我们创建化简文件" reducer.ts":

    1
    $ touch src/app/store/reducer.ts

    该文件将包含我们的reducer函数,稍后我们将进行介绍。 如前所述,此应用程序的某些功能是处理"从购物车中移除产品"和"将产品添加到购物车"。 因此,我们的减速机将处理移除和添加到购物车中的产品。

    回到我们之前所说的," reducer.ts"文件将包含一个reducer函数,该函数接受以前的状态和当前调度的动作作为参数。 然后,我们需要实现一个切换案例系统,以检查正确的操作并对该状态进行重新计算。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // src/app/store/reducer.ts

    import { CartActionTypes, CartActions } from"./actions";

    export let initialState = []

    export function reducer(state=initialState, action: CartActions) {
        switch (action.type) {
            case CartActionTypes.ADD_PRODUCT:
                return [...state, action.payload]
            case CartActionTypes.REMOVE_PRODUCT:
                let product = action.payload        
                return state.filter((el)=>el.id != product.id)
            default:
                return state
        }
    }

    这是我们的减速器功能。 请记住,我们不想使用状态更改方法。 不容忍是这里的关键。

    我确定您想知道上面代码中的某些对象,例如CartActionTypesCartActions。 不用担心,我们将在"设置操作"部分进行介绍。

    初始化商店

    目前,我们在此应用中只有一个状态,因此只有一个" getter"减速器。 您可以在此处看到演练,每个状态项都有其自己的reducer功能。 在一个大型而复杂的应用程序中,我们可以有多个化简器,该应用程序中的每个状态项都可以使用一个化简器。 这些减速器将使用combineReducers功能组合为单个减速器功能。

    现在,让我们的减速器进入商店:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // app.module.ts

    ...
    import { StoreModule } from"@ngrx/store";
    import { reducer } from './store/reducer';

    ...

    @NgModule({
      declarations: [
          ...
      ],
      imports: [
        ...
        StoreModule.forRoot({cart: reducer})
      ],
      providers: [],
      bootstrap: [AppComponent]
    });

    export class AppModule { }

    在这里,我们使应用程序的商店可用于整个应用程序。 我们可以在任何地方访问它。 查看代码,我们从@ ngrx / store导入了StoreModule,并从src/app/store/reducer.ts文件导入了reducer函数。 然后,我们调用forRoot方法,将{cart: reducer}作为参数传递给imports数组。

    我们传入了设置了cart属性的对象,因为由于只有一个状态,所以我们不需要考虑状态片。 因此,我们只是告诉ngrx仅向我们发送cart状态。

    投射状态

    现在,我们已经使商店可访问,我们可以使用select函数访问状态之一。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // app.componen.ts

    import { Component } from '@angular/core';
    import { Store, select } from '@ngrx/store';

    @Component({
      selector: 'app-root',
      template: `
      <p>
        app works !!
        Cart: {{cart.length}}
        <router-outlet></router-outlet>
      </p>
      `,
      styles: []
    })

    export class AppComponent {
      title = 'app';
      cart: Array<any>

      constructor(private store: Store<any>) {}

      ngOnInit() {
        // Called after the constructor, initializing input properties, and the first call to ngOnChanges.
        // Add 'implements OnInit' to the class.
        this.store.select('cart').subscribe((state => this.cart = state))
      }
    }

    select函数从应用程序中提取所需的状态并返回Observable,以便我们可以订阅对该状态所做的更改。

    设置动作

    操作是对应用商店的查询。 他们"分派"要在商店上执行的动作。

    首先,我们需要一个模型。 我们状态的基础是一系列产品。 数组中的产品代表用户可以购买的商品。 用户可以将其添加到cart或将其删除。

    因此,我们知道我们的购物车如何包含产品/物品,并且产品可以具有以下内容:

  • 名称

  • 价钱

  • 标签

  • ID

  • 描述

  • 在这里,我们仅选择nameidprice表示产品。

    接下来,我们将为我们的产品创建一个模型:

    1
    2
    3
    4
    5
    6
    7
    // src/app/store/product.model.ts

    export class Product {
        id: number
        name: string
        price: number
    }

    接下来,我们将动作定义为实现@ ngrx / store Action类的自定义动作。

    而不是调度这样的动作:

    1
    store.dispatch({type: '', payload: ''})

    我们将动作创建为新的类实例:

    1
    this.store.dispatch(new Cart.AddProduct(product))

    将动作表示为类可以在减速器功能中进行类型检查。 要创建我们的动作类,首先我们创建一个enum来保存我们的动作类型。 请记住,此应用程序唯一要做的就是"添加到购物车"和"从购物车中删除"。 因此,我们的枚举将如下所示:

    1
    2
    3
    4
    5
    6
    // src/app/store/actions.ts

    export enum CartActionTypes {
        ADD_PRODUCT = 'ADD_PRODUCT',
        REMOVE_PRODUCT = 'REMOVE_PRODUCT'
    }

    注意:您需要先创建actions文件:touch src/app/store/actions.ts

    现在,通过实现Action接口定义动作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // src/app/store/actions.ts

    import { Action } from '@ngrx/store'

    ...
    export class AddProduct implements Action {
        readonly type = CartActionTypes.ADD_PRODUCT
        constructor(public payload: any){}
    }

    export class RemoveProduct implements Action {
        readonly type = CartActionTypes.REMOVE_PRODUCT
        constructor(public payload: any){}
    }

    AddProductRemoveProduct这些操作实现了Action接口,并且因为我们需要添加或删除产品,所以我们添加了一个构造函数以采用payload参数。 这样我们就可以在使用new关键字实例化对象时传递产品:

    1
    new Cart.AddProduct(product)

    最后,我们将为上面定义的所有操作定义一个type别名,以便在我们的reducer函数中使用它:

    1
    2
    3
    4
    // src/app/store/actions.ts

    ...
    export type CartActions = AddProduct | RemoveProduct

    这是完整的动作代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // src/app/store/actions.ts

    import { Action } from '@ngrx/store'

    export enum CartActionTypes {
        ADD_PRODUCT = 'ADD_PRODUCT',
        REMOVE_PRODUCT = 'REMOVE_PRODUCT'
    }

    export class AddProduct implements Action {
        readonly type = CartActionTypes.ADD_PRODUCT
        constructor(public payload: any){}
    }

    export class RemoveProduct implements Action {
        readonly type = CartActionTypes.REMOVE_PRODUCT
        constructor(public payload: any){}
    }

    export type CartActions = AddProduct | RemoveProduct

    设置组件

    我认为我们现在已完成设置商店的工作! 现在,让我们看看如何在我们的组件中利用它们。

    我们已经创建了应用程序中需要的所有组件。

    这将显示产品列表。 对于本文,我们将对产品列表进行硬编码。 您可以扩展此应用程序以从资源中加载产品,但我们将由您自己决定。

    为了对我们的产品列表进行硬编码,我们将创建一个保存我们的产品列表的文件。 我们将创建一个market.ts文件:

    1
    $ touch src/app/store/market.ts

    接下来,我们将初始化类型为Product的数组变量PRODUCTS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // src/app/store/market.ts

    import { Product } from"./product.model";

    export const PRODUCTS: Product[] = [
        {
          id: 0,
          name:"HP Inspirion",
          price: 700
        },
        {
          id: 1,
          name:"MacBook Pro 2018",
          price: 15000
        },
        {
          id: 2,
          name:"Dell 5500",
          price: 3000
        }
    ]

    现在,我们可以在需要的任何地方导入PRODUCTS

    为了显示products.component文件中的产品列表,我们将导入PRODUCTS

    1
    2
    3
    4
    // src/app/products/products.component.ts

    import { PRODUCTS } from"./../store/market";
    ...

    接下来,我们将其分配给products变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // src/app/products/products.component.ts

    ...
    export class ProductsComponent implements OnInit {

      products = PRODUCTS

      constructor() { }

      ngOnInit() { }
    }

    我们将制作一个漂亮的HTML来显示我们的产品列表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // src/app/products/products.component.ts

    ...
    @Component({
      selector: 'app-products',
      template: `
            <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12" *ngFor="let product of products">
                <div class="my-list">
                    <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                    <h3>{{product.name}}</h3>
                    <span>$</span>
                    <span class="pull-right">{{product.price}}</span>
                    <div class="offer">Extra 5% Off. Cart value $ {{0.5 * product.price}}</div>
                    <div class="detail">
                        <p>{{product.name}} </p>
                        <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                        <a [routerLink]="['/product',product.id]" class="btn btn-info">View</a>
                    </div>
                </div>    
      `,
      styles: [ ]
    })
    ...

    我们使用*ngFor指令遍历products数组,并使用表达式绑定将其显示在HTML中。

    将所有内容放在一起,src/app/products/products.componenet.ts将如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // src/app/products/products.componenet.ts

    import { Component, OnInit } from '@angular/core';
    import { PRODUCTS } from"./../store/market";

    @Component({
      selector: 'app-products',
      template: `
            <div class="col-lg-3 col-md-3 col-sm-6 col-xs-12" *ngFor="let product of products">
                <div class="my-list">
                    <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                    <h3>{{product.name}}</h3>
                    <span>$</span>
                    <span class="pull-right">{{product.price}}</span>
                    <div class="offer">Extra 5% Off. Cart value $ {{0.5 * product.price}}</div>
                    <div class="detail">
                        <p>{{product.name}} </p>
                        <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
                        <a [routerLink]="['/product',product.id]" class="btn btn-info">View</a>
                    </div>
                </div>    
      `,
      styles: [ ]
    })

    export class ProductsComponent implements OnInit {

      products = PRODUCTS

      constructor() { }

      ngOnInit() { }
    }

    接下来,打开" src / app / styles.scss"并添加以下SCSS代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    // src/app/styles.scss
    ...
    img {
        max-width: 100%;
    }

    img {
        transition: all .5s ease;
        -moz-transition: all .5s ease;
        -webkit-transition: all .5s ease
    }

    .my-list {
        width: 100%;
        padding: 10px;
        border: 1px solid #f5efef;
        float: left;
        margin: 15px 0;
        border-radius: 5px;
        box-shadow: 2px 3px 0px #e4d8d8;
        position: relative;
        overflow: hidden;
    }

    .my-list h3 {
        text-align: left;
        font-size: 14px;
        font-weight: 500;
        line-height: 21px;
        margin: 0px;
        padding: 0px;
        border-bottom: 1px solid #ccc4c4;
        margin-bottom: 5px;
        padding-bottom: 5px;
    }

    .my-list span {
        float: left;
        font-weight: bold;
    }

    .my-list span:last-child {
        float: right;
    }

    .my-list .offer {
        width: 100%;
        float: left;
        margin: 5px 0;
        border-top: 1px solid #ccc4c4;
        margin-top: 5px;
        padding-top: 5px;
        color: #afadad;
    }

    .detail {
        position: absolute;
        top: -100%;
        left: 0;
        text-align: center;
        background: #fff;
        height: 100%;
        width: 100%;
    }

    .my-list:hover .detail {
        top: 0;
    }

    这样做将使SCSS代码可用于我们所有的组件。

    在这里,我们可以查看产品,查看其价格,名称,然后根据需要添加到购物车。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    // src/app/product/product.component.ts

    import { Component, OnInit } from '@angular/core';
    import { PRODUCTS } from"./../store/market";
    import { Product } from"./../store/product.model"
    import { ActivatedRoute } from"@angular/router";
    import { Store } from"@ngrx/store";
    import * as Cart from"./../store/actions";

    @Component({
      selector: 'app-product',
      template:
      `
        <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
          <div class="my-list">
              <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
              <h3>{{product.name}}</h3>
              <span>$</span>
              <span class="pull-right">{{product.price}}</span>
              <div class="offer">
                Extra 5% Off. Cart value $ {{0.5 * product.price}}
              </div>
              <div class="offer">
                <a (click)="addToCart(product)" class="btn btn-info">Add To Cart</a>
              </div>
          </div>
        </div>
      `,
      styles: [ ]
    })

    export class ProductComponent implements OnInit {

      product:Product

      constructor(private route: ActivatedRoute, private store: Store<any>) { }

      ngOnInit() {
        this.route.params.subscribe((p)=>{
            let id = p['id']
            let result = Array.prototype.filter.call(PRODUCTS,(v)=>v.id == id)
            if (result.length > 0) {
              this.product = result[0]
            }
        })
      }

      addToCart(product) {
            this.store.dispatch(new Cart.AddProduct(product))
      }
    }

    在这里,我们已经导入了ActivatedRoute以便获取id参数。 它订阅路由事件流,然后通过PRODUCTS数组订阅filter,以获取数组中匹配的参数id

    当执行addToCart方法时,我们导入了Store以调度ADD_PRODUCT操作。

    这将在我们的购物车中显示产品。 这是代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // src/app/cart/cart.component.ts

    import { Component, OnInit } from '@angular/core';
    import { Store, select } from"@ngrx/store";
    import { Observable } from"rxjs/Observable";
    import * as Cart from"./../store/actions";

    @Component({
      selector: 'app-cart',
      template:
      `
        <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" *ngFor="let product of cart | async">
          <div class="my-list">
              <img src="http://hpservicecenterschennai.in/images/hp_laptop_service_centers_in_guindy.png" alt="" />
              <h3>{{product.name}}</h3>
              <span>$</span>
              <span class="pull-right">{{product.price}}</span>
              <div class="offer">
                Extra 5% Off. Cart value $ {{0.5 * product.price}}
                <a (click)="removeFromCart(product)" class="btn btn-info">Remove From Cart</a>
              </div>
          </div>
        </div>
      `,
      styles: []
    })

    export class CartComponent implements OnInit {

      cart: Observable<Array<any>>
      constructor(private store:Store<any>) {
        this.cart = this.store.select('cart')
      }

      ngOnInit() { }

      removeFromCart(product) {
        this.store.dispatch(new Cart.RemoveProduct(product))
      }
    }

    我们声明了类型为Observablecart变量。 然后,我们从商店select -ed cart状态。 select方法返回一个Observable,我们将其分配给先前声明的cart变量。 使用AsyncPipe |预订和接收购物车值。 使用removeFromCart方法将REMOVE_PRODUCT动作添加到存储中。

    这是我们的最终组成部分:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // src/app/app.component.ts

    import { Component } from '@angular/core';
    import { Store, select } from '@ngrx/store';
    import { Observable } from 'rxjs/Observable';

    @Component({
      selector: 'app-root',
      template: `
      <div class="container">
        <div class="row">
          <div class="col-sm-12">
            <h1 class="text-center">Online Store</h1>
            <h6 class="text-center"><a [routerLink]="['/cart']">Cart: {{(cart | async).length}}</a></h6>
            <hr />
          </div>
        </div>
        <router-outlet></router-outlet>
      </div>
      `,
      styles: []
    })

    export class AppComponent {
      title = 'app';
      constructor(private store: Store<any>) {}

      cart: Observable<Array<any>>

      ngOnInit() {
        // Called after the constructor, initializing input properties, and the first call to ngOnChanges.
        // Add 'implements OnInit' to the class.
        this.cart = this.store.select('cart')
      }
    }

    您会看到我们已经修改了模板,以包含我们的应用程序"在线商店"的名称。 我们订阅了购物车商店以获取产品数量,然后使用{{(cart | async).length}}表达式进行显示。 当我们在cart存储中添加或删除产品时,它会实时更新。

    您将在单向数据流中看到RxJs的强大功能。

    要查看我们已经完成的所有工作,请确保保存了每个文件并在终端中运行以下命令:

    1
    $ ng serve

    瞧! 现在您可以使用该应用程序了。

    利用AsyncPipe

    AsyncPipe是一个内置管道,我们可以在模板中使用它们来解包Promise s或Observable s中的数据。

    异步管道在组件中使用时,将其标记为要检查的更改。

    查看执行此操作的组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // src/app/app.component.ts

    ...
    @Component({
      selector: 'app-root',
      template: `
    ...
            <h6 class="text-center"><a [routerLink]="['/cart']">Cart: {{(cart | async).length}}</a></h6>
    ...
      `,
      ...
    })
    ...

    我们本可以使用subscribe方法获得相同的结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // src/app/app.component.ts

    ...
    @Component({
      selector: 'app-root',
      template: `
      <div class="container">
        <div class="row">
          <div class="col-sm-12">
            <h1 class="text-center">Online Store</h1>
            <h6 class="text-center"><a [routerLink]="['/cart']">Cart: {{cart.length}}</a></h6>
            <hr />
          </div>
        </div>
        <router-outlet></router-outlet>
      </div>
      `,
      styles: []
    })

    export class AppComponent {
      title = 'app';
      constructor(private store: Store<any>) {}

      cart: Array<any>

      ngOnInit() {
        // Called after the constructor, initializing input properties, and the first call to ngOnChanges.
        // Add 'implements OnInit' to the class.
        this.cart = this.store.select('cart')
            .subscribe(state => this.cart = state)
      }
    }

    您可以看到我们的代码变长了一点,但是仍然有效!

    因此,"异步管道"为我们执行订阅,并将值附加到我们的模板中。 仅用几行代码即可完成大量工作。 完善!

    使用Redux-DevTools进行调试

    Redux-Devtools是用于测试UI状态的"时间旅行"调试工具。 它可以进行应用程序开发并提高开发效率。

    使用这些工具,您可以从字面上移到应用程序的未来或过去状态(因此称为"时间旅行"说明)。

    Redux-DevTools的"锁定"和"暂停"功能使您可以从历史记录中删除过去的操作或禁用它们。

    我们可以在Angular应用中使用Redux-Devtools,但是ngrx团队开发了自己的dev-tool,可以在任何支持ngrx的应用中使用。

    可以通过运行以下命令来安装它:

    1
    $ npm i @ngrx/store-devtools -S

    这是下载Redux-DevTools扩展的链接。

    StoreDevtoolsModule.instrumentOnlyWithExtension()导入您的app.module.ts中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import { StoreDevtoolsModule } from '@ngrx/store-devtools'

    @NgModule({
        imports: [
            StoreDevtoolsModule.instrumentOnlyWithExtension({
                maxAge: 6
            })
        ]
    })

    export AppModule() {}

    我们将不讨论有关如何使用Redux-DevTools的技术细节。 相反,您可以将其作为一项任务来完成,方法是将其添加到该项目中,以提高自己,我希望收到您的来信! 随时向我发送PR!

    资源资源

    想更多地了解Angular,TypeScript和其他有用的前端库吗? 我建议您查看一些更详细的资源,例如在线课程:

  • Angular 5-完整指南

  • 角与角材料,Angularfire和NgRx

  • 了解TypeScript

  • 结论

    我知道,我们违反了几种最佳做法,但是最重要的是,您已经了解了如何使用@ ngrx / store来构建Angular应用程序。

    如果您迷路或需要参考代码,可以对其进行分叉或从我的GitHub存储库中克隆它。 您可以扩展该应用程序以执行我没有想到的事情。 随便玩吧。