作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
谢尔盖·莫伊谢耶夫的头像

Sergey Moiseev

谢尔盖是一名具有开发复杂web项目经验的全栈软件工程师. 作为一名分析师,他拥有丰富的技能.

Previously At

giftly
Share

Angular是AngularJS框架的新版本,由Google开发. 它有一个完整的重写, 以及各种改进,包括优化的构建和更快的编译时间. 在这篇Angular 5教程中,我们将从头开始构建一个notes应用. 如果你一直在等待学习Angular 5,那么本教程就是为你准备的.

Angular 5教程:一步一步教你的第一个Angular 5应用

该应用程序的最终源代码可以找到 here.

该框架有两个主要版本:AngularJS(版本1)和Angular(版本2+)。. Since version 2, Angular不再是一个JavaScript框架, 所以它们之间有很大的区别, 需要一个基本的名称更改.

我应该使用Angular吗??

It depends. 一些开发人员会告诉你,使用React构建自己的组件会更好,而不需要额外的代码. 但这也可能是一个问题. Angular是一个完全集成的框架,它允许你快速开始你的项目,而不需要考虑选择哪些库以及如何处理日常问题. 我认为Angular是面向前端的,RoR是面向后端.

TypeScript

如果你不知道 TypeScript,不要害怕. 你的JavaScript知识足以让你快速学习TypeScript, 大多数现代编辑在这方面都很有效. 现在最可取的选择是 VSCode 和任何JetBrains IntelliJ家族(e.g., Webstorm or, in my case, RubyMine). 对我来说,使用更智能的编辑器比 vim,因为它会在代码中出现任何错误时给您额外的提示 TypeScript是强类型的. 另一件要提到的事情是,Angular CLI和它的Webpack负责把TS编译成JS, 所以你不应该让IDE为你编译它.

Angular CLI

Angular现在有了自己的 CLI, or 命令行接口,它可以为您做大部分的日常操作. 要开始使用Angular,我们必须安装它. 它需要Node 6.9.0或更高以及NPM 3或更高. 我们不打算介绍它们在您的系统中的安装, 因为最好自己查找最新的安装文档. 一旦它们都安装好了,我们将运行以下命令来安装Angular CLI:

NPM install -g @angular/cli

安装成功后,我们可以通过运行 ng new command:

新的开始
  创建getting-started-ng5 /自述.md (1033 bytes)
  创建getting-started-ng5 /.angular-cli.Json(1254字节)
  创建getting-started-ng5 /.Editorconfig(245字节)
  创建getting-started-ng5 /.Gitignore(516字节)
  创建getting-started-ng5 / src /资产/.Gitkeep(0字节)
  创建getting-started-ng5 / src /环境/环境.prod.ts (51 bytes)
  创建getting-started-ng5 / src /环境/环境.ts (387 bytes)
  创建getting-started-ng5 / src /图标.ico (5430 bytes)
  创建getting-started-ng5 / src /索引.html (304 bytes)
  创建getting-started-ng5 / src / main.ts (370 bytes)
  创建getting-started-ng5 / src / polyfills.ts (2405 bytes)
  创建getting-started-ng5 / src /风格.css (80 bytes)
  创建getting-started-ng5 / src /测试.ts (1085 bytes)
  创建getting-started-ng5 / src / tsconfig.app.json (211 bytes)
  创建getting-started-ng5 / src / tsconfig.spec.json (304 bytes)
  创建getting-started-ng5 / src /打字.d.ts (104 bytes)
  创建getting-started-ng5 / e2e /应用程序.e2e-spec.ts (301 bytes)
  创建getting-started-ng5 / e2e /应用程序.po.ts (208 bytes)
  创建getting-started-ng5 / e2e / tsconfig.e2e.json (235 bytes)
  创建getting-started-ng5 /业力.conf.js (923 bytes)
  创建getting-started-ng5 /包.Json(1324字节)
  创建getting-started-ng5 /量角器.conf.js (722 bytes)
  创建getting-started-ng5 / tsconfig.json (363 bytes)
  创建getting-started-ng5 / tslint.Json(3040字节)
  创建getting-started-ng5 / src /应用程序/应用程序.module.ts (316 bytes)
  创建getting-started-ng5 / src /应用程序/应用程序.component.css (0 bytes)
  创建getting-started-ng5 / src /应用程序/应用程序.component.HTML(1141字节)
  创建getting-started-ng5 / src /应用程序/应用程序.component.spec.ts (986 bytes)
  创建getting-started-ng5 / src /应用程序/应用程序.component.ts (207 bytes)
通过纱线安装工装包.
yarn install v1.3.2
info没有找到lockfile.
[1/4]🔍解析包...
[2/4]🚚获取包...
[3/4]🔗链接依赖...
warning "@angular/cli > @schematics/angular@0.1.10“有不正确的对等依赖”@angular-devkit/schematics@0.0.40".
warning "@angular/cli > @angular-devkit/schematics > @schematics/schematics@0.0.10“有不正确的对等依赖”@angular-devkit/schematics@0.0.40".
[4/4]📃构建新包...
成功保存lockfile.
✨  Done in 44.12s.
通过纱线安装工装包.
成功初始化git.
项目“getting started-ng5”成功创建.

完成这些之后,我们可以通过运行请求新应用程序启动 ng serve 离开它的目录:

ng serve
** Live开发服务器正在监听localhost:4200, 在http://localhost:4200/ **上打开浏览器
日期:2017 - 12 - 13 t17:48:30.322Z
散列:d147075480d038711dea
Time: 7425ms
Chunk {inline}内联.bundle.js (inline) 5.79kb[条目][渲染]
Chunk {main} main.bundle.js (main) 20.8kb[首字母][渲染]
块{polyfills} polyfills.bundle.js (polyfills) 554 kB[初始][渲染]
Chunk {styles}样式.bundle.js (styles) 34.1kb[首字母][渲染]
Chunk {vendor} vendor.bundle.js (vendor) 7.14mb[首字母][渲染]

webpack:编译成功.

如果我们将浏览器导航到该链接,它将显示如下图所示:

Angular应用欢迎页面

那么,这里到底发生了什么? Angular CLI运行 Webpack开发服务器, 这会在下一个空闲端口上呈现我们的应用程序(这样你就可以在同一台机器上运行多个应用程序), with live reload. 它还监视项目源中的每一个更改,并重新编译所有更改, 之后,它要求浏览器重新加载打开的页面. 所以通过使用Angular CLI, 我们已经在一个开发环境中工作,没有写任何配置或实际做任何事情. 但我们才刚刚开始…

Components

我们已经运行了空应用程序. 让我们来谈谈Angular中的应用组合. 如果你有一些背景知识 AngularJS发展,你知道有控制器、指令和组件 like 指令,但更简单,允许你升级到Angular 2. 对于那些没有这种奇妙经历的人来说,他们必须在两者之间做出选择,弄清楚什么放在哪里, don’t worry. 现在大部分都是组件. 组件是Angular世界中最基本的构建块. 让我们来看看Angular CLI为我们生成的代码.

First, here’s index.html:




  
  GettingStartedNg5
  

  
  


  

























它看起来就像你每天都能看到的那种加价. 但是有一个特殊的标签, app-root. Angular是如何做到这一点的,我们又如何知道它内部发生了什么?

Let’s open the src/app 目录,看看有什么. 你可以看看 ng new 或者在您选择的IDE中打开它. 你会看到我们做到了 app.component.ts 这里是下一位(这可能取决于你的Angular版本有多新):

从“@angular/core”中导入{Component};

@Component({
  选择器:“app-root”,
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
导出类AppComponent {
  title = 'app';
} 

@Component(...) 这里看起来像一个函数调用…它是什么? This is 打印稿装饰我们稍后会讨论这个问题. 现在,让我们试着理解它在做什么,传递的参数是这样的 selector 用来生成我们的组件声明. 它只是为我们做了大量的样板工作,并以工作形式返回我们的组件声明. 我们不需要实现额外的代码来支持装饰器的任何参数. 这一切都由装饰师来处理. 一般来说,我们称之为 factory methods.

We already saw app-root in our index.html. Angular是这样知道如何找到与我们的标签对应的组件的. Obviously, templateUrl and styleUrls 定义Angular应该从哪里获取我们的标记和CSS. 组件装饰器还有很多参数, 我们将在新应用中使用其中一些, 但如果你想要完整的参考资料, 你可以随时查看 here.

让我们看看这个组件的标记:


欢迎来到{{title}}!

Angular Logo

Here are some links to help you start:

So, 除了将Angular Logo嵌入为SVG之外, 这很简洁, 这似乎也是典型的日常加价. 除了一件事(欢迎来到{{title}}!),如果我们再看看我们的组件代码,我们就会看到 title = 'app';. So, 如果你已经有一些模板语言的实践,或者使用过AngularJS, 这里发生的事情很明显. 如果你不知道, 这被称为角插值, 通过它,双花括号内的表达式从我们的组件中被拉出来(您可以想到 {{ title }} 的简化形式 {{ this.title }}),并显示在我们的标记上.

现在,我们已经看到了自动生成的Angular应用中实际发生在浏览器页面上的所有部分. 让我们回顾一下它是如何工作的:Angular CLI运行Webpack,它会把我们的Angular应用编译成 JavaScript包 把它们注入我们的 index.html. 如果我们在浏览器中使用 inspect 功能,我们看到这样的东西:

Angular通过web检查器

每次我们修改代码, Angular CLI会重新编译, 必要时重新注入, 并要求浏览器重新加载打开的页面. Angular做得很快, 所以在大多数情况下, 当您将窗口从IDE切换到浏览器时, 它已经为你重新上膛了.

So, 让我们开始朝着目标前进, for a start, 让我们将项目从CSS切换到Sass,并打开我们的 .angular-cli.json and edit styles and styleExt 属性:

"styles": [
  "styles.scss"
],
[...]
"defaults": {
  :“styleExt scss”,
  "component": {}
}

我们还需要将Sass库添加到项目中并重命名 styles.css to styles.scss. 为了添加Sass,我使用 yarn:

yarn add sass 
yarn add v1.3.2
[1/4]🔍解析包...
[2/4]🚚获取包...
[3/4]🔗链接依赖...
[...]
[4/4]📃构建新包...
成功保存lockfile.
保存了1个新的依赖项.
└─ sass@1.0.0-beta.4
✨  Done in 12.06s.
纱线添加node-sass@4.7.2 --dev
✨  Done in 5.78s.

我还想在我们的项目中使用Twitter Bootstrap,所以我也运行 纱线添加bootstrap@v4.0.0-beta.2 and edit our styles.scss to include this:

/*你可以添加全局样式到这个文件,也可以导入其他样式文件*/
@import "../ node_modules引导/ scss引导”;
 
body {
  padding-top: 5眼动;
}

We need to edit index.html 为了使我们的页面响应通过改变我们的标记元:


现在我们可以替换 app.component.html with this:



Welcome to {{title}}!

现在,如果我们打开浏览器,我们会看到下面的内容:

欢迎来到这个应用

这就是样板文件的内容. 让我们继续创建我们自己的组件.

第一个分量

我们将在界面中将笔记显示为卡片, 我们从生成第一个分量开始, 代表牌本身. 为此,让我们通过运行以下命令来使用Angular CLI:

ng生成组件卡
  创建src / app /卡/卡.component.scss (0 bytes)
  创建src / app /卡/卡.component.html (23 bytes)
  创建src / app /卡/卡.component.spec.ts (614 bytes)
  创建src / app /卡/卡.component.ts (262 bytes)
  src /应用程序/应用程序更新.module.ts (390 bytes)

If we look into src / app /卡/卡.component.ts, 我们可以看到它们几乎是相同的代码, 就像我们在AppComponent中那样, 有一点小小的不同:

[...]
@Component({
  选择器:“app-card”,
[...]
导出类CardComponent实现OnInit {

  构造函数(){}

  ngOnInit() {
  }
}

我想提一下, at this point, 在我们的组件选择器前加上一个通用的前缀被认为是一种良好的做法, and by default, it’s app-. 可以将其更改为首选项的前缀 prefix property in .angular-cli.json,所以最好在使用前使用 ng generate 第一次.

因此,我们有一个组件的构造函数和一个 ngOnInit function for it. 如果你好奇我们为什么这么做,你可以在 角的文档. 但在基本层面上, 可以这样考虑这些方法:在创建组件之后立即调用构造函数, 早在要传递给它的数据准备好并填充之前, while ngOnInit 仅在数据更改的第一个周期之后运行,因此您可以访问组件输入. 我们很快会讨论输入和组件通信, but for now, 让我们记住,最好使用常量的构造函数, 比如那些被硬编码到组件中的东西, and ngOnInit 对于所有依赖于外部数据的东西.

让我们来填充CardComponent实现. 首先,让我们向它添加一些标记. 标记的默认内容是这样的:

card works!

让我们用代码替换它,这样它就会表现得像一张牌:

Text

现在是显示卡片组件的好时机, 但这又带来了额外的问题:谁来负责展示这些卡片? AppComponent? 但AppComponent会在应用中的其他任何东西之前加载, 所以我们必须考虑它的整洁和小. 我们最好再创建一个组件来存储卡片列表并将其显示在页面上.

正如我们所描述的组件的职责, 很明显,这应该是一个卡片列表组件. 让Angular CLI为我们生成它:

ng生成组件CardList
  创建src / app /贺卡/贺卡名单.component.scss (0 bytes)
  创建src / app /贺卡/贺卡名单.component.html (28 bytes)
  创建src / app /贺卡/贺卡名单.component.spec.ts (643 bytes)
  创建src / app /贺卡/贺卡名单.component.ts (281 bytes)
  src /应用程序/应用程序更新.module.ts (483 bytes)

在我们开始实施它之前, 让我们看一下在生成第一个组件后忽略的东西. Angular CLI告诉我们它已经更新了 app.module.ts for us. 我们从来没有调查过这个问题,所以让我们纠正一下:

从“@angular/platform-browser”中导入{BrowserModule};
从“@angular/core”中导入{NgModule};


导入{AppComponent}./app.component';
导入{CardComponent}./card/card.component';
导入{CardListComponent}.贺卡/贺卡.component';


@NgModule({
  declarations: [
    AppComponent,
    CardComponent,
    CardListComponent,
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  引导(AppComponent):
})
导出类AppModule {}

Obviously, BrowserModule and NgModule 都是Angular内部模块. 我们可以在文档中阅读更多关于它们的信息. AppComponent在我们开始生成任何代码之前就在这里了, 因此,我们的新组件实际上在两个地方填充模块:首先, 它们是从它们的定义文件导入的, and then, 它们被包含在NgModule装饰器的declarations数组中. 如果你正在从头开始创建一个新组件,却忘了在NgModule中添加一个新模块,那么就试着把它添加到你的标记中, 你的应用将无法在JS控制台中处理下一个错误:

未捕获的错误:模板解析错误:
'app-card-list'不是已知元素:
1. 如果“app-card-list”是Angular组件,那就验证它是不是该模块的一部分.
2. 如果'app-card-list'是一个Web组件,那么就把'CUSTOM_ELEMENTS_SCHEMA'添加到'@NgModule '.该组件的模式来抑制此消息. ("

所以如果你的应用无法正常运行,不要忘记检查你的控制台.

让我们来填充卡片列表组件标记(src / app /贺卡/贺卡名单.component.html):

如果我们在浏览器中打开它,我们会看到这样的内容:

Hard coded cards

目前,我们用硬编码标记显示卡片. 让我们通过将硬编码的卡片数组移动到我们的应用程序中,使我们的代码更接近实际情况:

导出类AppComponent {
  public cards: Array = [
    {text: 'Card 1'},
    {text: 'Card 2'},
    {text: 'Card 3'},
    {text: 'Card 4'},
    {text: 'Card 5'},
    {text: 'Card 6'},
    {text: 'Card 7'},
    {text: 'Card 8'},
    {text: 'Card 9'},
    {text: 'Card 10'},
  ];
}

我们有了初始列表,但是我们仍然需要将它传递给组件并在那里呈现它. 为此,我们需要创建第一个输入. 让我们把它添加到CardList组件中:

从“@angular/core”中导入{Component, Input, OnInit};
[...]
导出类CardListComponent实现OnInit {
  @Input() cards: Array;
[...]

We imported Input 并将其用作类级变量卡的装饰器,这些变量卡的类型是Array,可以是任何类型的对象. 理想情况下,我们不应该使用 any, 但是应该使用严格的类型,这样我们就可以定义像接口卡这样的东西, 它将包含我们卡的所有属性, 但我们稍后会让它工作,现在, we’re using any 只是为了得到一个快速而肮脏的实现.

现在,我们在CardList中有了纸牌数组. 我们如何显示它而不是当前的标记? 让我们看一下cardlist组件中的新代码:


这对我们来说是新的东西,一个以星号开头的属性名. 这是什么意思?? 这是Angular命名的默认约定 结构指示. 结构指令控制 structure of our template. 这里的星号实际上是“语法糖”,和 你可以继续阅读 去理解它是如何工作的. 但是对于你现在的例子, 只要理解将它添加到组件中会发生什么就足够了. So ngFor a 中继器指令 它会为纸牌数组中的每个元素重复我们的app card. 如果我们看看浏览器,我们会看到下面这个:

A blank page? 这是什么Angular 5教程啊?

Something isn’t right; we have our array of cards, but we are getting an empty page.

我们在AppComponent级别定义了我们的纸牌数组,但是我们还没有把它传递给CardList输入. 让我们编辑AppComponent模板来实现这一点.


这个语法——方括号中的属性——告诉Angular我们想要这样做 one-way bind 我们的分量变量 cards 到Card List组件 [cards] input. 一旦我们这样做,我们得到这个:

卡片在纸上,但文字全错了

Of course, 我们想要显示纸牌数组的实际内容, and for that, 我们还需要将card对象传递给card组件. 让我们扩展Card List组件:


如果我们现在在浏览器中查看,我们会在JS控制台中得到下一个错误: 不能绑定到" card "因为它不是" app-card "的已知属性.. Angular告诉我们,我们仍然需要在Card组件中定义输入. 所以我们可以这样编辑:

从“@angular/core”中导入{Component, Input, OnInit};
[...]
导出类CardComponent实现OnInit {
  @Input()卡:任何;
[...]

让我们把cardtext属性添加到card组件模板中:

[...]

{{ card.text }}

[...]

现在让我们看看它是如何工作的:

有正确文字的卡片

看起来不错,就是发型有点不对劲. 让我们通过添加一个新样式来解决这个问题 card.component.css:

.card {
    margin-top: 1.5rem;
}

现在看起来好多了:

有更好风格的卡片

组件通信

让我们添加一个New Card Input组件,它将允许我们添加注释:

组件NewCardInput
  创建src / app / new-card-input / new-card-input.component.scss (0 bytes)
  创建src / app / new-card-input / new-card-input.component.html (33 bytes)
  创建src / app / new-card-input / new-card-input.component.spec.ts (672 bytes)
  创建src / app / new-card-input / new-card-input.component.ts (300 bytes)
  src /应用程序/应用程序更新.module.ts (593 bytes)

然后在它的模板旁边添加:

接下来,把这个添加到组件装饰器中:

[...]
@Component({
  选择器:“app-new-card-input”,
[...]
  主机:{'class': 'col-4'}
})
[...]

然后把我们的新组件添加到AppComponent模板中:

[...]

让我们看一下浏览器.

添加输入字段

问题是我们的新组件没有做任何事情. 让我们来让它工作——让我们首先添加一个变量来保存我们的新卡片:

[...]
导出类NewCardInputComponent实现OnInit {
[...]
public newCard: any = {text: "};
[...]

我们如何用输入填充它? 如果你以前用过AngularJS,你可能知道 双向数据绑定. Or, 你可能在那些华丽的AngularJS演示中见过它, 你在哪里输入值然后它会为我们更新页面内容.

这里有一个有趣的花边新闻:双向数据绑定在Angular中已经不再存在了. 但这并不意味着我们无法了解这种行为. 我们已经见过和用过了 (价值)= "表达",它将表达式绑定到输入元素的value属性. 但是我们也有 (输入)= "表达"一种将表达式绑定到input元素的input事件的声明式方式. 它们可以这样组合使用:


所以,每次我们 newCard.text 值改变时,它会被传递给我们的组件输入. 每次用户输入数据到我们的输入和浏览器输出 input $event, we assign our newCard.text 到输入值.

在我们实现它之前还有一件事:这个输入看起来有点多,不是吗? Actually, Angular给了我们一点语法糖, 我们可以用哪个呢, 所以我从一个不同的角度开始解释这种糖是如何工作的.


This syntax, ([]), called banana in a box or ngModel是Angular的指令,负责从事件中获取价值等等. 所以我们可以编写更简单的代码,将我们的值绑定到输入值和代码中的变量.

不幸的是,在我们加上 ngModel,我们得到了错误, 不能绑定到'ngModel',因为它不是'input'的已知属性.. 我们需要导入 ngModel to our AppModule. But from where? If we check the documentation,我们可以看到它在Angular Forms模块中. 所以我们需要这样编辑掉AppModule:

[...]
从“@angular/forms”中导入{FormsModule};

@NgModule({
[...]
  imports: [
    BrowserModule,
    FormsModule
  ],
[...]

处理本地事件

我们填满了变量, 但我们仍然需要把那个值发送到AppComponent中的cardlist. 为了向Angular组件传递数据,我们必须有input. 似乎要在组件外部通信数据, we have output, 我们使用它的方式和使用input一样——我们从Angular代码中导入它,并使用一个装饰器来定义它:

从@angular/core中导入{Component, EventEmitter, OnInit, Output};
[...]
导出类NewCardInputComponent实现OnInit {
[...]
@Output() onCardAdd = new EventEmitter();
[...]
}

But there is more than just output; we also define something called EventEmitter because the component output is supposed to be an event, 但我们不应该像处理那些旧的JavaScript事件那样考虑它. They aren’t bubbles. 你不需要打电话 preventDefault 在每个事件侦听器中. 要从组件发送数据,我们应该使用它的有效负载. 所以我们需要订阅事件——我们怎么做呢? 让我们来修改AppComponent模板:


我们还将表达式绑定到事件 onCardAdd,正如我们在 NewCardInput component. 现在我们需要实现 addCard method on our AppComponent.

[...]
导出类AppComponent {
[...]
addCard(cardText: string) {
  this.cards.推动({文本:cardText});
}

但是我们仍然没有从任何地方输出它. 让我们试着让它在用户点击 enter key. 我们需要监听组件中的DOM按键事件,并输出由该事件触发的Angular事件. 为了监听DOM事件,Angular给了我们 HostListener decorator. 它是一个函数装饰器,以我们想要监听的本地事件和Angular想要调用来响应该事件的函数的名称为参数. 让我们来实现它并讨论它是如何工作的:

从“@angular/core”中导入{Component, EventEmitter, OnInit, Output, HostListener};
[...]
导出类NewCardInputComponent实现OnInit {
[...]
@HostListener(“文档:键盘按键”,['美元事件'])
handleKeyboardEvent(事件:KeyboardEvent) {
  if (event.code === "Enter" && this.newCard.text.length > 0) {
    this.addCard(this.newCard.text);
   }
}
[...]
addCard(text) {
  this.onCardAdd.emit(text);
  this.newCard.text = '';
}
[...]

So, if the 文档:键盘按键 事件发生时,我们检查按下的键是Enter和 newCard.text 里面有东西. 之后,我们可以叫我们的 addCard 方法,我们在其中输出Angular onCardAdd 使用我们的卡中的文本并将卡文本重置为空字符串,以便用户可以继续添加新卡而无需编辑旧卡的文本.

使用表单

在angular中有几种处理表单的方法,其中一种是模板驱动的,我们已经使用了其中最有价值的部分: ngModel 对于双向绑定. 但是Angular中的表单不仅与模型值有关,还与有效性有关. 目前,我们在HostListener函数中检查NewCardInput的有效性. 让我们将其移动到一个更受模板驱动的表单. 为此,我们可以修改组件的模板:

这是Angular的另一个语法糖. The hash #form is a 模板引用变量 我们可以使用它从代码中访问表单. 让我们使用它来确保我们实际上使用了所需的属性验证,而不是手动检查值长度:

从“@angular/core”中导入{Component, EventEmitter, OnInit, Output, HostListener, ViewChild};
从“@angular/forms”中导入{NgForm};
[...]
导出类NewCardInputComponent实现OnInit {
[...]
@ViewChild('form') public form: NgForm;
[...]
  @HostListener(“文档:键盘按键”,['美元事件'])
  handleKeyboardEvent(事件:KeyboardEvent) {
    if (event.code === "Enter" && this.form.valid) {
[...]

这里还有一个新的装饰师: ViewChild. Using that, 在本例中,我们可以访问任何由模板引用值标记的元素, our form, 我们实际上把它声明为Component的公共变量form, so we can write this.form.valid.

使用模板驱动表单和我们之前使用简单的HTML表单完全一样. 如果我们需要更复杂的东西, 在Angular中,这种情况有一种不同的形式:响应式. 我们将介绍在转换表单后它们对什么作出反应. 为此,让我们添加一个新的导入到 AppModule:

[...]
从“@angular/forms”中导入{FormsModule, ReactiveFormsModule};
[...]
imports: [
[...]
ReactiveFormsModule,
]
[...]

响应式表单是在代码中定义的,而不是模板驱动的表单 NewCardInput component code:

[...]
从“@angular/forms”中导入{FormBuilder, FormGroup, Validators};
[...]
导出类NewCardInputComponent实现OnInit {
[...]
newCardForm: FormGroup;

构造函数(fb: FormBuilder) {
  this.newCardForm = fb.group({
    'text':[",验证器.组成([验证器.要求,验证器.minLength(2)])],
  });
}
[...]
if (event.code === "Enter" && this.form.valid) {
   this.addCard(this.newCardForm.controls['text'].value);
[...]
addCard(text) {
  this.onCardAdd.emit(text);
  this.newCardForm.controls['text'].setValue('');
}

除了导入新模块之外,这里还发生了一些新的事情. 首先,我们在使用 依赖注入 在构造函数中使用FormBuilder,并使用它来构建表单. 这里的文本是我们字段的名称,空字符串是初始值,和 Validators.compose 显然,它允许我们在单个字段上组合多个验证器. We use .value and .setValue('') 访问字段的值.

让我们来看看这种处理表单的新方式的标记:

We are using FormGroupDirective 告诉Angular需要在哪个表单组中查找它的定义. 通过使用formControlName,我们告诉Angular我们应该使用响应式表单中的哪个字段.

For now, 之前使用模板驱动表单的方法与使用响应式表单的新方法之间的主要区别在于在响应端编写了更多的代码. 如果我们不需要动态定义表单,这真的值得吗?

绝对是. 去理解它是如何有帮助的, 让我们首先讨论一下为什么这种方法被称为“响应式”.

让我们从向New Card Input组件构造函数中添加额外的代码开始:

从rxjs/operators中导入{takeWhile, debounceTime, filter};
[...]
this.newCardForm.valueChanges.pipe(
    filter((value) => this.newCardForm.valid),
  debounceTime (500),
  takeWhile(() => this.alive)
).subscribe(data => {
   console.log(data);
});

打开浏览器和开发人员工具控制台, 看看当我们在输入中输入新的值会发生什么:

控制台将在每次更改时注销新值

RxJS

那么这里到底发生了什么? We are seeing RxJS in action. Let’s discuss it. 我猜你们至少都知道一些 promises 构建异步代码. 承诺处理单个事件. 我们要求浏览器做出 POST,它返回给我们一个promise. RxJS使用observable来处理事件流. 可以这样考虑:我们刚刚实现了在每次更改表单时调用的代码. 如果我们用承诺来处理用户的改变, 在我们需要重新订阅之前,将只处理第一个用户更改. The Observable, 同时, 是否能够处理几乎无穷无尽的“承诺”流中的每个事件.“我们可以通过在过程中出现一些错误或从可观察对象中取消订阅来打破这一点.

What is takeWhile here? 我们在组件中订阅了可观察对象. 它们在我们应用程序的不同部分使用, 所以他们可能会在途中被摧毁, 当我们在路由中使用组件作为页面时(我们将在本指南后面讨论路由). 但是,代替Observable的promise只会运行一次,之后就会被销毁, 只要流在更新,Observable就会持续存在,而且我们不会取消订阅. 所以我们的订阅需要取消订阅(如果我们不寻找内存泄漏),像这样:

Const订阅= observable.subscribe(value => console.log(value));
[...]
subscription.unsubscribe();

但在我们的应用中,我们有很多不同的订阅. 我们需要做所有的样板代码吗? 实际上,我们可以作弊,用 takeWhile operator. 通过使用它,我们可以确保我们的流将尽快停止发出新值 this.alive 变为false我们只需要在 onDestroy 分量的函数.

使用后端

因为我们不在这里构建服务器端,所以我们将使用Firebase作为我们的API. 如果你有自己的API后端,让我们 配置我们的后端开发服务器. 要做到这一点,就要创造 proxy.conf.json 在项目的根目录中添加以下内容:

{
  "/api": {
    “目标”:“http://localhost: 3000”,
    "secure": false
  }
}

对于从我们的应用程序到它的主机(如果你还记得,它是Webpack开发服务器)的每个请求 /api 路由服务器应该将请求代理到 http://localhost:3000/api. For that to work, we need to add one more thing to our app configuration; in package.json,我们需要更换 start 我们项目的命令:

[...]
"scripts": {
[...]
  "start": "ng serve——proxy-config proxy . config ".conf.json",

现在,我们可以运行项目 yarn start or npm start 并获得适当的代理配置. 我们如何使用Angular的API? Angular给了我们 HttpClient. 让我们为当前应用程序定义CardService:

从“@angular/core”中导入{Injectable};
从“@angular/common/http”中导入{HttpClient};

@Injectable()
导出类CardService {

  构造函数(私有http: HttpClient) {}
 
  get() {
    return this.http.(' / api / v1 /卡.json`);
  }

  add(payload) {
    return this.http.邮报》(' / api / v1 /卡.Json ', {text: trim(payload)});
  }

  删除(载荷){
    return this.http.删除(' / api / v1 /卡/ ${负载.id}.json`);
  }

  更新(载荷){
    return this.http.补丁(' / api / v1 /卡/ ${负载.id}.json`, payload);
  }
}

So what does Injectable here mean? 我们已经确定,依赖注入可以帮助我们将组件与我们使用的服务一起注入. 为了获得对新服务的访问权限,我们需要将其添加到 AppModule:

[...]
导入{CardService}./services/card.service';
[...]
@NgModule({
[...]
 提供者:[CardService],

现在我们可以把它注入到AppComponent中,例如:

导入{CardService}./services/card.service';
[...]
  构造函数(private cardService: cardService) {
    cardService.get().subscribe((cards: any) => this.cards = cards);
  }

Firebase

我们来配置一下 Firebase 现在,在Firebase中创建一个演示项目并点击 将Firebase添加到应用程序中 button. 然后,我们将Firebase显示的凭据复制到应用程序的环境文件中,如下所示: src /环境/

导出const环境= {
[...]
  firebase: {
    apiKey: "[...]",
    authDomain: "[...]",
    databaseURL: "[...]",
    projectId: "[...]",
    storageBucket:“(...]",
    messagingSenderId:“(...]"
  }
};

我们需要把它加到两者上 environment.ts and environment.prod.ts. 让您对这里的环境文件有一些了解, 它们实际上包含在项目的编译阶段, and .prod. 定义的部分 --environment switch for ng serve or ng build. 您可以在项目的所有部分中使用该文件中的值,并从 environment.ts 而Angular CLI负责从对应的 environment.your-environment.ts.

让我们添加Firebase支持库:

纱线添加firebase@4.8.0 angularfire2
yarn add v1.3.2
[1/4]🔍解析包...
[2/4]🚚获取包...
[3/4]🔗链接依赖...
[...]
成功保存lockfile.
节省了28个新的依赖项.
[...]
✨  Done in 40.79s.

现在让我们改变CardService来支持Firebase:

从“@angular/core”中导入{Injectable};
从'angularfire2/database'中导入{AngularFireDatabase, AngularFireList, AngularFireObject};
从'rxjs/Observable'中导入{Observable};

import {Card} from '../models/card';

@Injectable()
导出类CardService {

  private basePath = '/items';

  cardsRef: AngularFireList;
  cardRef:  AngularFireObject;

  构造器(私有db: AngularFireDatabase) {
    this.cardsRef = db.list('/cards');
  }

  getCardsList(): Observable {
    return this.cardsRef.snapshotChanges ().map((arr) => {
      return arr.map((snap) => Object.assign(snap.payload.Val (), {$key: snap.key }) );
    });
  }

  getCard(key: string): Observable {
    const cardPath = ' ${这个.basePath} / ${关键}';
    Const card = this.db.object(cardPath).valueChanges() as Observable;
    return card;
  }

  createCard(card: card):无效{
    this.cardsRef.push(card);
  }

  updateCard(key: string, value: any): void {
    this.cardsRef.更新(关键字,值);
  }

  deleteCard(key: string): void {
    this.cardsRef.remove(key);
  }

  deleteAll():无效{
    this.cardsRef.remove();
  }

  //所有动作的默认错误处理
  private handleError(error: error) {
    console.error(error);
  }
}

在导入的卡的第一个模型中,我们看到了一些有趣的东西. 我们来看看它的构成:

出口类卡{
    $key: string;
    text: string;

    构造函数(text: string) {
        this.text = text;
    }
}

因此,我们正在用类构建数据,除了文本之外,我们还添加 key$ from Firebase. 让我们修改AppComponent来使用这个服务:

[...]
从'angularfire2/database'导入{AngularFireDatabase};
从'rxjs/Observable'中导入{Observable};
import {Card} from './models/card';
[...]
导出类AppComponent {
public cards$: Observable;

addCard(cardText: string) {
  this.cardService.createCard(新卡(cardText));
}

构造函数(private cardService: cardService) {
  this.cards$ = this.cardService.getCardsList();
}

What is cards$? 我们通过添加来标记可观察变量 $ 让他们知道我们应该怎样对待他们. Let’s add our cards$ 到AppComponent模板中:

[...]

作为回报,我们在控制台中得到以下错误:

CardListComponent.html:3错误:无法找到类型为object的不同支持对象'[object object]'. NgFor只支持绑定到数组之类的可迭代对象.

Why so? 我们从Firebase接收到观测数据. But our *ngFor 在CardList组件中等待对象数组,而不是这些数组的可观察对象. 我们可以订阅那个可观察对象并将它分配给一个静态纸牌数组, 但有一个更好的选择:


The async pipe, 它实际上是Angular提供给我们的另一个语法糖, 做同样的事情,我们讨论-订阅观察和返回其当前值作为我们的表达式的评估结果.

响应式Angular - Ngrx

让我们谈谈我们的应用程序状态, 我指的是应用程序中定义其当前行为和状态的所有属性. State is a single, 不可变数据结构-至少Ngrx为我们实现它的方式. Ngrx是一个基于RxJS的Angular应用状态管理库,受Redux的启发.”

Ngrx is inspired by Redux. Redux是一种管理应用程序状态的模式.“所以它更像是一组约定(对于那些听说过Ruby on Rails中的约定胜过配置的人来说), 稍后您将看到一些相似之处,这些相似之处使我们能够回答应用程序应该如何决定它需要显示一些界面元素(如可折叠的侧边栏),或者在它从服务器接收到会话状态后应该在哪里存储会话状态的问题.

让我们看看这是如何实现的. We talked about State 它的不变性,也就是说我们不能在创建它之后改变它的任何属性. 这使得几乎不可能将应用程序状态存储在 State. 但也不完全是——每一个状态都是不可变的,但是 Store,这是我们访问的方式 State,实际上是状态的一个可观察对象. So State 流中的单个值是 Store values. 为了改变应用的状态,我们需要做一些 ActionS会带走电流 State 然后换一个新的. 两者都是不可变的,但第二个是基于第一个的,所以不是在我们的 State,我们创造一个新的 State object. 为此,我们使用 Reducers as pure functions这意味着对于任何给定的 State and Action and its payload reducer, 它将返回与使用相同参数的任何其他reducer函数调用相同的状态.

Actions 由动作类型和可选负载组成:

导出接口动作{
  type: string;
  payload?: any;
}

对于我们的任务,让我们来看看添加新卡片的操作是如何进行的:

store.dispatch({
  type: 'ADD',
  有效载荷:“测试卡”
});

我们来看一个减速器:

export const cardsReducer = (state = [], action) => {
  switch(action.type) {
    case 'ADD':
      return {...state, cards: [...卡牌,新卡牌(动作.payload)]};
    default:
      return state;
  }
}

这个函数在每次新建时都被调用 Action event. We’ll cover Action 稍后调度. 现在,假设如果我们调度 ADD_CARD Action,它会进入case语句. 那里发生了什么?? 我们正在退货 State 根据我们之前的 State 通过使用TypeScript spread syntax,所以我们不需要用 Object.assign in most cases. 我们永远不应该在这些case语句之外改变我们的状态, 或者,当我们浪费时间寻找代码行为不可预测的原因时,它会让生活变得悲惨.

让我们将Ngrx添加到应用程序中. 为此,让我们在控制台中运行:

Yarn添加@ngrx/core @ngrx/store ngrx-store-logger
yarn add v1.3.2
[1/4]🔍解析包...
[2/4]🚚获取包...
[3/4]🔗链接依赖...
[...]
[4/4]📃构建新包...
成功保存lockfile.
保存了2个新的依赖项.
├─ @ngrx/core@1.2.0
└─@ngrx / store@4.1.1
└─ngrx-store-logger@0.2.0
✨  Done in 25.47s.

Now, add our Action definition (应用程序/操作/卡.ts):

import {Action} from '@ngrx/store';

export const ADD = '[纸牌]ADD ';

export const REMOVE = '[Cards] REMOVE ';

export类Add实现Action {
    只读类型= ADD;

    构造函数(公共负载:any) {}
}

导出类Remove实现动作{
    readonly type = REMOVE;

    构造函数(公共负载:any) {}
}

导出类型
  = Add
| Remove;

And our Reducer definition (应用程序/还原剂/卡.ts):

从'导入* as卡'../动作/卡”;
import {Card} from '../models/card';

导出接口状态{
    cards: Array;
}

const initialState:状态= {
    cards: []
}

导出函数reducer(state = initialState, action: cards).动作):状态{
    switch (action.type) {
      case cards.ADD:
        return {
            ...state, 
            cards: [...state.cards, action.payload]
        };
      case cards.REMOVE:
        Const index = state.cards.map((card) => card.$key).indexOf(action.payload);
        return {
            ...state, 
            cards: [...state.cards.slice(0, index), ...state.cards.slice(index+1)]
        };
      default:
        return state;
    }
}

在这里,我们可以看到如何使用spread和原生TypeScript函数,比如 map 将元素从列表中删除.

让我们更进一步,确保我们的应用程序状态将包含多个类型的数据, 我们从每一种单独的孤立状态组成它. 我们用 模块的决议 using (应用程序/还原剂/索引.ts):

从' '中导入*./cards';
进口{ActionReducer, ActionReducerMap, createFeatureSelector, createSelector, MetaReducer} from '@ngrx/store';
从'ngrx-store-logger'导入{storeLogger};
从“导入{环境}”../../环境/环境”;

导出接口状态{
    卡:fromCards.State;
}

export const reducers: ActionReducerMap = {
    卡:fromCards.reducer
}

export function logger(reducer: ActionReducer): any {
    //默认情况下,没有选项
    返回storeLogger()(减速器);
}

export const metaReducers: MetaReducer[] = !environment.production
  ? [logger]
  : [];

/**
 *卡片减少器
 */
export const getCardsState = createFeatureSelector('cards');
导出const getCards = createSelector
    getCardsState,
    state => state.cards
);  

我们还在开发环境中为我们的Ngrx包含了一个记录器,并为我们的卡数组创建了一个选择器函数. 让我们把它包含在 AppComponent:

从“@angular/core”中导入{Component};
导入{CardService}./services/card.service';
从'rxjs/Observable'中导入{Observable};
import {Card} from './models/card';
从根目录中导入*./reducers';
从'导入* as卡'./动作/卡”;
从'@ngrx/ Store '导入{Store};

@Component({
  选择器:“app-root”,
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
导出类AppComponent {
  public cards$: Observable;

  addCard(card: card) {
    this.store.调度(新卡片.AddCard(card));
  }

  constructor(private store: Store) {
    this.cards$ = this.store.select(fromRoot.getCards);
  }
}

现在,我们看看如何使用store分派动作. 但是这个代码仍然是不可用的, 因为我们没有将reducer (reducer和metaReducer)包含到我们的应用中. 我们通过改变我们的 AppModule:

[...]
从'@ngrx/store'导入{StoreModule};
导入{reducers, metreducers}./还原剂/指数”;
[...]
imports: [
[...]
  StoreModule.forRoot(reducers, {metreducers}),
[...]

现在它起作用了. Kind of. 记住,我们的应用中碰巧集成了Firebase. 现在,由于高度可维护的Ngrx存储,它丢失了. 也就是说,它没有存储在任何地方. 我们可以用 ngrx-store-localstorage 将我们的数据存储在浏览器的localStore中,但是如何使用api呢? 也许我们可以将之前的API集成添加到Reducer中? 但是我们不能,因为我们的减速器函数应该是一个纯函数. So, 对结果的求值不会导致任何语义上可观察到的副作用或输出, 例如可变对象的突变或输出到I/O设备……我们能做些什么呢? 答案在这个定义中是正确的. Ngrx的副作用 to the rescue.

Ngrx effects

那么副作用是什么呢? 这段代码捕获了我们的 Actions 和我们的减速机差不多, 但我们并没有改变我们的国家, 它们实际上发送API请求, on the result, dispatch new Actions. 一如既往,展示给你比告诉你简单. 让我们让我们的新配置支持Firebase. 为此,让我们安装 effects module:

添加@ngrx/effects
[...]
保存了1个新的依赖项.
└─@ngrx / effects@4.1.1
✨  Done in 11.28s.

现在我们将添加新的动作到我们的卡动作加载支持(src / app /行动/卡.ts):

[...]
export const LOAD = '[Cards] LOAD ';

export const LOAD_SUCCESS = '[Cards] Load Success';

导出const SERVER_FAILURE = '[纸牌]服务器故障';
[...]
导出类Load实现动作{
    readonly type = LOAD;
}

导出类LoadSuccess实现动作{
    readonly type = LOAD_SUCCESS;

    构造函数(公共负载:any) {}
}

导出类ServerFailure实现动作{
    只读类型= SERVER_FAILURE;

    构造函数(公共负载:any) {}
}
[...]
导出类型
[...]
    | Load
  | LoadSuccess
  | ServerFailure

我们有三个新的动作, 一个用于加载卡片列表,两个用于处理成功和不成功的响应. 让我们实现我们的效果(src /卡/ app /影响.ts):

从“@angular/core”中导入{Injectable};
从“@ngrx/effects”中导入{Actions, Effect};
导入{CardService}../services/card.service';
从'rxjs/observable/of'中导入{of};

从' '输入* as卡片../动作/卡”;

从'rxjs/operators'中导入{expirstmap, map, mergeMap, catchError}

@Injectable()
导出类cardseeffects {
    @Effect()
    loadCards$ = this.actions$
        .ofType(Cards.LOAD).pipe(
            mergeMap(action => {
                return this.cardService.getCardsList().pipe(
                map(res => new Cards.LoadSuccess (res)),
                catchError(error => of(new Cards.ServerFailure(错误))))}
            )
        );

    @Effect({调度:假})
    serverFailure$ =这个.actions$
        .ofType(Cards.SERVER_FAILURE).pipe(
        地图(动作:卡片.ServerFailure) => action.payload),
        exhaustMap(errors => {
            console.log('服务器错误发生',错误);
            return of(null);
        }));        

    constructor(
        private actions$: actions; 
        private cardService: cardService) {}
}

我们有可注入的cardseeffects,它使用 @Effect 装饰器,用于在我们的 Actions ,只过滤必要的操作 ofType operator. 您可以使用ofType来创建将在多个操作类型上触发的效果. 但是现在,我们只需要三个动作中的两个. For the Load action, 我们正在将每个动作转换为getCardList方法调用结果的新可观察对象. 在成功的情况下,可观察对象将被映射到一个新的动作 LoadSuccess 使用请求结果的有效负载,如果出现错误,我们将返回单个 ServerFailure 行动(注意 of 操作符——它将单个值或值数组转换为可观察对象).

所以我们的效果在制作一些依赖于外部系统(我们的Firebase)的东西后调度新的动作, to be precise). 但是在相同的代码中,我们看到了另一个效果,它处理 ServerFailure 使用decorator参数的操作 dispatch: false. What does this mean? 从它的实现中可以看到,它也映射了 ServerFailure 操作指向其有效负载,然后将此有效负载(我们的服务器错误)显示为 console.log. 显然,在这种情况下,我们不应该更改状态内容,因此我们不必分派任何东西. 这就是我们如何使它工作,而不需要任何空洞的行动.

那么,现在我们已经讨论了三个动作中的两个,让我们继续 LoadSuccess. 据我们目前所知, 我们正在下载的卡列表从服务器,我们需要合并到我们的 State. 所以我们需要将它添加到我们的减速器(src / app /还原剂/卡.ts):

[...]
switch (action.type) {
[...]
case cards.LOAD_SUCCESS:
        return {
            ...state,
            cards: [...state.cards, ...action.payload]
        }  
[...]

和之前一样, 我们使用扩展运算符在其中打开对象和卡片数组,并将其与来自服务器的扩展有效负载(卡片)连接起来, in our case). 让我们把新的Load动作添加到AppComponent中:

[...]
导出类AppComponent实现OnInit {
  public cards$: Observable;

  addCard(card: card) {
    this.store.调度(新卡片.AddCard(card));
  }

  constructor(private store: Store) {
  }

  ngOnInit() {
    this.store.调度(新卡片.Load());
    this.cards$ = this.store.select(fromRoot.getCards);
  }
}

这会从Firebase加载我们的卡. 让我们来看看浏览器:

卡片无法载入

有什么不对劲. 我们显然是在派遣行动, 从我们的日志中可以看出, 但是这里没有给我们的服务器请求. What’s wrong? 我们忘记把特效加载到AppModule了. Let’s do that:

[...]
从“@ngrx/effects”中导入{EffectsModule};
导入{cardseeffects}./effects/cards.effects';
[...]
imports: [
[...]
    EffectsModule.forRoot ([CardsEffects]),

现在,回到浏览器…

卡片正在从firebase加载

Now it’s working. 这就是如何将效果集成到从服务器加载数据中. 但我们仍然需要在创建卡片时把它发送回去. 让我们让它也起作用. 为此,让我们改变我们的 CardService createCard方法:

  createCard(card: card): card {
    Const result = this.cardsRef.push(card);
    card.$key = result.key;
    return card;
  }

然后为添加卡添加一个效果:

    @Effect()
    addCards$ = this.actions$
        .ofType(Cards.ADD).pipe(
            地图(动作:卡片.Add) => action.payload),
            exhaustMap(payload => {
              Const card = this.cardService.createCard(载荷);
                if (card.$key) {
                    新卡的归还.LoadSuccess([卡]));
                }
            })
        );

如果纸牌要创建,它会得到 $key 我们将把它合并到我们的卡数组中. 我们还需要移除 case cards.ADD: 从我们的减速器分支. 让我们在实际操作中尝试一下:

卡片是创建的,但文字不会改变

由于某种原因,我们在卡片添加操作中得到了重复的数据. 让我们试着找出原因. 如果我们仔细观察控制台,我们会看到两个 LoadSuccess 行动首先被分配给我们的新卡,就像它应该的那样, 另一个正在用我们俩的名片发送. 如果没有效果,我们的行动中它会被派往哪里?

我们对卡片的Load效果有这样的代码:

return this.cardService.getCardsList().pipe(
  map(res => new Cards.LoadSuccess (res)),

我们的getCardsList是可观察对象. 因此,当我们向卡集合中添加一张新卡时,它将输出. 要么我们不需要自己添加那张牌,要么我们需要使用a take(1) 管子里的操作员. 它会取一个值,然后退订. 但直播订阅似乎更合理(想必如此), 我们将在系统中有多个用户), 因此,让我们修改代码来处理订阅.

让我们在我们的效果中添加一个非调度元素:

@Effect({调度:假})
addCards$ = this.actions$
  .ofType(Cards.ADD).pipe(
    地图(动作:卡片.Add) => action.payload),
    exhaustMap(payload => {
      this.cardService.createCard(载荷);
      return of(null);
    })
  );

现在我们只需要更换减速器 LoadSuccess 替换卡片,而不是组合卡片:

case cards.LOAD_SUCCESS:
  return {
    ...state,
    cards: action.payload
  };

现在它正常工作了:

正确添加卡片

你可以实现 remove action the same way now. 当我们从订阅中获取数据时,您只需实现 Remove effect. 但我把这个问题留给你.

路由和模块

让我们来谈谈我们的应用程序组合. 如果我们需要一个 About 页面? 我们如何将其添加到当前的代码库中? 显然,页面应该是一个组件(就像Angular中的其他东西一样)。. 让我们生成那个组件.

关于——inline-template——inline-style的组件
[...]
  创建src / app /关于/.component.ts (266 bytes)
  src /应用程序/应用程序更新.module.ts (1503 bytes)

并添加下一个标记:

[...]
@Component({
  选择器:“app-about”,
  template: `

Cards App

`, [...]

现在,我们有了About页面. 我们如何访问它? 让我们添加一些代码到 AppModule:

[...]
导入{AboutComponent}./about/about.component';
导入{MainComponent}./main/main.component';
从“@angular/ Router”导入{Routes, RouterModule, Router};


const routes: routes = [
  {path: ", redirectTo: 'cards', pathMatch: 'full'},
  {path: 'cards', component: MainComponent},
  {path: 'about',组件:AboutComponent},
]

@NgModule({
  declarations: [
[...]
    AboutComponent,
    MainComponent,
  ],
  imports: [
[...]   
    RouterModule.forRoot(routes, {useHash: true})

What is MainComponent here? 现在,我们用同样的方法生成它 AboutComponent 我们稍后会填充它. 至于路线结构,它或多或少不言自明. 我们定义了两条路由: /cards and /about. 我们确保空路径重定向到 /cards.

现在让我们将卡片处理代码移动到 MainComponent:

从“@angular/core”中导入{Component, OnInit};
从'rxjs/Observable'中导入{Observable};
import {Card} from '../models/card';
从根目录中导入*../reducers';
从'导入* as卡'../动作/卡”;
从'@ngrx/ Store '导入{Store};


@Component({
  选择器:“app-main”,
  template: `
`, styles: [] }) 导出类MainComponent实现OnInit { public cards$: Observable; addCard(card: card) { this.store.调度(新卡片.Add(card)); } constructor(private store: Store) { } ngOnInit() { this.store.调度(新卡片.Load()); this.cards$ = this.store.select(fromRoot.getCards); } }

我们把它从 AppComponent:

从“@angular/core”中导入{Component, OnInit};

@Component({
  选择器:“app-root”,
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
导出类AppComponent实现OnInit {
  constructor() {
  }

  ngOnInit() {
  }
}

以及从标记:




正如你所看到的,我们添加了更多的东西. 首先,我们为 RouterLinkActive,它在我们的路由处于活动状态时设置一个类 routerLink, which replaces href for us. And here is routerOutlet, which tells Router 在当前页面的何处显示其内容. So, combining those, 现在每一页都有菜单, 还有两个不同内容的页面:

根据命令取出卡片

欲了解更多详情,请参阅 Router Guide.

随着应用程序的增长,我们可能会开始考虑优化. For example, 如果我们希望默认加载About组件,并且只在用户通过点击Cards链接隐式请求后才加载其他组件,该怎么办. 为此,我们可以使用模块的延迟加载. 让我们从生成开始 CardsModule:

Ng模块卡——平面
  创建src / app /卡.module.ts (189 bytes)

By using the flat 标志,我们告诉Angular不要为我们的模块创建单独的目录. 让我们将所有与卡片相关的内容转移到我们的新模块中:

从“@angular/core”中导入{NgModule};
从“@angular/common”中导入{CommonModule};
导入{CardService}./services/card.service';
导入{CardComponent}./card/card.component';
导入{CardListComponent}.贺卡/贺卡.component';
导入{NewCardInputComponent}./ new-card-input new-card-input.component';

从“@angular/forms”中导入{FormsModule, ReactiveFormsModule};

从'angularfire2'中导入{AngularFireModule};
从'angularfire2/database'导入{AngularFireDatabaseModule};
从'angularfire2/auth'导入{AngularFireAuthModule};

从'@ngrx/store'导入{StoreModule};
从“@ngrx/effects”中导入{EffectsModule};
导入{reducers}./reducers';
导入{cardseeffects}./effects/cards.effects';

从“导入{环境}”./../环境/环境”;
导入{MainComponent}./main/main.component';

从“@angular/ Router”导入{Routes, RouterModule, Router};

const routes: routes = [
  {path: ", redirectTo: 'cards', pathMatch: 'full'},
  {path: 'cards', component: MainComponent},
]

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    StoreModule.forFeature(“卡片”,异径接头),
    EffectsModule.forFeature ([CardsEffects]),
    RouterModule.forChild(路线),
    AngularFireModule.initializeApp(环境.firebase),
    AngularFireDatabaseModule,
    AngularFireAuthModule,
  ],
  提供者:[CardService],
  declarations: [
    CardComponent,
    CardListComponent,
    NewCardInputComponent,
    MainComponent
  ]
})
导出类CardsModule {}

之前,我们看到了很多 forRoot 调用在我们的导入中,但是在这里,我们调用了很多 forFeature or forChild. 这就是我们告诉组件我们扩展了配置的方式, 而不是从头开始创建.

让我们看看还有什么 AppModule:

[...]
导入{reducers, metreducers}./reducers/root';

const routes: routes = [
  {path: ", redirectTo: 'about', pathMatch: 'full'},
  {path: 'about',组件:AboutComponent},
  {path: 'cards', loadChildren: './cards.模块# CardsModule '}
]

@NgModule({
  declarations: [
    AppComponent,
    AboutComponent,
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot(routes, {useHash: true}),
    StoreModule.forRoot(reducers, {metreducers}),
    EffectsModule.forRoot([]),
  ],
  
  引导(AppComponent):
})
导出类AppModule {}

这里,我们仍然定义 EffectsModule.forRoot 或者它不会在我们加载的模块中工作(因为它将无处添加惰性加载). 这里我们还看到了路由器的新语法 loadChildren 这告诉我们的路由器延迟加载 CardsModule located in the ./cards.module 文件时,我们要求的 cards route. 我们还包括了来自new的meta reducer ./reducers/root.ts 文件-让我们看一下:

进口{ActionReducer, ActionReducerMap, createFeatureSelector, createSelector, MetaReducer} from '@ngrx/store';
从'ngrx-store-logger'导入{storeLogger};
从“导入{环境}”../../环境/环境”;

导出接口状态{
}

export const reducers: ActionReducerMap = {
}

export function logger(reducer: ActionReducer): any {
    //默认情况下,没有选项
    返回storeLogger()(减速器);
}

export const metaReducers: MetaReducer[] = !environment.production
  ? [logger]
  : [];

On a root level, 我们目前没有任何状态, 但是我们仍然需要定义空状态,以便在惰性加载过程中扩展它. 这也意味着我们的卡牌状态必须在其他地方定义, 对于这个例子, we define it in src / app /还原剂/索引.ts:

从' '中导入*./cards';
进口{ActionReducer, ActionReducerMap, createFeatureSelector, createSelector, MetaReducer} from '@ngrx/store';
从'ngrx-store-logger'导入{storeLogger};
从“导入{环境}”../../环境/环境”;
从根目录中导入*./root';

导出接口卡状态{
    卡:fromCards.State;
}

导出接口状态从root扩展.State {
  卡:CardsState;
}

导出const reducers = {
    卡:fromCards.reducer
}

/**
 *卡片减少器
 */
export const getCardsState = createFeatureSelector('cards');
导出const getCards = createSelector
    getCardsState,
    state => state.cards.cards
);  

我们通过cardkey扩展根状态. 这给了我们在最后的键嵌套重复(作为一个模块和一个数组) cards).

如果我们现在打开应用,查看开发者控制台的网络选项卡,我们会看到这个 cards.module.chunk.js 只在我们点击 /cards link.

准备生产

让我们构建应用程序以供生产使用. 为此,我们运行 build command:

build - not -prod
 65%的建筑模块465/466模块1激活 ...g / getting-started-ng5 / src /风格.已弃用scssnode# moveTo. 使用容器#附加.
日期:2018 - 01 - 09年t22:14:59.803Z
散列:d11fb9d870229fa05b2d
Time: 43464ms
chunk {0} 0.657年b0d0ea895bd46a047.chunk.js () 427 kB[已呈现]
块{1}填充.fca27ddf9647d9c26040.bundle.Js (polyfills.9kb[首字母][渲染]
chunk {2} main.5 e577f3b7b05660215d6.bundle.js (main) 279 kB[初始][渲染]
chunk {3} styles.e5d5ef7041b9b072ef05.bundle.css(样式)136kb [initial][渲染]
chunk {4} inline.1 d85c373f8734db7f8d6.bundle.js (inline) 1.47kb[条目][渲染]

这里发生了什么? 我们正在将应用程序构建为静态资产,可以从任何web服务器(如果您想从子目录提供服务)提供服务 ng build我有选择权 --base-href ). By using -prod,我们告诉AngularCLI我们需要生产版本. And --aot 告诉它我们喜欢有吗 提前编译. 在大多数情况下,我们更喜欢这样,因为它允许我们获得更小的包和更快的代码. Also, 请记住,AoT对代码质量的要求过于严格, 所以它可能会产生你以前没见过的错误. 尽早运行构建,以便更容易修复.

I18n

构建应用的另一个原因是Angular的处理方式 i18n 或者,简单地说,国际化. Angular不是在运行时处理它,而是在编译时处理它. 让我们为我们的应用配置它. 为此,让我们将i18n属性添加到AboutComponent中.

Cards App

通过这样做,我们告诉Angular编译器,标签的内容需要被翻译. 它不是Angular指令, 在编译过程中,它被编译器删除,取而代之的是对给定语言的翻译. 所以我们标记了第一个翻译的信息,接下来呢? 我们怎么翻译呢? 为此,Angular为我们提供了 ng xi18n command:

ng xi18n
cat src/messages.xlf


  
    
      
        Cards App
        
          app/about/about.component.ts
          3
        
      
    























  

因此,我们有一个翻译文件,将我们的消息映射到源代码中的实际位置. 现在,我们可以把文件给 Phrase. 或者,我们可以手动添加翻译. 为此,让我们在src中创建一个新文件, messages.ru.xlf:



  
    
      
        Cards App
        Картотека
      
  























  

现在我们可以通过运行这个命令来服务我们的应用程序(例如,用俄语) Ng serve——aot——locale=ru——i18n-file=src/messages.ru.xlf. 让我们看看它是否有效:

在俄语应用中服务

Now, 让我们自动化构建脚本,这样我们就可以在每个产品构建中使用两种语言构建应用程序,并将其对应的目录称为en或ru. 为此,我们将build-i18n命令添加到 scripts section of our package.json:

 "build-i18n": "for lang in en ru; do yarn run ng build --output-path=dist/$lang --aot -prod --bh /$lang/ --i18n-file=src/messages.$lang.xlf --i18n-format=xlf --locale=$lang --missing-translation=warning; done"

Docker

现在让我们将应用打包以供生产环境使用,并使用Docker. Let’s start with Dockerfile:

####阶段1:构建###
##我们将舞台标记为“建造者”
FROM node:8.6-alpine作为建筑师

ENV APP_PATH /app
MAINTAINER Sergey Moiseev 

COPY package.json .
COPY yarn.lock .

将节点模块存储在单独的层上可以防止在每次构建时安装不必要的npm
运行纱线安装——生产 && Yarn全局添加gulp && mkdir $APP_PATH && cp -R ./node_modules .$APP_PATH

WORKDIR APP_PATH美元

COPY . .

###在生产模式下构建angular应用,并将构件保存在dist文件夹中
运行纱线,去除节点 && 添加node-sass && 纱线运行build-i18n && 纱线运行时,压缩

####第二阶段:设置###
FROM nginx:1.13.3-alpine

ENV APP_PATH /app
MAINTAINER Sergey Moiseev 

复制我们默认的nginx配置
执行rm -rf /etc/nginx/conf命令.d/*
复制nginx /违约.参看/etc/nginx/conf.d/

删除默认的nginx网站
执行rm -rf /usr/share/nginx/html/*

EXPOSE 80

从'builder'阶段复制dist文件夹中的工件到默认的nginx公共文件夹
COPY——from=builder $APP_PATH/dist/ usr/share/nginx/html/

CMD ["nginx", "-g", "daemon off;"]

因此,我们使用基于node的图像为我们的应用程序使用多级构建, 然后我们用一个基于nginx的映像构建服务器包. 我们还使用Gulp来压缩我们的工件,就像Angular CLI一样 不再为我们做了. 我觉得这很奇怪,但是好吧,让我们添加Gulp和压缩脚本.

yarn add gulp@3.9.1 gulp-zip@4.1.0 --dev
[...]
保存了2个新的依赖项.
├─ gulp-zip@4.1.0
└─ gulp@3.9.1
✨  Done in 10.48s.

Lets add gulpfile.js in our app root:

Const gulp = require('gulp');
Const zip = require('gulp-gzip');

gulp.任务('压缩',函数(){
    For (var lang in ['en', 'ru']) {
        gulp.src([`./dist/${lang}/*.js`, `./dist/${lang}/*.css`])
        .pipe(zip())
        .pipe(gulp.dest(`./ dist / ${朗}/ '));
    }
});

现在我们只需要Nginx配置来构建我们的容器. Let’s add it to nginx/default.conf:

server {
  listen 80;

  sendfile on;

  default_type应用程序/八进制;

  client_max_body_size 16米;

  gzip on;
  gzip_disable“msie6”;

  gzip_vary on;
  gzip_proxied任何;
  gzip_comp_level 6;
  Gzip_buffers 16 8k;
  gzip_http_version 1.0; # This allow us to gzip on nginx2nginx upstream.
  gzip_min_length 256;
  Gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.Ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;

  根/usr/share/nginx/html;

  location ~* \.(js|css)$ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  位置~ ^/(en|ru)/ {
    Try_files $uri $uri/ /索引.html =404;
  }

  location = / {
     return 301 /en/;
   }
}

因此,我们从目录中提供构建应用程序 en or ru 默认情况下,我们将从根URL重定向到 /en/.

方法来构建应用程序 Docker build -t app . command:

Docker build -t app .
向Docker守护进程发送构建上下文347MB
步骤1/17:FROM节点:8.6-alpine作为建筑师
 ---> b7e15c83cdaf
步骤2/17:ENV APP_PATH /app
[...]
移除中间容器1ef1d5b8d86b
成功构建db57c0948f1e
成功标记app:latest

然后我们可以在本地机器上使用Docker运行 Docker运行-p 80:80应用程序. 这是有效的:

It works! 本Angular 5教程现在已经完成了

Mind the /en/ in URL.

Summary

祝贺您完成了本教程. 你现在可以加入其他人的行列了 角的开发人员. 你刚刚创建了你的第一个Angular应用, 使用Firebase作为后端,并通过Nginx在Docker容器中提供服务.

对于任何新的框架,要想精通它,唯一的方法就是不断地练习. 希望你已经理解了Angular有多么强大. 当你准备好继续的时候 角的文档 是一个很棒的资源,并且有一整个部分是关于高级技术的.

如果你想做一些更高级的事情,试试吧 使用Angular 4表单:嵌套和输入验证 他的同事Igor Geshoki.

了解基本知识

  • 我们为什么要使用Angular?

    我们用它来开发富界面的客户端应用程序,比如单页应用程序和移动应用程序. Angular的主要优势在于,它是一个完全集成的web框架,为构建组件提供了自己的收件箱解决方案, routing, 并使用远程api.

  • 使用Angular的优势是什么?

    使用Angular的主要优势在于,它是一个完全集成的web框架,为构建组件提供了自己的收件箱解决方案, routing, 并使用远程api.

  • 模块在Angular中是如何工作的?

    模块中声明的作用域是分开的. 这样我们就可以为我们的应用构建多个独立的模块,并对这些模块使用延迟加载. 模块的目的是声明该模块中使用的所有内容,并允许Angular提前对其进行编译.

  • Angular是基于mvc的吗?

    与AngularJS不同,Angular不再是一个MVC框架. 它是一个基于组件的框架. 组件在这里扮演了控制器的角色,但只是在一个非常简化的抽象级别上.

  • What is RxJS?

    RxJS是JavaScript的响应式扩展库, 它允许我们操作可观察对象, 哪些事件流取代了我们的独立承诺.

  • What is NgRX?

    NgRX是一种管理应用程序状态的模式. 它是一个基于rxjs的Angular应用状态管理库. 它允许我们有一个单一的应用程序状态,将所有组件连接在一起,并为我们的应用程序提供可预测和一致的行为.

聘请Toptal这方面的专家.
Hire Now
谢尔盖·莫伊谢耶夫的头像
Sergey Moiseev

Located in Tallinn, Estonia

Member since May 9, 2014

作者简介

谢尔盖是一名具有开发复杂web项目经验的全栈软件工程师. 作为一名分析师,他拥有丰富的技能.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

giftly

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal开发者

Join the Toptal® community.