Эта статья о создании приложения todo с Angular CLI является первой в серии из четырех статей о том, как написать приложение todo в Angular 2:
- Часть 0 — Ultimate Angular CLI Справочное руководство
- Часть 1. Подготовка и запуск нашей первой версии приложения Todo
- Часть 2. Создание отдельных компонентов для отображения списка задач и одной задачи
- Часть 3. Обновление сервиса Todo для связи с REST API
- Часть 4. Использование углового маршрутизатора для разрешения данных
- Часть 5. Добавление аутентификации для защиты частного контента.
- Часть 6 — Как обновить Angular Projects до последней версии.
Предпочитаете изучать Angular с помощью пошагового видеокурса? Проверьте Learn Angular 5 на SitePoint Premium.
В каждой статье мы будем дорабатывать базовую архитектуру приложения и будем следить за тем, чтобы у нас была рабочая версия приложения, которая выглядит следующим образом:
К концу этой серии наша архитектура приложения будет выглядеть так:
Элементы, отмеченные красной рамкой, обсуждаются в этой статье, а элементы, не отмеченные красной рамкой, будут обсуждаться в последующих статьях этой серии.
В этой первой части вы узнаете, как:
- инициализировать приложение 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"
: установить свойство CSScolor
в значение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.