使用Angular CLI构建Todo应用

本文介绍如何使用 Angular CLI 构建一个 Todo 应用程序,涵盖初始化项目、实现业务逻辑、构建用户界面及部署应用到 GitHub Pages 的全过程。

关于如何使用Angular CLI构建todo应用程序的本文是有关如何在Angular 2中编写Todo应用程序的四部分系列文章的第一篇:

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

更喜欢使用分步视频课程学习Angular? 退房 了解角5 上SitePoint保费。

Angular CLI:插图中覆盖了便利贴和待办事项列表

在每篇文章中,我们将优化应用程序的基础体系结构,并确保我们具有该应用程序的工作版本,如下所示:

Angular CLI:完成的Todo应用程序的GIF动画

在本系列文章的最后,我们的应用程序体系结构将如下所示:

Angular CLI:完成的Todo应用程序的应用程序体系结构

本文将讨论标有红色边框的项目,而本系列的后续文章将讨论未标有红色边框的项目。

在第一部分中,您将学习如何:

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

因此,让我们开始吧!

Angular 2不是AngularJS 1.x的后继者,而是一个全新的框架,它是基于AngularJS 1.x的课程构建的。 因此,使用Angular表示Angular 2的名称更改和AngularJS指的是AngularJS1.x。 在本文中,我们将交替使用Angular和Angular 2,但它们均指Angular 2。

从2017年2月9日起, ng deploy命令已从Angular CLI的核心中删除。 在这里阅读更多。

使用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 :数字,待办事项的唯一ID
  • title :字符串,待办事项的标题
  • complete :布尔值,是否要完成的待办事项

我们还提供了构造函数逻辑,使我们可以在实例化期间指定属性值,以便我们可以轻松地创建新的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测试运行程序并运行我们的所有单元测试。 这应该输出:

[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在src/app/todo-data.service.tsTodoDataService生成了以下代码:

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服务中的业务逻辑按预期工作,我们还在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);
    }));

  });

});

Karma预先配置了Jasmine 您可以阅读Jasmine文档以了解有关Jasmine语法的更多信息。

让我们放大上面的单元测试中的一些部分:

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

首先,什么是TestBed

TestBed@angular/core/testing提供的实用程序,用于配置和创建Angular测试模块,我们要在其中运行单元测试。

我们使用TestBed.configureTestingModule()方法来配置和创建一个新的Angular测试模块。 我们可以通过传入配置对象来根据自己的喜好配置测试模块。 此配置对象可以具有常规Angular模块的大多数属性。

在这种情况下,我们使用providers属性将测试模块配置为在运行测试时使用实际的TodoDataService

在本系列的第3部分中,我们将让TodoDataService与真实的REST API通信,并且将看到如何在测试模块中注入模拟服务,以防止测试与真实的API通信。

接下来,我们使用@angular/core/testing提供的inject功能在测试函数中从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中,用户界面的一部分由component表示。

编辑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" :当expression值为真时,向元素添加special CSS类
  • [style.color]="expression" :将color CSS属性设置为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在input元素enter时按下enter键时执行addTodo()

不必担心newTodoaddTodo()来源; 我们很快就会到达那里。 现在就尝试了解该视图的语义。

接下来是显示现有待办事项的部分:

<section class="main" *ngIf="todos.length > 0">
  • *ngIf="todos.length > 0" :仅在至少有一个待办事项时显示section元素及其所有子元素

在该部分中,我们要求Angular为每个待办事项生成一个li元素:

<li *ngFor="let todo of todos" [class.completed]="todo.complete">
  • *ngFor="let todo of todos" :遍历所有待办事项,并为每次迭代将当前待办事项分配给名为todo的变量
  • [class.completed]="todo.complete" :当todo.complete为真时,将completed CSS类应用于li元素

最后,我们显示每个待办事项的待办事项详细信息:

<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!';
}

因此,我们可以立即开始添加自定义逻辑。

我们将在AppComponent逻辑中需要TodoDataService服务,因此让我们首先将服务注入到我们的组件中。

首先,我们导入TodoDataService并在Component装饰器的providers数组中指定它:

// Import class so we can register it as dependency injection token
import {TodoDataService} from './todo-data.service';

@Component({
  // ...
  providers: [TodoDataService]
})
export class AppComponent {
  // ...
}

AppComponent的依赖项注入器现在将TodoDataService类识别为依赖项注入令牌,并在TodoDataService时返回TodoDataService的单个实例。

Angular的依赖项注入系统接受各种依赖项注入方法。 上面的语法是 Class provider配方的缩写符号 ,它使用单例模式提供依赖关系。 查看Angular的依赖项注入文档以获取更多详细信息。

既然组件依赖项注入器知道了它需要提供的内容,我们就可以通过在AppComponent构造函数中指定依赖项来要求它在组件中注入TodoDataService实例:

// 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);
  }
}

在构造函数中使用publicprivate on参数是一种简写形式,它使我们能够自动创建具有该名称的属性,因此:

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() 。 在我们的视图中,这与[(ngModel)]的双向绑定表达式中指定的Todo实例相同:

<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应该是不言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抱怨不知道ngModelFormsModule是,当Karma使用TestBed.createComponent()方法实例化AppComponent时未加载AppComponent

要了解有关TestBed更多信息,请确保查看有关测试Official Angular文档

为了确保角也加载FormsModule当噶实例化AppComponent使用TestBed.createComponent()我们必须指定FormsModuleimports的测试平台配置对象的属性:

/* 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! 并且没有包含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)

如果您想了解有关测试的更多信息,请务必查看Official Angular文档中有关测试的章节

随时随地进行现场演示 ,看看结果如何。

在结束本文之前,让我们看一下Angular CLI的最后一个非常酷的功能。

部署到GitHub Pages

Angular CLI使使用单个命令将我们的应用程序部署到GitHub Pages变得非常简单:

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

github-pages:deploy命令告诉Angular CLI构建我们的Angular应用程序的静态版本,并将其推送到我们的GitHub存储库的gh-pages分支中:

$ 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 CLI启动新的Angular应用程序
  • 如何在Angular服务中实现业务逻辑以及如何使用单元测试来测试我们的业务逻辑
  • 如何使用组件与用户交互以及如何使用依赖项注入将逻辑委托给服务
  • Angular模板语法的基础知识,简要介绍了Angular依赖项注入的工作方式
  • 最后,我们学习了如何快速将应用程序部署到GitHub Pages

有关Angular 2的知识还有很多。在本系列的下一部分中,我们将了解如何创建单独的组件以显示待办事项列表和各个待办事项详细信息。

因此,请继续关注有关Angular 2奇妙世界的更多信息。


From: https://www.sitepoint.com/angular-2-tutorial/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值