Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

编写你的第一个Angular2 Web应用 #24

Open
kittencup opened this issue Jan 16, 2016 · 13 comments
Open

编写你的第一个Angular2 Web应用 #24

kittencup opened this issue Jan 16, 2016 · 13 comments

Comments

@kittencup
Copy link
Owner

该issue关闭讨论,如有问题请去 #43 提问

目录

@kittencup
Copy link
Owner Author

简单的Reddit克隆

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

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

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

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

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

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

image

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

image

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

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

@kittencup
Copy link
Owner Author

快速入门

TypeScript

开始使用TypeScript前,你需要先安装Node.js。有很多不同的方法安装Node.js,请参阅Node.js的网站https://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 'TypeScript@^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结构:

<!doctype html>
<html>
  <head>
    <title>Angular 2 - Simple Reddit</title> 
   </head>
   <body>
  </body>
</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>中添加这些标签

<script src="node_modules/es6-shim/es6-shim.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>

请注意,我们直接从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看起来应该是这样的

<!doctype html>
<html>
  <head>
    <title>Angular 2 - Simple Reddit</title>
    <!-- Libraries -->
    <script src="node_modules/es6-shim/es6-shim.js"></script>
    <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script> 
    <script src="node_modules/systemjs/dist/system.src.js"></script>
    <script src="node_modules/rxjs/bundles/Rx.js"></script>
    <script src="node_modules/angular2/bundles/angular2.dev.js"></script>
  </head>
  <body>
  </body>
</html>

添加CSS

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

<!doctype html>
<html>
  <head>
    <title>Angular 2 - Simple Reddit</title>
    <!-- Libraries -->
    <script src="node_modules/es6-shim/es6-shim.js"></script>
    <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script> 
    <script src="node_modules/systemjs/dist/system.src.js"></script>
    <script src="node_modules/rxjs/bundles/Rx.js"></script>
    <script src="node_modules/angular2/bundles/angular2.dev.js"></script>
        <!-- Stylesheet -->
    <link rel="stylesheet" type="text/css"
       href="resources/vendor/semantic.min.css">
    <link rel="stylesheet" type="text/css" href="styles.css">
  </head>
  <body>
  </body>
</html>

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

Repository owner locked and limited conversation to collaborators Jan 17, 2016
@kittencup
Copy link
Owner Author

我们的第一个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: `
  <div>
    Hello world
  </div>
`
})
class HelloWorld { }
bootstrap(HelloWorld);

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

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

我们从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中这样使用它

<hello-world></hello-world>

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

  • 一个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>标签,会使用这个组件类进行编译。

添加模板

我们可以通过@Componenttemplate选项来添加模板:

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

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

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

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

引导我们的应用

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

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

加载我们的应用

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

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

将以下添加到body部分:

<!doctype html>
<html>
  <head>
<title>First App - Hello world</title>
<!-- Libraries -->
<script src="node_modules/es6-shim/es6-shim.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script> <script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
    <!-- Stylesheet -->
<link rel="stylesheet" type="text/css" href="resources/vendor/semantic.min.css">
<link rel="stylesheet" type="text/css" href="styles.css"> </head>
  <body>
    <script>
      System.config({
        packages: {
          app: {
            format: 'register',
            defaultExtension: 'js'
} }
      });
      System.import('app.js')
            .then(null, console.error.bind(console));
</script>
    <hello-world></hello-world>
  </body>
</html>

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

System.import('app.js')

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

@kittencup
Copy link
Owner Author

运行应用

编译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

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

@kittencup
Copy link
Owner Author

将数据添加到组件

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

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

作出以下修改:

@Component({
selector: 'hello-world',
template: `<div>Hello {{ name }}</div>`
})
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

@kittencup
Copy link
Owner Author

使用数组

现在我们有一个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: `
  <ul>
    <li *ngFor="#name of names">Hello {{ name }}</li>
  </ul>
`
})
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非固定的,我们还可以写成

<li *ngFor="#foobar of names">Hello {{ foobar }}</li>

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

<li *ngFor="#name of foobar">Hello {{ name }}</li>

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

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

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

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

image

@kittencup
Copy link
Owner Author

扩大我们的应用

现在我们知道如何创建一个组件的基础部分,让我们重新审视我们的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: `
    <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> `
})
class RedditApp {
  constructor() {
} }
bootstrap(RedditApp);

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

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

我们需要使用新的RedditApp组件,需要将index.html里的<hello-world></hello-world>标签来替换为<reddit></reddit>标签

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

image

添加交互

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

@Component({
  selector: 'reddit',
  template: `
<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>
</div>
<div class="field">
<label for="link">Link:</label>
        <input name="link" #newlink>
      </div>
      <button (click)="addArticle(newtitle, newlink)"
              class="ui positive right floated button">
Submit link
      </button>
    </form>
` })
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. 我们在<input>标签上添加了#newtitle#newlink属性

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

为input绑定一个局部变量

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

<input name="title" #newtitle>

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

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

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

绑定事件

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

  • addArtcile是在组件类RedditApp定义的方法
  • newtitlename为title的<input>标签的引用
  • newlinkname为link的<input>标签的引用

所有在一起:

<button (click)="addArticle(newtitle, newlink)" class="ui positive right floated button">
Submit Link
</button>

定义action逻辑

RedditApp类中我们定义一个新的函数为addArticle,它接受2个参数,newtitlenewlink,
再一次,重要的是要意识到,newtitlenewlink都是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: `
    <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>
`
})
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;
    }
}

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

  • 通过@component注解来描述组件属性
  • 通过@component注解的template来描述组件视图
  • 创建一个组件类(ArticleComponent)来定义我们的组件逻辑

让我们来说说每个部分:

创建reddit-article组件

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

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

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

<reddit-article>
</reddit-article>

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

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

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

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

<reddit-article class="row">
</reddit-article>

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

创建reddit-artcile模板

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

template: `
    <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>
`

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

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个方法,voteUpvoteDown

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

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

使用reddit-article组件

为了使用该组件,使数据可见,在需要使用的地方添加上<reddit-article></reddit-article>标签

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

<button (click)="addArticle(newtitle, newlink)" class="ui positive right floated button">
Submit link
  </button>
</form>
<div class="ui grid posts">
  <reddit-article>
  </reddit-article>
</div>
`

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

每当碰到这种问题,第一件事就是打开你的浏览器的开发者控制台。如果我们检查标签(见下面的截图),我们可以看到,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;
} 

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

@kittencup
Copy link
Owner Author

渲染多行

现在我们在这个页面只有一篇文章,没有办法渲染更多文章,除非我们创建新的<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: `
<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内部的属性

问题是,我们的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组件:

<reddit-article [article]="article1"></reddit-article>
<reddit-article [article]="article2"></reddit-article>

Angular 允许我们这样做,通过使用Componentinputs选项

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

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

<reddit-article [article]="myArticle"></reddit-article>

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

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

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

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

@Component({
    selector: 'reddit-article',
    inputs: ['article'],
    host: {
        class: 'row'
    },
    template: `
<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>
`
})
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
  </button>
</form>
<!-- start adding here -->
<div class="ui grid posts">
  <reddit-article
    *ngFor="#article of articles"
    [article]="article">
  </reddit-article>
</div>
<!-- end adding here -->

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

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

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

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

<reddit-article
  *ngFor="#foobar of articles"
  [article]="foobar">
</reddit-article>

在这里我们有3个变量

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

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

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

image

@kittencup
Copy link
Owner Author

添加新文章

现在我们需要改变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,你会在列表中看到这篇新加的文章

@kittencup
Copy link
Owner Author

收尾

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

添加domain方法到Article类:

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()">

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

基于评分的排序

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

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

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

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

<div class="ui grid posts"> 
  <reddit-article *ngFor="#article of sortedArticles()" [article]="article"> 
  </reddit-article>
</div>

@kittencup
Copy link
Owner Author

完整的代码

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: `
    <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>
      <div class="meta">({{ article.domain() }})</div>
      <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>
  `
})
class ArticleComponent {
  article: Article;

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

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

@Component({
  selector: 'reddit',
  directives: [ArticleComponent],
  template: `
    <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>
      </div>
      <div class="field">
        <label for="link">Link:</label>
        <input name="link" #newlink>
      </div>

      <button (click)="addArticle(newtitle, newlink)"
              class="ui positive right floated button">
        Submit link
      </button>
    </form>

    <div class="ui grid posts">
      <reddit-article
        *ngFor="#article of sortedArticles()"
        [article]="article">
      </reddit-article>
    </div>
  `
})
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);

@kittencup
Copy link
Owner Author

结束语

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

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

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

@kittencup
Copy link
Owner Author

寻求帮助

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

@kittencup kittencup changed the title ng-book 2 翻译 编写你的第一个Angular2 Web应用 Jan 18, 2016
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

1 participant