
在本章中,我们将构建一个应用,它能让用户发表推荐文章(包括标题和URL)并对每篇文章投票。
你可以把该应用看作类似于Reddit
或Product Hunt
的起步版网站。
这个简单的应用将涵盖Angular中的大部分基本要素,包括:
●构建自定义组件;
●从表单中接收用户输入;
●把对象列表渲染到视图中;
●拦截用户的点击操作,并据此作出反应。
读完本章之后,你将掌握如何构建基本的Angular应用。
图1-1展示了该应用最终完成后的界面截图。

图1-1 完成后的应用
首先,用户将提交一个新的链接。之后,其他用户可以对每篇文章投票:“顶”或“踩”。每个链接都有一个最终得票数,我们可以对自己认为有用的链接投票(如图1-2所示)。

图1-2 包含新文章的应用
在本项目和整本书中,我们都将使用TypeScript。TypeScript是JavaScript ES6版的一个超集,增加了类型支持。本章不会深入讲解TypeScript;如果你熟悉ES5(“普通”的JavaScript)或ES6(ES2015),那么在后续的学习过程中应该不会有什么问题。
在第2章中,我们将更深入地学习TypeScript。因此,即使你对某些新语法不太熟悉,也不必担心。
1.2.1 TypeScript
要开始使用TypeScript,首先需要安装Node.js。安装方式很多,请参见Node.js官方网站(https://nodejs.org/download/)了解详情。
我必须用TypeScript吗?并非如此!要使用Angular,TypeScript并不是必需的,但它可能是最好的选择。Angular也有一套ES5
API,但Angular本身就是用TypeScript写成的,所以人们一般也会选用它。本书也将使用TypeScript,因为它确实很棒,能让Angular写起来更简单。当然,并不是非它不可。
安装完Node.js,接着就要安装TypeScript了。请确保安装1.7或更高的版本。要想安装它,请运行下列npm命令:
$ npm install -g typescript
通常,npm是Node.js的一部分。如果你的系统中没有npm命令,请确认你安装的Node.js是包含它的版本。
Windows用户:我们将在全书中使用Linux/Mac风格的命令行。强烈建议你安装Cygwin
。借助它,你就能直接运行本书中的这些命令了。
1.2.2 angular-cli
Angular提供了一个命令行工具angular-cli,它能让用户通过命令行创建和管理项目。它自动化了一系列任务,比如创建项目、添加新的控制器等。多数情况下,选用angular-cli都是明智的决定。当你创建和维护应用时,它能帮你遵循很多常用模式。
要想安装angular-cli,只要运行下列命令即可:
$ npm install -g angular-cli@1.0.0-beta.18
安装完毕之后,你就可以在命令行中用ng命令运行它了。运行ng命令时,你会看到一大堆输出,不过不用管它;往回滚屏,你会看到如下内容:
$ ng
Could not start watchman; falling back to NodeWatcher for file system events.
Visit http://ember-cli.com/user-guide/#watchman for more info.
Usage:ng <command(Default:help)>
之所以得到这一大堆输出,是因为当我们不带参数运行ng命令时,它就会执行默认的help命令。help命令会解释如何使用本工具。
如果你在OS X或Linux上运行,可能还会在输出中看到这一行:
Could not start watchman; falling back to NodeWatcher for file system events.
这意味着我们没有安装过一个名叫watchman的工具。此工具能帮助angular-cli监听文件系统的变化。如果你在OS X上运行,建议使用Homebrew工具安装它,命令如下:
$ brew install watchman

如果你是OS X用户并且运行这个brew命令时出现错误,那么表示你尚未正确安装Homebrew工具。请参阅http://brew.sh/来安装它,然后再试一次。
如果你是Linux用户,可以参阅https://ember-cli.com/user-guide/#watchman来学习如何安装watchman。
如果你是Windows用户,那么不必安装任何东西,angular-cli将使用原生的Node.js文件监视器。
现在你应该已经装好angular-cli及其依赖了。在本章中,我们就用它来创建第一个应用。
1.2.3 示例项目
现在,环境已经准备好了,我们这就来编写第一个Angular应用吧!
打开终端窗口并且运行ng new命令,快速创建一个新的项目:
$ ng new angular2_hello_world
运行之后,你将看到下列输出:
installing ng 2
create .editorconfig
create README.md
create src/app/app.component.css
create src/app/app.component.html
create src/app/app.component.spec.ts
create src/app/app.component.ts
create src/app/app.module.ts
create src/app/index.ts
create src/app/shared/index.ts
create src/assets/.gitkeep
create src/assets/.npmignore
create src/environments/environment.dev.ts
create src/environments/environment.prod.ts
create src/environments/environment.ts
create src/favicon.ico
create src/index.html
create src/main.ts
create src/polyfills.ts
create src/styles.css
create src/test.ts
create src/tsconfig.json
create src/typings.d.ts
create angular-cli.json
create e2e/app.e2e-spec.ts
create e2e/app.po.ts
create e2e/tsconfig.json
create .gitignore
create karma.conf.js
create package.json
create protractor.conf.js
create tslint.json
Successfully initialized git.
(Installing packages for tooling via npm
它将运行一段时间,进行npm依赖的安装。一旦安装结束,我们会看到一条成功信息:
Installed packages for tooling via npm.
这里生成了很多文件!现在不用关心它们都是什么。我们会在本书中讲解每一个文件的含义和用途。不过现在,我们先把注意力集中在如何用Angular代码开始工作上。
进入ng命令创建的angular2_hello_world目录,来看看它里面都有什么:
$ cd angular2_hello_world
$ tree -F -L 1
.
├── README.md // an useful README
├── angular-cli.json // angular-cli configuration file
├── e2e/ // end to end tests
├── karma.conf.js // unit test configuration
├── node_modules/ // installed dependencies
├── package.json // npm configuration
├── protractor.conf.js // e2e test configuration
├── src/ // application source
└── tslint.json // linter config file
3 directories, 6 files
我们目前关注的目录是src,应用代码就在里面。下面看看我们在那里创建了什么:
$ cd src
$ tree -F
.
|—— app/
| |—— app.component.css
| |—— app.component.html
| |—— app.component.spec.ts
| |—— app.component.ts
| |—— app.module.ts
| |—— index.ts
| `—— shared/
| `—— index.ts
|—— assets/
|—— environments/
| |—— environment.dev.ts
| |—— environment.prod.ts
| `—— environment.ts
|—— favicon.ico
|—— index.html
|—— main.ts
|—— polyfills.ts
|—— styles.css
|—— test.ts
|—— tsconfig.json
`—— typings.d.ts
4 directories, 18 files
用你惯用的文本编辑器打开index.html,应该会看到如下代码。
code/first_app/angular2_hello_world/src/index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Angular2HelloWorld</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root>Loading……</app-root>
</body>
</html>
我们把它分解一下。
code/first_app/angular2_hello_world/src/index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Angular2HelloWorld</title>
<base href="/">
如果你熟悉HTML,这第一部分就很平淡无奇了。我们在这里声明了页面的字符集(charset)、标题(title)和基础URL(base href)。
code/first_app/angular2_hello_world/src/index.html
<meta name="viewport" content="width=device-width, initial-scale=1">
如果你继续深入模板主体(body),就会看到下列代码。
code/first_app/angular2_hello_world/src/index.html
<app-root>Loading……</app-root>
</body>
</html>
我们的应用将会在app-root标签处进行渲染,稍后剖析源代码的其他部分时还会看到它。文本Loading……是一个占位符,在应用代码加载之前会显示它。我们可以借助此技巧来通知用户该应用正在加载,可以像这里一样显示一条消息,也可以显示一个加载动画或其他形式的进度通知。
之后就可以编写应用代码了。
在开始修改之前,我们先把这个自动生成的初始应用加载到浏览器中。angular-cli有一个内建的HTTP服务器,我们可以用它来启动应用。回到终端,进入应用的根目录(在本应用中是./angular2_hello_world目录)并运行命令。
$ ng serve
** NG Live Development Server is running on http://localhost:4200.**
// a bunch of debug messages
Build successful - 1342ms.
我们的应用正在localhost的4200端口上运行。打开浏览器并访问http://localhost:4200,结果如图1-3所示。
注意,如果4200端口由于某种原因被占用了,也可以在其他端口号上启动。仔细阅读你电脑上的输出信息,找出开发服务器的实际URL。

图1-3 运行中的应用
好,现在我们设置好了应用,而且知道了该如何运行它,可以开始写代码了。
1.3.1 制作Component
Angular背后的指导思想之一就是组件化。
在Angular应用中,我们写HTML标记并把它变成可交互的应用。不过浏览器只认识一部分标签,比如<select>、<form>和<video>等,它们的功能都是由浏览器的开发者预先定义好的。
如果我们想教浏览器认识一些新标签,该怎么办呢?比如我们想要一个<weather>标签,用来显示天气;又比如想要一个<login>标签,用来创建一个登录面板。
这就是组件化背后的基本思想:我们要教浏览器认识一些拥有自定义功能的新标签。
如果你用过AngularJS,那么可以把组件当作新版本的指令。
让我们来创建第一个组件。写完该组件之后,就能在HTML文档中使用它了,就像这样:
<app-hello-world></app-hello-world>
要使用angular-cli来创建新组件,可以使用generate(生成)命令。
要生成hello-world组件,我们需要运行下列命令:
$ ng generate component hello-world
installing component
create src/app/hello-world/hello-world.component.css
create src/app/hello-world/hello-world.component.html
create src/app/hello-world/hello-world.component.spec.ts
create src/app/hello-world/hello-world.component.ts
那该怎么定义一个新组件呢?最基本的组件包括两部分:
(1)Component注解
(2)组件定义类
下面来看看组件的代码,然后逐一讲解。打开第一个TypeScript文件:src/app/hello-world/hello-world.component.ts。
code/first_app/angular2_hello_world/src/app/hello-world/hello-world.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector:'app-hello-world',
templateUrl:'./hello-world.component.html',
styleUrls:['./hello-world.component.css']
})
export class HelloWorldComponent implements OnInit {
constructor(){ }
ngOnInit(){
}
}
注意,TypeScript文件的后缀是.ts而不是.js。问题在于浏览器并不知道该如何解释TypeScript文件。为了解决这个问题,ng
serve命令会自动把.ts文件编译为.js文件。
这个代码片段乍一看可能有点恐怖,但别担心,我们接下来就会一步步讲解它。
1.3.2 导入依赖
import语句定义了我们写代码时要用到的那些模块。这里我们导入了两样东西:Component和OnInit。
我们从"@angular/core"模块中导入了组件(import Component)。"@angular/core"部分告诉程序到哪里查找所需的这些依赖。这个例子中,我们告诉编译器:"@angular/core"定义并导出了两个JavaScript/TypeScript对象,名字分别是Component和OnInit。
同样,我们还从这个模块中导入了OnInit(import OnInit)。稍后你就会知道,OnInit能帮我们在组件的初始化阶段运行某些代码。不过现在先不用管它。
注意这个import语句的结构是import { things } from wherever格式。我们把{ things }这部分的写法叫作解构。解构是由ES6和TypeScript提供的一项特性,下一章会深入讲解。
import的用法很像Java中的import或Ruby中的require:从另一个模块中拉取这些依赖,并且让这些依赖在当前文件中可用。
1.3.3 Component注解
导入依赖后,我们还要声明该组件。
code/first_app/angular2_hello_world/src/app/hello-world/hello-world.component.ts
@Component({
selector:'app-hello-world',
templateUrl:'./hello-world.component.html',
styleUrls:['./hello-world.component.css']
})
如果你习惯用JavaScript编程,那么下面这段代码可能看起来有点怪异:
@Component({
// ……
})
这是什么?如果你有Java开发背景,应该会很熟悉:它们是注解。
AngularJS的依赖注入技术在幕后使用了注解的概念。也许你还不熟悉它们,但注解其实是让编译器为代码添加功能的途径之一。
我们可以把注解看作添加到代码上的元数据。当在HelloWorld类上使用@Component时,就把HelloWorld“装饰”(decorate)成了一个Component。
这个<app-hello-world>标签表示我们希望在HTML中使用该组件。要实现它,就得配置@Component并把selector指定为app-hello-world。
@Component({
selector:'app-hello-world'
// …… more here
})
有很多种方式来配置选择器(selector),类似于CSS选择器、XPath或JQuery选择器。Angular组件对选择器的混用方式添加了一些特有的限制,稍后会谈到。现在,只要记住我们正在定义一个新的HTML标签就可以了。
这里的selector属性用来指出该组件将使用哪个DOM元素。如果模板中有<app-hello-world></app-hello-world>标签,就用该Component类及其组件定义信息对其进行编译。
1.3.4 用templateUrl添加模板
在这个组件中,我们把templateUrl指定为./hello-world.component.html。这意味着我们将从与该组件同目录的hello-world.component.html文件中加载模板。下面来看看这个文件。
code/first_app/angular2_hello_world/src/app/hello-world/hello-world.component.html
<p>
hello-world works!
</p>
这里定义了一个p标签,其中包含了一些简单的文本。当Angular加载该组件时,就会读取此文件的内容作为组件的模板。
1.3.5 添加template
我们有两种定义模板的方式:使用@Component对象中的template属性;指定templateUrl属性。
我们可以通过传入template选项来为@Component添加一个模板:
@Component({
selector:'app-hello-world',
template:`
<p>
hello-world works inline!
</p>
`
})
注意,我们在反引号中(` …… `)定义了template字符串。这是ES6中的一个新特性(而且很棒),允许使用多行字符串。使用反引号定义多行字符串,可以让我们更轻松地把模板放到代码文件中。

你真的应该把模板放进代码文件中吗?答案是:视情况而定。在很长一段时间里,大家都觉得最好把代码和模板分开。这对于一些开发团队来说确实更容易,不过在某些项目中会增加成本,因为你将不得不在一大堆文件之间切换。
个人观点:如果模板行数短于一页,我更倾向于把模板和代码放在一起(也就是.ts文件中)。这样就能同时看到逻辑和视图部分,同时也便于理解它们之间如何互动。
把视图和代码内联在一起的最大缺点是,很多编辑器仍然不支持对内部HTML字符串进行语法高亮。我们期待能尽快看到有更多编辑器支持对模板字符串内嵌HTML的语法高亮。
1.3.6 用styleUrls添加CSS样式
注意styleUrls属性:
styleUrls:['./hello-world.component.css']
这段代码的意思是,我们要使用hello-world.component.css文件中的CSS作为该组件的样式。Angular使用一项叫作样式封装(style-encapsulation)的技术,它意味着在特定组件中指定的样式只会应用于该组件本身。14.1节会深入讨论它。
目前还用不到任何“组件局部样式”,你只要先知道它就行了(或整体删除此属性)。
你可能注意到了该属性与template有个不同点:它接收一个数组型参数。这是因为我们可以为同一个组件加载多个样式表。
1.3.7 加载组件
现在,我们已经写完了第一个组件的代码,那该如何把它加载到页面中呢?
如果再次在浏览器中访问此应用,我们会看到一切照旧。这是因为我们仅仅创建了该组件,但还没有使用它。
为了解决这一点,需要把该组件的标签添加到一个将要渲染的模板中去。打开文件first_app/angular2_hello_world/src/app/app.component.html。
记住,因为我们为HelloWorldComponent配置了app-hello-world选择器,所以要在模板中使用<app-hello-world></app-hello-world>。让我们把<app-hello-world>标签添加到app.component.html中。
code/first_app/angular2_hello_world/src/app/app.component.html
<h1>
{{title}}
<app-hello-world></app-hello-world>
</h1>
现在,刷新该页面就会看到如图1-4所示结果。

图1-4 “Hello world”一切正常
工作正常!
现在,该组件渲染了一个静态模板。这表示我们的组件还不够有趣。
设想有一个应用会显示一个用户列表,并且我们想在其中显示用户的名字。在渲染整个列表之前,需要先渲染一个单独的用户。因此,我们来创建一个新的组件,它将显示用户的名字。
再次使用ng generate命令:
ng generate component user-item
记住,想看到我们创建好的组件,就要把它添加到一个模板中。
让我们把app-user-item标签添加到app.component.html中,以便看到所作的改动。把app.component.html修改成下面这样。
code/first_app/angular2_hello_world/src/app/app.component.html
<h1>
{{title}}
<app-hello-world></app-hello-world>
<app-user-item></app-user-item>
</h1>
然后刷新页面,并确认你在该页看到文本user-item works!。
我们希望UserItemComponent显示一个指定用户的名字。
因此,引入name并声明为组件的一个新属性。有了name属性,我们就能在不同的用户之间复用该组件了(但要求页面脚本、逻辑和样式相同)。
为了添加名字,我们要在UserItemComponent类上引入一个属性,来声明该组件有一个名叫name的局部变量。
code/first_app/angular2_hello_world/src/app/user-item/user-item.component.ts
export class UserItemComponent implements OnInit {
name:string; // <—— added name property
constructor(){
this.name = 'Felipe'; // set the name
}
ngOnInit(){
}
}
注意,我们改变了以下两点。
●name属性
我们往UserItemComponent类添加了一个属性。注意,这相对于ES5 JavaScript来说是个新语法。在name:string;中,name是我们想设置的属性名,而string是该属性的类型。
为name指定类型是TypeScript中的特性,用来确保它的值必须是string。这些代码在UserItemComponent类的实例中设置了一个名为name的属性,并且编译器会确保name是一个string。
●构造函数
在UserItemComponent类中,我们定义了一个构造函数。这个函数会在创建这个类的实例时自动调用。
在我们的构造函数中,可以通过this.name来设置name属性。
如果这样写:
code/first_app/angular2_hello_world/src/app/user-item/user-item.component.ts
constructor(){
this.name = 'Felipe'; // set the name
}
就表示当一个新的UserItemComponent组件被创建时,把name设置为'Felipe'。
●渲染模板
填好这个值之后,我们可以用模板语法(也就是双花括号语法{{ }})在模板中显示该变量的值。
code/first_app/angular2_hello_world/src/app/user-item/user-item.component.html
<p>
Hello {{ name }}
</p>
注意,我们在template中引入了一个新的语法:{{ name }}。这些括号叫作“模板标签”(也叫“小胡子标签”)。模板标签中间的任何东西都会被当作一个表达式来展开。这里,因为template是绑定到组件上的,所以name将会被展开为this.name的值,也就是'Felipe'。
●试试看
进行这些修改之后,重新加载页面,页面上应该显示Hello Felipe,如图1-5所示。

图1-5 带有数据的应用
现在,我们可以对一个单独的名字问好了,但是如果想对一组名字问好呢?
如果你以前用过AngularJS,那么可能用过ng-repeat指令。在Angular中,NgFor是类似的指令(我们在模板标记中通过*ngFor语法来使用它,稍后会讲到)。它们在语法上略有不同,但作用是一样的:为一组对象反复渲染同样的页面脚本。
下面创建一个会渲染用户列表的新组件。我们还是从生成一个新组件开始:
ng generate component user-list
接着,把app.component.html文件中的<app-user-item>替换为<app-user-list>。
code/first_app/angular2_hello_world/src/app/app.component.html
<h1>
{{title}}
<app-hello-world></app-hello-world>
<app-user-list></app-user-list>
</h1>
就像给UserItemComponent添加了name属性一样,我们也给UserListComponent添加names属性。
不过,不再设置该属性只存储一个字符串,而是存储一个字符串数组。数组的语法就是在类型后面紧跟一对方括号[],如下所示。
code/first_app/angular2_hello_world/src/app/user-list/user-list.component.ts
export class UserListComponent implements OnInit {
names:string[];
constructor(){
this.names = ['Ari', 'Carlos', 'Felipe', 'Nate'];
}
ngOnInit(){
}
}
要留意的第一处变化是在UserListComponent类中添加了一个新的string[]属性。这种语法表示names的类型是string构成的数组。它的另一种写法是Array<string>。
我们还修改了构造函数,让它将this.names的值设置为['Ari', 'Carlos', 'Felipe', 'Nate']。
现在就可以更新模板,渲染出这个名字列表了。这时我们要用到*ngFor,它会在一个列表上进行迭代,为列表中的每一个条目生成一个新标签。新模板如下所示。
code/first_app/angular2_hello_world/src/app/user-list/user-list.component.html
<ul>
<li *ngFor="let name of names">Hello {{ name }}</li>
</ul>
我们用一个ul和一个添加了*ngFor="let name of names"属性的li元素来更新模板。这个*字符和let语法可能会让你摸不着头脑,我们把它们拆开来解释。
*ngFor语法是说我们想在这个属性上使用NgFor指令。你可以把NgFor理解成一个类似于for的循环,其目的是为集合中的每个条目都新建一个DOM元素。
它的值是"let name of names"。names是我们在HelloWorld对象中定义的名字数组。let name叫作引用。"let name of names"的意思是,循环处理names中的每一个元素并将其逐个赋值给一个名叫name的局部变量。
NgFor指令将为数组names中的每一个条目都渲染出一个li标签,并声明一个本地变量name来持有当前迭代的条目。然后,这个新变量将被插值到Hello {{ name }}代码片段里。

并不是必须把这个引用变量命名为name。我们也可以这样写:
<li *ngFor="let foobar of names">Hello {{ foobar }}</li>
但把顺序反过来行吗?来个小测验吧!如果写成下面这样会如何?
<li *ngFor="let name of foobar">Hello {{ name }}</li>
当然会出错!因为foobar并不是该组件上的属性。
NgFor会重复渲染ngFor所在的元素。也就是说,我们应该把它放到li标签上而不是ul标签上,因为我们希望重复的是列表元素(li)而非列表本身(ul)。
如果你想进一步探索,可以直接阅读Angular源代码来学习Angular核心团队是如何编写组件的。比如,你能在https://github.com/angular/angular/blob/master/modules/%40angular/common/src/directives/ng_for.ts找到NgFor指令的源代码。
现在刷新页面,就会看到此数组中的每个字符串都有了对应的li,如图1-6所示。

图1-6 带有数据的应用
还记得以前我们创建过UserItemComponent吗?这次不会在UserListComponent中直接渲染每个名字了,而是改用UserItemComponent作为子组件。也就是说,我们不再直接重复渲染li标签,而是让UserItemComponent来为列表中的每个条目指定模板(和功能)。
我们需要做三件事来实现这一点。
(1)配置UserListComponent来(在它的模板中)渲染UserItemComponent。
(2)配置UserItemComponent来接收name变量作为输入。
(3)配置UserListComponent的模板来把用户名传给UserItemComponent。
让我们来逐一完成。
1.6.1 渲染UserItemComponent
UserItemComponent指定了选择器app-user-item,接下来要把这个标签添加到模板中。我们要做的就是把li标签替换为app-user-item标签。
code/first_app/angular2_hello_world/src/app/user-list/user-list.component.html
<ul>
<app-user-item
*ngFor="let name of names">
</app-user-item>
</ul>
注意,当把li标签替换为app-user-item时,我们保留了ngFor属性。这是因为我们仍然要在用户名列表上进行循环。
注意,我们还移除了该模板内部的内容,因为UserItemComponent组件有自己的模板。如果刷新浏览器,看到的结果如图1-7所示。
它确实重复了,但有些不大对劲——每个用户名都是Felipe!我们需要某种方式来把数据传给子组件。
谢天谢地,Angular为此提供了一种方式:@Input注解。

图1-7 带有数据的应用
1.6.2 接收输入
还记得吗?UserItemComponent已经在其构造函数中设置了this.name = 'Felipe';。现在,我们需要进行一些改动,让组件的name属性从外部接收值。
这里要把UserItemComponent修改为:
code/first_app/angular2_hello_world/src/app/user-item/user-item.component.ts
import {
Component,
OnInit,
Input // <—— added this
} from '@angular/core';
@Component({
selector:'app-user-item',
templateUrl:'./user-item.component.html',
styleUrls:['./user-item.component.css']
})
export class UserItemComponent implements OnInit {
@Input()name:string; // <—— added Input annotation
constructor(){
// removed setting name
}
ngOnInit(){
}
}
注意,我们修改了name属性,使其具有一个@Input注解。在第3章中,我们会讨论更多关于Input(和Output)的知识,但目前你只要知道该语法能让我们从父模板中传进来一个值就可以了。
为了使用Input,我们还得把它添加到import的列表中去。
最后,我们不希望为name设置默认值,因此从构造函数中移除它。
现在我们有了一个名叫name的Input,那么该如何使用它呢?
1.6.3 传入Input值
为了把一个值传入组件,就要在模板中使用方括号[]语法。来看看修改过的模板。
code/first_app/angular2_hello_world/src/app/user-list/user-list.component.html
<ul>
<app-user-item
*ngFor="let name of names"
[name]="name">
</app-user-item>
</ul>
注意,我们在app-user-item标签上添加了新属性[name]="name"。在Angular中,添加一个带方括号的属性(比如[foo])意味着把一个值传给该组件上同名的输入属性(比如foo)。
在这个例子中,name右侧的值来自ngFor中的let name ……语句。也就是说,对于下列代码:
<app-user-item
*ngFor="let individualUserName of names"
[name]="individualUserName">
</app-user-item>
[name]部分指定的是UserItemComponent上的Input。注意,我们正在传入的并不是字符串字面量"individualUserName",而是individualUserName变量的值,也就是names中的每个元素。
在第3章中,我们会详细讲解输入属性和输出属性。现在,你所要知道的是:
(1)在names中迭代;
(2)为names中的每个元素创建一个新的UserItemComponent;
(3)把当前名字的值传给UserItemComponent上名叫name的Input属性。
现在,渲染名字列表的工作就完成了(如图1-8所示)!
恭喜!你已经用组件构建出了你的第一个Angular应用。
当然,该应用非常简单,你应该还希望构建更复杂的应用。别急,在本书中,我们将带你成为编写Angular应用的专家。事实上,我们在本章中还会构建一个投票应用(就像Reddit或Product Hunt)。该应用具有用户交互特性以及更多的组件。

图1-8 应用中的名字一切正常
在开始构建新的应用之前,先来仔细看看Angular应用是如何启动的。
每个应用都有一个主入口点。该应用是由angular-cli构建的,而angular-cli则是基于一个名叫webpack的工具。你不必理解webpack就能使用Angular,但理解应用的启动流程是很有帮助的。
我们可以通过运行下列命令来启动应用:
ng serve
ng会查阅angular-cli.json文件来找出该应用的入口点。我们来跟踪一下ng是如何找到我们刚刚构建的组件的。
大体来说,过程如下所示:
●angular-cli.json指定一个"main"文件,这里是main.ts;
●main.ts是应用的入口点,并且会引导(bootstrap)我们的应用;
●引导过程会引导一个Angular模块——我们尚未讨论过模块,不过很快就会谈到;
●我们使用AppModule来引导该应用,它是在src/app/app.module.ts中指定的;
●AppModule指定了将哪个组件用作顶层组件,这里是AppComponent;
●AppComponent的模板中有一个<app-user-list>标签,它会渲染出我们的用户列表。
我们将在稍后深入讨论这个过程,现在把目光聚焦在Angular的模块系统上:NgModule。
Angular有一个强大的概念:模块。当引导一个Angular应用时,并不是直接引导一个组件,而是创建了一个NgModule,它指向了你要加载的组件。
我们来看看代码。
code/first_app/angular2_hello_world/src/app/app.module.ts
@NgModule({
declarations:[
AppComponent,
HelloWorldComponent,
UserItemComponent,
UserListComponent
],
imports:[
BrowserModule,
FormsModule,
HttpModule
],
providers:[],
bootstrap:[AppComponent]
})
export class AppModule { }
我们首先看到的是@NgModule注解。像所有注解一样,这段@NgModule(……)代码为紧随其后的AppModule类添加了元数据。
@NgModule注解有三个属性:declarations、imports和bootstrap。
declarations指定了在该模块中定义的组件。你可能已经注意到了,当我们使用ng generate时,它会自动把生成的组件添加到这个列表里!这涉及Angular中的一个重要思想:
要想在模板中使用一个组件,你必须首先在NgModule中声明它。
imports描述了该模块有哪些依赖。我们正在创建一个浏览器应用,因此要导入BrowserModule。
bootstrap告诉Angular,当使用该模块引导应用时,我们要把AppComponent加载为顶层组件。
我们将在8.10节中深入讨论NgModule。
现在我们学会了如何创建一个基本的应用,下面就开始仿造一个Reddit吧。在开始编程之前,你最好先对此应用进行概览并把它拆解为一些逻辑组件,如图1-9所示。

图1-9 应用的逻辑组件
我们将在这个应用程序中构造两个组件:
(1)整体应用程序,包含一个用来提交新文章的表单(在图1-9中标示为深灰色方框);
(2)每个文章(在图1-9中标示为浅灰色方框)。
在较大的应用程序中,用来提交文章的表单本身也应该设计成单独的组件,但是这会让数据传送变得更加复杂。因此为了简化,在本章中我们只使用两个组件。我们目前只创建两个组件,但在本书后面的章节中,我们将学习如何处理更复杂的数据架构。
首先,像以前一样运行ng new命令,并传入一个想要的名字来生成新的应用(这里我们将创建一个名叫angular2_reddit的应用):
ng new angular2_reddit
我们在可下载的示例代码中提供了angular2_reddit的完整版。
1.8.1 添加CSS
我们要做的第一件事是添加一些CSS样式,来让应用不再完全“素颜”。

如果你正在从头构建应用,可以从完成版示例代码的first_app/angular2_reddit目录下复制一些文件过来。
复制以下文件到你的应用目录下:
●src/index.html
●src/styles.css
●src/app/vendor
●src/assets/images
在本项目中,我们将使用Semantic-UI
来帮助添加样式。Semantic-UI是一个CSS框架,类似于Zurb Foundation
或Twitter Bootstrap
。我们的示例代码中已经包含了它,所以你只需要复制上面指定的文件即可。
1.8.2 应用程序组件
现在来构建一个新的组件,它将:
(1)存储我们的当前文章列表;
(2)包含一个表单,用来提交新的文章。
我们可以在src/app/app.component.ts文件中找到主应用组件。打开它,可以看到与以前一样的初始内容。
code/first_app/angular2_reddit/src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector:'app-root',
templateUrl:'./app.component.html',
styleUrls:['./app.component.css']
})
export class AppComponent {
title = 'app works!';
}
我们对此模板稍作修改,使其包含一个表单,用于添加链接。我们将从semantic-ui包中借用一点样式来让这个表单看起来更漂亮一些。
code/first_app/angular2_reddit/src/app/app.component.html
<form class="ui large form segment">
<h3 class="ui header">Add a Link</h3>
<div class="field">
<label for="title">Title:</label>
<input name="title">
</div>
<div class="field">
<label for="link">Link:</label>
<input name="link">
</div>
</form>
我们要创建一个template,它定义了两个input标签:一个用于文章的标题(title),另一个用于文章的链接(link URL)。
刷新浏览器后,你就会看到渲染出了如图1-10所示的表单。

图1-10 表单
1.8.3 添加互动
现在我们有了带input标签的表单,但还没有任何方式来提交数据。下面在表单中添加一个提交按钮,来添加一些交互。
当提交该表单时,我们希望调用一个函数来创建并添加一个链接。可以往<button />元素上添加一个交互事件来实现这个功能。
把事件的名字包裹在圆括号()中就可以告诉Angular:我们要响应这个事件。比如,要想添加一个函数来响应<button />的onClick事件,可以像这样把它传进去:
<button(click)="addArticle()"
class="ui positive right floated button">
Submit link
</button>
这样,当点击这个按钮时,就会调用一个名叫addArticle()的函数;我们要在AppComponent类中定义这个函数。代码如下所示。
code/first_app/angular2_reddit/src/app/app.component.ts
export class AppComponent {
addArticle(title:HTMLInputElement, link:HTMLInputElement):boolean {
console.log(`Adding article title:${title.value} and link:${link.value}`);
return false;
}
}
一旦把addArticle()函数添加到AppComponent中并且把(click)事件处理器添加到<button />元素上,那么每当点击此按钮时,就会调用该函数。注意,addArticle()函数可以接收两个参数:title和link。我们还要修改模板来把它们传给addArticle()。
我们可以通过为表单中的input元素添加一个特殊的语法来取得模板变量。修改后的模板如下所示。
code/first_app/angular2_reddit/src/app/app.component.html
<form class="ui large form segment">
<h3 class="ui header">Add a Link</h3>
<div class="field">
<label for="title">Title:</label>
<input name="title" #newtitle> <!—— changed ——>
</div>
<div class="field">
<label for="link">Link:</label>
<input name="link" #newlink> <!—— changed ——>
</div>
<!—— added this button ——>
<button(click)="addArticle(newtitle, newlink)"
class="ui positive right floated button">
Submit link
</button>
</form>
注意,我们在input标签上使用了#(hash)来要求Angular把该元素赋值给一个局部变量。通过把#title和#link添加到适当的<input />元素上,就可以把它们作为变量传给按钮上的addArticle()函数!
总结一下,我们一共进行了四项修改:
(1)在模版中创建了一个button标签,向用户表明应该点击哪里;
(2)新建了一个名叫addArticle的函数,来定义按钮被点击时要做的事情;
(3)在button上添加了一个(click)属性,意思是“只要点击了这个按钮,就运行addArticle函数”;
(4)在两个<input>标签上分别添加了#newtitle和#newlink属性。
下面我们按照倒序讲解每一步。
●绑定input的值
注意,第一个输入标签是这样的:
<input name="title" #newtitle>
这段标记告诉Angular把这个<input>绑定到变量newtitle上。#newtitle语法被称作一个解析(resolve),其效果是让变量newtitle可用于该视图的所有表达式中。
newtitle现在是一个对象,它代表了这个input DOM元素(更确切地说,它的类型是HTMLInputElement)。由于newtitle是一个对象,我们可以通过newtitle.value表达式来获取这个输入框的值。
同样,我们把#newlink添加到了另一个<input>标签上,因此也可以用它来提取这个输入框的值。
●把事件绑定到动作
我们在button标签上添加了属性(click)来定义点击此按钮时应该怎么做。当发生(click)事件时,我们会调用addArticle并传入两个参数:newtitle和newlink。这个函数和这两个参数是从哪里来的?
(1)addArticle是组件定义类AppComponent里的一个函数。
(2)newtitle来自名叫title的<input>标签上的解析(#newtitle)。
(3)newlink来自名叫link的<input>标签上的解析(#newlink)。
全部合并起来是这样的:
<button(click)="addArticle(newtitle, newlink)"
class="ui positive right floated button">
Submit link
</button>
class="ui positive right floated button"标签来自Semantic
UI,它为这个按钮提供了赏心悦目的绿色。
●定义操作逻辑
在class AppComponent中,我们定义了一个名叫addArticle的新函数。它接收两个参数:title和link。要注意,title和link都是HTMLInputElement类型的对象,而并非直接输入的值;这一点很重要。要从input中获取值,就得调用title.value。目前,我们通过console.log来输出这些参数。
code/first_app/angular2_reddit/src/app/app.component.ts
addArticle(title:HTMLInputElement, link:HTMLInputElement):boolean {
console.log(`Adding article title:${title.value} and link:${link.value}`);
return false;
}

注意,我们又在使用反引号字符串了。这是ES6中非常便利的一个特性:反引号字符串会展开模板变量!
这里,我们把${title.value}放在了字符串中,它最终会被替换成title.value的值。
●试试看
现在,当你点击提交按钮时,就能看到这条消息被打印到控制台中了(如图1-11所示)。

图1-11 点击按钮
1.8.4 添加文章组件
现在,我们有了一个用来提交新文章的表单,但还没有在任何地方展示这些新文章。因为每篇新提交的文章都要显示在本页面的列表中,现在要新建一个组件。
下面就来新建一个组件,用来单独展示这些提交过的文章(如图1-12所示)。

图1-12 一篇文章
为此,我们借助ng工具生成一个新组件:
ng generate component article
定义这个新组件总共用到了三部分代码:
(1)在模板中定义了ArticleComponent的视图;
(2)通过为类加上@Component注解定义了ArticleComponent组件的元数据;
(3)定义了一个组件定义类(ArticleComponent),其中是组件本身的逻辑。
下面来深入讲解一下各部分的细节。
●创建ArticleComponent的template
我们使用文件article.component.html定义模板。
code/first_app/angular2_reddit/src/app/article/article.component.html
<div class="four wide column center aligned votes">
<div class="ui statistic">
<div class="value">
{{ votes }}
</div>
<div class="label">
Points
</div>
</div>
</div>
<div class="twelve wide column">
<a class="ui large header" href="{{ link }}">
{{ title }}
</a>
<ul class="ui big horizontal list voters">
<li class="item">
<a href(click)="voteUp()">
<i class="arrow up icon"></i>
upvote
</a>
</li>
<li class="item">
<a href(click)="voteDown()">
<i class="arrow down icon"></i>
downvote
</a>
</li>
</ul>
</div>
这里有很多页面脚本,我们来分解一下(如图1-13所示)。

图1-13 单行文章
我们有两列:
(1)左侧是投票的数量;
(2)右侧是文章的信息。
我们分别用four wide column和twelve wide column这两个CSS类来指定这两列。(记住,它们来自Semantic UI的CSS库。)
我们用模板展开字符串{{ votes }}和{{ title }}来展示votes和title。这些值来自ArticleComponent类中的votes和title属性,我们很快就会进行定义。
注意,我们可以在属性值中使用模板字符串,比如在a标签的href属性中:href="{{ link }}"。在这种情况下,href的值会根据组件类的link属性的值进行动态插值计算得出。
在upvote和downvote的链接上,我们还有一个动作。只要分别将其按钮上的(click)绑定到voteUp()和voteDown()就可以了。当upvote按钮被按下时,ArticleComponent类上的voteUp()函数就会被调用;当downvote按钮被按下时,voteDown()函数会被调用。
●创建ArticleComponent
接下来创建ArticleComponent。
code/first_app/angular2_reddit/src/app/article/article.component.ts
@Component({
selector:'app-article',
templateUrl:'./article.component.html',
styleUrls:['./article.component.css'],
host:{
class:'row'
}
})
首先,我们用@Component定义了一个新组件。selector表示会用<app-article>标签将该组件放在页面中(也就是说,该选择器是一个标签名)。
因此,该组件最基本的使用方式就是把下列标签放在我们的页面脚本中:
<app-article>
</app-article>
当页面被渲染出来时,这些标签仍然会留在视图中。
我们希望每个app-article都独占一行。我们使用的是Semantic UI,它提供了一个用来表示行的CSS类
,叫作row。
在Angular中,组件的宿主就是该组件所附着到的元素。你会注意到,我们在@Component中传入了一个选项:host:{ class:'row' }。它告诉Angular:我们要在宿主元素(app-article标签)上设置class属性,使其具有row类。
这个host选项很不错,它意味着我们可以把app-article的页面脚本封装在组件之内。也就是说,我们不必在使用app-article标签的同时要求父视图中的页面脚本具有class="row"属性。借助host选项,我们就可以在组件的内部配置宿主元素了。
●创建组件定义类ArticleComponent
最后,我们来创建组件定义类ArticleComponent。
code/first_app/angular2_reddit/src/app/article/article.component.ts
export class ArticleComponent implements OnInit {
votes:number;
title:string;
link:string;
constructor(){
this.title = 'Angular 2';
this.link = 'http://angular.io';
this.votes = 10;
}
voteUp(){
this.votes += 1;
}
voteDown(){
this.votes -= 1;
}
ngOnInit(){
}
}
此处我们在ArticleComponent上创建了以下三个属性。
(1)votes:一个数字,用来表示所有“赞”减去所有“踩”的数量之和。
(2)title:一个字符串,用来存放文章的标题。
(3)link:一个字符串,用来存放文章的URL。
在constructor()中,我们设置了一些默认属性。
code/first_app/angular2_reddit/src/app/article/article.component.ts
constructor(){
this.title = 'Angular 2';
this.link = 'http://angular.io';
this.votes = 10;
}
我们还为投票定义了两个函数,一个用来“赞”的voteUp和一个用来“踩”的voteDown。
code/first_app/angular2_reddit/src/app/article/article.component.ts
voteUp(){
this.votes += 1;
}
voteDown(){
this.votes -= 1;
}
在voteUp中,我们会把this.votes加一;而在voteDown中,则会把this.votes减一。
●使用app-article组件
为了用该组件呈现数据,我们要把<app-article></app-article>标签添加到页面脚本中的某个地方。
这个例子中,我们希望让AppComponent组件来渲染这个新组件。因此修改AppComponent的代码,把<app-article>标签添加到AppComponent的模板中,紧跟在</form>标签后面:
<button(click)="addArticle(newtitle, newlink)"
class="ui positive right floated button">
Submit link
</button>
</form>
<div class="ui grid posts">
<app-article>
</app-article>
</div>
如果现在刷新浏览器,就会看到<app-article>标签并没有被编译。啊?怎么回事?
无论什么时候遇到这种问题,首先要做的就是打开浏览器的开发者控制台。只要审查一下页面脚本(如图1-14所示),就会看到app-article标签已经出现在页面上了,但是并没有被编译。这是为什么呢?

图1-14 审查DOM时未能展开的标记
之所以出现这种情况,是因为AppComponent组件目前还不知道这个ArticleComponent**组件**。

AngularJS用户注意:如果你用过AngularJS,可能会惊讶于本应用不知道这个新的app-article组件。这是因为在AngularJS中,指令的匹配是全局的;而Angular中,你需要明确指定要使用哪个组件(即哪个选择器)。
一方面,这需要一点配置;但另一方面,这对于构建可伸缩的应用是非常有帮助的,因为这意味着我们不必被迫在全局命名空间中共享这些指令选择器。
为了把这个新的ArticleComponent组件引荐给AppComponent,我们需要把ArticleComponent添加到NgModule的declarations列表中。

之所以要把ArticleComponent添加到declarations中,是因为ArticleComponent是该模块(RedditAppModule)的一部分。然而,如果ArticleComponent是其他模块的一部分,可能就得通过imports来导入它了。
后面还会更深入地讨论NgModule,现在你只需要知道:当创建新组件时,必须同时把它放进NgModule的declarations中。
code/first_app/angular2_reddit/src/app/app.module.ts
import { AppComponent } from './app.component';
import { ArticleComponent } from './article/article.component.ts';
@NgModule({
declarations:[
AppComponent,
ArticleComponent // <—— added this
],
我们在这里:
(1)用import导入ArticleComponent;
(2)把ArticleComponent添加到declarations列表中。
把ArticleComponent添加到NgModule的declarations中之后,如果刷新浏览器,就会看到该文章正确渲染出来了(如图1-15所示)。

图1-15 渲染ArticleComponent组件
不过,如果你尝试点击“赞”或“踩”的链接,就会看到该页面发生了预料之外的刷新。
在默认情况下,JavaScript会把click事件冒泡到所有父级组件中。因为click事件被冒泡到了父级,浏览器就会尝试导航到这个空白链接,于是浏览器就重新刷新了。
要解决这个问题,我们得让click的事件处理器返回false。这能确保浏览器不会尝试刷新页面。我们要修改代码,以便让每一个voteUp()和voteDown()函数都返回一个布尔值false(告诉浏览器不要向上冒泡):
voteDown():boolean {
this.votes -= 1;
return false;
}
// and similarly with `voteUp()`
现在,如果你点击这些链接,就会看到投票数正确地增加或减少了,而且没有出现多余的页面刷新。
目前,在页面上只有一篇文章,而且也没法渲染更多了,除非我们复制一个<app-article>标签。但即使这样做,所有的文章也都会具有相同的内容,这可不是我们想要的。
1.9.1 创建Article类
写Angular代码时的最佳实践之一就是尝试从组件代码中把你正在使用的数据结构隔离出来。要做到这一点,就要创建一个数据结构,用以表示单个文章。下面就创建一个新文件article.model.ts来定义所需的Article类吧。
code/first_app/angular2_reddit/src/app/article/article.model.ts
export class Article {
title:string;
link:string;
votes:number;
constructor(title:string, link:string, votes?:number){
this.title = title;
this.link = link;
this.votes = votes || 0;
}
}
此处,我们创建了一个新类,用来表示Article。注意,这是一个普通类而不是Angular组件。在MVC模式中,它被称为模型(model)。
每篇文章都有一个标题title、一个链接link和一个投票总数votes。当创建新文章时,我们需要title和link。votes参数是可选的(用末尾的?标出来),并且默认为0。
现在,我们来修改ArticleComponent的代码,让它使用新的Article类。以前是直接把这些属性存到ArticleComponent组件上,现在则把它改为存到Article类的一个实例上。
code/first_app/angular2_reddit/src/app/article/article.component.ts
export class ArticleComponent implements OnInit {
article:Article;
constructor(){
this.article = new Article(
'Angular 2',
'http://angular.io',
10);
}
voteUp():boolean {
this.article.votes += 1;
return false;
voteDown():boolean {
this.article.votes -= 1
return false;
}
ngOnInit(){
}
}
注意我们改动了什么:以前我们直接把title、link和votes属性存到该组件上,而现在则存储一个对article的引用。把article变量的类型设置成新的Article类,代码变整洁了。
接下来修改voteUp(以及voteDown)时,我们不再递增组件上的votes了,而是需要递增article上的votes。
这次重构还引入了另一项修改:我们需要修改视图代码,从正确的位置获取模板变量。这样我们就要修改模板中的标签,使其从article中读取。也就是说,我们以前用的是{{ votes }},而现在要改成{{ article.votes }}。
code/first_app/angular2_reddit/src/app/article/article.component.html
<div class="four wide column center aligned votes">
<div class="ui statistic">
<div class="value">
{{ article.votes }}
</div>
<div class="label">
Points
</div>
</div>
</div>
<div class="twelve wide column">
<a class="ui large header" href="{{ article.link }}">
{{ article.title }}
</a>
<ul class="ui big horizontal list voters">
<li class="item">
<a href(click)="voteUp()">
<i class="arrow up icon"></i>
upvote
</a>
</li>
<li class="item">
<a href(click)="voteDown()">
<i class="arrow down icon"></i>
downvote
</a>
</li>
</ul>
</div>
刷新浏览器,仍然一切正常。
情况好多了,但还是有些代码不尽如人意:voteUp和voteDown方法打破了Article类的封装,因为它们直接修改了文章的内部属性。
当前的voteUp和voteDown违反了迪米特法则
。迪米特法则是指:一个对象对其他对象的结构或属性所作的假设应该越少越好。
问题在于ArticleComponent组件了解太多Article类的内部知识了。要解决这一点,就要为Article类添加voteUp和voteDown方法。
code/first_app/angular2_reddit/src/app/article/article.model.ts
export class Article {
title:string;
link:string;
votes:number;
constructor(title:string, link:string, votes?:number){
this.title = title;
this.link = link;
this.votes = votes || 0;
}
voteUp():void {
this.votes += 1;
}
voteDown():void {
this.votes -= 1;
}
domain():string {
try {
const link:string = this.link.split('//')[1];
return link.split('/')[0];
} catch(err){
return null;
}
}
}
然后可以修改ArticleComponent组件来调用这些方法。
code/first_app/angular2_reddit/src/app/article/article.component.ts
export class ArticleComponent implements OnInit {
article:Article;
constructor(){
this.article = new Article(
'Angular',
'http://angular.io',
10);
}
voteUp():boolean {
this.article.voteUp();
return false;
}
voteDown():boolean {
this.article.voteDown();
return false;
}
ngOnInit(){
}
}

为什么模型和组件中都有一个voteUp函数?
原因在于,这两个函数所做的事情略有不同。ArticleComponent上的voteUp()函数是与组件的视图有关的,而Article模型上的voteUp()定义了模型上的变化。
也就是说,当投票时,Article类可以对模型上的相应功能进行封装。在真实的应用中,Article模型的内部可能更加复杂,比如向Web服务器发起一个API调用,而你显然不希望这些本属于模型的代码出现在组件的控制器中。
同样,在ArticleComponent中,我们return false;从而“阻止事件冒泡”。这是属于视图的逻辑片段,我们不希望Article模型上的voteUp()函数懂得这些与视图有关的API。也就是说,Article模型应该让投票逻辑从特定的视图中分离出来。
在刷新浏览器之后,仍然一切正常,但我们已经有了更加清晰、更加简单的代码。
查看现在的ArticleComponent组件定义会发现:它太短了!我们把大量逻辑移出组件,放进了模型中。与此对应的MVC指南应该是“胖模型、皮包骨的控制器”
;其核心思想是,我们要把大部分领域逻辑移到模型中,以便让组件只做尽可能少的工作。
1.9.2 存储多篇文章
我们再写点代码,展示有多个Article的列表。
从让AppComponent拥有一份文章集合开始。
code/first_app/angular2_reddit/src/app/app.component.ts
export class AppComponent {
articles:Article[];
constructor(){
this.articles = [
new Article('Angular 2', 'http://angular.io', 3),
new Article('Fullstack', 'http://fullstack.io', 2),
new Article('Angular Homepage', 'http://angular.io', 1),
];
}
addArticle(title:HTMLInputElement, link:HTMLInputElement):boolean {
console.log(`Adding article title:${title.value} and link:${link.value}`);
this.articles.push(new Article(title.value, link.value, 0));
title.value = '';
link.value = '';
return false;
}
}
注意我们的AppComponent中多了这一行:
articles:Article[];
Article[]看起来可能有点陌生。这里的意思是articles是Article的数组。另一种写法是Array<Article>。这种模式被称为泛型。Java、C#和一些别的语言中都有这个概念,意思是你的集合(Array)是有类型的。也就是说,Array是一个集合,它只能存放Article类型的对象。
我们通过在构造函数中设置this.articles来初始化这个数组。
code/first_app/angular2_reddit/src/app/app.component.ts
constructor(){
this.articles = [
new Article('Angular 2', 'http://angular.io', 3),
new Article('Fullstack', 'http://fullstack.io', 2),
new Article('Angular Homepage', 'http://angular.io', 1),
];
}
1.9.3 使用inputs配置ArticleComponent
现在,我们已经有了一个Article模型的列表,该怎么把它们传给ArticleComponent组件呢?
这里我们又用到了Input。以前ArticleComponent类的定义是下面这样的。
code/first_app/angular2_reddit/src/app/article/article.component.ts
export class ArticleComponent implements OnInit {
article:Article;
constructor(){
this.article = new Article(
'Angular 2',
'http://angular.io',
10);
}
问题的关键是,我们在构造函数中硬编码了一个特定的Article;而制作组件时,不但要能封装,还要能复用。
我们真正想做的是配置要显示的Article。比如,假设我们有article1和article2两篇文章,那就要支持把一个Article型的“参数”传给组件来复用app-article组件,就像这样:
<app-article [article]="article1"></app-article>
<app-article [article]="article2"></app-article>
Angular通过Component上的Input注解来支持我们这样做:
class ArticleComponent {
@Input()article:Article;
// ……
现在,如果我们有一个Article型的变量myArticle,就可以把它传给视图中的ArticleComponent了。记住,可以用方括号包裹一个变量来把它传给元素,就像这样:
<app-article [article]="myArticle"></app-article>
注意这里的语法:我们把输入属性的名字放入方括号中([article]),而该属性的值就是我们要传给此输入属性的那个。
接下来,重点是ArticleComponent实例上的this.article将被设置成myArticle。我们可以把这个过程看作将myArticle变量作为一个参数传给(也就是输入给)了我们的组件。
ArticleComponent组件使用@Input之后变成了下面这样。
code/first_app/angular2_reddit/src/app/article/article.component.ts
export class ArticleComponent implements OnInit {
@Input()article:Article;
voteUp():boolean {
this.article.voteUp();
return false;
}
voteDown():boolean {
this.article.voteDown();
return false;
}
ngOnInit(){
}
}
1.9.4 渲染文章列表
我们之前配置过AppComponent来存储articles数组。这次我们要配置AppComponent来渲染所有articles。要实现这个功能,就不能单独使用<app-article>标签了,而要用NgFor指令在articles数组上进行迭代,并为其中的每一个都渲染一份app-article。
把下列内容添加到AppComponent前面@Component注解的template属性中,紧跟着</form>标签:
Submit link
</button>
</form>
<!—— start adding here ——>
<div class="ui grid posts">
<app-article
*ngFor="let article of articles"
[article]="article">
</app-article>
</div>
<!—— end adding here ——>
还记得我们之前用过NgFor指令把名称列表渲染成无序列表吗?它在渲染多个组件时也同样适用。
*ngFor="let article of articles"语法会对articles列表进行迭代,并且为列表中的每一个条目创建一个局部变量article。
要为组件指定一个输入属性article,就要使用[inputName]="inputValue"表达式。在这个例子中,该表达式的意思是:我们要把输入属性article设置为局部变量article的值,而后者是由ngFor所设置的。

article变量在这个代码片段中出现的次数太多了。如果我们把NgFor创建的临时变量命名为foobar,或许更清楚一些:
<app-article
*ngFor="let foobar of articles"
[article]="foobar">
</app-article>
那么,这里就有了三个变量:
(1)articles是一个Article的数组,由AppComponent组件定义;
(2)foobar是一个articles数组中的单个元素(一个Article对象),由NgFor定义;
(3)article是一个字段名,由ArticleComponent中的inputs属性定义。
本质上,NgFor首先生成了一个临时变量foobar,然后我们把它传给了apparticle。
刷新浏览器,就会看到所有的文章都渲染出来了(如图1-16所示)。

图1-16 渲染多篇文章
现在,我们需要修改addArticle以便在按下按钮时实际添加一篇新文章。修改addArticle方法,使其变成下面这样。
code/first_app/angular2_reddit/src/app/app.component.ts
addArticle(title:HTMLInputElement, link:HTMLInputElement):boolean {
console.log(`Adding article title:${title.value} and link:${link.value}`);
this.articles.push(new Article(title.value, link.value, 0));
title.value = '';
link.value = '';
return false;
}
这将会:
(1)创建一个具有所提交标题和URL的Article新实例;
(2)把它加入Article数组;
(3)清除input字段的值。
我们要如何清除input字段的值呢?回忆一下,title和link都是HTMLInputElement对象。这就意味着我们可以设置它们的属性。当我们修改value属性时,页面中的input标签也会跟着改变。
在输入框中添加新文章,并点击Submit Link之后,就会看到新的文章添加成功了!
1.11.1 显示文章所属的域名
我们先为链接添加一个提示信息,以便在用户点击链接时显示将重定向到的域名。
把domain方法添加到Article类中。
code/first_app/angular2_reddit/src/app/article/article.model.ts
domain():string {
try {
const link:string = this.link.split('//')[1];
return link.split('/')[0];
} catch(err){
return null;
}
}
把对该函数的调用添加到ArticleComponent的模板中:
<div class="twelve wide column">
<a class="ui large header" href="{{ article.link }}">
{{ article.title }}
</a>
<!—— right here ——>
<div class="meta">({{ article.domain()}})</div>
<ul class="ui big horizontal list voters">
<li class="item">
<a href(click)="voteUp()">
现在,当我们刷新浏览器时,就能看到每个URL所属的域名了(注意:URL必须包含http://)。
1.11.2 基于分数重新排序
如果你点击并投票,就会发现有些事情不太对劲:这些文章并没有基于分数排序!显然,我们更希望让分数最高的条目显示在顶部,让低分条目沉到底部。
我们把articles存储在了AppComponent类中,但这个数组是无序的。处理这种情况的简单方式是在AppComponent上创建一个新方法sortedArticles。
code/first_app/angular2_reddit/src/app/app.component.ts
sortedArticles():Article[] {
return this.articles.sort((a:Article, b:Article)=> b.votes - a.votes);
}
这样,在ngFor中,我们就可以在sortedArticles()上而不是直接在articles上迭代了:
<div class="ui grid posts">
<app-article
*ngFor="let article of sortedArticles()"
[article]="article">
</app-article>
</div>
在本章中,我们浏览了代码中的很多小片段。你可以到本书示例代码的下载站点找到该应用的全部文件和完整的TypeScript代码。
完工!我们已经创建了自己的第一个Angular应用。还不错,对吧?不过我们还会学到更多:理解数据流、发起AJAX请求、内置指令、路由、操纵DOM,等等。
现在,好好享受成功的喜悦吧!很多Angular程序的写法都和我们刚刚所做的类似:
(1)把应用拆分成组件;
(2)创建视图;
(3)定义模型;
(4)显示模型;
(5)添加交互。
在后面的章节中,我们将讲解用Angular编写各种复杂应用的全部知识。
如果你有关于本章的任何问题,比如发现了bug或在运行代码时遇到问题,欢迎告诉我们!
●(英文)加入我们的免费社区,在Gitter上跟我们聊聊:https://gitter.im/ng-book/ng-book。
●(英文)直接给我们发送邮件:us@fullstack.io。
●(中文)如果是与中文版相关的问题与勘误,请访问我们的GitHub:https://github.com/ng-book2/book。
●(中文)获取官方文档中文版,请访问angular.cn。
●(中文)如果想了解本书范围之外的问题,请访问wx.angular.cn向我们提问。
●(中文)要了解Angular的最新消息,欢迎搜索并关注微信公众号:Angular中文社区。
继续前进吧!