Статьи

Создание приложения Todo с угловым CLI

Эта статья о создании приложения todo с Angular CLI является первой в серии из четырех статей о том, как написать приложение todo в Angular 2:

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

Предпочитаете изучать Angular с помощью пошагового видеокурса? Проверьте Learn Angular 5 на SitePoint Premium.

Angular CLI: иллюстрация монитора, покрытого заметками post-it и списками задач

В каждой статье мы будем дорабатывать базовую архитектуру приложения и будем следить за тем, чтобы у нас была рабочая версия приложения, которая выглядит следующим образом:

Angular CLI: анимированный GIF-файл готового приложения Todo

К концу этой серии наша архитектура приложения будет выглядеть так:

Angular CLI: Архитектура приложения готового приложения Todo

Элементы, отмеченные красной рамкой, обсуждаются в этой статье, а элементы, не отмеченные красной рамкой, будут обсуждаться в последующих статьях этой серии.

В этой первой части вы узнаете, как:

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

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

Вместо того, чтобы преемник AngularJS 1.x, Angular 2 можно считать совершенно новой платформой, построенной на уроках AngularJS 1.x. Следовательно, изменение названия, где Angular используется для обозначения Angular 2, а AngularJS относится к AngularJS 1.x. В этой статье мы будем использовать Angular и Angular 2 взаимозаменяемо, но оба они относятся к Angular 2.

С 9 февраля 2017 г. команда ng deploy удалена из ядра Angular CLI. Узнайте больше здесь .

Инициализируйте приложение Todo, используя угловой интерфейс командной строки

Одним из самых простых способов запуска нового приложения Angular 2 является использование интерфейса командной строки Angular (CLI).

Чтобы установить Angular CLI, запустите:

 $ npm install -g angular-cli 

Это установит команду ng глобально в вашей системе.

Чтобы проверить, успешно ли завершена установка, вы можете запустить:

 $ ng version 

Это должно отобразить версию, которую вы установили:

 angular-cli: 1.0.0-beta.21 node: 6.1.0 os: darwin x64 

Теперь, когда у вас установлен Angular CLI, вы можете использовать его для создания приложения Todo:

 $ ng new todo-app 

Это создаст новый каталог со всеми файлами, необходимыми для начала:

 todo-app ├── README.md ├── angular-cli.json ├── e2e │ ├── app.e2e-spec.ts │ ├── app.po.ts │ └── tsconfig.json ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── index.ts │ ├── assets │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.json │ └── typings.d.ts └── tslint.json 

Если вы еще не знакомы с Angular CLI, обязательно ознакомьтесь с The Ultimate Angular CLI Reference .

Теперь вы можете перейти в новый каталог:

 $ cd todo-app 

Затем запустите сервер разработки Angular CLI:

 $ ng serve 

Это запустит локальный сервер разработки, к которому вы можете перейти в своем браузере по адресу http://localhost:4200/ .

Сервер разработки Angular CLI включает поддержку LiveReload, поэтому ваш браузер автоматически перезагружает приложение при изменении исходного файла.

Как это удобно!

Создание класса Todo

Поскольку Angular CLI генерирует файлы TypeScript , мы можем использовать класс для представления элементов Todo.

Итак, давайте используем Angular CLI для генерации класса Todo для нас:

 $ ng generate class Todo --spec 

Это создаст следующее:

 src/app/todo.spec.ts src/app/todo.ts 

Давайте откроем src/app/todo.ts :

 export class Todo { } 

Затем добавьте необходимую логику:

 export class Todo { id: number; title: string = ''; complete: boolean = false; constructor(values: Object = {}) { Object.assign(this, values); } } 

В этом определении класса Todo мы указываем, что каждый экземпляр Todo будет иметь три свойства:

  • id : номер, уникальный идентификатор элемента todo
  • title : string, название элемента todo
  • complete : boolean, независимо от того, complete ли элемент todo

Мы также предоставляем логику конструктора, которая позволяет нам указывать значения свойств во время реализации, чтобы мы могли легко создавать новые экземпляры Todo, например:

 let todo = new Todo({ title: 'Read SitePoint article', complete: false }); 

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

При создании класса Todo мы использовали параметр --spec . Это сказало Angular CLI также сгенерировать для нас src/app/todo.spec.ts с базовым модульным тестом:

 import {Todo} from './todo'; describe('Todo', () => { it('should create an instance', () => { expect(new Todo()).toBeTruthy(); }); }); 

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

 import {Todo} from './todo'; describe('Todo', () => { it('should create an instance', () => { expect(new Todo()).toBeTruthy(); }); it('should accept values in the constructor', () => { let todo = new Todo({ title: 'hello', complete: true }); expect(todo.title).toEqual('hello'); expect(todo.complete).toEqual(true); }); }); 

Чтобы проверить, работает ли наш код должным образом, теперь мы можем запустить:

 $ ng test 

Это запускает тестер кармы и запускает все наши юнит-тесты. Это должно вывести:

 [karma]: No captured browser, open http://localhost:9876/ [karma]: Karma v1.2.0 server started at http://localhost:9876/ [launcher]: Launching browser Chrome with unlimited concurrency [launcher]: Starting browser Chrome [Chrome 54.0.2840 (Mac OS X 10.12.0)]: Connected on socket /#ALCo3r1JmW2bvt_fAAAA with id 84083656 Chrome 54.0.2840 (Mac OS X 10.12.0): Executed 5 of 5 SUCCESS (0.159 secs / 0.154 secs) 

Если ваши модульные тесты не пройдены, вы можете сравнить ваш код с рабочим кодом на GitHub .

Теперь, когда у нас есть рабочий класс Todo для представления отдельных задач, давайте создадим сервис TodoDataService для управления всеми задачами.

Создание службы TodoDataService

TodoDataService будет отвечать за управление нашими элементами Todo.

В другой части этой серии вы узнаете, как взаимодействовать с REST API, но сейчас мы будем хранить все данные в памяти.

Давайте снова используем Angular CLI для создания сервиса для нас:

 $ ng generate service TodoData 

Это выводит:

 installing service create src/app/todo-data.service.spec.ts create src/app/todo-data.service.ts WARNING Service is generated but not provided, it must be provided to be used 

При генерации сервиса Angular CLI также генерирует модульный тест по умолчанию, поэтому нам не нужно явно использовать опцию --spec .

Angular CLI сгенерировал следующий код для нашего TodoDataService в src/app/todo-data.service.ts :

 import { Injectable } from '@angular/core'; @Injectable() export class TodoDataService { constructor() { } } 

и соответствующий модульный тест в src/app/todo-data.service.spec.ts :

 /* tslint:disable:no-unused-variable */ import { TestBed, async, inject } from '@angular/core/testing'; import { TodoDataService } from './todo-data.service'; describe('TodoDataService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [TodoDataService] }); }); it('should ...', inject([TodoDataService], (service: TodoDataService) => { expect(service).toBeTruthy(); })); }); 

Давайте откроем src/app/todo-data.service.ts и добавим нашу логику управления todo в TodoDataService :

 import {Injectable} from '@angular/core'; import {Todo} from './todo'; @Injectable() export class TodoDataService { // Placeholder for last id so we can simulate // automatic incrementing of ids lastId: number = 0; // Placeholder for todos todos: Todo[] = []; constructor() { } // Simulate POST /todos addTodo(todo: Todo): TodoDataService { if (!todo.id) { todo.id = ++this.lastId; } this.todos.push(todo); return this; } // Simulate DELETE /todos/:id deleteTodoById(id: number): TodoDataService { this.todos = this.todos .filter(todo => todo.id !== id); return this; } // Simulate PUT /todos/:id updateTodoById(id: number, values: Object = {}): Todo { let todo = this.getTodoById(id); if (!todo) { return null; } Object.assign(todo, values); return todo; } // Simulate GET /todos getAllTodos(): Todo[] { return this.todos; } // Simulate GET /todos/:id getTodoById(id: number): Todo { return this.todos .filter(todo => todo.id === id) .pop(); } // Toggle todo complete toggleTodoComplete(todo: Todo){ let updatedTodo = this.updateTodoById(todo.id, { complete: !todo.complete }); return updatedTodo; } } 

Фактические детали реализации методов не являются существенными для целей этой статьи. Основным выводом является то, что мы централизуем бизнес-логику в сервисе.

Чтобы убедиться, что бизнес-логика в нашем TodoDataService работает TodoDataService , мы также добавили несколько дополнительных модульных тестов в src/app/todo-data.service.spec.ts :

 import {TestBed, async, inject} from '@angular/core/testing'; import {Todo} from './todo'; import {TodoDataService} from './todo-data.service'; describe('TodoDataService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [TodoDataService] }); }); it('should ...', inject([TodoDataService], (service: TodoDataService) => { expect(service).toBeTruthy(); })); describe('#getAllTodos()', () => { it('should return an empty array by default', inject([TodoDataService], (service: TodoDataService) => { expect(service.getAllTodos()).toEqual([]); })); it('should return all todos', inject([TodoDataService], (service: TodoDataService) => { let todo1 = new Todo({title: 'Hello 1', complete: false}); let todo2 = new Todo({title: 'Hello 2', complete: true}); service.addTodo(todo1); service.addTodo(todo2); expect(service.getAllTodos()).toEqual([todo1, todo2]); })); }); describe('#save(todo)', () => { it('should automatically assign an incrementing id', inject([TodoDataService], (service: TodoDataService) => { let todo1 = new Todo({title: 'Hello 1', complete: false}); let todo2 = new Todo({title: 'Hello 2', complete: true}); service.addTodo(todo1); service.addTodo(todo2); expect(service.getTodoById(1)).toEqual(todo1); expect(service.getTodoById(2)).toEqual(todo2); })); }); describe('#deleteTodoById(id)', () => { it('should remove todo with the corresponding id', inject([TodoDataService], (service: TodoDataService) => { let todo1 = new Todo({title: 'Hello 1', complete: false}); let todo2 = new Todo({title: 'Hello 2', complete: true}); service.addTodo(todo1); service.addTodo(todo2); expect(service.getAllTodos()).toEqual([todo1, todo2]); service.deleteTodoById(1); expect(service.getAllTodos()).toEqual([todo2]); service.deleteTodoById(2); expect(service.getAllTodos()).toEqual([]); })); it('should not removing anything if todo with corresponding id is not found', inject([TodoDataService], (service: TodoDataService) => { let todo1 = new Todo({title: 'Hello 1', complete: false}); let todo2 = new Todo({title: 'Hello 2', complete: true}); service.addTodo(todo1); service.addTodo(todo2); expect(service.getAllTodos()).toEqual([todo1, todo2]); service.deleteTodoById(3); expect(service.getAllTodos()).toEqual([todo1, todo2]); })); }); describe('#updateTodoById(id, values)', () => { it('should return todo with the corresponding id and updated data', inject([TodoDataService], (service: TodoDataService) => { let todo = new Todo({title: 'Hello 1', complete: false}); service.addTodo(todo); let updatedTodo = service.updateTodoById(1, { title: 'new title' }); expect(updatedTodo.title).toEqual('new title'); })); it('should return null if todo is not found', inject([TodoDataService], (service: TodoDataService) => { let todo = new Todo({title: 'Hello 1', complete: false}); service.addTodo(todo); let updatedTodo = service.updateTodoById(2, { title: 'new title' }); expect(updatedTodo).toEqual(null); })); }); describe('#toggleTodoComplete(todo)', () => { it('should return the updated todo with inverse complete status', inject([TodoDataService], (service: TodoDataService) => { let todo = new Todo({title: 'Hello 1', complete: false}); service.addTodo(todo); let updatedTodo = service.toggleTodoComplete(todo); expect(updatedTodo.complete).toEqual(true); service.toggleTodoComplete(todo); expect(updatedTodo.complete).toEqual(false); })); }); }); 

Карма поставляется с предварительно настроенной Жасмин . Вы можете прочитать документацию по Jasmine, чтобы узнать больше о синтаксисе Jasmine.

Давайте увеличим некоторые из частей в модульных тестах выше:

 beforeEach(() => { TestBed.configureTestingModule({ providers: [TodoDataService] }); }); 

Прежде всего, что такое TestBed ?

TestBed — это утилита, предоставляемая @angular/core/testing для настройки и создания модуля тестирования Angular, в котором мы хотим запустить наши модульные тесты.

Мы используем метод TestBed.configureTestingModule() для настройки и создания нового модуля тестирования Angular. Мы можем настроить модуль тестирования по своему вкусу, передав объект конфигурации. Этот объект конфигурации может иметь большинство свойств обычного углового модуля .

В этом случае мы используем свойство providers чтобы настроить модуль тестирования на использование реального TodoDataService при запуске тестов.

В третьей части этой серии мы дадим возможность TodoDataService взаимодействовать с реальным API REST и увидим, как мы можем внедрить фиктивный сервис в нашем тестовом модуле, чтобы предотвратить взаимодействие тестов с реальным API.

Далее, мы используем функцию inject предоставляемую @angular/core/testing чтобы внедрить правильный сервис из инжектора TestBed в нашей тестовой функции:

 it('should return all todos', inject([TodoDataService], (service: TodoDataService) => { let todo1 = new Todo({title: 'Hello 1', complete: false}); let todo2 = new Todo({title: 'Hello 2', complete: true}); service.addTodo(todo1); service.addTodo(todo2); expect(service.getAllTodos()).toEqual([todo1, todo2]); })); 

Первым аргументом функции inject является массив маркеров внедрения угловой зависимости. Второй аргумент — это тестовая функция, параметры которой являются зависимостями, которые соответствуют токенам внедрения зависимостей из массива.

Здесь мы говорим инжектору TestBed внедрить TodoDataService , указав его в массиве в первом аргументе. В результате мы можем получить доступ к TodoDataService как service в нашей тестовой функции, потому что service — это имя первого параметра нашей тестовой функции.

Если вы хотите узнать больше о тестировании в Angular, обязательно ознакомьтесь с официальным руководством по тестированию Angular .

Чтобы проверить, работает ли наш сервис должным образом, мы снова запускаем наши модульные тесты:

 $ ng test 
 [karma]: No captured browser, open http://localhost:9876/ [karma]: Karma v1.2.0 server started at http://localhost:9876/ [launcher]: Launching browser Chrome with unlimited concurrency [launcher]: Starting browser Chrome [Chrome 54.0.2840 (Mac OS X 10.12.0)]: Connected on socket /#fi6bwZk8IjYr1DZ-AAAA with id 11525081 Chrome 54.0.2840 (Mac OS X 10.12.0): Executed 14 of 14 SUCCESS (0.273 secs / 0.264 secs) 

Отлично — все юнит-тесты прошли успешно!

Теперь, когда у нас есть работающий сервис TodoDataService , пришло время реализовать настоящий пользовательский интерфейс.

В Angular 2 части пользовательского интерфейса представлены компонентами .

Редактирование компонента AppComponent

Когда мы инициализировали приложение Todo, Angular CLI автоматически сгенерировал для нас основной компонент AppComponent :

 src/app/app.component.css src/app/app.component.html src/app/app.component.spec.ts src/app/app.component.ts 

Шаблон и стили также могут быть указаны внутри файла скрипта. Angular CLI создает отдельные файлы по умолчанию, поэтому мы будем использовать эту статью в этой статье.

Давайте откроем src/app/app.component.html :

 <h1> {{title}} </h1> 

Заменить его содержимое на:

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

Вот очень короткий учебник по синтаксису шаблонов Angular на тот случай, если вы его еще не видели:

  • [property]="expression" : установить для свойства элемента значение expression
  • (event)="statement" : выполнить оператор, когда event произошло
  • [(property)]="expression" : создать двустороннюю привязку с expression
  • [class.special]="expression" : добавьте special CSS-класс к элементу, если значение expression истинно
  • [style.color]="expression" : установить свойство CSS color в значение expression

Если вы не знакомы с синтаксисом шаблонов Angular, вам обязательно следует прочитать официальную документацию по синтаксису шаблонов .

Давайте посмотрим, что это значит для нашего взгляда. Вверху есть вход для создания новой задачи:

 <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()"> 
  • [(ngModel)]="newTodo.title" : добавляет двустороннюю привязку между input значением и newTodo.title
  • (keyup.enter)="addTodo()" : сообщает Angular выполнить addTodo() при нажатии клавиши ввода при вводе в элемент input

Не беспокойтесь о том, откуда newTodo или addTodo() ; мы скоро туда доберемся. Просто попробуйте понять семантику представления.

Далее есть раздел для отображения существующих задач:

 <section class="main" *ngIf="todos.length > 0"> 
  • *ngIf="todos.length > 0" : показывать элемент section и все его дочерние элементы только при наличии хотя бы одного todo

В этом разделе мы просим Angular сгенерировать элемент li для каждой задачи:

 <li *ngFor="let todo of todos" [class.completed]="todo.complete"> 
  • *ngFor="let todo of todos" : *ngFor="let todo of todos" все todos и назначить текущий todo переменной todo для каждой итерации
  • [class.completed]="todo.complete" : применить класс CSS, completed к элементу li когда 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> 
  • (click)="toggleTodoComplete(todo)" : выполнить toggleTodoComplete(todo) когда установлен флажок
  • [checked]="todo.complete" : назначить значение todo.complete checked свойству элемента
  • (click)="removeTodo(todo)" : выполнить removeTodo(todo) при нажатии кнопки уничтожения

ОК, давайте дышать. Это был довольно небольшой синтаксис, через который мы прошли.

Если вы хотите узнать все подробности о синтаксисе шаблонов Angular, обязательно ознакомьтесь с официальной документацией по шаблонам .

Вы можете задаться вопросом, как можно оценить такие выражения, как addTodo() и newTodo.title . Мы еще не определили их, так как же Angular узнает, что мы имеем в виду?

Именно в этом и заключается контекст выражения. Контекст выражения — это контекст, в котором оцениваются выражения. Контекст выражения компонента является экземпляром компонента. И экземпляр компонента является экземпляром класса компонента.

Класс компонентов нашего AppComponent определен в src/app/app.component.ts .

Angular CLI уже создал для нас шаблонный код:

 import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app works!'; } 

Таким образом, мы можем сразу же начать добавлять нашу собственную логику.

Нам понадобится сервис TodoDataService в нашей логике AppComponent , поэтому давайте начнем с внедрения сервиса в наш компонент.

Сначала мы импортируем TodoDataService и указываем его в массиве providers декоратора Component :

 // Import class so we can register it as dependency injection token import {TodoDataService} from './todo-data.service'; @Component({ // ... providers: [TodoDataService] }) export class AppComponent { // ... } 

AppComponent зависимостей AppComponent теперь распознает класс TodoDataService как токен внедрения зависимостей и возвращает один экземпляр TodoDataService когда мы его запрашиваем.

Система внедрения зависимостей Angular принимает множество рецептов внедрения зависимостей. Приведенный выше синтаксис является сокращенной записью для рецепта поставщика класса, который предоставляет зависимости с использованием шаблона синглтона. Посмотрите документацию по внедрению зависимостей Angular для более подробной информации.

Теперь, когда инжектор зависимостей компонентов знает, что ему нужно предоставить, мы просим его внедрить экземпляр TodoDataService в наш компонент, указав зависимость в конструкторе AppComponent :

 // Import class so we can use it as dependency injection token in the constructor import {TodoDataService} from './todo-data.service'; @Component({ // ... }) export class AppComponent { // Ask Angular DI system to inject the dependency // associated with the dependency injection token `TodoDataService` // and assign it to a property called `todoDataService` constructor(private todoDataService: TodoDataService) { } // Service is now available as this.todoDataService toggleTodoComplete(todo) { this.todoDataService.toggleTodoComplete(todo); } } 

Использование public или private для аргументов в конструкторе — это сокращенная запись, которая позволяет нам автоматически создавать свойства с этим именем, поэтому:

 class AppComponent { constructor(private todoDataService: TodoDataService) { } } 

Это сокращенное обозначение для:

 class AppComponent { private todoDataService: TodoDataService; constructor(todoDataService: TodoDataService) { this.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) { this.todoDataService.toggleTodoComplete(todo); } removeTodo(todo) { this.todoDataService.deleteTodoById(todo.id); } get todos() { return this.todoDataService.getAllTodos(); } } 

Сначала мы определяем свойство newTodo и назначаем new Todo() при создании экземпляра класса компонента. Это тот же экземпляр Todo указан в выражении двустороннего связывания [(ngModel)] в нашем представлении:

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

Всякий раз, когда входное значение изменяется в представлении, значение в экземпляре компонента обновляется. И всякий раз, когда значение в экземпляре компонента изменяется, значение в элементе ввода в представлении обновляется.

Далее мы реализуем все методы, которые мы использовали в нашем представлении:

 addTodo() { this.todoDataService.addTodo(this.newTodo); this.newTodo = new Todo(); } toggleTodoComplete(todo) { this.todoDataService.toggleTodoComplete(todo); } removeTodo(todo) { this.todoDataService.deleteTodoById(todo.id); } get todos() { return this.todoDataService.getAllTodos(); } 

Их реализация очень короткая и не требует пояснений, поскольку мы делегируем всю бизнес-логику todoDataService .

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

Прежде чем мы попробуем результат в нашем браузере, давайте снова запустим наши модульные тесты:

 $ ng test 05 12 2016 01:16:44.714:WARN [karma]: No captured browser, open http://localhost:9876/ 05 12 2016 01:16:44.722:INFO [karma]: Karma v1.2.0 server started at http://localhost:9876/ 05 12 2016 01:16:44.722:INFO [launcher]: Launching browser Chrome with unlimited concurrency 05 12 2016 01:16:44.725:INFO [launcher]: Starting browser Chrome 05 12 2016 01:16:45.373:INFO [Chrome 54.0.2840 (Mac OS X 10.12.0)]: Connected on socket /#WcdcOx0IPj-cKul8AAAA with id 19440217 Chrome 54.0.2840 (Mac OS X 10.12.0) AppComponent should create the app FAILED Can't bind to 'ngModel' since it isn't a known property of 'input'. (""> <h1>Todos</h1> <input class="new-todo" placeholder="What needs to be done?" autofocus="" [ERROR ->][(ngModel)]="newTodo.title" (keyup.enter)="addTodo()"> </header> <section class="main" *ngIf="tod"): AppComponent@3:78 Error: Template parse errors: at TemplateParser.parse (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/@angular/compiler/src/template_parser/template_parser.js:97:0 <- src/test.ts:11121:19) at RuntimeCompiler._compileTemplate (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/@angular/compiler/src/runtime_compiler.js:255:0 <- src/test.ts:25503:51) at webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/@angular/compiler/src/runtime_compiler.js:175:47 <- src/test.ts:25423:62 at Set.forEach (native) at RuntimeCompiler._compileComponents (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/@angular/compiler/src/runtime_compiler.js:175:0 <- src/test.ts:25423:19) at createResult (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/@angular/compiler/src/runtime_compiler.js:86:0 <- src/test.ts:25334:19) at RuntimeCompiler._compileModuleAndAllComponents (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/@angular/compiler/src/runtime_compiler.js:90:0 <- src/test.ts:25338:88) at RuntimeCompiler.compileModuleAndAllComponentsSync (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/@angular/compiler/src/runtime_compiler.js:62:0 <- src/test.ts:25310:21) at TestingCompilerImpl.compileModuleAndAllComponentsSync (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/@angular/compiler/bundles/compiler-testing.umd.js:482:0 <- src/test.ts:37522:35) at TestBed._initIfNeeded (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/@angular/core/bundles/core-testing.umd.js:758:0 <- src/test.ts:7065:40) ... Chrome 54.0.2840 (Mac OS X 10.12.0): Executed 14 of 14 (3 FAILED) (0.316 secs / 0.245 secs) 

Три теста терпят неудачу со следующей ошибкой: Can't bind to 'ngModel' since it isn't a known property of 'input'. ,

Давайте откроем src/app/app.component.spec.ts :

 /* tslint:disable:no-unused-variable */ import { TestBed, async } from '@angular/core/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], }); }); it('should create the app', async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); })); it(`should have as title 'app works!'`, async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; expect(app.title).toEqual('app works!'); })); it('should render title in a h1 tag', async(() => { let fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); let compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent).toContain('app works!'); })); }); 

Причина, по которой Angular жалуется на то, что не знает ngModel , заключается в том, что FormsModule не загружается, когда AppComponent создает экземпляр AppComponent с помощью TestBed.createComponent() .

Чтобы узнать больше о TestBed , обязательно ознакомьтесь с официальной документацией Angular по тестированию .

Чтобы Angular также FormsModule когда Karma создает экземпляр AppComponent с использованием TestBed.createComponent() , мы должны указать FormsModule в свойстве import объекта конфигурации FormsModule :

 /* tslint:disable:no-unused-variable */ import { TestBed, async } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { FormsModule } from '@angular/forms'; describe('AppComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ FormsModule ], declarations: [ AppComponent ], }); }); it('should create the app', async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); })); it(`should have as title 'app works!'`, async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; expect(app.title).toEqual('app works!'); })); it('should render title in a h1 tag', async(() => { let fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); let compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent).toContain('app works!'); })); }); 

Теперь у нас есть два провальных теста:

 Chrome 54.0.2840 (Mac OS X 10.12.0) AppComponent should have as title 'app works!' FAILED Expected undefined to equal 'app works!'. at webpack:///Users/jvandemo/Projects/jvandemo/todo-app/src/app/app.component.spec.ts:28:22 <- src/test.ts:46473:27 at ZoneDelegate.invoke (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/zone.js/dist/zone.js:232:0 <- src/test.ts:50121:26) at AsyncTestZoneSpec.onInvoke (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/zone.js/dist/async-test.js:49:0 <- src/test.ts:34133:39) at ProxyZoneSpec.onInvoke (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/zone.js/dist/proxy.js:76:0 <- src/test.ts:34825:39) Chrome 54.0.2840 (Mac OS X 10.12.0) AppComponent should render title in a h1 tag FAILED Expected 'Todos' to contain 'app works!'. at webpack:///Users/jvandemo/Projects/jvandemo/todo-app/src/app/app.component.spec.ts:35:53 <- src/test.ts:46479:58 at ZoneDelegate.invoke (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/zone.js/dist/zone.js:232:0 <- src/test.ts:50121:26) at AsyncTestZoneSpec.onInvoke (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/zone.js/dist/async-test.js:49:0 <- src/test.ts:34133:39) at ProxyZoneSpec.onInvoke (webpack:///Users/jvandemo/Projects/jvandemo/todo-app/~/zone.js/dist/proxy.js:76:0 <- src/test.ts:34825:39) Chrome 54.0.2840 (Mac OS X 10.12.0): Executed 14 of 14 (2 FAILED) (4.968 secs / 4.354 secs) 

Карма предупреждает нас, что экземпляр компонента не имеет title свойства, равного app works! и что нет элемента h1 который содержит app works! ,

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

 /* tslint:disable:no-unused-variable */ import { TestBed, async } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { FormsModule } from '@angular/forms'; import { Todo } from './todo'; describe('AppComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ FormsModule ], declarations: [ AppComponent ], }); }); it('should create the app', async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); })); it(`should have a newTodo todo`, async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; expect(app.newTodo instanceof Todo).toBeTruthy() })); it('should display "Todos" in h1 tag', async(() => { let fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); let compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent).toContain('Todos'); })); }); 

Сначала мы добавим модульный тест, чтобы убедиться, что свойство newTodo правильно:

 it(`should have a newTodo todo`, async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; expect(app.newTodo instanceof Todo).toBeTruthy() })); 

Затем мы добавляем модульный тест, чтобы убедиться, что элемент h1 содержит ожидаемую строку:

 it('should display "Todos" in h1 tag', async(() => { let fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); let compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent).toContain('Todos'); })); 

Теперь наши тесты успешно работают:

 $ ng test WARN [karma]: No captured browser, open http://localhost:9876/ INFO [karma]: Karma v1.2.0 server started at http://localhost:9876/ INFO [launcher]: Launching browser Chrome with unlimited concurrency INFO [launcher]: Starting browser Chrome INFO [Chrome 55.0.2883 (Mac OS X 10.12.0)]: Connected on socket /#S1TIAhPPqLOV0Z3NAAAA with id 73327097 Chrome 54.0.2840 (Mac OS X 10.12.0): Executed 14 of 14 SUCCESS (0.411 secs / 0.402 secs) 

Если вы хотите узнать больше о тестировании, обязательно ознакомьтесь с главой о тестировании в официальной документации Angular .

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

Прежде чем мы закончим эту статью, давайте взглянем на еще одну действительно классную особенность Angular CLI.

Развертывание на страницах GitHub

Angular CLI упрощает развертывание нашего приложения на страницах GitHub одной командой:

 $ ng github-pages:deploy --message 'deploy(dist): deploy on GitHub pages' 

Команда github-pages:deploy говорит Angular CLI создать статическую версию нашего приложения Angular и передать ее в ветку gh-pages нашего репозитория GitHub:

 $ ng github-pages:deploy --message 'deploy(dist): deploy on GitHub pages' Built project successfully. Stored in "dist/". Deployed! Visit https://sitepoint-editors.github.io/todo-app/ Github pages might take a few minutes to show the deployed site. 

Наше приложение теперь доступно по адресу https://sitepoint-editors.github.io/todo-app/ .

Как это здорово!

Резюме

Angular 2 — зверь, без сомнения. Очень сильный зверь!

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

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

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

Так что следите за новостями об этом замечательном мире Angular 2.