文档结构  
可译网翻译有奖活动正在进行中,查看详情 现在前往 注册?
原作者:Todd Motto (2016-12-20)    来源:SitePoint [英文]
CY2    计算机    2016-12-21    0评/605阅
翻译进度:已翻译   参与翻译: toypipi (15), zhongzhong (14), awsd (3), 班纳睿 (1)

Illustration of a monitor covered with post-it notes and to-do lists

本文由客座作者Todd MottoJurgen Van de Moere撰写。 SitePoint客座文章旨在为您带来由杰出作家和JavaScript社区演讲者创作的有吸引力的内容。

2016.12.20:本文根据读者反馈进行了修订,并使用Angular 2的当前发行版。

这是一个由4部分组成的关于如何在Angular 2中编写Todo应用程序的系列教程中的第一篇文章:

  1. 第1部分 - 启动并运行我们的第一个版本的Todo应用程序
  2. 第2部分 - 创建单独的组件以显示todo列表和单个待办事项的列表
  3. 第3部分 - 更新Todo服务以与REST API通信
  4. 第4部分 - 使用组件路由器路由到不同的组件
第 1 段(可获 1.58 积分)

在每篇文章中,我们将细化应用程序的底层架构,并确保我们有一个工作版本的应用程序,如下所示:

Animated GIF of Finished Todo Application

本系列的结束,我们的应用程序体系结构是这样的:

Application Architecture of Finished Todo Application

更多来自此作者

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

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

  • 使用Angular CLI初始化您的Todo应用程序
    创建一个Todo类来表示个别待办事项
    创建一个TodoDataService服务来创建,更新和删除todo
    使用AppComponent组件显示用户界面
  • 将应用程序部署到GitHub页面
第 2 段(可获 1.65 积分)

让我们开始吧!

Angular 2不是AngularJS 1.x的继承者,它可以被认为是一个建立在AngularJS 1.x的经验教训基础上的全新框架。 因此,它的名称也发生了改变,其中Angular用于表示Angular 2,AngularJS指AngularJS 1.x.。在本文中,我们将会交替使用Angular和Angular 2,但他们指的都是Angular 2。

使用Angular CLI初始化您的Todo应用程序

启动新的Angular 2应用程序的最简单方法之一是使用Angular的命令行界面(CLI)。

要安装Angular CLI,请运行:

$ npm install -g angular-cli
第 3 段(可获 1.23 积分)

它将在您的系统上全局安装 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
第 4 段(可获 0.7 积分)

如果您不熟悉Angular CLI,请确保查看Angular CLI 终极参考手册.。

您现在可以导航到新目录:

$ cd todo-app

并启动Angular CLI开发服务器:

$ ng serve

它将启动一个本地开发服务器,您可以在浏览器中通过 http://localhost:4200/ 导航到该服务器。

Angular CLI开发服务器包括LiveReload支持,因此当源文件更改时,浏览器会自动重新加载应用程序。

这太方便啦!

创建Todo类

因为Angular CLI生成TypeScript文件,因此我们可以使用一个类来表示Todo项目。

第 5 段(可获 1.18 积分)

让我们使用 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:number,todo项的唯一ID
  • title:string,todo项的标题
  • complete:boolean,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();
  });
});

 

第 6 段(可获 1.58 积分)

让我们添加一个额外的单元测试,以确保构造函数逻辑按预期工作:

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 t测试运行程序并运行我们所有的单元测试。 应该会输出:

[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 类来代表一个个体todo,让我们创建一个 TodoDataService 服务来管理所有的todo。

第 7 段(可获 1.03 积分)

创建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选项。

第 8 段(可获 0.95 积分)

Angular CLI在 src/app/todo-data.service.ts 中为我们的 TodoDataService 生成了以下代码:

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 id's
  lastId: number = 0;

  // Placeholder for todo's
  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;
  }

}

 

第 9 段(可获 0.35 积分)

方法的实际实现细节不是本文探讨的主要目的。 主要的是,我们将业务逻辑集中在服务中。

为了确保我们的 TodoDataService 服务中的业务逻辑按预期工作,我们还在src/app/todo.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);
    }));

  });

});
第 10 段(可获 0.61 积分)

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

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

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

首先什么是TestBed

TestBed是由 @angular/core/testing 提供的实用工具,用于配置和创建一个Angular 测试模块,其中我们要运行我们的单元测试。

我们使用TestBed.configureTestingModule() 方法来配置和创建一个新的Angular 测试模块。 我们可以通过传递配置对象来配置我们喜欢的测试模块。 此配置对象可以具有普通Angular模块的大多数属性。

第 11 段(可获 1.26 积分)

在这种情况下,我们使用 providers 属性来配置测试模块,以便在运行测试时使用真正的TodoDataService 。

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

接下来,我们使用 @angular/core/testing 提供的 inject 函数从TestBed injector在测试函数中注入正确的服务:

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]);
}));
第 12 段(可获 1 积分)

inject 函数的第一个参数是一个Angular依赖注入令牌数组。 第二个参数是测试函数,其参数是与来自数组的依赖注入令牌对应的依赖。

这里我们通过在第一个参数数组中指定的方式告诉TestBed injector注入TodoDataService 。 因此,我们可以在测试函数中以service 访问TodoDataService,因为 service 是测试函数的第一个参数的名称。

如果你想了解更多有关Angular测试的内容,请务必查阅官方的Angular测试指南

第 13 段(可获 1.23 积分)

为了验证我们的服务是否按预期工作,我们再次运行单元测试:

$ 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服务,是时候实现实际的用户界面。

第 14 段(可获 0.45 积分)

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

 

第 15 段(可获 0.88 积分)

这里是一个超短的Angular的模板语法介绍,以防你没有看到它:

  • [property]="expression": 将元素的属性设置为expression的值.
  • (event)="statement": 当event发生时执行语句
  • [(property)]="expression": 使用expression创建双向绑定
  • [class.special]="expression": 当expression的值为truthy时,向元素添加specialCSS类
  • [style.color]="expression": 将color CSS 属性设置为expression的值

如果你还不熟悉Angular的模板语法,那么你真应该阅读一下官方模板语法文档

让我们看看这对我们的视图意味着什么。在顶部有一个input标签创建一个新的todo:

<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元素中输入并按下回车键时执行addTodo()

不要担心newTodo 或addTodo()来自哪里,我们稍后会讲到。现在只是试图了解视图的语义。

第 16 段(可获 1.75 积分)

接下来通过一个section 显示现有的待办事项:

<section class="main" *ngIf="todos.length > 0">
  • *ngIf="todos.length > 0": 只有当至少有一个todo时才显示 section  元素及其所有子元素

在该section 中,我们要求Angular为每个todo生成一个li元素:

<li *ngFor="let todo of todos" [class.completed]="todo.complete">
  • *ngFor="let todo of todos": 循环遍历所有todo,每次迭代将当前todo分配给todo 变量
  • [class.completed]="todo.complete": 当todo.complete为truey时,将CSS类 complete 应用到li元素

最后我们显示每个todo的细节:

<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)
第 17 段(可获 1.19 积分)

好的, 让我们深呼吸. 我们刚刚接触了一些语法.

如果你想去学习关于angular模板语法的每一个细节, 请阅读 官方模板语法.

你也许好奇 addTodo() 和newTodo.title 是怎么被执行的. 我们还没有定义它们, 那angular怎么知道我们的意思?

这正是表达上下文的由来. 表达式上下文是一个执行表达式的上下文环境. 每一个组件都有一个表达式上下文环境. 并且这个组件一定是component类的实例.

第 18 段(可获 1.24 积分)

我们的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 的逻辑中, 所以我们通过注入来获取对应的service实例.

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

第 19 段(可获 0.71 积分)
// 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 类的单例对象.

Angular的依赖注入系统接受各种依赖注入配方. 上面的语法是一种速记符号类提供者配方,提供了使用单例模式的依赖. 跟多详细信息,请查看 Angular依赖注入文档.

第 20 段(可获 0.83 积分)

现在组件的依赖注入器知道它需要提供什么,我们要求它通过在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);
  }
}

对构造函数中的参数使用public或private是一种简化符号,它允许我们使用该名称自动创建属性,因此:

class AppComponent {

  constructor(private todoDataService: TodoDataService) {
  }
}

is a shorthand notation for:

class AppComponent {

  private todoDataService: TodoDataService;

  constructor(todoDataService: TodoDataService) {
    this.todoDataService = todoDataService;
  }
}

We can now implement all view logic by adding properties and methods to our AppComponent class:

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

}

 

第 21 段(可获 0.93 积分)

当组件初始化完成的时候,我们首先定义了一个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服务类.

将业务逻辑委托给服务类是一种很好的编程实践,它使我们能够集中管理和测试.

在我们尝试在浏览器中查看结果之前,让我们再次运行我们的单元测试.:

第 22 段(可获 1.6 积分)
$ 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!');
  }));
});

 

第 23 段(可获 0.15 积分)

原因是 Angular提示说不认知ngModel,因为FormsModule没有在Karma实例化AppComponent组件时的TestBed.createComponent()函数中加载进来。

要了解更多TestBed,请查看 Angular官方测试文档.

为了确保angular在实例化AppComponent组件的时候FormsModule已经加载进来了, 我们必须在TestBed配置对象的imports属性中指定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!');
  }));
});

我们现在有2个失败的测试:

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)

 

第 24 段(可获 0.83 积分)

Karma 警告我们说,组件实例中没有一个叫做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');
}));

 

第 25 段(可获 0.89 积分)

如果你想了解更多关于测试的内容,请查看 Angular官方测试文档中的这个章节.

请随意使用在线实例 去观察它们是如何工作的.

在我们结束这篇文章之前, 让我们来看看最后一个Angular CLI非常酷的功能.

部署到GitHub页面

Angular CLI 使它超级简单的将我们的应用程序部署到GitHub页面用一个命令如下:

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

github-pages:deploy 命令告诉Angular CLI去构建一个我们应用的静态版本然后上传到我们在Github上的仓库的gh-pages分支中 :

第 26 段(可获 1.31 积分)
$ 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页面
第 27 段(可获 1.36 积分)

关于Angular 2 要学的东西还有很多。在这个系列的下一部分中,我们将看看如何创建不同的组件来分别展示待办事项的列表和其中一项的详情。 

所以请继续关注更多关于这个Angular 2的精彩的世界。 

你已经使用Angular 2 构建过什么东西没有?你在计划将你的Angular 1.x 应用升级吗?请留下你的评论,让我们知道你在想些什么!

这篇文章由 Vildan Softic 做的同行评审。 非常感谢所有SitePoint的同行评审们,是你们让SitePoint的内容尽善尽美!

第 28 段(可获 1.35 积分)

文章评论