Angular2 Bootstrap4 Web 开发(全)
原文:
zh.annas-archive.org/md5/1998a305c23fbffe24116fac6b321687
译者:飞龙
前言
这本书是关于当代网页开发中两个巨大和最受欢迎的名字,Angular 2 和 Bootstrap 4。
Angular 2 是 AngularJS 的继任者,但在许多方面都比前任更好。它在处理大量数据需求时结合了在网页浏览器上可能的最大速度和可扩展性。这使得 Angular 2 成为构建新系统或从旧系统升级时的首选候选人。
Bootstrap 4 是构建响应式、移动优先网页应用程序的下一个进化阶段。它可以轻松高效地将网站从移动端扩展到桌面端,只需一个代码库。
如果您想利用 Angular 2 的强大功能和 Bootstrap 4 的灵活性来构建强大的大规模或企业级网页应用程序,您来对地方了。
我有写一本关于 Angular 2 和 Bootstrap 4 的书的愿望,不假设读者有先前的经验和知识。我的想法是书中充满了技术细节。现在,你手中拿着的书就是这个愿望的实现,因为它既适合初学者,又在技术上很深入。它涵盖了开发人员使用这两个框架进行严肃网页开发所需的一切。
这本书涵盖了什么
第一章 ,打招呼!,指导您建立一个开发环境,以便展示如何轻松地使用 Angular 2 和 Bootstrap 4 快速启动网页应用程序的最简单应用程序。
第二章 ,使用 Bootstrap 组件,展示了如何通过展示演示布局页面来开始使用 Bootstrap 4,以及如何探索框架并根据您的需求进行定制。
第三章 ,高级 Bootstrap 组件和定制,解释了如何使用诸如 Jumbotron、Carousel 之类的组件,并通过输入组件为您节省时间。
第四章 ,创建模板,让您学习如何使用内置的 Angular 2 指令构建 UI 模板。您将熟悉模板语法,以及如何在 HTML 页面中绑定属性和事件,并使用管道转换显示。
第五章,路由,帮助您了解路由器代码在用户执行应用程序任务时如何管理视图之间的导航。我们将看看如何创建静态路由以及包含参数的路由以及如何配置它们。
第六章,依赖注入,教读者如何解耦应用程序的需求,以及如何创建一个一致的数据源作为服务。
第七章,使用表单,向读者展示了如何使用与表单创建相关的 Angular 2 指令,以及如何将基于代码的表单组件用于 HTML 表单。我们将使用 Bootstrap 4 来增强表单的外观,并指示我们的 Web 应用程序的无效输入。
第八章,高级组件,描述了组件的生命周期以及可以在不同阶段使用的方法。我们将分析这个周期的每个阶段,并学习如何充分利用当组件从一个阶段转移到另一个阶段时触发的挂钩方法。
第九章,通信和数据持久性,解释了如何使用内置的 HTTP 库来处理端点。我们将学习如何使用 Firebase 作为应用程序的持久层。
第十章,高级 Angular 技术,介绍了高级的 Angular 技术。我们将借助 Webpack 来改造我们的应用程序,并学习如何安装和使用 ng2-bootstrap。我们将探索 Angular CLI 的世界,并使用 AOT 来显著减少生产代码的大小。
本书所需的内容
任何安装了 Windows、Linux 或 Mac OS 的现代个人电脑都足以运行本书中的代码示例。本书中使用的所有软件都是开源的,并且可以在网上免费获得:
本书适合谁
无论您对 Bootstrap 或 Angular 了解多少,还是完全初学者,本书都将增强您在这两个框架中的能力,并帮助您构建一个完全功能的 Web 应用程序。需要对 HTML、CSS 和 JavaScript 有一定了解,才能充分掌握 Bootstrap 和 Angular。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
约定
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:“它支持number
,boolean
和string
类型注释,用于原始类型和任何动态类型结构。”
代码块设置如下:
function add(first: number, second: number): number {
return first + second;
}
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
var x = 3;
function random(randomize) {
if (randomize) {
// x initialized as reference on function
** var x = Math.random();**
return x;
}
return x; // x is not defined
}
random(false); // undefined
任何命令行输入或输出都以以下方式编写:
**npm config list**
新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会在文本中显示为:“单击下一步按钮将您移至下一个屏幕。”
注意
警告或重要说明会以这样的方式出现在一个框中。
提示
技巧和诀窍会以这种方式出现。
第一章:说你好!
让我们按照几个步骤来建立一个尽可能简单的应用程序的开发环境,向你展示使用 Angular 2 和 Bootstrap 4 轻松地启动和运行 Web 应用程序有多容易。在本章结束时,你将对以下内容有扎实的理解:
-
如何设置你的开发环境
-
TypeScript 如何改变你的开发生活
-
Angular 和 Bootstrap 的核心概念
-
如何使用 Bootstrap 创建一个简单的 Angular 组件
-
如何通过它显示一些数据
设置开发环境
让我们来设置你的开发环境。这个过程是学习编程中最容易被忽视和经常令人沮丧的部分之一,因为开发人员不想考虑它。开发人员必须了解如何安装和配置许多不同的程序,然后才能开始真正的开发。每个人的电脑都不一样;因此,相同的设置可能在你的电脑上无法工作。我们将通过定义你需要设置的各种环境的各个部分来暴露和消除所有这些问题。
定义一个 shell
Shell 是你的软件开发环境的必需部分。我们将使用 Shell 来安装软件并运行命令来构建和启动 Web 服务器,为你的 Web 项目注入生命。如果你的电脑安装了 Linux 操作系统,那么你将使用一个叫做Terminal的 Shell。有许多基于 Linux 的发行版,它们使用不同的桌面环境,但大多数都使用相同的键盘快捷键来打开 Terminal。
注意
在 Ubuntu、Kali 和 Linux Mint 中使用键盘快捷键Ctrl + Alt + T来打开 Terminal。如果对你不起作用,请查看你的 Linux 版本的文档。
如果你的 Mac 电脑安装了 OS X,那么你也将使用 Terminal shell。
注意
使用键盘快捷键command + space来打开Spotlight,输入 Terminal 进行搜索和运行。
如果你的电脑安装了 Windows 操作系统,你可以使用标准的命令提示符,但我们可以做得更好。一会儿我会告诉你如何在你的电脑上安装 Git,并且你将免费获得 Git Bash。
注意
你可以在 Windows 上使用Git Bash
shell 程序打开一个 Terminal。
每当我需要在 Terminal 中工作时,我会在本书的所有练习中使用 Bash shell。
安装 Node.js
Node.js是我们将用作跨平台运行时环境来运行服务器端 Web 应用程序的技术。它是基于 Google 的 V8 JavaScript 引擎的本地、平台无关的运行时和大量用 JavaScript 编写的模块的组合。Node.js 附带了不同的连接器和库,帮助您使用 HTTP、TLS、压缩、文件系统访问、原始 TCP 和 UDP 等。作为开发人员,您可以在 JavaScript 中编写自己的模块,并在 Node.js 引擎内运行它们。Node.js 运行时使构建网络事件驱动的应用服务器变得容易。
注意
术语package和library在 JavaScript 中是同义的,因此我们将它们互换使用。
Node.js 广泛利用JavaScript 对象表示法(JSON)格式在服务器和客户端之间进行数据交换,因为它可以在几个解析图中轻松表达,特别是没有 XML、SOAP 和其他数据交换格式的复杂性。
您可以使用 Node.js 开发面向服务的应用程序,做一些与 Web 服务器不同的事情。最受欢迎的面向服务的应用程序之一是node package manager(npm),我们将使用它来管理库依赖关系、部署系统,并为 Node.js 的许多平台即服务(PaaS)提供商提供基础。
如果您的计算机上没有安装 Node.js,您应该从nodejs.org/en/download
下载预构建的安装程序,或者您可以使用来自nodejs.org/en/download/package-manager
的非官方包管理器。安装后,您可以立即开始使用 Node.js。打开终端并键入:
**node --version**
Node.js 将以安装的运行时的版本号作出响应:
**v4.4.3**
请记住,我计算机上安装的 Node.js 版本可能与您的不同。如果这些命令给您一个版本号,那么您已经准备好开始 Node.js 开发了。
设置 npm
npm 是 JavaScript 的包管理器。您可以使用它来查找、共享和重用来自世界各地许多开发人员的代码包。包的数量每天都在急剧增长,现在已经超过 250K。npm 是 Node.js 的包管理器,并利用它来运行自身。npm 包含在 Node.js 的安装包中,并在安装后立即可用。打开终端并键入:
**npm --version**
npm 必须以版本号的形式响应您的命令:
**2.15.1**
我的 Node.js 带有特定版本的 npm。npm 经常更新,所以您需要使用以下命令切换到最新版本:
**npm install npm@latest -g**
您可能会遇到使用 npm 搜索或安装软件包时的权限问题。如果是这种情况,我建议按照docs.npmjs.com/getting-started/fixing-npm-permissions
上的说明操作,不要使用超级用户权限来修复它们。
以下命令为我们提供了有关 Node.js 和 npm 安装的信息:
**npm config list**
有两种方法可以安装 npm 软件包:本地安装或全局安装。在您希望将软件包用作工具时,最好进行全局安装:
**npm install -g <package_name>**
如果您需要找到全局安装软件包的文件夹,可以使用以下命令:
**npm config get prefix**
全局安装软件包很重要,但最好在不需要时避免。大多数情况下,您会进行本地安装。
**npm i <package_name>**
您可以在项目的node_modules
文件夹中找到本地安装的软件包。
安装 Git
如果您不熟悉 Git,那您真的错过了很多!Git 是一个分布式版本控制系统,每个 Git 工作目录都是一个完整的仓库。它保留了完整的更改历史,并具有完整的版本跟踪功能。每个仓库都完全独立于网络访问或中央服务器。您可以在计算机上保存 Git 仓库并与同事共享,或者利用许多在线 VCS 提供者。您应该仔细查看的大公司是 GitHub、Bitbucket 和 Gitlab.com。每个都有自己的好处,取决于您的需求和项目类型。
Mac 计算机已经安装了 Git 到操作系统中,但通常 Git 的版本与最新版本不同。您可以通过官方网站git-scm.com/downloads
上提供的一组预构建安装程序来更新或安装 Git 到您的计算机上。安装完成后,您可以打开终端并输入:
**git -version**
Git 必须以版本号做出响应:
**git version 2.8.1.windows.1**
正如我所说,对于使用安装了 Windows 操作系统的计算机的开发人员,您现在可以在系统上免费使用 Git Bash。
代码编辑器
可以想象有多少用于代码编辑的程序存在,但我们今天只谈论免费、开源、可以在任何地方运行的微软 Visual Studio Code。您可以使用任何您喜欢的程序进行开发,但在我们未来的练习中,我将只使用 Visual Studio Code,请从code.visualstudio.com/Download
安装它。
TypeScript 的速成课程
TypeScript 是由微软开发和维护的开源编程语言。它最初于 2012 年 10 月公开发布,并由 C#的首席架构师、Delphi 和 Turbo Pascal 的创始人 Anders Hejlsberg 进行了介绍。
TypeScript 是 JavaScript 的一种类型化超集,可以编译为普通的 JavaScript。任何现有的 JavaScript 也都是有效的 TypeScript。它提供了类型检查、显式接口和更容易的模块导出。目前,它包括ES5,ES2015,ES2016,实际上有点像提前获得明天的 ECMAScript 的一些功能,这样我们就可以今天就使用一些这些功能。
以下是 ECMAScript 和 TypeScript 之间的关系:
如果您已经熟悉 JavaScript 语言,使用 TypeScript 编写代码相对简单。可以在 TypeScript playground www.typescriptlang.org/play
中使用 IntelliSense、查找引用等功能,直接从浏览器中进行操作。
类型
TypeScript 提供了静态类型检查操作,可以在开发周期中捕捉到许多错误。TypeScript 通过类型注解实现了编译时的类型检查。TypeScript 中的类型始终是可选的,因此如果您更喜欢 JavaScript 的常规动态类型,则可以忽略它们。它支持原始类型的number
,boolean
和string
类型注解,以及动态类型结构的any
。在下面的示例中,我为function
的return
和参数添加了类型注解:
function add(first: number, second: number): number {
return first + second;
}
在编译的某一时刻,TypeScript 编译器可以生成一个仅包含导出类型签名的声明文件。带有扩展名.d.ts
的结果声明文件以及 JavaScript 库或模块可以稍后由第三方开发人员使用。您可以在以下网址找到许多流行 JavaScript 库的声明文件的广泛集合:
-
DefinitelyTyped (
github.com/DefinitelyTyped/DefinitelyTyped
) -
Typings 注册表 (
github.com/typings/registry
)
箭头函数
JavaScript 中的函数是头等公民,这意味着它们可以像其他任何值一样传递:
var result = [1, 2, 3]
.reduce(function (total, current) {
return total + current;
}, 0); // 6
reduce
中的第一个参数是匿名函数。匿名函数在许多场景中非常有用,但太啰嗦了。TypeScript 引入了一种新的、不那么啰嗦的语法来定义匿名函数,称为 箭头函数 语法:
var result = [1, 2, 3]
.reduce( (total, current) => {
return total + current;
}, 0); // 6
甚至更简洁:
var result = [1, 2, 3]
.reduce( (total, current) => total + current, 0); // 6
在定义参数时,如果参数只是一个标识符,甚至可以省略括号。所以数组的常规 map
方法:
var result = [1, 2, 3].map(function (x) {
return x * x
});
可以更加简洁:
var result = [1, 2, 3].map(x => x * x);
两种语法 (x) => x * x
和 x => x * x
都是允许的。
箭头函数的另一个重要特性是它不会遮蔽 this
,而是从词法作用域中获取它。假设我们有一个构造函数 Counter
,它在超时中增加内部变量 age
的值并将其打印出来:
function Counter() {
this.age = 30;
setTimeout(() => {
this.age += 1;
console.log(this.age);
}, 100);
}
new Counter(); // 31
使用箭头函数的结果是,Counter
作用域中的 age
在 setTimeout
的回调函数中是可用的。以下是转换为 JavaScript ECMAScript 5 代码:
function Counter() {
var _this = this;
this.age = 30;
setTimeout(function () {
_this.age += 1;
console.log(_this.age);
}, 100);
}
以下变量在箭头函数内部都是词法作用域的:
-
arguments
-
super
-
this
-
new.target
块作用域变量
在 ES5 中,使用 var
声明的所有变量都是函数作用域的,它们的作用域属于封闭函数。以下代码的结果可能令人困惑,因为它返回 undefined
:
var x = 3;
function random(randomize) {
if (randomize) {
// x initialized as reference on function
var x = Math.random();
return x;
}
return x; // x is not defined
}
random(false); // undefined
x
是 random
函数的内部变量,与第一行定义的变量没有任何关系。在最后一行调用 random
函数的结果返回 undefined
,因为 JavaScript 解释 random
函数中的代码如下:
function random(randomize) {
var x; // x is undefined
if (randomize) {
// x initialized as reference on function
x = Math.random();
return x;
}
return x; // x is not defined
}
在 TypeScript 中,这段令人困惑的代码可以通过新的块作用域变量声明来修复:
-
let
是var
的块作用域版本 -
const
类似于let
,但只允许初始化变量一次
TypeScript 编译器使用新的块作用域变量声明会抛出更多错误,并防止编写复杂和损坏的代码。让我们在前面的例子中将 var
改为 let
:
let x = 3;
function random(randomize) {
if (randomize) {
let x = Math.random();
return x;
}
return x;
}
random(false); // 3
现在我们的代码按预期工作了。
注意
我建议使用 const
和 let
来使代码更清晰、更安全。
模板文字
如果我们需要字符串插值,通常会将变量的值与字符串片段组合在一起,例如:
let out: string = '(' + x + ', ' + y + ')';
TypeScript 支持模板文字--允许嵌入表达式的字符串文字。您可以直接使用模板文字的字符串插值功能:
let out: string = `(${x}, ${y})`;
如果需要多行字符串,模板文字可以再次帮助:
Let x = 1, y = 2;
let out: string = `
Coordinates
x: ${x},
y: ${y}`;
console.log(out);
最后一行打印结果如下:
Coordinates
x: 1,
y: 2
注意
我建议使用模板文字作为更安全的字符串插值方式。
for-of 循环
我们通常使用for
语句或 JavaScript ES5 中Array
的forEach
方法来迭代元素:
let arr = [1, 2, 3];
// The for statement usage
for (let i = 0; i < arr.length; i++) {
let element = arr[i];
console.log(element);
}
// The usage of forEach method
arr.forEach(element => console.log(element));
每种方法都有其好处:
-
我们可以通过
break
或continue
中断for
语句 -
forEach
方法更简洁
TypeScript 具有for-of
循环,结合了这两种方法:
const arr = [1, 2, 3];
for (const element of arr) {
console.log(element);
}
for-of
循环支持break
和continue
,并且可以使用entries
方法的index
和value
来使用每个数组:
const arr = [1, 2, 3];
for (const [index, element] of arr.entries()) {
console.log(`${index}: ${element}`);
}
默认值、可选和剩余参数
我们经常需要检查函数的输入参数并为它们分配默认值:
function square(x, y) {
x = x || 0;
y = y || 0;
return x * y;
}
let result = square(4, 5); // Out 20
TypeScript 有语法来处理参数的默认值,使之前的函数更短、更安全:
function square(x: number = 0, y: number = 0) {
return x * y;
}
let result = square(4, 5); // Out 20
注意
参数的默认值只有在其undefined
值时才会被赋值。
JavaScript ES5 中函数的每个参数都是可选的,因此省略的参数等于undefined
。为了使其严格,TypeScript 希望在我们想要可选的参数末尾加上一个问号。我们可以将square
函数的最后一个参数标记为可选,并使用一个或两个参数调用该函数:
function square(x: number = 0, y?: number) {
if (y) {
return x * y;
} else {
return x * x;
}
}
let result = square(4); // Out 16
let result = square(4, 5); // Out 20
注意
任何可选参数必须跟在必需参数后面。
在某些情况下,我们需要将多个参数作为一组处理,或者我们可能不知道函数需要多少个参数。JavaScript ES5 在函数范围内提供了arguments
变量来处理它们。在 TypeScript 中,我们可以使用一个形式变量来保存其余的参数。编译器使用省略号后面给定的名称构建传递的参数数组,以便我们可以在函数中使用它:
function print(name: number, ...restOfName: number[]) {
return name + " " + restOfName.join(" ");
}
let name = print("Joseph", "Samuel", "Lucas");
// Out: Joseph Samuel Lucas
接口
接口是定义项目内外合同的方式。我们在 TypeScript 中使用接口只是为了描述类型和数据的形状,以帮助我们保持代码无误。与许多其他语言相比,TypeScript 编译器不会为接口生成任何代码,因此它没有运行时成本。TypeScript 通过 interface 关键字定义接口。让我们定义一个类型Greetable
:
interface Greetable {
greetings(message: string): void;
}
它有一个名为greetings
的成员函数,接受一个字符串参数。以下是我们如何将其用作参数类型:
function hello(greeter: Greetable) {
greeter.greetings('Hi there');
}
类
JavaScript 具有基于原型的面向对象编程模型。我们可以使用对象文字语法或构造函数来实例化对象。它的基于原型的继承是在原型链上实现的。如果您来自面向对象的方法,当您尝试基于原型创建类和继承时,可能会感到不舒服。TypeScript 允许基于面向对象的类的方法编写代码。编译器将类转换为 JavaScript,并在所有主要的 Web 浏览器和平台上运行。这是类Greeter
。它有一个名为greeting
的属性,一个constructor
和一个greet
方法:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
要引用类的任何成员,我们需要在前面加上this
。要创建类的实例,我们使用new
关键字:
let greeter = new Greeter("world");
我们可以通过继承来扩展现有的类以创建新的类:
class EmailGreeter extends Greeter {
private email: string;
constructor(emailAddr: string, message: string) {
super(message);
this.email = emailAddr;
}
mailto() {
return "mailto:${this.email}?subject=${this.greet()}";
}
}
在类EmailGreeter
中,我们展示了 TypeScript 中继承的几个特性:
-
我们使用
extends
来创建一个子类 -
我们必须在构造函数的第一行调用
super
以将值传递给基类 -
我们调用基类的
greet
方法来创建一个mailto
的主题
TypeScript 类支持public
、protected
和private
修饰符,以访问我们在整个程序中声明的成员。类的每个成员默认为 public。不需要使用关键字标记所有public
成员,但您可以显式标记它们。如果需要限制从外部访问类的成员,则使用 protected 修饰符,但请记住它们仍然可以从派生类中访问。您可以将构造函数标记为 protected,以便我们无法实例化该类,但可以扩展它。private
修饰符限制了仅在类级别上访问成员。
如果您查看EmailGreeter
的构造函数,我们必须声明一个私有成员email
和一个构造函数参数emailAddr
。相反,我们可以使用参数属性来让我们在一个地方创建和初始化成员:
class EmailGreeter extends Greeter {
constructor(private email: string, message: string) {
super(message);
}
mailto() {
return "mailto:${this.email}?subject=${this.greet()}";
}
}
您可以在参数属性中使用任何修饰符。
注意
使用参数属性将声明和赋值 consoli 在一个地方。
TypeScript 支持使用 getter 和 setter 来组织拦截对象成员的访问。我们可以使用以下代码更改原始的Greeter
类:
class Greeter {
private _greeting: string;
get greeting(): string {
return this._greeting;
}
set greeting(value: string) {
this._greeting = value || "";
}
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
我们在“问候语”的 setter 中检查value
参数,并在将其分配给私有成员之前,根据需要将其修改为空字符串。
TypeScript 还通过静态修饰符支持类成员。这里的Types
类仅包含静态成员:
class Types {
static GENERIC: string = "";
static EMAIL: string = "email";
}
我们可以通过在类名前加上访问这些值:
console.log(Types.GENERIC);
TypeScript 通过抽象类为我们提供了极大的灵活性。我们无法创建它们的实例,但我们可以使用它们来组织基类,从而可以派生出每个不同的类。我们可以使用一个关键字将greeting
类转换为抽象类:
abstract class BaseGreeter {
private _greeting: string;
get greeting(): string {
return this._greeting;
}
set greeting(value: string) {
this._greeting = value || "";
}
abstract greet();
}
方法greet
被标记为abstract
。它不包含实现,必须在派生类中实现。
模块
当我们编写代码时,通常将其分成函数和这些函数内部的块。程序的大小可能会迅速增加,个别函数开始混为一体。如果我们将它们分成像模块这样的大型组织单位,可以使这样的程序更易读。在编写程序的开始阶段,您可能不知道如何构造它,可以使用无结构的原则。当您的代码变得稳定时,可以将功能片段放入单独的模块中,以便跟踪、更新和共享。我们将 TypeScript 的模块存储在文件中,每个文件恰好一个模块,每个模块一个文件。
JavaScript ES5 没有内置对模块的支持,我们使用 AMD 或 CommonJS 语法来处理它们。TypeScript 支持模块的概念。
作用域和模块如何相互依赖?JavaScript 的全局作用域无法访问执行模块的作用域。它为每个单独的执行模块创建自己的作用域,因此模块内声明的所有内容都无法从外部访问。我们需要显式导出它们以使其可见,并导入它们以使用它们。模块之间的关系是根据导出和导入在文件级别上定义的。任何文件都定义了顶级的export
或import
,并被视为一个模块。这里有一个string-validator.ts
文件,其中包含导出的声明:
export interface StringValidator {
isAcceptable(s: string): boolean;
}
我创建了另一个文件zip-validator.ts
,其中包含几个成员,但只导出其中一个,以隐藏另一个成员:
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
如果您的模块扩展其他模块,可以重新导出声明。这里的validators.ts
包含一个模块,包装其他验证器模块,并将它们的所有导出组合在一个地方:
export * from "./string-validator";
export * from "./zip-validator";
现在我们可以使用一种导入形式导入验证器模块。这是一个模块的单个导出:
import { StringValidator } from "./validators";
let strValidator = new StringValidator();
为了防止命名冲突,我们可以重命名导入的声明:
import { ZipCodeValidator as ZCV } from "./validators";
let zipValidator = new ZCV();
最后,我们可以将整个模块导入到单个变量中,并使用它来访问模块导出:
import * as validator from "./validators";
let strValidator = new validator.StringValidator();
let zipValidator = new validator.ZipCodeValidator();
泛型
TypeScript 的作者们竭尽全力帮助我们编写可重用的代码。其中一个帮助我们创建可以与各种类型一起工作而不是单一类型的代码的工具是泛型。泛型的好处包括:
-
允许您编写代码/使用方法,这些方法是类型安全的。
Array<string>
保证是一个字符串数组。 -
编译器可以对代码进行编译时检查,以确保类型安全。任何尝试将
number
分配到字符串数组中都会导致错误。 -
比使用
any
类型更快,以避免转换为所需的引用类型。 -
允许您编写适用于许多具有相同基本行为的类型的代码。
这是我创建的类,向您展示泛型有多么有用:
class Box<T> {
private _value : T;
set value(val : T) {
this._value = val;
}
get value() : T {
return this._value;
}
}
这个类保留特定类型的单个值。我们可以使用相应的 getter 和 setter 方法来设置或返回它:
var box1 = new Box<string>();
box1.setValue("Hello World");
console.log(box1.getValue());
var box2 = new Box<number>();
box2.setValue(1);
console.log(box2.getValue());
var box3 = new Box<boolean>();
box3.setValue(true);
console.log(box3.getValue());
// Out: Hello World
// Out: 1
// Out: true
什么是 promises?
承诺代表异步操作的最终结果。有许多支持在 TypeScript 中使用承诺的库。但在开始讨论这个之前,让我们先谈一下执行 JavaScript 代码的浏览器环境。
事件循环
每个浏览器选项卡都有一个事件循环,并使用不同的任务来协调事件、用户交互、运行脚本、渲染、网络等。它有一个或多个队列来保持任务的有序列表。其他进程围绕事件循环运行,并通过向其队列添加任务与其通信,例如:
-
计时器在给定时间后等待,然后将任务添加到队列
-
我们可以调用
requestAnimationFrame
函数来协调 DOM 更新 -
DOM 元素可以调用事件处理程序
-
浏览器可以请求解析 HTML 页面
-
JavaScript 可以加载外部程序并对其进行计算
上面列表中的许多项目都是 JavaScript 代码。它们通常足够小,但如果运行任何长时间的计算,可能会阻塞其他任务的执行,导致用户界面冻结。为了避免阻塞事件循环,我们可以:
-
使用web worker API在浏览器的不同进程中执行长时间运行的计算
-
不同步等待长时间运行的计算结果,并允许任务通过事件或回调异步通知我们结果
通过事件实现的异步结果
以下代码使用基于事件的方法来说服我们,并添加事件侦听器以执行内部的小代码片段:
var request = new XMLHttpRequest();
request.open('GET', url);
request.onload = () => {
if (req.status == 200) {
processData(request.response);
} else {
console.log('ERROR', request.statusText);
}
};
request.onerror = () => {
console.log('Network Error');
};
request.send(); // Add request to task queue
代码的最后一行中的send
方法只是向队列添加另一个任务。如果您多次接收结果,则此方法很有用,但对于单个结果来说,此代码相当冗长。
通过回调实现的异步结果
要通过回调管理异步结果,我们需要将回调函数作为参数传递给异步函数调用:
readFileFunctional('myfile.txt', { encoding: 'utf8' },
(text) => { // success
console.log(text);
},
(error) => { // failure
// ...
}
);
这种方法非常容易理解,但它也有其缺点:
-
混淆输入和输出参数
-
特别是在代码中组合了许多回调时,处理错误会变得复杂
-
从组合的异步函数返回结果更加复杂
通过承诺实现的异步结果
如我之前提到的,承诺代表将来发生的异步操作的最终结果。承诺具有以下优点:
-
您可以编写更干净的代码,而无需回调参数
-
您不需要调整底层架构的代码以交付结果
-
您的代码可以轻松处理错误
承诺可能处于以下状态之一:
-
待定状态:异步操作尚未完成
-
已解决状态:异步操作已完成,承诺具有值
-
Rejected state:异步操作失败,承诺有一个原因,指示失败的原因
承诺在解析或拒绝后变为不可变。
通常,您编写代码从函数或方法返回承诺:
function readFile(filename, encode){
return new Promise((resolve, reject) => {
fs.readFile(filename, enccode, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
}
我们使用 new
关键字与函数构造函数一起创建承诺。我们在构造函数中添加一个具有两个参数的工厂函数,该函数执行实际工作。两个参数都是回调函数。一旦操作成功完成,工厂函数将使用结果调用第一个回调。如果操作失败,它将使用原因调用第二个函数。
返回的承诺有几种方法,如 .then
和 .catch
,以通知我们执行的结果,以便我们可以相应地采取行动:
function readJSON(filename){
return readFile(filename, 'utf8').then((result) => {
console.log(result);
}, (error) => {
console.log(error);
});
}
我们可以调用另一个操作返回的承诺,快速转换原始操作的结果:
function readJSON(filename){
return readFile(filename, 'utf8').then((result) => {
return JSON.parse(result);
}, (error) => {
console.log(error);
}
}
Angular 2 概念
Angular 2 是一个用于构建 Web、移动和桌面应用程序的开发平台。它基于 Web 标准,使 Web 开发更简单、更高效,与 Angular JS 1.x 完全不同。Angular 2 的架构建立在 Web 组件标准之上,以便我们可以为其定义自定义 HTML 选择器和程序行为。Angular 团队开发 Angular 2 以在 ECMAScript 2015、TypeScript 和 Dart 语言中使用。
Angular 2 的构建块
任何构建在 Angular 2 上的 Web 应用程序包括:
-
具有 Angular 特定标记的 HTML 模板
-
指令和组件管理 HTML 模板
-
包含应用程序逻辑的服务
-
特殊的
bootstrap
函数帮助加载和启动 Angular 应用程序
模块
Angular 2 应用程序是许多模块的组合。Angular 2 本身是一组以 @angular
前缀开头的模块,组合成库:
-
@angular/core
是主要的 Angular 2 库,包含所有核心公共 API -
@angular/common
是一个限制 API 用于可重用组件、指令和表单构建的库 -
@angular/router
是支持导航的库 -
@angular/http
是帮助我们通过 HTTP 异步工作的库
元数据
元数据是我们可以通过 TypeScript 装饰器附加到底层定义的信息,告诉 Angular 如何修改它们。装饰器在 Angular 2 中扮演着重要的角色。
指令
指令是 Angular 2 的基本构建块,允许您将行为连接到 DOM 中的元素。有三种类型的指令:
-
属性指令
-
结构指令
-
组件
指令是一个带有分配的@Directive
装饰器的类。
属性指令
属性指令通常改变元素的外观或行为。我们可以通过将其绑定到属性来改变多种样式,或者使用它将文本呈现为粗体或斜体。
结构指令
结构指令通过添加和移除其他元素来改变 DOM 布局。
组件
该组件是一个带有模板的指令。每个组件由两部分组成:
-
定义应用程序逻辑的类
-
视图,由组件控制,并通过属性和方法的 API 与其交互
组件是一个带有分配的@Component
装饰器的类。
模板
组件使用模板来呈现视图。它是常规的 HTML,具有自定义定义的选择器和特定于 Angular 的标记。
数据绑定
Angular 2 支持数据绑定,以通过组件的属性或方法更新模板的部分。绑定标记是数据绑定的一部分;我们在模板上使用它来连接两侧。
服务
Angular 2 没有对服务的定义。任何值、函数或特性都可以是服务,但通常它是一个为特定目的创建的类,并带有分配的@Injectable
装饰器。
依赖注入
依赖注入是一种设计模式,它帮助通过外部实体配置对象并解决它们之间的依赖关系。松散耦合系统中的所有元素对彼此的定义知之甚少或根本不知道。我们几乎可以用替代实现替换任何元素,而不会破坏整个系统。
SystemJS 加载器和 JSPM 包管理器
我们已经讨论了 TypeScript 模块,现在是时候谈谈我们可以用来加载脚本中的模块的工具了。
SystemJS 加载器
SystemJS是一个通用的动态模块加载器。它在 GitHub 上托管源代码,地址为github.com/systemjs/systemjs
。它可以以以下格式在 Web 浏览器和 Node.js 中加载模块:
-
ECMAScript 2015(ES6)或 TypeScript
-
AMD
-
CommonJS
-
全局脚本
SystemJS 通过模块命名系统加载具有精确循环引用、绑定支持和资产的模块,如 CSS、JSON 或图像。开发人员可以通过插件轻松扩展加载器的功能。
我们可以将 SystemJS 加载器添加到我们未来的项目中:
-
通过直接链接到内容传送网络(CDN)
-
通过 npm 管理器安装
在这两种情况下,我们在代码中包含对 SystemJS 库的引用,并通过config
方法进行配置:
<!DOCTYPE html>
<html>
<head>
<script src="https://jspm.io/system.js"></script>
<script src="https://jspm.io/system.js"></script>
<script>
System.config({
packages: {
'./': {
defaultExtension: false
}
}
});
</script>
<script>
System.import('./app.js');
</script>
</head>
<body>
<div id="main"></div>
</body>
</html>
我们将在本章稍后讨论通过 npm 管理器进行安装。
JSPM 包管理器
SystemJS 的开发人员遵循单一职责原则,并实现了一个仅执行一项任务的加载器。为了使模块在项目中可用,我们需要使用包管理器。我们在开头谈到了 npm 包管理器,现在我们将谈论坐落在 SystemJS 之上的 JSPM 包管理器。它可以:
-
从任何注册表(如 npm 和 GitHub)下载模块
-
使用单个命令将模块编译成简单、分层和自执行的捆绑包
JSPM 包管理器看起来像 npm 包管理器,但它将浏览器加载器放在首位。它可以帮助您组织无缝的工作流程,以便在浏览器中轻松安装和使用库。
编写您的第一个应用程序
现在,当一切就绪时,是时候创建我们的第一个项目了,实际上它是一个 npm 模块。打开终端并创建名为hello-world
的文件夹。我特意遵循 npm 包命名约定:
-
包名长度应大于零且不得超过 214
-
包名中的所有字符必须为小写
-
包名可以包含/包括连字符
-
包名必须包含任何 URL 安全字符(因为名称最终成为 URL 的一部分)
-
包名不应以点或下划线字母开头
-
包名不应包含任何前导或尾随空格
-
包名不能与
node.js/io.js
核心模块或保留/黑名单名称相同,如http
、stream、node_modules
等。
将文件夹移入并运行命令:
**npm init**
npm 将询问您几个问题以创建package.json
文件。该文件以 JSON 格式保存有关您的包的重要信息:
-
项目信息如名称、版本、作者和许可证
-
项目依赖的一组包
-
一组预配置的用于构建和测试项目的命令
package.js
可能如下所示:
{
"name": "hello-world",
"version": "1.0.0",
"description": "The Hello World",
"author": " Put Your Name Here",
"license": "MIT"
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
}
}
我们已经准备好配置我们的项目了。
TypeScript 编译配置
运行 Visual Studio 代码并打开项目文件夹。我们需要创建一个配置文件,指导 TypeScript 编译器在哪里找到源文件夹和所需的库,以及如何编译项目。从 文件 菜单创建 tsconfig.json
文件,并复制/粘贴以下内容:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": false
},
"exclude": [
"node_modules",
"typings/main",
"typings/main.d.ts"
]
}
让我们更仔细地看一下 compilerOptions
:
-
target
选项指定 ECMAScript 版本,例如es3
,es5
或es6
。 -
module
选项指定模块代码生成器,可以是以下之一:none
,commojs
,amd
,system
,umd
,es6
或es2015
。 -
moduleResolution
选项确定模块的解析方式。使用node
来进行Node.js/io.js
风格的解析,或者使用classic
。 -
sourceMap
标志告诉编译器生成相应的map
文件。 -
emitDecoratorMetadata
会为源中装饰的声明发出设计类型元数据。 -
experimentalDecorator
启用对 ES7 装饰器的实验性支持,比如迭代器、生成器和数组推导。 -
removeComments
会移除除版权头注释以外的所有注释,这些注释以/*!
开头。 -
noImplicitAny
会在具有隐含的any
类型的表达式和声明上引发错误。
您可以在这里找到编译器选项的完整列表:www.typescriptlang.org/docs/handbook/compiler-options.html
。
TypeScript 编译器需要 JavaScript 库的类型定义文件,这些文件位于我们项目的 node_modules
中,因为它无法本地识别它们。我们可以通过 typings.json
文件来帮助它。您应该创建该文件并复制/粘贴以下内容:
{
"ambientDependencies": {
"es6-shim": "registry:dt/es6-shim#0.31.2+20160317120654"
}
}
我们应该提供足够的信息给 typings 工具来获取任何 typings 文件:
-
dt 注册表位于 DefinitelyTyped 源中。这个值可以是 npm,git
-
DefinitelyTyped
源中的包名是es6-shim
。 -
我们正在寻找更新于
2016.03.17 12:06:54
的版本0.31.2
。
任务自动化和依赖项解析
现在,是时候将应用程序所需的库添加到 package.json
文件中了。请相应地更新它:
{
"name": "hello-world",
"version": "1.0.0",
"description": "The Hello World",
"author": "Put Your Name Here",
"license": "MIT",
"scripts": {
"start": "tsc && concurrently "npm run tsc:w" "npm run lite" ",
"lite": "lite-server",
"postinstall": "typings install",
"tsc": "tsc",
"tsc:w": "tsc -w",
"typings": "typings"
},
"dependencies": {
"@angular/common": "~2.0.1",
"@angular/compiler": "~2.0.1",
"@angular/core": "~2.0.1",
"@angular/http": "~2.0.1",
"@angular/platform-browser": "~2.0.1",
"@angular/platform-browser-dynamic": "~2.0.1",
"@angular/router": "~3.0.1",
"@angular/upgrade": "~2.0.1",
"systemjs": "0.19.39",
"core-js": "².4.1",
"reflect-metadata": "⁰.1.8",
"rxjs": "5.0.0-beta.12",
"zone.js": "⁰.6.25",
"angular-in-memory-web-api": "~0.1.1",
"bootstrap": "4.0.0-alpha.4"
},
"devDependencies": {
"concurrently": "³.0.0",
"lite-server": "².2.2",
"typescript": "².0.3",
"typings":"¹.4.0"
}
}
我们的配置包括 scripts
来处理常见的开发任务,比如:
-
postinstall
脚本在包安装后运行 -
start
脚本由 npm 的start
命令运行 -
任意脚本
lite
,tsc
,tsc:w
和typings
都可以通过npm run <script>
来执行。
您可以在以下网页上找到更多文档:docs.npmjs.com/misc/scripts
。
完成配置后,让我们运行 npm
管理器来安装所需的软件包。返回到终端并输入以下命令:
**npm i**
在安装过程中,您可能会看到以红色开始的警告消息:
**npm WARN**
如果安装成功完成,您应该忽略它们。安装完成后,npm 执行 postinstall
脚本来运行 typings
安装。
创建和引导 Angular 组件
Angular 2 应用程序必须始终具有顶层组件,其中包含所有其他组件和逻辑。让我们创建它。转到 Visual Studio 代码并在根目录下创建一个名为 app
的子文件夹,我们将在其中保存源代码。在 app
文件夹下创建文件 app.component.ts
,并复制/粘贴以下内容:
// Import the decorator class for Component
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: '<h1> Hello, World</h1>'
})
export class AppComponent { }
正如您所看到的,我们通过 @Component
装饰器向 AppComponent
类添加了元数据。此装饰器告诉 Angular 如何通过以下选项的配置来处理该类:
-
selector
定义了我们的组件将链接的 HTML 标签的名称 -
我们在
providers
属性中传递任何服务。在此注册的任何服务都将对该组件及其子组件可用 -
我们可以将任意数量的样式文件赋予
styles
特定组件 -
template
属性将保存组件的模板 -
template url
是指向包含视图模板的外部文件的 URL
我们需要导出 AppComponent
类,以使其从其他模块可见,并且 Angular 可以实例化它。
Angular 应用程序是由多个使用 NgModule
装饰器标记的模块组成。任何应用程序必须至少有一个根模块,因此让我们在 app.module.ts
文件中创建 AppModule
:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
imports: [ BrowserModule ]
})
export class AppModule { }
WebBrowser 是专为 Web 浏览器而设计的模块和提供程序的集合,例如 document DomRootRenderer 等。我们将 WebBrowser 导入应用程序模块,以使所有这些提供程序和模块在我们的应用程序中可用,从而减少所需的样板代码编写量。Angular 包含 ServerModule
:用于服务器端的类似模块。
现在我们需要启动我们的应用程序。在 app
文件夹下创建 main.ts
文件,并复制/粘贴以下内容:
import { platformBrowserDynamic } from
'@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);
最后但并非最不重要的是,我们依赖于bootstrap
函数来加载顶层组件。我们从'@angular/platform-browser-dynamic'
中导入它。Angular 有一种不同类型的bootstrap
函数:
-
Web 工作者
-
在移动设备上进行开发
-
在服务器上渲染应用程序的第一页
Angular 在实例化任何组件后执行几项任务:
-
它为其创建了一个影子 DOM
-
将选定的模板加载到影子 DOM 中
-
它创建了所有配置了'providers'和'viewProviders'的可注入对象
最后,Angular 2 会对所有模板表达式和语句进行评估,与组件实例进行比较。
现在,在 Microsoft Visual Studio 代码中在根文件夹下创建index.html
文件,并包含以下内容:
<html>
<head>
<title>Angular 2 First Project</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- 1\. Load libraries -->
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js">
</script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js">
</script>
<script src="node_modules/systemjs/dist/system.src.js">
</script>
<!-- 2\. Configure SystemJS -->
<script src="systemjs.config.js"></script>
<script>
System.import('app')
.catch(function(err){ console.error(err); });
</script>
</head>
<!-- 3\. Display the application -->
<body>
<my-app>Loading...</my-app>
</body>
</html>
因为我们正在引用systemjs.config.js
文件,让我们在根文件夹中创建它,其中包含以下代码:
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api',
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
},
'angular-in-memory-web-api': {
main: './index.js',
defaultExtension: 'js'
}
}
});
})(this);
编译和运行
我们已经准备好运行我们的第一个应用程序。返回终端并键入:
**npm start**
此脚本运行两个并行的 Node.js 进程:
-
TypeScript 编译器处于监视模式
-
静态的
lite-server
加载index.html
并在应用程序文件更改时刷新浏览器
在您的浏览器中,您应该看到以下内容:
提示
您可以在chapter_1/1.hello-world
文件夹中找到源代码。
添加用户输入
现在我们需要包含我们的文本输入,并指定我们想要使用的模型。当用户在文本输入中键入时,我们的应用程序会在标题中显示更改后的值。此外,我们应该将FormsModule
导入到AppModule
中:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
**import { FormsModule } from '@angular/forms';**
import { AppComponent } from './app.component';
@NgModule({
imports: [ BrowserModule, **FormsModule**
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
这是app.component.ts
的更新版本:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>Hello, {{name || 'World'}}</h1>
**<input type="text" [(ngModel)]="name" placeholder="name">**
`})
export class AppComponent {
name: string = 'World';
}
ngModel
属性在该元素上声明了一个模型绑定,我们在输入框中输入的任何内容都将被 Angular 自动绑定到它。显然,这不会通过魔术显示在我们的页面上;我们需要告诉框架我们想要在哪里回显它。要在页面上显示我们的模型,我们只需要用双大括号包裹它的名称:
**{{name}}**
我将其放在了我们的<h1>
标签中的 World 位置,并在浏览器中刷新了页面。如果您在输入框中输入您的姓名,您会注意到它会实时自动显示在您的标题中。Angular 为我们完成了所有这些工作,而我们没有写一行代码:
现在,虽然这很棒,但如果我们可以有一个默认值,这样在用户输入他们的名字之前就不会看起来不完整,那就更好了。令人惊奇的是,花括号之间的所有内容都被解析为 Angular 表达式,因此我们可以检查并查看模型是否有值,如果没有,它可以回显'World'
。Angular 将其称为表达式,只需添加两个管道符号,就像在 TypeScript 中一样:
{{name || 'World'}}
记住这是 TypeScript,这就是为什么我们需要在这里包含引号,让它知道这是一个字符串,而不是模型的名称。删除它们,您会注意到 Angular 再次显示为空。这是因为名称和World
模型都未定义。
提示
您可以在chapter_1/2.hello-input
文件夹中找到源代码。
集成 Bootstrap 4
现在我们已经创建了我们的Hello World
应用程序,并且一切都按预期工作,是时候参与 Bootstrap 并为我们的应用程序添加一些样式和结构了。在撰写本书时,Bootstrap 4 处于 alpha 版本,因此请记住您的应用程序的代码和标记可能略有不同。我们需要将 Bootstrap 4 样式表添加到index.html
文件中:
<meta name="viewport" content="width=device-width, initial-scale=1">
**<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css">**
<link rel="stylesheet" href="styles.css">
应用程序目前左对齐,一切看起来很拥挤,所以让我们首先用一些脚手架解决这个问题。Bootstrap 带有一个很棒的移动优先响应式网格系统,我们可以利用它来包含一些div
元素和类。不过,首先让我们在内容周围获取一个容器,以立即清理它:
注意
移动优先是一种首先为最小屏幕设计/开发并添加设计元素而不是减少元素的方法。
<div class="container">
<h1>Hello, {{name || 'World'}}</h1>
<input type="text" [(ngModel)]="name">
</div>
如果您调整浏览器窗口大小,您应该开始注意到框架的一些响应性,并看到它的折叠:
现在,我认为将其包装在 Bootstrap 称为 Jumbotron 的东西中是个好主意(在以前的 Bootstrap 版本中,这被称为 hero unit)。这将使我们的标题更加突出。我们可以通过将我们的H1
和input
标签包装在一个带有jumbotron
类的新div
中来实现这一点:
<div class="container">
<div class="jumbotron">
<h1>Hello, {{name || 'World'}}</h1>
<input type="text" ng-model="name">
</div>
</div>
它开始看起来好多了,但我对我们的内容触及浏览器顶部并不太满意。我们可以通过一个页面标题让它看起来更好,但是那个输入框对我来说看起来还是不合适。
首先,让我们整理一下页面标题:
<div class="container">
<div class="page-header">
<h2>Chapter 1 <small>Hello, World</small></h2>
</div>
<div class="jumbotron">
<h1>Hello, {{name || 'World'}}</h1>
<input type="text" [(ng-model)]="name">
</div>
</div>
我在这里包含了章节编号和标题。在我们的<h2>
标签中的<small>
标签给我们的章节编号和标题之间提供了一个很好的区分。page-header 类本身只是给我们一些额外的边距和填充,以及底部的细微边框。
我认为我们最需要改进的是输入框。Bootstrap 带有一些很酷的输入样式,所以让我们包含进去。首先,我们需要在文本输入中添加 form-control 类。这将把宽度设置为 100%,并且在我们聚焦在元素上时,还会带出一些漂亮的样式,比如圆角和发光。
<input type="text" [(ngModel)]="name" class="form-control">
好多了,但是对我来说,与标题相比,它看起来有点小。Bootstrap 提供了两个额外的类,我们可以包含其中一个,要么使元素更小,要么更大:form-control-lg
和form-control-sm
。在我们的情况下,我们想要的是form-control-lg
类,所以继续添加到输入框中。
<input type="text" [(ngModel)]="name"
class="form-control form-control-lg">
提示
你可以在chapter_1/3.hello-bootstrap
中找到源代码。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
摘要
我们的应用程序看起来很棒,而且正好按照预期工作,所以让我们回顾一下我们在第一章学到的东西。
首先,我们看到了如何设置工作环境并完成 TypeScript 速成课程。
我们创建的Hello World
应用程序虽然非常基础,但演示了一些 Angular 的核心功能:
-
组件指令
-
应用程序引导
-
双向数据绑定
所有这些都是可能的,而不需要写一行 TypeScript,因为我们创建的组件只是为了演示双向数据绑定。
使用 Bootstrap,我们利用了许多可用的组件,比如 Jumbotron 和 page-header 类,为我们的应用程序增添了一些样式和内容。我们还看到了框架的新的移动优先响应式设计,而不会用不必要的类或元素来混乱我们的标记。
在第二章中,使用 Bootstrap 组件,我们将探索更多 Bootstrap 基础知识,并介绍本书将要构建的项目。
第二章:使用 Bootstrap 组件
在网页设计和开发领域,我们听说过很多关于Twitter Bootstrap 3。我们今天的英雄是Bootstrap 4,这是一个 CSS 框架,最终帮助设计网页应用更容易更快。
在本章中,我将解释如何通过展示演示布局页面来开始使用 Bootstrap 4,以及如何探索框架并根据您的要求进行自定义。在本章结束时,您将对以下内容有扎实的理解:
-
如何使用Sass(Syntactically Awesome Style Sheets)
-
如何将 Bootstrap 4 添加到您的项目中
-
如何使用网格和容器设计布局
-
如何添加导航元素
-
如何自定义选定的组件
Bootstrap 4
在第一章中,我们简要谈到了 Twitter Bootstrap 4,但现在是时候更仔细地看看这个 CSS 框架了。然而,在深入了解 Bootstrap 4 之前,让我们先谈谈所有新引入的功能:
-
Bootstrap 4 的源 CSS 文件是基于 Sass 的
-
rem
是主要的 CSS 单位,而不是px
-
全局字体大小从
14px
增加到16px
-
新的网格层级已经为小型设备(从
~480px
及以下)添加了 -
Bootstrap 4 可选择支持Flex Box Grid
-
添加了改进的媒体查询
-
新的Card组件取代了Panel,Well和Thumbnail
-
有一个名为
Reboot.css
的新的重置组件 -
使用 Sass 变量可以自定义一切
-
不再支持 IE 8 和 iOS 6
-
它不再支持非响应式使用
Sass 简介
如果您不熟悉 Sass,我认为现在是介绍这个梦幻般的 CSS 预处理框架的合适时机。毫无疑问,对 CSS 文件进行预处理可以让您编写更简洁、更简洁的样式表。Sass 的第一个版本的语法使用缩进,不需要分号,有简写操作符,并使用.sass
文件扩展名。它与 CSS 有很大不同,以至于 Sass 第 3 版开始支持带有大括号、分号和.scss
文件扩展名的新格式。让我们将各种形式进行比较。
这是一个原始的 CSS 样式:
#container {
width:100px;
padding:0;
}
#container p {
color: red;
}
在扩展名为.sass
的文件中,我们应该只使用缩进,并且它严重依赖于空格:
$red: #ff0000
#container
width:100px
padding: 0
p
color:$red
在扩展名为.scss
的文件中,我们使用大括号和分号:
$red: #ff0000;
#container {
width:100px;
padding:0;
p {
color :$red;
}
}
最终由您决定使用哪种样式,但我将在本书中使用基于.scss
文件的最新样式。
设置 Ruby
在开始使用 Sass 之前,你需要安装 Ruby,但首先检查一下你是否已经有了。打开终端,输入 ruby -v
。
如果你没有收到错误,请跳过安装 Ruby 的步骤。否则,你将从官方 Ruby 网站www.ruby-lang.org/en/documentation/installation
安装一个新的 Ruby。
Sass 的设置
完成 Ruby 的安装后,打开终端并输入以下命令。
- 对于 Windows:
**gem install sass**
- 对于 Linux 和 Mac:
**sudo gem install sass**
这个命令将为你安装 Sass 和必要的依赖项。运行以下命令检查你的 PC 上是否安装了 Sass:
**sass -v**
Sass 必须以版本号做出响应:
**Sass 3.4.22 (Selective Steve)**
请记住,我电脑上安装的 Sass 版本可能与你的不同。如果这些命令给你一个版本号,你就可以开始使用 Sass 进行开发了。
现在我们已经安装了 Sass,我们可以探索它的文件并将它们输出为 CSS。你可以使用 CLI 或 GUI 来开始使用 Sass。如果你喜欢 GUI 风格的开发,请从以下列表中选择一个:
-
CodeKit(Mac,付费):
incident57.com/codekit
-
Compass.app(Windows,Mac,Linux,付费,开源):
compass.kkbox.com/
-
Ghostlab(基于 Web,付费):
www.vanamco.com/ghostlab
-
Hammer(Mac,付费):
hammerformac.com
-
Koala(Windows,Mac,Linux,开源):
koala-app.com
-
LiveReload(Mac,付费,开源):
livereload.com
-
Prepros(Windows,Mac,Linux,付费):
prepros.io
-
Scout(Windows,Mac,开源):
github.com/scout-app/scout-app
我个人更喜欢 Scout GUI,它在一个独立的 Ruby 环境中运行 Sass 和 Compass;它会处理所有繁重的工作,所以我们永远不必担心设置 Ruby 之类的技术问题。
我推荐的另一个有趣的选择是基于 Web 的 Sass 游乐场SassMeister,你可以在www.sassmeister.com
找到。我们将在 Sass 速成课程中稍微使用一下它。
Sass 速成课程
Sass 的主要理念是我们创建可重用、不冗长的易于阅读和理解的代码。让我们看看是哪些特性实现了这一点。请打开 SassMeister 网站,准备进行练习。
变量
我们可以在 Sass 中创建变量,特别是为了在整个文档中重用它们。变量的可接受值包括:
-
数字
-
字符串
-
颜色
-
空值
-
列表
-
映射
我们使用$
符号来定义一个变量。切换到 SassMeister 并创建我们的第一个变量:
$my-pad: 2em;
$color-primary: red;
$color-secondary: #ff00ed;
SassMeister 对它们进行编译,但没有输出任何 CSS。我们只是在作用域中定义变量,就是这样。我们需要在 CSS 声明中使用它们才能看到编译的结果:
body {
background-color: $color-primary;
}
.container {
padding: $my-pad;
color: $color-secondary;
}
这是从 Sass 到 CSS 的编译结果:
body {
background-color: red;
}
.container {
padding: 2em;
color: #ff00ed;
}
数学表达式
Sass 允许我们在算术表达式中使用以下数学运算符:
-
加法(+)
-
减法(-)
-
除法(/)
-
乘法(*)
-
模数(%)
-
相等(==)
-
不相等(!=)
转到 SassMeister 并尝试一些引入的数学运算:
$container-width: 100%;
$font-size: 16px;
.container {
width: $container-width;
}
.col-4 {
width: $container-width / 4;
font-size: $font-size - 2;
}
这是一些 CSS 编译器代码:
.container {
width: 100%;
}
.col-4 {
width: 25%;
font-size: 14px;
}
我想警告您不要在数学运算中使用不兼容的单位。在您的游乐场中尝试以下 Sass 代码:
h2 {
// Error: Incompatible units: 'em' and 'px'.
width: 100px + 2em;
// Result: 52px
height: 50px + 2;
}
然而,将两个相同单位的数字相乘会产生无效的 CSS 值:
h2 {
// Error: 100px*px isn't a valid CSS value.
width: 50px * 2px;
}
斜杠符号(/)是 CSS 简写属性的一部分。例如,这里是字体声明:
font-style: italic;
font-weight: bold;
font-size: .8em;
line-height: 1.2;
font-family: Arial, sans-serif;
它可以缩短为以下内容:
font: italic bold .8em/1.2 Arial, sans-serif;
为了避免任何可能的问题,您应该始终将包含非变量值的除法运算符的表达式用括号括起来,例如:
h2 {
// Result: Outputs as CSS
font-size: 16px / 24px;
// Result: Does division because uses parentheses
width: (16px / 24px);
}
注意
应避免在数学运算中使用不同的单位。
函数
Sass 有一套丰富的内置函数,您可以在以下地址找到它们:
sass-lang.com/documentation/Sass/Script/Functions.html
这是使用rgb($red, $green, $blue)
函数的最简单的例子。它从红色、绿色和蓝色值创建一个color
:
$color-secondary: rgb(ff,00,ed);
嵌套
Sass 允许我们在另一个声明中有一个声明。在以下原始 CSS 代码中,我们定义了两个语句:
.container {
width: 100px;
}
.container h1 {
color: green;
}
我们有一个容器类和容器内的标题样式声明。在 Sass 中,我们可以创建紧凑的代码:
.container {
width: 100px;
h1 {
color: green;
}
}
嵌套使代码更易读,更简洁。
导入
Sass 允许您将样式分成单独的文件并将其导入到另一个文件中。我们可以使用@import
指令,带有或不带有文件扩展名。有两行代码产生相同的结果:
@import "components.scss";
@import "components";
扩展
如果您需要从现有样式中继承样式,Sass 有@extend
指令可以帮助您:
.input {
color: #555;
font-size: 17px;
}
.error-input {
@extend .input;
color: red;
}
这是 Sass 编译器正确处理编译代码的结果:
.input, .error-input {
color: #555;
font-size: 17px;
}
.error-input {
color: red;
}
占位符
在您想要扩展不存在的一组样式声明时,Sass 可以帮助使用占位符选择器:
%input-style {
font-size: 14px;
}
.input {
@extend %input-style;
color: #555;
}
我们使用%
符号来给类名加前缀,并借助@extend
,魔法就发生了。Sass 不会渲染占位符,它只会渲染其扩展元素的结果。以下是编译后的代码:
.input {
font-size: 14px;
}
.input {
color: #555;
}
混合
我们可以使用 mixin 创建可重用的 CSS 样式块。Mixin 始终返回标记代码。我们使用@mixin
指令来定义 mixin,并使用@include
在文档中使用它们。您可能以前经常看到以下代码:
a:link { color: white; }
a:visited { color: blue; }
a:hover { color: green; }
a:active { color: red; }
实际上,更改元素的颜色取决于状态。通常我们一遍又一遍地写这段代码,但是使用 Sass,我们可以这样做:
@mixin link ($link, $visit, $hover, $active) {
a {
color: $link;
&:visited {
color: $visit;
}
&:hover {
color: $hover;
}
&:active {
color: $active;
}
}
}
这里的&
符号指向父元素,即锚元素。让我们在下面的示例中使用这个 mixin:
.component {
@include link(white, blue, green, red);
}
这是编译为 CSS 代码的 mixin:
.component a {
color: white;
}
.component a:visited {
color: blue;
}
.component a:hover {
color: green;
}
.component a:active {
color: red;
}
函数指令
函数指令是 Sass 的另一个特性,它通过@return
指令帮助创建可重用的 CSS 样式返回值块。我们使用@function
指令来定义它:
@function getTableWidth($columnWidth,$numColumns,$margin){
@return $columnWidth * $numColumns + $margin * 2;
}
在这个函数中,我们计算表的宽度取决于单独的列宽、列数和边距值:
$column-width: 50px;
$column-count: 4;
$margin: 2px;
.column {
width: $column-width;
}
.table {
background: #1abc9c;
height: 200px;
width: getTableWidth($column-width,$column-count,$margin);
margin: 0 $margin;
}
生成的 CSS 代码如下:
.column {
width: 50px;
}
.table {
background: #1abc9c;
height: 200px;
width: 204px;
margin: 0 2px;
}
我认为是时候结束我们的 Sass 速成课程了,但请不要认为你对它了解一切。Sass 非常强大,所以如果你决定继续我们在这里开始的旅程,请在这里获取更多信息:sass-lang.com/documentation/file.SASS_REFERENCE.html
。
示例项目
让我们在阅读本书的同时谈谈我们将开发什么样的 Web 应用程序。我已经决定电子商务应用程序是展示不同 Bootstrap 4 组件完整风格的最佳候选人。
电子商务这个词,如今我们所理解的,指的是通过互联网购买和销售商品或服务,因此我们基于真实场景设计网络应用程序。在介绍之后,我们将整理出一个高层次的客户需求清单。然后,我们将准备一系列模型,这将帮助您更清楚地了解最终应用程序对最终用户的外观。最后,我们将把客户需求分解成一系列实施任务,并构建应用程序,以便清晰定义功能组件之间的责任和交互。
情景
Dream Bean 是一家小型杂货店,与几家当地农场合作,供应有机食品和农产品。该店拥有长期的客户群,并为该地区带来不断增长的利润。由于最近的一项调查显示,其常客中有 9%持续拥有互联网访问权限,83%有兴趣使用此服务,因此该店决定调查提供在线送货服务的可能性。
杂货店经理要求您创建一个网站,使他们的客户能够从各种设备上在线购物,包括手机、平板电脑和台式电脑。
收集客户需求
在做出任何设计或实施决策之前,您需要从客户那里收集信息;因此,在与客户直接沟通后,我们有以下条件:
-
客户可以购买实体店中的产品。有以下产品类别:
-
肉类
-
海鲜
-
面包店
-
乳制品
-
水果和蔬菜
-
带走
-
客户可以浏览所有商品或按类别筛选商品
-
客户有一个虚拟购物车
-
客户可以在购物车中添加、删除或更新商品数量
-
客户可以查看所有内容的摘要
-
客户可以下订单并通过安全的结账流程付款
准备用例
现在,当需求确定后,是时候与 Dream Bean 的经理合作,了解网站应该如何展现和运作。我们创建一组用例,描述客户如何使用网络应用程序:
-
客户访问欢迎页面,并按类别选择产品
-
客户在所选类别页面浏览产品,然后将产品添加到购物车中
-
客户点击信息按钮打开包含产品完整信息的单独页面,然后将产品添加到购物车
-
客户继续购物并选择不同的类别
-
客户从这个类别中添加了几种产品到购物车
-
客户选择查看购物车选项并更新购物车中产品的数量
-
客户验证购物车内容并进行结账
-
在结账页面,客户查看订单成本和其他信息,填写个人数据,然后提交详细信息
我们继续与 Dream Bean 的工作人员合作,需要以以下一种方式创建模型:
-
使用故事板软件
-
创建一组线框
-
使用纸质原型
我使用Balsamiq Mockups来帮助我快速创建线框。Balsamiq Mockups 的完全功能试用版本可用 30 天,并可从官方网站balsamiq.com
下载。
欢迎页面
欢迎页面是应用程序的入口点。它向客户介绍业务和服务,并使他/她能够导航到任何产品类别。我们在欢迎页面中间添加了一个幻灯片,如下所示:
欢迎页面的线框
产品页面
产品页面提供了所选类别中所有商品的列表。从这个页面,客户可以查看所有产品信息,并将列出的任何产品添加到他或她的购物车中。用户还可以导航到任何提供的类别,或者使用快速购物功能按名称搜索产品,如下所示:
产品页面的线框
产品页面
产品页面显示有关产品的信息。在此页面上,客户可以执行以下操作:
-
检查产品的可用性
-
更新产品的数量
-
通过点击“购买”将产品添加到购物车
-
通过点击“继续购物”返回产品列表
产品页面的线框
购物车页面
购物车页面列出了用户购物车中的所有物品。它显示了每件物品的产品详细信息,用户可以从这个页面执行以下操作:
-
通过点击清空购物车来从购物车中删除所有商品
-
更新任何列出的物品的数量
-
点击“继续购物”返回产品列表
-
点击结账继续结账
以下是购物车页面可能的样子:
购物车页面的线框
结账页面
结账页面显示客户详细信息表单、购买条件和订单信息。客户应填写表单,确认付款,并点击提交按钮开始付款流程,如下所示:
结账页面的线框
我们已经准备好使用 Angular 2 和 Bootstrap 4 开始项目。我们已经将业务需求投影到模型上,现在需要做以下事情:
-
打开终端,创建名为
ecommerce
的文件夹并进入其中 -
将项目的内容从
ecommerce-seed
文件夹复制到新项目中 -
运行以下脚本以安装 npm 模块:
**npm install**
- 使用以下命令启动TypeScript监视器和 lite 服务器:
**npm run start**
这个脚本会打开网页浏览器并导航到项目的欢迎页面。我们已经准备好开始开发了。
注意
您可以在chapter_2/1.ecommerce-seed
文件夹中找到源代码。
使用网格和容器设计布局
Bootstrap 包括一个强大的面向移动设备的网格系统,用于构建各种形状和大小的设计,这听起来非常有前途,因为我们需要为项目创建多个页面。我们将使用网格系统通过一系列行和列来创建页面布局。由于 Bootstrap 是为移动设备优先开发的,我们使用了一些媒体查询来为我们的布局和界面创建合理的断点。这些断点主要基于最小视口宽度,并允许我们随着视口的变化而扩展元素。网格系统有三个主要组件,它们是:
-
容器
-
行
-
列
容器是 Bootstrap 中的核心和必需的布局元素。有两个类可以创建所有其他项目的容器:
-
您可以使用
container
类创建一个响应式的固定宽度容器。这个容器在托管元素的两侧没有额外的空间,并且它的max-width
属性在每个断点处都会改变。 -
您可以使用
container-fluid
类创建全宽度的容器。这个容器始终占据视口的 100%宽度。
要为我们的项目创建一个简单的布局,请打开app.component.html
文件,并在其中插入一个带有container
类的div
元素:
<div class="container">
</div>
我们可以嵌套容器,但大多数布局不需要。容器只是行的占位符,所以让我们在其中添加行:
<div class="container">
<div class="row">
</div>
</div>
行具有row
类,容器可以包含所需的行数。
注意
我建议使用一个或多个容器,所有行都在其中,以包裹页面内容并在屏幕上居中元素。
行是列的水平组。它只存在一个目的:保持列正确对齐。我们必须将页面内容仅放在列内,并指示要使用的列数。每行最多可以包含 12 列。
我们可以将列添加到行中,作为col
类的组合,并且它有前缀大小:
<div class="col-md-12">
Bootstrap 4 支持五种不同大小的显示屏,并且列类名称取决于它们:
-
col-xs
:用于超小显示屏(屏幕宽度小于 34em 或 544px) -
col-sm
:用于较小的显示屏(屏幕宽度 34em 或 544ps 及以上) -
col-md
:用于中等显示屏(屏幕宽度 48em 或 768px 及以上) -
col-lg
:用于更大的显示屏(屏幕宽度 62em 或 992px 及以上) -
col-xl
:用于超大显示屏(屏幕宽度 75em 或 1200px 及以上)
列类名称始终适用于屏幕宽度大于或等于断点大小的设备。
列的宽度设置为百分比,因此它始终是流动的,并且大小约等于父元素。每列都有水平填充,以在各列之间创建空间。第一列和最后一列有负边距,这就是为什么网格内的内容与网格外的内容对齐。这是一个针对超小设备的网格示例:
看看我们项目欢迎页面的示意图,并想象将其分成行和列:
欢迎页面的线框图
我们的标记至少有三行。第一行有一个带有公司标志和菜单的标题。它跨越 12 个中等大小的列,标记为col-md-12
。我现在使用网格,但以后我会将其更改为更合适的组件:
<div class="container">
<div class="row">
<div class="col-md-12 table-bordered">
<div class="product-menu">Logo and Menu</div>
</div>
</div>
<!-- /.row -->
</div>
第二个有一个单独的列,包含一个 1110x480px 的图像,并跨越所有 12 个中等大小的列,标记为col-md-12
,就像前一个一样:
<div class="container">
<div class="row">
<div class="col-md-12 table-bordered">
<img class="img-fluid center-block product-item"
src="http://placehold.it/1110x480" alt="">
</div>
</div>
<!-- /.row -->
最后一个包括六个产品类别的位置,每个类别占据的列数取决于布局的大小:
-
四个中等大小的列标记为
col-md-4
-
六个标记为
col-sm-6
的小列 -
用
col-xs-12
标记的十二个额外小的列
每个图像的尺寸为 270x171px。屏幕底部的标记相当长,所以我把它截断了:
<div class="row">
<div class="col-xs-12 col-sm-6 col-md-4 table-bordered">
<a href="#">
<img class="img-fluid center-block product-item"
src="http://placehold.it/270x171" alt="">
</a>
</div>
<!-- /.col -->
<div class="col-xs-12 col-sm-6 col-md-4 table-bordered">
<a href="#">
<img class="img-fluid center-block product-item"
src="http://placehold.it/270x171" alt="">
</a>
</div>
<!-- /.col -->
...
<div class="col-xs-12 col-sm-6 col-md-4 table-bordered">
<a href="#">
<img class="img-fluid center-block product-item"
src="http://placehold.it/270x171" alt="">
</a>
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</div>
<!-- /.container -->
我故意添加了 Bootstrap 类table-bordered
来显示列的边界。稍后我会将其移除。这是网站的外观结果:
如果我将视口更改为较小的尺寸,Bootstrap 立即将列转换为行,就像您在前面的图表中看到的那样。我在页面上没有使用真实图像,而是指向了placehold.it
。这是一个在网上动态生成指定尺寸占位图像的服务。像这样的链接placehold.it/270x171
返回尺寸为 270x171px 的占位图像。
使用图像
在我们的标记中,我使用了图像,所以请注意img-fluid
类,它使图像具有响应式行为:
<img class="img-fluid center-block product-item"
src="http://placehold.it/270x171" alt="">
该类的逻辑永远不会使图像变大于父元素。同时,它通过类轻量级地管理样式。您可以轻松地设计图片的形状如下:
-
用
img-rounded
类使其变成圆角。边框半径为 0.3rem -
用
img-circle
来使其成为圆形,所以边框半径变成了 50% -
用
img-thumbnail
来转换它
在我们的示例中,center-block
使图像居中,但您也可以使用辅助浮动或文本对齐类来对齐它:
-
pull-sm-left
类在小尺寸或更宽的设备上向左浮动 -
pull-lg-right
类在大尺寸和更大的设备上向右浮动 -
pull-xs-none
类防止在所有视口尺寸上浮动
注意
您可以在chapter_2/2.ecommerce-grid
文件夹中找到源代码。
现在,我想创建板块,并在页面底部用图像替换它们。我们可以用于此目的的最佳组件是Card。
使用 Cards
Card 组件是一个非常灵活和可扩展的内容容器,只需要少量的标记和类就可以做出很棒的东西。Cards 替换了 Bootstrap 3 中存在的以下元素:
-
面板
-
Wells
-
缩略图
创建它的最简单方法是向元素添加card
和card-block
类:
<div class="col-xs-12 col-sm-6 col-md-4">
<div class=" **card**
">
<img class="card-img-top center-block product-item"
src="http://placehold.it/270x171" alt="Bakery">
<div class="card-block">
<h4 class="card-title">Bakery</h4>
<p class="card-text">The best cupcakes, cookies, cakes,
pies, cheesecakes, fresh bread,
biscotti, muffins, bagels, fresh coffee
and more.</p>
<a href="#" class="btn btn-primary">Browse</a>
</div>
</div>
</div>
card-block
类在内容和卡片边框之间添加了填充空间。在我的示例中,我将其移动到内部,以使卡片标题与卡片边缘对齐。如果需要,您可以使用card-header
创建标题和card-footer
类创建页脚。正如您所看到的,它在卡片中包括了各种组件,如图像、文本、列表组等。这是我们的卡片组件的外观:
但这不是唯一使用卡片组件的地方。我们将在接下来的章节中经常使用它们。
注意
您可以在chapter_2/3.ecommerce-cards
文件夹中找到源代码。
使用按钮
我已经向卡片组件添加了一个按钮,并且我想谈谈它。您可以将按钮样式应用于以下元素:
-
标准的
button
在所有浏览器中都能正常工作。 -
具有
type="button"
的input
元素。 -
锚元素,只有在使用
role="button"
时才像按钮一样。仅用于触发页面功能,而不是链接到当前页面或部分内的新页面。 -
在处理复选框和单选按钮时的标签。
常规按钮样式
在 Bootstrap 4 中,我们可以找到七种按钮样式,每种都有不同的语义目的。btn
类为独立放置、在表单中或对话框中的按钮添加了样式的上下文变化、大小和状态:
主要行动样式提供了额外的视觉重量:
<button type="button" class=" **btn btn-primary**
">Primary</button>
次要的,比主要行动不太重要的样式提供了减少背景颜色的选项:
<button type="button" class=" **btn btn-secondary**
">Secondary</button>
成功指示任何成功的操作或位置行动:
<button type="button" class=" **btn btn-success**
">Success</button>
信息是为了指导用户进行信息性行动或警报:
<button type="button" class=" **btn btn-info**
">Info</button>
警告提供了警告和警告行动:
<button type="button" class=" **btn btn-warning**
">Warning</button>
危险指示危险或潜在的负面行动:
<button type="button" class=" **btn btn-danger**
">Danger</button>
链接按钮呈现为链接的按钮:
<button type="button" class=" **btn btn-link**
">Link</button>
轮廓按钮样式
您可以通过用.btn-outline-*
样式替换默认修改的类来删除任何预定义样式的按钮上的沉重背景图像和颜色。
<button type="button"
class=" **btn btn-outline-primary**
">Primary</button>
<button type="button"
class=" **btn btn-outline-secondary**
">Secondary</button>
<button type="button"
class=" **btn btn-outline-success**
">Success</button>
<button type="button"
class=" **btn btn-outline-info**
">Info</button>
<button type="button"
class=" **btn btn-outline-warning**
">Warning</button>
<button type="button"
class=" **btn btn-outline-danger**
">Danger</button>
注意
链接按钮没有轮廓(即没有btn-outline-link
类)。
按钮尺寸
按钮可以有小号和大号:
使用btn-sm
和btn-lg
类来实现这一点:
<button type="button"
class="btn btn-primary btn-lg">Large button</button>
<button type="button"
class="btn btn-primary btn-sm">Small button</button>
块级按钮样式
如果您计划创建占满父元素整个宽度的块级按钮,只需添加btn-block
类:
<button type="button"
class="btn btn-primary btn-lg btn-block">Block</button>
具有活动样式的按钮
按钮样式中的伪类根据用户操作更新元素的视觉状态,但如果您需要手动更改状态,请使用active
类:
<a href="#" class="btn btn-primary btn-lg active"
role="button">Primary link</a>
具有非活动状态的按钮
我们可以使用disabled
属性使按钮看起来不活动:
<button type="button" disabled
class="btn btn-lg btn-primary">Primary button</button>
单选按钮和复选框
Bootstrap 4 为input
元素提供了具有切换功能的按钮样式,类似于单选按钮和复选框。为了实现这一点,您需要创建包括组元素、标签和输入元素本身的大量构造:
**<div class="btn-group" data-toggle="buttons">**
**<label class="btn btn-primary active">**
<input type="checkbox" checked autocomplete="off">
Checkbox 1 (active)
</label>
**<label class="btn btn-primary">**
<input type="checkbox" autocomplete="off"> Checkbox 2
</label>
**<label class="btn btn-primary">**
<input type="checkbox" autocomplete="off"> Checkbox 3
</label>
</div>
**<div class="btn-group" data-toggle="buttons">**
**<label class="btn btn-primary active">**
<input type="radio" name="options" id="option1"
autocomplete="off" checked> Radio 1 (preselected)
</label>
**<label class="btn btn-primary">**
<input type="radio" name="options" id="option2"
autocomplete="off"> Radio 2
</label>
**<label class="btn btn-primary">**
<input type="radio" name="options" id="option3"
autocomplete="off"> Radio 3
</label>
</div>
导航
Bootstrap 4 为导航元素提供了基本样式。它公开了基本的nav
类,通过扩展它来共享一般的标记和样式。所有导航组件都是基于此构建的,通过指定额外的样式。它没有活动状态的样式。顺便说一句,您可以使用这些方法来禁用按钮。
基本导航
任何Nav组件必须具有基于ul
或nav
元素的外部导航元素。这是一种基于列表的方法,垂直显示导航元素:
<ul class="nav">
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Another link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li>
</ul>
我们的标记可以非常灵活,因为所有组件都基于类。我们可以使用nav
与常规锚元素水平布局导航:
<nav class="nav">
<a class="nav-link active" href="#">Active</a>
<a class="nav-link" href="#">Link</a>
<a class="nav-link" href="#">Another link</a>
<a class="nav-link disabled" href="#">Disabled</a>
</nav>
我喜欢这种方法,因为它比基于列表的方法更简洁。
内联导航
您可以使用nav-inline
类轻松添加水平间距的内联导航元素,就像前面的示例一样:
<ul class="nav nav-inline">
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Another link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li>
</ul>
选项卡
我们可以快速将前述的 Nav 组件转换为具有nav-tabs
类的选项卡界面:
<ul class="nav **nav-tabs**
">
<li class="nav-item">
<a class="nav-link active" href="#">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Another link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li>
</ul>
药丸
只需将nav-tabs
更改为nav-pills
以显示药丸:
<ul class="nav **nav-pills**
">
<li class="nav-item">
<a class="nav-link active" href="#">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Another link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li>
</ul>
堆叠的药丸
如果您需要垂直布局药丸,请使用nav-stacked
类:
<ul class="nav **nav-pills nav-stacked**
">
<li class="nav-item">
<a class="nav-link active" href="#">Active</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Another link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li>
</ul>
带下拉菜单的导航
您可以通过将dropdown
类应用于列表项,并使用一些额外的 HTML 和下拉 JavaScript 插件,将下拉菜单添加到内联导航、选项卡或药丸中:
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" href="#">Active</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown"
href="#" role="button" aria-haspopup="true"
aria-expanded="false">Dropdown</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Separated link</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Another link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Disabled</a>
</li>
</ul>
导航栏
我之前提到了带有公司标志和菜单的标题,暂时实现为网格。现在我们将这个构造更改为合适的组件。请欢迎Navbars。
Navbar 只是一个简单的包装器,有助于定位包含元素。通常,它显示为水平条,但您可以将其配置为在较小的布局上折叠。
与 Bootstrap 的许多其他组件一样,Navbar 容器需要少量的标记和类来使其工作:
-
要创建一个,您必须同时使用
navbar
类和颜色方案 -
最顶部必须是带有
role="navigation"
的nav
或div
元素
内容
我们可以在必要时包含内置的子组件来添加占位符:
-
使用
navbar-brand
类为您的公司、产品或项目名称。 -
使用
navbar-nav
类来实现全高度和轻量级导航。它包括对下拉菜单的支持。 -
使用
navbar-toggler
类来组织可折叠的行为。
让我们利用我们对 Navbar 的了解来构建我们的标题。首先,我使用nav
来创建最顶层的元素:
**<nav class="navbar navbar-light bg-faded">**
然后,我需要为公司名称使用navbar-brand
类。我们可以将此类应用于大多数元素,但锚点效果最佳:
<a class=" **navbar-brand**
" href="#">Dream Bean</a>
最后,我添加了一组导航链接,首先是active
:
<ul **class="nav navbar-nav"**
>
<lii **class="nav-tem active"**
>
<a **class="nav-link"**
href="#">
Home **<span class="sr-only">(current)</span>**
</a>
</li>
<li **class="nav-item"**
>
<a **class="nav-link"**
href="#">Products</a>
</li>
<li **class="nav-item"**
>
<a **class="nav-link"**
href="#">Checkout</a>
</li>
<li **class="nav-item"**
>
<a **class="nav-link"**
href="#">Sign out</a>
</li>
</ul>
</nav>
<!-- /.navbar -->
这是我们带有品牌和一组链接的标题:
借助nav
类,我们可以通过完全避免基于列表的方法来简化导航:
<nav class="navbar navbar-light bg-faded">
<a class="navbar-brand" href="#">Dream Bean</a>
<div class="nav navbar-nav">
<a class="nav-item nav-link active" href="#">
Home <span class="sr-only">(current)</span>
</a>
<a class="nav-item nav-link" href="#">Products</a>
<a class="nav-item nav-link" href="#">Checkout</a>
<a class="nav-item nav-link" href="#">Sign out</a>
</div>
</nav>
颜色
您可以非常优雅地管理 Navbar 的颜色:
-
使用
navbar-light
或navbar-dark
类来指定方案 -
通过 Bootstrap 颜色类之一添加颜色值,或者使用 CSS 创建自己的颜色。
在我的示例中,我使用了浅色方案和 Bootstrap 淡色背景颜色。让我们将其更改为深色方案和自定义颜色:
<nav class="navbar **navbar-dark**
" style=" **background-color: #666**
">
<a class="navbar-brand" href="#">Dream Bean</a>
<div class="nav navbar-nav">
<a class="nav-item nav-link active" href="#">
Home <span class="sr-only">(current)</span>
</a>
<a class="nav-item nav-link" href="#">Products</a>
<a class="nav-item nav-link" href="#">Checkout</a>
<a class="nav-item nav-link" href="#">Sign out</a>
</div>
</nav>
看起来不错,但 Navbar 横跨整个视口的宽度。这不是 Dream Bean 的经理们想要的。标题必须居中并具有特定的大小。
容器
我们将在container
类中包装我们的 Navbar,以使其居中在页面上:
**<div class="container">**
<nav class="navbar navbar-dark" style="background-color: #666">
<a class="navbar-brand" href="#">Dream Bean</a>
<div class="nav navbar-nav">
<a class="nav-item nav-link active" href="#">
Home <span class="sr-only">(current)</span>
</a>
<a class="nav-item nav-link" href="#">Products</a>
<a class="nav-item nav-link" href="#">Checkout</a>
<a class="nav-item nav-link" href="#">Sign out</a>
</div>
</nav>
**</div>**
另一个他们想要的更正是标题必须静态放置在页面顶部。我使用了navbar-fixed-top
类将其放置在视口顶部:
<div class="container">
<nav class="navbar navbar-fixed-top navbar-dark"
style="background-color: #666">
<a class="navbar-brand" href="#">Dream Bean</a>
<div class="nav navbar-nav">
<a class="nav-item nav-link active" href="#">
Home <span class="sr-only">(current)</span>
</a>
<a class="nav-item nav-link" href="#">Products</a>
<a class="nav-item nav-link" href="#">Checkout</a>
<a class="nav-item nav-link" href="#">Sign out</a>
</div>
</nav>
</div>
您可以使用navbar-fixed-bottom
类来在页面底部达到相同的效果。
通过最后这些更改,页眉再次跨越整个视口的宽度。为了解决这个问题,我们需要将container
移到navbar
内部以包装其内容:
<nav class=" **navbar**
navbar-fixed-top navbar-dark"
style="background-color: #666">
**<div class="container">**
<a class="navbar-brand" href="#">Dream Bean</a>
<div class="nav navbar-nav">
<a class="nav-item nav-link active" href="#">
Home <span class="sr-only">(current)</span>
</a>
<a class="nav-item nav-link" href="#">Products</a>
<a class="nav-item nav-link" href="#">Checkout</a>
<a class="nav-item nav-link" href="#">Sign out</a>
</div>
**</div>**
</nav>
我们的导航栏隐藏了视口下方的部分,因此我们需要添加填充来补偿这个问题:
body {
padding-top: 51px;
}
如果您的导航栏固定在底部,请为其添加填充:
body {
padding-bottom: 51px;
}
响应式导航栏
Dream Bean 的员工想要解决的另一个问题是内容必须在给定的视口宽度下折叠。让我们使用navbar-toggler
类以及navbar-toggleable
类和它们的前缀大小来做到这一点:
如我之前提到的,navbar-toggler
类有助于组织可折叠的行为。可折叠插件使用data-toggle
属性中的信息来触发操作,并使用data-target
中定义的一个元素。data-target
保留了包含在navbar-toggleable
类中的元素的 ID,并且它会加上前缀大小。
可折叠的页眉只能通过它们的组合响应地工作:
响应式实用程序
为了让开发者的生活更轻松,Bootstrap 提供了用于更快速移动友好开发的实用程序类。它们可以帮助:
-
通过媒体查询来显示和隐藏内容
-
打印时切换内容
我不想为不同的移动设备创建完全不同的 Web 应用程序版本。相反,我将使用以下实用程序类来补充每个设备的呈现:
-
实用程序类
hidden-*-up
在视口位于给定断点或更宽时隐藏元素 -
实用程序类
hidden-*-down
在视口位于给定断点或更小时隐藏元素 -
通过组合
hidden-*-up
和hidden-*-down
实用程序类,我们可以仅在给定的屏幕尺寸间隔上显示元素
请记住,没有退出响应式实用程序类来显式显示元素。事实上,我们不会在特定的断点大小隐藏它。
在我们的项目中,我们只为屏幕尺寸小于 544px 的设备显示我们喜欢称为汉堡按钮的 UI 元素:
这是一个快速提示表,可以帮助您选择正确的实用程序类来在屏幕上显示元素:
-
hidden-xs-down
在小型设备(横向手机)及以上(>= 544px)显示元素 -
hidden-sm-down
在中等设备(平板电脑)及以上(>= 768px)显示元素 -
hidden-md-down
显示来自大型设备(台式电脑)及以上(>= 992px)的元素 -
hidden-lg-down
显示来自小型设备(台式电脑)及以上(>= 1200px)的元素 -
hidden-sm-up
显示超小型设备(竖屏手机)的元素(< 544px) -
hidden-md-up
显示小型设备(竖屏手机)及以下的元素(< 768px) -
hidden-lg-up
显示中等设备(平板电脑)及以下的元素(< 992px) -
hidden-xl-up
显示大型设备(台式电脑)及以下的元素(< 1200px)
导航栏内容对齐
我们需要修复的最后一件事是菜单在导航栏中的位置。我们可以使用pull-*left
或pull-*right
类中的任何一个来对齐菜单和导航栏中的所有其他组件。Dream Bean 的管理人员希望将购物车项目添加为菜单的最后一项,并将其与右侧对齐:
<ul class="nav navbar-nav **pull-xs-right**
">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle"
data-toggle="dropdown" href="#" role="button"
aria-haspopup="true" aria-expanded="false">Cart</a>
<div class="dropdown-menu">
<span>The Cart Placeholder</span>
</div>
</li>
</ul>
我创建了一个单独的菜单组,并在所有布局大小上使用pull-xs-right
将其对齐到右侧:
注意
您可以在chapter_2/4.ecommerce-responsive
文件夹中找到源代码。
摘要
在本章中,我们发现了 Sass 的世界;这个 CSS 预处理框架允许您编写更简洁的样式表。Sass 文件格式良好,需要较少的重复技术,通常在编写 CSS 代码时会发现。因此,我们拥有了更具动态风格的样式,并节省了开发高质量网站和应用程序的时间。在接下来的章节中,我们将使用 Sass 来定制我们的项目。
我们介绍了我们将在本书中构建的项目。这些信息包括如何从头开始开发项目的重要方面。
我们探索了最基本的网格组件,帮助我们在页面上布局所有其他元素。
我们介绍了灵活的卡片组件,并构建了包含建筑项目产品类别的板块。
现在我们知道如何使用 Nav 和 Navbar 组件来组织响应式可折叠的标题菜单,并对其进行定制。
在第三章中,高级 Bootstrap 组件和定制,我们将探索更多 Bootstrap 基础知识,并继续构建我们在本章开始开发的项目。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
第三章:高级 Bootstrap 组件和自定义
在本章中,我们将继续探索 Bootstrap 4 的世界。您将遇到新的组件,并且我们将继续通过展示我们在上一章开始构建的项目来演示 Bootstrap 4 的使用。在本章结束时,您将对以下内容有扎实的理解:
-
如何使用 Jumbotron 显示内容
-
如何使用 Bootstrap 创建幻灯片放映
-
如何在文本中使用排版
-
如何创建输入、按钮和列表组
-
通过图片和标签吸引注意力
-
使用下拉菜单和表格
如何吸引顾客的注意力
欢迎页面向网站用户展示核心营销信息,并且需要从他们那里获得额外的关注。我们可以使用两种不同的组件来实现这一点。让我们从以下开始:
-
打开终端,创建名为
ecommerce
的文件夹并进入。 -
将项目的内容从文件夹
chapter_3/1.ecommerce-seed
复制到新项目中。 -
运行以下脚本以安装 npm 模块:
**npm install**
- 使用下一个命令启动 TypeScript 监视器和轻量级服务器:
**npm run start**
此脚本将打开一个网页浏览器并导航到项目的欢迎页面。现在,我们准备开始开发。
使用 Jumbotron 显示内容
我们可以使用Jumbotron组件来吸引人们对营销信息的重要关注。它是一个轻量级组件,样式为大文本和密集填充。我们需要展示:
-
营销信息
-
标语
-
顾客的基本信息
打开app.component.html
页面,找到导航栏下的第一个容器,并将其内容更改为 Jumbotron 组件:
<div class="jumbotron">
<h1>FRESH ORGANIC MARKET</h1>
<p>Nice chance to save a lot of money</p>
<hr>
<p>We are open 7 Days a Week 7:00am to 10:00pm</p>
</div>
您可以使用jumbotron-fluid
类和container
或container-fluid
类来强制 Jumbotron 使用整个页面的宽度。
我在 Jumbotron 中使用了标准的 HTML 标记元素,但使用不同的样式可能会更好。
排版
在上述代码中,我们使用了没有任何类的文本元素,以查看 Bootstrap 如何在页面上呈现它们。它使用全局默认的font-size
为16px
和line-height1,5
。Helvetica Neue,Helvetica,Arial,Sans Serf
是 Bootstrap 4 的默认font-family
。每个元素都有一个box-sizing
,以避免由于填充或边框而超出宽度。段落元素的底部边距为1rem
。页面声明了白色的background-color
。任何链接到 Bootstrap 4 样式表的页面都会使用这些页面范围的默认值。
标题
所有标题元素,<h1>
到<h6>
,都具有500
的权重和1.1
的line-height
。Bootstrap 的开发人员已经从中删除了margin-top
,但为了方便间距,添加了0.5rem
的margin-bottom
。
在需要显示一些内联文本的情况下,您可以使用h1
到h6
类来样式化模仿标题的元素:
<p class="h1">.h1 (Semibold 36px)</p>
<p class="h2">.h2 (Semibold 30px)</p>
<p class="h3">.h3 (Semibold 24px)</p>
<p class="h4">.h4 (Semibold 18px)</p>
<p class="h5">.h5 (Semibold 14px)</p>
<p class="h6">.h6 (Semibold 12px)</p>
子标题
如果您需要包含次级标题或比原始文本小的文本,可以使用<small>
标签:
<h1>Heading 1 <small>Sub-heading</small></h1>
<h2>Heading 2 <small>Sub-heading</small></h2>
<h3>Heading 3 <small>Sub-heading</small></h3>
<h4>Heading 4 <small>Sub-heading</small></h4>
<h5>Heading 5 <small>Sub-heading</small></h5>
<h6>Heading 6 <small>Sub-heading</small></h6>
我们可以使用text-muted
类显示淡化和较小的文本:
<h3>
The heading
<small class="text-muted">with faded secondary text</small>
</h3>
显示标题
当标准标题不够用,您需要引起用户对特殊事物的注意时,我建议使用display-*
类。有四种不同的大小,这意味着您可以使用四种不同的样式呈现<h1>
元素:
<h1 class="display-1">Display 1</h1>
<h1 class="display-2">Display 2</h1>
<h1 class="display-3">Display 3</h1>
<h1 class="display-4">Display 4</h1>
引导
我们可以将lead
类添加到任何段落中,使其与其他文本脱颖而出:
<p class="lead">
This is the article lead text.
</p>
<p>
This is the normal size text.
</p>
让我们更新 Jumbotron 组件,使其看起来更好:
<div class="jumbotron">
<h1 class="display-3">FRESH ORGANIC MARKET</h1>
<p class="lead">Nice chance to save a lot of money</p>
<hr class="m-y-2">
<p>We are open 7 Days a Week 7:00am to 10:00pm</p>
</div>
营销信息看起来很漂亮,口号也很到位,但我们没有改变顾客的基本信息,因为没有必要。
内联文本元素
这是一组不同的样式,我们可以将其用作内联文本:
<p>The **mark**
tag is <mark>highlight</mark> text.</p>
<p>The **del**
tag marks <del>text as deleted.</del></p>
<p>The **s**
tag marks <s> text as no longer accurate.</s></p>
<p>The **ins**
tag marks <ins>text as an addition to the document.</ins></p>
<p>The **u**
tag renders <u>text as underlined.</u></p>
<p>The **small**
tag marks <small>text as fine print.</small></p>
<p>The **strong**
tag renders <strong>text as bold.</strong></p>
<p>The **em**
tag mark renders <em>text as italicized.</em></p>
缩写
要将任何文本标记为缩写或首字母缩略词,我们可以使用<abbr>
标签。当您将鼠标悬停在其上时,它会突出显示其他文本,并提供扩展版本,帮助您使用title
属性:
<p>The Ubuntu is <abbr >OS</abbr>.</p>
initialism
类使缩写字体略小。
块引用
我们可以使用blockquote
标签和类在文档中引用另一个来源的内容:
< **blockquote**
class=" **blockquote**
">
<p>Love all, trust a few, do wrong to none.</p>
</ **blockquote**
>
此外,我们可以使用嵌套的footer
和cite
标签在blockquote
底部添加作者。
<blockquote class="blockquote">
<p>Love all, trust a few, do wrong to none.</p>
< **footer**
class="blockquote-footer">William Shakespeare in
< **cite**
>All's Well That Ends Well</ **cite**
>
</ **footer**
>
</blockquote>
您喜欢将块引用对齐到右侧吗?让我们使用blockquote-reverse
类:
<blockquote class="blockquote blockquote-reverse">
<p>Love all, trust a few, do wrong to none.</p>
<footer class="blockquote-footer">William Shakespeare in
<cite>All's Well That Ends Well</cite>
</footer>
</blockquote>
地址
我们使用address
元素在页面底部显示客户联系信息:
<footer class="footer">
<div class="container">
<address>
<strong>Contact Info</strong><br>
0000 Market St, Suite 000, San Francisco, CA 00000,
(123) 456-7890, <a href="mailto:#">[email protected]</a>
</address>
</div>
</footer>
注意
您可以在chapter_3/2.ecommerce-jumbotron
文件夹中找到源代码
使用轮播显示内容
我们可以使用的另一个组件来吸引顾客的额外注意是旋转木马。它帮助我们创建优雅和交互式的图像或文本幻灯片。旋转木马是不同组件的组合,每个组件都扮演着非常具体的角色。
旋转木马容器
容器包裹所有其他内容,因此插件 JavaScript 代码可以通过carousel
和slide
类找到它。它必须具有旋转木马控件和内部组件正常运行所需的id
。如果希望旋转木马在页面加载时开始动画,请使用data-ride="carousel"
属性:
<div id=" **welcome-products**
"
class=" **carousel slide**
" data-ride=" **carousel**
">
旋转木马内部
该容器将旋转木马项目作为可滚动内容,并使用carousel-inner
类对其进行标记:
<div class=" **carousel-inner**
" role="listbox">
旋转木马项目
carousel-item
类保持幻灯片的内容,例如图像、文本或它们的组合。您需要使用carousel-caption
容器包装基于文本的内容。使用active
类将项目标记为已初始化,没有它,旋转木马将不可见。
<div class=" **carousel-item active**
">
<img src="http://placehold.it/1110x480" alt="Bread & Pastry">
<div class=" **carousel-caption**
">
<h3>Bread & Pastry</h3>
</div>
</div>
旋转木马指示器
旋转木马可能有指示器来显示和控制幻灯片播放,可以通过单击或轻触来选择特定幻灯片。通常,它是一个带有carousel-indicators
类的有序列表。列表上的每个项目都必须具有保持旋转木马容器id
的data-target
属性。因为它是一个有序列表,所以不需要对其进行排序。如果需要在当前位置周围改变幻灯片位置,请使用data-slide
属性来接受关键字prev
和next
。另一个选项是使用data-slide-to
属性传递幻灯片的索引。使用active
类来标记初始指示器:
<ol class=" **carousel-indicators**
">
<li data-target=" **#welcome-products" data-slide-to="0"**
class="active"></li>
<li data-target= **"#welcome-products" data-slide-to="1"**
></li>
<li data-target= **"#welcome-products" data-slide-to="2"**
></li>
</ol>
旋转木马控件
您可以使用另一种方式通过旋转木马控件按钮显示幻灯片。在这种情况下,两个锚元素扮演按钮的角色。将特定按钮与carousel-control
一起添加left
或right
类。在href
属性中使用旋转木马容器id
作为链接。将prev
或next
设置为data-slide
属性:
<a class=" **left carousel-control**
" href= **"#welcome-products"**
role= **"button" data-slide="prev"**
>
<span class="icon-prev" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class=" **right carousel-control**
" href= **"#welcome-products"**
role= **"button" data-slide="next"**
>
<span class="icon-next" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
让我们比较欢迎页面的最终结果和线框图:
欢迎页面的线框图
正如您所看到的,它们看起来完全相同。实际上,我们已经完成了欢迎页面,现在是时候继续进行产品页面的开发了。
注意
您可以在chapter_3/3.ecommerce-carousel
文件夹中找到源代码
产品页面布局
让我们看一下产品页面的线框图,并想象将其分成行和列,就像我们为欢迎页面所做的那样:
第一行仍然包含我们的导航标题,但我把其他内容放到了另一行。有两列,一列是快速购物和分类,另一列是一组产品。为什么我要把产品页面分成这样?答案很简单。Bootstrap 总是先按行渲染内容,然后再按列渲染。在布局较小的设备上,第一行的标题通常会折叠成汉堡菜单。在底部,它会显示第二行,包括快速购物,分类,以及垂直对齐的一组产品。
我克隆了上一个项目并清理了代码,但保留了导航标题和页脚,因为我不想将产品页面的开发与原始页面混在一起。让我们先讨论第一列中的组件。
快速购物组件
这个组件只是一个带有按钮的搜索输入。我没有实现业务逻辑,只是设计页面。这是基于我们在第二章中探讨过的卡片元素,使用 Bootstrap 组件。我想使用输入组件,让我们看看它能做什么。
输入组
这是一组表单控件和文本组合在一行中。它旨在通过在输入字段的两侧添加文本、按钮或按钮组并将它们对齐来扩展表单控件。创建输入组件非常容易。只需用标有input-group
类的元素包装input
,并附加或前置另一个带有input-group-addon
类的元素。您可以在任何表单之外使用输入组,但我们需要用form-control
类标记输入元素,以使其宽度达到 100%。
注意
仅对文本输入元素使用输入组。
文本附加
这是一个带有附加组件的文本字段的示例:
<div class="input-group">
<input type="text" class="form-control"
placeholder="Pricein USD">
<span class=" **input-group-addon**
">.00</span>
</div>
另一个带有前置附加组件的示例是:
<div class="input-group">
<span class=" **input-group-addon**
">https://</span>
<input type="text" class="form-control"
placeholder="Your address">
</div>
最后,我们可以将它们全部组合在一起:
<div class="input-group">
<span class=" **input-group-addon**
">$</span>
<input type="text" class="form-control"
placeholder="Price per unit">
<span class=" **input-group-addon**
">.00</span>
</div>
尺寸
有两个表单尺寸类,input-group-lg
和input-group-sm
,可以使输入组比标准尺寸更大或更小。你需要将其中一个应用到标有input-group
类的元素上,其中的内容将自动调整大小:
<div class="input-group **input-group-lg**
">
<input type="text" class="form-control"
placeholder="Large">
<span class="input-group-addon">.00</span>
</div>
<div class="input-group">
<input type="text" class="form-control"
placeholder="Standard">
<span class="input-group-addon">.00</span>
</div>
<div class="input-group **input-group-sm**
">
<input type="text" class="form-control"
placeholder="Small">
<span class="input-group-addon">.00</span>
</div>
复选框和单选按钮附件
我们可以使用复选框或单选按钮选项来代替文本附件:
<div class="input-group">
**<span class="input-group-addon">**
**<input type="checkbox">**
**</span>**
<input type="text" class="form-control"
placeholder="Select">
</div>
<div class="input-group">
**<span class="input-group-addon">**
**<input type="radio">**
**</span>**
<input type="text" class="form-control"
placeholder="Choose">
</div>
按钮附件
最常见的元素是按钮,你可以在输入组中使用它们。只需增加一层额外的复杂性:
<div class="input-group">
<input type="text" class="form-control"
placeholder="Search for...">
**<span class="input-group-btn">**
**<button class="btn btn-secondary" type="button">Go!</button>**
**</span>**
</div>
下拉菜单附件
我们可以使用按钮来显示下拉菜单。我们稍后在本章中会讲到下拉菜单。以下代码演示了如何使用下拉菜单:
<div class="input-group">
<input type="text" class="form-control">
<div class="input-group-btn">
<button type="button"
class="btn btn-secondary dropdown-toggle"
data-toggle="dropdown">
Action
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
<div role="separator" class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Separated link</a>
</div>
</div>
</div>
分段按钮
有时将按钮和下拉菜单分开可能会很有用,这样布局也会更清晰:
<div class="input-group">
<input type="text" class="form-control">
<div class="input-group-btn">
<button type="button" class="btn btn-secondary">Action</button>
<button type="button" class="btn btn-secondary dropdown-toggle"
data-toggle="dropdown">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
<div role="separator" class="dropdown-divider"></div>
<a class="dropdown-item" href="#">Separated link</a>
</div>
</div>
</div>
现在我们知道如何使用输入组,让我们创建一个快速购物组件:
<div class="container">
<div class="row">
<div class="col-md-3">
<div class="card">
<div class="card-header">
Quick Shop
</div>
<div class="card-block">
<div class="input-group">
<input type="text" class="form-control"
placeholder="Search for...">
<span class="input-group-btn">
<button class="btn btn-secondary"
type="button">Go!</button>
</span>
</div>
</div>
</div>
<!-- /.card -->
</div>
<!-- /.col -->
</div>
<!-- /.row -->
</div>
<!-- /.container -->
注意
你可以在chapter_3/4.ecommerce-input-group
文件夹中找到源代码。
类别组件
类别组件位于快速购物下面。我想使用列表组件来保存顾客可以选择的类别。
列表组
这是一个灵活的组件,用于轻松显示无序元素列表,比如简单的项目或自定义内容。只需用list-group
类标记任何无序列表元素,并用list-group-item
标记每个项目,就可以快速创建列表组组件:
<ul class="list-group">
<li class="list-group-item">Apple</li>
<li class="list-group-item">Banana</li>
<li class="list-group-item">Grapefruit</li>
<li class="list-group-item">Carrot</li>
</ul>
带标签的列表
有时我们需要显示关于每个项目的更多信息,比如数量、活动等。为此,我们可以为每个项目和列表组自动添加标签,以将其定位到右侧:
<ul class="list-group">
<li class="list-group-item">
<span class="tag tag-default tag-pill
pull-xs-right">15</span>
Apple
</li>
<li class="list-group-item">
<span class="tag tag-default tag-pill
pull-xs-right">5</span>
Banana
</li>
<li class="list-group-item">
<span class="tag tag-default tag-pill
pull-xs-right">0</span>
Grapefruit
</li>
<li class="list-group-item">
<span class="tag tag-default tag-pill
pull-xs-right">3</span>
Carrot
</li>
</ul>
链接列表组
我们可以快速创建一个带有链接列表组件的垂直菜单。这种列表基于div
标签而不是ul
。这个列表的整个项目都是一个锚元素,它可以是:
-
可点击
-
可悬停
-
通过
active
类进行突出显示 -
通过同名类进行禁用
<div class="list-group">
<a href="#" class="list-group-item">Apple</a>
<a href="#" class="list-group-item active">Banana</a>
<a href="#" class="list-group-item **disabled**
">Grapefruit</a>
<a href="#" class="list-group-item">Carrot</a>
</div>
按钮列表组
如果您愿意,可以使用按钮代替锚元素,然后您需要更改每个项目的标签名称,并在其中添加list-group-item-action
类。我们可以使用active
或disabled
使项目显示不同:
<div class="list-group">
<button type="button" class="list-group-item list-group-item-action active ">Apple</button>
<button type="button" class="list-group-item item list-group-item-action ">Banana</button>
<button type="button" class="list-group-item item list-group-item-action disabled">Grapefruit</button>
<button type="button" class="list-group-item item list-group-item-action ">Carrot</button>
</div>
注意
在列表组中使用标准的btn
类是被禁止的。
上下文类
您还可以使用上下文类样式化单独的列表项。只需将上下文类后缀添加到list-group-item
类中。具有active
类的项目会显示为变暗版本:
<div class="list-group">
<a href="#" class="list-group-item
list-group-item-success">Apple</a>
<a href="#" class="list-group-item
list-group-item-success active">Watermelon</a>
<a href="#" class="list-group-item
list-group-item-info">Banana</a>
<a href="#" class="list-group-item
list-group-item-warning">Grapefruit</a>
<a href="#" class="list-group-item
list-group-item-danger">Carrot</a>
</div>
自定义内容
最后,您可以在列表组件的每个项目中添加 HTML,并使用锚元素使其可点击。Bootstrap 4 为标题和文本内容提供了list-group-item-heading
和list-group-item-text
类。具有active
类的项目会显示为自定义内容的变暗版本:
<div class="list-group">
<a href="#" class="list-group-item list-group-item-success">
<h4 class=" **list-group-item-heading**
">Apple</h4>
<p class=" **list-group-item-text**
">It is sweet.</p>
</a>
<a href="#" class="list-group-item list-group-item-success **active**
">
<h4 class=" **list-group-item-heading**
">Watermelon</h4>
<p class=" **list-group-item-text**
">
It is a fruit and a vegetable.
</p>
</a>
</div>
现在,是时候创建我们的类别组件了:
<div class="card">
<div class="card-header">
Categories
</div>
<div class="card-block">
<div class="list-group">
<a href="#" class="list-group-item">All</a>
<a href="#" class="list-group-item">Meat</a>
<a href="#" class="list-group-item">Seafood</a>
<a href="#" class="list-group-item">Bakery</a>
<a href="#" class="list-group-item">Dairy</a>
<a href="#" class="list-group-item">Fruit & Vegetables</a>
</div>
</div>
</div>
我们已经完成了第一列,现在继续开发第二列,其中包含一组产品的网格。
注意
您可以在chapter_3/5.ecommerce-list-group
文件夹中找到源代码。
创建产品网格
我们需要在第二列内显示一组产品的行和列网格。
嵌套行
我们可以在任何列内嵌套额外的行,以创建类似于我们所拥有的更复杂的布局:
<div class="col-md-9">
<div class="row">
<div class="col-xs-12 col-sm-6 col-lg-4">
<!-- The Product 1 -->
</div>
<!-- /.col -->
<div class="col-xs-12 col-sm-6 col-lg-4">
<!-- The Product 2 -->
</div>
<!-- /.col -->
<div class="col-xs-12 col-sm-6 col-lg-4">
<!-- The Product N -->
</div>
<!-- /.col -->
</div>
</div>
我们在一行内创建所需的列数,Bootstrap 会根据视口大小正确显示它们:
-
在超小视口上,一列占据整个大小
-
小视口上的两列
-
大型视口上的三列
产品组件
以类似的方式,我们使用卡片在产品组件中显示信息和控件:
<div class="card">
<img class="card-img-top img-fluid center-block product-item"
src="http://placehold.it/270x171" alt="Product 1">
<div class="card-block text-xs-center">
<h4 class="card-title">Product 1</h4>
<h4 class="card-subtitle">
<span class="tag tag-success">$10</span>
</h4>
<hr>
<div class="btn-group" role="group">
<button class="btn btn-primary">Buy</button>
<button class="btn btn-info">Info</button>
</div>
</div>
</div>
<!-- /.card -->
让我们稍微谈谈我们在这里使用的元素。
注意
您可以在chapter_3/6.ecommerce-grid-in-grid
文件夹中找到源代码。
图片
在卡片元素中使用图片时,我认为讨论具有响应行为和图片形状的图片是个好主意。
响应式图片
您可以使用img-fluid
类使任何图片具有响应性。它会将以下内容应用于图片,并与父元素一起缩放:
-
将
max-width
属性设置为100%
-
将
height
属性设置为auto
<div class="container">
<div class="row">
<div class="col-md-3">
<img class="img-fluid" src="http://placehold.it/270x171">
</div>
</div>
</div>
图片形状
在需要呈现图像的情况下:
-
使用
img-rounded
类来实现圆角 -
在圆圈内,使用
img-circle
类 -
使用
img-thumbnail
类作为缩略图
图像对齐
要水平对齐图像,我们可以使用文本对齐或辅助浮动类:
-
在图片的父元素上使用
text-*-center
类来使其居中 -
在图像上应用
center-block
类可以使其居中 -
使用
pull-*-left
或pull-*-right
类将图像浮动到左侧或右侧
<div class="container">
<div class="row">
<div class="col-md-6 table-bordered">
This is text around pull image to left
<img class="img-rounded **pull-xs-left**
"
src="http://placehold.it/270x171">
</div>
<div class="col-md-6 table-bordered">
This is text around pull image to right
<img class="img-circle **pull-xs-right**
"
src="http://placehold.it/270x171">
</div>
<div class="col-md-6 table-bordered">
This is text around center block image
<img class="img-thumbnail center-block"
src="http://placehold.it/270x171">
</div>
<div class="col-md-6 **text-xs-center**
table-bordered">
This is centered<br>
<img class="img-thumbnail"
src="http://placehold.it/270x171">
</div>
</div>
</div>
我在上面的代码中只使用了table-border
类来显示边框。
标签
如果我需要在文本字符串中突出显示一些信息,我会使用标签。要创建一个标签,我需要将tag
类与上下文tag-*
一起应用到span
元素上:
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Example heading
<span class=" **tag tag-default**
">Default</span>
</h1>
<h2>Example heading
<span class=" **tag tag-primary**
">Primary</span>
</h2>
<h3>Example heading
<span class=" **tag tag-success**
">Success</span>
</h3>
<h4>Example heading
<span class=" **tag tag-info**
">Info</span>
</h4>
<h5>Example heading
<span class=" **tag tag-warning**
">Warning</span>
</h5>
<h6>Example heading
<span class=" **tag tag-danger**
">Danger</span>
</h6>
</div>
</div>
</div>
标签使用父元素的相对字体大小,因此它始终按比例缩放以匹配其大小。如果您需要标签看起来像徽章,请使用tag-pill
类来实现:
<div class="container">
<div class="row">
<div class="col-md-12">
<span class="label **label-pill**
label-default">Default</span>
<span class="label **label-pill**
label-primary">Primary</span>
<span class="label **label-pill**
label-success">Success</span>
<span class="label **label-pill**
label-info">Info</span>
<span class="label **label-pill**
label-warning">Warning</span>
<span class="label **label-pill**
label-danger">Danger</span>
</div>
</div>
</div>
按钮组
我们可以使用按钮组组件将按钮水平或垂直分组在一起。按钮默认是水平排列的。要创建一个按钮组,请在带有btn-group
类的容器中使用带有btn
类的按钮:
<div class="container">
<div class="row">
<div class="col-md-12">
<div class=" **btn-group**
" role="group">
<button type="button" class=" **btn btn-default**
">Left</button>
<button type="button" class=" **btn btn-secondary**
">Middle</button>
<button type="button" class=" **btn btn-danger**
">Right</button>
</div>
</div>
</div>
</div>
大小
有两种尺寸可以使按钮组件比标准尺寸更大或更小。将btn-group-lg
或btn-group-sm
类添加到按钮组件中,可以一次调整组中所有按钮的大小:
<div class="btn-group btn-group-lg" role="group">
<button type="button" class="btn btn-default">Left</button>
<button type="button" class="btn btn-secondary">Middle</button>
<button type="button" class="btn btn-danger">Right</button>
</div><br><br>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default">Left</button>
<button type="button" class="btn btn-secondary">Middle</button>
<button type="button" class="btn btn-danger">Right</button>
</div><br><br>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-default">Left</button>
<button type="button" class="btn btn-secondary">Middle</button>
<button type="button" class="btn btn-danger">Right</button>
</div>
按钮工具栏
我们可以将按钮组合成一个更复杂的按钮工具栏:
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary">Create</button>
<button type="button" class="btn btn-secondary">Edit</button>
<button type="button" class="btn btn-danger">Delete</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default">Fetch</button>
</div><br><br>
</div>
嵌套下拉菜单
我们可以将下拉菜单作为按钮组的一部分嵌套到另一个按钮组中:
<div class=" **btn-group**
" role="group">
<button type="button" class="btn btn-secondary">Create</button>
<button type="button" class="btn btn-secondary">Delete</button>
<div class=" **btn-group**
" role="group">
<button id=" **btnGroupDrop1**
" type="button"
class="btn btn-secondary **dropdown-toggle**
"
**data-toggle="dropdown"**
aria-haspopup="true"
aria-expanded="false">
Actions
</button>
<div class=" **dropdown-menu**
" aria-labelledby=" **btnGroupDrop1**
">
<a class="dropdown-item" href="#">Get One</a>
<a class="dropdown-item" href="#">Get Many</a>
</div>
</div>
</div>
此外,您可以使用按钮组件创建一个分割下拉菜单组件:
<div class="btn-group" role="group">
<button type="button" class="btn btn-secondary">Actions</button>
<button id="btnGroupDrop1" type="button"
class="btn btn-secondary dropdown-toggle"
data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
<a class="dropdown-item" href="#">Get One</a>
<a class="dropdown-item" href="#">Get Many</a>
</div>
</div>
垂直按钮组
如果需要将按钮组垂直排列,请将btn-group
替换为btn-group-vertical
类:
<div class="btn-group-vertical" role="group">
<button type="button"
class="btn btn-default">Left</button>
<button type="button"
class="btn btn-secondary">Middle</button>
<button type="button"
class="btn btn-danger">Right</button>
</div>
垂直按钮组不支持分割下拉菜单。
下拉菜单
我们经常谈论下拉菜单,让我们更仔细地看看它们。下拉菜单是一个用于显示链接列表的切换覆盖层。它是几个组件的组合。
下拉菜单容器
这个包装了所有其他元素。通常,它是一个带有dropdown
类的div
元素,或者另一个使用position: relative
的元素。
下拉触发器
这是用户可以点击或点击以展开下拉菜单的任何项目。我们需要用dropdown-toggle
类标记它,并设置data-toggle="dropdown"
属性。
带有项目的下拉菜单
下拉菜单本身是具有dropdown-item
类的元素的组合,包装器包含所有标记为dropdown-menu
类的元素。这是一个无列表的组件。对于菜单项,您可以使用锚点或按钮元素:
<div class=" **dropdown**
">
<button class="btn btn-secondary **dropdown-toggle**
" type="button"
id="dropdownMenu1" **data-toggle="dropdown"**
aria-haspopup="true"
aria-expanded="false">
Action
</button>
<div class=" **dropdown-menu**
" aria-labelledby="dropdownMenu1">
<a class=" **dropdown-item**
" href="#">Create</a>
<a class=" **dropdown-item**
" href="#">Edit</a>
<a class=" **dropdown-item**
" href="#">Delete</a>
</div>
</div>
菜单对齐
下拉菜单默认向左对齐。如果需要向右对齐,则需要向其应用dropdown-menu-right
类。我已经向父元素添加了text-xs-right
类来将整个组件对齐到右侧:
<div class="col-md-3 **text-xs-right**
">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle"
type="button" id="dropdownMenu1"
data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
Action
</button>
<div class="dropdown-menu **dropdown-menu-right**
"
aria-labelledby="dropdownMenu1">
<a class="dropdown-item" href="#">Create</a>
<a class="dropdown-item" href="#">Edit</a>
<a class="dropdown-item" href="#">Delete</a>
</div>
</div>
</div>
菜单标题和分隔符
下拉菜单可以有几个标题元素。您可以使用标题元素和dropdown-header
类来添加它们:
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button"
id="dropdownMenu1" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
Action
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenu1">
<h6 class="dropdown-header">Document</h6>
<a class="dropdown-item" href="#">Create</a>
<a class="dropdown-item" href="#">Edit</a>
<a class="dropdown-item" href="#">Delete</a>
<h6 class="dropdown-header">Print</h6>
<a class="dropdown-item" href="#">Print Now</a>
<a class="dropdown-item" href="#">Configuration</a>
</div>
</div>
菜单分隔符
我们不仅可以使用标题,还可以使用分隔符来分隔菜单项的组。使用dropdown-divider
类将菜单项标记为分隔符:
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button"
id="dropdownMenu1" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
Action
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenu1">
<a class="dropdown-item" href="#">Create</a>
<a class="dropdown-item" href="#">Edit</a>
<a class="dropdown-item" href="#">Delete</a>
<div class=" **dropdown-divider**
"></div>
<a class="dropdown-item" href="#">Print Now</a>
<a class="dropdown-item" href="#">Configuration</a>
</div>
</div>
禁用菜单项
如果需要,我们可以通过disabled
类禁用菜单项:
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button"
id="dropdownMenu1" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
Action
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenu1">
<a class="dropdown-item" href="#">Create</a>
<a class="dropdown-item" href="#">Edit</a>
<a class="dropdown-item **disabled**
" href="#">Delete</a>
</div>
</div>
表格
有新的类来构建一致样式和响应式的表格。因为我们需要表格来设计购物车组件,我现在想看一下它。这是一个选择加入的功能,所以通过添加table
类很容易将任何表格转换为 Bootstrap 表格。结果是我们有一个带有水平分隔线的基本表格:
<table class=" **table**
">
<thead>
<tr>
<th>#</th>
<th>First Name</th>
<th>Last Name</th>
<th>Username</th>
</tr>
</thead>
<tfoot>
<tr>
<th colspan="4">Number <strong>2</strong></th>
</tr>
</tfoot>
<tbody>
<tr>
<th scope="row">1</th>
<td>Mark</td>
<td>Otto</td>
<td>@mdo</td>
</tr>
<tr>
<th scope="row">2</th>
<td>Jacob</td>
<td>Thornton</td>
<td>@fat</td>
</tr>
</tbody>
</table>
table-inverse
table-inverse
类反转了表格的颜色:
<table class="table **table-inverse**
">
条纹行
我们可以使用table-striped
类改变行的背景颜色:
<table class="table **table-striped**
">
边框表
如果需要四周都有边框的表格,请使用table-bordered
类:
<table class="table **table-bordered**
">
使行可悬停
在鼠标悬停在表格行上时实现悬停效果,请使用table-hover
类:
<table class="table **table-hover**
">
表头选项
有两个类可以改变table
的thead
元素。添加thead-default
类可以应用略带灰色背景颜色:
<table class="table">
<thead class=" **thead-default**
">
thead-inverse
类可以反转thead
的文本和背景颜色:
<table class="table">
<thead class=" **thead-inverse**
">
使表格更小
我们可以用table-sm
类将表格的填充减半,使其变小:
<table class="table table-sm">
上下文类
有五个上下文类可以应用于单独的行或单元格:table-active
、table-success
、table-info
、table-warning
和table-danger
。
响应式表格
响应式表格支持在小型和超小型设备(小于 768px)上的水平滚动。在大于小型的设备上,你不会看到任何区别。用带有table-responsive
类的div
元素包裹表格,就可以实现这种效果:
<div class="table-responsive">
<table class="table">
...
</table>
</div>
重排表格
有一个table-reflow
类可以帮助表格内容重新流动:
<table class="table **table-reflow**
">
<thead>
<tr>
<th>#</th>
<th>First Name</th>
<th>Last Name</th>
<th>Username</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">1</th>
<td>Mark</td>
<td>Otto</td>
<td>@mdo</td>
</tr>
<tr>
<th scope="row">2</th>
<td>Jacob</td>
<td>Thornton</td>
<td>@fat</td>
</tr>
</tbody>
</table>
购物车组件
我们还没有触及产品页面线框上的最后一个组件:购物车。这是购物车信息和下拉菜单的结合,包含顾客添加到购物车的商品表格:
我们将购物车信息显示为按钮文本:
<button class="btn btn-primary dropdown-toggle" type="button"
id="cartDropdownMenu" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
Cart: 2 item(s) - $20.00
</button>
我使用了一个反转的、有边框的表格来打印顾客添加到购物车的一组产品:
<div class="dropdown-menu dropdown-menu-right"
aria-labelledby="cartDropdownMenu">
<table class="table table-bordered table-inverse">
<thead>
<tr>
<th>Name</th><th>Amount</th><th>Qty</th><th>Sum</th>
</tr>
</thead>
<tfoot>
<tr>
<td colspan="4" style="text-align:center">
Total:<strong>$20.00</strong><br>
<div class="btn-group">
<button class="btn btn-primary">View Cart</button>
<button class="btn btn-success">Checkout</button>
</div>
</td>
</tr>
</tfoot>
<tbody>
<tr>
<td >Product 1</td><td >$10</td><td >x1<br>
<span class="delete-cart">Del</span>
</td>
<td >$10.00</td>
</tr>
<tr>
<td >Product 2</td><td >$5.00</td><td >x2<br>
<span class="delete-cart">Del</span>
</td>
<td >$10.00</td>
</tr>
</tbody>
</table>
</div>
我结合了我们学到的一切,现在产品页面看起来是这样的:
注意
你可以在chapter_3/7.ecommerce-dropdown
文件夹中找到源代码。
总结
在本章中,我们涵盖了很多内容,现在是时候中断我们的旅程,休息一下,回顾一切了。
Bootstrap 让我们很容易地用 Jumbotron 和轮播幻灯片抓住了顾客的注意力。
我们还研究了 Bootstrap 中包含的强大响应式网格系统,并创建了一个简单的两列布局。在这个过程中,我们了解了五种不同的列类前缀,还嵌套了我们的网格。为了调整我们的设计,我们发现了一些框架中包含的辅助类,让我们能够浮动、居中和隐藏元素。
在本章中,我们详细了解了如何在项目中使用输入、按钮和列表组。像下拉菜单和表格这样简单但强大的组件帮助我们更快速、更高效地创建我们的组件。
在第四章 创建模板中,我们将更深入地探讨 Bootstrap 的基础知识,并继续构建我们在本章和上一章开始开发的项目。
在下一章中,读者将学习如何使用一些内置的 Angular 2 指令来创建 UI 模板。读者将熟悉模板语法。我们将向您展示如何在 HTML 页面中绑定属性和事件,并使用管道来转换显示。
第四章:创建模板
在本章中,我们将学习如何使用内置的 Angular 2 指令构建 UI 模板。您将熟悉模板语法,以及如何在 HTML 页面中绑定属性和事件,并使用管道转换显示。当然,我们需要讨论 Angular 2 背后的设计原则。
在本章结束时,您将对以下内容有扎实的理解:
-
模板表达式
-
各种绑定类型
-
输入和输出属性
-
使用内置指令
-
本地模板变量
-
管道和 Elvis 运算符
-
自定义管道
-
设计应用程序的组件
深入了解 Angular 2
我们已经读了三章,但还没有涉及 Angular 2。我认为现在是时候邀请 Angular 2 上台,演示这个框架如何帮助我们创建项目组件。正如我在第一章中提到的,说你好!,Angular 2 的架构建立在标准 Web 组件的基础上,因此我们可以定义自定义 HTML 选择器并对其进行编程。这意味着我们可以创建一组 Angular 2 元素来在项目中使用。在之前的章节中,我们设计并开发了两个页面,您可以在那里找到许多重复的标记,因此我们也可以在那里重用我们的 Angular 2 组件。
让我们开始:
-
打开终端,创建名为
ecommerce
的文件夹并进入 -
将项目的内容从文件夹
chapter_4/1.ecommerce-seed
复制到新项目中 -
运行以下脚本以安装
npm
模块:
**npm install**
- 使用以下命令启动 TypeScript 监视器和 lite 服务器:
**npm run start**
此脚本打开 Web 浏览器并导航到项目的欢迎页面。现在打开 Microsoft Visual Studio 代码并从app
文件夹中打开app.component.html
。我们准备分析欢迎页面。
欢迎页面分析
欢迎页面的结构相当简单,因此我想创建以下 Angular 2 组件,以封装当前标记和未来的业务逻辑:
-
带有菜单的
Navbar
-
基于 carousel Bootstrap 组件的幻灯片放映
-
基于 card Bootstrap 组件的产品网格
在开发项目时,我将遵循 Angular 2 风格指南(angular.io/docs/ts/latest/guide/style-guide.html
),以使我们的应用代码更清洁、易于阅读和维护。我建议在你的计划中遵循我的示例,否则,开发结果可能是不可预测的,而且成本极高。
单一职责原则
我们将在项目的所有方面应用单一职责原则,因此每当我们需要创建一个组件或服务时,我们将为其创建新文件,并尽量保持在最多 400 行代码内。保持一个文件中只有一个组件的好处是显而易见的:
-
使代码更具可重用性,减少错误
-
易于阅读、测试和维护
-
防止与团队在源代码控制中发生冲突
-
避免不必要的代码耦合
-
组件路由可以在运行时进行惰性加载
命名约定
命名约定对于可读性和可维护性至关重要是公开的。能够找到文件并理解其包含的内容可能会对未来的开发产生重大影响,因此我们应该在命名和组织文件时保持一致和描述性,以便一目了然地找到内容。约定包括以下规则:
-
所有功能的推荐模式描述了名称,然后是其类型:
feature.type.ts
-
描述性名称中的单词应该用破折号分隔:
feature-list.type.ts
-
其中包括
service
、component
、directive
和pipe
等类型的命名是众所周知的:feature-list.service.ts
桶
有桶模块——导入、聚合和重新导出其他模块的 TypeScript 文件。它们有一个目的——减少代码中的import
语句数量。它们提供了一个一致的模式,从一个文件夹中引入桶中导出的所有内容。这个文件的常规名称是index.ts
。
应用程序结构
我们将应用代码保存在app
文件夹中。为了方便快速访问文件,建议尽可能保持扁平的文件夹结构,直到创建新文件夹有明显的价值为止。
按功能划分的文件夹结构
对于小型项目,你可以将所有文件保存在app
文件夹中。我们的项目有许多功能,所以我们将每个功能放在它们的文件夹中,包括 TypeScript、HTML、样式表和规范文件。每个文件夹的名称代表它所具有的功能。
共享文件夹
有一些功能我们可以在多个地方使用。最好将它们移动到shared
文件夹中,并根据需要将它们分开放置到文件夹中。如果项目中存在功能,请定义整体布局并将其保存在这里。
导航组件
整个应用程序需要一个导航组件,因此我们需要在navbar
文件夹中创建navbar.component.ts
和navbar.component.html
文件。以下是我们项目的文件夹结构:
打开navbar.component.ts
文件,并粘贴以下内容:
import { Component } from '@angular/core';
@Component({
selector: 'db-navbar',
templateUrl: 'app/navbar/navbar.component.html'
})
export class NavbarComponent {}
在代码中,我们刚刚使用@Component
装饰器定义了NavbarComponent
类,告诉 Angular 该类是一个组件。我们在这里使用import
语句来指定模块,TypeScript 编译器可以在其中找到@Component
装饰器。
装饰器
装饰器是 ECMAScript 2016 的一个提议标准,并作为 TypeScript 的一个关键部分,定义了一种可重用的结构模式。每个装饰器都遵循@expression
的形式。expression
是一个在运行时评估的函数,提供有关装饰语句的信息,以改变其行为和状态。我们可以使用装饰器函数,它作为expression
的评估结果返回,来自定义装饰器应用于声明的方式。可以将一个或多个装饰器附加到任何类、方法、访问器、属性或参数声明上。
@Component
是一个类装饰器,应用于NavbarComponent
类的构造函数,用于以下目的:
-
修改通过一组参数传递的类定义
-
添加建议的方法来组织组件的生命周期
我们必须为每个@Component
装饰器定义selector
参数,并使用kebab-case
进行命名。样式指南建议通过selector
将组件标识为元素,因为这为代表模板内容的组件提供了一致性。我使用db-navbar
选择器名称来命名NavigationComponent
,这是一个组合名称。
-
db
前缀显示了 Dream Bean 公司的名称缩写
-
navbar
作为功能的名称
注意
始终使用前缀为选择器名称,以防止与其他库中的组件名称冲突。
模板是@Component
装饰器的必需部分,因为我们将其与在页面上放置内容相关联。您可以在代码中提供template
作为内联字符串或templateUrl
作为外部资源。最好将模板的内容保留为外部资源:
-
当它有超过三行时
-
因为一些编辑器不支持内联模板的语法提示
-
因为不与内联模板混合在一起时,组件的逻辑更容易阅读
现在,打开app.component.html
,找到顶部的nav
元素。将其与内容剪切并粘贴到navbar.component.html
中,并替换为:
<db-navbar></db-navbar>
现在我们需要将NavbarComponent
添加到AppModule
中。打开app.module.ts
,在那里添加对NavbarComponent
的引用:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
/*
* Components
*/
import { AppComponent } from './app.component';
**import { NavbarComponent } from './navbar/navbar.component';**
@NgModule({
imports: [BrowserModule],
declarations: [AppComponent, **NavbarComponent**
],
bootstrap: [AppComponent]
})
export class AppModule { }
组件树
每个 Angular 应用程序都有一个顶级元素来显示内容。在我们的应用程序中,它是一个AppComponent
。在第三章中,高级引导组件和自定义,我们将欢迎页面拆分为引导组件,现在我们将它们移动到单独的模块中,并借助 Angular 框架将它们组合在一起。Angular 框架将应用程序呈现为组件树,从顶级元素、其子级以及更深层次。当我们需要向任何组件添加子级时,我们通过 Angular 模块的declarations
属性进行注册。NavigatorComponent
不属于任何 Angular 特性模块,因此我在最顶层模块AppModule
中注册它。
让我们回到navbar.component.html
,找到其他重复的元素。在显示导航栏的地方,我们有导航项:
<div class="nav navbar-nav">
<a class="nav-item nav-link active" href="#">
Home <span class="sr-only">(current)</span>
</a>
<a class="nav-item nav-link" href="#">Checkout</a>
<a class="nav-item nav-link" href="#">Create Account</a>
<a class="nav-item nav-link" href="#">Login</a>
</div>
因为在标记中有重复项,我建议创建一个链接数组并将其保存在NavbarComponent
中作为属性,这样 Angular 可以在这里显示它们。
NavItem 对象
我建议您创建一个单独的NavItem
接口来保存有关导航的信息,因为每个项目都应该有href
、label
和active
属性:
export interface NavItem {
// Navigation link
href: string;
// Navigation Label
label: string;
// Status of Navigation Item
active: boolean;
}
复制并粘贴代码到NavbarComponent
类的顶部和最后一个导入语句之间。现在我们可以将navItems
属性添加到NavbarComponent
中,以公开导航项:
@Component({
selector: 'db-navbar',
templateUrl: 'app/navbar/navbar.component.html'
})
export class NavbarComponent {
// App name
appName: string = 'Dream Bean';
// Navgation items
navItems: NavItem[] = [
{href: '#', label: 'Home', active: true},
{href: '#', label: 'Products', active: false},
{href: '#', label: 'Checkout', active: false},
{href: '#', label: 'Sign out', active: false}
];
}
我添加了appName
属性,以便将应用程序名称从模板中排除。我们已经准备好使用数据绑定,但在这样做之前,让我们更仔细地看一下模板表达式和语句。
模板表达式
模板表达式是数据绑定的核心部分。其主要目的是执行表达式以产生一个值,以便 Angular 可以将其分配给 HTML 元素、指令或组件的绑定属性。我们可以以两种形式将表达式放入模板中:
-
在插值大括号内。Angular 首先评估大括号内的内容,然后转换为字符串:
{{a + 1 - getVal()}}.
-
在引号内设置视图元素的属性为模板表达式的值时:
<button [disabled]="isUnchanged">Disabled</button>.
模板表达式基于类似 JavaScript 的语言。我们可以使用任何 JavaScript 表达式,但有以下限制:
-
禁止使用
=
,+=
,-=
等赋值操作符 -
不要使用
new
关键字 -
不要创建带有
;
或,
的链式表达式 -
避免使用递增
++
和递减--
运算符 -
不支持位运算符
|
和&
以及新的模板表达式运算符|
和?
表达式上下文
模板表达式的内容仅属于组件实例,不能引用全局上下文中的变量或函数。组件实例提供了模板表达式可以使用的一切。通常是表达式的上下文,但也可以包括除组件以外的对象,如模板引用变量。
模板引用变量
模板引用变量是模板内的 DOM 元素或指令的引用。您可以将其用作任何本机 DOM 元素和 Angular 2 组件的变量。我们可以在某人、兄弟或任何子元素上引用它。我们可以以两种形式定义它:
- 在前缀哈希(
#
)和变量名内:
<input **#product**
placeholder="Product ID">
<button (click)="findProduct(product.value)">Find</button>
- 使用
ref-
前缀和变量名的规范替代:
<input **ref-product**
placeholder="Product ID">
<button (click)="findProduct(product.value)">Find</button>
在这两个位置,变量product
将其value
传递给findProduct
方法。(click)
是数据绑定的形式,我们将很快讨论它。
表达式指南
Angular 框架的作者建议在模板表达式中遵循这些指南:
-
它们应该只改变目标属性的值。禁止对其他应用程序状态进行更改。
-
它们必须尽快执行,因为它们比其他代码执行更频繁。当计算昂贵时,考虑缓存计算值以获得更好的性能。
-
请避免创建复杂的模板表达式。通常,您可以从属性中获取值或调用方法。将复杂逻辑移到组件的方法中。
-
请创建幂等表达式,它们总是返回相同的值,直到其中一个依赖值发生变化。在事件循环期间不允许更改依赖值。
表达式操作符
模板表达式语言包括一些特定场景的操作符。
Elvis 操作符
有很常见的情况是,我们想要绑定到视图的数据是暂时未定义的。比如我们渲染一个模板并同时从服务器获取数据。在获取调用是异步的情况下,数据是不确定的。由于 Angular 默认不知道这一点,它会抛出错误。在下面的标记中,我们看到product
可能等于null
:
<div>Product: {{product.name | uppercase}}</div>
渲染视图可能会因为null
引用错误而失败,或者更糟糕的是完全消失:
TypeError: Cannot read property 'name' of null in [null]
每次编写标记时,都需要进行分析。如果您决定product
变量绝对不应该是null
,但实际上却是null
,那么您会发现应该捕获和修复的编程错误,因此这就是抛出异常的原因。相反,null
值可能会出现在属性中,因为获取调用是异步的。在最后一种情况下,视图必须在没有异常的情况下呈现,并且null
属性路径必须显示为空白。我们可以通过几种方式解决这个问题:
-
包括未定义的检查
-
确保数据始终具有初始值
它们都很有用并且有价值,但看起来很繁琐。例如,我们可以在ngIf
中包裹代码,并检查product
变量及其属性的存在:
<div ***ngIf="product && product.name">**
Product: {{product.name | uppercase}}
</div>
这段代码值得注意,但如果路径很长,它会变得繁琐且难看。没有什么解决方案比Elvis操作符更优雅。Elvis 或安全导航操作符是保护模板表达式在属性路径中避免null
或undefined
异常的便捷方式。
<div>Product: {{product?.name | uppercase}}</div>
当遇到第一个 null 值时,Angular 停止表达式评估,显示为空白,并且应用程序不会崩溃。
管道操作符
模板的主要目的之一是显示数据。我们可以直接将字符串值的原始数据显示到视图中。但大多数时候,我们需要将原始日期转换为简单格式,给原始浮点数添加货币符号,等等,因此我们知道有些值在显示之前需要一点处理。我觉得我们在许多应用程序中都需要相同的转换。Angular 框架为我们提供了管道,一种在模板中声明的显示值转换的方法。
管道是简单的函数,接受一个输入值并返回一个转换后的值。我们可以在模板表达式中使用它们,使用管道操作符 (|
):
<div>Product: {{product.name | **uppercase**
}}</div>
uppercase
是我们放在管道操作符之后的管道函数。可以通过多个管道链接表达式:
<div>Product: {{product.name | **uppercase**
| **lowercase**
}}</div>
管道链总是从第一个管道将产品名称转换为 大写
开始,然后转换为 小写
。可以向管道传递参数:
<div>Expiry Date: {{product.expDate | **date:'longDate'**
}}</div>
这里有一个带有配置参数的管道,指示将到期日期转换为长日期格式:1969 年 8 月 2 日
。Angular 2 中提供了一系列常见的管道:
-
async
管道订阅可观察对象或承诺,并返回它发出的最新值。 -
date
根据请求的格式将值格式化为字符串。 -
i18nSelect
是一个通用选择器,显示与当前值匹配的字符串。 -
percent
将数字格式化为本地百分比。 -
大写
实现了文本的大写转换。 -
number
将数字格式化为本地文本。即基于活动区域设置的组大小和分隔符等区域特定配置。 -
json
使用JSON.stringify
转换任何输入值。用于调试。 -
replace
创建一个新字符串,其中某些或所有模式的匹配项被替换为替换项。 -
currency
将数字格式化为本地货币。 -
i18nPlural
将值映射到正确复数形式的字符串。 -
lowercase
将文本转换为小写。 -
slice
创建一个只包含元素的子集(切片)的新列表或字符串。
自定义管道
我们可以根据需要创建类似于 json
的自定义管道,如下所示:
-
从 Angular 核心模块导入
Pipe
和PipeTransform
-
创建一个实现
PipeTransform
的JsonPipe
类 -
将
@Pipe
装饰器应用到JsonPipe
类,并给它一个名字db-json
-
使用
string
类型的输入值编写transform
函数
这是我们管道的最终版本:
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({name: 'db-json'})
export class JsonPipe implements PipeTransform {
transform(value: any): string {
return JSON.stringify(value);
}
}
现在我们需要一个组件来演示我们的管道:
import {Component} from '@angular/core';
import { **JsonPipe**
} from './shared/json.pipe';
@Component({
selector: 'receiver',
template: `
<h2>Receiver</h2>
<p>Received: {{data | db-json}}</p>
`
})
export class PowerBoosterComponent {
data: any = {x: 5, y: 6};
}
模板语句
模板语句 是数据绑定的另一个重要部分。我们使用模板语句来响应由绑定目标(如元素、指令或组件)引发的事件。它基于类似 JavaScript 的语言,就像模板表达式一样,但 Angular 解析方式不同,因为:
-
它支持基本赋值
=
-
它支持使用
;
或,
连接表达式
语句上下文
语句表达式,就像模板表达式一样,只能引用绑定事件的组件实例或模板引用变量。您可以在事件绑定语句中使用保留的 $event
,它表示引发事件的有效负载。
语句指南
Angular 框架的作者建议避免创建复杂的语句表达式。通常,您可以为属性分配一个值或调用方法。将复杂逻辑移到组件的方法中。
数据绑定
我在第一章 说你好! 中简单提到了数据绑定,但现在我们要更深入地了解 Angular 框架的这个关键工具。数据绑定是通过组件的属性或方法更新模板的机制。
数据绑定流支持数据源和目标 HTML 元素之间的三个方向:
- 从数据源到目标 HTML 的单向绑定。这组包括插值、属性、属性、类和样式绑定类型:
{{expression}}
[target] = "expression"
bind-target = "expression"
- 从目标 HTML 到数据源的单向绑定。这是事件数据绑定:
(target) = "statement"
on-target = "statement"
- 双向数据绑定:
[(target)] = "expression"
bindon-target = "expression"
target
是指令或组件的输入属性,用于接收外部数据。在开始使用之前,我们必须明确声明任何输入属性。有两种方法可以做到这一点:
使用 @Input
装饰器标记属性:
**@Input()**
product: Product;
将属性标识为指令或组件元数据的 inputs
数组的元素:
@Component({
**inputs:**
['product']
})
托管父元素可以使用 product
属性名称:
<div>
<db-product [ **product**
]="product"></db-product>
</div>
可以使用 别名 为属性获取不同的公共名称,以满足传统的期望:
@Input( **'bestProduct'**
) product: Product;
现在任何托管父元素都可以使用 bestProduct
属性名称而不是 product
:
<div>
<db-product **[bestProduct]**
="product"></db-product>
</div>
HTML 属性与 DOM 属性
HTML 属性和 DOM 属性不是同一回事。我们只使用 HTML 属性来初始化 DOM 属性,不能以后更改它们的值。
注意
模板绑定与 DOM 属性和事件一起工作,而不是 HTML 属性。
内插
当我们需要在页面上显示组件的属性值时,我们使用双大括号标记告诉 Angular 如何显示它。让我们以这种方式更新navbar.component.html
中的代码:
<a class="navbar-brand" href="#"> **{{appName}}**
</a>
Angular 会自动从NavbarComponent
类中提取appName
属性的值,并将其插入到页面中。当属性发生变化时,框架会更新页面。内插只是一种语法糖,让我们的生活更轻松。实际上,它是属性绑定的一种形式之一。
属性绑定
属性绑定是一种设置元素、组件或指令属性的技术。我们可以以这种方式更改前面的标记:
<a class="navbar-brand" href="#" [ **innerHTML**
]="appName"></a>
我们可以通过ngClass
属性来更改类:
<div [ **ngClass**
]="classes">Binding to the classes property</div>
以下是如何更改组件或指令的属性:
<product-detail [ **product**
]="currentProduct"></product-detail>
由于模板表达式可能包含恶意内容,Angular 在显示它们之前会对值进行消毒。内插和属性绑定都不允许带有脚本标记的 HTML 泄漏到 Web 浏览器中。
属性绑定
HTML 元素有一些属性没有相应的 DOM 属性,比如ARIA
,SVG
和表格跨度。如果你尝试编写这样的代码:
<tr><td colspan="{{1 + 1}}">Table</td></tr>
因为表格数据标签具有colspan
属性,但没有colspan
属性,所以您将立即收到以下异常:
browser_adapter.js:77 EXCEPTION: Error: Uncaught (in promise): Template parse errors:
Can't bind to 'colspan' since it isn't a known native property ("
<tr><td [ERROR ->]colspan="{{1 + 1}}">Three-Four</td></tr>
")
在这种特殊情况下,我们可以使用属性绑定作为属性绑定的一部分。它使用前缀attr
后跟一个点(.
)和属性的名称。其他都一样:
<tr><td [attr.colspan]="1 + 1">Three-Four</td></tr>
类绑定
Angular 提供了对class binding的支持。类似于属性绑定,我们使用前缀class
,可选地跟着一个点(.
)和 CSS 类的名称。
<div class="meat special">Meat special</div>
我们可以将其替换为绑定到所需类名meatSpecial
的字符串:
<div [ **class**
]="meatSpecial">Meat special</div>
或者添加模板表达式isSpecial
来评估真或假,告诉 Angular 向目标元素添加或移除special
类:
<div [ **class.special**
]="isSpecial">Show special</div>
注意
使用NgClass指令同时管理多个类名。
样式绑定
可以通过样式绑定管理目标元素的样式。我们使用前缀style
,可选地跟着一个点(.
)和 CSS 样式属性的名称:
<button **[style.color]**
="isSpecial?'red':'green'">Special</button>
注意
使用NgStyle指令一次设置多个内联样式时。
在属性绑定中,数据始终是单向流动的,从组件的数据属性到目标元素。我们不能使用属性绑定从目标元素获取值或调用目标元素上的方法。如果元素引发事件,我们可以通过事件绑定来监听它们。
事件绑定
页面上的任何用户操作都会生成事件,因此 Angular 框架的作者引入了事件绑定。这种绑定的语法非常简单,由括号内的目标事件、等号和带引号的模板语句组成。目标事件是事件的名称:
<button **(click)**
="onSave()">Save</button>
如果愿意,您可以使用事件绑定的规范格式。它支持在名称前面加上on-
前缀,而不需要括号,如下所示:
<button **on-click**
="onSave()">Save</button>
在事件的名称在元素上不存在或输出属性未知的情况下,Angular 会报告这个错误为未知指令
。
我们可以使用事件对象名称$event
传递的有关事件的信息。Angular 使用目标事件来确定$event
的形状,如果 DOM 元素生成事件,则$event
是一个 DOM 事件对象,并且它包含target
和target.value
属性。检查这段代码:
<div #product>
<input [value]="product.name"
(input)="product.name=$event.target.value"><br>
{{product.name}}
</div>
我们定义了局部变量 product,并将输入元素的值绑定到其名称,并附加输入事件以侦听更改。当用户开始输入时,组件会生成 DOM 输入事件,并且绑定会执行该语句。
自定义事件
JavaScript 提供了一打事件,适用于各种情况,但有时我们想要为特定需求触发自定义事件。使用它们会很好,因为自定义事件在应用程序中提供了出色的解耦。JavaScript 提供了CustomEvent
,可以执行各种令人惊叹的操作,但 Angular 暴露了一个EventEmitter
类,我们可以在指令和组件中使用它来发出自定义事件。我们需要做的是创建一个EventEmitter
类型的属性,并调用emit
方法来触发事件。可以传递任何消息负载。这个关于 Angular 的属性作为输出,因为它从指令或组件向外部触发事件。我们必须在开始使用之前明确声明任何输出属性。有两种方法可以做到这一点:
用@Output
装饰器标记属性:
**@Output()**
select:EventEmitter<Product>
将属性标识为指令或组件元数据的outputs
数组的元素:
@Component({
**outputs:**
['select']
})
如果需要,我们可以使用别名为属性提供不同的公共名称,以满足传统的期望:
**@Output('selected')**
select:EventEmitter<Product>
假设客户在产品网格中选择产品。我们可以在标记中监听鼠标click
事件,并在组件的browse
方法中处理它们:
<a class="btn btn-primary" (click)="browse(product)">Browse</a>
当方法处理鼠标事件时,我们可以使用选定的product
触发自定义事件:
import {Component, Input, Output, EventEmitter} from
'@angular/core';
export class Product {
name: string;
price: number;
}
@Component({
selector: 'db-product',
templateUrl: 'app/product/product.component.html'
})
export class ProductComponent {
@Input product: Product;
@Output() select:EventEmitter<Product> =
new EventEmitter<Product>();
browse($event) {
this.select.emit(<Product>$event);
}
}
从现在开始,任何托管父组件都可以绑定到ProductComponent
触发的select
事件。
<db-product [product]="product"
(select)="productSelected($event)"></db-product>
当select
事件触发时,Angular 调用父组件中的productSelected
方法,并将Product
传递给$event
变量。
双向数据绑定
大多数情况下,我们只需要单向绑定,其中数据从组件到视图或反之。通常,我们不捕获需要应用回 DOM 的输入,但在某些情况下,这可能非常有用。这就是为什么 Angular 支持双向数据绑定。如前所示,我们可以使用属性绑定将数据输入到指令或组件属性中,借助方括号:
<input **[value]**
="product.selected"></input>
相反的方向用括号括起来的事件名称表示:
<input **(input)**
="product.selected=$event.target.value">Browse</a>
我们可以结合这些技术,借助ngModel
指令来兼顾两者的优点。有两种形式的双向数据绑定:
- 括号放在方括号内。很容易记住,因为它的形状像“盒子里的香蕉”:
<input **[(ngModel)]**
="product.selected"></input>
- 使用规范前缀
bindon-
:
<input **bindon-ngModel**
="product.selected"></input>
当 Angular 解析标记并遇到这些形式之一时,它使用ngModel
输入和ngModelChange
输出来创建双向数据绑定,并隐藏幕后的细节。
注意
ngModel
指令仅适用于ControlValueAccessor
支持的 HTML 元素。
在自定义组件中无法使用ngModel
,直到我们实现合适的值访问器。
内置指令
Angular 具有少量强大的内置指令,可以涵盖我们在模板中需要执行的许多常规操作。
NgClass
我们使用类绑定来添加和移除单个类:
<div [ **class.special**
]="isSpecial">Show special</div>
在需要同时管理多个类的情况下,最好使用NgClass
指令。在使用之前,我们需要创建一个key:value
控制对象,其中键是 CSS 类名,值是布尔值。如果值为true
,Angular 会将键中的类添加到元素中,如果为false
,则会将其移除。这是返回key:value
控制对象的方法:
**getClasses()**
{
let classes = {
modified: false,
special: true
};
return classes;
}
因此,现在是添加NgClass
属性并将其绑定到getClasses
方法的时候了:
<div [ **ngClass**
]=" **getClasses**
()">This is special</div>
NgStyle
样式绑定有助于根据组件的状态设置内联样式。
<button [ **style.color**
]="isSpecial?'red':'green'">Special</button>
如果我们需要设置许多内联样式,最好使用NgStyle
指令,但在使用之前,我们需要创建key:value
控制对象。对象的键是样式名称,值是适用于特定样式的任何内容。这是key:value
控制对象:
getStyles() {
let styles = {
'font-style': 'normal',
'font-size': '24px'
};
return styles;
}
让我们添加NgStyle
属性并将其绑定到getStyles
方法:
<div [ **ngStyle**
]=" **getStyles**
()">
This div has a normal font with 8 px size.
</div>
NgIf
我们可以使用不同的技术来管理 DOM 中元素的外观。其中一种使用hidden
属性来隐藏页面中任何不需要的部分:
<h3 [ **hidden**
]="!specialName">
Your special is: {{specialName}}
</h3>
在上述代码中,我们将specialName
变量绑定到 HTML 的hidden
属性。另一个方法是使用内置指令如NgIf
来完全添加或移除页面中的元素:
<h3 * **ngIf**
="specialName">
Your special is: {{specialName}}
</h3>
隐藏和删除之间的差异是实质性的。不可见元素的好处是显而易见的:
-
它显示得非常快
-
它保留了先前的状态并准备好显示
-
不需要重新初始化
隐藏元素的副作用是:
-
它仍然存在于页面上,其行为继续
-
它占用资源,利用与后端的连接等
-
Angular 会持续监听事件并检查可能影响数据绑定等的变化
NgIf
指令的工作方式不同:
-
它完全移除了组件和所有子元素
-
已移除的元素不会占用资源
-
Angular 停止变更检测,分离元素并销毁它。
注意
我建议您使用 ngIf
来移除不需要的组件,而不是隐藏它们。
NgSwitch
如果我们想要基于某些条件仅显示一个元素树中的一个元素树,我们可以使用 NgSwitch
指令。为了使其工作,我们需要:
-
定义一个包含带有 switch 表达式的
NgSwitch
指令的容器元素 -
为每个元素定义一个带有
NgSwitchCase
指令的内部元素 -
不要超过一个带有
NgSwitchDefault
指令的项
NgSwitch
根据 NgSwitchCase
中的匹配表达式和从 switch 表达式评估的值来插入嵌套元素:
<div [ **ngSwitch**
]="condition">
<p * **ngSwitchWhen**
="true">The true value</p>
<p * **ngSwitchWhen**
="false">The false value</p>
<p * **ngSwitchDefault**
>Unknown value</p>
</div>
如果找不到匹配的表达式,则显示带有 NgSwitchDefault
指令的元素。
NgFor
与 NgSwitch
相反,NgFor
指令会渲染集合中的每个项。我们可以将其应用于简单的 HTML 元素或组件,语法如下:
<div ***ngFor**
="let product of products">{{product.name}}</div>
分配给 NgFor
指令的文本不是模板表达式。它是 微语法 ——Angular 解释如何迭代集合的语言。此外,Angular 将指令翻译为一组新的元素和绑定。NgFor
指令遍历 products
数组以返回 Product
的实例,并复制应用到它的 DIV 元素的实例。表达式中的 let
关键字创建了一个称为 product
的 模板输入变量,可在宿主及其子元素的作用域中使用,因此我们可以像在插值 {{product.name}}
中所做的那样使用它的属性。
注意
模板输入变量既不是模板,也不是状态引用变量。
有时候了解当前迭代元素的更多信息会很有用。NgFor
指令提供了几个类似于索引的导出值:
-
index
值设置为从 0 到集合长度的当前循环迭代 -
first
是一个布尔值,指示该项是否是迭代中的第一个 -
last
是一个布尔值,指示该项是否是集合中的最后一项 -
even
是一个布尔值,指示该项是否具有偶数索引 -
odd
是一个布尔值,指示该项是否具有奇数索引
因此,我们可以使用这些值中的任何一个来捕获一个本地变量,并在迭代上下文中使用它:
<div *ngFor="let product of products; let **i=index**
">
{{ **i**
+ 1}} - {{product.name}}
</div>
现在,让我们想象一个从后端查询到的产品数组。每次刷新操作都会返回包含一些甚至全部更改项目数量的列表。因为 Angular 不知道这些更改,它会丢弃旧的 DOM 元素,并用新的 DOM 元素重新构建一个新的列表。在列表中有大量项目时,NgFor
指令可能表现不佳,冻结 UI,并使 Web 应用程序完全无响应。如果我们给 Angular 提供一个跟踪集合中项目的函数,就可以解决这个问题,从而避免这种 DOM 重建的噩梦。跟踪依赖于对象标识,这样我们就可以使用一个或多个属性来比较集合中的新旧项目。术语对象标识是指基于===
标识运算符的对象相等性。以下是一个根据产品的 ID 属性进行跟踪的示例:
<div *ngFor="let product of products; trackBy: product.id; let i=index">
{{i + 1}} - {{product.name}}
</div>
因此,我们可以使用跟踪函数,如下所示:
trackByProductId(index: number, product: Product): any {
return product.id;
}
是时候将跟踪函数添加到NgFor
指令表达式中了:
<div *ngFor="let product of products; trackBy:trackByProductId;
let i=index">
{{i + 1}} - {{product.name}}
</div>
跟踪函数无法删除 DOM 更改,但可以减少 DOM 更改的数量,使 UI 更加流畅和响应更快。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
结构指令
我们经常在内置指令中看到星号前缀,但我还没有解释它的目的。现在是时候揭开 Angular 开发人员对我们隐藏的秘密了。
我们正在开发单页面应用程序,有时候,我们需要高效地操作 DOM。Angular 框架通过几个内置指令来根据应用程序状态显示和隐藏页面的部分。一般来说,Angular 有三种指令:
-
组件:这是一个带有模板的指令,在我们的项目中会创建很多这样的组件。
-
属性指令:这种指令改变元素的外观或行为。
-
结构指令:这种指令通过添加或删除 DOM 元素来改变 DOM 布局。
结构指令使用 HTML 5 的template
标签来管理页面上组件的外观。模板允许声明 HTML 标记片段作为原型。我们可以将它们插入到页面的任何位置——头部、主体或框架集,但不显示:
<template id="special_template">
<h3>Your are special</h3>
</template>
使用模板必须克隆并将其插入到 DOM 中:
// Get the template
var template: HTMLTemplateElement =
<HTMLTemplateElement>document.
querySelector("#special_template");
// Find place where
var placeholder: HTMLElement =
<HTMLElement>document.
querySelector("place");
// Clone and insert template into the DOM
placeholder.appendChild(template.content.cloneNode(true));
Angular 将结构指令的内容保存在template
标签中,用script
标签替换,并在必要时使用它。由于模板形式冗长,Angular 开发人员引入了语法糖——用于隐藏冗长的指令的星号(*
)前缀:
<h3 *ngIf="condition">Your are special</h3>
当 Angular 读取和解析上述 HTML 标记时,它将星号替换回模板形式:
<template [ngIf]="condition">
<h3>Your are special</h3>
</template>
自定义结构指令
让我们创建类似于NgIf
的结构指令,我们可以使用它根据条件在页面上显示内容。在 Microsoft Studio Code 中打开项目,并创建if.directive.ts
文件,内容如下:
import {Directive, Input} from '@angular/core';
@Directive({ selector: '[dbIf]' })
export class IfDirective {
}
我们导入Directive
将其应用于IfDirective
类。我们可以在任何 HTML 元素或组件中将我们的指令作为属性使用。因为我们要操作模板的内容,所以我们需要TemplateRef
。此外,Angular 使用特殊的渲染器ViewContainerRef
来渲染模板的内容,因此我们需要导入它们并将它们作为私有变量注入到构造函数中:
import {Directive, Input} from '@angular/core';
import {TemplateRef, ViewContainerRef} from '@angular/core';
@Directive({ selector: '[dbIf]' })
export class IfDirective {
constructor(
private templateRef: TemplateRef<any>,
**private viewContainer: ViewContainerRef**
) { }
}
最后,属性用于保持布尔条件,以便指令根据该值添加或移除模板:它必须与指令同名,另外我们可以将其设置为只读:
@Input() set dbIf(condition: boolean) {
if (condition) {
**this.viewContainer.createEmbeddedView(this.templateRef);**
} else {
**this.viewContainer.clear();**
}
}
如果condition
为true
,则前面的代码调用视图容器创建引用模板内容的嵌入式视图,否则将其移除。这是我们指令的最终版本:
import {Directive, Input} from '@angular/core';
import {TemplateRef, ViewContainerRef} from '@angular/core';
@Directive({ selector: '[dbIf]' })
export class IfDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) { }
@Input() set dbIf(condition: boolean) {
if (condition) {
this.viewContainer.
createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
现在我们可以将我们的指令添加到宿主组件的directives
数组中,以便在使用NgIf
的地方使用它。
提示
您可以在chapter_4/2.ecommerce-navbar
找到源代码。
类别产品组件
我们将继续为我们的应用程序创建 Angular 组件。现在,我们对模板了如指掌,是时候创建Category
产品了。让我们创建category
目录和文件category.ts
。复制并粘贴以下代码:
export class Category {
// Unique Id
id: string;
// The title
title: string;
// Description
desc: string;
// Path to image
image: string;
}
因此,每个产品类别都有唯一的标识符、标题、描述和图片。现在创建文件category-card.component.ts
,复制并粘贴以下代码:
import {Component, Input, Output, EventEmitter}
from '@angular/core';
import {Category} from './category';
@Component({
selector: 'db-category-card',
templateUrl:
'app/category/category-card.component.html'
})
export class CategoryCardComponent {
@Input() category: Category;
@Output() select: EventEmitter<Category> =
new EventEmitter<Category>();
browse() {
this.select.emit(this.category);
}
}
这是一个我们在类别网格中使用的类别组件。它具有输入属性category
和输出事件select
。让我们看看标记是什么样的:
<div class="col-xs-12 col-sm-6 col-md-4">
<div class="card">
<img class="card-img-top img-fluid center-block product-item"
src="{{category.image}}" alt="{{category.title}}">
<div class="card-block">
<h4 class="card-title">{{category.title}}</h4>
<p class="card-text">{{category.desc}}</p>
<a class="btn btn-primary" (click)="browse()">Browse</a>
</div>
</div>
</div>
<!-- /.col -->
这是从app.component.html
中的标记的精确副本。我们在所有地方使用插值数据绑定。现在创建category-slide.component.ts
,复制并粘贴以下代码:
import {Component, Input, Output, EventEmitter} from '@angular/core';
import {Category} from './category';
@Component({
selector: 'db-category-slide',
templateUrl:
'app/category/category-slide.component.html'
})
export class CategorySlideComponent {
@Input() category: Category;
@Output() select: EventEmitter<Category> =
new EventEmitter<Category>();
browse() {
this.select.emit(this.category);
}
}
这个文件的源代码看起来非常类似于卡片类别,但标记不同:
<img src="{{category.image}}" alt="{{category.title}}">
<div class="carousel-caption">
<h2>{{category.title}}</h2>
</div>
这是轮播组件的 HTML 副本。现在是创建我们的第一个 Angular 特性模块的时候了。创建文件category.module.ts
并包含以下内容:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { CategoryCardComponent } from './category-card.component';
import { CategorySlideComponent } from './category-slide.component';
@NgModule({
imports: [CommonModule, RouterModule],
declarations: [CategoryCardComponent, CategorySlideComponent],
exports: [CategoryCardComponent, CategorySlideComponent]
})
export class CategoryModule { }
正如我们所知,Angular 模块是一个用NgModule
装饰器装饰的类。让我们看看我们用它定义了什么:
-
有
CategoryCardComponent
和CategorySlideComponent
组件属于该模块,所以我们必须将它们声明为其他组件、指令和管道一样在declarations
属性内声明 -
我们通过
exports
属性公开了CategoryCardComponent
和CategorySlideComponent
组件,以便其他组件模板可以使用它们 -
最后,我们在
imports
属性内导入了CommonModule
和RouterModule
,因为我们在这个模块中使用了它们的组件和服务
现在我们可以在其他模块或应用程序模块中包含这个模块文件,以使导出可用。打开app.module.ts
文件并相应地更新它:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
/**
* Modules
*/
**import { CategoryModule } from './category/category.module';**
/*
* Components
*/
import { AppComponent } from './app.component';
import { NavbarComponent } from './navbar/navbar.component';
@NgModule({
imports: [BrowserModule, CategoryModule],
declarations: [AppComponent, NavbarComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
这个改变使得CategoryCardComponent
和CategorySlideComponent
组件立即对应用程序组件可用。我定义了两个变量slideCategories
和cardCategories
来保存网格和幻灯片中卡片的数据。
以下是app.component.html
中的更改:
<!-- Indicators -->
<ol class="carousel-indicators">
<li data-target="#welcome-products"
*ngFor="let category of slideCategories; let first=first; let i=index"
attr.data-slide-to="{{i}}" [ngClass]="{active: first}"></li>
</ol>
我们在这里使用NgFor
指令与first
和index
值来初始化第一个组件的data-slide-to
属性和active
类:
<!-- Content -->
<div class="carousel-inner" role="listbox">
<div *ngFor="let category of slideCategories; let first=first"
class="carousel-item" [ngClass]="{active: first}">
<db-category-slide
[category]="category" (select)="selectCategory($event)">
</db-category-slide>
</div>
</div>
在这个标记中,我们形成了轮播图像的内容,所以我们在carousel-inner
组件中使用了NgFor
指令。我们使用第一个值来管理第一个组件的活动类:
<div class="row">
<db-category-card *ngFor="let category of cardCategories"
[category]="category" (select)="selectCategory($event)">
</db-category-card>
</div>
这是最后的更改,我们利用NgFor
指令创建了卡片网格。
提示
你可以在chapter_4/3.ecommerce-category
找到源代码。
总结
我们一直在谈论 Angular 应用程序的结构以及保持扁平化文件夹结构的重要性。因为我们遵循单一职责原则,我们每个文件只创建一个组件,并尽量保持它小。对于每个 Angular 应用程序来说,拥有一个共享文件夹是最佳实践。
我们已经谈了很多关于装饰器、组件树和模板。我们知道模板表达式和模板语句是数据绑定的关键部分。它们都基于类似 JavaScript 的受限版本。
模板表达式包括 Elvis 和管道运算符,用于特定场景。数据绑定支持三种流向,包括插值、属性绑定、属性绑定、类绑定、样式绑定、事件绑定和双向绑定。
Angular 有几个非常强大的指令,帮助我们操作 DOM 元素,如NgFor
,NgIf
,NgClass
,NgStyle
和NgSwitch
。我们学会了为什么要使用星号前缀以及什么是结构指令。
在第五章中,路由,我们将使用 Bootstrap 设置顶部导航。您将熟悉 Angular 的组件路由以及如何配置它。此外,我们将继续构建在前几章中开始开发的项目。
第五章:路由
许多 Web 应用程序需要多个页面或视图,Angular 很好地配备了其路由器来处理这一点。路由器使用 JavaScript 代码并在用户执行应用程序任务时管理视图之间的导航。在本章中,我们将看看如何创建静态路由,以及包含参数的路由,以及如何配置它们。我们还将发现一些可能会遇到的问题。在本章中,我们将使用 Angular 设置顶部导航。
在本章结束时,您将对以下内容有扎实的理解:
-
组件路由器
-
路由器配置
-
路由器链接和路由器出口
-
为我们的应用程序创建组件和导航
现代 Web 应用程序
你已经多次听说过单页应用程序(SPA),但为什么要开发这样的 Web 应用程序?有什么好处吗?
使用 SPAs 的主要想法非常简单-用户希望使用看起来和行为像本机应用程序的 Web 应用程序。 SPA 是一个 Web 应用程序,它加载单个 HTML 页面,并在用户与其上的多个组件交互时动态更新它。一些组件支持许多状态,例如打开,折叠等。使用服务器端渲染实现所有这些功能很难做到,因此大部分工作发生在客户端,即 JavaScript 中。这是通过通过具有处理数据的模型层和从模型读取的视图层来将数据与数据的呈现分离来实现的。
这个想法给代码带来了一定程度的复杂性,并经常导致改变人们对开发过程的看法。现在我们开始考虑应用程序的概念部分,文件和模块结构,引导性能问题等。
路由
由于我们正在制作 SPA,并且不希望有任何页面刷新,因此我们将使用 Angular 的路由功能。路由模块是 Angular 的重要部分。一方面,它有助于在用户浏览应用程序时更新浏览器的 URL。另一方面,它允许更改浏览器的 URL 以通过 Web 应用程序进行导航,从而允许用户创建书签以深入 SPA 中的位置。作为奖励,我们可以将应用程序拆分为多个包并按需加载它们。
随着 HTML 5 的引入,浏览器获得了通过编程方式创建新的浏览器历史记录条目的能力,而不需要新的请求来改变显示的 URL。这是通过使用历史记录的pushState
方法来实现的,它将浏览器的导航历史记录暴露给 JavaScript。因此,现代框架不再依赖锚点技巧来导航路由,而是可以依靠pushState
来执行历史记录操作而无需重新加载。
Angular 路由器使用这个模型来将浏览器 URL 解释为导航到客户端生成的视图的指令。我们可以传递可选参数给视图组件,以帮助它决定呈现什么具体内容。
让我们从以下开始:
-
打开终端,创建文件夹
ecommerce
并进入。 -
将项目文件夹
chapter_5/1.ecommerce-seed
中的内容复制到新项目中。 -
运行以下脚本安装 NPM 模块:
**npm install**
- 使用以下命令启动 TypeScript 监视器和轻量级服务器:
**npm run start**
这个脚本打开了网页浏览器并导航到项目的欢迎页面。
路由路径
在开始之前,让我们确切地规划一下梦幻豆杂货店网站所需的路由:
-
欢迎视图使用
/#/welcome
路径。这将是我们应用程序的入口点,它将以网格和幻灯片放映方式列出所有类别。 -
产品视图利用
/#/products
路径。我们将能够在选择的类别中看到商品。 -
我们在
/#/product/:id
上显示产品视图。在这里,我们将显示关于产品的信息。这里和下一个示例中的:id
是路由参数的标记。我们将在本章后面讨论它。 -
/#/cart
路径是我们将在其中看到购物车视图列出用户购物车中的所有商品的地方。 -
在
/#/checkout/:id
路径的结账视图中,我们将包括一个表单,允许用户添加联系信息;它还提供订单信息和购买条件。
这些是我们所有必要的路由;现在让我们看看如何创建它们。
安装路由器
路由器被打包为 Angular 内的一个模块,但它不是 Angular 核心的一部分,所以我们需要在systemjs.config.js
文件的引导配置中手动包含它:
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
**'@angular/router': 'npm:@angular/router/bundles/router.umd.js',**
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
基本 URL
如果我们决定使用路由,则应将base
元素作为head
标记中的第一个子元素添加。此标记中的引用解析相对 URL 和超链接,并告诉路由器如何组成导航 URL。对于我们的项目,我将"/"
分配给base
元素的href
,因为app
文件夹是应用程序根目录:
<base href="/">
如果我们将应用程序部署到特定上下文的服务器上,例如portal
,那么我们必须相应地更改这个值:
<base href="/portal">
Angular 路由器
从一个视图实际路由到另一个视图是通过Angular 路由器完成的。这是一个可选的服务,并表示特定 URL 的组件视图。它有自己的库包,我们必须在使用之前从中导入:
import { RouterModule } from '@angular/router';
路由器配置
应用程序只能有一个路由器。我们应该配置它,以便它知道如何将浏览器的 URL 映射到相应的Route
并确定要显示的组件。这样做的主要方法是使用带有路由数组的RouterModule.forRoot
函数,它用它引导应用程序。
创建基本路由
创建文件app.routes.ts
并从路由器包中导入必要的元素:
import { Routes, RouterModule } from **'@angular/router'**
;
现在创建常量以保持应用程序路由:
const routes: Routes = [
{ path: 'welcome', component: WelcomeComponent },
{ path: 'products', component: ProductListComponent },
// { path: 'products/:id', component: ProductComponent }
];
我们定义了描述导航方式的路由对象数组。每个路由将一个 URL“路径”映射到要显示的“组件”。路由器解析和构造 URL,帮助我们使用以下内容:
-
对基本元素的路径引用,消除了使用前导斜杠的必要性
-
绝对路径
查询参数
路由器配置中的第二项只指向products
,但正如我之前提到的,我们将能够在选择的类别中看到商品。听起来我们想要在我们的 URL 中包含的信息是可选的:
-
我们可以在不带额外信息的情况下离开请求以获取所有产品
-
我们可以使用特定类别来获取属于该类别的产品
这种类型的参数不容易适应 URL 路径,因此通常很难或不可能创建所需的模式匹配,以将传入的 URL 转换为命名路由。幸运的是,Angular 路由器支持URL 查询字符串,用于在导航期间传递任意信息。
路由器参数
routes
数组中的第三个元素在其路径中有一个id
。这是一个路由参数的标记;与视图组件对应的值将使用它来查找和呈现产品信息。在我们的示例中,URL 'product/20'
保留了id
参数的值20
。ProductComponent
可以使用这个值来查找并显示 ID 等于20
的产品。这个路由被注释掉,因为我们还没有实现ProductComponent
。
路由参数与查询参数
以下是一般规则,帮助您选择要使用的参数。当满足以下条件时,请使用路由参数:
-
该值是必需的
-
该值对于导航到另一个路由是必要的
当满足以下条件时,请使用查询参数:
-
该值是可选的
-
该值是复杂的或包含多变量。
在引导中注册路由
最后,我们应该使用RouterModule.forRoot
方法返回一个包含配置和准备就绪的路由服务提供程序和所需路由库的新RouterModule
实例:
export const routing = RouterModule.forRoot(routes);
之后,我们需要在AppModule
中注册返回的模块:
/*
* Routing
*/
import {routing} from './app.routes';
@NgModule({
imports: [BrowserModule, FormsModule,
routing, CategoryModule],
declarations: [AppComponent, NavbarComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
重定向路由
通常,当用户输入 Dream Bean 网站的地址时,他/她提供网站域名:www.dreambean.com
。
此 URL 不匹配任何配置的路由,并且 Angular 此刻无法显示任何组件。用户必须点击某个链接以导航到视图,或者我们可以教会配置使用redirectTo
属性显示特定路由:
const routes: Routes = [
{ path: '', redirectTo: 'welcome', pathMatch: 'full' },
{ path: 'welcome', component: WelcomeComponent },
{ path: 'products', component: ProductListComponent },
//{ path: 'products/:id', component: ProductComponent }
];
在进行这些更改之后,如果用户导航到原始 URL,路由器将从初始 URL(''
)转换为默认 URL('welcome'
)并显示欢迎视图。
重定向的路由具有一个必需属性pathMatch
,告诉路由器如何将 URL 与路径匹配。对于这个值,我们有两个选项:
-
full
表示所选路由必须与整个 URL 匹配 -
prefix
指示路由器将重定向路由与以path
中的前缀值开头的任何 URL 匹配。
路由出口
现在,一旦我们解决了路由器配置,就该在屏幕上展示一些组件了。但等等——我们需要一个地方放它们,这就是为什么路由出口要登场了。
RouterOutlet
是 Angular 动态填充的占位符,根据应用的路由。RouterOutlet
是之前导入的RouterModule
的一部分,所以我们不需要在其他地方导入它。这是一个将 SPA 分成三行的线框图:
SPA 的线框图
在第一行,我们保留NavigationComponent
;在最后一行,是页脚容器。中间的所有空间是RouterOutlet
将显示相应视图的地方。
欢迎视图
我们配置了应用程序路由并将它们添加到AppModule
,所以现在我们需要创建欢迎视图,因为它是路由的重要部分。创建一个welcome
文件夹,并在其中创建两个文件welcome.component.html
和welcome.component.ts
。现在将app.component.html
的内容移动到welcome.component.html
中,并替换它。标记将RouterOutlet
表示为一个组件:
<db-navbar></db-navbar>
<router-outlet></router-outlet>
<footer class="footer">
<div class="container">
<address>
<strong>Contact Info</strong><br>
0000 Market St, Suite 000, San Francisco, CA 00000, (123) 456-7890,
<a href="mailto:#">[email protected]</a>
</address>
</div>
</footer>
将以下内容复制粘贴到welcome.component.ts
中:
/*
* Angular Imports
*/
import {Component} from '@angular/core';
@Component({
selector: 'db-welcome',
templateUrl: 'app/welcome/welcome.component.html'
})
export class WelcomeComponent { }
我将几乎所有的代码从AppComponent
中移动到WelcomeComponent
中,并且它的大小大大减小了。
/*
* Angular Imports
*/
import {Component} from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: 'app/app.component.html',
})
export class AppComponent { }
我使用链接从欢迎视图导航到带有选定类别的产品视图,而不是调用selectCategory
方法,所以我也删除了最后一个。
页脚组件
现在,当你知道如何创建一个组件时,你可以自己动手做。创建footer
文件夹,footer.component.ts
和footer.component.html
。这里,footer.component.ts
的源代码如下:
/*
* Components
*/
import {Component} from '@angular/core';
@Component({
selector: 'db-footer',
templateUrl: 'app/footer/footer.component.html'
})
export class FooterComponent {}
正如你所看到的,它看起来和我们之前创建的其他组件一样。将application.component.html
中的页脚容器的内容移动到footer.component.html
中,并用FooterComponent
标签替换它,现在我们应用的 HTML 看起来非常整洁:
<db-navbar></db-navbar>
**<router-outlet></router-outlet>**
<db-footer></db-footer>
类别数据
我将类别数据保留为AppComponent
的一部分,因为在我们开始开发时,这是一种快速明显的方式。现在,随着应用程序的增长,是时候将所有类别数据移动到类别文件中了。打开category.ts
文件,将以下源代码复制到那里:
export interface Category {
// Unique Id
id: string;
// The title
title: string;
// Description
desc: string;
// Path to small image
imageS: string;
// Path to large image
imageL: string;
}
var categories: Category[] = [
{ id: '1', title: 'Bread & Bakery', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'The best cupcakes, cookies, cakes, pies, cheesecakes, fresh bread, biscotti, muffins, bagels, fresh coffee and more.' },
{ id: '2', title: 'Takeaway', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'It's consistently excellent, dishes are superb and healthily cooked with high quality ingredients.' },
{ id: '3', title: 'Dairy', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'A dairy product is food produced from the milk of mammals, primarily cows, water buffaloes, goats, sheep, yaks.' },
{ id: '4', title: 'Meat', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Only superior quality beef, lamb, pork.' },
{ id: '5', title: 'Seafood', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Great place to buy fresh seafood.' },
{ id: '6', title: 'Fruit & Veg', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'A variety of fresh fruits and vegetables.' }
];
export function getCategories() {
return categories;
}
export function getCategory(id: string): Category {
for (let i = 0; i < categories.length; i++) {
if (categories[i].id === id) {
return categories[i];
}
}
throw new CategoryNotFoundException(`Category ${id} not found`);
}
export class CategoryNotFoundException extends Error {
constructor(message?: string) {
super(message);
}
}
getCategories
函数返回类别列表。getCategory
返回根据 ID 找到的类别,或抛出CategoryNotFoundException
。
分类卡视图
让我们打开category-card.component.html
文件,并将标记更改如下:
<div class="col-xs-12 col-sm-6 col-md-4">
<div class="card">
<img class="card-img-top center-block product-item"
src="{{category.image}}" alt="{{category.title}}">
<div class="card-block">
<h4 class="card-title">{{category.title}}</h4>
<p class="card-text">{{category.desc}}</p>
<a class="btn btn-primary"
**(click)="filterProducts(category)">Browse</a>**
</div>
</div>
</div>
当用户点击浏览按钮时,Angular 会调用带有指定类别参数的filterProducts
方法。
打开category-card.component.ts
文件,从库中导入Router
,并在组件的构造函数中添加引用:
import {Component, Input} from '@angular/core';
**import {Router} from '@angular/router';**
import {Category} from './category';
@Component({
selector: 'db-category-card',
templateUrl:
'app/shared/category/category-card.component.html'
})
export class CategoryCardComponent {
@Input() category: Category;
constructor( **private router: Router**
) {}
filterProducts(category: Category) {
**this.router.navigate(['/products'],**
**{queryParams: { category: category.id} });**
}
}
注意filterProducts
方法。我们在应用程序的引导中使用了一个配置好的路由器,并且在这个组件中可用。因为我们决定使用查询参数,所以我调用了一个导航方法,并传递了相同的名称作为第二个参数对象。我们可以传递任何信息,Angular 会将其转换为 URL 的查询字符串,就像这样:
/products?category=1
我们已经完成了欢迎视图,现在转到产品视图。
产品数据
我们还没有使用后端服务器返回产品数据,所以让我们创建product.ts
文件,内容如下:
export interface Product {
// Unique Id
id: string;
// Ref on category belongs to
categoryId: string;
// The title
title: string;
// Price
price: number;
// Mark product with specialproce
isSpecial: boolean;
// Description
desc: string;
// Path to small image
imageS: string;
// Path to large image
imageL: string;
}
var products: Product[] = [
// Bakery
{ id: '1', categoryId: '1', title: 'Baguette/French Bread', price: 1.5, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Great eaten fresh from oven. Used to make sub sandwiches, etc.' },
{ id: '2', categoryId: '1', title: 'Croissants', price: 0.5, isSpecial: true, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'A croissant is a buttery, flaky, viennoiserie-pastry named for its well-known crescent shape.' },
// Takeaway
{ id: '3', categoryId: '2', title: 'Pizza', price: 1.2, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Pizza is a flatbread generally topped with tomato sauce and cheese and baked in an oven.' },
// Dairy
{ id: '4', categoryId: '3', title: 'Milk', price: 1.7, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Milk is a pale liquid produced by the mammary glands of mammals' },
{ id: '5', categoryId: '3', title: 'Cream Cheese', price: 2.35, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Cream cheese is a soft, mild-tasting fresh cheese with a high fat content.' },
// Meat
{ id: '6', categoryId: '4', title: 'Pork Tenderloin', price: 5.60, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'The pork tenderloin, in some countries called pork fillet, is a cut of pork. ' },
{ id: '7', categoryId: '4', title: 'Ribs, Baby Back', price: 4.85, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Pork ribs are a cut of pork popular in North American and Asian cuisines. ' },
{ id: '8', categoryId: '4', title: 'Ground Beef', price: 9.20, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Ground beef, beef mince, minced beef, minced meat is a ground meat made of beef that has been finely chopped with a large knife or a meat grinder.' },
// Seafood
{ id: '9', categoryId: '5', title: 'Tuna', price: 3.45, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'A tuna is a saltwater finfish that belongs to the tribe Thunnini, a sub-grouping of the mackerel family - which together with the tunas, also includes the bonitos, ackerels, and Spanish mackerels.' },
{ id: '10', categoryId: '5', title: 'Salmon', price: 4.55, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Salmon is the common name for several species of ray-finned fish in the family Salmonidae.' },
{ id: '11', categoryId: '5', title: 'Oysters', price: 7.80, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'The word oyster is used as a common name for a number of different families of saltwater clams, bivalve molluscs that live in marine or brackish habitats.' },
{ id: '12', categoryId: '5', title: 'Scalops', price: 2.70, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Scallop is a common name that is primarily applied to any one of numerous species of saltwater clams or marine bivalve mollusks in the taxonomic family Pectinidae, the scallops.' },
// Fruit & Veg
{ id: '13', categoryId: '6', title: 'Banana', price: 1.55, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'The banana is an edible fruit, botanically a berry, produced by several kinds of large herbaceous flowering plants in the genus Musa.' },
{ id: '14', categoryId: '6', title: 'Cucumber', price: 1.05, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Cucumber is a widely cultivated plant in the gourd family, Cucurbitaceae. ' },
{ id: '15', categoryId: '6', title: 'Apple', price: 0.80, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'The apple tree is a deciduous tree in the rose family best known for its sweet, pomaceous fruit, the apple.' },
{ id: '16', categoryId: '6', title: 'Lemon', price: 3.20, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'The lemon is a species of small evergreen tree native to Asia.' },
{ id: '17', categoryId: '6', title: 'Pear', price: 4.25, isSpecial: false, imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'The pear is any of several tree and shrub species of genus Pyrus, in the family Rosaceae.' }
];
export function getProducts() {
return products;
}
export function getProduct(id: string): Product {
for (let I = 0; I < products.length; i++) {
if (products[i].id === id) {
return products[i];
}
}
throw new ProductNotFoundException(`Product ${id} not found`);
}
export class ProductNotFoundException extends Error {
constructor(message?: string) {
super(message);
}
}
如果你仔细看,你会发现与category.ts
文件有相似之处。我只是遵循命名约定。
产品视图
产品视图提供了所选类别中所有商品的列表。从中,客户可以查看所有产品信息,并将列出的任何产品添加到他或她的购物车中。用户还可以导航到任何提供的类别,或使用Quick Shop功能按名称搜索产品。
产品视图的线框图
这个组件的布局是由两列组成的:
-
第一列包含Quick Shop和类别列表
-
第二列是一个嵌套的列,组合成行
快速购物组件
这是一个用于搜索的input-group
字段,并使用Quick Shop
来查看杂货店中的产品。我们使用 URL 查询字符串来传递搜索信息,就像我们为类别所做的那样,因为我们不知道用户会在搜索字段中输入什么。创建product
文件夹,我们将在其中添加所有属于product
的组件和服务。
让我们在product
文件夹中创建product-search.component.html
,内容如下:
<div class="card">
<div class="card-header">Quick Shop</div>
<div class="input-group">
<input #search type="text" class="form-control"
placeholder="Search for...">
<span class="input-group-btn">
<button class="btn btn-secondary" type="button"
(click)="searchProduct(search.value)">Go!
</button>
</span>
</div>
</div>
我使用了 Bootstrap 4 的input-groups
,里面有一个按钮在Card
组件中。模板引用变量search
使我们直接访问输入元素,这样当用户输入产品名称并点击Go!按钮时,我们可以在searchProduct
方法中使用文本值。创建product-search.component.ts
文件,并创建类似于CategoryCard
的ProductSearch
组件:
import {Component} from '@angular/core';
import {Router} from '@angular/router';
import {Product} from './product';
@Component({
selector: 'db-product-search',
templateUrl: 'app/product/product-search.component.html'
})
export class ProductSearchComponent {
constructor(private router: Router) {}
searchProduct(value: string) {
**this.router.navigate(['/products'],**
**{ queryParams: { search: value} });**
}
}
我使用Router
的导航方法通过以下 URL 搜索产品名称:
/products?search=Apple
现在,我们准备创建CategoryList
组件,以便用户可以使用它来选择类别。
类别列表组件
在第三章中,高级 Bootstrap 组件和自定义,我们介绍了灵活的 Bootstrap 4 list-group
组件。Categories
是一个无序项目列表,所以我们可以使用这个特定的列表来快速渲染类别。我使用相同的机制来更新 URL,使用CategoryCard
组件中使用的特定类别。在category
文件夹中创建category-list.component.html
,内容如下:
<div class="card">
<div class="card-header">Categories</div>
<div class="card-block">
<div class=" **list-group list-group-flush**
">
**<a class="list-group-item"**
***ngFor="let category of categories"**
**(click)="filterProducts(category)">**
**{{category.title}}</a>**
</div>
</div>
</div>
Card
组件包装了list-group
。内置的NgFor
指令帮助组织对类别的迭代,以显示项目。创建category-list.component.ts
,并复制并粘贴以下代码:
/*
* Angular Imports
*/
import {Component} from '@angular/core';
import {Router} from '@angular/router';
/*
* Components
*/
import {Category, getCategories} from './category';
@Component({
selector: 'db-category-list',
templateUrl: 'app/category/category-list.component.html'
})
export class CategoryListComponent {
categories: Category[] = getCategories();
constructor(private router: Router) {}
filterProducts(category: Category) {
this.router.navigate(['/products'], {
queryParams: { category: category.id}
});
}
}
我们使用category
文件中的getCategories
函数将它们全部分配给categories
变量。
更新 CategoryModule
您应该按照以下方式更新CategoryModule
:
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
**import {CategoryListComponent} from './category-list.component';**
import {CategoryCardComponent} from './category-card.component';
import {CategorySlideComponent} from './category-slide.component';
@NgModule({
imports: [CommonModule, FormsModule, RouterModule],
declarations: [CategoryListComponent, CategoryCardComponent, CategorySlideComponent],
exports: [CategoryListComponent, CategoryCardComponent, CategorySlideComponent]
})
export class CategoryModule {}
我导出了CategoryListComponent
,因为我们将在其他模块中使用它。
路由链接
大多数情况下,用户在页面之间导航是由于他们在链接上执行的操作,比如单击锚标签。我们可以将路由器绑定到页面上的链接,这样当用户单击链接时,它将导航到适当的应用程序视图。
注意
路由器在浏览器的历史日志中记录活动,以便返回和前进按钮按预期工作。
Angular 团队引入了RouterLink
指令到锚标签,将其绑定到包含路由链接参数数组的模板表达式。让我们借助RouterLink
创建Product Card
组件。
产品卡
我认为将产品呈现为卡片是一个好主意。我在product
文件夹中创建了product-card.component.html
,内容如下:
<div class="col-xs-12 col-sm-6 col-md-4">
<div class="card">
<img class="card-img-top center-block product-item"
src="{{product.imageS}}" alt="{{product.title}}">
<div class="card-block">
<h4 class="card-title">{{product.title}}</h4>
<p class="card-text">{{product.desc}}</p>
<a class="btn btn-primary"
**[routerLink]="['/product', product.id]">Browse</a>**
</div>
</div>
</div>
在我们的代码中,RouterLink
绑定在锚标签中。注意我们绑定到routerLink
的模板表达式。显然,它是一个数组,这意味着我们可以添加多个项目,Angular 将它们组合起来构建 URL。我们可以单独指定路由的所有部分,比如"product/1"
,但我故意将它们留作数组的分离项目,因为这样更容易维护。让我们解析一下:
-
第一项标识父根
"/product"
路径 -
对于这个父元素没有参数,比如
"product/groups/1"
,所以我们完成了 -
第二项标识产品的子路由,并需要 ID
RouterLink
的导航非常灵活,因此我们可以使用链接参数数组编写具有多级路由的应用程序。
在product
文件夹中创建一个product-card.component.ts
。RouterLink
属于RouterModule
,所以现在可以在标记上使用它。复制并粘贴以下代码到product-card.component.ts
中:
import {Component, Input} from '@angular/core';
import {Product} from './product';
@Component({
selector: 'db-product-card',
templateUrl: 'app/product/product-card.component.html'
})
export class ProductCardComponent {
@Input() product: Product;
}
我们将从ProductGreedComponent
绑定数据到ProductCardComponent
的实例中,通过product
属性。
产品网格组件
我们需要以三列和多行的网格形式显示产品。卡片组件是显示产品信息并导航到产品视图的最合适的组件。行中的所有卡片必须具有相同的宽度和高度。我们如何在父网格布局的特定位置显示它们?让我们在product
文件夹中创建product-grid.component.html
和product-grid.component.ts
文件。复制并粘贴以下代码到product-grid.component.ts
文件中:
/*
* Angular Imports
*/
import {Component} from '@angular/core';
/*
* Components
*/
import {Product, getProducts} from './product';
@Component({
selector: 'db-product-grid',
templateUrl: 'app/product/product-grid.component.html'
})
export class ProductGridComponent {
products: Product[] = getProducts();
}
卡片组
我们可以使用Bootstrap 4 Card组来呈现多个卡片作为单个附加元素,具有相等的宽度和高度。我们只需要将所有卡片放在带有card-group
类的父元素中。复制并粘贴以下代码到product-grid.component.html
文件中:
<div class= **"card-group"**
>
<db-product-card *ngFor="let product of products"
[product]="product"></db-product-card>
</div>
结果不是我想要的,因为一些卡片彼此附着:
!卡片组
卡片列
另一个布局是来自 Bootstrap 4 的card-columns
。它允许您在每列中显示多个卡片。每列中的每张卡片都堆叠在另一张卡片上。在card-columns
类中包含所有卡片。复制并粘贴以下代码到product-grid.component.html
文件中:
<div class= **"card-columns"**
>
<db-product-card *ngFor="let product of products"
[product]="product"></db-product-card>
</div>
结果看起来很有趣:
!卡片列
卡片桌
最后的布局是来自 Bootstrap 4 的卡片桌。它类似于卡片组,只是卡片之间没有连接。这需要两个包装元素:card-deck-wrapper
和card-deck
。它使用表格样式来设置card-deck
的大小和间距。card-deck-wrapper
用于在card-deck
上消除边框间距。
让我们回到product-card.component.html
文件,并使用以下内容进行更新:
<div class= **"card-deck-wrapper"**
>
<div class= **"card-deck"**
>
<div class="card" *ngFor="let product of products">
<div class="card-header text-xs-center">
{{product.title}}
</div>
<img class="card-img-top center-block product-item"
src="{{product.imageS}}" alt="{{product.title}}">
<div class="card-block text-xs-center"
[ngClass]="setClasses(product)">
<h4 class="card-text">
Price: ${{product.price}}
</h4>
</div>
<div class="card-footer text-xs-center">
<a class="btn btn-primary"
(click)="buy(product)">Buy Now</a>
<a class="btn btn-secondary"
[routerLink]="['/product', product.id]">
More Info
</a>
</div>
<div class="card-block">
<p class="card-text">{{product.desc}}</p>
</div>
</div>
</div>
</div>
卡片桌足够完美地使用一行,所以我们在ProductCardComponent
中公开products
输入:
import {Component, Input} from '@angular/core';
import {Product} from './product';
@Component({
selector: 'db-product-card',
templateUrl: 'app/product/product-card.component.html',
directives: [ROUTER_DIRECTIVES]
})
export class ProductCardComponent {
@Input() products: Product[];
setClasses(product: Product) {
return {
'card-danger': product.isSpecial,
'card-inverse': product.isSpecial
};
}
buy(product: Product) {
console.log('We bought', product.title);
}
}
setClasses
方法帮助更改卡片的背景,如果产品有special
价格。当用户点击立即购买按钮时,我们调用buy
方法。
有了这一切,我们可以更新ProductGridComponent
的标记:
<db-product-card *ngFor="let row of products"
[products]="row"></db-product-card>
相当整洁,不是吗?
但在使用我们漂亮的组件之前,我们需要将产品数组转换为每行三个产品的数组。请注意ProductGridComponent
构造函数中的代码:
import {Component} from '@angular/core';
import {Product, getProducts} from './product';
@Component({
selector: 'db-product-grid',
templateUrl: 'app/product/product-grid.component.html'
})
export class ProductGridComponent {
products: any = [];
constructor() {
let index = 0;
let products: Product[] = getProducts();
let length = products.length;
this.products = [];
while (length) {
let row: Product[] = [];
if (length >= 3) {
for (let i = 0; i < 3; i++) {
row.push(products[index++]);
}
this.products.push(row);
length -= 3;
} else {
for (; length > 0; length--) {
row.push(products[index++]);
}
this.products.push(row);
}
}
}
}
我们将产品分成多行,每行最多包含三列。
将它们全部组合在一起
现在我们创建一个组件,将所有其他产品组件组合起来,以便在路由器出口标记提供的位置显示它们。请欢迎ProductListComponent
!
创建一个product-list.component.ts
文件,内容如下:
/*
* Angular Imports
*/
import {Component} from '@angular/core';
/*
* Components
*/
@Component({
selector: 'db-products',
templateUrl: 'app/product/product-list.component.html'
})
export class ProductListComponent {}
现在,创建product-list.component.html
,并复制并粘贴下一个标记:
<div class="container">
<div class="row">
<div class="col-md-3">
<db-product-search></db-product-search>
<db-category-list></db-category-list>
</div>
<div class="col-md-9">
<db-product-grid></db-product-grid>
</div>
</div>
</div>
正如你所看到的,它在第一列中绘制了ProductSearchComponent
和CategoryListComponent
,在第二列中绘制了ProductGridComponent
,这与我们的线框相对应。
产品模块
在product
文件夹中的最后两个角色是ProductModule
。创建product.module.ts
文件如下:
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
import {ProductListComponent} from './product-list.component';
import {ProductCardComponent} from './product-card.component';
import {ProductSearchComponent} from './product-search.component';
import {ProductGridComponent} from './product-grid.component';
import {CategoryModule} from '../category/category.module';
@NgModule({
imports: [CommonModule, FormsModule, RouterModule, CategoryModule],
declarations: [ProductListComponent, ProductCardComponent, ProductSearchComponent, ProductGridComponent],
exports: [ProductListComponent, ProductCardComponent, ProductSearchComponent, ProductGridComponent]
})
export class ProductModule {}
它导入了CategoryModule
以及系统模块。我们声明并导出了之前创建的所有四个组件。
更新 AllModule
现在,有了CategoryModule
和ProductModule
,我们需要使它们所有的组件都可用于应用程序,以便我们可以将它们导入到AppModule
中:
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
/**
* Modules
*/
**import {CategoryModule} from './category/category.module';**
**import {ProductModule} from './product/product.module';**
/*
* Components
*/
import {AppComponent} from './app.component';
import {NavbarComponent} from './navbar/navbar.component';
import {FooterComponent} from './footer/footer.component';
import {WelcomeComponent} from './welcome/welcome.component';
/*
* Routing
*/
import {routing} from './app.routes';
@NgModule({
imports: [BrowserModule, FormsModule, routing,
**CategoryModule, ProductModule],**
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
NavbarComponent
,FooterComponent
和WelcomeComponent
直接属于AppModule
。
提示
您可以在chapter_5/2.ecommerce-router
找到源代码。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
路由更改事件
正如我们在查看路由器配置时提到的,当用户导航到以下 URL 时,ProductListComponent
可以表示产品视图:
/products?category=1
或者
/products?search=apple
ActivatedRouter
类包含与加载在出口中的组件相关联的路由信息。我们可以订阅路由更改事件,以通知ProductGridComponent
有关 URL 查询参数发生的变化。打开product-grid.component.ts
文件,从库中导入ActivatedRouter
,并将其注入到构造函数的router
属性中。现在我们可以订阅路由更改:
constructor(private router: ActivatedRouter) {
this.router
.queryParams
.subscribe(params => {
let category: string = params['category'];
let search: string = params['search'];
// Return filtered data from getProducts function
let products: Product[] =
getProducts(category, search);
// Transform products to appropriate data
// to display
this.products = this.transform(products);
});
}
在上面的代码中,我们正在监听只在queryParams
中发生的更改,并使用它们来过滤getProducts
函数中的数据。稍后,借助transform
方法,我们将把过滤后的产品转换成适合显示的数据。
transform(source: Product[]) {
let index = 0;
let length = source.length;
let products = [];
while (length) {
let row: Product[] = [];
if (length >= 3) {
for (let i = 0; i < 3; i++) {
row.push(source[index++]);
}
products.push(row);
length -= 3;
} else {
for (; length > 0; length--) {
row.push(source[index++]);
}
products.push(row);
}
}
return products;
}
最后,我们必须改变getProducts
函数的签名,因为现在我们可以传递两个参数:
export function getProducts(category?: string, search?: string) {
if (category) {
return products.filter(
(product: Product, index: number, array: Product[]) => {
return product.categoryId === category;
});
} else if (search) {
let lowSearch = search.toLowerCase();
return products.filter(
(product: Product, index: number, array: Product[]) => {
return product.title.toLowerCase().
indexOf(lowSearch) != -1;
});
} else {
return products;
}
}
这个函数根据我们发送给函数的参数,可以按类别过滤数据,搜索文本,或保持原样。保存代码,尝试使用过滤后的数据进行操作:
路由策略
我们已经配置了所有必要的路由,并且现在可以访问它们的单独视图。这很棒,但也许你不喜欢 URL 中跟随#
符号的路径。正如我提到的,现代 web 浏览器支持pushState
技术,可以在不向服务器发送请求的情况下改变浏览器的位置和历史记录。Router
使用这种方法来构建 URL。Angular 路由器使用不同的LocationStrategy
来支持新旧两种方式:
-
PathLocationStrategy
提供了默认的基于pushState
的 HTML 5 样式 -
HashLocationStrategy
利用 URL 样式中的哈希符号。
选择策略对于未来的开发至关重要,因为以后更改它不会很容易,所以最好在合适的时候做。如果您的服务器不支持在找不到路由时重定向到备用页面的能力,您可以使用HashLocationStrategy
。我们在开发中使用的lite-server
可能支持这个功能。
打开app.module.ts
文件,从 common 模块导入策略:
import {LocationStrategy, HashLocationStrategy ,
PathLocationStrategy} from '@angular/common';
我们将PathLocationStrategy
或HashLocationStrategy
注册为LocationStrategy
的提供者:
@NgModule({
imports: [BrowserModule, FormsModule,
routing, CategoryModule, ProductModule],
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent],
**providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}],**
bootstrap: [AppComponent]
})
export class AppModule { }
保存并检查应用程序在浏览器 URL 中是否有或没有哈希的工作方式。
提示
您可以在chapter_5/3.ecommerce-router-search
找到源代码。
摘要
在本章中,我们将我们的应用程序从单页面转变为多页面视图和多路由应用程序,我们可以在 Dream Bean 杂货店上构建。在编写任何代码之前,我们首先规划了应用程序中的基本路由。
然后,我们构建了包含参数的静态和动态路由。
最后,我们看了如何使用 HTML 5 的pushState
来删除 URL 中的#
符号,以及如何链接两种类型的路由。
在第六章中,依赖注入,我们将讨论依赖注入,教读者如何解耦应用程序的需求,以及如何创建一个一致的数据源作为服务。此外,我们将继续构建我们在前几章中开始开发的项目。
第六章:依赖注入
本章介绍了依赖注入,教您如何解耦应用程序的需求以及如何创建一个一致的数据源作为服务。您将了解注入器和提供者类。我们还将讨论 Injectable 装饰器,这是创建对象所必需的。
在本章结束时,您将对以下内容有扎实的理解:
-
什么是依赖注入?
-
关注点的分离
-
创建一个服务
-
注入器和提供者类
-
Injectable 和 inject 装饰器
-
为我们的应用程序创建数据服务
什么是依赖注入?
在这里,我将讨论依赖注入的概念,并提供一些具体的例子,希望能够演示它试图解决的问题以及它为开发人员带来的好处。Angular 主要基于依赖注入,您可能已经熟悉或不熟悉。如果您已经了解依赖注入的概念,可以安全地跳过本章,直接阅读下一章。
依赖注入可能是我所知道的最著名的设计模式之一,您可能已经使用过它。我认为这是最难解释清楚的设计模式之一,部分原因是由于大多数依赖注入介绍中使用的无意义的例子。我尝试提出更适合 Angular 世界的例子。
一个现实生活的例子
想象一下,您开了自己的业务,经常需要乘飞机出差,所以需要安排航班。您总是使用航空公司的电话号码自己预订航班。
因此,您的典型旅行计划例程可能如下所示:
-
决定目的地和期望的到达日期和时间
-
给航空公司打电话,传达必要的信息以预订航班
-
取票并上路
现在,如果您突然更改了首选机构及其联系机制,您将受到以下重新学习情景的影响:
-
新的机构及其新的联系机制(假设新机构提供基于互联网的服务,预订方式是通过互联网而不是通过电话)
-
必要的预订通过的典型对话顺序(数据而不是语音)
您需要调整自己以适应新情景。这可能会导致大量时间花在重新调整过程上。
假设你的业务正在发展,你在公司里雇了一个秘书,所以每当你需要出差时,你只需给他或她发送一封电子邮件,说明目的地、期望的到达日期和时间。机票会为你预订好,并送到你手上。
现在,如果首选机构发生了变化,秘书会意识到这种变化,并可能重新调整他或她的工作流程,以便与机构进行沟通。然而,你不需要重新学习。你仍然按照以前的协议继续,因为秘书以一种方式进行了所有必要的适应,这意味着你不需要做任何不同的事情。
在这两种情况下,你都是客户,并且依赖机构提供的服务。然而,第二种情况有一些不同之处:
-
你不需要知道机构的联系点——秘书会为你做这件事
-
你不需要知道机构通过语音、电子邮件、网站等方式进行活动的确切对话顺序,因为你知道与秘书的特定标准对话序列
-
你所依赖的服务以一种方式提供给你,如果服务提供者发生变化,你不需要重新调整
这就是现实生活中的依赖注入。
依赖注入
我们项目中使用的 Angular 和自定义组件都是一组协作组件的一部分。它们彼此依赖以完成其预期目的,并且它们需要知道:
-
要与哪些组件进行通信?
-
在哪里找到它们?
-
如何与它们沟通?
当访问方式发生变化时,这些变化可能需要修改许多组件的源。以下是我们可以使用的可能解决方案,以防止组件的剧烈变化:
-
我们可以将位置和实例化的逻辑嵌入到我们通常的组件逻辑中
-
我们可以创建外部代码片段来承担位置和实例化的责任,并在必要时提供引用
我们可以将最后一个解决方案看作是我们现实生活示例中的秘书。当定位任何外部依赖项的方式发生变化时,我们不需要更改组件的代码。这个解决方案是依赖注入的实现,其中一个外部代码片段是 Angular 框架的一部分。
使用依赖注入需要声明组件,并让框架处理实例化、初始化、排序和根据需要提供引用的复杂性。
将依赖项传递给使用它的依赖对象是依赖注入。组件可以以至少三种常见方式接受依赖项:
-
构造函数注入:在这种情况下,依赖项通过类构造函数提供。
-
Setter 注入:在这种情况下,注入器利用组件公开的 setter 方法来注入依赖项。
-
接口注入:在这种情况下,依赖项提供了一个方法,该方法将依赖项注入到传递给它的任何组件中。
构造函数注入
该方法要求组件在构造函数中提供依赖项的参数。我们在ProductGridService
组件的代码中注入了Router
实例:
**constructor(private router: ActivatedRoute) {**
this.router
.queryParams
.subscribe(params => {
let category: string = params['category'];
let search: string = params['search'];
// Return filtered data
let products: Product[] =
getProducts(category, search);
// Transform products to appropriate data
// to display
this.products = this.transform(products);
});
}
构造函数注入是最可取的方法,可以用来确保组件始终处于有效状态,但它缺乏能够稍后更改其依赖项的灵活性。
其他注入方法
Setter 和接口方法在 Angular 框架中没有实现。
组件与服务
Angular 2 在 Web 应用程序的代码上有所区别:
-
代表视觉部分的组件
-
可重用的数据服务
数据服务是一个简单的类,提供了返回或更新一些数据的方法。
ReflectiveInjector
ReflectiveInjector
是一个注入容器,我们将其用作替代new
运算符,用于自动解析构造函数的依赖项。当应用程序中的代码询问构造函数中的依赖项时,ReflectiveInjector
会解析它们。
import {Injectable, ReflectiveInjector} from '@angular/core';
@Injectable()
export ProductGridService {
constructor(private router: ActivatedRoute) {...}
}
const injector = ReflectiveInjector.resolveAndCreate
([ActivatedRoute, ProductGridService]);
const service = injector.get(ProductGridService);
使用resolveAndCreate
方法,ReflectiveInjector
创建了一个Injector
的实例。我们将服务提供者数组传递给注入器以配置它,否则它将不知道如何创建它们。
使用Injector
,创建ProductGridService
非常容易,因为它完全负责提供和注入ActivatedRoute
到ProductGridService
中。
让我们讨论为什么我们导入并应用了Injectable
装饰器到类中?
可注入装饰器
我们在应用程序中为特定需求创建多种类型。其中一些可能依赖于其他类型。我们必须使用Injectable
装饰器标记任何可用于注入器的类型。注入器使用类构造函数的元数据来获取参数类型,并确定实例化和注入的依赖类型。任何依赖类型都必须用Injectable
装饰器标记,否则在尝试实例化时,注入器将报告错误。
注意
为每个服务类添加@Injectable()
以防止依赖注入错误。
我们必须显式地为我们的所有服务类导入和应用Injectable
装饰器,以使它们可用于注入器进行实例化。没有这个装饰器,Angular 就不知道这些类型的存在。
注入装饰器
正如我所提到的,注入器使用类构造函数的元数据来确定依赖类型:
constructor(private router: ActivatedRoute) {...}
注入器使用 TypeScript 生成的元数据将ActivatedRoute
类型的实例注入到构造函数中。对于注入 TypeScript 原语,如string
、boolean
或数组,我们应该定义并使用 Opaque Token:
import { OpaqueToken } from '@angular/core';
export let APP_TITLE = new OpaqueToken('appTitle');
现在,有了定义的APP_TITLE
标记,我们可以在依赖提供者的注册中使用它:
providers: [{ provide: APP_TITLE, useValue: 'Dream Bean' }]
当我们将应用程序标题注入到我们应用程序的任何构造函数中时,我们使用@Inject
装饰器:
import {Inject} from '@angular/core';
constructor(@Inject('APP_TITLE') private appTitle) {...}
我们很快会谈到标记。
可选装饰器
在类具有可选依赖项的情况下,我们可以使用@Optional
装饰器来标记构造函数参数:
import {Optional} from '@angular/core';
constructor(@Optional('config') private config) {
if (config) {
// Use the config
...
}
}
我在上面的代码中添加了条件语句,因为我预期config
属性将等于null
。
配置注入器
在上面的示例中,我使用了ReflectiveInjector
的resolveAndCreate
方法来创建Injector
,但在现实生活中,这是不必要的。
const injector = ReflectiveInjector.resolveAndCreate
([ActivatedRoute, ProductGridService]);
在应用程序的启动过程中,Angular 框架为我们创建了一个应用程序范围的注入器:
platformBrowserDynamic().bootstrapModule(AppModule);
我们必须通过注册创建应用程序所需服务的提供者来配置注入器。我们可以通过两种方式来做到这一点:
-
在
NgModule
中注册提供者 -
在
AppComponent
中注册提供者
哪一个更好?注入到AppModule
中的服务在整个应用程序中都是广泛可用的,并且可以注入到惰性加载模块及其组件中。注入到AppComponent
中的服务仅对该组件及其子组件可用,并且不可用于惰性加载模块。
注意
在根AppModule
中注册应用程序范围的提供者,而不是在AppComponent
中。
我们可以在适当的情况下配置注入器以使用替代提供者:
-
提供的对象行为或外观与原始对象相似
-
提供替代类
-
提供一个工厂函数
例如对于AppModule
类:
@NgModule({
imports: [BrowserModule, FormsModule,
routing, CategoryModule, ProductModule],
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent],
**providers: [ProductService],**
bootstrap: [AppComponent]
})
export class AppModule { }
在注册注入器中提供者时,我们使用了一种简写表达式。Angular 将其转换为以下冗长格式:
[{provide: Router, useClass: Router]
首先的provide
属性是作为键的标记:
-
定位依赖值
-
注册提供者
第二个属性useClass
是一个类似于许多其他use东西的定义对象,如useValue
、useExisting
等,并告诉框架如何创建依赖关系。借助use定义,我们可以快速切换实现,定义常量和工厂函数。让我们来看看它们。
类提供者
大多数时候,我们将利用useClass
定义来要求不同的类提供服务。我们可以创建我们自己的BetterRouter
类作为原始类的扩展,并注册它,如下所示:
[{ provide: Router, **useClass: BetterRouter**
}]
注入器知道如何构建BetterRouter
并将其解决。
别名类提供者
在需要使用同一个单例的许多提供者的情况下,我们可以使用useExisting
定义:
class BetterRouter extends Router {}
var injectorClass = ReflectiveInjector.resolveAndCreate([
BetterRouter, {provide: Router, **useClass:**
BetterRouter}
]);
var injectorAlias = ReflectiveInjector.resolveAndCreate([
BetterRouter, {provide: Router, **useExisting:**
BetterRouter}
]);
看下面的例子,useExisting
如何帮助组织模拟请求:
var injector = Injector.resolveAndCreate([
HTTP_PROVIDERS,
MockBackend,
{ provide: XHRBackend, useExisting: MockBackend }
]);
var http = injector.get(Http);
var backend = injector.get(MockBackend);
下面的代码演示了如何使用MockBackend
而不是真实的后端,进行 AJAX 请求:
var people = [{name: 'Jeff'}, {name: 'Tobias'}];
// Listen for any new requests
**backend.**
connections.observer({
next: connection => {
var response = new Response({body: people});
setTimeout(() => {
// Send a response to the request
connection.mockRespond(response);
});
}
});
**http.**
get('people.json').observer({
next: res => {
// Response came from mock backend
console.log('first person', res.json()[0].name);
}
});
useExisting
另一个有用的地方是提供自定义管道、自定义指令或自定义验证器的多个值:
@Directive({
selector: '[custom-validator]',
providers: [{ provide: NG_VALIDATORS,
**useExisting: CustomValidatorDirective, multi: true }]**
})
class CustomValidatorDirective implements Validator {
validate(c: Control): { [key: string]: any } {
return { "custom": true };
}
}
借助multi
选项,可以将CustomValidatorDirective
添加到默认集合中,使其在应用程序中全局可用。
值提供者
有时我们需要在应用程序中使用配置对象、字符串或函数,并不总是类的实例。在这里,接口定义了配置的结构:
export interface **Config**
{
url: string;
title: string;
}
export const **CUSTOM_CONFIG:**
Config = {
url: 'www.dreambean.com',
title: 'Dream Bean Co.'
};
我们可以使用useValue
定义注册现成的对象。没有Config
类,所以我们不能用它来作为标记。相反,我们可以使用字符串字面量来注册和解析依赖项:
providers: [{ provide: **'app.config', useValue: CUSTOM_CONFIG**
}]
现在我们可以通过@Inject
装饰器将其注入到任何构造函数中:
constructor(@Inject( **'app.config'**
) config: **Config**
) {
this.title = config.title + ':' + config.url;
}
不幸的是,使用字符串标记会导致命名冲突的潜在问题。Angular 提供了一个优雅的解决方案,使用Opaque Token
来处理非类依赖项:
import { OpaqueToken } from '@angular/core';
export let **CONFIG**
= new **OpaqueToken**
('app.config');
我们正在使用值提供者在注入器中注册CUSTOM_CONFIG
:
providers: [{ provide: **CONFIG, useValue:**
**CUSTOM_CONFIG**
}]
将其注入到任何构造函数中:
constructor(@Inject( **CONFIG**
) config: **Config**
) {
this.title = config.title + ':' + config.url;
}
多个值
借助multi
选项,可以随后向相同的绑定添加其他值:
bootstrap(AppComponent, [
provide('languages', {useValue: 'en', multi:true }),
provide('languages', {useValue: 'fr', multi:true })
);
在代码的某个地方,我们可以获得languages
的多个值:
constructor(@Inject('languages') languages) {
console.log(languages);
// Logs: "['en','fr']"
}
工厂提供者
在需要根据在引导程序发生后的任何时刻更改的信息动态创建依赖值的情况下,我们可以应用useFactory
定义。
让我们想象我们使用SecurityService
来授权用户。CategoryService
必须了解有关用户的信息。授权可以在用户会话期间动态更改,因为他或她可以随时多次登录和注销。直接将SecurityService
注入CategoryService
会导致将其注入到应用程序的所有服务中。
解决方案非常巧妙,使用原始的布尔authorization
属性来控制CategoryService
,而不是使用SecurityService
:
categories: Category[] = [...];
constructor(private **authorized:**
boolean) { }
getCategories() {
**return this.authorized ? this.categories : [];**
}
授权属性将动态更新,因此我们不能使用值提供者,而必须通过工厂函数接管创建新的CategoryService
实例:
let categoryServiceFactory = (securityService: SecurityService) => {
return new CategoryService(securityService.authorized);
}
在工厂提供者中,我们将SecurityService
与工厂函数一起注入:
export let categoryServiceProvider = {
provide: CategoryService,
useFactory: categoryServiceFactory,
deps: [ **SecurityService**
]
};
注入器的层次结构
Angular 1 在整个应用程序中只有一个注入器,并且它很好地管理了所有依赖项的创建和解析。每个注册的依赖项都变成了单例,因此在整个应用程序中只有一个实例可用。这种解决方案有一个副作用,即您需要在应用程序的不同部分注入相同依赖项的多个实例。因为 Angular 2 应用程序是一个组件树,该框架具有分层依赖注入系统——注入器树与应用程序的组件树并行存在。每个组件都有自己的注入器,或者与树中同一级别的其他组件共享。当树底部的组件请求依赖项时,Angular 会尝试在该组件的注入器中查找已注册的提供者。如果该级别上不存在提供者,注入器会将请求传递给其父注入器,依此类推,直到找到能够处理请求的注入器。如果祖先用尽,Angular 会抛出异常。这种解决方案帮助我们在不同级别和组件上创建相同依赖项的不同实例。特定服务实例仍然是单例,但仅在主机组件实例及其子级的范围内。
让我们开始:
-
打开终端,创建名为
ecommerce
的文件夹并进入其中 -
将项目的内容从文件夹
chapter_6/1.ecommerce-seed
复制到新项目中 -
运行以下脚本以安装
npm
模块:
**npm install**
- 使用以下命令启动 TypeScript 监视器和轻量级服务器:
**npm start**
此脚本将打开 Web 浏览器并导航到项目的欢迎页面。
类别服务
我在第五章中提到了路由,在实现 SPA 时,需要将数据与呈现逻辑解耦的必要性。我在类别和产品视图中部分实现了这一点。CategoryListComponent
和WelcomeComponent
使用了从getCategories
函数返回的类别。现在它还没有受到影响,但当我们开始从服务器获取和更新数据时,我们将需要更多的函数。最好将实现细节隐藏在单个可重用数据服务类中,以便在多个组件中使用它。
让我们将类别数据获取业务重构为一个提供类别的单一服务,并与所有需要它们的组件共享该服务。
将 category.ts
重命名为 category.service.ts
,以遵循服务名称以小写字母拼写,后跟 .service
的命名约定。如果服务名称由多个单词组成,我们将以小写的 dash-case
拼写基本文件名。在文件顶部添加一个导入语句:
import { **Injectable**
} from '@angular/core';
现在创建 CategoryService
类,并将 categories
变量、getCategories
和 getCategory
函数移到其中。
**@Injectable()**
export class **CategoryService**
{
**categories:**
Category[] = [
{ id: '1', title: 'Bread & Bakery', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'The best cupcakes, cookies, cakes, pies, cheesecakes, fresh bread, biscotti, muffins, bagels, fresh coffee and more.' },
{ id: '2', title: 'Takeaway', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'It's consistently excellent, dishes are superb and healthily cooked with high quality ingredients.' },
{ id: '3', title: 'Dairy', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'A dairy product is food produced from the milk of mammals, primarily cows, water buffaloes, goats, sheep, yaks, horses.' },
{ id: '4', title: 'Meat', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Only superior quality beef, lamb, and pork.' },
{ id: '5', title: 'Seafood', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'Great place to buy fresh seafood.' },
{ id: '6', title: 'Fruit & Veg', imageL: 'http://placehold.it/1110x480', imageS: 'http://placehold.it/270x171', desc: 'A variety of fresh fruits and vegetables.' }
];
**getCategories()**
{
return this.categories;
}
**getCategory**
(id: string): Category {
for (let i = 0; i < this.categories.length; i++) {
if (this.categories[i].id === id) {
return this.categories[i];
}
}
throw new CategoryNotFoundException(
`Category ${id} not found`);
}
}
不要忘记将对 categories
属性的所有引用添加 this
。
类别服务的注射器提供者
我们必须在注射器中注册服务提供者,告诉 Angular 如何创建服务。这样做的最佳位置是在 NgModule
的 providers
属性中。我们只需要一个类别的实例,所以当我们将 CategoryModule
导入 AppModule
时,Angular 将注册并创建来自 CategoryService
类的单例,可在整个应用程序中使用。打开 category.module.ts
文件,导入 CategoryService
并使用以下代码更改 @NgModule
装饰器:
import {CategoryService} from './category.service';
@NgModule({
imports: [CommonModule, FormsModule, RouterModule],
declarations: [CategoryListComponent, CategoryCardComponent, CategorySlideComponent],
exports: [CategoryListComponent, CategoryCardComponent, CategorySlideComponent],
**providers: [CategoryService]**
})
export class CategoryModule {}
转到您的网络浏览器并打开浏览器控制台。我们会收到一大堆问题,主要是关于文件名错误,category.ts
被重命名为 category.service.ts
。我们可以轻松解决这个问题。另一个问题是使用函数 getCategory
和 getCategories
。为了解决这个问题,我们需要导入 CategoryService
:
import {Category, **CategoryService**
} from './category.service';
并将其注入到所有必要位置的构造函数中:
export class CategoryListComponent {
categories: Category[];
constructor(private router: Router,
**private categoryService: CategoryService) {**
**this.categories = this.categoryService.getCategories();**
}
filterProducts(category: Category) {
this.router.navigate(['/products'],
{ queryParams: { category: category.id} });
}
}
暂时将所有变量的初始化移动到构造函数中,类似于前面示例中的 categories
。
产品服务
将 product.ts
重命名为 product.service.ts
。创建 ProductService
类,并将 products
变量、getProducts
和 getProduct
函数移到其中:
export class ProductService {
private **products:**
Product[] = [
// ...
];
**getProducts**
(category?: string, search?: string) {
if (category) {
return this.products.filter((product: Product, index: number, array: Product[]) => {
return product.categoryId === category;
});
} else if (search) {
let lowSearch = search.toLowerCase();
return this.products.filter((product: Product, index: number, array: Product[]) => {
return product.title.toLowerCase().indexOf(lowSearch) != -1;
});
} else {
return this.products;
}
}
**getProduct**
(id: string): Product {
for (let i = 0; i < this.products.length; i++) {
if (this.products[i].id === id) {
return this.products[i];
}
}
throw new ProductNotFoundException(`Product ${id} not found`);
}
}
修复所有类中的 import
,以引用旧方法。
产品服务的注射器提供者
我们对 ProductService
采取相同的步骤来注册服务提供者。因为我们只需要一个应用程序中的服务实例,所以我们可以在 ProductModule
中注册它。打开 product.module.ts
文件,导入 ProductService
并使用以下代码更改 @NgModule
装饰器:
import {ProductService} from './product.service';
@NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule, RouterModule, CategoryModule],
declarations: [ProductListComponent, ProductCardComponent, ProductSearchComponent, ProductGridComponent],
exports: [ProductListComponent, ProductCardComponent, ProductSearchComponent, ProductGridComponent],
**providers: [ProductService]**
})
export class ProductModule {}
现在重新启动应用程序,以再次查看所有产品和类别:
提示
您可以在 chapter_6/2.ecommerce-di
找到此源代码。
购物车
购物车是一种软件,充当在线商店的目录,并允许用户选择最终购买的商品。它被称为购物篮。购物车(或篮子)允许用户在浏览产品在线目录时收集商品。用户应单击“立即购买”按钮将所选商品添加到购物车中。购物车中的总金额和商品数量显示在导航栏组件中。用户可以转到结账或查看购物车以管理购买商品的数量。
购物车必须存储用户放入购物车中的商品。商品应该是:
-
可获取以显示购物车内容
-
可更新以更改购物车中商品的数量
-
可移除
考虑到这一点,让我们首先创建基本的购物车功能:添加、更新和删除商品,并定义一个简单的项目类,并浏览代码的使用。
让我们创建cart
文件夹和cart.service.ts
文件。我们将在该文件中实现模型定义,如Cart
和CartItem
,以及CartService
。
购物车模型和 CartItem
在开始时,Cart
类需要一个内部数组来存储购物车中的所有items
:
export class Cart {
count: number = 0;
amount: number = 0;
items: CartItem[] = [];
}
接下来,它必须“计算”所有项目的数量并保持“金额”。 CartItem
是一个定义购物车可以使用的数据结构的接口:
import {Product} from '../product/product.service';
export interface CartItem {
product: Product;
count: number;
amount: number;
}
购物车服务
CartService
保持cart
实例,以使其在整个应用程序中可用:
**cart:**
Cart = new Cart();
addProduct
方法应将商品添加到购物车中:
**addProduct**
(product: Product) {
// Find CartItem in items
let item: CartItem = this.findItem(product.id);
// Check was it found?
if (item) {
// Item was found.
// Increase the count of the same products
item.count++;
// Increase amount of the same products
item.amount += product.price;
} else {
// Item was not found.
// Create the cart item
item = {
product: product,
count: 1,
amount: product.price
};
// Add item to items
this.cart.items.push(item);
}
// Increase count in the cart
this.cart.count++;
// Increase amount in the cart
this.cart.amount += product.price;
}
该方法接受一个Product
类型的参数,并尝试找到包含相同产品的项目。该方法需要增加产品数量并增加找到的购物车项目的金额。否则,它将创建新的CartItem
实例并将产品分配给它。最后,它增加了购物车中的总商品数量和金额。
接下来,该类的removeProduct
方法可用于快速从购物车中删除产品:
**removeProduct**
(product: Product) {
// Find CartItem in items
let item: CartItem = this.findItem(product.id);
// Check is item found?
if (item) {
// Decrease the count
item.count--;
// Check was that the last product?
if (!item.count) {
// It was last product
// Delete item from items
this.remove(item);
}
// Decrease count in the cart
this.cart.count--;
// Decrease amount in the cart
this.cart.amount -= product.price;
}
}
该方法接受一个产品类型的参数,并尝试找到包含相同产品的项目。该方法需要减少与此购物车项目相关联的商品数量。它删除包含零个产品的购物车项目。最后,它减少了购物车中的总商品数量和金额。
removeItem
方法删除特定项目,并减少了购物车中的总商品数量和金额:
removeItem(item: CartItem) {
// Delete item from items
this.remove(item);
// Decrease count in the cart
this.cart.count -= item.count;
// Decrease amount in the cart
this.cart.amount -= item.amount;
}
以下私有方法findItem
帮助通过Product
id 找到CartItem
:
private **findItem**
(id: string): CartItem {
for (let i = 0; i < this.cart.items.length; i++) {
if (this.cart.items[i].product.id === id) {
return this.cart.items[i];
}
}
return null;
}
最后一个私有方法remove
,减少了购物车中商品的数量:
private **remove**
(item: CartItem) {
// Find the index of cart item
let indx: number = this.cart.items.indexOf(item);
// Check was item found
if (indx !== -1) {
// Remove element from array
this.cart.items.splice(indx, 1);
}
}
购物车菜单组件
我认为购物车设计中必须存在的关键方面是,用户一眼就能看到购物车中有多少商品。您需要让用户了解购物车中有多少商品,这样用户就可以在不使用下拉菜单的情况下知道他们添加了什么商品到购物车中。
注意
确保购物者可以轻松看到购物车中的商品,并且它们出现在页面上方,而不是在另一页上。
这是一个非常重要的 UX 设计模式。如果你将购物车内容保留在侧边栏或页面右上方附近,你就可以简化结账流程,让购物者更容易在网站上移动并随时跟踪商品和订单总额。
考虑到这一点,让我们创建cart-menu.component.ts
和cart-menu.component.html
。将以下代码复制并粘贴到cart-menu.component.ts
文件中:
import {Component, Input} from '@angular/core';
import {Cart, CartService} from './cart.service';
@Component({
selector: 'db-cart-menu',
templateUrl: 'app/cart/cart-menu.component.html'
})
export class CartMenuComponent {
private cart: Cart;
constructor(private cartService: CartService) {
this.cart = this.cartService.cart;
}
}
本地cart
变量的目的是在视图上表示内容,并在用户添加或移除产品到购物车后更新它。
我们在下拉菜单的标签中显示商品总数和金额:
<ul class="nav navbar-nav float-xs-right">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown"
href="#" role="button" aria-haspopup="true"
aria-expanded="false">
Cart: {{cart. **amount | currency:'USD':true:'1.2-2'**
}}
({{cart.count}} items)
</a>
<div class="dropdown-menu dropdown-menu-right"
aria-labelledby="cart">
<!-- ... -->
注意带有以下参数的货币管道:
-
第一个参数是 ISO 4217 货币代码,例如
USD
代表美元,EUR
代表欧元。 -
- 第二个位置是一个布尔值,指示是否在输出中使用货币符号(例如
$
)或货币代码(例如USD
)
- 第二个位置是一个布尔值,指示是否在输出中使用货币符号(例如
-
在最后一个位置,我们以以下格式添加数字信息:
minIntegerDigits.minFractionDigits-maxFractionDigits
我建议在这里和所有其他需要显示货币金额的地方使用这个管道。
我们在 Bootstrap 4 表格中显示购物车的内容:
<div class="table-responsive">
<table class="table table-sm table-striped table-bordered
table-cart">
<tbody>
<tr>
<td class="font-weight-bold">Title</td>
<td class="font-weight-bold">Price</td>
<td class="font-weight-bold">Count</td>
<td class="font-weight-bold">Amount</td>
</tr>
<tr *ngFor="let item of cart.items">
<td>{{item.product.title}}</td>
<td>{{item.product.price |
currency:'USD':true:'1.2-2'}}</td>
<td>{{item.count}}</td>
<td>{{item.amount |
currency:'USD':true:'1.2-2'}}</td>
</tr>
</tbody>
</table>
</div>
在菜单底部,我们显示总金额和两个按钮,用于跳转到购物车
和结账
:
<div class="row">
<div class="col-md-12">
<div class="total-cart float-xs-right">
<b>Total:
{{cart.amount | currency:'USD':true:'1.2-2'}}
</b>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<a [routerLink]="['/cart']"
class="btn btn-primary float-xs-right btn-cart">
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
**Cart**
</a>
<a [routerLink]="['/checkout']"
class="btn btn-success float-xs-right btn-cart">
<i class="fa fa-credit-card" aria-hidden="true"></i>
**Checkout**
</a>
</div>
</div>
购物车模块
让我们将CartManuComponent
和Cart Service
添加到CartModule
中,以便在整个应用程序中轻松访问它们:
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {CartMenuComponent} from './cart-menu.component';
import {CartService} from './cart.service';
@NgModule({
imports: [CommonModule, RouterModule],
declarations: [CartMenuComponent],
exports: [CartMenuComponent],
providers: [CartService]
})
export class CartModule {}
我们需要将CartModule
添加到AppModule
中:
//...
import { **CartModule**
} from './cart/cart.module';
//...
@NgModule({
imports: [
BrowserModule, FormsModule, ReactiveFormsModule,
routing, **CartModule**
, CategoryModule, ProductModule],
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
更新导航栏
打开navbar.component.html
并找到购物车占位符:
<ul class="nav navbar-nav float-xs-right">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown"
href="#" role="button" aria-haspopup="true"
aria-expanded="false">Cart</a>
<div class="dropdown-menu">
<span>The Cart Placeholder</span>
</div>
</li>
</ul>
将其更改为更加优雅的样式:
<db-cart-menu></db-cart-menu>
通过服务更新购物车
我们必须做的最后一件事是将CartService
注入到ProductGrid
组件中,并开始监听addToCart
事件。在同名方法中,我们调用CartService
的addProduct
将所选商品添加到购物车中:
**addToCart**
(product:Product) {
this.cartService. **addProduct**
(product);
}
现在,尝试点击不同产品上的立即购买,并查看导航栏中发生的变化。单击下拉菜单以显示购物车内容:
提示
您可以在chapter_6/3.ecommerce-cart
找到源代码。
总结
现在,您将熟悉 Angular 依赖注入的使用。正如我们所见,我们将 Angular 代码分成了可视组件和服务。它们彼此依赖,并且依赖注入提供了引用透明性。依赖注入允许我们告诉 Angular 我们的可视组件依赖于哪些服务,框架将为我们解决这些问题。
我们创建了产品和类别的类,以将功能隐藏到可重用的服务中。此外,我们创建了购物车组件和服务,并将最后一个与产品连接起来,以便用户可以将产品添加到购物车中。
在第七章中,处理表单,我们将讨论如何使用与表单创建相关的 Angular 2 指令,以及如何将基于代码的表单组件链接到 HTML 表单。此外,我们将继续构建我们在前几章中开始开发的项目。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
Booklist sharing
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
第七章:使用表单
本章将向读者展示如何使用与表单创建相关的 Angular 2 指令,以及如何使用基于代码的表单组件来创建 HTML 表单。本章将使用 Bootstrap 4 来增强表单的外观,并指示我们的 Web 应用程序的无效输入。
在本章结束时,您将对以下内容有扎实的理解:
-
Bootstrap 4 表单
-
Angular 2 表单指令
-
单向和双向数据绑定
-
如何向表单添加验证
-
连接我们应用程序的各个部分
让我们从以下步骤开始:
-
打开终端,创建一个名为
ecommercem
的文件夹并打开它。 -
将项目内容从文件夹
chapter_7/1.ecommerce-seed
复制到新项目中。 -
运行以下脚本以安装 NPM 模块:
**npm install**
- 使用以下命令启动 TypeScript 监视器和 lite 服务器:
**npm start**
这个脚本打开了网页浏览器并导航到项目的欢迎页面。
HTML 表单
HTML 表单是网页文档的一个部分,包含:
-
文本
-
图片
-
标记
-
特殊元素,如控件,如复选框、单选按钮等
-
描述控件目的的标签
用户通过输入文本或选择下拉菜单来修改控件,完成表单并将其提交到后端进行处理。每个控件都有一个name
属性,表单用它来收集特定的数据。这些名称很重要,因为:
-
在客户端,它告诉浏览器给每个数据片段起什么名字
-
在服务器端,它让服务器通过名称处理每个数据片段
表单通过action
和method
属性定义了向服务器发送数据的位置和方式。表单通常有一个提交按钮,允许用户向服务器发送数据。
Bootstrap 表单
Bootstrap 4 提供了默认样式的表单控件和布局选项,以创建一致的渲染效果,适用于各种浏览器和设备。
注意
为了正确渲染,所有输入都必须有一个type
属性。
表单控件
Bootstrap 支持特定的类来自定义以下表单控件:
-
form-group
类用于任何一组表单控件。您可以将其与fieldset
或div
等块级元素一起使用。 -
form-control
类用于文本输入、选择菜单和文本区域。 -
form-control-file
是唯一适用于文件输入的类。 -
有
form-check
和formcheck-inline
类,我们可以用它们来处理复选框和单选按钮。
表单布局
所有的表单默认都是垂直堆叠的,因为 Bootstrap 4 将display: block
和width: 100%
应用到所有的表单控件上。我们可以使用额外的类来改变这种布局。
标准表单
使用form-group
类快速创建表单:
<form>
<div class="form-group">
<label for="user_name">User Name</label>
<input type="text" class="form-control" id="user_name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password">
</div>
</form>
这个类为标签和控件周围添加了margin-bottom
,以获得最佳间距:
内联表单
如果需要将表单元素在单个水平行中左对齐布局,请使用form-inline
类。
注意
表单只在宽度大于 768px 的视口中内联对齐控件。
表单控件的行为不同,因为它们接收到的是width:auto
而不是width: 100%
。为了使它们垂直对齐,使用display: inline-block
。您可能需要手动调整各个控件的宽度和对齐方式:
<form class="form-inline">
<div class="form-group">
<label for="user_name">User Name</label>
<input type="text" class="form-control" id="user_name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password">
</div>
</form>
注意
每个表单控件都应该有label
元素。
我只是在表单元素中添加了form-inline
类来水平布局元素:
隐藏标签
您可以隐藏标准和内联表单的标签,以便使用占位符:
<form class="form-inline">
<div class="form-group">
<label class="sr-only" for="user_name">User Name</label>
<input type="text" class="form-control" id="user_name"
**placeholder="User Name">**
</div>
<div class="form-group">
<label class="sr-only" for="password">Password</label>
<input type="password" class="form-control" id="password"
**placeholder="Password">**
</div>
</form>
我们只需为每个标签添加sr-only
类:
为什么我们不能删除表单中的标签使它们不可见?这个问题的答案在于辅助技术的使用,比如对于有限能力的人来说,屏幕阅读器。如果我们不为每个输入添加标签,屏幕阅读器会错误地渲染表单。Bootstrap 的作者特意设计了sr-only
类,只为屏幕阅读器隐藏页面布局中的信息。
表单控件尺寸
表单控件有两种额外的尺寸,除了默认的尺寸,我们可以使用它们来增加或减小表单的尺寸:
-
使用
form-control-lg
来增加输入控件的尺寸 -
使用
form-control-sm
来减小输入控件的尺寸
帮助文本
有时我们需要显示与表单控件相关的帮助文本。Bootstrap 4 支持标准和内联表单的帮助文本。
您可以使用form-text
类来创建块级帮助。它包括display: block
并为易于与前面的输入控件间隔添加一些顶部边距:
<form>
<div class="form-group">
<label for="user_name">User Name</label>
<input type="text" class="form-control" id="user_name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password">
**<p id="passwordHelpBlock" class="form-text text-muted">**
**The password must be more than 8 characters long.**
**</p>**
</div>
</form>
使用text-muted
类与任何典型的内联元素(如span
或small
)一起创建内联表单的帮助文本:
<form class="form-inline">
<div class="form-group">
<label for="user_name">User Name</label>
<input type="text" class="form-control" id="user_name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password">
**<small id="passwordHelpInline" class="text-muted">**
**Must be 8-20 characters long.**
**</small>**
</div>
</form>
表单网格布局
我们可以使用 Bootstrap 4 网格为表单创建更结构化的布局。以下是一些指南:
-
将表单包装在具有
container
类的元素中 -
将
row
类添加到form-group
-
使用
col-*-*
类来指定标签和控件的宽度 -
将
col-form-label
类添加到所有标签上,以使它们垂直对齐相应的控件 -
将
col-form-legend
添加到传奇元素,以使它们看起来与常规标签类似
让我们使用网格更新我们的标记:
<div class="container">
<form>
<div class="form-group row">
<label for="user_name" class="col-sm-2 col-form-label">
User Name
</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="user_name">
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">
Password
</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="password">
</div>
</div>
<div class="form-group row">
<label class="col-sm-2">Connection</label>
<div class="col-sm-10">
<div class="form-check">
<label class="form-check-label">
<input class="form-check-input" type="checkbox">
Secure (SSL)
</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-10">
<button type="submit" class="btn btn-primary">Sign in
</button>
</div>
</div>
</form>
</div>
堆叠复选框和单选按钮
Bootstrap 4 通过form-check*
类改进了复选框和单选按钮的布局和行为。这两种类型只有一个类,可以帮助垂直堆叠和间距兄弟元素。标签和输入必须具有适当的form-check-label
和form-check-input
类,以实现这种魔术。
<div class="container">
<form>
<div class="form-group row">
<label for="user_name" class="col-sm-2 col-form-label">
User Name
</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="user_name">
</div>
</div>
<div class="form-group row">
<label for="password"
class="col-sm-2 col-form-label">Password</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="password">
</div>
</div>
<fieldset class="form-group row">
<legend class="col-form-legend col-sm-2">Language</legend>
<div class="col-sm-10">
<div class="form-check">
**<label class="form-check-label">
<input class="form-check-input" type="radio"
name="language" id="lngEnglish" value="english"
checked>
English
</label>**
</div>
<div class="form-check">
**<label class="form-check-label">
<input class="form-check-input" type="radio"
name="language" id="lngFrench" value="french">
French
</label>**
</div>
<div class="form-check disabled">
**<label class="form-check-label">
<input class="form-check-input" type="radio"
name="language" id="lngSpain" value="spain"
disabled>
Spain
</label>**
</div>
</div>
</fieldset>
</form>
</div>
内联复选框和单选按钮
在需要复选框或单选按钮水平排列的情况下,您可以:
-
将
form-check-inline
类添加到标签元素 -
将
form-check-input
添加到输入
<form class="form-inline">
<div class="form-group">
<label for="user_name">User Name</label>
<input type="text" class="form-control" id="user_name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password">
<small id="passwordHelpInline" class="text-muted">
Must be 8-20 characters long.
</small>
</div>
<div class="form-group">
**<label class="form-check-inline">**
**<input class="form-check-input" type="radio" name="language"**
**id="lngEnglish" value="english" checked>**
**English**
**</label>**
**<label class="form-check-inline">**
**<input class="form-check-input" type="radio" name="language"**
**id="lngFrench" value="french">**
**French**
**</label>**
**<label class="form-check-inline">**
**<input class="form-check-input" type="radio" name="language"**
**id="lngSpain" value="spain" disabled>**
**Spain**
**</label>**
</div>
</form>
静态控件
在需要显示纯文本而不是输入字段的情况下,可以使用带有form-control-static
类的段落元素:
<div class="container">
<form>
<div class="form-group row">
<label for="user_name" class="col-sm-2 col-form-label">
User Name
</label>
<div class="col-sm-10">
**<p class="form-control-static">Admin</p>**
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">
Password
</label>
<div class="col-sm-10">
<input type="password" class="form-control" id="password">
</div>
</div>
</form>
</div>
禁用状态
我们可以使用相同名称的属性禁用一个或多个控件上的输入:
<form>
<div class="form-group">
<label for="user_name">User Name</label>
**<input type="text" class="form-control" id="user_name"
value="Admin" disabled>**
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password">
<p id="passwordHelpBlock" class="form-text text-muted">
The password must be more than 8 characters long.
</p>
</div>
</form>
禁用的输入字段显示为较浅颜色,并带有not-allowed
光标:
注意
使用自定义 JavaScript 代码禁用锚点和字段集,因为 IE 11 及以下的浏览器不完全支持此属性。
只读输入
为了防止修改任何输入字段,可以使用只读属性:
<form>
<div class="form-group">
<label for="user_name">User Name</label>
**<input type="text" class="form-control" id="user_name"
value="Admin" readonly>**
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password">
<p id="passwordHelpBlock" class="form-text text-muted">
The password must be more than 8 characters long.
</p>
</div>
</form>
这些字段看起来较浅,带有标准光标:
验证样式
Bootstrap 支持三种验证状态,并为表单控件提供适当的样式:
-
has-success
类定义了成功状态 -
has-danger
类定义了危险状态 -
has-warning
类定义了警告状态
我们应该将这些类应用到父元素,这样所有的control-label
、form-control
或text-muted
元素都将继承验证样式。我们可以在文本输入中使用反馈图标,比如form-control-success
、form-control-warning
和form-control-danger
。为了给予额外的验证注意,我们可以使用form-control-feedback
样式的上下文验证文本。它会根据父has-*
类自适应颜色:
<form>
<div class="form-group **has-success**
">
<label class="control-label" for="username">Success
</label>
<input type="text" class="form-control
**form-control-success**
" id="username">
<div class=" **form-control-feedback**
">That username's is
ok.</div>
</div>
<div class="form-group **has-warning**
">
<label class="control-label" for="password">Warning
</label>
<input type="password" class="form-control
**form-control-warning**
" id="password">
<div class=" **form-control-feedback**
">The password is
weak</div>
</div>
<div class="form-group **has-danger**
">
<label class="control-label" for="card">Card</label>
<input type="card" class="form-control
**form-control-danger**
"
id="card">
<div class=" **form-control-feedback**
">We accept only VISA and
Master cards</div>
</div>
</form>
无表单搜索
查看product-search.component.html
文件中的标记:
<div class="card">
<div class="card-header">Quick Shop</div>
<div class="input-group">
**<input #search type="text" class="form-control"**
**placeholder="Search for...">**
**<span class="input-group-btn">**
**<button class="btn btn-secondary" type="button"**
**(click)="searchProduct(search.value)">Go!</button>**
**</span>**
</div>
</div>
我这里没有使用form
标签。为什么?答案相当棘手。表单标签主要用于以下情况:
-
您想执行非 AJAX 请求或向服务器发送文件
-
您需要以编程方式捕获
submit
或reset
事件 -
您想要向表单添加验证逻辑
对于其他情况,我们可以放弃它。搜索字段背后的逻辑是在不向服务器发出任何请求的情况下,使用适当的信息更新 URL。这就是为什么搜索是无表单的。
搜索表单存在一个问题;即使搜索字段为空,Go按钮也始终处于启用状态。这会导致不合适的搜索结果。我们需要添加验证来解决这个问题,这里有两个选项:
-
开始监听搜索字段的键事件,以管理Go按钮的
enabled
属性 -
添加验证,并让 Angular 管理Go按钮的
enabled
属性
让我们两者都做一下,看看有什么区别。
事件对象中的用户输入
用户与网页交互,修改控件,这会触发 DOM 事件。我们使用事件绑定来监听那些更新组件和模型的事件,并借助一些简单的语法:
<div class="card">
<div class="card-header">Quick Shop</div>
<div class="input-group">
<input #search type="text" class="form-control"
placeholder="Search for..."
**(keyup)="searchChanged($event)">**
<span class="input-group-btn">
<button class="btn btn-secondary" type="button"
**[disabled]="disabled"**
(click)="searchProduct(search.value)">Go!</button>
</span>
</div>
</div>
$event
的形状取决于哪个元素引发了事件。当用户在输入元素上输入内容时,它会触发键盘事件,并在ProductSearchComponent
的searchChanged
方法中监听:
import {Component} from '@angular/core';
import {Router} from '@angular/router';
@Component({
selector: 'db-product-search',
templateUrl: 'app/product/product-search.component.html'
})
export class ProductSearchComponent {
**disabled: boolean = true;**
constructor(private router: Router) {}
searchProduct(value: string) {
this.router.navigate(['/products'], { queryParams: {
search: value} });
}
**searchChanged(event: KeyboardEvent) {**
**// Get an input element**
**let element:HTMLInputElement =**
**<HTMLInputElement>event.target;**
**// Update the disabled property depends on value**
**if (element.value) {**
**this.disabled = false;**
**} else {**
**this.disabled = true;**
**}**
**}**
}
首先,我们从事件target
中找到输入元素,并更改与submit
按钮的相同名称属性绑定的组件的disabled
属性。默认情况下,禁用值等于 true,提交按钮被灰掉:
当用户输入要搜索的文本时,触发的事件会使按钮更新 URL:
来自模板引用变量的用户输入
我们可以使用#search
模板引用变量直接从输入元素中获取值,就像这样:
<div class="card">
<div class="card-header">Quick Shop</div>
<div class="input-group">
<input #search type="text" class="form-control"
placeholder="Search for..."
**(keyup)="searchChanged(search.value)">**
<span class="input-group-btn">
<button class="btn btn-secondary" type="button"
[disabled]="disabled"
(click)="searchProduct(search.value)">Go!</button>
</span>
</div>
</div>
searchChanged
方法的代码变得更小了:
searchChanged(value: string) {
// Update the disabled property depends on value
if (value) {
this.disabled = false;
} else {
this.disabled = true;
}
}
注意
选择使用模板引用变量将值传递给组件监听方法,而不是 DOM 事件。
您可以在chapter_7/2.ecommerce-key-event-listenning
找到源代码。
产品视图
在产品网格中显示的产品卡片组件有一个更多信息按钮。当用户点击按钮时,它会导航到产品视图,您可以进行以下操作:
-
显示产品信息
-
检查产品的可用性
-
通过点击加入购物车或从购物车中移除来更新产品的数量
-
点击继续购物返回产品列表
产品视图的线框图
让我们创建product-view.component.html
。这个视图的内容相当大,所以我会按列来解释它。
产品图片
在第一列中,我们展示了产品的图片。产品界面有大图的参考,所以在屏幕上呈现它很简单:
<div class="container">
<div class="row">
<div class="col-md-5">
**<img class="center-block product-img" src="{{product.imageL}}"**
**alt="{{product.title}}">**
</div>
<!-- ... -->
这是这一列的样子:
产品信息
第二列保存了关于产品的信息。我决定使用 Bootstrap 4 的卡片组件来在屏幕上呈现信息:
<div class="col-md-4">
<div class="card">
<div class="card-block">
<h4 class="card-title">{{product.title}}</h4>
<p class="card-text">{{product.desc}}</p>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">ID: {{product.id}}</li>
<li class="list-group-item">Category:
{{product.categoryId | categoryTitle}}</li>
</ul>
<div class="card-footer">
<p class="card-text">Availability: In Stock</p>
</div>
</div>
<div class="card" *ngIf="!product.isSpecial">
<div class="card-block">
<h4 class="card-title">Price:
{{product.price | currency:'USD':true:'1.2-2'}}</h4>
</div>
</div>
<div class="card card-inverse card-danger"
*ngIf="product.isSpecial">
<div class="card-block">
<h4 class="card-title">Price:
{{product.price | currency:'USD':true:'1.2-2'}}</h4>
</div>
</div>
</div>
这里有三张卡片。第一张包含产品的一般信息,比如title
和description
。接下来的列表保存了产品的id
和category
。我们使用categoryTitle
管道来打印出类别标题。最后,我们用虚假数据打印出可用性信息。我们将在下一章更新这个块,所以现在就保持原样。
第二和第三张卡片相互对立,并根据产品的isSpecial
属性的值呈现信息。当这个属性为true
时,我们会以交替的颜色显示价格:
CategoryTitle 管道
正如在第四章中提到的,创建模板,Angular 框架为我们提供了管道:一种在模板中声明的显示值转换的方法。管道是一个简单的函数,接受一个输入值并返回一个转换后的值。在我们的情况下,我们在购物车项目中保留了类别 ID,但我们需要显示类别的标题。出于这个原因,我们创建了文件category.pipe.ts
,内容如下:
import {Pipe, PipeTransform} from '@angular/core';
import {Category, CategoryService} from './category.service';
/*
* Return category title of the value
* Usage:
* value | categoryTitle
* Example:
* {{ categoryId | categoryTitle }}
* presume categoryId='1'
* result formats to 'Bread & Bakery'
*/
@Pipe({ name: 'categoryTitle' })
export class CategoryTitlePipe implements PipeTransform {
constructor(private categoryService: CategoryService) { }
transform(value: string): string {
let category: Category = this.categoryService.getCategory(value);
return category ? category.title : '';
}
}
此外,我们更新了CategoryModule
来声明和导出CategoryTitlePipe
:
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {CategoryListComponent} from './category-list.component';
**import {CategoryTitlePipe} from './category.pipe';**
import {CategoryCardComponent} from './category-card.component';
import {CategorySlideComponent} from './category-slide.component';
import {CategoryService} from './category.service';
@NgModule({
imports: [CommonModule, RouterModule],
declarations: [CategoryListComponent, CategoryTitlePipe,
CategoryCardComponent, CategorySlideComponent],
exports: [CategoryListComponent, CategoryTitlePipe,
CategoryCardComponent, CategorySlideComponent],
providers: [CategoryService]
})
export class CategoryModule {}
现在,CategoryTitlePipe
在整个应用程序中都可用。
产品视图中的购物车信息
我在购物车组件的最后一列中使用了 Bootstrap 4 表单布局,以保留和管理来自购物车的信息,如下所述。
数量和金额
产品的数量和金额对于购物的用户至关重要。为了在视图上呈现它们,我将相同名称的组件属性绑定到模板中:
<div class="form-group row">
<label for="first_name" class="col-xs-3 form-control-label">Quantity</label>
<div class="col-xs-9">
<h4 class="form-control-static">{{quantity}}</h4>
</div>
</div>
<div class="form-group row">
<label for="last_name" class="col-xs-3 form-control-label">Amount</label>
<div class="col-xs-9">
<h4 class="form-control-static">{{amount | currency:'USD':true:'1.2-2'}}</h4>
</div>
</div>
操作
用户使用加入购物车和从购物车中移除按钮来增加和减少购物车中产品的数量。这些按钮调用CartService
的适当方法来对购物车进行必要的更改:
<div class="form-group row">
<div class="col-xs-12">
**<a class="btn btn-primary btn-block"**
**(click)="addToCart()">Add to Cart</a>**
**<a class="btn btn-warning btn-block"**
**(click)="removeFromCart()">Remove from Cart</a>**
</div>
</div>
<div class="form-group row">
<div class="col-xs-12">
**<a class="btn btn-secondary btn-block"**
**[routerLink]="['/products']">Continue Shopping</a>**
</div>
</div>
</form>
最后,我们有一个继续购物按钮,帮助用户返回到产品视图。
用户每次向购物车中添加或移除产品时,更改都会发生在产品视图中,该视图会更新导航栏中的购物车菜单中的信息。
产品视图组件
现在让我们创建product-view.component.ts
,其中包含以下代码:
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Product, ProductService } from './product.service';
import { Cart, CartItem, CartService } from
'../cart/cart.service';
@Component({
selector: 'db-product-view',
templateUrl: 'app/product/product-view.component.html'
})
export class ProductViewComponent {
product: Product;
cartItem: CartItem;
get quantity(): number {
return this.cartItem ? this.cartItem.count : 0;
}
get amount(): number {
return this.cartItem ? this.cartItem.amount : 0;
}
constructor(private route: ActivatedRoute,
private productService: ProductService,
private cartService: CartService) {
this.route
.params
.subscribe(params => {
// Get the product id
let id: string = params['id'];
// Return the product from ProductService
this.product = this.productService.getProduct(id);
// Return the cart item
this.cartItem = this.cartService.findItem(id);
});
}
addToCart() {
this.cartItem = this.cartService.addProduct(this.product);
}
removeFromCart() {
this.cartItem = this.cartService.removeProduct(this.product);
}
}
在ProductViewComponent
中有两个属性,product
和cartItem
,用于获取模板中的信息。我们使用product
属性在产品视图的第二列中显示信息。cartItem
属性保留了与产品相关联的购物车中的项目的引用:
export interface CartItem {
product: Product;
count: number;
amount: number;
}
我们只需要在产品视图的第三列中显示count
和amount
,但这是不可能的,需要额外的工作:
第一个问题是,我们无法在将产品添加到购物车之前显示CartItem
的信息。为了解决这个问题,我们引入了count
和amount
属性的 getter 方法:
get quantity(): number {
return this.cartItem ? this.cartItem.count : 0;
}
get amount(): number {
return this.cartItem ? this.cartItem.amount : 0;
}
当用户第一次将产品添加到购物车或从中删除最后一个时,另一个问题就会发生。作为解决方案,我们需要在调用addToCart
和removeFromCart
方法时重新分配来自购物车的cartItem
:
addToCart() {
this.cartItem = this.cartService.addProduct(this.product);
}
removeFromCart() {
this.cartItem = this.cartService.removeProduct(this.product);
}
我们使用ActivatedRoute
服务在构造函数中检索路由的参数。由于我们的参数是作为Observable
提供的,我们通过名称订阅它们以获取id
参数,并告诉productService
和cartService
获取适当的信息。我们将保留对此Subscription
的引用,以便稍后整理事情。
将 ProductView 添加到 ProductModule
打开product.module.ts
文件以在那里引用ProductView
:
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {ProductListComponent} from './product-list.component';
**import {ProductViewComponent} from './product-view.component';**
import {ProductCardComponent} from './product-card.component';
import {ProductSearchComponent} from './product-search.component';
import {ProductGridComponent} from './product-grid.component';
import {ProductService} from './product.service';
import {CategoryModule} from './category/category.module';
@NgModule({
imports: [CommonModule, RouterModule, CategoryModule],
declarations: [ProductListComponent, **ProductViewComponent**
,
ProductCardComponent, ProductSearchComponent,
ProductGridComponent],
exports: [ProductListComponent, **ProductViewComponent**
,
ProductCardComponent, ProductSearchComponent,
ProductGridComponent],
providers: [ProductService]
})
export class ProductModule {}
ProductView
现在在整个应用程序中可用。
带参数的产品视图路由定义
我们必须在app.routes.ts
中更新路由配置,这样,当用户选择产品时,Angular 就会导航到ProductViewComponent
:
/*
* Angular Imports
*/
import {Routes, RouterModule} from '@angular/router';
/*
* Components
*/
import {WelcomeComponent} from './welcome/welcome.component';
import {ProductListComponent} from
'./product/product-list.component';
**import {ProductViewComponent} from**
**'./product/product-view.component';**
/*
* Routes
*/
const routes: Routes = [
{ path: '', redirectTo: 'welcome', pathMatch: 'full' },
{ path: 'welcome', component: WelcomeComponent },
{ path: 'products', component: ProductListComponent },
**{ path: 'products/:id', component: ProductViewComponent },**
];
/*
* Routes Provider
*/
export const routing = RouterModule.forRoot(routes);
第三个路由中的id
是路由参数的标记。在 URL 中,比如/product/123
,123
就是id
参数的值。相应的ProductViewComponent
使用该值来查找并呈现其id
等于123
的产品。
导航到产品视图
当用户在产品视图中的卡片上点击更多信息按钮时,路由器使用作为数组提供的信息来构建导航到产品视图的导航 URL:
<div class="card-deck-wrapper">
<div class="card-deck">
<div class="card" *ngFor="let product of products">
<div class="card-header text-xs-center">
{{product.title}}
</div>
<img class="card-img-top center-block product-item"
src="{{product.imageS}}" alt="{{product.title}}">
<div class="card-block text-xs-center"
[ngClass]="setClasses(product)">
<h4 class="card-text">Price:
${{product.price}}</h4>
</div>
<div class="card-footer text-xs-center">
<button class="btn btn-primary"
(click)="buy(product)">Buy Now</button>
**<a class="btn btn-secondary"**
**[routerLink]="['/products', product.id]">**
**More Info**
**</a>**
</div>
<div class="card-block">
<p class="card-text">{{product.desc}}</p>
</div>
</div>
</div>
</div>
以下是产品视图的三列的外观:
提示
您可以在chapter_7/3.ecommerce-product-view
找到源代码。
Angular 2 表单
我们之前没有在项目中使用过 Angular 2 表单,所以现在是时候揭开那些主要灵活的工具了。根据 Web 应用程序从用户请求的信息的性质,我们可以将其分为静态和动态表单:
-
我们使用模板驱动的方法来构建静态表单
-
我们使用模型驱动的方法来构建动态表单
表单设置
在使用新的 Angular 2 表单模块之前,我们需要安装它。打开终端窗口,导航到 Web 项目,并使用以下命令运行 npm 包管理器:
**$ npm install @angular/forms --save**
现在,当表单模块安装完成后,我们在应用程序引导期间启用它。打开app.module.ts
文件,并使用以下代码进行更新:
/*
* Angular Imports
*/
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
**import { FormsModule, ReactiveFormsModule } from '@angular/forms';**
import { RouterModule } from '@angular/router';
/**
* Modules
*/
import { CartModule } from './cart/cart.module';
import { CategoryModule } from './category/category.module';
import { ProductModule } from './product/product.module';
/*
* Components
*/
import { AppComponent } from './app.component';
import { NavbarComponent } from './navbar/navbar.component';
import { FooterComponent } from './footer/footer.component';
import { WelcomeComponent } from './welcome/welcome.component';
/*
* Routing
*/
import { routing } from './app.routes';
@NgModule({
imports: [
BrowserModule, **FormsModule, ReactiveFormsModule,**
routing, CartModule, CategoryModule, ProductModule],
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
我们在AppModule
中注册了两个不同的模块,因为:
-
FormsModule
用于模板驱动表单 -
ReactiveFormsModule
用于响应式或动态表单
我们很快就会发现它们两个。
模板驱动表单
这种方式是构建表单最简单的方式,几乎不需要应用程序代码。我们在模板中使用内置的 Angular 2 指令来声明性地创建表单,这些指令在幕后为我们执行所有的魔术。让我们谈谈在表单中可以使用的 Angular 2 特定指令。
NgForm 指令
NgForm
指令创建一个顶级FormGroup
实例,提供有关表单当前状态的信息,例如:
-
JSON 格式的表单值
-
表单有效性状态
查看 Angular 2 源代码中的form_group_directive.ts
中FormGroupDirective
类的指令定义:
@Directive({
selector: '[formGroup]',
providers: [formDirectiveProvider],
host: {'(submit)': 'onSubmit()', '(reset)': 'onReset()'},
**exportAs: 'ngForm'**
})
export class FormGroupDirective extends ControlContainer implements Form, OnChanges { //
指令元数据的exportAs
属性通过名称ngForm
向模板公开FormGroupDirective
的实例,因此在任何模板中,我们都可以使用模板变量引用它:
<form #myForm="ngForm">
...
</form>
模板变量myForm
可以访问表单值,因此我们可以使用 handle 函数来管理提交的值,就像这样:
<form #myForm="ngForm" (ngSubmit)="handle(myForm.value)">
...
</form>
ngSubmit
是一个事件信号,用户触发表单提交时会触发该事件。
NgModel 指令
NgModel
指令有助于在NgForm
实例上注册表单控件。我们必须为每个表单控件指定name
属性。通过ngModel
和name
属性的组合,表单控件将自动出现在表单的value
中:
<form #myForm="ngForm" (ngSubmit)="handle(myForm.value)">
<label>User Name:</label>
<input type="text" name="name" **ngModel**
>
<label>Password:</label>
<input type="password" name="password" **ngModel**
>
<button type="submit">Submit</button>
</form>
让我们在handle
函数中打印表单的value
:
handle(value) {
console.log(value);
}
结果以 JSON 格式打印出来:
{
name: 'User',
password: 'myPassword'
}
我们可以使用ngModel
作为属性指令,通过表达式将现有模型绑定到表单控件。我们可以用两种方式来处理这个问题。
单向绑定通过属性绑定将现有值应用于表单控件:
<form #myForm="ngForm" (ngSubmit)="handle(myForm.value)">
<label>User Name:</label>
<input type="text" name="name" [ngModel]="name">
<label>Password:</label>
<input type="password" name="password" [ngModel]="password">
<label>Phone:</label>
<input type="text" name="phone" [ngModel]="phone">
<label>Email:</label>
<input type="email" name="email" [ngModel]="email">
<button type="submit">Submit</button>
</form>
在MyForm
类中,我们有同名的属性:
@Component({...})
export class MyForm {
**name: string = 'Admin';**
**password: string;**
**phone: string;**
**email: string = '[email protected]';**
handle(value) {
console.log(value);
}
}
双向绑定将表单控件上的更改反映到属性的现有值,反之亦然:
<form #myForm="ngForm" (ngSubmit)="handle(myForm.value)">
<label>User Name:</label>
<input type="text" name="name" [(ngModel)]="name">
<p>Hi {{name}}</p>
<button type="submit">Submit</button>
</form>
使用 NgModel 跟踪更改状态和有效性
每当我们手动或以编程方式操作表单控件时,NgModel
都会跟踪发生在它们上面的状态更改。根据这些信息,NgModel
会更新具有特定类的控件。我们可以使用这些类来组织视觉反馈,以反映组件的状态:
-
ng-untouched
类标记尚未访问的控件 -
ng-touched
类标记访问过的控件 -
类
ng-pristine
标记具有未更改值的控件 -
类
ng-dirty
标记具有更改值的控件 -
类
ng-invalid
标记无效控件 -
类
ng-valid
标记有效控件
因此,我们应该能够使用ng-valid
或ng-invalid
类来向用户提供有关无效表单控件的反馈。让我们打开ecommerce.css
文件并添加以下样式:
.ng-valid[required], .ng-valid.required {
border-left: 2px solid green;
}
.ng-invalid:not(form) {
border-left: 2px solid red;
}
现在,所有标记为必填字段的控件将显示绿色的左边框,而所有无效字段将具有红色的左边框。
NgModelGroup 指令
我们可以将表单控件分组到控件组中。表单本身就是一个控件组。可以跟踪组中控件的有效状态。就像控件使用ngModel
指令一样,组使用NgModelGroup
指令:
<form #myForm="ngForm" (ngSubmit)="handle(myForm.value)">
<fieldset **ngModelGroup="user"**
>
<label>User Name:</label>
<input type="text" name="name" ngModel>
<label>Password:</label>
<input type="password" name="password" ngModel>
</fieldset>
<fieldset **ngModelGroup="contact"**
>
<label>Phone:</label>
<input type="text" name="phone" ngModel>
<label>Email:</label>
<input type="email" name="email" ngModel>
</fieldset>
<button type="submit">Submit</button>
</form>
我们可以使用fieldset
或div
元素来分组控件。借助ngModelGroup
,我们可以将控件语义上分组为user
和contact
信息:
{
user: {
name: 'User',
password: 'myPassword'
},
contact: {
phone: '000-111-22-33',
email: '[email protected]'
}
}
基于模型的表单
这种方法有助于构建无需 DOM 要求的表单,并使其易于测试。这并不意味着我们不需要模板。我们需要它们与基于模型的方式结合使用。我们在模板中创建表单并创建代表 DOM 结构的表单模型。我们可以在这里使用两种不同的 API:
-
基于
FormGroup
和FormControl
类的低级 API -
基于
FormBuilder
类的高级 API
任何表单都是一个FormGroup
。任何FormGroup
代表一组FormControls
。让我们想象一下我们有以下模板:
<form>
<label>User Name:</label>
<input type="text" name="name">
<label>Password:</label>
<input type="password" name="password">
<label>Phone:</label>
<input type="text" name="phone">
<label>Email:</label>
<input type="email" name="email">
<button type="submit">Submit</button>
</form>
现在为我们的表单创建一个模型:
import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
@Component({...})
export class MyForm {
myForm:FormGroup = new FormGroup({
name: new FormControl('Admin'),
password: new FormControl(),
contact: new FormGroup({
phone: new FormControl(),
email: new FormControl()
})
});
}
myForm
代表我们在模板中的表单。我们为表单的每个字段创建FormControl
,并为每个组创建FormGroup
。在第一个属性中,我们为名称分配默认值。FormGroup
可以包含另一个组,并有助于创建层次结构以复制 DOM 结构。
FormGroup 指令
现在,我们需要使用 Angular 2 的FormGroup
指令将模型绑定到表单元素。我们需要将表达式评估分配到FormGroup
实例中:
<form [formGroup]="myForm">
...
</form>
FormControlName 指令
下一个非常重要的步骤是将模型属性与表单元素关联起来。我们使用FormControlName
而不是 name 属性来注册控件:
import {Component} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
@Component({
selector: 'logon-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div *ngIf="userName.invalid">Name is too short. </div>
<input formControlName="userName" placeholder="User name">
<input formControlName="password" placeholder="Password">
<input formControlName="phone" placeholder="Phone">
<input formControlName="email" placeholder="Email">
<button type="submit">Submit</button>
</form>`
})
export class LogonFormGroup {
form = new FormGroup({
userName: new FormControl('', Validators.minLength(2)),
password: new FormControl('', Validators.minLength(5)),
phone: new FormControl(''),
email: new FormControl('')
});
get userName(): any { return this.form.get('userName'); }
get password(): any { return this.form.get('password'); }
constructor() {
this.form.setValue({userName: 'admin', password: '12345', phone: '123-123', email: '[email protected]'});
}
onSubmit(): void {
console.log(this.form.value);
// Will print {userName: 'admin', password: '12345',
// phone: '123-123', email: '[email protected]'}
}
}
该指令将FormControl
和FormGroup
中的userName
和password
与同名的 DOM 元素保持同步。任何变化都是以编程方式发生的,FormGroup
属性将立即写入 DOM 元素,反之亦然。我们使用get
和set
方法来访问和更新表单属性。
FormGroupName 指令
在有一组控件的情况下,我们可以使用FormGroupName
指令将一组控件与父FormGroupDirective
(正式为FormGroup
选择器)关联起来。您应该通过名称属性指定要链接到哪个嵌套的FormGroup
元素,因此单独组织子组元素的验证可能非常方便:
import {Component} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
@Component({
selector: 'logon-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<p *ngIf="userName.invalid">Name is invalid.</p>
<input formControlName="userName" placeholder="User name">
<input formControlName="password" placeholder="Password">
<fieldset **formGroupName**
="contact">
<input formControlName="phone">
<input formControlName="email">
</fieldset>
<button type="submit">Submit</button>
</form>`
})
export class LogonFormComponent {
form = new FormGroup({
userName: new FormControl('', Validators.minLength(2)),
password: new FormControl('', Validators.minLength(5)),
contact: new FormGroup({
phone: new FormControl(''),
email: new FormControl('')
})
});
get userName(): any { return this.form.get(userName'); }
get password(): any { return this.form.get('password'); }
get phone(): any { return this.form.get('contact.phone'); }
get email(): any { return this.form.get('contact.email'); }
constructor() {
this.form.setValue({userName: 'admin', password: '12345',
phone: '123-123', email: '[email protected]'});
}
onSubmit() {
console.log(this.form.value);
// Will print: {userName: 'admin', password: '12345',
// phone: '123-123', email: '[email protected]'}
console.log(this.form.status);
// Will print: VALID
}
}
我们使用FormGroup
的get
方法来访问属性。个别控件可以通过点语法来访问,如前面的代码所示。
FormBuilder 类
FormBuilder
从用户指定的配置创建一个AbstractControl
表单对象。因此,我们不需要创建FormGroup
,FormControl
和FormArray
元素,我们构建配置来构造模型。我们只需要在构造函数中注入它,并调用group
方法来创建表单组:
import {Component} from '@angular/core';
import {FormBuilder, FormGroup} from '@angular/forms';
@Component({...})
export class MyForm {
myForm:FormGroup;
constructor(private **formBuilder: FormBuilder**
) {}
ngOnInit() {
this.myForm = **this.formBuilder.group**
({
name: [],
password: ,
contect: this.formBuilder.group({
phone: [],
email: []
})
});
}
}
import {Component, Inject} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
@Component({
selector: 'logon-form',
template: `
<form [formGroup]="form">
<div formGroupName="name">
<input formControlName="first" placeholder="First">
<input formControlName="last" placeholder="Last">
</div>
<input formControlName="email" placeholder="Email">
<button>Submit</button>
</form>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<p *ngIf="userName.invalid">Name is invalid.</p>
<input formControlName="userName" placeholder="User name">
<input formControlName="password" placeholder="Password">
<fieldset **formGroupName**
="contact">
<input formControlName="phone">
<input formControlName="email">
</fieldset>
<button type="submit">Submit</button>
</form>
<p>Value: {{ form.value | json }}</p>
<p>Validation status: {{ form.status }}</p>
`
})
export class LogonFormComponent {
form: FormGroup;
constructor(@Inject(FormBuilder) fb: FormBuilder) {
this.form = fb.group({
userName: ['', Validators.minLength(2)],
password: ['', Validators.minLength(5)],
contact: fb.group({
phone: [''],
email: ['']
})
});
}
get userName(): any { return this.form.get(userName'); }
get password(): any { return this.form.get('password'); }
get phone(): any { return this.form.get('contact.phone'); }
get email(): any { return this.form.get('contact.email'); }
constructor() {
this.form.setValue({userName: 'admin', password: '12345',
phone: '123-123', email: '[email protected]'});
}
onSubmit() {
console.log(this.form.value);
// Will print: {userName: 'admin', password: '12345',
// phone: '123-123', email: '[email protected]'}
console.log(this.form.status);
// Will print: VALID
}
}
因此,我们有了更简洁的代码。
FormControl 指令
在本章的开头,我们谈到了无形搜索表单。这个表单只有一个元素,我们根本不需要FormGroup
。Angular 有一个FormControl
指令,它不必在FormGroup
内。它只将其添加到单个表单控件中:
<div class="card">
<div class="card-header">Quick Shop</div>
<div class="input-group">
<input #search type="text" class="form-control"
placeholder="Search for..."
**[formControl]="seachControl">**
<span class="input-group-btn">
<button class="btn btn-secondary" type="button"
[disabled]="disabled"
(click)="searchProduct(search.value)">Go!</button>
</span>
</div>
</div>
脚本的更新版本如下:
import {Component} from '@angular/core';
import {Router} from '@angular/router';
**import {FormControl} from '@angular/forms';**
@Component({
selector: 'db-product-search',
templateUrl: 'app/product/product-search.component.html'
})
export class ProductSearchComponent {
disabled: boolean = true;
**seachControl: FormControl;**
constructor(private router: Router) {}
**ngOnInit() {**
**this.seachControl = new FormControl();**
**this.seachControl.valueChanges.subscribe((value: string) => {**
**this.searchChanged(value);**
**});**
**}**
searchProduct(value: string) {
this.router.navigate(['/products'], { queryParams:
{ search: value} });
}
searchChanged(value: string) {
// Update the disabled property depends on value
if (value) {
this.disabled = false;
} else {
this.disabled = true;
}
}
}
内置验证器
我无法想象没有验证器的表单。Angular 2 带有几个内置验证器,我们可以使用声明式指令或使用FormControl
,FormGroup
或FormBuilder
类进行命令式使用。以下是它们的列表:
-
带有
required
验证器的表单控件必须具有非空值 -
带有
minLength
的表单控件必须具有最小长度的值 -
带有
maxLength
的表单控件必须具有最大长度的值 -
带有
pattern
的表单控件必须具有与给定正则表达式匹配的值
以下是如何在声明式中使用它们的示例:
<form novalidate>
<input type="text" name="name" ngModel **required**
>
<input type="password" name="password" ngModel **minlength="6"**
>
<input type="text" name="city" ngModel **maxlength="10"**
>
<input type="text" name="phone" ngModel
**pattern="^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$">**
</form>
请记住,novalidate
不是 Angular 2 的一部分。这是一个 HTML5 布尔表单属性。当表单提交时,它不会验证输入字段。
我们可以使用相同的验证器来命令性地使用FormGroup
和FormControl
:
@Component({...})
export class MyForm {
myForm: FormGroup;
ngOnInit() {
this.myForm = new FormGroup({
name: new FormControl('', Validators.required)),
password: new FormControl('', Validators.minLength(6)),
city: new FormControl('', Validators.maxLength(10)),
phone: new FormControl('', Validators.pattern(
'[^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$'))
});
}
}
如前所述,我们可以使用FormBuilder
和更少冗长的代码:
@Component({...})
export class MyForm {
myForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this. myForm = this.fb.group({
name: ['', Validators.required],
password: ['', Validators.minLength(6)],
city: ['', Validators.maxLength(10)],
phone: ['', Validators.pattern(
'[^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$')]
});
}
}
在这两种情况下,我们必须使用formGroup
指令将myForm
模型与 DOM 中的表单元素关联起来:
<form novalidate [ **formGroup**
]=" **myForm**
">
...
</form>
创建自定义验证器
Angular 2 有一个接口Validator
,可以由可以充当验证器的类来实现:
export interface Validator {
validate(c: AbstractControl): {
return [key: string]: any
};
}
让我们创建一个函数来验证邮政编码的正确性。在shared
文件夹中创建文件zip.validator.ts
,并使用以下代码:
import {FormControl} from '@angular/forms';
export function validateZip(c: FormControl) {
let ZIP_REGEXP:RegExp = new RegExp('[A-Za-z]{5}');
return ZIP_REGEXP.test(c.value) ? null : {
validateZip: {
valid: false
}
};
}
validateZip
函数期望FormControl
作为参数,并且如果值不匹配正则表达式,则必须返回一个错误对象,如果值有效,则返回 null。现在,我们可以导入validateZip
函数并在我们的类中使用它:
import {Component} from '@angular/core';
import { **validateZip**
} from '../shared/zip.validator';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
@Component({...})
export class MyForm {
form: FormGroup;
constructor(private **fb: FormBuilder**
) {}
ngOnInit() {
this.form = this.fb.group({
name: ['', Validators.required],
password: ['', Validators.minLength(6)],
city: ['', Validators.maxLength(10)],
zip: ['', **validateZip**
]
});
}
}
创建自定义验证器指令
我们可以使用 Angular 2 内置的验证器命令式地或声明式地,借助一些内部代码来执行表单控件上的验证器。所有内置和自定义验证器都必须在多提供者依赖令牌NG_VALIDATORS
中注册。正如您在第六章中所记得的,依赖注入,提供者的多属性允许将多个值注入到相同的令牌中。Angular 注入NG_VALIDATORS
,实例化表单,并对表单控件进行验证。让我们创建自定义验证指令,我们可以在模板驱动的表单中使用。打开zip.valdator.ts
并复制粘贴以下代码:
import {FormControl} from '@angular/forms';
import {Directive,forwardRef} from '@angular/core';
import {NG_VALIDATORS} from '@angular/forms';
export function validateZip(c: FormControl) {
let ZIP_REGEXP:RegExp = new RegExp('[A-Za-z]{5}');
return ZIP_REGEXP.test(c.value) ? null : {
validateZip: {
valid: false
}
};
}
@Directive({
selector: '[validateZip][ngModel],[validateZip][formControl]',
providers: [
{provide: NG_VALIDATORS, useExisting: forwardRef(() =>
ZipValidator), multi: true}
]
})
export class ZipValidator {
validator: Function = validateZip;
validate(c: FormControl) {
return this.validator(c);
}
}
现在在表单中,我们可以使用ZipValidator
作为指令:
<form novalidate>
<input type="text" name="name" ngModel **required**
>
<input type="password" name="password" ngModel **minlength="6"**
>
<input type="text" name="city" ngModel **maxlength="10"**
>
<input type="text" name="zip" ngModel **validateZip**
>
</form>
购物车视图
购物车视图列出了用户购物车中持有的所有商品。它显示了每件商品的产品详细信息,并且用户可以从这个页面:
-
通过单击清空购物车从购物车中删除所有商品
-
更新任何列出的商品的数量
-
通过单击继续购物返回产品列表
-
通过单击结账进行结账
购物车视图的线框
购物车视图的重要部分是由网格中动态内容组成的。看一下线框的第一列。有一排类似的数据,我们可以用来显示、修改和验证。为此,我们可以使用 Angular 静态表单来在视图上呈现购物车的内容。
让我们创建cart-view.component.html
。在第一列,我们需要打印出添加到购物车的产品信息:
<div *ngIf="cart.count">
<form #form="ngForm">
<div class="table-responsive">
<table class="table table-sm table-striped
table-bordered table-cart">
<tbody>
<tr>
<td class="font-weight-bold">Title</td>
<td class="font-weight-bold">Price</td>
<td class="font-weight-bold">Count</td>
<td class="font-weight-bold">Amount</td>
</tr>
<tr *ngFor="let item of cart.items">
<td>{{item.product.title}}</td>
<td>{{item.product.price |
currency:'USD':true:'1.2-2'}}</td>
<td>
<input type="number"
name="{{item.product.id}}" min="1"
[ngModel]="item.count"
(ngModelChange)="item.count = update($event, item)">
</td>
<td>{{item.amount |
currency:'USD':true:'1.2-2'}}</td>
</tr>
</tbody>
</table>
</div>
</form>
</div>
<div class="emty-cart" *ngIf="!cart.count">The cart is empty!</div>
我们在这里使用模板驱动方法,并将表单变量分配给公开的ngForm
。我将双向绑定格式拆分为两个语句:
-
[ngModel]="item.count"
:这用作属性绑定。 -
(ngModelChange)="item.count = update($event, item)"
:这用作事件绑定。
每当用户更新count
值时,此代码调用update
方法来添加或从购物车中删除产品:
import {Component, Input} from '@angular/core';
import {Cart, CartItem, CartService} from './cart.service';
@Component({
selector: 'db-cart-view',
templateUrl: 'app/cart/cart-view.component.html'
})
export class CartViewComponent {
private cart: Cart;
constructor(private cartService: CartService) {
this.cart = this.cartService.cart;
}
clearCart() {
this.cartService.clearCart();
}
**update(value, item: CartItem) {**
**let res = value - item.count;**
**if (res > 0) {**
**for (let i = 0; i < res; i++) {**
**this.cartService.addProduct(item.product);**
**}**
**} else if (res < 0) {**
**for (let i = 0; i < -res; i++) {**
**this.cartService.removeProduct(item.product);**
**}**
**}**
**return value;**
**}**
}
因为我们有一个清空购物车按钮,我们需要在CartService
中实现同名的方法:
clearCart() {
this.cart.items = [];
this.cart.amount = 0;
this.cart.count = 0;
}
购物车视图路由定义
我更新了app.routes.ts
中的路由器配置,以反映必要的更改以导航到CartViewComponent
:
const routes: Routes = [
{ path: '', redirectTo: 'welcome', pathMatch: 'full' },
{ path: 'welcome', component: WelcomeComponent },
{ path: 'products', component: ProductListComponent },
{ path: 'products/:id', component: ProductViewComponent },
**{ path: 'cart', component: CartViewComponent }**
];
导航到购物车视图
当用户在购物车菜单的标记中单击购物车按钮时,路由器使用链接中的信息导航到购物车视图:
<div class="row">
<div class="col-md-12">
**<a [routerLink]="['/cart']"**
class="btn btn-primary pull-xs-right btn-cart">
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
Cart
</a>
<a [routerLink]="['/checkout']"
class="btn btn-success pull-xs-right btn-cart">
<i class="fa fa-credit-card" aria-hidden="true"></i>
Checkout
</a>
</div>
</div>
我们需要更新CartModule
,将CartViewComponent
添加到NgModule
的declarations
属性中:
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
import {CartMenuComponent} from './cart-menu.component';
**import {CartViewComponent} from './cart-view.component';**
import {CartService} from './cart.service';
@NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule, RouterModule],
declarations: [CartMenuComponent, **CartViewComponent**
],
exports: [CartMenuComponent, **CartViewComponent**
],
providers: [CartService]
})
export class CartModule {}
这是购物车视图的屏幕截图:
提示
您可以在chapter_7/4.ecommerce-cart-view
找到源代码。
结账视图
结账视图显示客户详细信息表单,购买条件和订单信息。客户应填写表单,接受付款,并单击提交按钮开始付款流程。
结账视图的线框图
创建checkout
文件夹和checkout-view.component.ts
文件:
import {Component, Input} from '@angular/core';
import {FormGroup, FormBuilder, Validators} from '@angular/forms';
import {Cart, CartItem, CartService} from '../cart/cart.service';
@Component({
selector: 'db-checkout-view',
templateUrl: 'app/checkout/checkout-view.component.html'
})
export class CheckoutViewComponent {
private cart: Cart;
form: FormGroup;
constructor(private cartService: CartService,
private fb: FormBuilder) {
this.cart = this.cartService.cart;
}
ngOnInit() {
this.form = this.fb.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
email: ['', Validators.required],
phone: ['', Validators.required],
address: []
});
}
submit() {
alert('Submitted');
this.cartService.clearCart();
}
}
我在这里使用了模型驱动方法来创建表单的定义。当用户单击提交按钮时,它会显示消息并清空购物车。创建checkout-view.component.html
并将以下内容复制到那里:
<form [formGroup]="form">
<div class="form-group row">
<label for="firstName"
class="col-xs-2 col-form-label">First Name:</label>
<div class="col-xs-10">
<input class="form-control" type="text" value=""
id="firstName" formControlName="firstName">
<p [hidden]="form.controls.firstName.valid ||
form.controls.firstName.pristine"
class="form-text alert-danger">
The First Name is required
</p>
</div>
</div>
<div class="form-group row">
<label for="lastName" class="col-xs-2 col-form-label">
Last Name:</label>
<div class="col-xs-10">
<input class="form-control" type="text" value=""
id="lastName" formControlName="lastName">
<p [hidden]="form.controls.lastName.valid ||
form.controls.lastName.pristine"
class="form-text alert-danger">
The Last Name is required
</p>
</div>
</div>
<div class="form-group row">
<label for="email"
class="col-xs-2 col-form-label">Email:</label>
<div class="col-xs-10">
<input class="form-control" type="email" value=""
id="email">
<p [hidden]="form.controls.email.valid ||
form.controls.email.pristine"
class="form-text alert-danger">
The Email is required
</p>
</div>
</div>
<div class="form-group row">
<label for="phone"
class="col-xs-2 col-form-label">Phone:</label>
<div class="col-xs-10">
<input class="form-control" type="phone" value=""
id="phone">
<p [hidden]="form.controls.phone.valid ||
form.controls.phone.pristine"
class="form-text alert-danger">
The Phone is required
</p>
</div>
</div>
<div class="form-group row">
<label for="address"
class="col-xs-2 col-form-label">Address:</label>
<div class="col-xs-10">
<input class="form-control" type="text" value=""
id="address">
</div>
</div>
</form>
我们有几个必填字段,所以当它们为空时,Angular 2 通过NgModel
将它们的条变成红色。这是可以接受的,以指示问题,但不足以说明出了什么问题。我们可以使用验证错误消息来显示控件是否无效或未被触摸。看一下我从前面的代码中复制的标记:
<input class="form-control" type="text" value=""
id="firstName" formControlName="firstName">
<p [hidden]="form.controls.firstName.valid ||
form.controls.firstName.pristine"
class="form-text alert-danger">
The First Name is required
</p>
我们直接从表单模型中读取FormControl
状态的信息。我们检查firstName
字段是否有效,或者是否是原始的,并显示或隐藏错误消息。
最后,我们将提交按钮的禁用属性绑定到表单的有效性,因此只有在表单的所有字段都有效时,用户才有机会将数据发送到服务器:
<div class="col-xs-9">
<button class="btn btn-primary" (click)="submit()"
**[disabled]="!form.valid"**
>Submit</button>
<button class="btn btn-secondary"
[routerLink]="['/products']">Continue Shopping</button>
</div>
检查视图路由定义
更新app.routes.ts
中的路由器配置以添加CheckoutViewComponent
:
const routes: Routes = [
{ path: '', redirectTo: 'welcome', pathMatch: 'full' },
{ path: 'welcome', component: WelcomeComponent },
{ path: 'products', component: ProductListComponent },
{ path: 'products/:id', component: ProductViewComponent },
{ path: 'cart', component: CartViewComponent },
**{ path: 'checkout', component: CheckoutViewComponent }**
];
导航到结账视图
当用户在购物车菜单的标记上单击结账按钮时,路由器会导航到该视图:
<div class="row">
<div class="col-md-12">
<a [routerLink]="['/cart']"
class="btn btn-primary pull-xs-right btn-cart">
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
Cart
</a>
**<a [routerLink]="['/checkout']"**
class="btn btn-success pull-xs-right btn-cart">
<i class="fa fa-credit-card" aria-hidden="true"></i>
Checkout
</a>
</div>
</div>
CheckoutViewComponent
不属于任何模块,因此我们需要将其添加到AppModule
中:
/*
* Components
*/
import {AppComponent} from './app.component';
import {NavbarComponent} from './navbar/navbar.component';
import {FooterComponent} from './footer/footer.component';
import {WelcomeComponent} from './welcome/welcome.component';
**import {CheckoutViewComponent} from**
**'./checkout/checkout-view.component';**
/*
* Routing
*/
import {routing} from './app.routes';
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule,
routing, CartModule, CategoryModule, ProductModule],
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent, **CheckoutViewComponent**
],
bootstrap: [AppComponent]
})
export class AppModule { }
这是带有验证错误消息的结账视图的屏幕截图:
提示
您可以在chapter_7/5.ecommerce-checkout-view
找到源代码。
总结
在本章中,我们发现了如何使用 Bootstrap 4 创建表单。我们知道 Bootstrap 支持从简单到复杂的不同布局。
我们调查了 Angular 2 表单模块,现在可以创建基于模型和基于模板的表单。
我们把应用程序的所有部分都连接起来了,现在看起来非常好。
在第八章中,高级组件,我们将讨论组件的生命周期以及可以在组件不同阶段使用的方法。本章还讨论了如何创建多组件应用程序。像往常一样,我们将继续构建我们在之前章节中开始开发的项目。
第八章:高级组件
本章描述了组件的生命周期以及可以在生命周期的不同阶段使用的方法。在本章中,我们将分析此周期的每个阶段,并学习如何充分利用在组件从一个阶段转移到另一个阶段时触发的钩子方法。本章还讨论了如何创建多组件应用程序。读者将能够使用 Bootstrap 为应用程序添加更多功能。
在本章结束时,您将对以下内容有扎实的理解:
-
组件生命周期钩子接口
-
生命周期钩子方法
-
实现钩子接口
-
变更检测
-
组件之间的通信
指令
指令是 Angular 2 的基本构建块,允许您将行为连接到 DOM 中的元素。有三种类型的指令:
-
属性指令
-
结构指令
-
组件
指令是一个带有分配的类@Directive
装饰器。
属性指令
属性指令通常改变元素的外观或行为。我们可以通过绑定属性来改变多个样式或使用它来渲染文本为粗体或斜体。
结构指令
结构指令通过添加和删除其他元素来改变 DOM 布局。
组件
组件是带有模板的指令。每个组件由两部分组成:
-
我们定义应用程序逻辑的类
-
由组件控制的视图,通过属性和方法的 API 与其交互
组件是一个带有分配的类@Component
装饰器。
指令生命周期
要为任何项目开发自定义指令,您应该了解 Angular 2 指令生命周期的基础知识。指令在创建和销毁之间经历了许多不同阶段:
-
实例化
-
初始化
-
变更检测和渲染
-
内容投影(仅适用于组件)
-
视图之后(仅适用于组件)
-
销毁
Angular 生命周期钩子
Angular 提供了指令生命周期钩子,使我们能够在发生这些关键时刻时采取行动。我们可以在 Angular core
库中实现一个或多个生命周期钩子接口。每个接口都有一个单一的方法,其名称是以ng
为前缀的接口名称。接口对于 TypeScript 是可选的,如果定义了接口,Angular 将调用钩子方法。
注意
我建议实现生命周期钩子接口到指令类中,以便从强类型和编辑器工具中受益。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
实例化
注入器使用new
关键字创建指令实例。每个指令最多可以包含一个构造函数声明。如果一个类不包含构造函数声明,将提供自动构造函数。构造函数的主要目的是创建对象的新实例并为其设置初始属性。Angular 2 使用构造函数进行依赖注入,因此我们可以保存对依赖实例的引用以供以后使用:
export class CategoryListComponent {
categories: Category[];
constructor(private router: Router,
private categoryService: CategoryService) {
this.categories = this.categoryService.getCategories();
}
filterProducts(category: Category) {
this.router.navigate(['/products'], {
queryParams: { category: category.id}
});
}
}
在上面的例子中,CategoryListComponent
类有一个构造函数,其中有两个参数引用Router
和CategoryService
。
初始化
在每个指令中都有数据绑定的输入属性,并且 Angular 在初始化阶段保存了绑定属性的值:
export class CategorySlideComponent {
@Input() category: Category;
@Output() select: EventEmitter<Category> =
new EventEmitter<Category>();
}
CategorySlideComponent
类有一个与模板中同名属性绑定的类别。
我们可以实现OnInit
和OnChanges
接口来做出相应的响应:
-
当数据绑定的输入属性值发生变化时,Angular 调用
ngOnChanges
方法 -
Angular 在第一次
ngOnChanges
之后调用ngOnInit
方法,并向我们发出信号,表明组件已经初始化
在下面的代码中,我们实现了OnInit
接口来创建表单控件并开始监听其值的变化:
@Component({
selector: 'db-product-search',
templateUrl: 'app/product/product-search.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductSearchComponent implements OnInit {
disabled: boolean = true;
seachControl: FormControl;
constructor(private router: Router,
private changeDetection: ChangeDetectorRef) {}
**ngOnInit() {**
**this.seachControl = new FormControl();**
**this.seachControl.valueChanges.subscribe((value: string)**
**=> {**
**this.searchChanged(value);**
**this.changeDetection.markForCheck();**
**});**
**}**
...
}
大多数情况下,我们依赖ngOnInit
方法有以下原因:
-
我们需要在构造函数之后执行初始化
-
在 Angular 设置输入属性后,完成组件设置
这个方法是一个完美的位置,用于从服务器获取数据或根据输入属性更新内部状态的繁重初始化逻辑。
变更检测和渲染
这个阶段故意结合了 Angular 2 用来为应用程序注入生命的两种重要技术。一方面,框架的变更检测模块负责监视程序的内部状态的变化。它可以检测任何数据结构的变化,从基本类型到对象数组。另一方面,Angular 的渲染部分使这些变化在 DOM 中可见。Angular 将这两种技术结合在一个阶段中,以最小化工作量,因为重建 DOM 树是昂贵的。
NgZone 服务
大多数情况下,应用程序状态的变化是因为应用程序中发生了以下异步任务:
-
用户或应用程序触发的事件
-
指令和管道属性变化
-
从 AJAX 响应中调用回调函数
-
来自定时器的回调函数调用
Angular 使用 Zone 库中的执行上下文NgZone
来钩入这些异步任务以检测变化、处理错误和进行分析。当代码进入或退出区域时,区域可以执行几个重要的操作,例如:
-
启动或停止计时器
-
保存堆栈跟踪
-
覆盖执行代码的方法
-
将数据与各个区域关联等
每个 Angular 应用程序都有一个包装可执行代码的全局区域对象,但我们也可以使用NgZone
服务在 Angular 区域内外执行工作。 NgZone
是一个扩展了标准区域 API 并向执行上下文添加了一些附加功能的分支区域。 Angular 使用NgZone
来 monkey-patch 全局异步操作,例如setTimeout
和addEventListener
以更新 DOM。
变更检测
Angular 框架中的每个指令都有一个变更检测器,因此我们可以定义如何执行变更检测。指令的分层结构将变更带入了舞台上的检测器树,因此 Angular 始终使用单向数据流作为一种工具,将数据从父级传递给子级。
大多数情况下,Angular 的变更检测发生在属性上,并相应地更新视图,与数据结构无关:
@Component({
selector: 'db-product-card',
templateUrl: 'app/product/product-card.component.html'
})
export class ProductCardComponent {
**@Input() products: Product[];**
**@Output() addToCart: EventEmitter<Product> =**
**new EventEmitter<Product>();**
setClasses(product: Product) {
return {
'card-danger': product.isSpecial,
'card-inverse': product.isSpecial
};
}
buy(product: Product) {
this.addToCart.emit(product);
}
}
属性绑定用于向product
提供数据,事件绑定用于通知其他组件进行任何更新,并将其委托给存储。 product
是一个指向具有许多字段的真实对象的引用:
export interface Product {
// Unique Id
id: string;
// Ref on category belongs to
categoryId: string;
// The title
title: string;
// Price
price: number;
// Mark product with special price
isSpecial: boolean;
// Description
desc: string;
// Path to small image
imageS: string;
// Path to large image
imageL: string;
}
即使任何字段都可以更改,product
引用本身仍然保持不变。由于框架变更检测系统可以在几毫秒内执行数百甚至数千次检查指令属性的更改而不会降低性能,因此 Angular 将在每次执行大量的检查变更时执行大量的检查变更。有时这种大规模的变更检测可能非常昂贵,因此我们可以根据每个指令选择变更检测策略。
指令的内部状态仅取决于其输入属性,因此如果这些属性从一次检查到下一次没有发生变化,那么指令就不需要重新渲染。请记住,所有的 JavaScript 对象都是可变的,因此变更检测应该检查所有输入属性字段,以在必要时重新渲染指令。如果我们使用不可变结构,那么变更检测可以更快。让我们看看可能发生的情况。
不可变对象
不可变对象是不会改变的。它始终只有一个内部状态,如果我们想对这样的对象进行更改,我们将始终得到对该更改的新引用。
变更检测策略
Angular 支持以下变更检测策略:
-
Default
策略意味着变更检测器将深度检查每个脏检查的属性 -
OnPush
策略意味着变更检测器将检查每个脏检查的属性引用的变化
我们可以通过装饰器的changeDetection
属性指示 Angular 可以为特定指令使用哪种变更检测策略:
@Component({
selector: 'db-product-card',
templateUrl: 'app/product/product-card.component.html',
**changeDetection: ChangeDetectionStrategy.OnPush**
})
export class ProductCardComponent {
...
}
只有当通过输入属性提供给指令的所有值都是不可变的时,OnPush
策略才能正常工作。
注意
不要在OnPush
检测策略中使用可变值,因为这可能会使 Angular 应用程序处于不一致或不可预测的状态。
Angular 会自动触发变更检测,以检查指令在OnPush
模式下是否发生以下情况:
-
当任何指令输入属性发生变化时
-
每当指令触发事件时
-
当属于该指令的任何可观察对象触发事件时
以编程方式触发变更检测
如前所述,每个指令都有一个自动工作的变更检测器。在需要以编程方式触发变更检测的情况下,我们可以使用ChangeDetectionRef
类。我们可以在发生变化的地方调用该类的markForCheck
方法,这样它就会标记从该指令到根的路径,以便在下一次变更检测运行时进行检查:
import {Component, ChangeDetectionStrategy, ChangeDetectorRef}
from '@angular/core';
import {Router} from '@angular/router';
import {FormControl} from '@angular/forms';
@Component({
selector: 'db-product-search',
templateUrl: 'app/product/product-search.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductSearchComponent {
disabled: boolean = true;
seachControl: FormControl;
constructor(private router: Router,
private changeDetection: ChangeDetectorRef) {}
ngOnInit() {
this.seachControl = new FormControl();
this.seachControl.valueChanges.subscribe((value: string)
=> {
this.searchChanged(value);
**this.changeDetection.markForCheck();**
});
}
searchProduct(value: string) {
this.router.navigate(['/products'], {
queryParams: { search: value}
});
}
searchChanged(value: string) {
// Update the disabled property depends on value
if (value) {
this.disabled = false;
} else {
this.disabled = true;
}
}
}
在前面的代码中,我们触发了变更检测,因为字符串值来自searchControl
,而该值始终是不可变的。
正如我们所提到的,我们可以实现OnChanges
接口来检测指令的输入属性的变化,并相应地做出响应:
- 当数据绑定的输入属性值发生变化时,Angular 调用
ngOnChanges
方法。大多数情况下,我们不使用这个方法,但如果需要根据输入属性改变内部状态的依赖关系,这就是合适的地方。
在下面的代码中,我们使用OnChanges
接口来监视category
输入属性的变化:
import {Component, Input, OnChanges, SimpleChanges}
from '@angular/core';
import {Router} from '@angular/router';
import {Category} from './category.service';
@Component({
selector: 'db-category-card',
templateUrl: 'app/category/category-card.component.html'
})
export class CategoryCardComponent implements OnChanges {
**@Input() category: Category;**
constructor(private router: Router) {}
**ngOnChanges(changes: SimpleChanges): void {**
**for (let propName in changes) {**
**let change = changes[propName];**
**let current = JSON.stringify(change.currentValue);**
**let previous = JSON.stringify(change.previousValue);**
**console.log(`${propName}: currentValue = ${current},**
**previousValue = ${previous}`);**
**}**
**}**
filterProducts(category: Category) {
this.router.navigate(['/products'], {
queryParams: { category: category.id}
});
}
}
当将值分配给category
时,ngOnChanges
方法打印以下信息:
category: currentValue = {"id":"1", "title":"Bread & Bakery", "imageL":"http://placehold.it/1110x480", "imageS":"http://placehold.it/270x171", "desc":"The best cupcakes, cookies, cakes, pies, cheesecakes, fresh bread, biscotti, muffins, bagels, fresh coffee and more."}, previousValue = {}
SimpleChanges
类保留了每个更改的属性名称的当前值和先前值,因此我们可以遍历并记录它们。
我们可以在指令中实现DoCheck
接口来检测并对 Angular 无法捕捉到的变化做出反应。Angular 在每次变化检测周期中调用ngDoCheck
方法。请谨慎使用此方法,因为 Angular 会以极高的频率调用它,因此实现必须非常轻量级。
内容投影(仅适用于组件)
一般来说,组件是一个 HTML 元素,可能包含文本或标记等内容。Angular 2 使用带有ng-content
标记的特定入口点将内容注入到组件模板中。这种技术被称为内容投影,Angular 使用 Shadow DOM 来实现。
Angular 2 利用 Web 组件标准并使用一组以下技术:
-
用于结构性 DOM 更改的模板
-
Shadow DOM 用于样式和 DOM 封装
我们在项目中使用了模板,现在是时候谈谈 Angular 如何在不同的封装类型中使用 Shadow DOM 了。
Shadow DOM 允许我们将 DOM 逻辑隐藏在其他元素后面,并在其范围内应用样式。Shadow DOM 内部的所有内容对其他组件不可用,因此我们称之为封装。事实上,Shadow DOM 是一种新技术,并非所有的 Web 浏览器都支持它,因此 Angular 使用模拟来模仿 Shadow DOM 的行为方式。
Angular 中有三种封装类型:
-
ViewEncapsulation.None
:Angular 不使用 Shadow DOM 和样式封装 -
ViewEncapsulation.Emulated
:Angular 不使用 Shadow DOM,但模拟样式封装 -
ViewEncapsulation.Native
:Angular 使用原生 Shadow DOM 并获得所有的好处
我们将使用@Component
装饰器的encapsulation
属性来指示 Angular 使用哪种封装类型。
组件样式
在 Angular 2 中,我们可以为整个文档和特定组件应用样式。这种变化带来了另一种粒度级别,并有助于比常规样式表更模块化的设计。组件样式与任何全局样式都不同。组件样式中的任何选择器都在该组件及其子元素的范围内应用。组件样式带来以下好处:
-
我们可以在组件的上下文中使用任何 CSS 类或选择器,而不必担心与应用程序其他部分中使用的类和选择器发生名称冲突。
-
组件中封装的样式对应用程序的其余部分是不可见的,也不能在其他地方更改。我们可以更改或删除组件样式而不影响整个应用程序的样式。
-
组件样式可以放在单独的文件中,并且可以与 TypeScript 和 HTML 代码共存,这使得项目代码更加结构化和有组织。
特殊选择器
组件样式可能包括几个特殊选择器。所有这些都来自 Shadow DOM 世界。
:host 伪类
托管组件的任何元素都称为宿主。从托管组件中的宿主元素中定位宿主元素的样式的唯一方法是使用:host
伪类选择器:
:host {
display: block;
border: 1px solid black;
}
在前面的代码片段中,我们更改了父组件模板中的显示和边框样式。在需要有条件地应用宿主样式时,可以使用另一个选择器作为样式函数形式的参数:
:host(.active) {
border-width: 3px;
}
前面的样式只有在宿主具有active
类时才适用。
:host-context 伪类
想象一下,当您为 Web 应用程序创建主题并希望根据其他选择器的存在或不存在应用特定样式到您的组件时。您可以轻松地使用:host-context
函数来实现:
:host-context(.theme-dark) p {
background-color: gray;
}
前面代码的逻辑是在组件宿主元素到文档根部之间查找theme-dark
CSS 类,并将gray
应用于组件内所有段落元素的background-color
样式。
/deep/选择器
组件的样式仅适用于其模板。如果我们需要将它们应用于所有子元素,那么我们需要使用/deep/
选择器:
:host /deep/ h4 {
font-weight: bold;
}
前面代码片段中的/deep/
选择器将把bold
应用于组件中所有h4
标题元素的font-weight
样式,通过子组件树一直到所有子组件视图。
/deep/
选择器有一个别名>>>
,我们可以在模拟视图封装中互换使用。
非视图封装
Angular 不使用此类型的 Shadow DOM 和样式封装。让我们想象一下,在我们的项目中有一个ParentComponent
:
import {Component, Input, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'my-parent',
template: `
<div class="parent">
<div class="parent__title">
{{title}}
</div>
<div class="parent__content">
<ng-content></ng-content>
</div>
</div>`,
styles: [`
.parent {
background: green;
color: white;
}
`],
**encapsulation: ViewEncapsulation.None**
})
export class ParentComponent {
@Input() title: string;
}
在AppComponent
的代码中,我们有以下内容:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
**<my-parent >**
**<my-child></my-child>**
**</my-parent>`**
})
export class AppComponent { }
ParentComponent
有自己的样式,它可以用另一个组件覆盖它,因为它将稍后应用于文档头部:
<head>
...
<style>
.parent {
background: green;
color: white;
}
</style>
<style>.child[_ngcontent-ced-3] {
background: red;
color: yellow;
}</style>
</head>
Angular 生成以下在浏览器中运行的 HTML 代码:
<my-app>
<my-parent ng-reflect->
<div class="parent">
<div class="parent__title">
Parent
</div>
<div class="parent__content">
<my-child _nghost-fhc-3="">
<div _ngcontent-fhc-3="" class="child">
Child
</div>
</my-child>
</div>
</div>
</my-parent>
</my-app>
没有 Shadow DOM 的参与,应用程序将样式应用于整个文档。Angular 用子组件的内容替换了ng-content
。
模拟视图封装
模拟视图是 Angular 用来创建组件的默认视图封装。Angular 不使用 Shadow DOM,但模拟样式封装。让我们改变encapsulation
属性的值来看看区别。这是 Angular 为模拟视图封装生成的样式:
<head>
...
<style>.parent[_ngcontent-xdn-2] {
background: green;
color: white;
}</style><style>.child[_ngcontent-xdn-3] {
background: red;
color: yellow;
}</style>
</head>
父组件的样式看起来不同,并属于特定元素。这就是 Angular 模拟样式封装的方式:
<my-app>
<my-parent _nghost-xdn-2=""
ng-reflect->
<div _ngcontent-xdn-2="" class="parent">
<div _ngcontent-xdn-2="" class="parent__title">
Parent
</div>
<div _ngcontent-xdn-2="" class="parent__content">
<my-child _nghost-xdn-3="">
<div _ngcontent-xdn-3="" class="child">
Child
</div>
</my-child>
</div>
</div>
</my-parent>
</my-app>
页面的标记部分看起来与非视图封装非常相似。
原生视图封装
原生视图是最简单的封装之一。它使用原生 Shadow DOM 来封装内容和样式。Angular 不需要为父组件生成任何样式:
<head>
...
<style>.child[_ngcontent-sgt-3] {
background: red;
color: yellow;
}</style>
</head>
现在,父组件的样式对其他应用程序以及标记代码都不可用:
<my-app>
<my-parent ng-reflect->
**#shadow-root**
<style>.child[_ngcontent-sgt-3] {
background: red;
color: yellow;
}</style>
<style>.parent {
background: green;
color: white;
}</style>
<div class="parent">
<div class="parent__title">
Parent
</div>
<div class="parent__content">
<my-child _nghost-sgt-3="">
<div _ngcontent-sgt-3="" class="child">
Child
</div>
</my-child>
</div>
</div>
</my-parent>
</my-app>
如果我们需要投影多个子内容,我们可以使用带有专用select
属性的ng-content
:
import {Component, Input, ViewEncapsulation} from '@angular/core';
@Component({
selector: 'my-parent',
template: `
<div class="parent">
<div class="parent__title">
{{title}}
</div>
<div class="parent__content">
**<ng-content></ng-content>**
</div>
<div class="parent__content">
**<ng-content select=".another"></ng-content>**
</div>
</div>`,
styles: [`
.parent {
background: green;
color: white;
}
`],
encapsulation: ViewEncapsulation.Native
})
export class ParentComponent {
@Input() title: string;
}
请记住,select
属性期望 Angular 可以在document.querySelector
中使用的字符串值。在应用程序组件中,我们有类似的东西:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<my-parent >
**<my-child></my-child>**
**<my-child class="another"></my-child>**
</my-parent>`
})
export class AppComponent { }
这是 Angular 生成的结果标记:
<div class="parent">
<div class="parent__title">
Parent
</div>
<div class="parent__content">
**<my-child _nghost-cni-3="">**
**<div _ngcontent-cni-3="" class="child">**
**Child**
**</div>**
**</my-child>**
</div>
<div class="parent__content">
**<my-child class="another" _nghost-cni-3="">**
**<div _ngcontent-cni-3="" class="child">**
**Child**
**</div>**
**</my-child>**
</div>
</div>
提示
您可以在chapter_8/1.view-encapsulation
找到源代码。
现在,我们知道内容投影是 Angular 从组件外部导入 HTML 内容并将其插入到模板的设计部分的方式。当 Angular 把外部内容投影到组件中时,它会调用 AfterContentInit
和 AfterContentChecked
接口的钩子方法:
-
在 Angular 把外部内容投影到其视图中并且内容已初始化后,它会调用
ngAfterContentInit
方法 -
在 Angular 检查它投影到其视图中的外部内容的绑定后,它会调用
ngAfterContentChecked
钩子方法
我们可以使用其中任何一个来操作内容元素的属性。为了组织对一个或多个内容元素的访问,我们必须获取父组件的属性并用 @ContentChild
或 @ContentChildren
进行装饰。Angular 使用传递给装饰器的参数来选择内容元素:
-
如果参数是一个类型,Angular 将找到与相同类型的指令或组件绑定的元素
-
如果参数是一个字符串,Angular 将解释它为一个选择器,以查找相应的元素
Angular 在调用 ngAfterContentInit
方法之前设置装饰属性的值,以便我们可以在方法内部访问它。稍后,当 Angular 检查和更新内容元素时,它会调用 ngAfterContentChecked
来通知我们包含元素已更新。让我们看看如何使用它。这是我们将用作父组件内容的子组件:
import {Component, Input} from '@angular/core';
@Component({
selector: 'my-child',
template: `
<div class="child">
Child is {{status}}
</div>`,
styles: [`
.child {
background: red;
color: yellow;
}
`]
})
export class ChildComponent {
@Input() status: string = 'Not Ready';
}
我们将查看子组件的 status
属性,并从父组件在控制台上打印出值:
import {Component, Input, AfterContentInit, AfterContentChecked,
ContentChild} from '@angular/core';
import {ChildComponent} from './child.component';
@Component({
selector: 'my-parent',
template: `
<div class="parent">
<div class="parent__title">
{{title}}
</div>
<div class="parent__content">
<ng-content></ng-content>
</div>
</div>`,
styles: [`
.parent {
background: green;
color: white;
}
`]
})
export class ParentComponent implements
AfterContentInit, AfterContentChecked {
@Input() title: string;
// Query for a CONTENT child of type ChildComponent`
**@ContentChild(ChildComponent) contentChild: ChildComponent;**
**ngAfterContentInit() {**
// contentChild is set after the content has been initialized
console.log('AfterContentInit. Child is',
this.contentChild.status);
this.title = 'Parent';
}
**ngAfterContentChecked() {**
console.log('AfterContentChecked. Child is',
this.contentChild.status);
// contentChild is updated after the content has been checked
if (this.contentChild.status == 'Ready') {
console.log('AfterContentChecked (no change)');
} else {
this.contentChild.status = 'Ready';
}
}
}
让我们将它们组合在应用程序组件模板中:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
**<my-parent >**
**<my-child></my-child>**
**</my-parent>`**
})
export class AppComponent { }
现在,运行应用程序,我们将在控制台上得到以下登录信息:
**AfterContentInit. Child is Not Ready**
**AfterContentChecked. Child is Not Ready**
**AfterContentChecked. Child is Ready**
**AfterContentChecked (no change)**
提示
您可以在 chapter_8/2.after-content
找到此源代码。
视图之后(仅适用于组件)
当 Angular 完成组件视图及其子视图的初始化时,它会调用两个钩子接口 AfterViewInit
和 AfterViewChecked
的方法。我们可以利用初始化的时刻来更新或操作视图元素:
-
当 Angular 完成组件视图及其子视图的初始化时,它会调用
ngAfterViewInit
方法 -
Angular 在检查组件视图的绑定和其子视图的视图之后调用
ngAfterViewChecked
方法
我们可以使用其中任何一个来操作视图元素。为了组织对一个或多个视图元素的访问,我们必须在父组件中拥有该属性,并用@ViewChild
或@ViewChildren
装饰它。Angular 使用传递到装饰器的参数来选择视图元素:
-
如果参数是一个类型,Angular 将找到一个与相同类型的指令或组件绑定的元素
-
如果参数是一个字符串,Angular 会将其解释为一个选择器,以找到相应的元素
在调用ngAfterViewInit
方法之前,Angular 会设置装饰属性的值。稍后,在检查和更新视图元素后,它会调用ngAfterViewChecked
来通知我们视图元素已更新。让我们看看我们如何使用它。这是我们将在父组件模板中使用的子组件:
import {Component, Input} from '@angular/core';
@Component({
selector: 'my-child',
template: `
<div class="child">
Child is {{status}}
</div>`,
styles: [`
.child {
background: red;
color: yellow;
}
`]
})
export class ChildComponent {
@Input() status: string = 'Not Ready';
}
我们正在观察子组件的status
属性,并将从父组件打印出值:
import {Component, Input, AfterViewInit, AfterViewChecked,
ViewChild, ChangeDetectionStrategy} from '@angular/core';
import {ChildComponent} from './child.component';
@Component({
selector: 'my-parent',
**changeDetection: ChangeDetectionStrategy.OnPush,**
template: `
<div class="parent">
<div class="parent__title">
{{title}}
</div>
<div class="parent__content">
**<my-child></my-child>**
</div>
</div>`,
styles: [`
.parent {
background: green;
color: white;
}
`]
})
export class ParentComponent implements
**AfterViewInit, AfterViewChecked {**
@Input() title: string;
// Query for a VIEW child of type `ChildComponent`
**@ViewChild(ChildComponent) viewChild: ChildComponent;**
ngAfterViewInit() {
// viewChild is set after the view has been initialized
console.log('AfterViewInit. Child is', this.viewChild.status);
this.title = 'Parent';
}
ngAfterViewChecked() {
console.log('AfterViewChecked. Child is',
this.viewChild.status);
// viewChild is updated after the view has been checked
if (this.viewChild.status == 'Ready') {
console.log('AfterViewChecked (no change)');
} else {
this.viewChild.status = 'Ready';
}
}
}
请记住,我们在这段代码中使用OnPush
变更检测,以防止循环调用ngAfterViewChecked
方法。这是应用程序组件模板:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
**<my-parent >**
**</my-parent>`**
})
export class AppComponent { }
现在,运行应用程序,我们将得到以下登录控制台:
**AfterViewInit. Child is Not Ready**
**AfterViewChecked. Child is Not Ready**
**AfterViewChecked. Child is Ready**
**AfterViewChecked (no change)**
提示
你可以在chapter_8/3.after-view
找到这个的源代码。
从父到子的通信
组织父子组件之间的通信并不是微不足道的,所以让我们谈谈我们可以使用的不同技术来实现这一点。
通过输入绑定进行父到子的通信
每个指令可能有一个或多个输入属性。我们可以将子组件的任何属性与静态字符串或父组件变量绑定,以组织它们之间的通信。这是子组件:
import {Component, Input, Output, EventEmitter, OnInit }
from '@angular/core';
@Component({
selector: 'my-child',
template: `
<div class="child">
**{{desc}} belongs to {{parent}} with {{emoji}}**
</div>`,
styles: [`
.child {
background: red;
color: yellow;
}
`]
})
export class ChildComponent {
**@Input() desc: string;**
**@Input('owner') parent: string;**
**private _emoji: string;**
**@Input() set emoji(value: string) {**
**this._emoji = value || 'happy';**
**}**
**get emoji(): string {**
**return this._emoji;**
**}**
@Output() status: EventEmitter<string> =
new EventEmitter<string>();
ngOnInit(): void {
this.status.emit('Ready');
}
}
它有三个用@Input
装饰器标记的输入属性:
-
属性
desc
由其自然名称装饰 -
属性
parent
用别名装饰,以便父组件将其视为owner
的名称 -
属性
emoji
是一组 getter/setter 方法的组合,这样我们可以添加一些逻辑来为私有变量分配值
它有一个输出属性status
,用于从子级到父级进行通信。我特意添加了一个OnInit
钩子接口,这样我们可以在创建子级后向父级发送消息。这是父组件:
import {Component, Input} from '@angular/core';
@Component({
selector: 'my-parent',
template: `
<div class="parent">
<div class="parent__title">
{{title}}. Child is {{status}}
</div>
<div class="parent__content">
**<my-child [desc]="'Child'"**
**[owner]="title"**
**[emoji]="'pleasure'"**
**(status)="onStatus($event)" ></my-child>**
</div>
</div>`,
styles: [`
.parent {
background: green;
color: white;
}
`]
})
export class ParentComponent {
@Input() title: string;
status: string;
onStatus(value: string) {
this.status = value;
}
}
父组件设置子组件的所有输入属性,并在onStatus
方法中监听status
事件。创建后,子组件会发出状态事件,父组件会在标题附近打印该信息。
提示
您可以在chapter_8/4.parent-child-input-binding
找到此源代码。
通过本地变量进行父子通信
父元素无法访问子组件的属性或方法。我们可以在父模板中创建一个模板引用变量,以便访问子组件的类成员:
import {Component, Input} from '@angular/core';
@Component({
selector: 'my-parent',
template: `
<div class="parent" [ngInit]="child.setDesc('You are mine')">
<div class="parent__title">
{{title}}
</div>
<div class="parent__content">
<my-child #child></my-child>
</div>
</div>`,
styles: [`
.parent {
background: green;
color: white;
}
`]
})
export class ParentComponent {
@Input() title: string;
}
在前面的父组件中,我们创建了child
本地模板变量,并在NgInit
指令中使用它来调用子组件的setDesc
方法:
import {Component, Input} from '@angular/core';
@Component({
selector: 'my-child',
template: `
<div class="child">
{{desc}}
</div>`,
styles: [`
.child {
background: red;
color: yellow;
}
`]
})
export class ChildComponent {
@Input() desc: string;
setDesc(value: string) {
this.desc = value;
}
}
我们使用了NgInit
指令来初始化子组件的desc
属性:
import {Directive, Input} from '@angular/core';
@Directive({
selector: '[ngInit]'
})
export class NgInit {
@Input() ngInit;
ngOnInit() {
if(this.ngInit) {
this.ngInit();
}
}
}
提示
您可以在chapter_8/5.parent-child-local-variable
找到此源代码。
通过调用 ViewChild 进行父子通信
当我们需要从父组件访问子组件时,我们可以使用AfterViewInit
和AfterViewChecked
钩子。Angular 在创建组件的子视图后调用它们。以下是子组件:
import {Component, Input} from '@angular/core';
@Component({
selector: 'my-child',
template: `
<div class="child">
{{desc}}
</div>`,
styles: [`
.child {
background: red;
color: yellow;
}
`]
})
export class ChildComponent {
@Input() desc: string;
}
父组件导入必要的类并实现AfterViewInit
接口:
import {Component, Input, AfterViewInit, ViewChild}
from '@angular/core';
import {ChildComponent} from './child.component';
@Component({
selector: 'my-parent',
template: `
<div class="parent">
<div class="parent__title">
{{title}}
</div>
<div class="parent__content">
**<my-child></my-child>**
</div>
</div>`,
styles: [`
.parent {
background: green;
color: white;
}
`]
})
export class ParentComponent implements AfterViewInit {
@Input() title: string;
**@ViewChild(ChildComponent)**
**private child: ChildComponent;**
**ngAfterViewInit()
{**
**this.child.desc = "You are mine";**
**}**
}
我们通过之前介绍的@ViewChild
装饰器将子组件注入到父组件中。在这种情况下,AfterViewInit
接口非常重要,因为在 Angular 显示父视图并调用ngAfterViewInit
方法之前,child
组件是不可用的。
提示
您可以在chapter_8/6.parent-child-viewchild
找到此源代码。
通过服务进行父子通信
组织父子通信的另一种可能方式是通过一个公共服务。我们将服务分配给父组件,并在该父组件和其子组件之间锁定服务实例的范围。此子树之外的任何单个组件都无法访问该服务或它们的通信。在这里,子组件可以通过构造函数注入来访问服务:
import {Component, Input, OnDestroy} from '@angular/core';
import {Subscription} from 'rxjs/Subscription';
import {CommonService} from './common.service';
@Component({
selector: 'my-child',
template: `
<div class="child">
{{desc}}
</div>`,
styles: [`
.child {
background: red;
color: yellow;
}
`]
})
export class ChildComponent implements OnDestroy {
@Input() desc: string;
subscription: Subscription;
constructor(private common: CommonService) {
this.subscription = this.common.childQueue.subscribe(
message => {
this.desc = message;
}
);
}
ngOnDestroy() {
// Clean after yourself
this.subscription.unsubscribe();
}
}
我们在构造函数中对来自父组件的消息进行订阅。请在实现时注意OnDestroy
接口。ngOnDestroy
方法中的代码是一个内存泄漏保护步骤。父组件已经注册了一个CommonService
作为提供者,并通过构造函数注入:
import {Component, Input, OnInit} from '@angular/core';
import {CommonService} from './common.service';
@Component({
selector: 'my-parent',
template: `
<div class="parent">
<div class="parent__title">
{{title}}
</div>
<div class="parent__content">
<my-child></my-child>
</div>
</div>`,
styles: [`
.parent {
background: green;
color: white;
}
`],
**providers: [CommonService]**
})
export class ParentComponent implements OnInit {
@Input() title: string;
constructor(private common: CommonService) {
**this.common.parentQueue.subscribe(**
**message => {**
**this.title = message;**
**}**
**);**
}
ngOnInit() {
**this.common.toChild("You are mine");**
}
}
在这里我们不需要内存泄漏保护步骤,因为父组件控制着注册提供者的生命周期。
提示
您可以在chapter_8/7.parent-child-service
找到此源代码。
销毁
这个阶段是最后一个存在指令的阶段。我们可以实现OnDestroy
接口来捕获这一时刻:
-
在销毁指令之前,Angular 会调用
ngOnDestroy
方法 -
Angular 在这个方法中添加了清理逻辑,以取消订阅可观察对象并分离事件处理程序,以避免内存泄漏
我们可以通知另一个组件(父组件或同级组件),指令即将消失。我们必须释放分配的资源,取消订阅可观察对象和 DOM 事件监听器,并取消来自服务的所有回调。
总结
在本章中,我们了解了组件的生命周期以及可以在不同阶段使用的方法。我们了解到 Angular 具有具有钩子方法的接口,以及如何充分利用这些钩子方法,这些钩子方法在组件从一个阶段转移到另一个阶段时触发。
我们揭示了 Angular 变化检测的工作原理以及我们如何管理它。我们讨论了如何组织组件之间的通信。
在第九章中,通信和数据持久性,我们将学习如何进行 HTTP 请求并在 Firebase 平台上存储数据。我们将学习如何使用内置的 HTTP 库来处理端点。此外,我们还将学习如何使用可观察对象来处理数据。在本章末尾,我们将学习如何将 Firebase 作为应用程序的持久性层。与往常一样,我们将继续构建我们在之前章节中开始开发的项目。
第九章:通信和数据持久性
本章讲解如何处理 HTTP 请求并将数据存储在服务器上。我们将学习如何使用内置的 HTTP 库来处理端点。此外,我们还将学习如何使用可观察对象来处理数据。在本章结束时,我们将学习如何将 Firebase 作为应用程序的持久层。和往常一样,我们将继续构建之前章节中开始开发的项目。
在本章结束时,您将对以下内容有扎实的了解:
-
HttpModule
-
创建连接
-
可观察对象
-
安装 Firebase 工具
-
连接到 Firebase
让我们开始吧:
-
打开终端,创建文件夹
ecommerce
,并进入该文件夹。 -
将项目文件夹
chapter_9/1.ecommerce-seed
中的内容复制到新项目中。 -
运行以下脚本以安装 npm 模块:
**npm install**
- 使用以下命令启动 TypeScript 监视器和轻量级服务器:
**npm start**
此脚本打开 Web 浏览器并导航到项目的欢迎页面。
客户端到服务器的通信
Web 浏览器和服务器作为客户端-服务器系统运行。一般来说,Web 服务器保存数据,并在请求时与任意数量的 Web 浏览器共享数据。Web 浏览器和服务器必须有共同的语言,并且必须遵循规则,以便双方知道可以期待什么。通信的语言和规则在通信协议中定义。传输控制协议(TCP)是一种标准,定义了如何建立和维护网络会话,通过该会话应用程序可以交换数据。TCP 与互联网协议(IP)一起工作,后者定义了计算机如何相互发送数据包。TCP 和 IP 共同定义了互联网的基本规则。Web 浏览器和服务器通过TCP/IP堆栈进行通信。要在 TCP/IP 网络上发送数据,需要四个步骤或层:
-
应用层对发送的数据进行编码。它不关心数据如何在两点之间传输,对网络状态知之甚少。应用程序将数据传递给 TCP/IP 堆栈中的下一层,然后继续执行其他功能,直到收到回复。
-
传输层将数据分割成可管理的块,并添加端口号信息。传输层使用端口号进行寻址,端口号范围从 1 到 65,535。从 0 到 1,023 的端口号被称为众所周知的端口。256 以下的数字保留用于在应用层上运行的公共服务。
-
互联网层添加 IP 地址,说明数据的来源和去向。它是将网络连接在一起的“胶水”。它允许数据的发送、接收和路由。
-
链接层添加媒体访问控制(MAC)地址信息,以指定消息来自哪个硬件设备,消息将要传送到哪个硬件设备。MAC 地址在接口制造时固定,并且无法更改。
所有客户端-服务器协议都在应用层操作。应用层协议规定了基本的通信模式。为了使数据交换格式得到正式化,服务器实现了一个应用程序接口(API),比如一个网络服务。API 是资源(比如数据库和自定义软件)的抽象层。超文本传输协议(HTTP)是实现万维网(WWW)的应用层协议。虽然网络本身有许多不同的方面,但 HTTP 的主要目的是将超文本文档和其他文件从网络服务器传输到网络客户端。
Web API
Web 客户端和服务器资产之间的交互是通过定义的接口Web API进行的。它是一种服务器架构方法,为不同类型的消费者提供可编程接口。Web API 通常被定义为一组 HTTP 请求和响应消息。一般来说,回复消息的结构以可扩展标记语言(XML)或JavaScript 对象表示(JSON)格式表示。
在 Web 1.0 时代,Web API 是简单对象访问协议(SOAP)为基础的网络服务和面向服务的架构(SOA)的同义词。在 Web 2.0 中,这个术语正在向表述状态转移(REST)风格的网络资源和面向资源的架构(ROA)转变。
REST
REST 是万维网的一种架构风格,用于设计网络应用程序。REST 没有标准或 W3C 推荐。REST 这个术语是由 Roy Fielding 在他的博士论文中于 2000 年引入和定义的。后来,他使用 REST 设计了 HTTP 1.1 和统一资源标识符(URIs)。
作为一种编程方法,REST 是:
-
与平台无关,因此服务器可以安装在 Linux、Windows 等上。
-
与语言无关,因此我们可以使用 C#、Java、JavaScript 等。
-
基于标准,并且可以在 HTTP 标准之上运行。
REST 使用简单的 HTTP 协议在客户端和服务器之间进行调用,而不是使用复杂的机制,如远程过程调用(RPC)、公共对象请求代理体系结构(CORBA)或 SOAP。任何调用 RESTful 的应用程序都符合 REST 的约束:
-
客户端-服务器约束意味着客户端和服务器是分离的,因此它们可以独立替换和开发。
-
客户端和服务器的通信基于无状态约束,因此在请求之间服务器上没有存储客户端上下文,每个请求都包含了服务请求所需的所有信息。
-
可缓存约束定义了服务器响应是否必须隐式或显式地标记自己为可缓存或不可缓存。
-
遵守层系统约束,客户端和服务器使用分层架构来提高整个系统的能力和可扩展性
-
服务器可以遵循代码应请求可选约束,通过传输可执行代码(如 JavaScript)来自定义客户端的功能。
RESTful 应用程序使用 HTTP 请求进行所有四个 CRUD(创建、读取、更新和删除)操作。REST 不包括安全性、加密、会话管理等,但我们可以在 HTTP 的基础上构建它们。
让我们看一个典型的端点,我们用来读取产品:http://localhost:9000/product/123
。
只需使用简单的 HTTP GET 请求将 URL 发送到服务器。这里的“产品”是 URL 中的资源。在 REST 设计中有一个标准约定,即使用名词来标识资源。REST 可以处理更复杂的请求,比如:http://localhost:3000/products?category=1
。
如果需要,我们可以利用 HTTP 的 POST 方法在 POST 主体内发送长参数或二进制数据。
REST 响应格式
大多数情况下,服务器以 XML、逗号分隔值(CSV)或 JSON 格式响应 REST。选择取决于格式的优势:
-
XML 易于扩展和类型安全
-
CSV 非常紧凑
-
JSON 易于解析
REST 和 AJAX
我们使用异步 JavaScript 和 XML(AJAX)客户端技术来创建异步 Web 应用程序。AJAX 使用XMLHttpRequest
对象向服务器发送请求以动态更改网页。AJAX 和 REST 请求类似。
REST API 设计指南
我们需要采取下一步来创建适当的 REST API 吗?这个问题没有简单的答案,因为没有一个被广泛采用的标准适用于所有情况,我建议我们从微软 REST API 指南等知名来源获取答案,可在以下网址找到:github.com/Microsoft/api-guidelines
。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
HttpModule
到目前为止,我们只开发了应用程序的前端,因此它几乎没有用。我们需要一个地方来存储我们的产品和类别,以便以后可以获取它们。为此,我们将连接到一个服务器,该服务器将承载提供 JSON 的 RESTful API。
Angular 2 默认包含HttpModule
来组织一些低级方法来获取和发布我们的数据。
要在我们的项目中使用新的HttpModule
,我们必须将其导入为一个名为@angular/http
的单独的附加模块,作为 Angular npm 包的一部分,以单独的脚本文件的形式提供。我们在systemjs.config.js
文件中导入@angular/http
,配置SystemJS
在需要时加载该库:
var ngPackageNames = [
'common',
'compiler',
'core',
'forms',
**'http',**
'platform-browser',
'platform-browser-dynamic',
'router',
'router-deprecated',
'upgrade',
];
我们的应用程序将从应用程序的任何地方访问HttpModule
服务,因此我们应该通过将HttModule
添加到AppModule
的imports
列表中来注册它们。引导之后,所有HttpModule
服务都将对AppComponent
的根级别可用:
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
**import {HttpModule} from '@angular/http';**
/**
* Modules
*/
import {CartModule} from './cart/cart.module';
import {CategoryModule} from './category/category.module';
import {ProductModule} from './product/product.module';
/*
* Components
*/
import {AppComponent} from './app.component';
import {NavbarComponent} from './navbar/navbar.component';
import {FooterComponent} from './footer/footer.component';
import {WelcomeComponent} from './welcome/welcome.component';
import {CheckoutViewComponent} from
'./checkout/checkout-view.component';
/*
* Routing
*/
import {routing} from './app.routes';
@NgModule({
imports: [ **HttpModule**
, BrowserModule, FormsModule,
ReactiveFormsModule, routing, CartModule,
CategoryModule, ProductModule],
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent, CheckoutViewComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
内存中的 Web API
因为我们没有一个真正能处理我们请求的网络服务器,我们将使用模拟服务来模仿真实服务器的行为。这种方法具有以下优点:
-
它快速地创建 API 设计和新的端点。服务模拟使您能够使用测试驱动开发(TDD)。
-
它在团队成员之间共享 API。我们不会因为前端团队等待其他团队完成而停机。这种方法使得模拟的财务论点异常高。
-
它控制模拟响应和性能条件。我们可以使用模拟来创建概念验证,作为线框图或演示,因此它们非常具有成本效益。
它有一些我们应该知道的缺点:
-
我们必须做双倍的工作,有时这意味着相当多的工作
-
如果需要在某个地方部署它,它有部署约束
-
模拟代码容易出现错误
-
模拟只是对所模拟的东西的一种表示,它可能会误导真实的服务
内存 Web API 是angular-in-memory-web-api
库中的一个可选服务。它不是 Angular 2 的一部分,因此我们需要将其安装为单独的 npm 包,并在systemjs.config.js
文件中通过SystemJS
进行模块加载注册:
// map tells the System loader where to look for things
var map = {
'app': 'app',
'rxjs': 'node_modules/rxjs',
**'angular-in-memory-web-api':
'node_modules/angular-in-memory-web-api',**
'@angular': 'node_modules/@angular'
};
// packages tells the System loader how to load when no filename
// and/or no extension
var packages = {
'app': { main: 'main.js', defaultExtension: 'js' },
'rxjs': { defaultExtension: 'js' },
**'angular-in-memory-web-api':
{ main: 'index.js', defaultExtension: 'js' },**
};
接下来,我们需要创建一个实现InMemoryDbService
的InMemoryDataService
类,以创建一个内存数据库:
import {InMemoryDbService} from 'angular-in-memory-web-api';
import {Category} from './category/category.service';
import {Product} from './product/product.service';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
let categories: Category[] = [
{ id: '1', title: 'Bread & Bakery',
imageL: 'http://placehold.it/1110x480',
imageS: 'http://placehold.it/270x171',
desc: 'The best cupcakes, cookies, cakes, pies,
cheesecakes, fresh bread, biscotti, muffins,
bagels, fresh coffee and more.' },
{ id: '2', title: 'Takeaway',
imageL: 'http://placehold.it/1110x480',
imageS: 'http://placehold.it/270x171',
desc: 'It's consistently excellent, dishes are superb
and healthily cooked with high quality
ingredients.' },
// ...
];
let products: Product[] = [
// Bakery
{ id: '1', categoryId: '1', title: 'Baguette',
price: 1.5, isSpecial: false,
imageL: 'http://placehold.it/1110x480',
imageS: 'http://placehold.it/270x171',
desc: 'Great eaten fresh from oven. Used to make sub
sandwiches, etc.' },
{ id: '2', categoryId: '1', title: 'Croissants',
price: 0.5, isSpecial: true,
imageL: 'http://placehold.it/1110x480',
imageS: 'http://placehold.it/270x171',
desc: 'A croissant is a buttery, flaky,
viennoiserie-pastry named for its well-known
crescent shape.' },
//
];
return {
categories,
products
};
}
}
createDb
方法应该创建一个数据库对象哈希,其键是集合名称,其值是组对象的数组。可以安全地再次调用它,因为它返回具有新对象的新数组。这允许InMemoryBackendService
在不触及源数据的情况下改变数组和对象。我将这个文件中的数据集从ProductService
和CategoryService
中移动过来。
与HttModule
类似,我们正在将InMemoryWebApiModule
和InMemoryDataService
导入到AppModule
的imports
列表中。它们替换了内存 Web API 替代服务中的默认Http
客户端后端:
import {HttpModule} from '@angular/http';
// Imports for loading & configuring the in-memory web api
import { **InMemoryWebApiModule**
} from 'angular-in-memory-web-api';
import { **InMemoryDataService**
} from './in-memory-data.service';
And finally, we need to link the InMemoryWebApiModule to use the InMemoryDataService:
@NgModule({
imports: [HttpModule,
**InMemoryWebApiModule.forRoot(InMemoryDataService),**
BrowserModule, FormsModule, ReactiveFormsModule,
forRoot
方法在根应用程序模块中准备内存 Web API,以在引导时创建内存数据库。它有一个InMemoryBackendConfigArgs
类型的第二个参数,并保留InMemoryBackend
配置选项,例如延迟(以毫秒为单位)以模拟延迟,为此服务的主机等。
现在一切准备就绪,可以更改ProductService
和CategoryService
,以开始使用HTTP
服务。
HTTP 客户端
Angular HTTP 客户端通过 HTTP 协议的 AJAX 请求与服务器通信。我们项目的组件将永远不会直接与 HTTP 客户端服务通信。我们将数据访问委托给服务类。让我们按照以下所示更新ProductService
中的导入:
import {Injectable} from '@angular/core';
**import {Headers, Http, Response} from '@angular/http';**
import 'rxjs/add/operator/toPromise';
接下来,使用Http
服务获取产品:
getProducts(category?:string, search?:string):Promise<Product[]> {
let url = this.productsUrl;
if (category) {
url += `/?categoryId=${category}`;
} else if (search) {
url += `/?title=${search}`;
}
return this.http
.get(url)
.toPromise()
.then((response:Response) => response.json().data as Product[])
.catch(this.handleError);
}
如您所见,我们使用标准的 HTTP GET 请求来获取产品集。InMemoryWebApiModule
非常聪明地理解了请求 URL 中的查询参数。在这里,ProductGridComponent
利用ProductService
在网页上显示我们的产品网格:
@Component({
selector: 'db-product-grid',
templateUrl: 'app/product/product-grid.component.html'
})
export class ProductGridComponent implements OnInit {
**products: any = [];**
constructor(private route: ActivatedRoute,
private productService: ProductService,
private cartService: CartService) {}
ngOnInit(): void {
this.route
.queryParams
.subscribe(params => {
let category: string = params['category'];
let search: string = params['search'];
// Clear view before request
this.products = [];
// Return filtered data from getProducts function
this.productService.getProducts(category, search)
**.then((products: Product[]) => {**
// Transform products to appropriate data
// to display
this.products = this.transform(products);
});
});
}
//
}
这里的products
属性只是一个产品数组。我们使用简单的NgFor
指令来遍历它们:
<db-product-card ***ngFor="let row of products"**
[products]="row" (addToCart)="addToCart($event)">
</db-product-card>
由于类别数据的性质不同,CategoryService
中的源代码更改有些不同。类别集是静态的,所以我们不需要每次都获取它们,可以将它们保存在CategoryService
内的缓存中:
@Injectable()
export class CategoryService {
// URL to Categories web api
private categoriesUrl = 'app/categories';
// We keep categories in cache variable
**private categories: Category[] = [];**
constructor(private http: Http) {}
getCategories(): Promise<Category[]> {
return this.http
.get(this.categoriesUrl)
.toPromise()
.then((response: Response) => {
this.categories = response.json().data as Category[];
return this.categories;
})
.catch(this.handleError);
}
getCategory(id: string): Category {
for (let i = 0; i < this.categories.length; i++) {
if (this.categories[i].id === id) {
return this.categories[i];
}
}
return null;
}
private handleError(error: any): Promise<any> {
window.alert(`An error occurred: ${error}`);
return Promise.reject(error.message || error);
}
}
在getCategory
方法中,我们可以很容易地通过 ID 找到类别,因为我们只是从缓存中获取它。
HTTP 承诺
仔细看看我们如何从 HTTP GET 请求中返回数据。我们在Http
类的get
方法之后立即使用toPromise
方法:
getCategories(): Promise<Category[]> {
return this.http
.get(this.categoriesUrl)
**.toPromise()**
.then((response: Response) => {
this.categories = response.json().data as Category[];
return this.categories;
})
.catch(this.handleError);
}
那么,为什么我们需要这个方法,它到底是做什么的呢?
几乎所有的Http
服务方法都返回 RxJSObservable
。可观察对象是管理异步数据流的强大方式。要将 RxJSObservable
转换为Promise
,我们使用toPromise
操作符。它只是立即获取单个数据块并立即返回。在使用toPromise
操作符之前,我们需要隐式从 RxJS 导入它,因为该库非常庞大,我们应该只包含我们需要的功能:
import 'rxjs/add/operator/toPromise';
让我们谈谈Observable
以及为什么Http
在各处使用它们。
提示
您可以在chapter_9/2.ecommerce-promise
找到此源代码。
RxJS 库
RxJS是微软与许多开源开发人员合作积极开发的项目。它是一组组织为异步和基于事件的编程 API 的库。我们使用可观察对象来表示异步数据流。有许多操作符来查询和调度器来参数化它们的并发性。简而言之 - RxJS 是观察者和迭代器模式以及函数式编程的结合。
在使用之前,我们可以导入所有核心模块:
import * as Rx from 'rxjs/Rx';
如果你关心应用程序的大小,最好只导入必要的功能:
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
Observable.of(1,2,3).map(x => x * x); // Result: [1, 4, 9]
RxJs 非常庞大,我建议参考官方网站了解更多:reactivex.io
可观察对象与承诺
在我们的日子里,可观察对象是 JavaScript 版本 ES2016(ES7)的一个提议功能,因此我们使用 RxJS 作为填充库将它们引入项目,直到下一个新版本的 JavaScript 发布。Angular 2 对可观察对象有基本支持,我们使用 RxJS 来扩展这个功能。Promise 和可观察对象提供了帮助我们处理应用程序异步特性的抽象,有一些关键的区别:
-
可观察对象随时间发出多个值,与只能返回一个值或错误的 Promise 相反
-
可观察对象被视为数组,并允许我们使用操作符、类似集合的方法来操作值
-
可观察对象可以被取消
-
可观察对象可以使用重试操作符之一进行重试
因此,我们特别使用toPromise
将请求的数据流转换为单个值。我们真的需要吗?我对项目进行了一些更改,以向您展示在 Angular 2 应用程序中如何轻松使用可观察对象。只需查看ProductService
的修改版本:
getProducts(category?:string,search?:string):Observable<Product[]>{
let url = this.productsUrl;
if (category) {
url += `/?categoryId=${category}`;
} else if (search) {
url += `/?title=${search}`;
}
return this.http
.get(url)
.map((response:Response) => response.json().data as Product[])
.catch(this.handleError);
}
getProduct(id: string): Observable<Product> {
return this.http
.get(this.productsUrl + `/${id}`)
.map((response: Response) => response.json().data as Product)
.catch(this.handleError);
}
我们在上面的代码中使用了 RxJS 包中的几个转换操作符,所以不要忘记从包中导入它们。RxJS 中有许多操作符,可以帮助我们组织不同类型的转换:
-
map
操作符通过对每个项目应用函数来转换项目。 -
flatMap
,concatMap
和flatMapIterable
操作符将项目转换为可观察对象或可迭代对象,并将它们展平为一个。 -
switchMap
操作符将项目转换为可观察对象。最近转换的可观察对象发出的项目将被镜像。 -
scan
操作符依次对每个发出的项目应用函数,以仅发出连续的值。 -
groupBy
操作符帮助按键划分和组织可观察对象,以从原始对象中发出项目组。 -
buffer
操作符将发出的项目组合成包。它发出包而不是一次发出一个项目。 -
cast将源可观察对象中的所有项目转换为特定类型,然后重新发出它们。
RxJS 非常庞大,我建议从官方网站开始学习更多关于它的知识:github.com/Reactive-Extensions/RxJS
。
成功的请求返回Response
类的实例。响应数据以 JSON 字符串格式返回,因此我们必须通过调用Response
类的json
方法将该字符串解析为 JavaScript 对象。通常情况下,我们应该处理错误,因为我们必须为可能出错的情况做好准备。我们通过调用我们的类的handleError
方法来捕获错误。请记住,我们必须将错误转换为用户友好的消息,并通过Observable.throw
返回一个新的失败的 observable:
private handleError(error: any): Promise<any> {
window.alert(`An error occurred: ${error}`);
return Promise.reject(error.message || error);
}
在网页上显示 Observable 数据有两种不同的技术。第一种方法是组织 Observable 数据的订阅,如下所示:
ProductViewComponent:
@Component({
selector: 'db-product-view',
templateUrl: 'app/product/product-view.component.html'
})
export class ProductViewComponent implements OnInit {
**product: Product;**
constructor(private route: ActivatedRoute,
private productService: ProductService,
private cartService: CartService) { }
ngOnInit(): void {
this.route
.params
.subscribe(params => {
// Get the product id
let id: string = params['id'];
// Return the product from ProductService
**this.productService.getProduct(id)
.subscribe((product:Product) =>
this.product = product);**
// Return the cart item
this.cartItem = this.cartService.findItem(id);
});
}
}
}
我们订阅了在ProductService
中发生的所有变化,并立即将它们分配给product
属性,因此 Angular 将它们传递到模板中。
另一种方法是将 Observable 结果转发到模板中,如下所示:
ProductGridComponent:
@Component({
selector: 'db-product-grid',
templateUrl: 'app/product/product-grid.component.html'
})
export class ProductGridComponent implements OnInit {
**products: Observable<Product[]>;**
constructor(private route: ActivatedRoute,
private productService: ProductService,
private cartService: CartService) {}
ngOnInit(): void {
this.route
.queryParams
.debounceTime(300) // wait for 300ms pause in events
.subscribe(params => {
let category: string = params['category'];
let search: string = params['search'];
**this.products = this.productService
.getProducts(category, search)
.map(this.transform);**
});
}
//
}
然后,我们通过product
属性将 Observable 结果转发到模板,其中NgFor
中的async
管道处理订阅:
<db-product-card ***ngFor="let row of products | async"**
[products]="row" (addToCart)="addToCart($event)">
</db-product-card>
有时,我们可能需要开始一个请求,然后取消它,并在服务器对第一个请求做出响应之前进行不同的请求。使用 Promises 来实现这样的顺序是复杂的,所以让我们看看 Observables 如何帮助我们。
搜索标题中的 Observables
我们有一个按标题搜索产品的功能。用户输入标题,然后按下Go按钮从服务器请求数据。我们可以在这里改善用户体验,当用户在搜索框中输入标题时,我们将重复进行产品的 HTTP 请求,以标题进行过滤。看一下ProductSearchComponent
的更新标记:
<div class="card">
<div class="card-header">Quick Shop</div>
<input #search type="text" class="form-control"
placeholder="Search for..."
(keyup)="searchProduct(search.value)">
</div>
我们移除了Go按钮。输入元素从用户那里收集搜索标题,并在每次keyup
事件后调用searchProduct
方法。searchProduct
方法更新 URL 的查询参数:
@Component({
selector: 'db-product-search',
templateUrl: 'app/product/product-search.component.html'
})
export class ProductSearchComponent {
constructor(private router: Router) {}
**searchProduct(value: string) {
this.router.navigate(['/products'], {
queryParams: { search: value} });**
**}**
}
ProductGridComponent
监听route
中查询参数变化的流,并在到达productService
之前操作流:
ngOnInit(): void {
**this.route
.queryParams
.debounceTime(300) // wait for 300ms pause in events
.distinctUntilChanged() // only changed values pass
.subscribe(params => {**
let category: string = params['category'];
let search: string = params['search'];
this.products = this.productService
.getProducts(category, search)
.map(this.transform);
});
}
在前面的代码中,我们使用debounceTime
操作符等待用户停止输入至少 300 毫秒。只有改变的搜索数值通过distinctUntilChanged
操作符传递到服务端。之后,我们获取类别和搜索查询参数,并从productService
请求产品。
我们可以快速启动服务器并在浏览器中打开我们的 Web 应用程序,以检查所有内容是否按预期工作。从那时起,我们可以向同事或利益相关者展示我们的项目,作为我们将来开发中使用的概念验证。
接下来,我们需要一个真正的数据库和托管服务器来完成开发,并在真实环境中测试所有内容。让我们使用 Firebase 实时存储和同步我们的数据,并更快地提供 Web 内容。
提示
您可以在chapter_9/3.ecommerce-promise
找到此源代码。
Firebase 简介
Firebase 是一个实时的 NoSQL JSON 数据库。任何数据都可以通过 URL 访问。Firebase 包含不同平台的 SDK,比如 Web 的 JavaScript,IOS,Android 等。它包括身份验证内置在核心库中,因此我们可以通过 GitHub、Google、Twitter 和 Facebook 提供的 OAuth 直接从客户端快速验证用户。它还支持匿名和密码验证。Firebase 通过 Firebase 控制台或 CLI 提供静态资产的托管服务。Firebase 使用 Web 套接字实时更新所有连接的客户端上的数据。
如果您以前从未使用过 Firebase,您需要先注册一个帐户。打开您的 Web 浏览器,转到firebase.google.com/
。点击“登录”并使用您的 Google 帐户设置您的 Firebase 帐户。
创建 Firebase 项目
我们计划使用 Firebase SDK 库来访问和存储数据。但在此之前,我们需要将 Firebase 添加到我们的 Web 应用程序中。我们需要一个 Firebase 项目、Firebase SDK 和一个关于我们项目的一些细节的初始化代码片段。点击“转到控制台”或从以下地址打开“Firebase 控制台”:firebase.google.com/console
。
点击“创建新项目”按钮,添加项目名称和您的原籍国:
不到一分钟,我们将可以访问 Firebase 相关的数据库、身份验证、存储等。
安装 Firebase CLI 工具
我们将使用 Firebase CLI 工具从终端管理、查看和部署我们的项目到 Firebase。让我们打开终端,转到我们的项目,并运行以下命令:
**npm install -g firebase-tools**
安装后,我们将拥有一个全局可用的 Firebase 命令。现在,我们可以从终端登录到 Firebase。请记住,您必须已经设置了 Google 账户才能继续:
**firebase login**
这个命令建立了与远程 Firebase 账户的连接,并授予了我们对项目的访问权限:
如果您想知道 Firebase CLI 支持哪些命令,请访问官方网站:firebase.google.com/docs/cli/
。
初始化项目目录
我们将使用 Firebase CLI 执行许多琐碎的任务,比如运行本地服务器或部署。在使用之前,我们需要为包含firebase.json
文件的文件夹初始化项目目录。通常我们使用 Angular 项目的根文件夹作为 Firebase 项目目录。打开终端,导航到我们项目的根文件夹,并执行以下命令:
**firebase init**
这个命令将引导您设置项目目录。如果需要,您可以安全地再次运行此命令。
请回答“是”以回答问题:“配置为单页应用程序(将所有 URL 重写为/index.html)?” Firebase CLI 会在firebase.json
文件中创建rewrites
设置。我们使用重写是因为我们希望为多个 URL 显示相同的内容。这适用于我们的应用程序,因为我们使用默认的 HTML 5 pushState
策略配置了 Angular 组件路由。它生成了用户更容易理解的 URL,并保留了以后进行服务器端渲染的选项。
将数据导入 Firebase
在使用之前,我们需要将我们的数据导入 Firebase 数据库。打开 Firebase 控制台,找到您的项目,然后点击移动它:
在侧边栏上找到数据库菜单项并点击。这将把 Firebase 实时数据库实例带到舞台上。点击右侧的上下文菜单按钮,从下拉菜单中选择导入 JSON。我已经准备好了firebase.import.json
文件供导入,所以只需从项目的根文件夹中选择它,然后点击导入:
Firebase 实时数据库将数据存储为 JSON 对象。它看起来像是一个托管在云端的 JSON 树。与 SQL 数据库相反,这里没有表或记录。每个添加到 JSON 树中的数据都成为现有 JSON 结构中的一个节点,并带有关联的键。我们可以提供自己的键,例如category
或product
ID,或者 Firebase 可以在我们使用 POST 请求保存数据时为我们提供它们。
注意
键必须采用 UTF-8 编码,长度不能超过 768 字节。它们不能包含.,$,#,[,],/或 ASCII 控制字符,如 0-31 或 127。
Dream Bean 网站的数据结构非常简单,只包含两个实体,具有产品到类别的关系。Firebase 实时数据库支持嵌套数据,最多可以深达 32 级,最初的诱惑是将category
添加到product
中,但要小心,因为当您稍后检索数据时,Firebase 将返回产品及其所有子节点。此外,当我们尝试授予某人对节点的读取或写入访问权限时,会遇到麻烦。这里的最佳解决方案是对数据进行去规范化,以尽可能保持结构的扁平化。我们可以遵循以下建议:
-
将数据拆分为单独的路径
-
向数据添加索引或键
-
使用索引或键来获取关联数据
在开始阶段,我们故意将categoryId
添加到产品实体中,以便通过索引快速高效地获取数据:
Firebase 数据库规则
Firebase 始终为每个新数据库创建默认规则:
Firebase 实时数据库的规则非常灵活且基于表达式。我们可以使用类似 JavaScript 的语言来定义:
-
数据结构
-
数据索引
-
使用 Firebase 身份验证服务保护数据
默认情况下,数据库规则要求 Firebase 身份验证并仅授予完全读写权限给经过身份验证的用户,因此它对每个人都不可访问。我们将更改规则以使每个人都可以读取,但保持写入权限给经过身份验证的用户。规则可以以两种不同的方式进行配置。创建立即生效的最简单方法是使用 Firebase 控制台,因此让我们打开它,从侧边栏中选择数据库菜单,然后选择规则选项卡。您应该看到带有当前规则的文本区域。您可以手动更改它们,或者复制以下规则并粘贴到文本区域中:
{
"rules": {
**".read": true,**
".write": "auth != null"
}
}
单击发布以将新规则应用于数据库。管理数据库规则的另一种方法是创建一个特殊的 JSON 文件,这样当我们将项目部署到 Firebase 时,Firebase CLI 将使用这个文件。打开终端,进入我们的项目并运行以下命令:
**firebase init**
现在,选择数据库:部署 Firebase 实时数据库规则选项。对所有问题保持默认答案:
打开database.rules.json
并更新它:
{
"rules": {
**".read": true,**
".write": "auth != null"
}
}
现在,一旦数据被导入到数据库,就该将我们的项目连接到它了。
连接到 Firebase
为了组织通信,我们需要AngularFire2库来将 Firebase 实时观察者和身份验证与 Angular2 集成。
安装 AngularFire2 和 Firebase
首先,将 AngularFire2 和 Firebase SDK 库安装为 npm 模块:
**npm install -save angularfire2 firebase**
下一步是在本地安装 Typescript 2,因为 AngularFire2 依赖于它:
**npm install -save-dev [email protected]**
现在,使用这两个库更新systemjs.config.js
文件,因为它们需要与SystemJS
进行映射以进行模块加载:
// map tells the System loader where to look for things
var map = {
'app': 'app',
'rxjs': 'node_modules/rxjs',
'@angular': 'node_modules/@angular',
**'firebase': 'node_modules/firebase',
'angularfire2': 'node_modules/angularfire2'**
};
// packages tells the System loader how to load
// when no filename and/or no extension
var packages = {
'app': {main: 'main.js', defaultExtension: 'js'},
'rxjs': {defaultExtension: 'js'},
**'firebase': {main: 'firebase.js', defaultExtension: 'js'},
'angularfire2': {main: 'angularfire2.js', defaultExtension: 'js'}**
};
AngularFire2 和 Firebase 设置
在使用之前,我们需要设置 AngularFire2 模块和 Firebase 配置。打开app.module.ts
文件并导入AngularFireModule
。现在打开 Web 浏览器,导航到 Firebase 控制台,并选择您的项目(如果尚未打开)。接下来,单击将 Firebase 添加到您的应用程序链接:
Firebase 创建了初始化代码片段,我们将在我们的应用程序中使用:
选择初始化配置并复制到剪贴板。切换回我们的项目并粘贴,这样我们的代码将如下所示:
/*
* Angular Firebase
*/
import {AngularFireModule} from 'angularfire2';
**// Initialize Firebase
export var firebaseConfig = {
apiKey: "AIzaSyDDrc42huFLZqnG-pAg1Ly9VnFtVx3m-Cg",
authDomain: "ecommerce-a99fc.firebaseapp.com",
databaseURL: "https://ecommerce-a99fc.firebaseio.com",
storageBucket: "ecommerce-a99fc.appspot.com",
};**
@NgModule({
imports: [HttpModule,
**AngularFireModule.initializeApp(firebaseConfig),**
BrowserModule, FormsModule, ReactiveFormsModule,
routing, CartModule, CategoryModule, ProductModule],
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent, CheckoutViewComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
我们准备在我们的项目中使用 Firebase。
从 Firebase 获取类别
AngularFire2 通过FirebaseListObservable
将数据同步为列表,因此打开category.service.ts
文件并导入它:
import {Injectable} from '@angular/core';
**import {AngularFire, FirebaseListObservable} from 'angularfire2';**
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
//
@Injectable()
export class CategoryService {
// URL to Categories Firebase api
private categoriesUrl = 'categories';
// We keep categories in cache variable
private categories: Category[] = [];
constructor( **private af: AngularFire**
) {}
getCategories(): Observable<Category[]> {
**return this.af.database
.list(this.categoriesUrl)
.catch(this.handleError);**
}
getCategory(id: string): Category {
for (let i = 0; i < this.categories.length; i++) {
if (this.categories[i].id === id) {
return this.categories[i];
}
}
return null;
}
//
}
我们将AngularFire
服务注入到构造函数中。通过AngularFire.database
服务创建FirebaseListObservable
,我们在getCategories
方法中使用相对 URL 进行调用。
从 Firebase 获取产品
对于获取产品数据来说情况就不同了。仅有一个 URL 是不够的,我们需要使用查询参数。AngularFire.database
服务的列表方法有一个第二个参数对象,我们可以用它来指定查询参数:
import {Injectable} from '@angular/core';
import {AngularFire, FirebaseListObservable} from 'angularfire2';
**import {Observable} from 'rxjs/Observable';**
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/empty';
//...
export class ProductService {
// URL to Products web api
private productsUrl = 'products';
constructor( **private af: AngularFire**
) {}
getProducts(category?: string, search?: string):
Observable<Product[]> {
if (category || search) {
let query = <any>{};
if (category) {
query.orderByChild = 'categoryId';
query.equalTo = category;
} else {
query.orderByChild = 'title';
query.startAt = search.toUpperCase();
query.endAt = query.startAt + '\uf8ff';
}
return **this.af.database
.list(this.productsUrl, {
query: query
})**
.catch(this.handleError);
} else {
return Observable.empty();
}
}
getProduct(id: string): Observable<Product> {
return this.af.database
.object(this.productsUrl + `/${id}`)
.catch(this.handleError);
}
//...
}
我们使用 Firebase 实时数据库查询有选择地检索基于各种因素的数据。要为products
构建查询,我们首先指定如何使用其中一个排序函数对数据进行排序:
-
orderByChild
按子键检索排序的节点 -
orderByKey
按其键检索排序的节点 -
orderByValue
按其子节点的值检索排序的节点 -
orderByPriority
按优先级值检索排序的节点
指定子键的orderByChild
函数的结果将按以下顺序排序:
-
具有空值的子节点
-
具有 false 布尔值的子节点
-
具有 true 布尔值的子节点
-
具有按升序排序的数值的子节点
-
带有按字典顺序升序排序的字符串的子节点
-
具有按键名按升序排序的对象的子节点
注意
Firebase 数据库键只能是字符串。
orderByKey
函数的结果将按键名升序返回:
-
可以解析为 32 位整数的键的子节点首先出现,并按升序排序
-
具有字符串值键的子节点紧随其后,并按字典顺序升序排序
orderByValue
函数的结果将按其值排序。
注意
Firebase 数据库优先级值只能是数字和字符串。
orderByPriority
函数的结果将是子节点的排序,由其优先级和键决定如下:
-
没有优先级的子节点按键排序
-
具有数字的子节点按数字顺序排序
-
带有字符串的子节点按字典顺序排序
-
具有相同优先级的子节点按键排序
在决定检索到的数据应该如何排序之后,我们可以使用限制或范围方法进行复杂的查询:
-
limitToFirst
创建一个查询,限制为第一组子元素 -
limitToLast
创建一个查询,限制为最后一组子元素 -
startAt
创建一个具有特定起始点的查询 -
endAt
创建一个具有特定结束点的查询 -
equalTo
创建一个具有特定匹配值的查询
我们使用 limitToFirst
和 limitToLast
查询来设置 Firebase 将返回的最大子元素数量。使用 startAt
和 endAt
查询帮助我们在 JSON 树中选择任意的起始点和结束点。equalTo
查询根据精确匹配过滤数据。
当我们选择类别时,我们基于组合创建一个查询,orderByChild
和 equalTo
,因为我们知道 categoryId
的确切值来进行过滤:
let query = <any>{};
query.orderByChild = 'categoryId';
query.equalTo = category;
return this.af.database
.list(this.productsUrl, {
query: query
})
.catch(this.handleError);
当用户通过输入标题进行搜索时,我们使用 orderByChild
、startAt
和 endAt
的组合:
let query = <any>{};
query.orderByChild = 'title';
query.startAt = search.toUpperCase();
query.endAt = query.startAt + '\uf8ff';
return this.af.database
.list(this.productsUrl, {
query: query
})
.catch(this.handleError);
在前面的查询中使用的 \uf8ff
字符帮助我们创建一个技巧。它是 Unicode 范围内的一个非常高的值,因为它在大多数常规字符之后,所以查询匹配以用户输入值开头的所有值。
将应用程序部署到 Firebase
我们的应用只有静态内容,这意味着我们可以将其部署到 Firebase Hosting。我们可以用一条命令来做到这一点:
**firebase deploy**
Firebase CLI 将我们的 Web 应用部署到域名:https://<your-firebase-app>.firebaseapp.com
。
我们可以从 Firebase 控制台管理和回滚部署:
提示
您可以在 chapter_9/4.ecommerce-firebase
找到本章的源代码。
总结
在本章中,我们发现了数据持久性是什么,以及在客户端到服务器通信中它有多重要。我们从对 Web API 的简要介绍开始,然后深入研究 REST,以提醒主要原则。
我们看了 Angular 2 从 HttpModule
的离开,并讨论了如何使用它来组织客户端到服务器的通信。作为奖励,我们了解到我们可以使用内存 Web API 来创建概念验证、线框或演示。
可观察对象是 JavaScript 版本 ES2016(ES7)的一个提议特性,我们讨论了在 Angular 2 中使用 RxJS polyfill 库与可观察对象来帮助我们处理应用程序的异步特性。
Firebase 是一个实时的无 SQL JSON 数据库,可以通过 URL 访问任何数据。Firebase 包含不同平台的 SDK,比如 Web 的 JavaScript,IOS 和 Android。我们演示了如何将其用作应用程序的持久层。
在第十章中,高级 Angular 技术,我们将借助 Firebase 平台保护我们的数据。我们将学习如何安装ng2-bootstrap
,以及这将如何使我们更容易地创建指令。最后,我们将结束在之前章节中开始开发的项目。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
第十章:高级 Angular 技术
本章介绍了高级的 Angular 技术。我们将学习如何在客户端创建身份验证,并在 Firebase 上进行测试。我们将介绍 Webpack 来管理模块及其依赖关系,并将静态资产转换为构建捆绑包。我们将学习如何安装ng2-bootstrap
,以及它如何使读者能够更轻松地创建应用程序。最后,我们将完成在前几章中开始开发的项目。
在本章结束时,您将对以下内容有扎实的理解:
-
Webpack
-
Firebase 身份验证
-
ng2-bootstrap
组件 -
Angular CLI
-
JIT 与 AOT 编译
让我们开始:
-
打开终端,创建名为
ecommerce
的文件夹,并进入其中 -
将
chapter_10/1.ecommerce-seed
文件夹中的项目内容复制到新项目中。 -
运行以下脚本以安装
npm
模块:
**npm install**
- 使用以下命令启动 TypeScript 监视器和轻量级服务器:
**npm start**
此脚本打开 Web 浏览器并导航到项目的欢迎页面。
Webpack
到目前为止,我们已经使用 SystemJS 动态加载应用程序中的模块。现在我们将开始使用 Webpack 的方法来与 SystemJS 进行比较。随着本书的章节不断增加,我们的代码也在急剧增长,我们必须决定使用什么策略来加载塑造我们的 Web 应用程序的模块。Webpack 具有核心功能,并支持许多捆绑策略,可以直接使用或使用特定加载器和插件进行扩展。它遍历项目的所需语句,以生成我们定义的捆绑包。我们可以使用插件来执行特定任务,例如最小化,本地化等。以下是支持的功能的小列表:
-
热模块重新加载可以在不刷新的情况下立即更新 Angular 2 组件
-
通过延迟加载机制根据需要加载捆绑包
-
将应用程序代码分开成捆绑包
-
使用哈希在浏览器中高效地缓存 Web 应用程序的捆绑包
-
为捆绑包生成源映射,以便轻松调试捆绑包的最小化版本等
Webpack 迁移
当然,使用 Webpack 需要一些时间的承诺,但我们可以获得管理独立依赖项和性能改进的所有好处。我已经准备了详细的迁移计划,可以轻松地从 SystemJS 迁移到 Webpack。
安装 Webpack CLI
在使用之前,我们必须全局安装 Webpack。在终端中运行以下命令以使该命令可用:
**npm install -g webpack**
更新包
到目前为止,我们一直使用lite-server
来提供我们的应用程序。Webpack 有自己的webpack-dev-server
,一个小型的 Node.js Express 服务器,通过 Webpack 中间件来提供捆绑文件。webpack-dev-server
是一个单独的 npm 包,因此我们需要相应地更新package.json
中的devDependencies
:
"devDependencies": {
"typescript": "².0.0",
"typings": "¹.0.5",
"ts-loader": "⁰.8.2",
"webpack": "¹.12.2",
**"webpack-dev-server": "¹.12.1"**
}
webpack-dev-server
将提供当前目录中的文件,除非我们对其进行配置。根据以下代码定义,更改package.json
中的scripts
部分:
"scripts": {
"start": "webpack-dev-server",
"build": "webpack",
"postinstall": "typings install"
}
更新 TypeScript 配置
从一方面,Webpack 是一个模块捆绑器,它使用 CommonJS 或 AMD 格式来解析模块之间的依赖关系。从另一方面,Typescript 编译器支持几种模块代码生成。我们需要选择一个适用于两者的模块格式,所以我决定使用 CommonJS,因为它更方便。请打开tsconfig.json
文件并使用以下内容进行更新:
{
"compilerOptions": {
"target": "es5",
**"module": "commonjs",**
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": false
}
}
现在,是时候安装所有必要的npm
模块和typings
了。打开终端并运行以下命令:
**npm install**
创建 Webpack 配置文件
有两种配置 Webpack 的方法:
-
通过 CLI,当 Webpack 读取一个名为
webpack.config.js
的文件或我们将其指定为--config
选项 -
通过 Node.js API,我们将配置对象作为参数传递
第一种方法对我们来说更方便,所以让我们创建webpack.config.js
文件。Webpack 中的配置文件是一个 CommonJS 模块。我们将所有配置、加载器和与构建相关的其他特定信息放入这个文件中。每个配置文件必须具有两个主要属性:
-
一个或多个捆绑的入口点。
-
输出影响编译结果,告诉 Webpack 如何将编译后的文件写入磁盘。即使我们有多个入口点,也只有一个输出属性。
让我们将以下内容添加到webpack.config.js
文件中:
module.exports = {
entry: "./app/main",
output: {
path: __dirname,
filename: "./dist/bundle.js"
},
resolve: {
extensions: ['', '.js', '.ts']
},
devServer: {
historyApiFallback: true,
open: true,
watch: true,
inline: true,
colors: true,
port: 9000
},
module: {
loaders: [{
test: /\.ts/, loaders: ['ts-loader'],
exclude: /node_modules/
}]
}
};
我们将在 app 文件夹中使用main.js
文件作为入口点。我们计划将编译结果保存到dist
目录下的bundle.js
文件中。__dirname
是当前执行脚本所在的目录的名称。我添加了一个extensions
数组,Webpack 将用它来resolve
模块。
Webpack 只能原生处理 JavaScript,因此我们需要将ts-loader
添加到loaders
中以处理 TypeScript 文件。加载器允许我们在请求文件时预处理文件。加载器可以链接在一起,并且始终从右到左应用。我们可以在模块request
中指定加载器,但如果我们想要避免重复性,有一个更好的方法。只需将它们添加到 Webpack 配置文件中,并指定如何将它们应用于不同的文件类型。Webpack 使用加载器的test
属性来查找特定文件并相应地转换它们的内容。我们可以添加额外的条件来通过include
和exclude
条件属性找到文件。条件始终针对绝对路径进行测试,并且可以是以下之一:
-
正则表达式
-
一个带有路径的字符串
-
一个以路径作为参数并返回布尔结果的函数
-
上述任一组合的数组与
and
最后但同样重要的是开发服务器配置。我们可以通过 CLI 配置webpack-dev-server
,但更优雅的方法是将devServer
部分添加到webpack.config.js
文件中,我们可以在其中放置服务器需要的所有属性:
-
historyApiFallback
有助于使用 HTML5 历史 API -
open
标志只是在 Web 浏览器中打开后端服务器 URL -
watch
标志告诉运行时监视源文件,并在更改时重新编译捆绑包 -
inline
标志将webpack-dev-server
运行时嵌入到捆绑包中 -
colors
选项为输出添加一些颜色 -
port
包含后端服务器 URL 端口号 -
host
保留服务器 URL 主机
让我们测试 Webpack 如何使用以下命令构建项目:
**npm run build**
Webpack 应该在dist
文件夹内创建bundle.js
文件。
更新标记
接下来要做的是更新index.html
文件。我们需要删除所有属于 SystemJS 的代码并插入新代码:
<html>
<head>
<title>The Dream Bean Grocery Store</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<link rel="stylesheet"
href="node_modules/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" href="assets/ecommerce.css">
</head>
<body>
<my-app>Loading...</my-app>
</body>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/jquery/dist/jquery.min.js"></script>
<script
src="node_modules/tether/dist/js/tether.min.js"></script>
<script
src="node_modules/bootstrap/dist/js/bootstrap.js"></script>
**<script src="dist/bundle.js"></script>**
</html>
现在我们准备启动webpack-dev-server
服务器。打开终端并运行以下命令:
**npm start**
Webpack 打开 Web 浏览器并导航到以下 Web 地址:http://localhost:9000
。
提示
你可以在chapter_10/2.ecommerce-webpack
找到此源代码。
为生产准备我们的项目
我们可以使用项目,但最好进行一些更改以改进构建流程,以便我们可以准备部署项目结构。让我们创建一个源文件夹,并将我们的源代码、样式和模板文件放在里面。我想将所有资源都包含在包中,并向您展示 Webpack 通过插件的完全潜力使用。
三个主要条目
我们的项目代码仍然远未达到生产状态。我们在index.html
文件中留下了对 JavaScript 资源的引用,另外我们还需要考虑如何加载样式文件、Angular 2 和其他第三方模块。计划非常简单:我们需要将所有依赖项拆分为它们自己的包:
-
main
文件将保留对我们应用程序的引用 -
polyfill
文件包含所有必要的 polyfill 的引用 -
vendor
文件包含我们使用的所有供应商
这种方法的一个好处是,我们可以独立于我们的代码添加和删除 polyfills 和 vendors,因此我们不需要重新编译它。
Webpack 插件
Webpack 有一组内置插件。我们需要将它们添加到 Webpack 配置文件中的plugins
属性中。Webpack 将插件按组进行拆分,例如配置、输出、优化、依赖注入、本地化、调试等。您可以在这里找到内置的 Webpack 插件列表:webpack.github.io/docs/list-of-plugins.html
。
DefinePlugin
很明显,我们需要分开开发和生产配置,因为它们具有不同的全局常量和行为。这个插件允许我们在编译时创建全局常量,并在所有其他插件中使用:
const NODE_ENV = process.env.NODE_ENV;
//...
config.plugins = [
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(NODE_ENV)
}),
//...
];
现在process.env.NODE_ENV
在全局范围内可用,main.ts
文件中的以下代码将起作用:
if (process.env.NODE_ENV === 'production') {
enableProdMode();
}
ProvidePlugin
Bootstrap 模块需要包含在我们的应用程序中的第三方库,例如jQuery和Tether。我们将使用ProvidePlugin
自动加载这些模块,并使它们在 Bootstrap 中可用:
new ProvidePlugin({
jQuery: 'jquery',
$: 'jquery',
jquery: 'jquery',
"Tether": 'tether',
"window.Tether": "tether"
})
ProgressPlugin
我们使用这个插件在终端中显示编译进度。
LoaderOptionsPlugin
这个插件允许我们向一些特定的加载器添加选项:
new LoaderOptionsPlugin({
debug: false,
minimize: ENV_PRODUCTION
})
如果您开发自己的加载程序,可以将其激活为debug
模式,将其设置为true
。在我们的情况下,我们使用ENV_PRODUCTION
全局常量仅在生产中激活加载程序以最小化模式。
CommonsChunkPlugin
Webpack 有一个可选择的功能,可以帮助将代码拆分成块并按需加载。此外,我们需要定义拆分点,Webpack 会处理一切,如依赖关系、输出和运行时的东西:
new CommonsChunkPlugin({
name: ['vendor', 'polyfills'],
minChunks: Infinity
})
在我们的项目中,我明确地将供应商和 polyfills 文件隔离在它们的包中。minChunks
选项是模块在移动到公共块之前需要包含的最小块数。它可以包含一个数字、回调函数或Infinity
关键字。传递Infinity
会创建一个公共块,而不将模块移动到其中。
UglifyJsPlugin
这个插件最小化了所有块的 JavaScript 输出:
new UglifyJsPlugin({
comments: false,
compress: {
dead_code: true, // eslint-disable-line camelcase
screw_ie8: true, // eslint-disable-line camelcase
unused: true,
warnings: false
},
mangle: {
screw_ie8: true // eslint-disable-line camelcase
}
})
我们只在生产中使用它,它会删除注释,并压缩和混淆 JavaScript 文件中的变量名。
以下是我们项目中使用的几个第三方插件。它们都必须作为单独的 npm 模块安装:
extract-text-webpack-plugin
我在main.ts
文件中导入了我们项目的样式文件,如下所示:
/**
* Import styles
*/
import './assets/ecommerce.scss';
Webpack 将在bundle.js
文件中包含ecommerce.scss
文件的代码。这个解决方案在开发中运行得很完美,但我希望在生产中将样式保持为单独的文件,原因如下:
-
CSS 不是 JavaScript 包的一部分
-
CSS 包并行请求到 JavaScript 包
-
CSS 被单独缓存
-
由于代码和 DOM 操作减少,运行时更快
ExtractTextPlugin
必须添加到两个地方:
-
在加载程序中提取 CSS 文件
-
在插件中指定结果文件名和编译器的必要行为:
config.module.loaders.push({
test: /\.scss$/,
loader: ExtractTextPlugin
.extract('css?-autoprefixer!postcss!sass'),
include: path.resolve('src/assets/ecommerce.scss')
});
config.plugins.push(
new ExtractTextPlugin('styles.[contenthash].css')
)
编译后,我们将得到准备好用于生产的样式和源映射文件。
webpack-md5-hash 插件
每当 Webpack 将资源编译成包时,它会计算每个包的hash
总和,并将这个数字作为chunkhash
字符串用于文件名:
[chunkhash].[id].chunk.js
我更喜欢使用基于 Md5 的哈希生成器插件来替换我们项目中标准的 Webpack chunkhash
。
html-webpack-plugin
正如我们所说,Webpack 每次都会计算和生成捆绑包文件名的哈希值,因此我们必须以某种方式更新我们的index.html
文件中的信息。html-webpack-plugin
有助于使这个过程变得轻松,并快速地将所有 HTML 生成的捆绑包添加到应用程序中:
new HtmlWebpackPlugin({
chunkSortMode: 'dependency',
filename: 'index.html',
hash: false,
inject: 'body',
template: './src/index.html'
})
我使用源文件中的index.html
作为模板
,插件将使用它来生成dist
文件夹中的最终 HTML 文件。我们可以将生成的 JavaScript 捆绑包注入
到head
中,但通常我们会将它们添加到页面底部,就在body
标签关闭之前。该插件将按照依赖关系的顺序添加捆绑包。模板的格式基于嵌入式 JavaScript(EJS)模板系统,因此我们可以将值传递给插件,它将直接在 HTML 中检索这些值。
加载器
项目的最新版本使用了更多的加载器。加载器,就像模块一样,可以通过 npm 安装。我们使用加载器来教导 Webpack 新的功能。您可以在这里找到 Webpack 加载器的列表:webpack.github.io/docs/list-of-loaders.html
。
加载器命名约定和搜索顺序
通常,加载器的命名为<context-name>-loader
,以便在配置中通过它们的全名或简称轻松引用它们。您可以通过 Webpack 配置中的resolveLoader
的moduleTemplates
属性更改加载器的命名约定和优先搜索顺序:
["*-webpack-loader", "*-web-loader", "*-loader", "*"]
bootstrap-loader
bootstrap-loader
在 Webpack 捆绑包中加载 Bootstrap 样式和脚本。默认情况下,它预先配置为加载 Bootstrap 3。我们可以使用一个特殊的配置文件.bootstraprc
来调整加载过程的许多细节。bootstrapVersion
选项告诉加载器要加载哪个 Bootstrap 的主要版本。插件的作者建议使用默认配置作为起点,以防止不必要的升级或错误。您可以以 YAML 或 JSON 格式编写它。您可以在官方网站上找到完整的文档:github.com/shakacode/bootstrap-loader
。
css-loader
css-loader
可以将 CSS 文件作为捆绑包的一部分下载。它解析和解释imports
和url
语句作为require
:
url(image.png) => require("./image.png")
默认情况下,css-loader
会在模块系统指定的情况下最小化 CSS 文件。项目在 GitHub 上的网址是github.com/webpack/css-loader
。
file-loader
file-loader
将文件复制到输出文件夹并返回公共 URL:
var url = require("file!./file.png");
它处理文件内容以对 MD5 哈希求和,并将其用作结果文件的文件名:
/public-path/0dcbbaa701328a3c262cfd45869e351f.png
您可以通过查询参数(如name
,ext
,path
,hash
)配置自定义文件名模板:
require("file?name=js/[hash].script.[ext]!./javascript.js");
// => js/0dcbbaa701328a3c262cfd45869e351f.script.js
您可以在官方网站上找到文档:github.com/webpack/file-loader
。
postcss-loader
有超过 200 个 PostCSS 插件可用于解决全局 CSS 问题,使用未来的 CSS,或改善 CSS 文件的可读性。可以在官方插件列表中发现的网址是:postcss.parts/
。
postcss-loader
使用 PostCSS JS 插件来转换样式:
const autoprefixer = require('autoprefixer');
//...
config.module.loaders.push({
test: /\.scss$/,
loader:
ExtractTextPlugin.extract('css?-autoprefixer!postcss!sass'),
include: path.resolve('src/assets/ecommerce.scss')
});
您可以在这里找到有关如何使用它的完整文档:github.com/postcss/postcss-loader
。
raw-loader
此加载程序只是读取文件内容并将其作为字符串返回:
var fileContent = require("raw!./file.txt");
// => returns file.txt content as string
在官方网站上查看:github.com/webpack/raw-loader
。
resolve-url-loader
通常,我们将此文件与其他加载程序一起使用。它基于原始源文件解析url
语句中的相对路径:
var css = require('!css!resolve-url!./file.css');
在官方网站上查看链接:github.com/bholloway/resolve-url-loader
。
sass-loader
它加载 SASS 文件进行处理:
var css = require("!raw!sass!./file.scss");
// returns compiled css code from file.scss, resolves Sass imports
在这里找到完整的文档:github.com/jtangelder/sass-loader
。
style-loader
使用 style-loader 的帮助,您可以忘记手动将 CSS 文件添加到 HTML 文件中。它通过注入样式标签将 CSS 添加到 DOM 中。这对开发非常有用,但我建议您在构建生产环境时将 CSS 内容提取到单独的文件或捆绑包中。有关完整信息的网址是:github.com/webpack/style-loader
。
ts-loader
在使用此加载程序之前,您必须安装 TypeScript。它加载 TypeScript 文件并运行编译:
module.exports.module = {
loaders: [
// all files with a `.ts` or `.tsx` extension
// will be handled by `ts-loader`
{ test: /\.tsx?$/, loader: 'ts-loader' }
]
}
}
访问官方网站以获取更多信息:github.com/TypeStrong/ts-loader
。
url-loader
这个加载器类似于文件加载器,但如果文件大小小于限制,它只返回数据 URL:
require("url?limit=10000!./file.png");
// => DataUrl if "file.png" is smaller that 10kb
在这里找到更多信息:github.com/webpack/url-loader
。
提示
您可以在chapter_10/3.ecommerce-webpack-advanced
找到源代码。
用户身份验证
身份验证是向用户提供身份的过程。没有这个,我们无法为用户提供特定的服务来授予用户数据的权限。这对于安全敏感信息如信用卡详细信息来说是高风险的,因此我们需要安全地保存用户数据。
在应用程序中添加身份验证
Firebase 带来了简单的身份验证,因此我们可以将其与任何现有的登录服务器或清晰的基于云的解决方案集成。它支持来自 GitHub、Google、Twitter 和 Facebook 的第三方身份验证,以及通过电子邮件的内置身份验证。每个提供商都有他们自己的设置步骤。我将使用密码身份验证提供商,但您随时可以添加其他提供商。请在以下网页中找到官方文档:firebase.google.com/docs/auth
。
启用身份验证提供程序
当我们创建我们的 Web 应用程序实例时,Firebase 禁用了所有提供程序,因此我们需要在使用之前启用一个。打开 Web 浏览器,导航到 Firebase 控制台,并进入我们的应用程序。点击左侧边栏上的Auth菜单项:
点击设置登录方法进入。点击电子邮件/密码并在弹出对话框中激活提供程序:
以下OAuth 重定向域部分仅保留我们列入白名单以启动我们应用程序身份验证的域:
当我们注册我们的应用程序时,Firebase 添加了以下来源:
-
localhost
,以便我们可以在本地开发和测试 -
https://<your-project-id>.firebaseapp.com
,以便我们可以使用 Firebase 托管
如果您计划在其他授权来源中托管您的应用程序,您需要将它们全部添加到此处以启用来自其域的身份验证。
AngularFirebase2 身份验证
AngularFire2 身份验证可以在不配置的情况下工作,但最好在使用之前进行配置。最好的地方是app.module.ts
文件,我们在其中定义了AppModule
:
/*
* Angular Firebase
*/
import {AngularFireModule, AuthProviders, AuthMethods}
from 'angularfire2';
import * as firebase from 'firebase';
// Initialize Firebase
export var firebaseConfig = {
apiKey: "AIzaSyDDrc42huFLZqnG-pAg1Ly9VnFtVx3m-Cg",
authDomain: "ecommerce-a99fc.firebaseapp.com",
databaseURL: "https://ecommerce-a99fc.firebaseio.com",
storageBucket: "ecommerce-a99fc.appspot.com",
};
// Initialize Firebase Authentication
**const firebaseAuthConfig = {**
**provider: AuthProviders.Password,**
**method: AuthMethods.Redirect**
**}**
@NgModule({
imports: [HttpModule,
**AngularFireModule.initializeApp(firebaseConfig,**
**firebaseAuthConfig),**
BrowserModule, FormsModule, ReactiveFormsModule,
routing, CartModule, CategoryModule, ProductModule],
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent, CheckoutViewComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
在firebaseAuthConfig
中,我们指示使用密码身份验证,Firebase 将重定向到登录页面进行登录。创建auth
文件夹和其中的auth.service.ts
文件。
身份验证服务
AuthService
是一个适配器类,它隐藏了 Firebase 如何对用户进行身份验证的实现细节。它使用FirebaseAuth
类为我们完成所有工作:
constructor(public auth$: FirebaseAuth) {
auth$.subscribe((state: FirebaseAuthState) => {
this.authState = state;
});
}
我们应用程序的一些组件需要实时了解用户的身份验证状态,因此我订阅了从FirebaseAuth
服务监听FirebaseAuthState
事件。我们的类包括两个主要方法来管理用户的身份验证:
signIn(email: string, password: string):
firebase.Promise<FirebaseAuthState> {
return this.auth$.login({
email: email,
password: password
}, {
provider: AuthProviders.Password,
method: AuthMethods.Password,
});
}
signOut(): void {
this.auth$.logout();
}
signIn
方法期望用户凭据,如电子邮件和密码,以登录并在 Firebase promise 中返回FirebaseAuthState
。signOut
帮助我们从应用程序中注销
SignInComponent
创建sign-in.component.ts
文件来保存SignInComponent的代码。这是一个表单,用户在其中输入他/她的凭据并点击登录以将电子邮件和密码传递到身份验证服务。它监听 Firebase 返回的响应以将用户重定向到欢迎页面:
onSubmit(values:any): void {
this.submitted = true;
this.auth.signIn(values.email, values.password)
.then(() => this.postSignIn())
.catch((error) => {
this.error = 'Username or password is incorrect';
this.submitted = false;
});
}
private postSignIn(): void {
this.router.navigate(['/welcome']);
}
该代码在电子邮件和密码组合不正确时显示错误消息。为了保护应用程序的路由免受未经授权的用户的访问,我们将使用名为 Guards 的 Angular 2 功能。
Angular Guards
Angular 2 路由器提供了一个名为Guard的功能,它返回Observable<boolean>
、Promise<boolean>
或boolean
来允许激活、停用或加载组件。它可以注册为依赖注入的函数或类。如果我们需要依赖注入功能,则类注册具有好处。要将 Guard 注册为类,我们需要实现 Angular 提供的接口之一。
有四个 Angular Guards 接口:
-
CanActivate
Guard 检查路由是否可以被激活 -
CanActivateChild
Guard 检查特定路由的子路由是否可以被激活 -
CanDeactivate
Guard 检查路由是否可以被停用 -
CanLoad
Guard 检查模块是否可以加载
因为我们的守卫代码将使用身份验证服务,所以我创建了auth.guard.ts
文件和AuthGuard
作为实现CanActivate
接口的类:
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router)
{ }
canActivate(): Observable<boolean>|boolean {
return this.auth.auth$.map((authState: FirebaseAuthState)=>{
if (authState) {
return true;
} else {
this.router.navigateByUrl('/login');
return false;
}
}).first();
}
}
Angular 路由器将调用接口的canActivate
方法来决定是否可以通过监听FirebaseAuthState
事件来激活路由。如果用户成功验证,该方法返回true
,然后将激活路由中注册的组件。如果没有,则返回false
并将用户重定向到登录页面。
在 Navbar 中注销
我认为如果我们的用户有选择从 Web 应用程序注销的选项是一个好主意。我们应该将身份验证服务注入Navbar
组件,并创建注销方法来调用它以注销用户:
export class NavbarComponent {
constructor(private authService: AuthService,
private router: Router) { }
logout() {
this.authService.signOut();
this.router.navigateByUrl("/login");
}
}
正如您所记得的,我们在AuthService
中有一个authenticated
属性,它在用户登录或退出应用程序时更改状态。我们将使用它来管理标记中Sign Out和Cart组件的外观:
<div class="collapse navbar-toggleable-xs"
id="exCollapsingNavbar">
<a class="navbar-brand" href="">Dream Bean</a>
<div class="nav navbar-nav">
<a class="nav-item nav-link" (click)="logout()"
***ngIf="authService.authenticated"**
>Sign out</a>
</div>
<db-cart-menu ***ngIf="authService.authenticated"**
></db-cart-menu>
</div>
当用户登录应用程序时,这两个组件变得可见。
更新 Firebase 数据库规则
现在,当我们在客户端保护我们的应用程序时,我们必须更改 Firebase 数据库规则,以便只有经过身份验证的用户才能访问数据:
{
"rules": {
**".read": "auth != null",**
".write": "auth != null",
"products": {
".indexOn": ["categoryId", "title"]
}
}
}
玩的时间
打开终端并运行以下命令进行生产构建:
**npm run build**
成功完成后,我们可以将应用程序部署到 Firebase 托管上:
**firebase login**
**firebase deploy**
打开 Web 浏览器并导航到 Web 应用程序:https://<your-project-id>.firebaseapp.com
。
任何组合的电子邮件和密码都会在屏幕上带来异常授权消息:
我们的应用程序没有注册表单,因此我们可以通过控制台直接将测试用户添加到 Firebase 中。打开 Firebase 控制台并导航到我们的应用程序。点击左侧边栏上的Auth链接以打开Authentication页面:
点击添加用户以打开弹出对话框:
填写空白字段并点击添加用户。现在我们有了注册的测试用户,我们可以回到我们的应用程序并使用电子邮件和密码成功登录。
提示
您可以在chapter_10/ 4.ecommerce-firebase-auth
找到此源代码。
ng2-bootstrap
有一个ng2-bootstrap
库,它不依赖于 jQuery 和 Bootstrap JavaScript 文件。它具有一组用于 Bootstrap 版本 3 和 4 的原生 Angular 2 指令,因此尝试它不会花费任何费用。有关如何使用不同组件的更多信息,请访问官方网站:valor-software.com/ng2-bootstrap/index-bs4.html
。
首先,我们将清除项目中基于 bootstrap 的模块:
**npm uninstall -save bootstrap bootstrap-loader jquery tether**
然后,从vendors.js
文件中删除 Bootstrap 4 模块:
// Bootstrap 4
import "jquery";
import "bootstrap-loader";
现在,我们已经准备好安装,所以打开终端并安装ng2-bootstrap
:
npm install ng2-bootstrap --save
ng2-bootstrap
模块必须导入到AppModule
中。安装后,ng2-bootstrap
支持 Bootstrap 版本 3,因此在使用之前必须将主题设置为 Bootstrap 4:
**import {Ng2BootstrapModule, Ng2BootstrapConfig, Ng2BootstrapTheme}**
**from 'ng2-bootstrap';**
**Ng2BootstrapConfig.theme = Ng2BootstrapTheme.BS4;**
@NgModule({
imports: [
AngularFireModule.initializeApp(firebaseConfig,
firebaseAuthConfig), AuthModule,
BrowserModule, FormsModule, ReactiveFormsModule,
routing, CartModule, CategoryModule, ProductModule,
**Ng2BootstrapModule],**
declarations: [AppComponent, NavbarComponent, FooterComponent,
WelcomeComponent, CheckoutViewComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
最后,我们将在index.html
中添加引用 CDN 上的 Bootstrap 4 CSS 链接:
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.2/css/bootstrap.min.css" crossorigin="anonymous">
迁移计划已经很清楚:找到所有可以使用ng2-bootstrap
的地方,并将基于 Bootstrap 4 的代码更改为相应的组件。
更新欢迎页面上的幻灯片播放
在欢迎页面上,我们使用幻灯片组件来循环播放图片作为轮播图。ng2-bootstrap
有一个原生组件轮播图可以做同样的事情。使用新组件的主要好处是编写更少的标记代码:
<carousel>
<slide *ngFor="let category of slideCategories; let i=index"
[active]="category.active">
<db-category-slide [category]="category"></db-category-slide>
</slide>
</carousel>
轮播图具有以下属性,我们可以用来管理幻灯片播放:
-
interval
属性是以毫秒为单位延迟自动循环项目之间的时间量。默认情况下,此数量等于 5,000。如果将其更改为false
,则轮播图将不会自动循环。 -
noTransition
属性将禁用幻灯片之间的过渡。默认情况下为false
。 -
noPause
属性将禁用鼠标悬停时的轮播暂停。默认情况下为false
。 -
noWrap
属性将阻止连续循环。默认情况下设置为false
。
更新导航栏中的下拉购物车
我们在导航栏中使用下拉组件来显示用户的购物车信息。让我们从ng2-bootstrap
中更新此组件。
首先,我们需要将DropdownModule
导入CartModule
:
**import {DropdownModule} from 'ng2-bootstrap';**
@NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule,
RouterModule, DropdownModule],
declarations: [CartItemCountComponent, CartMenuComponent,
CartViewComponent],
exports: [CartMenuComponent, CartViewComponent,
CartItemCountComponent],
providers: [CartService]
})
export class CartModule {}
之后,打开cart-menu.component.html
并更新包裹购物车内容的标记代码:
<div class="nav navbar-nav float-xs-right">
<div class="nav-item">
<div dropdown>
<a href class="nav-link" id="cart-dropdown"
dropdownToggle>
Cart: {{cart.amount | currency:'USD':true:
'1.2-2'}} ({{cart.count}} items)
</a>
<div class="dropdown-menu dropdown-menu-right"
dropdownMenu aria-labelledby="cart-dropdown">
<!-- cart content -->
</div>
</div>
</div>
</div>
任何基于ng2-bootstrap
的下拉解决方案都应包括以下组件:
-
带有
dropdown
指令标记的下拉根元素 -
一个可选的标记为
dropdownToggle
的切换元素 -
一个包含用
dropdownMenu
标记的内容的下拉菜单
我们可以使用isOpen
属性来管理下拉菜单的打开状态。
提示
你可以在chapter_10/5.ecommerce-ng2-bootstrap
找到这个项目的源代码。
Angular CLI
现在您已经了解了如何使用 SystemJS 或 Webpack 创建 Web 应用程序,并且了解到这不是一个简单的过程。请记住,属于不同模块加载程序的所有配置都太复杂了,有时您会花费太多时间在例行任务上。到目前为止,我们已经自己处理了所有事情,但是值得添加 Angular CLI 到脚手架中来处理繁琐的任务和构建 Angular 应用程序。
以下命令将安装 Angular CLI:
**npm install -g angular-cli**
运行以下命令以获取使用命令:
**ng --help**
我们将使用以下命令创建新的 Angular 项目:
**ng new ecommerce**
几分钟后,您将准备好使用安装的 NPM 模块启动 Angular 2 项目。进入项目文件夹并启动开发服务器:
**cd ecommerce**
**ng serve**
现在在 Web 浏览器中打开localhost:4200
。当生成源代码和文件夹时,Angular CLI 遵循推荐的应用程序结构和样式指南。我们在开发项目时也遵循了相同的原则,以便可以顺利地将代码从上一个项目移动到新项目中。
停止服务器并安装以下模块:
**npm i angularfire2 firebase @types/request ng2-bootstrap**
在项目的根目录中找到angular-cli.json
文件,并在styles
和scripts
中进行以下更改,以添加对我们的ecommerce.scss
样式和ng2-bootstrap
包的引用:
"styles": [
"assets/ecommerce.scss"
],
"scripts": [
"../node_modules/ng2-bootstrap/bundles/ng2-bootstrap.umd.js"
],
从上一个项目中复制database.rules.json
,firebase.json
和firebase.import.json
文件,这样我们就可以使用 Firebase CLI 将项目部署到主机。
删除src/app
文件夹中的所有文件,除了index.ts
文件。将上一个项目的src/app
文件夹中的所有文件复制到新项目中。
现在运行开发服务器,并在 Web 浏览器中打开或刷新localhost:4200
,看看我们的项目是如何重新上线的。
从现在开始,您可以生成新的组件、路由、服务和管道,只需简单的命令,还可以运行测试和构建。请查看 Angular CLI 的官方网站以获取更多信息:cli.angular.io
。
即时编译
我最担心的是我们应用的大小。看看 Webpack 在构建块文件时通常打印的统计数据:
捆绑文件超过 4 兆字节。为什么应用这么庞大?
当应用程序在浏览器中加载时,Angular 会使用即时(JIT)编译器在运行时对其进行编译。该编译器是我们在引导应用程序时加载的代码的一部分。请注意,我们在基于 SystemJS 或 Webpack 模块加载器构建项目时使用了这种方法。该解决方案具有以下缺点:
-
性能惩罚,因为代码总是在使用之前编译
-
渲染惩罚,因为每个视图在显示之前都要编译
-
大小惩罚,因为代码包含 JIT 编译
-
代码质量惩罚,因为 JIT 编译在运行时发现错误
如果我们开始使用提前编译(AOT)编译,我们可以解决许多这些问题。
AOT 编译
在 AOT 方法中,我们预先编译所有资源,因此无需将编译器下载到 Web 浏览器中。这有以下好处:
-
较小的应用程序大小,因为代码不包括编译器
-
炽热的渲染,因为浏览器代码和模板是预编译的
-
更少的资源请求,因为样式和模板被编译到代码中
-
更好的模板绑定错误检测在编译时
-
更少的注入攻击可能性,因为 Web 浏览器不需要评估预编译的模板和组件
那么,我们如何使用这种奇妙的方法呢?答案很简单:使用 Angular CLI。它支持 AOT 开箱即用,因此我们只需要将以下命令添加到package.json
的脚本中:
"scripts": {
"start": "ng serve",
"lint": "tslint "src/**/*.ts"",
"test": "ng test",
"pree2e": "webdriver-manager update",
"e2e": "protractor",
**"prod:build": "ng build --prod --aot",**
**"prod:serve": "ng serve --prod --aot"**
},
保存更改,打开终端并运行 AOT 构建:
**npm run prod:build**
在gzip
压缩后,捆绑文件的大小小于 400 千字节:
您可以使用 AOT 启动开发服务器,并检查应用程序在 Web 浏览器中的最终大小:
**npm run prod:serve**
提示
您可以在chapter_10/6.ecommerce-aot-compilation
找到源代码。
读累了记得休息一会哦~
公众号:古德猫宁李
-
电子书搜索下载
-
书单分享
-
书友学习交流
网站:沉金书屋 https://www.chenjin5.com
-
电子书搜索下载
-
电子书打包资源分享
-
学习资源分享
总结
在本章中,我们学习了如何为帐户管理和身份验证创建客户端解决方案,并在 Firebase 上进行测试。我们介绍了 Webpack,并将我们的应用程序从 SystemJS 迁移过来。我们知道它会遍历项目的必需语句,以生成我们定义的捆绑包。后来,我们重新发现了我们的项目,并进行了更多的更改以使用 Webpack 插件。现在我们知道 Webpack 有一组内置插件,我们可以按组进行拆分,例如配置、输出、优化、依赖注入、本地化、调试等。
我们了解到,身份验证是向用户提供身份的过程,没有它,我们无法为用户提供特定的服务来授予用户数据的权限。我们了解到,Firebase 提供了与任何现有登录服务器的简单身份验证,或者使用清晰的基于云的解决方案。它支持来自 GitHub、Google、Twitter 和 Facebook 的第三方身份验证,以及通过电子邮件的内置身份验证。现在我们知道 AngularFire2 身份验证可以在没有配置的情况下工作。
ng2-bootstrap
库具有一组用于 Bootstrap 版本 3 和 4 的原生 Angular 2 指令,并且不依赖于 jQuery 和 Bootstrap JavaScript 文件。我们很快将其集成到我们的项目中。
Angular CLI 有助于轻松创建应用程序,开箱即用地生成组件、路由、服务和管道。它支持提前编译,以显著减小应用程序的大小,并提高性能和安全性。
最后,我们通过构建我们在前几章中开始开发的项目来结束了。
标签:Web,product,Angular,组件,Angular2,使用,Bootstrap4,import,我们 From: https://www.cnblogs.com/apachecn/p/18199192