十二、Angular 2 的新特性

Angular 1 基于 MVC 架构,而 Angular 2 基于组件和服务架构。Angular 1 和 Angular 2 在架构和 API 方面完全不同,所以之前对 Angular 1 的了解不太可能对你学习 Angular 2 有太大帮助。在本章中,我们将学习 Angular 2,但不会将其与 Angular 1 进行比较,因为这样做会造成混乱,而且没有必要。即使你没有关于 Angular 1 的知识,也可以继续这一章。

我们将涵盖以下主题:

  • Web 组件
  • Angular 2 架构
  • 模板语言
  • 组件输入和输出
  • 组件生命周期
  • 事件
  • 形式
  • 服务

还有更多...

Angular 2 架构

Angular 2 是一个基于服务和组件架构构建 web 应用客户端的框架。

Angular 2 应用由一个视图和各种服务组成。服务是保存应用逻辑和状态的简单 JavaScript 对象。服务应该是可重用的。视图消费服务,服务也可以相互交互。

视图和服务是松散耦合的,因此 Angular 2 视图可以用于任何其他体系结构,例如 Flux。同样,服务可以与任何其他视图一起使用,例如 React。

Angular 2 视图基于面向组件的架构。在面向组件的体系结构中,应用用户界面被分成可重用的组件。组件有一个用户界面,其中包含更新用户界面和处理用户操作的代码。自定义标记与组件相关联,每当自定义标记出现时,就会创建并呈现组件的新实例。因此,我们可以说面向组件的架构是应用视图的架构。实际上,组件使用服务。

在前两章中,我们研究了 React,它也是基于面向组件的体系结构,因为使用 React,我们将应用构建为一组组件。

下面是来自官方 Angular 2 网站( https://angular.io 的图,展示了 Angular 2 的完整架构:

The Angular 2 architecture

在这里,您可以看到组件的用户界面是使用模板定义的。模板是使用模板 HTML 编写的,即 HTML 和许多其他标记的组合。组件还保存用户界面的状态和事件处理程序。

我们不应该在组件中存储应用逻辑和状态,因为这会影响代码的可重用性,并在开发大型复杂应用时引发问题。应用状态和逻辑应该存储在服务中。

Angular 2 仅实现单向数据绑定。这使得大型复杂的应用更容易调试。

服务被注入到需要它们的特定组件中,而不是所有的组件中。

引入 web 组件

在我们进入网络组件之前,你需要知道我们为什么要学习它们。我们正在学习网络组件,因为 Angular 2 组件利用了阴影 DOM 和模板,它们是网络组件的一部分。

简而言之,web 组件是四种不同浏览器规范的集合,它们支持在网页中创建可重用的组件。这四个规格分别是 HTML 导入阴影 DOM模板自定义元素。它们可以一起使用,也可以分开使用。

Web 组件提供了面向组件架构的本地实现。使用 web 组件创建的组件也称为 web 组件。

在我们了解 web 组件之前,让我们考虑一个用于演示目的的项目。创建一个名为web-components的目录,然后在其中创建一个名为index.html的文件。网页组件的浏览器支持很差,所以让我们下载webcomponents.js polyfill。从https://github.com/webcomponents/webcomponentsjs下载文件,放入web-components目录。

现在,将此代码放入index.html文件:

<!doctype html>
<html>
  <head>
    <title>Web Components Demo</title>
    <script src="webcomponents.js"></script>
  </head>
  <body>
    <script>
      //place JavaScript code here
    </script>
  </body>
</html>

现在,让我们通过构建一个组件来显示一个包含图像、标题和描述的卡片,从而了解阴影 DOM、模板和自定义元素的概述。

模板

模板是用来定义可重用代码的。使用<template>标签定义模板。模板的代码放在这个标签中。我们可以放置任何标签,如<script><style>

<template>标签内的代码只被解析,不被渲染。

下面是一个如何创建模板的示例。将该代码置于body标签中:

<template id="cardTemplate">
  <style type="text/css">
    .container
    {
      width: 250px;
      float: left;
      margin-right: 10px;
    }

    img
    {
      width: 100%;
    }
  </style>
  <div class="container">
    <img src="" />
    <div>
      <h3></h3>
      <p></p>
    </div>
  </div>
</template>

这里,模板保存了卡组件的用户界面代码。现在,如果你在浏览器中打开index.html文件,你将看不到任何东西,因为<template>标签只是被解析,而不是被渲染。

自定义元素

定制元素让让我们定义新类型的 HTML 元素(也就是新类型的 HTML 标签)。当我们使用浏览器无法识别的标签名称时,浏览器只是将其视为<span>标签。但是当我们注册一个custom标签时,它会被浏览器识别。它可以继承其他元素,让我们在元素生命周期的不同阶段执行不同的操作,等等。

让我们为组件创建一个custom元素。无论标签出现在哪里,都会显示该组件的新实例。

下面是显示custom元素的代码。将其放入<body>标签中:

<custom-card data-img="http://placehold.it/250x250" data-title="Title 1" data-description="Description 1" is="custom-card"></custom-card>
<custom-card data-img="http://placehold.it/250x250" data-
title="Title 2" data-description="Description 2"></custom-card>

我们必须在自定义元素名称中使用-字符。这是强制性的,因为这种限制允许解析器区分自定义元素和常规元素,并确保在向 HTML 添加新标签时向前兼容。这里,我们将组件的属性作为数据属性传递。

现在,让我们将<custom-card>定义为一个自定义元素,并在创建<custom-card>的新实例时将模板代码放入标签中。为此,请将此代码放在<script>标签中:

var customCardProto = Object.create(HTMLElement.prototype);
customCardProto.createdCallback = function(){
  var template = document.querySelector("#cardTemplate");
  template.content.querySelector("img").src = this.getAttribute("data-img");
  template.content.querySelector("h3").innerHTML = this.getAttribute("data-title");
  template.content.querySelector("p").innerHTML = this.getAttribute("data-description");

  var clone = document.importNode(template.content, true);
  this.appendChild(clone)
}
var customCard = document.registerElement("custom-card", {
  prototype: customCardProto
});

下面是代码的工作原理:

  • 默认情况下,自定义元素继承HTMLElement的方法和属性。
  • 要注册一个自定义元素,我们需要使用document.registerElement方法。第一个参数是自定义标记名,第二个参数是可选对象。这个可选对象可以取一个属性叫做原型prototype属性定义了它所继承的 HTML 元素,也就是它所继承的 HTML 元素的属性和方法。默认情况下,它被分配给Object.create(HTMLElement.prototype)
  • 我们还可以通过向分配给prototype属性的对象添加新的属性和方法,向自定义元素添加新的属性和方法。
  • 在这里,我们添加了一个名为createdCallback的方法,每当创建一个自定义元素的实例时,即使用 JavaScript 或 HTML 创建的实例时,都会调用该方法。
  • createdCallback内部,我们正在检索我们的模板并设置图像源、标题和描述,然后通过创建它的克隆将其附加到自定义元素,因为许多自定义元素将共享相同的模板。

现在,如果您在浏览器中打开index.html,您将看到以下输出:

Custom elements

影子天赋

阴影 DOM 允许 HTML 元素获得一种新的 Node,称为与其关联的阴影根。有关联的阴影根的元素称为阴影宿主。不呈现阴影宿主的内容;而是呈现阴影根的内容。一个阴影根下面可以有另一个阴影根。

阴影 DOM 的好处是在阴影根内部定义的 CSS 样式不会影响其父文档,在阴影根外部定义的 CSS 样式不会影响阴影根内部的元素。这对于定义特定于组件的样式很有用。简而言之,我们可以说影子 DOM 提供了样式封装。

样式封装并不是影子 DOM 的唯一好处。影子根中的 HTML 受到保护,不会被 JavaScript 意外修改。我们仍然可以在浏览器开发工具中检查影子根。

很多原生元素,比如<video><audio>都有一个影根,但是你检查的时候就看不到影根了。默认情况下,浏览器隐藏这些元素的阴影根。要查看它们的阴影根,您需要更改浏览器特定的设置。

让我们修改之前的自定义元素代码来渲染阴影 DOM 中的模板。用这个方法代替之前的createdCallback方法:

customCardProto.createdCallback = function(){
  var template = document.querySelector("#cardTemplate");
  template.content.querySelector("img").src = this.getAttribute("data-img");
  template.content.querySelector("h3").innerHTML = this.getAttribute("data-title");
  template.content.querySelector("p").innerHTML = this.getAttribute("data-description");

  var clone = document.importNode(template.content, true);

  var shadow = this.createShadowRoot();

  shadow.appendChild(clone);
}

这里,我们没有将模板代码直接追加到自定义元素中,而是使用createShadowRoot创建了一个阴影根,并将模板代码追加到其中。

设置 Angular 2 项目

Angular 2 代码可以用 JavaScript、TypeScript 或 Dart 编写。如果您正在用 TypeScript 或 Dart 编写 Angular 2 代码,那么在提供给客户端之前,您需要将代码转换成 JavaScript。我们将使用 JavaScript 编写 Angular 2 代码。

创建一个名为angular2-demo的目录。然后,在目录中创建app.jspackage.json文件。然后,创建一个名为public的目录,在该目录中,再创建四个名为htmljscomponentTemplatescomponentStyles的目录。现在,创建一个名为index.html的文件,并将其放入html目录中。

然后从https://cdnjs.com/libraries/angular.js/下载angular2-polyfills.js``Rx.umd.jsangular2-all.umd.js放入angular2-demo/js目录。这些文件听起来是这样的。如果您愿意,也可以直接将 CDN 链接入队。

index.html文件中,放置这个起始代码:

<!doctype html>
<html>
  <head>
    <title>Angular 2 Demo</title>
  </head>
  <body>

    <script src="/js/angular2-polyfills.js"></script>
    <script src="/js/Rx.umd. js"></script>
    <script src="/js/angular2-all.umdn.js"></script>
    <script>
      //App code here
    </script>
  </body>
</html>

app.js文件中,放置此代码:

var express = require("express");
var app = express();

app.use(express.static(__dirname + "/public"));

app.get("/", function(httpRequest, httpResponse, next){
  httpResponse.sendFile(__dirname + "/public/html/index.html");
})

app.listen(8080);

这是服务器端代码。不言自明。

现在,在package.json文件中,放置该代码并运行npm install以下载express包:

{
  "name": "Angular2-Demo",
  "dependencies": {
    "express": "4.13.3"
  }
}

要启动服务器,运行node app.js。然后,在浏览器中使用localhost:8080作为地址打开应用。

角度 2 基础

Angular 2 应用被完全拆分成组件。从技术上来说,Angular 2 组件是一个可重用的custom标签,它是可变的,并封装了一个嵌入的状态,也就是说,对状态或属性的更改会改变用户界面。

请记住,Angular 2 不会将自定义标记名称注册为自定义元素。

应用的所有组件都以树形结构排列,其中一个组件作为根 Node。

下面是一个如何创建组件的示例。它创建了一个显示图像、标题和描述的卡片组件。将该代码置于<script>标签中:

var Card = ng.core.Component({
  selector: "card",
  inputs: ["src", "title", "desc"],
  templateUrl: "templates/card-template.html",
  styleUrls: ["templateStyles/card-style.css"]
})
.Class({
  constructor: function(){
  }
})

然后,创建一个名为card-template.html的文件,并将其放入componentTemplates目录中。将此代码放入文件中:

<style>
  .container
  {
    width: 250px;
    float: left;
    margin-right: 10px;
  }

  img
  {
    width: 100%;
  }
</style>
<div class="container">
  <img src="{{src}}" />
  <div>
    <h3>{{title}}</h3>
    <p>{{desc}}</p>
  </div>
</div>

之后,创建一个名为card-style.css的文件,并将其放入componentStyles目录中。将此代码放入文件中:

.container
{
  width: 250px;
  float: left;
  margin-right: 10px;
}

img
{
  width: 100%;
}

这就是这三个代码片段的工作原理:

  • 一个组件需要通过链接属于一个ng.core对象的ComponentClass方法来创建。
  • Component方法采用具有各种属性的配置对象,而Class方法采用具有组件生命周期方法、构造函数和用户界面操作处理程序的对象。
  • 这里,我们提供的配置属性是selectorinputstemplateUrlstyleUrlsselector属性用于定义组件的自定义标签。inputs属性用于定义自定义标签采用的属性。templateUrl属性用于定义包含组件模板的文件。如果想内嵌模板代码,也可以使用template。最后,styleUrls用于定义包含组件样式的 CSS 文件。您也可以使用styles属性来内联 CSS 代码,或者您可以使用模板本身内部的<style>标签来定义 CSS。以这三种方式中的任何一种定义的 CSS 都不会影响其他组件,也就是说,它被封装到组件本身。
  • Class方法中,我们将不得不提供constructor方法,即使它什么也不做。它在组件的新实例的构建过程中被调用。组件的构造,我指的是组件在内存中的构造——不是解析属性、解析其子组件、呈现视图等等。constructor方法的主要用途是将服务注入组件。服务不能自动注入,因为我们有时可能需要为每个组件初始化服务,Angular 不知道如何做到这一点。constructor方法可以访问组件的状态,但不能访问其属性。在这里,我们不应该做任何繁重的工作或其他会减慢或导致组件构建失败的事情。constructor不是一个组件生命周期方法。
  • 然后,我们有了组件模板代码。在这个模板文件中,我们只是呈现传递给组件的属性。为了渲染任何处于组件状态的东西,我们需要使用{{}}标记。

让我们创建另一个名为Cards的组件,它显示一个卡片列表。它从服务中获取关于卡片的信息。

将该代码放入index.html文件的<script>标签中:

var CardsService = ng.core.Class({
  constructor: function() {
  },
  getCards: function() {
    return [{
      src: "http://placehold.it/350x150",
      title: "Title 1",
      desc: "Description 1"
    },
    {
      src: "http://placehold.it/350x150",
      title: "Title 2",
      desc: "Description 2"
    },
    {
      src: "http://placehold.it/350x150",
      title: "Title 3",
      desc: "Description 3"
    }]
  }
});

var Cards = ng.core.Component({
  selector: "cards",
  viewProviders: [CardsService],
  directives: [Card],
  templateUrl: "componentTemplates/cards-template.html"
}).Class({
  constructor: [CardsService, function(cardsService){
    this.getCards = cardsService.getCards;
}],
  ngOnInit: function(){
    this.cards = this.getCards();
  }
})

var App = ng.core.Component({
  selector: "app",
  directives: [Cards],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){

  }
})

ng.platform.browser.bootstrap(App);

现在,在componentTemplates目录中创建一个名为cards-template.html的文件,并将该代码放入其中:

<card *ngFor="#card of cards" title="{{card.title}}" src="{{card.src}}" desc="{{card.desc}}"></card>

现在,在componentTemplates目录中创建一个名为app-template.html的文件,并将该代码放入其中:

<cards></cards>

现在,在index.html文件的<body>标签中,放置以下代码:

<app></app>

以下是这四个代码片段的工作原理:

  • 要创建服务,我们需要使用ng.core.Class方法。它采用带有constructor方法和服务公开的其他方法或属性的对象。将服务注入其他服务或组件时,会创建并注入服务的新实例。创建服务的新实例时,调用constructor方法。我们必须提供这种方法,即使它不起任何作用。该方法的主要目的是注入该服务所依赖的服务。这里,我们的CardsService方法不依赖于任何其他服务,因此我们在constructor方法中没有代码。然后,我们定义了一个getCards方法,返回三张不同卡片的数据进行显示。
  • 然后,我们创建了一个Cards组件。它从CardsService获取数据,并为每个卡片数据渲染一个Card组件。创建Cards组件时,我们为配置对象提供viewProvidersdirectives属性。viewProviders是该组件依赖的服务列表,directives是该组件呈现的其他组件列表。在这里,您可以看到,我们不是直接将函数分配给constructor属性,而是分配一个包含组件所依赖的服务列表的数组,最后一个数组项作为实际函数。这是将服务注入组件的格式。在constructor方法内部,我们存储对组件需要的服务的方法或属性的引用,也就是说,我们可以在constructor方法内部使用服务。稍后我们将了解更多关于viewProviders的信息。传递给Class方法的任何方法中的this关键字都指向组件的状态。创建组件实例后,只要组件状态发生变化,模板绑定就会更新。我们这里还有一个方法,叫做ngOnInit。这是一种生命周期方法,在组件的新实例创建并解析其属性后调用。在这里,我们调用getCards方法,并将返回值存储在状态的cards属性中。请注意,在创建组件实例后,可以使用this关键字访问传递给组件标签的属性。
  • CardsComponent的模板内部,我们使用*ngFor指令来显示卡片。稍后我们将了解更多关于指令的信息。
  • 然后,我们创建一个App组件,作为我们组件的根。在这个组件中,我们显示的是Cards组件。
  • 最后,我们初始化应用。Angular 2 应用被显式初始化。初始化它时,我们需要提供对根组件的引用。这样做是为了确保应用始终由嵌套组件组成。根组件是添加到<body>标签的组件。将其他组件的标签添加到主体标签中不会有任何作用。

现在,如果您在浏览器中刷新您的localhost:8080页面,您将看到以下输出:

Angular 2 fundamentals

造型组件和阴影 DOM

前面我们看到有三种方式定义特定于组件的样式(封装在组件模板范围内的样式)。组件的 CSS 甚至不会影响它所拥有的组件。

Angular 2 默认不使用阴影 DOM 相反,它使用不同的技术来实现样式封装。这是由于缺乏浏览器支持。

默认情况下,Angular 2 修改 CSS 选择器的方式是只针对组件中的元素,然后将 CSS 放在页面的<head>标签中。如果您使用浏览器开发工具检查我们当前的应用,您将看到以下内容:

Styling components and shadow DOM

在这里可以看到 CSS 已经修改并插入<head>标签。

要强制 Angular 2 使用阴影 DOM,我们需要将组件配置对象的封装属性赋给ng.core.ViewEncapsulation.Native。默认情况下,它被分配给ng.core.ViewEncapsulation.Emulated

当将CardCards组件的封装属性分配给ng.core.ViewEncapsulation.Native后检查 app 时,会看到如下内容:

Styling components and shadow DOM

在这里,您可以看到影子 DOM 用于实现样式封装。

如果不想对组件进行样式封装,可以将封装属性分配给ng.core.ViewEncapsulation.None。在这种情况下,所有的 CSS 都将直接放置在<head>标签中。

角度 2 变化检测

变化检测 是检测元件状态变化的过程。使用this关键字存储和操作组件的状态。因此无法直接让 Angular 2 检测到状态何时变化。因此,Angular 2 使用复杂的算法和第三方库来检测状态变化。

Angular 2 检测状态变化的第一件事是假装所有的变化都是异步发生的。然后,它使用zone.js库来监控浏览器事件、计时器、AJAX 请求、网络套接字以及zone.js支持的其他异步事物。

现在,每当这些异步活动发生时,它都会检查所有可能改变的东西,包括对象属性和来自根 Node 的所有组件的this关键字的数组元素;如果检测到任何变化,那么组件的模板绑定将被更新。Angular 2 不会简单地重新渲染整个组件。相反,它会检查已更改的绑定,并专门选择和更新它们。

有些组件可能有很多状态数据,如果它们的状态没有改变,那么检查每个异步操作的状态将会不必要地影响应用的性能。因此,Angular 2 提供了一个选项来标记这种类型的组件,这样它就不会检查它们的状态,除非组件本身告诉 Angular 2 在下一个检测周期期间检查它的状态,也就是说,当下一个异步活动发生时。让我们看一个例子来证明这一点。

将该代码置于index.html文件的<script>标签中的App组件代码上方:

var SampleComponent1 = ng.core.Component({
  selector: "sampleone",
  template: "{{value}}",
  viewProviders: [ng.core.ChangeDetectorRef],
  changeDetection: ng.core.ChangeDetectionStrategy.Detached
}).Class({
  constructor: [ng.core.ChangeDetectorRef, function(cd){
    this.cd = cd;
  }],
  ngOnInit: function(){
    this.value = 1;
    setInterval(function(){
      this.value++;
      this.cd.markForCheck();
    }.bind(this), 2000)
  }
})

然后,将SampleComponent1添加到App组件的directives数组中。所以现在,App组件的代码应该是这样的:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){
  }
})

现在,将这个代码添加到app-template.html文件的末尾:

<br style="clear: both">
<sampleone></sampleone>

以下是这三个代码片段的工作原理:

  1. 在这个例子中,我们显示了一个每 2 秒增加一次的值,模板被重新渲染以显示更新的值。
  2. 首先,我们创建一个名为SampleComponent1的组件。它只是显示value。我们已经将changeDetection属性设置为ng.core.ChangeDetectionStrategy.Detached,这告诉 Angular 2 不要检查其状态变化。默认情况下,changeDetection属性被分配给ng.core.ChangeDetectionStrategy.Default,它告诉 Angular 2 在每个变化检测周期检查其状态变化。然后我们将ng.core.ChangeDetectorRef服务注入到组件中,该组件提供与变更检测相关的各种 API。然后,在ngOnInit方法中,我们每 2 秒增加value的值,之后我们调用ng.core.ChangeDetectorRefmarkForCheck方法,该方法告诉 Angular 2 在下一个变化检测周期中检查组件的状态变化。markForCheck将仅在下一个检测周期进行 Angular 2 状态变化检查,而不是之后的检测周期。
  3. 然后,我们只需在App组件中显示SampleComponent1

如果一个组件只依赖于它的输入和/或用户界面事件,或者如果你想要一个组件的状态改变,只检查它的输入是否已经改变或者事件是否已经触发;然后,可以将changeDetection分配给ng.core.ChangeDetectionStrategy.OnPush

如果您想在任何时候强制一个变更检测周期,而不是等待一个异步操作发生,您可以调用ng.core.ChangeDetectorRef服务的detectChanges方法。

理解观子和内容子

出现在组件标签内的元素称为内容子级,出现在组件模板内的元素称为视图子级

要在组件视图中显示组件的内容子组件,我们需要使用<ng-content>标签。让我们看一个这样的例子。

将该代码置于App组件代码之上:

var ListItem = ng.core.Component({
  selector: "item",
  inputs: ["title"],
  template: "<li>{{title}} | <ng-content></ng-content></li>",
}).Class({
  constructor: function(){}
})

var List = ng.core.Component({
  selector: "list",
  template: "<ul><ng-content select='item'></ng-content></ul>"
}).Class({
  constructor: function(){}
})

现在,将App组件的代码改为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

app-template.html文件的末尾,添加以下代码:

<br>
<list>
  <item title="first">first</item>
  <item title="second">second</item>
</list>

该代码的输出如下:

Understanding view children and content children

这就是这三个代码片段的工作原理:

  1. App组件的模板文件中,我们添加了一个<list>标签,它显示了一个列表。在它的开始和结束标签中,我们定义了它应该显示的单个列表项。
  2. 我们创建分别绑定到<list><item>标签的ListItemList组件。
  3. 我们将List组件添加到App组件的directives属性中,而不是List属性中,因为<list>标签存在于App组件的模板中,而App组件负责创建其实例。
  4. App组件在List组件的模板中查找<ng-content>标签,并在那里渲染List组件实例。
  5. <ng-content>接受一个可选的select属性,该属性被分配给一个 CSS 选择器,该选择器指示我们想要显示的内容子元素。一个模板中可以有多个<ng-content>标签。如果没有提供select属性,那么所有的内容子级都将被渲染。这里不需要select属性;我们只是用它来做示范。

获取内容子元素的组件引用,查看子元素

要获取视图子项或内容子项的组件的引用,我们可以使用ng.core.ContentChildrenng.coreViewChildrenng.core.ContentChildng.core.ViewChild构造函数。ng.core.ContentChildrenng.core.ContentChild的区别在于第一个返回给定组件的所有引用,而第二个返回第一个引用。相同的区别也代表ng.core.ViewChildng.core.ViewChildren

这里有一个例子来演示ng.core.ContentChildren。将List组件的代码替换为:

var List = ng.core.Component({
  selector: "list",
  template: "<ul><ng-content select='item'></ng-content></ul>",
  queries: {
    list_items: new ng.core.ContentChildren(ListItem)
  }
}).Class({
  constructor: function(){},
  ngAfterContentInit: function(){
    this.list_items._results.forEach(function(e){
      console.log(e.title);
    })
  }
})

控制台中这段代码的输出如下:

first
second

这段代码的大部分内容是不言自明的。新的是ngAfterContentInit生命周期法。它在内容子对象初始化后被触发。同样,如果我们想要访问视图子视图,我们需要使用ngAfterViewInit生命周期方法。

请注意,我们只能访问组件的状态,不能访问其他任何内容。

局部模板变量

我们可以给一个内容子或者视图子分配一个局部模板变量。局部模板变量让我们得到内容子级或者视图子级的任意元素的引用,也就是组件引用或者 HTML 元素引用。

要给视图子对象或内容子对象的元素分配一个局部模板变量,我们需要在开始标签中放置#variable_name

这里有一个例子来演示局部模板变量是如何工作的。将该代码置于App组件上方:

var SampleComponent2 = ng.core.Component({
  selector: "sampletwo",
  template: "<input type='text' #input />",
  queries: {
    input_element: new ng.core.ViewChild("input")
  }
}).Class({
  constructor: function(){},
  ngAfterViewInit: function(){
    this.input_element.nativeElement.value = "Hi";
  }
})

App组件的代码改为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem,
  SampleComponent2],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

然后,将此代码添加到app-template.html文件的末尾:

<sampletwo></sampletwo>

该代码的输出如下:

Local template variables

以下是这三个代码片段的工作原理:

  1. 我们创建一个名为SampleComponent2的新组件,它显示一个 HTML 输入文本元素。我们将输入元素分配给一个名为input的局部模板变量。
  2. 然后,我们使用ng.core.ViewChild来获取对元素的引用。如果我们将一个字符串传递给ng.core.ViewChildng.core.ViewChildrenng.core.ContentChildng.core.ContentChildren,那么它们会寻找与该字符串具有相同局部变量名的元素,如果我们传递一个组件,它们会寻找该组件,就像我们之前看到的那样。
  3. 我们从本地模板变量中获得的组件引用与我们之前获得的接口相同。但是对于 HTML 元素引用,我们可以使用nativeElement属性访问元素的真实 DOM。

组件生命周期方法

component标签出现时,Angular 2 创建一个组件实例,渲染它,检查属性的变化,检查状态的变化,并在不再需要时销毁它。这些步骤共同构成了组件的生命周期。

Angular 2 允许我们注册在组件生命周期的不同阶段调用的方法。

以下是 Angular 2 提供的各种生命周期方法;生命周期挂钩按照它们出现的顺序进行解释:

  • ngOnChanges:每当组件的属性发生变化时都会调用。它也在组件的属性在组件的新实例创建后第一次被解析后被调用。它在状态因属性而改变之后,但在视图更新之前被调用。此方法接收当前和以前的属性值。
  • ngOnInit:这是ngOnChanges一审后援引的。它表示组件已成功创建,属性已被读取。
  • ngDoCheck:这是在每个变化检测周期期间和ngOnInit之后调用的。我们可以检测到 Angular 2 自身无法或不会检测到的变化,并根据这些变化采取行动。这是在 Angular 2 检查完组件的状态更改后调用的,如果属性有任何更改,则在组件视图更新前更新状态。该调用结束后,视图被渲染,渲染时调用ngAfterContentInitngAfterContentCheckedngAfterViewInitngAfterViewChecked
  • ngAfterContentInit:这是在内容子对象已经初始化但是还没有渲染之后调用的,也就是说在已经调用了内容子对象的ngOnChangesngOnInitngDoCheckngAfterContentInitngAfterContentChecked方法之后调用的。
  • ngAfterContentChecked:每当变更检测周期检查内容子代是否已经变更时,以及在ngAfterContentInit之后,都会调用。如果有变化,它会在内容子视图更新之前被调用。在调用之前,更新ng.core.ViewChildrenng.core.ContentChildren等的查询结果,即在内容子项的ngAfterContentChecked被调用后调用。此调用后,内容子视图将被更新。
  • ngAfterViewInit:这是视图子项已经初始化但尚未渲染后调用的,也就是调用了视图子项的ngOnChangesngOnInitngDoCheckngAfterContentInitngAfterContentCheckedngAfterViewInitngAfterViewChecked方法后调用的。
  • ngAfterViewChecked:每当变更检测周期检查视图子项是否已经变更时,以及在ngAfterViewInit之后,都会调用。如果有更改,则在视图子项的视图更新之前调用,但在视图子项的ngAfterViewChecked方法被调用之后调用。
  • ngOnDestroy:这是组件被销毁前调用的。组件的ngOnDestroy方法在其内容子级和视图子级的ngOnDestroy方法之前被调用。

书写模板

我们需要使用模板语言来编写组件模板。模板语言由 HTML 以及{}[]()[()]*|#标记组成。让我们看看这些都是用来做什么的,以及如何使用它们。

渲染值

为了简单地渲染this关键字的属性,我们需要使用{{}}标记。在这些大括号中,我们可以简单地放置属性名。

我们只能将表达式放在大括号内。我们放在里面的表达式看起来像 JavaScript。但是有一些 JavaScript 表达式我们不允许在这些大括号中使用。它们在这里:

  • 作业(=+=-=)
  • new操作员
  • ;,链接表达式
  • 递增和递减运算符(++--)
  • 按位运算符|&

管道

我们也可以将放入牙套中。管道是接受输入值并返回转换值的函数。管道由|操作符表示。大括号内的表达式的最终结果可以使用管道进行转换。支架里可以有我们想要的那么多管子。管道也可以接受参数。

Angular 2 提供了一些内置管道:dateuppercaselowercasecurrencypercent。我们也可以创建自己的管道。

这里有一个使用{{}}的例子。将该代码置于App组件上方:

var SampleComponent3 = ng.core.Component({
  selector: "samplethree",
  template: "{{info.firstname + info.lastname | uppercase}}"
}).Class({
  constructor: function(){
    this.info = {
      firstname: "firstname",
      lastname: " lastname"
    }
  }
})

App组件代码替换为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem,
  SampleComponent2, SampleComponent3],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

然后,将其放入app-template.html文件中:

<br><br>
<samplethree></samplethree>

代码的输出如下:

Pipes

请注意,如果最终值不是字符串,大括号内的表达式的最终结果将转换为字符串。

处理事件

要处理模板中元素的事件,我们需要使用()操作符。下面是一个如何处理事件的例子。将该代码置于App组件代码之上:

var SampleComponent4 = ng.core.Component({
  selector: "samplefour",
  template: "<input (click)='clicked($event)' (mouseover)='mouseover($event)' type='button'value='Click Me!!!' />"
}).Class({
  constructor: function(){
    this.clicked = function(e){
      alert("Hi from SampleComponent4");
    };

    this.mouseover = function(e){
      console.log("Mouse over event");
    }

  }
})

App组件代码替换为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem,
  SampleComponent2, SampleComponent3, SampleComponent4],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

将该代码置于app-template.html:

<br><br>
<samplefour></samplefour>

前面的代码是不言自明的。

将状态绑定到元素属性

要将关键字的属性值绑定到模板中元素的属性,我们可以简单地使用{{}},例如:

<component title="{{title}}"></component>

但是如果想要传递一个对象,这个方法将不起作用,因为{{}}标记内的表达式总是被转换成一个字符串。因此,Angular 2 提供了[]运算符,该运算符使组件能够通过属性将对象传递给其模板中的组件。

这里有一个例子来证明这一点。将该代码置于App组件代码之上:

var SampleComponent5 = ng.core.Component({
  selector: "samplefive",
  inputs: ["info"],
  template: "{{info.name}}"
}).Class({
  constructor: function(){}
})

var SampleComponent6 = ng.core.Component({
  selector: "samplesix",
  directives: [SampleComponent5],
  template: "<samplefive [info]='myInfo'></samplefive>"
}).Class({
  constructor: function(){
    this.myInfo = {
      name: "Name"
    }
  }
})

App组件的代码替换为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem, SampleComponent2, SampleComponent3, SampleComponent4, SampleComponent6],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

将这个代码放在app-template.html文件的末尾:

<br><br>
<samplesix></samplesix>

该代码的输出如下:

Binding state to element attributes

请注意,在为 HTML 标记分配属性时,如果我们分配了一个不是元素原生的属性,我们需要使用attr.为属性名称加前缀。例如,要给一个<span>标签分配一个value属性,我们需要给属性命名attr.value,而不是简单的value。否则,Angular 2 将抛出一个错误。这是因为在解释模板并创建其 DOM 时,Angular 2 通过将值赋给 DOM 元素的属性来设置属性。因此,当我们使用attr.前缀时,它会向 Angular 2 发出使用setAttribute的信号。

双向数据绑定

默认情况下,Angular 2 不使用双向数据绑定。它使用单向绑定,但如果需要,它为双向数据绑定提供[()]运算符。

这里举个例子来演示[()]。将该代码放在App组件的代码上方:

var SampleComponent7 = ng.core.Component({
  selector: "sampleseven",
  template: "<input [(ngModel)]='name' /><input (click)='clicked()' value='Click here' type='submit' />"
}).Class({
  constructor: function(){},
  clicked: function(){
    alert(this.name);
  }
})

App组件代码替换为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem, SampleComponent2, SampleComponent3, SampleComponent4,SampleComponent6, SampleComponent7],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

将这个代码放在app-template.html文件中:

<br><br>
<sampleseven></sampleseven>

该代码的输出如下:

Two-way data binding

在这里,在文本字段中输入一些内容,然后单击按钮。您将看到一个带有文本字段值的警告框。

为了捕捉 HTML 表单元素的值,我们需要将ngModel放在[()]括号内。如果我们在输入和输出之间设置双向数据绑定,我们可以设置属性名。稍后我们将了解更多关于输出的信息。

指令

指令 用于根据状态改变 DOM。有两种类型的指令:属性指令和结构指令。让我们看看他们每个人。

属性指令

一个属性指令 根据状态的变化改变 DOM 元素的外观或行为。ngClassngStyle是内置属性指令。我们还可以创建自己的属性指令。

ngClass指令用于在元素中添加或移除 CSS 类,而ngStyle指令用于设置内联样式。

下面是如何使用ngClassngStyle指令的例子。将这个代码放在App组件的代码上面:

var SampleComponent8 = ng.core.Component({
  selector: "sampleeight",
  template: "<div [ngStyle]='styles' [ngClass]='classes'></div>"
}).Class({
  constructor: function(){
    this.styles = {
      "font-size": "20px",
      "font-weight": "bold"
    }

    this.classes = {
      a: true,
      b: true,
      c: false
    };
  }
})

App组件的代码替换为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem, SampleComponent2, SampleComponent3, SampleComponent4, SampleComponent6, SampleComponent7, SampleComponent8],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

然后,将该代码放在app-template.html文件的末尾:

<sampleeight></sampleeight>

现在,如果您在浏览器开发工具中检查<sampleeight>标签,您将看到以下内容:

Attribute directives

这个代码的大部分是不言自明的。可以看到同样的[]令牌也用于属性指令。当使用[]标记时,Angular 2 首先检查该名称是否存在内置属性指令或自定义指令,如果不存在,则将其视为属性。

结构指令

一个结构指令 通过添加或删除 DOM 元素来改变 DOM 布局。ngIfngSwitchngFor是三个内置的结构指令。我们还可以创建自己的定制结构指令。

这里有一个例子来演示ngIfngSwitch。我们之前已经看到过ngFor的例子。将该代码放在App组件的代码上方:

var SampleComponent9 = ng.core.Component({
  selector: "samplenine",
  templateUrl: "componentTemplates/samplecomponent9-template.html"
}).Class({
  constructor: function(){
    this.display1 = true;
    this.display2 = false;
    this.switchOption = 'A';
  }
})

创建一个名为samplecomponent9-template.html的文件,放在componentTemplates目录下。将此代码放在文件中:

<br><br>

<div *ngIf="display1">Hello</div>
<div *ngIf="display2">Hi</div>

<span [ngSwitch]="switchOption">
  <span *ngSwitchWhen="'A'">A</span>
  <span *ngSwitchWhen="'B'">B</span>
  <span *ngSwitchWhen="'C'">C</span>
  <span *ngSwitchWhen="'D'">D</span>
  <span *ngSwitchDefault>other</span>
</span>

App组件的代码替换为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem, SampleComponent2, SampleComponent3, SampleComponent4, SampleComponent6, SampleComponent7, SampleComponent8, SampleComponent9],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

最后,将这个代码放在app-template.html文件中:

<samplenine></samplenine>

该代码的输出如下:

Structural directives

这段代码的大部分内容是不言自明的。您可以看到,我们正在使用*标记来表示结构指令。*标记将元素视为模板,也就是说,它不渲染元素,而是将其用作模板来创建 DOM。

实际上,属性和结构指令都是使用[]标记编写的,但是使用结构指令和[]标记编写代码会使代码变长。因此,Angular 2 引入了*标记,这使得使用结构指令编写代码变得很容易。在内部,Angular 2 将使用*令牌的代码转换为使用[]令牌。在此了解更多信息:

https://angular . io/docs/ts/latest/guide/template-语法. html#

输出

输出允许组件发出自定义事件。例如,如果我们有一个显示按钮的组件,并且我们希望父组件能够为子组件的 click 事件添加一个事件处理程序,那么我们可以使用输出来实现这一点。

下面是一个如何集成输出的示例。将该代码置于App组件代码之上:

var SampleComponent10 = ng.core.Component({
  selector: "sampleten",
  outputs: ["click"],
  template: ""
}).Class({
  constructor: function(){
    this.click = new ng.core.EventEmitter();
    setInterval(function(){
      this.click.next({});
    }.bind(this), 10000)
  }
})

var SampleComponent11 = ng.core.Component({
  selector: "sampleeleven",
  directives: [SampleComponent10],
  template: "<br><sampleten
  (click)='clicked($event)'></sampleten>{{value}}"
}).Class({
  constructor: function(){
    this.value = 1;
    this.clicked = function(e){
      this.value++;
    }
  }
})

用以下代码替换App组件的代码:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem, SampleComponent2, SampleComponent3, SampleComponent4, SampleComponent6, SampleComponent7, SampleComponent8, SampleComponent9, SampleComponent11],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

最后,将此代码放在app-template.html文件的末尾:

<sampleeleven></sampleeleven>

现在,您将开始看到页面上出现一个计数器。

outputs属性用于定义组件发出的事件。我们需要在这个关键字中创建一个与输出同名的属性,并将其分配给ng.core.EventEmitter的一个新实例,这样它就可以发出事件。ng.core.EventEmitter为物体提供观察者模式。

为了捕获事件,我们需要使用()标记,就像我们使用它来捕获本机 UI 事件一样。

请注意,我们需要将输出分配给构造函数属性内部的ng.core.EventEmitter的新实例,也就是说,在创建组件的新实例期间。

具有输入和输出的双向数据绑定

您可以实现输入和输出之间的双向数据绑定。例如,如果父组件将一个属性传递给视图子组件的一个组件,并且每当输入值发生变化时,子组件都通知父组件,那么我们可以使用[()]而不是分别使用()[]

这里有一个例子来证明这一点。将该代码置于App组件代码之上:

var SampleComponent12 = ng.core.Component({
  selector: "sampletwelve",
  inputs: ["count"],
  outputs: ["countChange"],
  template: ""
}).Class({
  constructor: function(){
    this.countChange = new ng.core.EventEmitter();
    setInterval(function(){
      this.count++;
      this.countChange.next(this.count);
    }.bind(this), 10000);
  }
})

var SampleComponent13 = ng.core.Component({
  selector: "samplethirteen",
  directives: [SampleComponent12],
  template: "<br><sampletwelve
  [(count)]='count'></sampletwelve>{{count}}"
}).Class({
  constructor: function(){
    this.count = 1;
  }
})

App组件的代码替换为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem, SampleComponent2, SampleComponent3, SampleComponent4, SampleComponent6, SampleComponent7, SampleComponent8, SampleComponent9, SampleComponent11, SampleComponent13],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

最后,将这段代码添加到app-template.html文件的末尾:

<samplethirteen></samplethirteen>

这里,输出与前面的示例相同。大多数事情都是不言自明的。您唯一需要知道的是,这两个代码片段都做了同样的事情:

<sampletwelve [(count)]='count'></sampletwelve>
<sampletwelve [count]='count' (countChange)= 'count=$event'></sampletwelve>

了解提供商

一个提供者告诉 Angular 2 如何在注入服务的同时创建一个服务实例。使用组件的providersviewProviders属性设置提供程序。

让我们看一个如何创建提供者的例子。将该代码置于App组件代码之上:

var Service1 = ng.core.Class({
  constructor: function() {
  },
  getValue: function() {
    return "xyz"
  }
});

var Service2 = ng.core.Class({
  constructor: function() {
  },
  getValue: function() {
    return "def"
  }
});

var Service3 = ng.core.Class({
  constructor: function() {
  },
  getValue: function() {
    return "mno"
  }
});

var Service4 = ng.core.Class({
  constructor: [Service2, Service3, function(s2, s3) {
    console.log(s2);
    console.log(s3);
  }],
  getValue: function() {
    return "abc"
  }
});

var ServiceTest1 = ng.core.Component({
  selector: "st1",
  viewProviders: [
    ng.core.provide(Service1, {useClass: Service4}),
    ng.core.provide(Service2, {useValue: "def"}),
    ng.core.provide(Service3, {useFactory: function(){
      return "mno";
    }})
  ],
  template: ""
}).Class({
  constructor: [Service1, function(s1){
    console.log(s1.getValue());
  }]
})

用以下代码替换App组件的代码:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem, SampleComponent2, SampleComponent3, SampleComponent4, SampleComponent6, SampleComponent7, SampleComponent8,
  SampleComponent9, SampleComponent11, SampleComponent13, ServiceTest1],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

最后,将此添加到app-template.html文件的末尾:

<st1></st1>

这是控制台输出的代码:

def
mno
abc

这就是它的工作原理:

  • 首先,我们创建四个服务:Service1Service2Service3Service4。它们都有一个getValue方法,返回一个字符串。Service4依赖于Service2Service3
  • 然后,我们创建一个名为ServiceTest1的组件。这取决于Service1。在viewProviders属性中,我们通过了一系列提供者。使用ng.core.provide方法创建提供者。它需要两个参数;第一个是服务名,第二个是配置对象,说明如何创建这个服务的实例。useClass属性告诉 Angular 2 在请求第一个参数中的服务实例时创建该服务的实例。所以在这里,当需要Service1的实例时,实际创建的就是Service4的实例。类似地,useValue用于提供一个值,useFactory用于将控制传递给一个函数,以决定在请求新实例时返回什么。所以在这里,当请求Service2的一个实例时,我们得到def字符串,当请求Service3时,我们得到mno字符串。

在本章的前面,我们只是简单地将viewProviders分配给服务本身。服务还实现了提供者的接口,从而创建了服务本身的实例。

如果有多个提供者匹配一个服务,那么最新的提供者将覆盖先前的提供者。

提供程序和视图提供程序属性之间的区别

viewProviders属性允许我们使提供者只对组件的视图可用,而providers属性使提供者对其内容子级和视图子级可用。

providers属性只创建一个服务实例一次,并向任何需要它的组件提供相同的服务实例。我们已经看到了viewProviders是如何工作的。让我们来看一个providers如何工作的例子。将此代码放在应用组件的代码之上:

var counter = 1;

var Service5 = ng.core.Class({
  constructor: function(){}
})

var ServiceTest2 = ng.core.Component({
  selector: "st2",
  template: ""
}).Class({
  constructor: [Service5, function(s5){
    console.log(s5);
  }]
})

var ServiceTest3 = ng.core.Component({
  selector: "st3",
  providers: [ng.core.provide(Service5, {useFactory: function(){
    counter++;
    return counter;
  }})],
  directives: [ServiceTest2],
  template: "<st2></st2>"
}).Class({
  constructor: [Service5, function(s5){
    console.log(s5);
  }]
})

App组件的代码替换为:

var App = ng.core.Component({
  selector: "app",
  directives: [Cards, SampleComponent1, List, ListItem, SampleComponent2, SampleComponent3, SampleComponent4, SampleComponent6, SampleComponent7, SampleComponent8, SampleComponent9, SampleComponent11, SampleComponent13,
  ServiceTest1, ServiceTest3],
  templateUrl: "componentTemplates/app-template.html"
}).Class({
  constructor: function(){}
})

最后,在app-template.html文件的末尾,放置以下代码:

<st3></st3>

该代码的控制台输出如下:

2
2

这段代码中的大部分内容都是不言自明的。我们用providers代替viewProvidersServiceTest2组件依赖于Service5,但是它没有Service5的提供者,所以 Angular 2 使用ServiceTest3提供的提供者,因为ServiceTest3是它的父级。如果ServiceTest3没有Service5的提供者,Angular 2 会更进一步,在App组件中寻找提供者。

ng.platform.browser.bootstrap方法还接受第二个参数,这是所有组件都可用的提供者列表。因此,我们可以通过ng.platform.browser.bootstrap方法传递提供者,而不是在App组件中传递提供者。

总结

在这一章中,我们学习了 Angular 2。我们看到了什么是组件,如何编写模板,如何创建服务等等。我们还了解了网络组件以及 Angular 2 如何利用它们。现在,您应该可以轻松构建 Angular 2 应用了。

在下一章中,我们将通过构建一个完整的应用来学习如何使用 Angular 2 构建 SPA。