Статьи

Понимание архитектуры компонентов: рефакторинг углового приложения

В первой части этой серии мы узнали, как запустить наше приложение Todo и развернуть его на страницах GitHub. Это работало просто отлично, но, к сожалению, все приложение было собрано в один компонент. В этой статье мы рассмотрим более модульную компонентную архитектуру. Мы рассмотрим, как разбить этот отдельный компонент на структурированное дерево более мелких компонентов, которые легче понять, использовать повторно и поддерживать.

Эта статья является второй частью Учебника SitePoint Angular 2+ о том, как создать приложение CRUD с помощью Angular CLI .

  1. Часть 0 — Ultimate Angular CLI Справочное руководство
  2. Часть 1. Подготовка и запуск нашей первой версии приложения Todo
  3. Часть 2. Создание отдельных компонентов для отображения списка задач и одной задачи
  4. Часть 3. Обновление сервиса Todo для связи с REST API
  5. Часть 4. Использование углового маршрутизатора для разрешения данных .
  6. Часть 5. Добавление аутентификации для защиты частного контента.
  7. Часть 6 — Как обновить Angular Projects до последней версии.

Вам не нужно следовать первой части этого урока, чтобы вторая часть имела смысл. Вы можете просто взять копию нашего репо, получить код из первой части и использовать его в качестве отправной точки. Это объясняется более подробно ниже.

Архитектура угловых компонентов

Краткий обзор

Итак, давайте посмотрим на то, что мы рассмотрели в первой части, чуть подробнее. Мы научились:

  • инициализировать наше приложение Todo с помощью Angular CLI
  • создать класс Todo для представления отдельных задач
  • создать сервис TodoDataService для создания, обновления и удаления задач
  • используйте компонент AppComponent для отображения пользовательского интерфейса
  • разверните наше приложение на страницах GitHub.

Архитектура приложения части 1 выглядела так:

Архитектура приложений

Компоненты, которые мы обсуждали, отмечены красной рамкой.

Во второй статье мы AppComponent часть работы, AppComponent , более мелким компонентам, которые легче понять, использовать повторно и поддерживать.

Мы создадим:

  • TodoListComponent для отображения списка задач
  • TodoListItemComponent для отображения одного todo
  • TodoListHeaderComponent для создания новой задачи
  • TodoListFooterComponent чтобы показать, сколько TodoListFooterComponent .

Архитектура приложений

К концу этой статьи вы поймете:

  • основы угловой компонентной архитектуры
  • как вы можете передавать данные в компонент, используя привязки свойств
  • как вы можете прослушивать события, испускаемые компонентом, используя прослушиватели событий
  • почему рекомендуется разбивать компоненты на компоненты многократного использования
  • Разница между умными и немыми компонентами и то, почему держать компоненты немыми — это хорошая практика.

Итак, начнем!

Вверх и работает

Первое, что вы должны будете следовать этой статье, это последняя версия 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 . Каждая статья в этой серии имеет соответствующий тег в репозитории, чтобы вы могли переключаться между различными состояниями приложения.

Код, который мы закончили в первой части и с которого мы начинаем в этой статье, помечен как часть-1 . Код, которым мы заканчиваем эту статью, помечен как часть-2 .

Вы можете думать о тегах как псевдоним определенного идентификатора коммита. Вы можете переключаться между ними, используя 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 

Затем посетите http: // localhost: 4200 / . Если все хорошо, вы должны увидеть работающее приложение Todo.

Оригинальный AppComponent

Давайте откроем src/app/app.component.html и посмотрим на 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 больше и сложнее, что 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. 

Чтобы узнать больше о объявлениях модулей, обязательно ознакомьтесь с 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 для сохранения нового todo, мы генерируем событие add и передаем новое todo в качестве аргумента.

Мы уже узнали, что синтаксис шаблона Angular позволяет нам прикрепить обработчик к событию. Например, рассмотрим следующий код:

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

Это говорит Angular запускать метод addTodo() при нажатии клавиши ввода внутри ввода. Это работает, потому что событие 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> 

Каждый раз, когда мы вызываем add.emit(value) в TodoListHeaderComponent , будет вызываться обработчик onAddTodo($event) , а $event будет равен value .

Это отделяет наш TodoListHeaderComponent от TodoDataService и позволяет родительскому компоненту решать, что должно произойти при создании нового задания.

Когда мы обновим TodoDataService для связи с REST API в третьей части, нам не придется беспокоиться о TodoListHeaderComponent поскольку он даже не знает о существовании TodoDataService .

Смарт против тупых компонентов

Возможно, вы уже слышали о умных и тупых компонентах . TodoListHeaderComponent от TodoDataService делает компонент 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) для захвата событий add которые отправляются TodoListHeaderComponent когда пользователь вводит новый заголовок TodoListHeaderComponent :

 <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 { } 

Теперь мы возьмем HTML, связанный со src/app/app.component.html из src/app/app.component.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 который еще не существует. Однако добавление его в шаблон уже позволяет нам изучить, какой 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 немым компонентом.

Сначала мы определяем @Input() входного свойства , помечая его декоратором @Input() . Это позволяет нам вводить todos из родительского компонента.

Далее мы определяем два выходных события, remove и toggleComplete , используя декоратор @Output() . Обратите внимание, как мы устанавливаем их тип в EventEmitter<Todo> и назначаем им каждый новый экземпляр EventEmitter .

Аннотация типа EventEmitter<Todo> — это универсальный тип TypeScript, который сообщает TypeScript, что одновременно remove и toggleComplete являются экземплярами toggleComplete и что значения, которые они toggleComplete являются экземплярами Todo .

Наконец, мы определяем обработчики событий onToggleTodoComplete(todo) и onRemoveTodo(todo) которые мы указали в нашем представлении, используя (toggleComplete)="onToggleTodoComplete($event)" и (remove)="onRemoveTodo($event)" .

Обратите внимание, как мы используем $event качестве имени аргумента в шаблоне представления и todo качестве имени параметра в определении метода. Чтобы получить доступ к полезной нагрузке (излучаемому значению) события в шаблоне Angular, мы всегда должны использовать $event качестве имени аргумента.

Поэтому, указав (toggleComplete)="onToggleTodoComplete($event)" в нашем шаблоне представления, мы сообщаем Angular использовать полезную нагрузку события в качестве первого аргумента при вызове метода onToggleTodoComplete , который будет соответствовать первому параметру метода onToggleTodoComplete , а именно todo ,

Мы знаем, что полезная нагрузка будет экземпляром todo , поэтому мы определяем метод onToggleTodoComplete как onToggleTodoComplete(todo: Todo) , что облегчает чтение, понимание и сопровождение нашего кода.

Наконец, мы определяем наши обработчики событий, чтобы они также toggleComplete событие toggleComplete и remove при получении входящей полезной нагрузки и определяем todo в качестве полезной нагрузки события.

По сути, мы позволяем TodoListComponent всплывать события из его дочерних экземпляров 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> 

Нам не нужно ничего менять в разметке, но мы должны убедиться, что события обрабатываются правильно, поэтому давайте добавим необходимый код нашего 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 событие toggleComplete при нажатии флажка и событие remove при нажатии toggleComplete «X»:

 @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 когда пользователь щелкает ссылку для завершения или удаления 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 .

TodoListItemComponent события из TodoListItemComponent через TodoListComponent позволяют нам сохранять тупость обоих компонентов и гарантируют, что нам не придется обновлять их при рефакторинге 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.html в src/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 не требует никаких методов. Мы определяем свойство todos только с помощью декоратора @Input() поэтому мы можем передавать 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 чтобы делегировать его функциональность TodoListHeaderComponent , TodoListComponent и TodoListFooterComponent .

Прежде чем мы закончим эту статью, нам нужно сделать еще одно изменение.

Перемещение провайдера TodoDataService

В 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 загрузкой не смогут получить доступ к 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 { } 

На этом заканчивается вторая часть этой серии.

Резюме

В первой статье мы узнали, как:

  • инициализировать наше приложение Todo с помощью Angular CLI
  • создать класс Todo для представления отдельных задач
  • создать сервис TodoDataService для создания, обновления и удаления задач
  • используйте компонент AppComponent для отображения пользовательского интерфейса
  • разверните наше приложение на страницах GitHub.

Во второй статье мы реорганизовали AppComponent чтобы делегировать большую часть его работы:

  • TodoListComponent для отображения списка задач
  • TodoListItemComponent для отображения одного todo
  • TodoListHeaderComponent для создания новой задачи
  • TodoListFooterComponent чтобы показать, сколько TodoListFooterComponent .

В процессе мы узнали:

  • основы угловой компонентной архитектуры
  • как передать данные в компонент, используя привязки свойств
  • как прослушивать события, испускаемые компонентом, используя слушатели событий
  • как разбиение компонентов на более мелкие повторно используемые компоненты облегчает повторное использование и поддержку нашего кода
  • как мы можем использовать умные и глупые, чтобы сделать нашу жизнь намного проще, когда нам нужно реорганизовать бизнес-логику нашего приложения.

Весь код из этой статьи доступен по адресу https://github.com/sitepoint-editors/angular-todo-app .

В следующей части мы проведем рефакторинг TodoService для связи с REST API.

Так что следите за обновлениями для третьей части!


Эта статья была рецензирована Vildan Softic . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

На онлайн-курсах Angular под руководством экспертов вы не можете пройти мимо Ultimate Angular от Todd Motto. Попробуйте его курсы здесь и используйте код SITEPOINT_SPECIAL, чтобы получить скидку 50% и помочь в поддержке SitePoint.