【译者注】本文应用开发及测试环境为Mac平台(即类Linux环境);因此,使用Windows平台的读者在使用CLI时可能需要作一定的调整。所谓CLI,实际上是Angular 2新引入的一种命令行操作方式,在这种方式下能够对常规的Angular 2操作以更快的方式实现。另外,本文作者使用的Javascript脚本是TypeScript。还有,作者使用了Karma工具(https://karma-runner.github.io/)对文中的TypeScript脚本进行了较全面的单元测试。因此,虽然本文介绍的是一个基础型Angular 2实例开发过程,但是还是值得一读。
简介
Angular 2是一个世界著名的开源Web前端开发框架,用于构建跨移动设备和桌面平台的Web应用程序。在本文中,我们将开发一个基于Angular 2 CLI方式的Todo Web应用程序。这个程序中实现的基本功能包括允许用户:
使用输入字段快速创建新的todo任务
切换todo任务的完成与未完成状态
删除不再需要的todo任务,等等
【注意】本文示例工程源码下载地址是https://github.com/sitepoint-editors/angular2-todo-app。上述工程的一个在线展示网址是https://sitepoint-editors.github.io/angular2-todo-app/。下面仅给出这个程序的一个静态截图。
Angular CLI简介
创建一个新的Angular 2应用程序最简单的方法之一是使用全新的Angular命令行界面(CLI)。CLI允许您实现: 生成新的Angular 2应用程序的样板文件代码
向现有的Angular 2应用程序添加指定的功能(包括组件、指令、服务、管道等)
若要安装Angular的CLI,请运行如下命令:
$ npm install -g angular-cli
这将在您的系统中以全局方式安装ng命令。
为了验证您的安装是否成功,您可以运行如下命令:
$ ng version
这个命令应会显示你已经安装的angular-cli版本号。更多的细节,请参考官方安装说明(https://github.com/angular/angular-cli#installation)。
生成Todo应用程序
现在,我们已经安装了Angular CLI。下面,我们可以使用它来生成我们的Todo应用程序了,命令如下:
- $ ng new angular2-todo-app
这将创建一个目录结构,其中包含我们所需要的一切基础内容,如下图所示:
├── angular-cli-build.js
├── angular-cli.json
├── config
│ ├── environment.dev.ts
│ ├── environment.js
│ ├── environment.prod.ts
│ ├── karma.conf.js
│ ├── karma-test-shim.js
│ └── protractor.conf.js
├── e2e
│ ├── app.e2e-spec.ts
│ ├── app.po.ts
│ ├── tsconfig.json
│ └── typings.d.ts
├── package.json
├── public
├── README.md
├── src
│ ├── app
│ │ ├── app.component.css
│ │ ├── app.component.html
│ │ ├── app.component.spec.ts
│ │ ├── app.component.ts
│ │ ├── environment.ts
│ │ ├── index.ts
│ │ └── shared
│ │ └── index.ts
│ ├── favicon.ico
│ ├── index.html
│ ├── main.ts
│ ├── system-config.ts
│ ├── tsconfig.json
│ └── typings.d.ts
├── tslint.json
├── typings
│ └── ...
└── typings.json
现在,你可以运行如下命令:
#切换到CLI刚刚为你创建的新目录下
- $ cd angular2-todo-app
#启动开发服务器
- $ ng serve
上述命令将启动一个本地开发服务器,你可以在你的浏览器中导航到如下URL来观察你的程序的初始界面:
http://localhost:4200/
使用Angular组件
当我们使用ng new命令时,Angular CLI已经为我们生成整个Angular 2应用程序的样板内容了。但它并非仅提供这些功能。它还可以帮助我们通过ng generate命令把其他对象添加到我们现有的Angular应用程序中,命令如下:
- # Generate a new component
- $ ng generate component my-new-component
- # Generate a new directive
- $ ng generate directive my-new-directive
- # Generate a new pipe
- $ ng generate pipe my-new-pipe
- # Generate a new service
- $ ng generate service my-new-service
- # Generate a new class
- $ ng generate class my-new-class
- # Generate a new interface
- $ ng generate interface my-new-interface
- # Generate a new enum
- $ ng generate enum my-new-enum
【提示】如果您还不熟悉Angular 2程序中的基本模块,特别推荐您先读一下这篇文章(https://angular.io/docs/ts/latest/quickstart.html)。
为了满足我们的Todo程序的需要,我们还需要实现如下功能:
创建一个Todo类来描述单个todo任务
创建一个TodoService服务来实现创建、更新和删除已有的todo任务
开发一个TodoApp组件来显示用户界面
下面,让我们一项一项地完成这些任务。
创建Todo类
因为我们使用的是TypeScript脚本语言,所以我们可以使用一个类来描述Todo任务项。我们可以通过Angular CLI命令来生成一个Todo类,命令如下:
- $ ng generate class Todo
上述命令将生成如下两个文件:
src/app/todo.spec.ts
src/app/todo.ts
让我们打开文件src/app/todo.ts,并使用如下内容替换掉原来内容:
- export class Todo {
- id: number;
- title: string = '';
- complete: boolean = false;
- constructor(values: Object = {}) {
- Object.assign(this, values);
- }
- }
每一个Todo项都有三个属性:
id:数字类型,对应于todo项的唯一的ID值
title:字符串类型,对应于todo项的标题
complete:布尔类型,指明当前todo项是否已完成
接下来,开始建立构造函数代码,从而允许我们在实例化过程中指定属性值:
- let todo = new Todo({
- title: 'Read SitePoint article',
- complete: false
- });
注意,CLI已经为我们生成了文件src/app/todo.spec.ts,所以我们可以添加一个单元测试来确保上述构造器按我们的期望结果那样工作:
- import {
- beforeEach, beforeEachProviders,
- describe, xdescribe,
- expect, it, xit,
- async, inject
- } from '@angular/core/testing';
- 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程序(https://karma-runner.github.io/)来运行上面我们创建的所有单元测试代码。
目前,我们已经有了一个Todo类。接下来,我们要创建Todo服务来管理所有todo任务项。
创建TodoService服务
TodoService服务将负责管理我们的Todo项目。在以后的文章中,你将会看到我们如何与REST API进行通信;但现在,我们只是在内存中存储所有的数据。
让我们再次使用Angular CLI来生成我们的服务:
- $ ng generate service Todo
生成内容如下:
src/app/todo.service.spec.ts
src/app/todo.service.ts
现在,我们可以把todo管理逻辑添加到我们的TodoService了,内容如下:
- import {Injectable} from '@angular/core';
- import {Todo} from './todo';
- @Injectable()
- export class TodoService {
- // 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): TodoService {
- if (!todo.id) {
- todo.id = ++this.lastId;
- }
- this.todos.push(todo);
- return this;
- }
- // Simulate DELETE /todos/:id
- deleteTodoById(id: number): TodoService {
- 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;
- }
- }
就本文目的而言,上述方法的具体实现细节并不至关重要。关键的内容是我们要实现服务中的业务逻辑。
为了确保我们的逻辑按预期方式工作,让我们向文件src/app/todo.service.spec.ts(已经由CLI生成)添加单元测试。
因为Angular CLI已经为我们生成了样板代码,所以我们只需要实现测试即可:
- import {
- beforeEach, beforeEachProviders,
- describe, xdescribe,
- expect, it, xit,
- async, inject
- } from '@angular/core/testing';
- import {Todo} from './todo';
- import {TodoService} from './todo.service';
- describe('Todo Service', () => {
- beforeEachProviders(() => [TodoService]);
- describe('#getAllTodos()', () => {
- it('should return an empty array by default', inject([TodoService], (service: TodoService) => {
- expect(service.getAllTodos()).toEqual([]);
- }));
- it('should return all todos', inject([TodoService], (service: TodoService) => {
- 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([TodoService], (service: TodoService) => {
- 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([TodoService], (service: TodoService) => {
- 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([TodoService], (service: TodoService) => {
- 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([TodoService], (service: TodoService) => {
- 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([TodoService], (service: TodoService) => {
- 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([TodoService], (service: TodoService) => {
- 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(https://github.com/jasmine/jasmine),你可以阅读资料http://jasmine.github.io/2.4/introduction.html来更多地了解有关它的语法。
为了校验我们编写的业务逻辑都是有效的,让我们再来运行单元测试:
- $ ng test
现在,既然我们已经有了一个可以使用的TodoService,那么接下来我们要实现程序的UI部分了。
值得注意的是,在Angular 2中,部分界面是使用组件(Components)来描述的。
创建TodoApp组件
让我们再一次使用CLI来生成我们所需要的程序组件吧:
- $ ng generate component TodoApp
上述命令生成内容如下:
- src/app/todo-app/todo-app.component.css
- src/app/todo-app/todo-app.component.html
- src/app/todo-app/todo-app.component.spec.ts
- src/app/todo-app/todo-app.component.ts
- src/app/todo-app/index.ts
【提示】 模板和样式可以在内联的脚本文件内指定。默认情况下,Angular的CLI将创建单独的文件;所以,在这篇文章中我们也是使用单独的文件。
接下来,让我们把组件的视图添加到文件src/app/todo-app/todo-app.component.html中:
- <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”:当事情发生时执行statement
[(property)]="expression":使用expression创建双向绑定
[class.special]="expression":当expression为真时在元素上添加special类
[style.color]="expression":把css属性color设置为expression的结果
【提示】如果你还不熟悉Angular的模板语法,那么你应当阅读一下官方有关文档,地址是https://angular.io/docs/ts/latest/guide/template-syntax.html。
下面,让我们具体地看一下上面的代码对我们的视图的影响。首先,在顶部使用了一个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()”:在Input控件中输入内容并在按下回车时告诉Angular执行addTodo()命令。
【提示】目前你先不用担心newTodo和addTodo()的存在问题,稍后会做这项工作。现在,只需尽力弄懂视图语义即可。
接下来,使用一个节显示todo部分:
- <section class="main" *ngIf="todos.length > 0">
其中,*ngIf="todos.length > 0"的含义是:当至少有一个todo项时,仅显示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为真时把CSS类completed应用于元素li。
最后,我们通过ngFor循环显示每一个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)
好,让我们稍微喘口气吧。到此我们已经使用了不少新的语法格式。
你可能想知道像addTodo()和newTodo.title这样的表达式是如何计算的。到目前,我们还没有定义它们,那么Angular是如何理解我们的意图的呢?
这正是表达式上下文(expression context)产生的原因。一个组件的表达式上下文就是组件实例。而组件实例就是组件类的一个实例。
我们的TodoAppComponent的组件类定义于文件src/app/todo-app/todo-app.component.ts中。
Angular CLI已经为我们的TodoAppComponent类创建了模板,代码如下:
- import { Component } from '@angular/core';
- @Component({
- moduleId: module.id,
- selector: 'app-todo-app',
- templateUrl: 'todo-app.component.html',
- styleUrls: ['todo-app.component.css']
- })
- export class TodoAppComponent {
- constructor() {}
- }
所以,我们可以马上开始加入我们自定义的逻辑。我们将需要TodoService实例;因此,让我们开始将它注入到我们的组件中。
首先,我们导入TodoService类,并在组件的修饰词数组部分指定它:
- // Import class so we can register it as dependency injection token
- import {TodoService} from '../todo.service';
- @Component({
- // ...
- providers: [TodoService]
- })
- export class TodoAppComponent {
- // ...
- }
TodoAppComponent的依赖注入器现在能够识别出TodoService类为依赖性注入符号并在我们要求时返回TodoService的单一实例。
【提示】Angular的依赖注入系统能够接受各种各样的依赖项注入。上述语法只是类提供器(Class Provider:使用单例模式提供依赖性)格式的一个速记表示。有关此内容更多的细节,请参考官方的网址https://angular.io/docs/ts/latest/guide/dependency-injection.html。
现在,组件的依赖注入器知道它需要提供什么了,我们要求它通过在TodoAppComponent构造函数中指定依赖项来在我们的组件中注入TodoService实例:
- // Import class so we can use it as dependency injection token in the constructor
- import {TodoService} from '../todo.service';
- @Component({
- // ...
- })
- export class TodoAppComponent {
- // Ask Angular DI system to inject the dependency
- // associated with the dependency injection token `TodoService`
- // and assign it to a property called `todoService`
- constructor(private todoService: TodoService) {
- }
- // Service is now available as this.todoService
- toggleTodoComplete(todo) {
- this.todoService.toggleTodoComplete(todo);
- }
- }
现在,我们可以实现我们的视图中需要的所有逻辑了。为此,只需要向我们 TodoAppComponent类中添加属性和方法就可以了:
- import {Component} from '@angular/core';
- import {Todo} from '../todo';
- import {TodoService} from '../todo.service';
- @Component({
- moduleId: module.id,
- selector: 'todo-app',
- templateUrl: 'todo-app.component.html',
- styleUrls: ['todo-app.component.css'],
- providers: [TodoService]
- })
- export class TodoAppComponent {
- newTodo: Todo = new Todo();
- constructor(private todoService: TodoService) {
- }
- addTodo() {
- this.todoService.addTodo(this.newTodo);
- this.newTodo = new Todo();
- }
- toggleTodoComplete(todo) {
- this.todoService.toggleTodoComplete(todo);
- }
- removeTodo(todo) {
- this.todoService.deleteTodoById(todo.id);
- }
- get todos() {
- return this.todoService.getAllTodos();
- }
- }
当组件类实例化时,我们首先实例化一个newTodo属性并分配新的Todo()。下面的代码展示了在我们的视图中添加的双向绑定到的newTodo:
- <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">
无论视图中的输入值何时改变,组件实例中的值都被更新。而无论组件实例中的输入值何时改变,视图中的输入元素中的值都将更改。
接下来,我们要实现我们的视图中使用的所有方法。
它们的具体实现代码很短,应该是不需要给予过多解释的,因为我们已经把所有业务逻辑委派到todoService了。
【提示】把业务逻辑委派到一个专门的服务中是一种良好的编程实践,因为它使我们能够集中精力管理和测试业务逻辑。
最后,在结束本教程前,让我们来了解一下Angular CLI的最后一个很酷的功能吧。
部署到GitHub网站
Angular的CLI使得将我们的应用部署到GitHub页变得超级简单——使用类似于下面的这样一个命令即可搞定:
- $ 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/angular2-todo-app/
- Github pages might take a few minutes to show the deployed site.
现在,我们的应用程序可以通过网站地址https://sitepoint-editors.github.io/angular2-todo-app/进行访问了。
赶快去打开这个网址去试试吧。
小结
Angular 2无疑是一只猛兽!一只非常强大的猛兽!
在本文中,我向你介绍了很多很多。现在,让我们回顾一下我们在这篇文章中所学到的内容吧:
我们学习了如何安装Angular CLI并了解了在创建新的应用程序或添加现有应用程序的新特征时它如何节约我们的时间。
我们学习了如何在一个Angular服务中实现业务逻辑以及如何使用单元测试来测试我们的业务逻辑。
我们学习了如何使用组件与用户交互以及如何使用依赖注入委派逻辑到服务中。
我们学习了Angular模板语法基础知识,并简要地谈论了Angular依赖项注入的工作原理。
最后,我们学习了如何把我们的应用程序快速部署到GitHub网页。
在以后的文章中,我们还有很多有关Angular 2的内容探讨,例如:
使用Angular 2 HTTP服务与REST API后端进行通信
使用Angular管道功能过滤todo内容
通过路由来使本文中的应用变成一个多页式应用程序
以及其他更多更多……
所以,敬请期待更多的关于Angular 2这个奇妙的世界吧。