查看原文
其他

手把手教你用ngrx管理Angular状态(下)

野草 前端早读课 2019-06-26

预览组件

现在让我们来创建标签预览组件,预期效果如下:



用命令行来创建TagPreviewComponent组件的手脚架,它将是Create页面和Complete页面的子组件,所以把这个组件放在app文件夹下:

$ ng g component tag-preview

预览组件脚本

打开tag-preview.component.ts文件,修改如下:

// src/app/tag-preview/tag-preview.component.ts
import { Component, OnChanges, Input } from '@angular/core';
import { PetTag } from './../core/pet-tag.model';

@Component({
 selector: 'app-tag-preview',
 templateUrl: './tag-preview.component.html',
 styleUrls: ['./tag-preview.component.css']
})
export class TagPreviewComponent implements OnChanges {
 @Input() petTag: PetTag;
 imgSrc = '';
 tagClipText: string;
 gemsText: string;

 constructor() { }

 ngOnChanges() {
   this.imgSrc = `/assets/images/${this.petTag.shape}.svg`;
   this.tagClipText = this.boolToText(this.petTag.clip);
   this.gemsText = this.boolToText(this.petTag.gems);
 }

 private boolToText(bool: boolean) {
   return bool ? 'Yes' : 'No';
 }

}

TagPreviewComponent是接受父组件CreateComponent输入的木偶子组件,它并不向父组件传递数据。引入Input装饰器以及OnChanges生命周期函数钩子,同时需要引入PetTag表明输入的类型。

TagPreviewComponent类需要实现OnChanges接口,也就是在该类中调用ngOnChanges方法。每当组件的输入参数发生变化时,ngOnChanges就会执行。这样才能实现在用户编辑修改的时候实时预览效果。

从父组件中接受的数据@Input() petTag是个状态对象,数据类型是之前定义的PetTag。比如,一个petTag对象可能是这样的:

{
 shape: 'bone',
 font: 'serif',
 text: 'Fawkes',
 clip: true,
 gems: false,
 complete: false
}

我们希望友好直观地展示数据,所以我们将会显示标签的形状图片,文案,以及是否包含了clip和gems。

当用户进行操作时,需要专门设置一下图片的路径以及clip和gems的文案(Yes或者No)。输入是由CreateComponent对tagState$这个可观察对象的订阅提供。

预览组件模版

打开tag-preview.component.html文件,添加代码如下:

<!-- src/app/tag-preview/tag-preview.component.html -->
<div *ngIf="petTag.shape" class="row tagView-wrapper">

 <div class="col-sm-12">
   <div class="tagView {{petTag.shape}}">
     <img [src]="imgSrc" />
     <div class="text {{petTag.font}}">
       {{petTag.text}}
     </div>
   </div>

   <p class="text-center">
     <strong>Tag clip:</strong> {{tagClipText}}<br>
     <strong>Gems:</strong> {{gemsText}}
   </p>
 </div>

</div>

预览将会在用户选择标签形状之后显示。用一个shape样式类来控制显示正确的形状图片,同时也会显示标签的文字,字体是用font样式类来控制的。最后,直接标注出用户是否选择了包含clip,gems两个特性。

预览组件样式

之前我们定义4个标签形状:骨头形,方形,圆形,心形。为了优雅地展示预览效果,我们需要额外的样式。打开tag-preview.component.css样式,添加如下:

/* src/app/tag-preview/tag-preview.component.css */
.tagView-wrapper {
 padding-top: 20px;
}
.tagView {
 height: 284px;
 position: relative;
 width: 100%;
}
img {
 display: block;
 height: 100%;
 margin: 0 auto;
 width: auto;
}
.text {
 font-size: 48px;
 position: absolute;
 text-align: center;
 text-shadow: 1px 1px 0 rgba(255,255,255,.8);
 top: 99px;
 width: 100%;
}
.bone .text,
.rectangle .text {
 font-size: 74px;
 top: 85px;
}
.sans-serif {
 font-family: Arial, Helvetica, sans-serif;
}
.serif {
 font-family: Georgia, 'Times New Roman', Times, serif;
}

除了一些定位之外,我们根据不同形状设置了不同的字体大小,根据用户选择设置不同字体。

现在,我们可以添加<app-tag-preview>到Create页面了。

将预览组件添加至Create页面

打开create.component.htm,将该组件添加至最底部:

<!-- src/app/pages/create/create.component.html -->
...
<app-tag-preview
 [petTag]="petTag"></app-tag-preview>

方括号[...]单向绑定属性,我们在CreateComponen组件的订阅tagStateSubscription中已经创建了petTag,现在将其传递给预览组件。

现在,我们应该可以看到标签的实时预览效果了:



提交完成标签

有了标签生成器和标签预览之后,添加一个Done按钮来提交创建好的标签到Complete页面。完成之后,页面如下:



我们已经在CreateComponent中创建了submit()方法,该方法派发带有载体的COMPLETE行为至归约器。我们只需要在create.component.html页面创建一个调用该方法的按钮即可:

<!-- src/app/pages/create/create.component.html -->
...
<div class="row">
 <div class="col-sm-12 text-center">
   <p class="form-text text-muted" *ngIf="petTag.shape">
     Preview your customized tag above.<br>
     If you're happy with the results,<br>
     click the button below to finish!
   </p>
   <p>
     <button
       class="btn btn-success btn-lg"
       *ngIf="petTag.shape"
       [disabled]="!done"
       (click)="submit()"
       routerLink="/complete">Done</button>
   </p>
 </div>
</div>

我们之前在CreateComponent的tagStateSubscription对象中定义了done属性。 当done属性是false的时候,我们禁用提交按钮。

this.done = !!(this.petTag.shape && this.petTag.text);

当标签有形状和文字时,我们就认为该标签已经可以提交了。如果用户已经添加了这些,他们就可以点击提交按钮完成标签的创建。同时,我们将用户导航至Complete页面。

“Complete”页面组件

在设置路由的时候,我们就搭建好了Complete页面的手脚架。当这个页面创建好之后,页面效果如下(前提是用户创建了一个标签):



“Complete”页面组件脚本

打开智能组件complete.component.ts,添加代码如下:

// src/app/pages/complete/complete.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Store } from '@ngrx/store';
import { RESET } from './../../core/pet-tag.actions';
import { PetTag } from './../../core/pet-tag.model';

@Component({
 selector: 'app-complete',
 templateUrl: './complete.component.html'
})
export class CompleteComponent implements OnInit, OnDestroy {
 tagState$: Observable<PetTag>;
 private tagStateSubscription: Subscription;
 petTag: PetTag;

 constructor(private store: Store<PetTag>) {
   this.tagState$ = store.select('petTag');
 }

 ngOnInit() {
   this.tagStateSubscription = this.tagState$.subscribe((state) => {
     this.petTag = state;
   });
 }

 ngOnDestroy() {
   this.tagStateSubscription.unsubscribe();
 }

 newTag() {
   this.store.dispatch({
     type: RESET
   });
 }

}

CompleteComponent组件是可路由的容器组件。需要导入OnInit, OnDestroy, Observable, Subscription以及Store管理store订阅。同时,有个重置按钮,用户点击之后可以重新创建标签。store中的状态也会跟着重置,所以需要导入RESET行为, PetTag数据类型以及默认初始值initialTag。

这个组件不需要Bootstrap以外的样式,所以我们可以把样式文件complete.component.css以及相应的引用删掉。

类似CreateComponent组件,我们需要创建tagState$的可观察对象tagStateSubscription,以及局部变量petTag。同时,我们需要创建PetTag数据类型的emptyTag变量,然后将其赋值为initialTag。

在构造函数中,将tagState$设为store可观察对象。然后在ngOnInit()函数中,订阅这个可观察对象,并且设置petTag属性。在ngOnDestroy()函数中,通过退订来销毁订阅。最后,newTag()函数派发RESET行为,使得应用的状态重置,用户可以继续定制他们的下一个标签了。

“Complete”页面组件模板

CompleteComponent组件的模板代码如下:

<!-- src/app/pages/complete/complete.component.html -->
<div *ngIf="petTag.complete">
 <div class="row">
   <p class="col-sm-12 alert alert-success">
     <strong>Congratulations!</strong> You've completed a pet ID tag for <strong>{{petTag.text}}</strong>. Would you like to <a (click)="newTag()" routerLink="/create" class="alert-link">create another?</a>
   </p>
 </div>
 <app-tag-preview [petTag]="petTag"></app-tag-preview>
</div>

<div *ngIf="!petTag.complete" class="row">
 <p class="col-sm-12 alert alert-danger">
   <strong>Oops!</strong> You haven't customized a tag yet. <a routerLink="/create" class="alert-link">Click here to create one now.</a>
 </p>
</div>

首先展示恭喜用户成功为他们的宠物创建个性标签的提示,名字也会从petTag状态对象中取过来。同时,提供一个能重新创建新标签的链接,点击之后执行newTag()方法,该方法会将路由导航到创建页面重新开始。

接着,展示带有petTag的标签预览组件: <app-tag-preview [petTag]="petTag">。

最后,如果用户在没有完成标签的定制下手动导航到/complete页面的话,我们就会显示一个错误信息。同样有个链接,可以让用户回到创建页面。错误页面效果如下:



至此,我们简单的Angular + ngrx/store应用完成了。

题外话:你可能不需要ngrx/store

状态管理库很棒,但请你确保在正式投入实际项目前你已经读过了这篇文章You Might Not Need Redux。

这个例子很简单,因为我们是用ngrx/store来教学。当你想用来投入实际项目时,你需要权衡一下必要性以及它的利弊。Angular(吸纳了RxJS)已经可以很方便地用service管理全局状态。因此,小型简单应用用局部变量就能很好地维护了。这种场景下,如果引入非必要的ngrx/store,可能会带来困扰和麻烦。

而在管理大型复杂项目的状态时,ngrx/store及其同类库是非常出色的工具。希望你现在已经有能力判断Redux和ngrx/store使用的原理。这样你就能知道如何以及何时应该使用状态管理库了。

附状态管理相关资源


附上一些学习状态管理的好资源:

  • ngrx/store on GitHub

  • @ngrx/store in 10 minutes

  • Comprehensive Introduction to @ngrx/store

  • ng-conf: Reactive Angular 2 with ngrx - Rob Womald

  • Angular 2 Service Layers: Redux, RxJS and Ngrx Store - When to Use a Store and Why?

  • Getting Started with Redux - Dan Abramov on Egghead.io


Angular的服务和组件通信在小型应用中的数据传递变得相当简单,但在复杂应用中仍是个棘手的问题。类似ngrx/store的全局store在组织状态管理中起到了很大的辅助作用。希望你现在已经准备好用ngrx/store构建自己的Angular 应用了!

关于本文

译者:@野草

译文:https://github.com/fezaoduke/TranslationInstitute/blob/master/手把手教你用ngrx管理Angular状态.md

作者:@Kim MaidaMr Raindrop

原文:https://auth0.com/blog/managing-state-in-angular-with-ngrx-store/

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存