摩拜前端重构angular

在本系列的第1部分中,我们学习了如何启动并运行Todo应用程序并将其部署到GitHub页面。 这样做很好,但不幸的是,整个应用程序都挤在一个组件中。 在本文中,我们将研究更模块化的组件体系结构。 我们将研究如何将单个组件分解为较小的组件的结构化树,这些较小的组件更易于理解,重用和维护。

本文是SitePoint Angular 2+教程的第2部分,有关如何使用Angular CLI创建CRUD应用程序。

  1. 第0部分— Ultimate Angular CLI参考指南
  2. 第1部分-启动并运行我们的Todo应用程序的第一个版本
  3. 第2部分-创建单独的组件以显示待办事项列表和一个待办事项
  4. 第3部分-更新Todo服务以与REST API通信
  5. 第4部分-使用Angular路由器解析数据
  6. 第5部分-添加身份验证以保护私有内容
  7. 第6部分—如何将Angular项目更新到最新版本。

并不需要遵循本教程的第一部分,对第二部分是有道理的。 您可以简单地获取一份我们的仓库的副本,从第一部分中检出代码,然后以此为起点。 下面将对此进行详细说明。

角组件架构

快速回顾

因此,让我们更详细地看一下第一部分中介绍的内容。 我们学习了如何:

  • 使用Angular CLI初始化我们的Todo应用程序
  • 创建一个Todo类来代表单个Todo
  • 创建TodoDataService服务以创建,更新和删除待办事项
  • 使用AppComponent组件显示用户界面
  • 将我们的应用程序部署到GitHub页面。

第1部分的应用程序架构如下所示:

应用架构

我们讨论的组件标有红色边框。

在第二篇文章中,我们将把AppComponent所做的一些工作委托给更易于理解,重用和维护的较小的组件。

我们将创建:

  • TodoListComponent以显示TodoListComponent列表
  • TodoListItemComponent以显示单个待办事项
  • 一个TodoListHeaderComponent来创建一个新的待办事项
  • TodoListFooterComponent来显示还剩下多少个TodoListFooterComponent

应用架构

到本文结尾,您将了解:

  • Angular组件架构的基础
  • 如何使用属性绑定将数据传递到组件中
  • 如何使用事件侦听器侦听组件发出的事件
  • 为什么将组件拆分为较小的可重用组件是一个好习惯
  • 智能组件和组件之间的区别以及为什么保持组件是一个好习惯。

因此,让我们开始吧!

启动并运行

您需要在本文中遵循的第一件事是Angular CLI的最新版本。 您可以使用以下命令进行安装:

npm install -g @angular/cli@latest

如果您需要删除以前版本的Angular CLI,请按以下步骤操作:

npm uninstall -g @angular/cli angular-cli
npm cache clean
npm install -g @angular/cli@latest

之后,您将需要第一部分中的代码副本。 可从https://github.com/sitepoint-editors/angular-todo-app获得 。 本系列中的每篇文章在存储库中都有一个相应的标记,因此您可以在应用程序的不同状态之间来回切换。

我们在第一部分结尾和在本文开始的代码被标记为part-1 。 本文结尾处的代码标记为part-2

您可以将标签视为特定提交ID的别名。 您可以使用git checkout在它们之间切换。 您可以在此处阅读更多内容

因此,要启动并运行(已安装Angular CLI的最新版本),我们将执行以下操作:

git clone git@github.com:sitepoint-editors/angular-todo-app.git
cd angular-todo-app
npm install
git checkout part-1
ng serve

免费学习PHP!

全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。

原价$ 11.95 您的完全免费

然后访问http:// localhost:4200 / 。 如果一切顺利,您应该会看到正在运行的Todo应用程序。

原始AppComponent

让我们打开src/app/app.component.html ,看看在第一AppComponent中完成的AppComponent

<section class="todoapp">
  <header class="header">
    <h1>Todos</h1>
    <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
  </header>
  <section class="main" *ngIf="todos.length > 0">
    <ul class="todo-list">
      <li *ngFor="let todo of todos" [class.completed]="todo.complete">
        <div class="view">
          <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
          <label>{{todo.title}}</label>
          <button class="destroy" (click)="removeTodo(todo)"></button>
        </div>
      </li>
    </ul>
  </section>
  <footer class="footer" *ngIf="todos.length > 0">
    <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
  </footer>
</section>

这是src/app/app.component.ts对应的类:

import {Component} from '@angular/core';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [TodoDataService]
})
export class AppComponent {

  newTodo: Todo = new Todo();

  constructor(private todoDataService: TodoDataService) {
  }

  addTodo() {
    this.todoDataService.addTodo(this.newTodo);
    this.newTodo = new Todo();
  }

  toggleTodoComplete(todo: Todo) {
    this.todoDataService.toggleTodoComplete(todo);
  }

  removeTodo(todo: Todo) {
    this.todoDataService.deleteTodoById(todo.id);
  }

  get todos() {
    return this.todoDataService.getAllTodos();
  }

}

尽管我们的AppComponent可以正常工作,但是将所有代码都保留在一个大组件中并不能很好地扩展,因此不建议这样做。

向我们的Todo应用程序添加更多功能将使AppComponent更大,更复杂,从而使其更难以理解和维护。

因此,建议将功能委派给较小的组件。 理想情况下,较小的组件应该是可配置的,以便在业务逻辑更改时我们不必重写其代码。

例如,在本系列的第三部分中,我们将更新TodoDataService以与REST API进行通信,并且我们希望确保在重构TodoDataService时不必更改任何较小的组件。

如果我们查看AppComponent模板,则可以将其基础结构提取为:

<!-- header that lets us create new todo -->
<header></header>

<!-- list that displays todos -->
<ul class="todo-list">

    <!-- list item that displays single todo -->
    <li>Todo 1</li>

    <!-- list item that displays single todo -->
    <li>Todo 2</li>
</ul>

<!-- footer that displays statistics -->
<footer></footer>

如果将此结构转换为Angular组件名称,则会得到:

<!-- TodoListHeaderComponent that lets us create new todo -->
<app-todo-list-header></app-todo-list-header>

<!-- TodoListComponent that displays todos -->
<app-todo-list>

    <!-- TodoListItemComponent that displays single todo -->
    <app-todo-list-item></app-todo-list-item>

    <!-- TodoListItemComponent that displays single todo -->
    <app-todo-list-item></app-todo-list-item>
</app-todo-list>

<!-- TodoListFooterComponent that displays statistics -->
<app-todo-list-footer></app-todo-list-footer>

让我们看看如何利用Angular的组件驱动开发的力量来实现这一目标。

更具模块化的组件体系结构-创建TodoListHeaderComponent

让我们从创建TodoListHeader组件开始。

从项目的根源开始,我们使用Angular CLI为我们生成组件:

$ ng generate component todo-list-header

这会为我们生成以下文件:

create src/app/todo-list-header/todo-list-header.component.css
create src/app/todo-list-header/todo-list-header.component.html
create src/app/todo-list-header/todo-list-header.component.spec.ts
create src/app/todo-list-header/todo-list-header.component.ts

它会自动将TodoListHeaderComponent添加到AppModule声明中:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';

// Automatically imported by Angular CLI
import { TodoListHeaderComponent } from './todo-list-header/todo-list-header.component';

@NgModule({
  declarations: [
    AppComponent,

    // Automatically added by Angular CLI
    TodoListHeaderComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

需要将组件添加到模块声明中,以确保模块中的所有视图模板都可以使用该组件。 Angular CLI为我们方便地添加了TodoListHeaderComponent ,因此我们不必手动添加它。

如果TodoListHeaderComponent不在声明中,而我们在视图模板中使用了它,Angular将抛出以下错误:

Error: Uncaught (in promise): Error: Template parse errors:
'app-todo-list-header' is not a known element:
1. If 'app-todo-list-header' is an Angular component, then verify that it is part of this module.
2. If 'app-todo-list-header' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.

要了解有关模块声明的更多信息,请确保查看Angular Module FAQ

现在我们已经为新的TodoListHeaderComponent生成了所有文件,我们可以将<header>元素从src/app/app.component.html移至src/app/todo-list-header/todo-list-header.component.html

<header class="header">
  <h1>Todos</h1>
  <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title"
         (keyup.enter)="addTodo()">
</header>

还将相应的逻辑添加到src/app/todo-list-header/todo-list-header.component.ts

import { Component, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo';

@Component({
  selector: 'app-todo-list-header',
  templateUrl: './todo-list-header.component.html',
  styleUrls: ['./todo-list-header.component.css']
})
export class TodoListHeaderComponent {

  newTodo: Todo = new Todo();

  @Output()
  add: EventEmitter<Todo> = new EventEmitter();

  constructor() {
  }

  addTodo() {
    this.add.emit(this.newTodo);
    this.newTodo = new Todo();
  }

}

而不是将TodoDataService注入新的TodoListHeaderComponent以保存新的待办事项,而是发出一个add事件并将新的待办事项作为参数传递。

我们已经了解到,Angular模板语法允许我们将处理程序附加到事件。 例如,考虑以下代码:

<input (keyup.enter)="addTodo()">

这告诉Angular在输入中按下Enter键时运行addTodo()方法。 之所以keyup.enter ,是因为keyup.enter事件是Angular框架定义的事件。

但是,我们还可以通过创建EventEmitter并使用@Output()装饰器装饰组件,使组件发出自己的自定义事件:

import { Component, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo';

@Component({
  // ...
})
export class TodoListHeaderComponent {

  // ...

  @Output()
  add: EventEmitter<Todo> = new EventEmitter();

  addTodo() {
    this.add.emit(this.newTodo);
    this.newTodo = new Todo();
  }

}

因此,我们现在可以使用Angular的事件绑定语法在视图模板中分配事件处理程序:

<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>

每次我们在TodoListHeaderComponent调用add.emit(value)TodoListHeaderComponent调用add.emit(value) onAddTodo($event)处理函数,并且$event等于value

这将我们的TodoListHeaderComponentTodoDataService分离,并允许父组件决定创建新的待办事项时需要做什么。

当我们在TodoDataService部分中更新TodoDataService与REST API通信时,我们不必担心TodoListHeaderComponent因为它甚至不知道TodoDataService存在。

智能与哑组件

您可能已经听说过智能组件 。 将TodoListHeaderComponentTodoDataService使TodoListHeaderComponent成为愚蠢的组件。 愚蠢的组件不知道外部发生了什么。 它仅通过属性绑定接收输入,并且仅将输出数据作为事件发出。

使用智能哑巴组件是一个好习惯。 它极大地改善了关注点的分离,使您的应用程序更易于理解和维护。 如果您的数据库或后端API发生了变化,则不必担心笨拙的组件。 它还使您的哑组件更加灵活,使您可以在不同情况下更轻松地重用它们。 如果您的应用程序需要两次相同的组件,一次需要将其写入后端数据库,而另一次需要将其写入内存数据库,那么哑巴组件可以使您准确地完成此任务。

现在,我们已经创建了TodoListHeaderComponent ,让我们更新AppComponent模板以使用它:

<section class="todoapp">

  <!-- header is now replaced with app-todo-list-header -->
  <app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>

  <section class="main" *ngIf="todos.length > 0">
    <ul class="todo-list">
      <li *ngFor="let todo of todos" [class.completed]="todo.complete">
        <div class="view">
          <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
          <label>{{todo.title}}</label>
          <button class="destroy" (click)="removeTodo(todo)"></button>
        </div>
      </li>
    </ul>
  </section>

  <footer class="footer" *ngIf="todos.length > 0">
    <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
  </footer>

</section>

请注意,当用户输入新的待办事项标题时,我们如何使用onAddTodo($event)处理程序捕获TodoListHeaderComponent发出的add事件:

<app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>

我们将onAddTodo()处理函数添加到AppComponent类中,并删除不再需要的逻辑:

import {Component} from '@angular/core';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [TodoDataService]
})
export class AppComponent {

  // No longer needed, now handled by TodoListHeaderComponent
  // newTodo: Todo = new Todo();

  constructor(private todoDataService: TodoDataService) {
  }

  // No longer needed, now handled by TodoListHeaderComponent
  // addTodo() {
  //   this.todoDataService.addTodo(this.newTodo);
  //   this.newTodo = new Todo();
  // }

  // Add new method to handle event emitted by TodoListHeaderComponent
  onAddTodo(todo: Todo) {
    this.todoDataService.addTodo(todo);
  }

  toggleTodoComplete(todo: Todo) {
    this.todoDataService.toggleTodoComplete(todo);
  }

  removeTodo(todo: Todo) {
    this.todoDataService.deleteTodoById(todo.id);
  }

  get todos() {
    return this.todoDataService.getAllTodos();
  }

}

现在,我们已经成功地将<header>元素和所有基础逻辑从AppComponent移至其自己的TodoListHeaderComponent

TodoListHeaderComponent是一个愚蠢的组件, AppComponent仍然负责使用TodoDataService存储待办事项。

接下来,让我们解决TodoListComponent

创建TodoListComponent

让我们再次使用Angular CLI生成我们的TodoListComponent

$ ng generate component todo-list

这会为我们生成以下文件:

create src/app/todo-list/todo-list.component.css
create src/app/todo-list/todo-list.component.html
create src/app/todo-list/todo-list.component.spec.ts
create src/app/todo-list/todo-list.component.ts

它还会自动将TodoListComponent添加到AppModule声明中:

// ...
import { TodoListComponent } from './todo-list/todo-list.component';

@NgModule({
  declarations: [
    // ...
    TodoListComponent
  ],
  // ...
})
export class AppModule { }

现在,我们从src/app/app.component.html获取与待办事项列表相关HTML:

<section class="main" *ngIf="todos.length > 0">
  <ul class="todo-list">
    <li *ngFor="let todo of todos" [class.completed]="todo.complete">
      <div class="view">
        <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
        <label>{{todo.title}}</label>
        <button class="destroy" (click)="removeTodo(todo)"></button>
      </div>
    </li>
  </ul>
</section>

我们还将其移至src/app/todo-list/todo-list.component.html

<section class="main" *ngIf="todos.length > 0">
  <ul class="todo-list">
    <li *ngFor="let todo of todos" [class.completed]="todo.complete">
      <app-todo-list-item
        [todo]="todo"
        (toggleComplete)="onToggleTodoComplete($event)"
        (remove)="onRemoveTodo($event)"></app-todo-list-item>
    </li>
  </ul>
</section>

注意,我们引入了一个尚不存在的TodoListItemComponent 。 但是,将其添加到模板中已经使我们能够探索TodoListItemComponent应该提供的API。 这使我们在TodoListItemComponent中编写TodoListItemComponent更加容易,因为我们现在知道我们期望TodoListItemComponent具有哪些输入和输出。

我们使用[todo] 输入属性语法通过todo属性传递todo项,并将事件处理程序附加到我们期望TodoListItemComponent发出的事件上,例如toggleComplete事件和remove事件。

让我们打开src/app/todo-list/todo-list.component.ts并添加视图模板所需的逻辑:

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Todo } from '../todo';

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

  @Input()
  todos: Todo[];

  @Output()
  remove: EventEmitter<Todo> = new EventEmitter();

  @Output()
  toggleComplete: EventEmitter<Todo> = new EventEmitter();

  constructor() {
  }

  onToggleTodoComplete(todo: Todo) {
    this.toggleComplete.emit(todo);
  }

  onRemoveTodo(todo: Todo) {
    this.remove.emit(todo);
  }

}

为了进一步说明智能组件和组件之间的区别,我们还将TodoListComponent 组件。

首先我们定义了一个输入属性 todos与标记它@Input()装饰。 这使我们能够从父组件中注入todos

接下来,我们使用@Output()装饰器定义两个输出事件, removetoggleComplete 。 请注意,我们如何将它们的类型设置为EventEmitter<Todo>并为每个对象分配一个新的EventEmitter实例。

EventEmitter<Todo>类型批注是TypeScript泛型 ,它告诉TypeScript removetoggleComplete都是EventEmitter实例,它们发出的值是Todo实例。

最后,我们使用(toggleComplete)="onToggleTodoComplete($event)"(remove)="onRemoveTodo($event)"定义我们在视图中指定的onToggleTodoComplete(todo)onRemoveTodo(todo)事件处理程序。

注意我们如何在视图模板中使用$event作为参数名称,并在方法定义中使用todo作为参数名称。 要访问Angular模板中事件的有效负载(发射值),我们必须始终使用$event作为参数名称。

因此,通过指定(toggleComplete)="onToggleTodoComplete($event)"在我们的视图模板,我们告诉角度使用事件负载作为第一个参数调用时onToggleTodoComplete方法,这将在第一个参数匹配onToggleTodoComplete方法,即todo

我们知道有效负载将是一个todo实例,因此我们将onToggleTodoComplete方法定义为onToggleTodoComplete(todo: Todo) ,这使我们的代码更易于阅读,理解和维护。

最后,我们定义事件处理程序以在接收到有效载荷时也发出toggleCompleteremove事件,并将todo指定为事件有效载荷。

本质上,我们让TodoListComponent对其子TodoListItemComponent实例中的事件进行TodoListItemComponent

这使我们能够处理TodoListComponent之外的业务逻辑,从而使TodoListComponent保持愚蠢 ,灵活和轻便。

我们还需要在AppComponent重命名两个方法来反映这一点:

...
export class AppComponent {

  // rename from toggleTodoComplete
  onToggleTodoComplete(todo: Todo) {
    this.todoDataService.toggleTodoComplete(todo);
  }

  // rename from removeTodo
  onRemoveTodo(todo: Todo) {
    this.todoDataService.deleteTodoById(todo.id);
  }

}

如果我们在此阶段尝试运行应用程序,Angular将抛出错误:

Unhandled Promise rejection: Template parse errors:
Can't bind to 'todo' since it isn't a known property of 'app-todo-list-item'.
1. If 'app-todo-list-item' is an Angular component and it has 'todo' input, then verify that it is part of this module.
2. If 'app-todo-list-item' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.

那是因为我们还没有创建TodoListItemComponent

接下来,让我们继续。

创建TodoListItemComponent

同样,我们使用Angular CLI生成TodoListItemComponent

$ ng generate component todo-list-item

这将生成以下文件:

create src/app/todo-list-item/todo-list-item.component.css
create src/app/todo-list-item/todo-list-item.component.html
create src/app/todo-list-item/todo-list-item.component.spec.ts
create src/app/todo-list-item/todo-list-item.component.ts

它将TodoListItemComponent自动添加到AppModule声明中:

// ...
import { TodoListItemComponent } from './todo-list-item/todo-list-item.component';

@NgModule({
  declarations: [
    // ...
    TodoListItemComponent
  ],
  // ...
})
export class AppModule { }

现在,我们可以将原始标记从<li>内部移动到src/app/todo-list-item.component.html

<div class="view">
  <input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">
  <label>{{todo.title}}</label>
  <button class="destroy" (click)="removeTodo(todo)"></button>
</div>

我们不必对标记进行任何更改,但必须确保事件得到正确处理,因此让我们在src/app/todo-list-item/todo-list-item.component.ts添加必要的代码TodoListItemComponent src/app/todo-list-item/todo-list-item.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Todo } from '../todo';

@Component({
  selector: 'app-todo-list-item',
  templateUrl: './todo-list-item.component.html',
  styleUrls: ['./todo-list-item.component.css']
})
export class TodoListItemComponent {

  @Input() todo: Todo;

  @Output()
  remove: EventEmitter<Todo> = new EventEmitter();

  @Output()
  toggleComplete: EventEmitter<Todo> = new EventEmitter();

  constructor() {
  }

  toggleTodoComplete(todo: Todo) {
    this.toggleComplete.emit(todo);
  }

  removeTodo(todo: Todo) {
    this.remove.emit(todo);
  }

}

该逻辑与TodoListComponent的逻辑非常相似。

首先我们定义一个@Input()以便我们可以传入一个Todo实例:

@Input() todo: Todo;

然后,我们为模板定义click事件处理程序,并在单击复选框时发出toggleComplete事件,并在单击'X'时发出remove事件:

@Output()
remove: EventEmitter<Todo> = new EventEmitter();

@Output()
toggleComplete: EventEmitter<Todo> = new EventEmitter();

toggleTodoComplete(todo: Todo) {
  this.toggleComplete.emit(todo);
}

removeTodo(todo: Todo) {
  this.remove.emit(todo);
}

请注意,我们实际上是如何不更新或删除数据的。 我们只是发出从事件TodoListItemComponent当用户点击一个链接,完成或删除待办事项,使得我们TodoListItemComponent也是一个愚蠢的组成部分。

记住我们如何将事件处理程序附加到TodoListComponent模板中的这些事件:

<section class="main" *ngIf="todos.length > 0">
  <ul class="todo-list">
    <li *ngFor="let todo of todos" [class.completed]="todo.complete">
      <app-todo-list-item
        [todo]="todo"
        (toggleComplete)="onToggleTodoComplete($event)"
        (remove)="onRemoveTodo($event)"></app-todo-list-item>
    </li>
  </ul>
</section>

然后, TodoListComponent只需从TodoListItemComponent重新发出事件。

通过TodoListComponentTodoListItemComponent冒泡事件使我们能够保持两个组件都哑巴,并确保我们在重构TodoDataService以便与REST API进行通信时不必更新它们(在本系列的第三部分中)。

多么酷啊!

在继续之前,让我们更新AppComponent模板以使用新的TodoListComponent

<section class="todoapp">

  <app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>

  <!-- section is now replaced with app-todo-list -->
  <app-todo-list [todos]="todos" (toggleComplete)="onToggleTodoComplete($event)"
                 (remove)="onRemoveTodo($event)"></app-todo-list>

  <footer class="footer" *ngIf="todos.length > 0">
    <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
  </footer>

</section>

最后,让我们解决TodoListFooterComponent

创建TodoListFooterComponent

同样,从项目的根源开始,我们使用Angular CLI为我们生成TodoListFooterComponent

$ ng generate component todo-list-footer

这将生成以下文件:

create src/app/todo-list-footer/todo-list-footer.component.css
create src/app/todo-list-footer/todo-list-footer.component.html
create src/app/todo-list-footer/todo-list-footer.component.spec.ts
create src/app/todo-list-footer/todo-list-footer.component.ts

它将TodoListFooterComponent自动添加到AppModule声明中:

// ...
import { TodoListFooterComponent } from './todo-list-footer/todo-list-footer.component';

@NgModule({
  declarations: [
    // ...
    TodoListFooterComponent
  ],
  // ...
})
export class AppModule { }

现在,将<footer>元素从src/app/app.component.htmlsrc/app/todo-list-footer/todo-list-footer.component.html

<footer class="footer" *ngIf="todos.length > 0">
  <span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>
</footer>

我们还将相应的逻辑添加到src/app/todo-list-footer/todo-list-footer.component.ts

import { Component, Input } from '@angular/core';
import { Todo } from '../todo';

@Component({
  selector: 'app-todo-list-footer',
  templateUrl: './todo-list-footer.component.html',
  styleUrls: ['./todo-list-footer.component.css']
})
export class TodoListFooterComponent {

  @Input()
  todos: Todo[];

  constructor() {
  }

}

TodoListFooterComponent不需要任何方法。 我们仅使用@Input()装饰器定义todos属性,因此我们可以使用todos属性传入todos

最后,让我们更新AppComponent模板,使其也使用新的TodoListFooterComponent

<section class="todoapp">
  <app-todo-list-header (add)="onAddTodo($event)"></app-todo-list-header>
  <app-todo-list [todos]="todos" (toggleComplete)="onToggleTodoComplete($event)"
                 (remove)="onRemoveTodo($event)"></app-todo-list>
  <app-todo-list-footer [todos]="todos"></app-todo-list-footer>
</section>

现在,我们已经成功地重构了AppComponent以将其功能委托给TodoListHeaderComponentTodoListComponentTodoListFooterComponent

在总结本文之前,我们还需要进行一些更改。

移动TodoDataService提供程序

在第一部分,我们注册了TodoDataService作为一个供应商AppComponent

import {Component} from '@angular/core';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [TodoDataService]
})
export class AppComponent {

  newTodo: Todo = new Todo();

  constructor(private todoDataService: TodoDataService) {
  }

  addTodo() {
    this.todoDataService.addTodo(this.newTodo);
    this.newTodo = new Todo();
  }

  toggleTodoComplete(todo: Todo) {
    this.todoDataService.toggleTodoComplete(todo);
  }

  removeTodo(todo: Todo) {
    this.todoDataService.deleteTodoById(todo.id);
  }

  get todos() {
    return this.todoDataService.getAllTodos();
  }

}

尽管这对于我们的Todo应用程序来说效果很好,但是Angular团队建议将应用程序范围的提供程序添加到根AppModule而不是根AppComponent

AppComponent中注册的服务仅对AppComponent及其组件树可用。 在AppModule中注册的服务可用于整个应用程序中的所有组件。

如果我们的Todo应用程序会增长并引入延迟加载的模块,则延迟加载的模块将无法访问TodoDataService ,因为TodoDataService仅可用于AppComponent及其组件树,而不能在整个应用程序中使用。

因此,我们删除TodoDataService作为一个供应商AppComponent

import {Component} from '@angular/core';
import {Todo} from './todo';
import {TodoDataService} from './todo-data.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: []
})
export class AppComponent {

  newTodo: Todo = new Todo();

  constructor(private todoDataService: TodoDataService) {
  }

  addTodo() {
    this.todoDataService.addTodo(this.newTodo);
    this.newTodo = new Todo();
  }

  toggleTodoComplete(todo: Todo) {
    this.todoDataService.toggleTodoComplete(todo);
  }

  removeTodo(todo: Todo) {
    this.todoDataService.deleteTodoById(todo.id);
  }

  get todos() {
    return this.todoDataService.getAllTodos();
  }

}

接下来,将其添加为AppModule的提供者:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { TodoDataService } from './todo-data.service';
import { TodoListComponent } from './todo-list/todo-list.component';
import { TodoListFooterComponent } from './todo-list-footer/todo-list-footer.component';
import { TodoListHeaderComponent } from './todo-list-header/todo-list-header.component';
import { TodoListItemComponent } from './todo-list-item/todo-list-item.component';

@NgModule({
  declarations: [
    AppComponent,
    TodoListComponent,
    TodoListFooterComponent,
    TodoListHeaderComponent,
    TodoListItemComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [TodoDataService],
  bootstrap: [AppComponent]
})
export class AppModule { }

到此结束本系列的第二部分。

摘要

第一篇文章中 ,我们学习了如何:

  • 使用Angular CLI初始化我们的Todo应用程序
  • 创建一个Todo类来代表单个Todo
  • 创建TodoDataService服务以创建,更新和删除待办事项
  • 使用AppComponent组件显示用户界面
  • 将我们的应用程序部署到GitHub页面。

在第二篇文章中,我们重构了AppComponent ,将其大部分工作委托给:

  • TodoListComponent以显示TodoListComponent列表
  • TodoListItemComponent以显示单个待办事项
  • 一个TodoListHeaderComponent来创建一个新的待办事项
  • TodoListFooterComponent来显示还剩下多少个TodoListFooterComponent

在此过程中,我们了解到:

  • Angular组件架构的基础
  • 如何使用属性绑定将数据传递到组件中
  • 如何使用事件侦听器侦听组件发出的事件
  • 如何将组件拆分为较小的可重用组件,使我们的代码更易于重用和维护
  • 当我们需要重构应用程序的业务逻辑时,如何使用智能哑巴来简化我们的生活。

这篇文章中的所有代码都可以从https://github.com/sitepoint-editors/angular-todo-app获得

在下一部分中,我们将重构TodoService以与REST API通信。

因此,请继续关注第三部分!


本文由Vildan Softic同行评审。 感谢所有SitePoint的同行评审员使SitePoint内容达到最佳状态!

对于专家主导的在线Angular培训课程,您不能超越Todd Motto的Ultimate Angular。 在这里尝试他的课程 ,并使用代码SITEPOINT_SPECIAL获得50%的折扣并帮助支持SitePoint。

翻译自: https://www.sitepoint.com/understanding-component-architecture-angular/

摩拜前端重构angular

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐