Web开发编程网
分享Web开发相关技术

编写你的第一个 Angular2 Web 应用

简单的Reddit克隆

在本章中,我们要构建一个应用程序,允许用户发布了一篇文章(带有标题和URL)并且可以给文章投票

你可以认为这个应用是一个站点的初期,像RedditProduct Hunt

在这个简单的应用中,我们将一起涉及到Angular 2大部分内容。 包括:

  • 构建自定义组件
  • 从表单中接受用户输入
  • 将对象列表渲染到视图
  • 拦截用户点击并处理他们

当你完成这一章你会掌握如何构建基本的Angular 2应用程序。

我们的应用将会和下面的截图看起来差不多

image

首先,用户提交新的链接后,用户将能够对每篇内容进行upvote和downvote。每一个链接都会有一个分数,可以投票给我们发现有用的链接。

image

在这个项目和整本书中,我们使用TypeScript来编写,TypeScript是JavaScript的ES6的超集,增加了数据类型。在本章中我们不会深度讨论TypeScript,但如果你熟悉ES5/ES6,那应该没有任何问题。

我们会在下一章深度了解TypeScript。如果你遇到了一些新的语法无需太担心。

快速入门

TypeScript

开始使用TypeScript前,你需要先安装Node.js。有很多不同的方法安装Node.js,请参阅Node.js的网站nodejs.org/download

我必须用TypeScript吗?不,你可以不使用TypeScript来编写Angular 2,ng2有ES5 API,但是通常每个人都会使用TypeScript来编写anuglar2,
这本书中我们将使用Typescript,因为他编写Angular2更为简单。也就是说,它没有严格要求你必须使用TypeScript。

一旦你有了Node.js,下一步就是安装TypeScript。确保您安装的版本至少是1.7或更高版本。运行下面的命令,安装1.5版本:

$ npm install -g '[email protected]^1.7.3'

npm是node.js安装中的一部分,如果您在系统上没有npm,确保您使用Node.js的安装程序包含它。

window用户:在这本书我们在命令行中将使用Linux/Mac风格的命令,我们强烈建议您安装cygwin2,因为它会让你可以运行这本书中的非window命令。

示例项目

现在,你的环境已准备好了,让我们开始写第一个Angular2应用!

打开随这本书下载的代码并解压。在你的命令行中,通过cd进入first_app/angular2-reddit-base目录

cd first_app/angular2-reddit-base

如果你不熟悉cd命令,它表示“更改目录”。你可以在mac上进行以下尝试:
1. 打开 /Applications/Utilities/Terminal.app
2. 输入cd

3. 在mac的finder中,将first_app/angular2-reddit-base文件夹拖拽到命令行窗口中
4. 按下回车,你会切换到该目录下,你可以继续下一步操作了

首先让我们先使用npm安装所有依赖

$ npm install

在项目的根目录下创建一个新的index.html文件,并添加一些基本HTML结构:

  
     
   
   
  

你的angular2-reddit-base目录看上去应该是这样子的

 |-- README.md // A helpful readme
 |-- index.html // Your index file
 |-- index-full.html // Sample index file
 |-- node_modules/ // installed dependencies
 |-- package.json // npm configuration
 |-- resources/ // images etc.
 |-- styles.css // stylesheet
 |-- tsconfig.json // compiler configuration
 |-- tslint.json // code-style guidelines

Angular 2本身是一个JavaScript文件。所以我们需要一个script标签来引入它。并且还需引入一些Angular/TypeScript依赖的文件:

Angular的依赖

你并不需要为了使用Angular 2而严格地理解这些依赖,但你需要导入这些依赖,如果你对依赖并不感兴趣,请跳过本章节,但要确保你复制并粘贴这些脚本标签。

Angular 2依赖于这四个库:

  • es6-shim – (为了旧浏览器)
  • angular2-polyfills
  • SystemJS
  • RxJS

在你的<head>中添加这些标签


请注意,我们直接从node_modules目录中加载这些.js文件,node_modules目录会在运行npm install 时被创建,如果你没有node_modules目录,请确保你是在angular2-reddit-base目录下输入的npm install

ES6 Shim

ES Shim 为旧版的Javascript引擎提供了ECMAScript 6的行为,该Shim对于较新新版本的Safari,Chrome等并不严格需要,但是对于旧版本的IE是需要的。

什么是Shim? 也许你听说过shims和polyfills,但你不知道它们是什么。
Shim是代码,它有助于适应跨浏览器之间的一种标准化的行为。

例如,看看这个ES6兼容性表.不是每个浏览器的每一个功能都完全兼容。通过使用不同的shim,我们能够得到在不同浏览器(和环境)的标准的行为。

参见:shim和polyfill之间的区别是什么?

Angular 2 Polyfill

像ES6 Shim, angular2-polyfills提供跨浏览器的一些基本的标准化。

angular2-polyfills包含的代码专门用于zone,promise和reflection,如果你不知道这些东西是什么,你也不必担心。

SystemJS

SystemJS是一个模块加载器。它帮助我们创建模块和解决模块之间依赖,模块加载在浏览器端的JavaScript是出奇的复杂,SystemJS使得过程变得更加容易。

RxJS

RxJS是一个库用于在Javascript中进行反应式编程,一般来说,RxJS给了我们使用Observables的工具,用于发出的数据流。Angular 在许多地方使用了Observables,如在处理异步代码(例如: HTTP请求)

我们会在本书的RxJS这章讨论更多关于RxJS的内容,虽然在本章中它不是严格需要的,但值得一提的,你会在项目中经常使用它。

加载所有依赖

现在我们已经添加了所有的依赖,我们的index.html看起来应该是这样的

  
    
    
    
     
    
    
    
  
  
  

添加CSS

我们也想添加一些CSS样式,使我们的应用不是完全无样式。让我们导入两个样式表:

  
    
    
    
     
    
    
    
        
    
    
  
  
  

对于这个项目,我们将要使用Semantic-UI,Semantic-UI是一个CSS框架,类似于Foundation或Twitter Bootstrap,我们已经在示例代码中下载了因此你需要做的就是添加link标记。

我们的第一个Typescript

现在创建我们第一个TypeScript文件,在同级目录下添加一个叫app.ts的文件,并添加些代码:

注意 TypeScript文件的后缀为.ts而不是.js,这里的问题是,我们的浏览器并不知道怎么读取ts文件,所以后面我们需要将ts文件编译成js文件

code/first_app/hello-world/app.ts

import { bootstrap } from "angular2/platform/browser"; 
import { Component } from "angular2/core";
@Component({
  selector: 'hello-world',
  template: `
Hello world
`
})
class HelloWorld { }
bootstrap(HelloWorld);

这段代码可能看起来完全看不懂,但别担心。我们会一步步解释。

import语句定义了,在我们代码中需要用到的模块,在这里我们导入了2个模块,Component和Bootstrap.

我们从angular2/core模块中导入了Component模块,angular/core这部分告诉我们程序在哪里可以找到我们正在寻找的依赖。

同样我们从模块angular2/playform/browser中导入bootstrap模块

注意,这个import语句的结构格式是import { things } from wherever. { things } 这部分是只我们需要导入的模块,这个是一个ES6的特性,我们将在下一章讨论更多。
import的想法是类似于java或者ruby的require,我们只是将这些依赖模块提供给这个文件。

创建一个组件

组件是Angular 2其背后一个很大的想法之一。

在我们的Angular应用中编写HTML标签来成为我们的交互式应用程序、但是浏览器只认识那些内置的标签,如<select>、<form>或<video>等。但是,如果我们想教浏览器认识新的标签呢,如<weather>标签应该怎么显示天气,<login>标签应该如果处理登录等。

这背后就是组件的想法。我们教浏览器来认识新功能的新标签。

如果你有用过Angular 1,组件就是新版本的指令

来创建我们第一个组件.当我们编写完这个组件后,我们可以在HTML中这样使用它


那么,我们如何定义一个新的组件?一个基本组件有二个部分:

  • 一个Component注解
  • 一个定义组件的类

如果你已经有一段时间的JavaScript编程经验,当看到下面的JavaScript会有点奇怪:

@Component({
    //...
})

这里发生了什么事?如果你有Java背景,它看起来你会很熟悉:他们是注解。

注解会作为元数据添加到您的代码里。当我们在HelloWorld类上使用@Component时,我们会“decorating(装饰)” HelloWorld类成为一个组件

我们希望用<hello-world>标签来使用我们的组件。要做到点,我们将@Component配置中的selector属性设置为hello-world

@Component({
  selector: 'hello-world'
})

如果你熟悉CSS选择器,XPath,jQuery选择器等,你就知道有很多方法来配置一个selector。Angular 2 向selector添加了自己的特殊混合酱料,在以后,我们将会讨论。现在,只需要知道,在本例中,我们只定义了一个新的标签。

这里的selector属性表示对应的DOM元素将要被组件使用。这样,在模板中的<hello-world></hello-world>标签,会使用这个组件类进行编译。

添加模板

我们可以通过@Component中template选项来添加模板:

@Component({
  selector: 'hello-world',
  template:`
Hello World
  `
})

请注意,模板字符串定义在我们的反引号(`…`)之间。这是一个新的ES6的功能,可以让我们实现多行字符串。使用反引号里的多行字符串太棒了,使用它能更容易的来构建的模板代码。

我真的应该把模板放在我的代码文件里吗?答案是,这取决于你。长期以来普遍的理念是,你应该保持你的代码和模板分离。虽然这对一些团队来说可能更容易一些,但对于某些项目来说,它只是增加了很多开销。
当你需要在很多文件之间切换时,它会增加你的开发的开销。我个人而言,如果我的模板是小于一页,我更喜欢有模板和代码一起的。我可以看到逻辑和视图在一起,这很容易理解他们是如何相互作用的

把你的视图与代码内联最大的缺点是,许多IDE还不支持内部字符串语法高亮。我希望我们会看到更多的IDE支持语法高亮的HTML模板。

引导我们的应用

我们文件的最后行 bootstrap(HelloWorld),将启动我们的应用,第一个参数表明,我们的应用的“主(main)”组件是HelloWorld。

一旦被引导,在index.html文件中<hello-world></hello-world>会被我们的组件渲染,让我们尝试一下!

加载我们的应用

要运行我们的应用程序,我们需要做2件事情:

  1. 需要告诉我们的HTML文件导入app文件
  2. 需要在body中使用hello-world组件

将以下添加到body部分:

  



 


    

 
  
    
    
  

在script标签中,我们配置模块加载器System.js,在这里重要的是要了解这行:

System.import('app.js')

这行告诉System.js 要加载app.js作为我们的主要入口。不过还有一个问题:我们还没有一个app.js文件!(我们的文件是app.ts TypeScript文件。)

运行应用

编译TypeScript代码为.js

我们使用TypeScript编写我们的应用,我们有一个app.ts文件,下一步是将这个文件编译成Javascript,让我们的浏览器可以理解。

为了做到这一点,让我们运行TypeScript编译器的命令行,称为tsc:

tsc

如果你得到一个没有错误信息的提示,这意味着编译成功,我们现在在同一目录下应该有app.js文件

ls app.js
# app.js should exist

故障排除:
也许你会收到以下消息: tsc: command not found,这意味着,tsc未安装或不在PATH,尝试使用路径在node_modules目录中的tsc二进制:./node_modules/.bin/tsc

在这种情况下你不需要指定,编译器tsc任何参数,因为它能编译当前目录中的所有ts文件。如果你没有得到一个app.js文件,用cd来更改目录,确保目录和你的app.ts文件在同一目录

当你运行TSC你也可能会得到一个错误。例如,它可能表示 app.ts(2,1): error TS2304: Cannot find name or app.ts(12,1): error TS1068: Unexpected token。

在本例中,当错误时编译器会给你一些提示,app.ts(12,1):表示错误在app.ts第12行。您还可以在网上搜索错误代码,可能会有如何解决这个错误的帮助。

使用npm

如果你在tsc命令上面工作,你也可以使用npm来编译文件,在package.json里包含了一些简单的代码,我们定义了一些快捷命令来帮助你编译

尝试运行:

npm run tsc // compiles TypeScript code once and exits
npm run tsc:w // watches for changes and compiles on change

应用的server

我们来测试应用还有一个步骤。我们需要一个webserver来运行测试应用。
如果你早期使用npm安装,你已经有了一个安装在本地的webserver,要运行它,只需运行下面命令

npm run serve

打开你得浏览器访问http://localhost:8080

我为什么需要一个webserver?如果你之前开发的JavaScript应用程序,您可能知道,有时你只需打开index.html文件,就能运行在你的浏览器。但这样对我们来说没有用,因为我们使用SystemJs。

当你直接打开index.html文件,你的浏览器将使用file:///URL。由于安全限制,当file:///protocol 时你的浏览器将不允许Ajax请求发生(这是一件好事,因为否则JavaScript可以读任何在您的系统上的文件做一些恶意的事)。

所以我们运行一个简单的本地网络服务器提供文件系统。这对于测试真的很方便,并不需要你知道如何部署生产应用。

,如果一切运行正常,你应该看到以下内容:

image

如果无法运行此应用程序,你可以有几件事情尝试:

  • 请确保您的app.js文件是从TypeScript编译器tsc创建的
  • 请确保您的网络服务器开始和app.js文件在同一目录
  • 请确保您的index.html文件符合我们上面的代码示例
  • 尝试在Chrome中打开该网页,点击右键,并选择“检查元素”。然后点击“控制台”选项卡,并检查错误“。
  • 如果一切都失败了,加入Gitter聊天室来提问

任何改变自动编译

我们将对我们的应用程序代码进行大量的修改。我们可以利用–watch选项,不必每次都运行tsc生成新的js代码。
–watch选项将告诉tsc监视我们的TypeScript文件,文件有任何变化就自动重新编译新的JavaScript:

tsc --watch
message TS6042: Compilation complete. Watching for file changes.

其实,这是很常见的,我们已经为其创建了一个快捷方式

1.文件改变后重新编译
2.重载你的开发服务器

npm run go

现在,您可以编辑你的代码,变化将会自动在浏览器中体现出来。

将数据添加到组件

我们的组件现在不是很有趣。大多数组件将具有动态数据。

让我们把name作为组件部分的一个新属性。这样,我们可以重用相同的组件,用于不同的输入。

作出以下修改:

@Component({
selector: 'hello-world',
template: `
Hello {{ name }}
`
})
class HelloWorld {
  name: string;
  constructor() {
    this.name = 'Felipe';
  } 
}

在这里我们做了三个改变:

1.name 属性

在HelloWorld类中添加了一个属性,注意,语法是相对于ES5 JavaScript,在这里name: string;这意味着name是属性的名字,string表示这个name是字符串类型的。

属性的类型是由TypeScript提供的特性。这将在我们的HelloWorld类中设置一个name属性,编译器可以确保这个name是一个字符串。

2.一个constructor

在HelloWorld类中我们定义了constructor,既,当我们实例化这个类时会调用该方法。

在我们的构造函数中,可以通过使用this.name 给name属性赋值。

当我们写成:

 constructor() {
    this.name = 'Felipe';
  } 

我们可以理解为,每当创建一个新的HelloWorld时将name设置为“Felipe”。

3.模板变量

在视图上我们添加了一个新的语法:{{ name }}, 这对大括号叫做模板标记(template-tags),模板标记之间的任何内容都将被扩展为表达式。在这里,因为组件绑定了我们的视图,name会被渲染成Felipe

试试看

尝试这些更改后,重新加载页面。我们应该看到“Hello Felipe”

image

使用数组

现在我们有一个name会说“Hello”,但如果我们有一系列name都想要说“Hello”呢?

如果之前你用过Angular 1,你会使用ng-repeat指令,在Angular 2中,这个相似的指令名为NgFor,
它的语法有点不同,但它们有相同的目的:用于遍历集合对象。

让我们app.ts代码进行如下变化:

import { bootstrap } from "angular2/platform/browser"; 
import { Component } from "angular2/core";
import { NgFor } from "angular2/common";
@Component({
  selector: 'hello-world',
  template: `
  • Hello {{ name }}
`
})
class HelloWorld {
  names: string[];
  constructor() {
    this.names = ['Ari', 'Carlos', 'Felipe', 'Nate'];
  }
}
bootstrap(HelloWorld);

第一个指出的变化是在我们的HelloWorld类中又一个新的属性,类型为string[],这个语法意味着这个names是一个数组,数组中每一个元素是字符串类型

我们改变类中的this.names值为[‘Ari’, ‘Carlos’, ‘Felipe’, ‘Nate’].

接下去改变的是我们的模板,我们现在有一个ul和一个li,li上有一个属性为*ng-For=”#name of names”。*和#字符可能有点混乱的,让我们将其分解:

*ngFor语法说明我们想要在这个属性上使用ngFor指令,NgFor类似于一个for循环,我们的想法是为集合中的每项元素创建新的DOM元素。

值#name of names指出,names是我们在HelloWorld里定义的names数组,#name是names里每个元素的引用变量。

该NgFor指令会渲染names数组中的每一个元素产生一个新的li,每个li都会产生一个局部的name变量,这个变量会替换模板里的{{ name }},渲染到页面

引用变量name非固定的,我们还可以写成


  • Hello {{ foobar }}

但如果相反呢,如果我们这样写会发生什么事:


  • Hello {{ name }}

我们会得到一个错误,因为foobar不是我们组件的属性

ngFor会重复该ngFor附着的这个元素,也就是说,我们把它放在li标签上,而不是ul标签,因为我们想要重复列表的li元素,而不是这个列表ul本身

如果你感到很抽象,你可以通过直接阅读源代码来了解Angular 核心团队如何编写组件。例如,你可以在这里找到NgFor指令的源码

当你重新加载页面,你可以看到我们数组中的每一个字符串:

image

扩大我们的应用

现在我们知道如何创建一个组件的基础部分,让我们重新审视我们的Reddit。在我们开始编码之前,先看看我们的应用,一个好的主意是将它分解成一个个单一逻辑组件。

我们将在这个应用程序中,使用两个组件:

image

  • 用于提交新文章的表单将是一个组件(在图片中的红色标记)
  • 每一篇文章(绿标记)

在一个大型应用中,用于提交文章的表单很可能会成为独立的组件,然而独立的组件使数据传递更为复杂,在本章中我们将其简化,只有2个组件。

现在,我们只做2个组件,但是在本书的后面章节,我们将学习如何处理更复杂的数据架构

应用组件

让我们开始构建顶层应用组件,这个组件将会

1.存储我们目前的文章列表
2.包含提交新文章的表单

我们要建立一个组件来代表我们整个应用:一个RedditApp组件。

为了做到这一点,我们将创建一个模板,一个新的组件:

在这个例子中,我们使用了 Semantic UI CSS,在我们下面的模板里当你看到属性上得class,类似于class=”ui large form segment”这些样式都来自于Semantic。这让我们的应用看起来不错,没有太多额外的标记。

import { bootstrap } from 'angular2/platform/browser'; import { Component } from 'angular2/core';
@Component({
  selector: 'reddit',
  template: `

Add a Link

 `
})
class RedditApp {
  constructor() {
} }
bootstrap(RedditApp);

在这里我们申明了一个RedditApp组件,我们的selector是reddit,意味着这个组件会将标签解析为组件

我们创建了的模板定义了两个input,一个是文章的标题,一个是文章的链接地址

我们需要使用新的RedditApp组件,需要将index.html里的标签来替换为标签

当您重新加载浏览器,你应该可以看到表单被渲染:

image

添加交互

现在我们在表单中有input标签了,但我们没有任何的方式来提交数据。让我们通过在表单中添加一个提交按钮来增加一些交互:

@Component({
  selector: 'reddit',
  template: `

Add a Link

` })
class RedditApp {
  constructor() {
  }
  addArticle(title: HTMLInputElement, link: HTMLInputElement): void { 
    console.log(`Adding article title: ${title.value} and link: ${link.value}`);
  }
}

注意我们已经做了4个变化

  1. 创建了一个按钮标签,用于给用户点击
  2. 我们创建了一个名为addArticle的函数,用来定义当我们点击按钮时我们想做的事情
  3. 我们在标签上添加了#newtitle和#newlink属性

让我们以相反的顺序来看看每一个步骤:

为input绑定一个局部变量

请注意下面是我们的第一个input

#newtitle 是新引用语法,这个标记告诉angular将这个绑定给newtitle变量,这使得在这个视图中可使用这个变量来访问这个input

newtitle是一个对象,表示该input的DOM元素(具体地说,其类型是HTMLInputElement),由于newtitle是一个对象,这意味着我们可以使用newtitle.value得到表单输入的值。

同样我们为另一个input标签添加一个#newlink,这样我们就可以从中提取出值。

绑定事件

在我们的button按钮上添加了一个(click)属性来定义了点击事件,当button被点击时,会调用addArticle方法,addArticle方法有2个参数newtitle和newlink.这些东西是从哪里来的?

  • addArtcile是在组件类RedditApp定义的方法
  • newtitle是name为title的标签的引用
  • newlink是name为link的标签的引用

所有在一起:

定义action逻辑

在RedditApp类中我们定义一个新的函数为addArticle,它接受2个参数,newtitle和newlink,
再一次,重要的是要意识到,newtitle和newlink都是HTMLInputElement类型的对象,而不是直接输入值,要从input中获取值,我们需要调用title.value,现在,通过console.log打印出这2个参数

addArticle(title: HTMLInputElement, link: HTMLInputElement):void { 
    console.log(`Adding article title: ${title.value} and link: ${link.value}`);
}

尝试执行它!

现在,当你点击提交按钮,你可以看到,该消息在控制台上打印出来:

image

添加文章组件

现在我们有一个发布新文章的组件,但我们没有在任何地方展示新的文章。

因为每一篇文章提交将在页面上显示为一个列表,这是一个新的组件的最佳人选。

让我们创建一个新的组件来显示提交的文章。

为此,我们在同一文件内创建一个新的组件,将下面的代码在RedditApp组件中加上

@Component({
    selector: 'reddit-article',
    host: {
        class: 'row'
    },
    template: `
{{ votes }}
Points


`
})
class ArticleComponent {
    votes:number;
    title:string;
    link:string;

    constructor() {
        this.votes = 10;
        this.title = 'Angular 2';
        this.link = 'http://angular.io';
    }

    voteUp() {
        this.votes += 1;
    }

    voteDown() {
        this.votes -= 1;
    }
}

请注意,我们有三个部分来定义这个新组件:

让我们来说说每个部分:

创建reddit-article组件

@Component({
  selector: 'reddit-article',
  host: {
    class: 'row'
  },

首先,我们通过@Component注解来定义新的组件,selector表示将使用标签来使用组件(selector就是标签名)

因此,使用该组件的最重要的方法是将下列标记放置在标记中:


当页面渲染时,这些标记将留在我们的视图中。

我们希望每个reddit-article是独立的一行,我们使用Semantic UI,它提供了CSS class for rows

在Angular 2中,一个组件的host表示该组件元素,你会注意到,将host:{class:row}传递给我们的@Component,这告诉Angular,我们要在host元素上设置class属性为row

使用host选项是一个比较好的选择,如果不是用host选项,我们需要在父视图中写上


通过使用host选项,我们可以从组件中配置host元素。

创建reddit-artcile模板

然后我们通过template选项来定义模板

template: `
{{ votes }}
Points



在这里有很多的标签,让我们把它分解:

image

我们有2列

1.投票数在左边
2.文章信息在右边

我们在模板中显示votes和title使用模板语法{{ votes }} 和 {{ title }}.这2个值将使用ArticleComponent类中的votes和title属性来渲染

我们也可以将模板语法使用在属性内。如a标签的href=”{{ link }}” , 这个href的值会动态的从我们组件类中获取

我们的upvote/downvote链接也有一个行为,我们使用(click)事件来绑定voteUp()/voteDown(),当点击该链接时,组件类ArticleComponent中的voteUp()/voteDown()方法将被调用

创建reddit-article的ArticleComponent定义类

最后,我们创建ArticleComponent定义类:

class ArticleComponent {
    votes:number;
    title:string;
    link:string;

    constructor() {
        this.votes = 10;
        this.title = 'Angular 2';
        this.link = 'http://angular.io';
    }

    voteUp() {
        this.votes += 1;
    }

    voteDown() {
        this.votes -= 1;
    }
}

该ArticleComponent类中有3个属性

  • votes 一个number类型代表所有upvotes减去downvotes的总和
  • title 文章里的标题 string类型
  • link 文章的url string类型

在controlstor()我们设置下属性

constructor(){
    this.votes = 10;
    this.title = 'Angular 2';
    this.link = 'http://angular.io';
}

并且我们对于投票也定义了2个方法,voteUp 和 voteDown

voteUp() {
    this.votes += 1;
}
voteDown() {
    this.votes -= 1;
}

voteUp中我们让this.votes自加1,在voteDown中我们让this.votes自减1

使用reddit-article组件

为了使用该组件,使数据可见,在需要使用的地方添加上标签

在本例中,我们想RedditApp组件中使用这个新组件,让我们改变该组件代码。首先我们需要在RedditApp模板的··标签后加上标签

`

让我们重新加载下浏览器,我们会看到没有被渲染

每当碰到这种问题,第一件事就是打开你的浏览器的开发者控制台。如果我们检查标签(见下面的截图),我们可以看到,reddit-article标签在我们的网页中,但它并没有被编译成组件。为什么呢?

image

这标签未被渲染是因为RedditApp组件不知道ArticleComponent组件是什么。

Angular 1注意:如果你使用过Angular 1,你可能会觉得奇怪应用为什么不知道新的reddit-article组件,这是因为在Angular 1中,指令是全局的。然而在Angular 2你需要明确指定组件需要使用的组件是什么。

一方面,这需要更多的配置代码,但在另一方面,它非常适合构建可扩展的应用程序,因为这意味着你不必在一个全局命名空间中分享你得指令选择器。

为了告诉RedditApp关于新ArticleComponent组件,我们需要在RedditApp指令中添加属性

// for RedditApp
@Component({
  selector: 'reddit', 
  directives: [ArticleComponent],
  template: `
// ...

现在,我们重新加载浏览器,我们应该看到文章被正确渲染了:

image

不过,如果你现在点击voteUp 和 voteDown链接,你会看到页面竟然被重新加载

这是因为,javascript的默认情况下,点击事件会冒泡到所有的父组件上,因为点击事件被传播给父元素上,我们的浏览器试图访问空的链接。

要解决这个错误,我们只需使click事件处理程序中返回一个false。这将确保浏览器不会尝试刷新页面。来改变我们的代码:

voteUp() {
    this.votes += 1;
    return false;
}
voteDown() {
    this.votes -= 1;
    return false;
}

现在,如果你点击链接,你会看到投票的增加和减少。

渲染多行

现在我们在这个页面只有一篇文章,没有办法渲染更多文章,除非我们创建新的<reddit-article>标签,即使我们这样做,所有的文章将会有相同的内容。

创建Article类

一个较好的做法是,当编写Angular2代码时试图从你的组件代码中独立出你的数据结构,为了实现这点,做任何进一步的更改组件之前,让我们创建将代表文章的数据结构,在ArticleComponent组件代码前添加下面代码

class Article {
  title: string;
  link: string;
  votes: number;
  constructor(title, link) {
    this.title = title;
    this.link = link;
    this.votes = 0;
  } 
}

在这里我们创建一个表示Artcile的类,注意这只是一个普通的类,并不是一个组件,在MVC模式中它属于model

每篇文章有一个title,link和一个总的votes,当我们创建一个新的文章时,我们需要title和link,我们还假设默认的votes是0

现在在ArticleComponent代码中使用我们新的Article类,而不是直接在ArticleComponent组件存储的article的内容,

class ArticleComponent { 
  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;
  }
}

注意:现在在组件上已经不直接保存我们的title,link和votes,而是保存一个article的引用

当涉及到voteUp(和voteDown),我们不该增减组件上得vote属性,而是应该增减article上得vote属性

这个重构带来了另一个变化:我们需要更新我们的视图,从正确的位置获得模板变量。为了做到这一点,我们需要改变我们的模板标签。也就是说,在之前使用{{votes}},我们需要将其更改为{{article.votes}}:

template: `
{{ article.votes }}
Points


`

如果你重新加载浏览器,你会看到和前面一样的显示效果。

这是不错的,但在我们的代码的东西仍然是一个小问题:在我们的组件中我们的voteUp/voteDown方法会直接修改article内部的属性

问题是,我们的ArticleComponent组件知道太多关于Article类的内部。为了解决这个问题,让我们为Article也添加相应方法,ArticleComponent也要做出相应修改:

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

然后我们改变ArticleComponent来调用这些方法:

class ArticleComponent { 
  article: Article;
  voteUp(): boolean {
    this.article.voteUp();
    return false;
  }
  voteDown(): boolean { 
    this.article.voteDown(); 
    return false;
  }
}

现在看看我们的ArticleComponent组件定义,代码很少,我们从组件中移除了一点逻辑到我们的model内,这里的对应MVC的指导方针是Fat Models, Skinny Controllers,我们的想法是,我们要把大部分的逻辑转移到我们的model中,使我们的组件尽可能地完成最少的工作。

当我们重新加载浏览器后,你会注意到所有的显示都是一样的,但我们现在有更清晰的代码。

存储多个文章

让我们写一个允许我们有多篇文章的代码。

改变RedditApp的属性,创建一个articles集合

class RedditApp {
  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): void {

注意我们RedditApp的这行

articles: Article[];

如果你不用TypeScript,Article[]你可能看起来是有点奇怪。这种模式就是泛型,它的概念出现在Java,这个想法是,该集合中元素的类型必须是Article,该数组是一个集合,将只持有文章类型的对象。

我们可以在构造函数设置this.articles这个列表:

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),
  ];
}

配置ArticleComponent组件的inputs属性

现在,我们已经创建了一个article model,我们怎样才能将他们交给ArticleComponent组件用呢?

在这里,我们引入了一个新的组件属性称为inputs。我们可以配置组件的inputs属性,它会接收从父传递进来数据。

以前我们的ArticleComponent组件类定义成这样的:

class ArticleComponent {
  article: Article;
  constructor() {
    this.article = new Article('Angular 2', 'http://angular.io');
  }
}

这里的问题是,我们已经将特定文章硬编码在构造函数中,制作组件的要点不仅是封装,而且还具有可重用性。

我们真正喜欢做的是配置我们想要显示的文章。如果,例如,我们有两篇文章,文章1和文章2,我们希望能够通过article作为一个”参数”来传递给reddit-article组件:


Angular 允许我们这样做,通过使用Component的inputs选项

@Component({
  selector: 'reddit-article', 
  inputs: ['article'],
  // ... same
})
class ArticleComponent {
  article: Article; 
  //...

现在如果我们有一篇文章在变量myArticle里,我们可以在ArticleComponent的view里这样写


请注意这里的语法:把在input的名称放在[]内,像这样:[article]属性的值就是我们要传递给该input的内容

然后,这是很重要的,在ArticleComponent实例上的this.article将会被设置为myArticle,你可以认为myArticle作为参数传递到你的组件内

注意 inputs是一个数组,这是因为你可以指定一个组件有许多inputs。

所以我们ArticleComponent完整的代码看起来像这样:

@Component({
    selector: 'reddit-article',
    inputs: ['article'],
    host: {
        class: 'row'
    },
    template: `
{{ article.votes }}
Points


`
})
class ArticleComponent {
    article:Article;

    voteUp():boolean {
        this.article.voteUp();
        return false;
    }

    voteDown():boolean {
        this.article.voteDown();
        return false;
    }
}

渲染文章列表

早些时候我们配置RedditApp来存储articles数组,现在来配置RedditApp来渲染所有的文章。要做到不只有一个<reddit-article>标签的话,将使用ngFor指令来遍历文章列表,并为每一篇文章渲染一个reddit-article

添加这些到RedditApp @Component的template内的</form>后,

Submit link

还记得我们在前面章节使用ngFor指令来渲染name列表吗? 嗯,这也适用于渲染多个组件。

*ngFor=”#article of articles”语法将通过迭代articles并创建局部article变量。

我们使用[inputName]=”inputValue”表达式来指定组件需要的article input.
在这里,我们可以通过ngFor,将局部变量article设置给组件的article input`

我意识到,我们在前面的代码段中多次使用到article变量。如果我们重命名ngFor创建的临时变量
名为foobar,你可能觉得更清楚。


在这里我们有3个变量

  1. articles 是保存Articles的数组,定义在RedditApp组件里
  2. foobar是articles中的每个元素,由NgFor定义
  3. article是在ArticleComponent中定义的input字段名

基本上,NgFor产生一个临时变量foobar,然后我们传递它到reddit-article内

如果你现在重新加载你的浏览器,你可以看到所有文章将被渲染:

image

添加新文章

现在我们需要改变addArticle,为了当按下按钮时候添加一篇新的文章。改变后的addArticle方法如下

addArticle(title: HTMLInputElement, link: HTMLInputElement): void { 
  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 = '';
}

这将:

  • 通过提交过来的title和value创建一个新的Article实例
  • 将新的article添加到Articles中
  • 清空input的值

我们如何清除input字段值?好吧,如果你还记得,title和link是HTMLInputElement对象。这意味着我们可以设置它们的属性。当我们改变属性值,input标签在我们页面上就更改了

如果你点击submit添加一篇新的article,你会在列表中看到这篇新加的文章

收尾

让我们添加一个功能,显示用户点击该链接当会被重定向到的域名

添加domain方法到Article类:

domain(): string { 
  try {
    const link: string = this.link.split('//')[1];
    return link.split('/')[0]; 
  } catch (err) {
      return null;
  }
}

将其添加到 ArticleComponent 模板里



{{ article.title }}

({{ article.domain() }})

现在当我们重新加载浏览器时,应该看到每个网址的域名。

基于评分的排序

如果你点击投票,你会发现有一些不太正确:我们的文章不按评分排序!我们绝对希望看到的最高评分的文章。

我们将文章存储在RedditApp的articles数组里,但数组是未排序的,最简单的方法是在RedditApp上创建一个新的方法sortedArticles

sortedArticles(): Article[] {
  return this.articles.sort((a: Article, b: Article) => b.votes - a.votes);
}

现在我们可以使用ngFor来遍历我们的sortedArticles() (而不是直接遍历articles)



完整的代码

import { bootstrap } from 'angular2/platform/browser';
import { Component } from 'angular2/core';

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

  domain(): string {
    try {
      const link: string = this.link.split('//')[1];
      return link.split('/')[0];
    } catch (err) {
      return null;
    }
  }

  voteUp(): void {
    this.votes += 1;
  }

  voteDown(): void {
    this.votes -= 1;
  }
}

@Component({
  selector: 'reddit-article',
  inputs: ['article'],
  host: {
    class: 'row'
  },
  template: `
{{ article.votes }}
Points


  `
})
class ArticleComponent {
  article: Article;

  voteUp(): boolean {
    this.article.voteUp();
    return false;
  }

  voteDown(): boolean {
    this.article.voteDown();
    return false;
  }
}

@Component({
  selector: 'reddit',
  directives: [ArticleComponent],
  template: `

Add a Link


  `
})
class RedditApp {
  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): void {
    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 = '';
  }

  sortedArticles(): Article[] {
    return this.articles.sort((a: Article, b: Article) => b.votes - a.votes);
  }

}

bootstrap(RedditApp);

结束语

我们成功了,我们创建了第一个Angular 2应用,我们在后面还会学习更多,理解数据流,使用ajax,组件的创建,路由,操纵DOM等。

但现在,请享受你的成功!大部分的Angular 2应用仅仅是象我们上面那样:

1.分解你得应用成为组件
2.创建视图
3.定义model
4.显示model
5.添加交互

寻求帮助

这一章你有什么麻烦吗?你有没有发现一个bug或者有不能运行的代码?我们很乐意听到您的声音!

未经允许不得转载:WEB开发编程网 » 编写你的第一个 Angular2 Web 应用
微信扫码关注微信公众号

WEB开发编程网

谢谢支持,我们一直在努力

安全提示:您正在对WEB开发编程网进行赞赏操作,一但支付,不可返还。