Angular9 高级教程(二)
五、JavaScript 和 TypeScript:第一部分
在这一章中,我快速浏览了 JavaScript 语言应用于 Angular 开发的最重要的基本特性。我没有足够的空间来完整地描述 JavaScript,所以我把重点放在了你需要快速掌握并遵循本书中的例子的要点上。在第六章中,我描述了一些你需要的更高级的 JavaScript 特性和一些 TypeScript 提供的额外特性。
JavaScript 语言是通过定义新特性的标准流程来管理的。JavaScript 的每一个新版本都拓宽了 JavaScript 开发人员可用的特性,并使 JavaScript 的使用与更传统的语言(如 C#或 Java)更加一致。
现代浏览器会自我更新,这意味着,例如,谷歌 Chrome 用户很可能拥有一个最新版本的浏览器,该浏览器至少实现了一些最新的 JavaScript 特性。可悲的是,不自我更新的旧浏览器仍在广泛使用,这意味着你不能依赖于应用中可用的现代功能。
有两种方法可以解决这个问题。第一种方法是只使用核心 JavaScript 特性,这些特性可以在应用所面向的浏览器中找到。第二种方法是使用编译器处理您的 JavaScript 文件,并将它们转换成可以在旧浏览器上运行的代码。这是 Angular 发展中使用的第二种方法,也是我在本章中描述的。表 5-1 总结了本章内容。
表 5-1。
章节总结
|问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 创建 JavaScript 功能 | 使用 JavaScript 语句 | five |
| 创建根据命令执行的语句组 | usb 功能 | 6, 7, 10–12 |
| 定义可以处理比参数更多或更少的参数的函数 | 使用默认或 rest 参数 | 8, 9 |
| 更简洁地表达功能 | 使用箭头功能 | Thirteen |
| 存储值和对象以备后用 | 使用let或var关键字声明变量 | 14–16 |
| 存储基本数据值 | 使用 JavaScript 基本类型 | 17–20 |
| 控制 JavaScript 代码流 | 使用条件语句 | Twenty-one |
| 确定两个对象或值是否相同 | 使用质量和标识运算符 | 22–23 |
| 显式转换类型 | 使用to<type>方法 | 24–26 |
| 将相关的对象或值按顺序存储在一起 | 使用数组 | 27–33 |
准备示例项目
为了创建本章的示例项目,打开一个新的命令提示符,导航到一个方便的位置,并运行清单 5-1 中所示的命令。
Tip
你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。
ng new JavaScriptPrimer --routing false --style css --skip-git --skip-tests
Listing 5-1.Creating the Example Project
该命令创建一个名为JavaScriptPrimer的项目,用于 Angular 开发。在这一章中我不做任何 Angular 的开发,但是我将使用 Angular 开发工具作为一种方便的方式来演示不同的 JavaScript 和 TypeScript 特性。为了做好准备,我用清单 5-2 中所示的一条 JavaScript 语句替换了src文件夹中的main.ts文件的内容。
Tip
注意文件的扩展名。尽管本章只使用 JavaScript 特性,但它依赖 TypeScript 编译器将它们转换成可以在任何浏览器中运行的代码。这意味着必须使用.ts文件,然后允许 TypeScript 编译器创建相应的.js文件,供浏览器使用。
console.log("Hello");
Listing 5-2.Replacing the Contents of the main.ts File in the src Folder
运行JavaScriptPrimer文件夹中清单 5-3 所示的命令,启动 Angular development 编译器和 HTTP 服务器。
ng serve --open
Listing 5-3.Starting the Development Tools
一个新的浏览器标签或窗口将会打开,但将会是空的,如图 5-1 所示,因为我替换了main.ts文件的内容,浏览器没有任何东西可以显示。
图 5-1。
运行示例应用
本章中示例的所有结果都显示在浏览器的 JavaScript 控制台中。打开浏览器的 F12 开发工具(之所以这样叫是因为它们通常是通过按 F12 键打开的)并查看控制台选项卡,如图 5-2 所示。
图 5-2。
谷歌浏览器 JavaScript 控制台
JavaScript 控制台显示了调用清单 5-2 中的console.log函数的结果。我将只显示文本结果,而不是显示每个示例的浏览器 JavaScript 控制台的屏幕截图,如下所示:
Hello
了解基本工作流程
将单词 Hello 写入 JavaScript 控制台是一个简单的例子,但是在幕后还有很多事情要做。为了了解开发工作流程,将清单 5-4 中所示的语句添加到main.ts文件中。
console.log("Hello");
console.log("Apples");
Listing 5-4.Adding a Statement in the main.ts File in the src Folder
当保存对main.ts文件的更改时,会发生以下过程:
-
TypeScript 编译器将检测到对
main.ts文件的更改,并编译它以生成一个可以在任何浏览器中运行的新的main.js文件。生成的代码与编译器生成的其他 JavaScript 代码组合成一个名为包的文件。 -
开发 HTTP 服务器通知浏览器重新加载 HTML 文档。
-
浏览器重新加载 HTML 文档,并开始处理其中包含的元素。它加载由 HTML 文档中的
script元素指定的 JavaScript 文件,包括一个指定包含来自main.ts文件的语句的包文件的文件。 -
浏览器执行最初在
main.ts文件中的语句,将两条消息写到浏览器的 JavaScript 控制台。
总体结果是,您将看到显示以下消息:
Hello
Apples
对于一个简单的应用来说,这似乎是一个很大的步骤,但是这个过程允许使用 TypeScript 特性,并自动检测更改、运行编译器和更新浏览器。
使用语句
基本的 JavaScript 构建块是语句。每条语句代表一条命令,语句通常以分号(;)结束。分号是可选的,但是使用分号会使代码更容易阅读,并且允许在一行中有多个语句。在清单 5-5 中,我在 JavaScript 文件中添加了一对语句。
console.log("Hello");
console.log("Apples");
console.log("This is a statement");
console.log("This is also a statement");
Listing 5-5.Adding JavaScript Statements in the main.ts File in the src Folder
浏览器依次执行每条语句。在本例中,所有语句都只是将消息写入控制台。结果如下:
Hello
Apples
This is a statement
This is also a statement
定义和使用函数
当浏览器收到 JavaScript 代码时,它会按照定义的顺序执行其中包含的语句。这就是上一个示例中发生的情况。浏览器加载器加载了main.js文件,其中包含的语句被逐一执行,都向控制台写了一条消息。
你也可以将语句打包成一个函数,直到浏览器遇到一个调用该函数的语句时才会执行,如清单 5-6 所示。
let myFunc = function () {
console.log("This is a statement");
};
myFunc();
Listing 5-6.Defining a JavaScript Function in the main.ts File in the src Folder
定义一个函数很简单:使用let关键字,后跟您想要给函数起的名字,再加上等号(=)和function关键字,再加上括号((和)字符)。您希望函数包含的语句用大括号括起来(字符{和})。
在清单中,我使用了名称myFunc,该函数包含一个向 JavaScript 控制台写入消息的语句。在浏览器到达另一个调用myFunc函数的语句之前,函数中的语句不会被执行,如下所示:
...
myFunc();
...
在函数中执行语句会产生以下输出:
This is a statement
除了演示函数是如何定义的,这个例子没有什么特别的用处,因为函数在定义后会立即被调用。当响应某种变化或事件(如用户交互)而调用函数时,函数会更有用。
用参数定义函数
JavaScript 允许您为函数定义参数,如清单 5-7 所示。
let myFunc = function(name, weather) {
console.log("Hello " + name + ".");
console.log("It is " + weather + " today");
};
myFunc("Adam", "sunny");
Listing 5-7.Defining Functions with Parameters in the main.ts File in the src Folder
我给myFunc函数添加了两个参数,称为name和weather。JavaScript 是一种动态类型语言,这意味着在定义函数时不必声明参数的数据类型。当我在本章后面讲述 JavaScript 变量时,我会回到动态类型。要调用带参数的函数,需要在调用函数时提供值作为参数,如下所示:
...
myFunc("Adam", "sunny");
...
该清单的结果如下:
Hello Adam.
It is sunny today
使用默认和 Rest 参数
调用函数时提供的参数数量不需要与函数中的参数数量相匹配。如果调用函数时使用的参数少于它拥有的参数,那么任何没有提供值的参数的值都是undefined,这是一个特殊的 JavaScript 值。如果调用函数时使用的参数多于实际参数,那么多余的参数将被忽略。
这样做的结果是,您不能创建两个具有相同名称和不同参数的函数,并期望 JavaScript 根据您在调用函数时提供的参数来区分它们。这被称为多态性,尽管它在 Java 和 C#等语言中受支持,但在 JavaScript 中不可用。相反,如果您定义了两个同名的函数,那么第二个定义将替换第一个定义。
有两种方法可以修改函数,以响应函数定义的参数数量和用于调用函数的参数数量之间的不匹配。默认参数处理实参比参数少的情况,它们允许你为没有实参的参数提供默认值,如清单 5-8 所示。
let myFunc = function (name, weather = "raining") {
console.log("Hello " + name + ".");
console.log("It is " + weather + " today");
};
myFunc("Adam");
Listing 5-8.Using a Default Parameter in the main.ts File in the src Folder
函数中的weather参数已被赋予默认值raining,如果仅使用一个参数调用该函数,将使用该值,产生以下结果:
Hello Adam.
It is raining today
Rest 参数用于在用附加参数调用函数时捕获任何附加参数,如清单 5-9 所示。
let myFunc = function (name, weather, ...extraArgs) {
console.log("Hello " + name + ".");
console.log("It is " + weather + " today");
for (let i = 0; i < extraArgs.length; i++) {
console.log("Extra Arg: " + extraArgs[i]);
}
};
myFunc("Adam", "sunny", "one", "two", "three");
Listing 5-9.Using a Rest Parameter in the main.ts File in the src Folder
rest 参数必须是函数定义的最后一个参数,其名称以省略号为前缀(三个句点,...)。rest 参数是一个数组,任何额外的参数都将被赋给它。在清单中,该函数将每个额外的参数打印到控制台,产生以下结果:
Hello Adam.
It is sunny today
Extra Arg: one
Extra Arg: two
Extra Arg: three
定义返回结果的函数
您可以使用return关键字从函数中返回结果。清单 5-10 显示了一个返回结果的函数。
let myFunc = function(name) {
return ("Hello " + name + ".");
};
console.log(myFunc("Adam"));
Listing 5-10.Returning a Result from a Function in the main.ts File in the src Folder
这个函数定义了一个参数,并用它来产生一个结果。我调用函数并将结果作为参数传递给console.log函数,如下所示:
...
console.log(myFunc("Adam"));
...
请注意,您不必声明该函数将返回一个结果或表示结果的数据类型。该清单的结果如下:
Hello Adam.
将函数用作其他函数的参数
JavaScript 函数可以作为对象传递,这意味着您可以使用一个函数作为另一个函数的参数,如清单 5-11 所示。
let myFunc = function (nameFunction) {
return ("Hello " + nameFunction() + ".");
};
console.log(myFunc(function () {
return "Adam";
}));
Listing 5-11.Using a Function as an Argument to Another Function in the main.ts File in the src Folder
myFunc函数定义了一个名为nameFunction的参数,它调用这个参数来获取插入到它返回的字符串中的值。我将一个返回Adam作为参数的函数传递给myFunc,它产生以下输出:
Hello Adam.
函数可以链接在一起,从小而容易测试的代码片段中构建更复杂的功能,如清单 5-12 所示。
let myFunc = function (nameFunction) {
return ("Hello " + nameFunction() + ".");
};
let printName = function (nameFunction, printFunction) {
printFunction(myFunc(nameFunction));
}
printName(function () { return "Adam" }, console.log);
Listing 5-12.Chaining Functions Calls in the main.ts File in the src Folder
这个例子产生了与清单 5-11 相同的结果。
使用箭头功能
箭头函数(也称为胖箭头函数或 lambda 表达式)是定义函数的另一种方法,通常用于定义仅用作其他函数的参数的函数。清单 5-13 用箭头函数替换了前面例子中的函数。
let myFunc = (nameFunction) => ("Hello " + nameFunction() + ".");
let printName = (nameFunction, printFunction) => printFunction(myFunc(nameFunction));
printName(function () { return "Adam" }, console.log);
Listing 5-13.Using Arrow Functions in the main.ts File in the src Folder
这些函数与清单 5-12 中的函数执行相同的工作。箭头函数有三个部分:输入参数、等号和大于号(“箭头”),最后是函数结果。只有当 arrow 函数需要执行多条语句时,才需要关键字return和花括号。本章后面还有更多箭头函数的例子。
使用变量和类型
let关键字用于声明变量,或者在一条语句中为变量赋值。用let声明的变量的作用域是定义它们的代码区域,如清单 5-14 所示。
let messageFunction = function (name, weather) {
let message = "Hello, Adam";
if (weather == "sunny") {
let message = "It is a nice day";
console.log(message);
} else {
let message = "It is " + weather + " today";
console.log(message);
}
console.log(message);
}
messageFunction("Adam", "raining");
Listing 5-14.Using let to Declare Variables in the main.ts File in the src Folder
在这个例子中,有三个语句使用let关键字来定义一个名为message的变量。每个变量的范围限于定义它的代码区域,产生以下结果:
It is raining today
Hello, Adam
这似乎是一个奇怪的例子,但是还有另一个关键字可以用来声明变量:var。let关键字是 JavaScript 规范中相对较新的内容,旨在解决var行为方式中的一些奇怪之处。清单 5-15 以清单 5-14 为例,将let替换为var。
let messageFunction = function (name, weather) {
var message = "Hello, Adam";
if (weather == "sunny") {
var message = "It is a nice day";
console.log(message);
} else {
var message = "It is " + weather + " today";
console.log(message);
}
console.log(message);
}
messageFunction("Adam", "raining");
Listing 5-15.Using var to Declare Variables in the main.ts File in the src Folder
当您保存列表中的更改时,您将看到以下结果:
It is raining today
It is raining today
问题是var关键字创建的变量的作用域是包含函数,这意味着所有对message的引用都是指同一个变量。这甚至会给有经验的 JavaScript 开发人员带来意想不到的结果,这也是引入更传统的let关键字的原因。
Using Let and Const
let关键字用于定义变量,const关键字用于定义不会改变的常数值。对任何不希望修改的值使用const关键字是一个很好的实践,这样如果试图修改,就会收到一个错误。然而,这是我很少遵循的一种做法——一部分是因为我太习惯于使用var关键字,另一部分是因为我用一系列语言编写代码,并且有一些我避免使用的特性,因为当我从一种语言切换到另一种语言时,它们会绊倒我。如果你是 JavaScript 新手,那么我建议你试着正确使用const和let,避免步我后尘。
使用可变闭包
如果你在另一个函数中定义一个函数——创建内部和外部函数——那么内部函数能够访问外部函数的变量,使用一个叫做闭包的特性,如清单 5-16 所示。
let myFunc = function(name) {
let myLocalVar = "sunny";
let innerFunction = function () {
return ("Hello " + name + ". Today is " + myLocalVar + ".");
}
return innerFunction();
};
console.log(myFunc("Adam"));
Listing 5-16.Using Variable Closure in the main.ts File in the src Folder
这个例子中的内部函数能够访问外部函数的局部变量,包括它的name参数。这是一个强大的特性,意味着您不必在内部函数上定义参数来传递数据值,但是需要小心,因为当使用像counter或index这样的普通变量名时,很容易得到意想不到的结果,您可能没有意识到您正在重用外部函数中的变量名。此示例产生以下结果:
Hello Adam. Today is sunny.
使用基本类型
JavaScript 定义了一组基本的原语类型:string、number和boolean。这似乎是一个很短的列表,但是 JavaScript 设法将很大的灵活性融入到这三种类型中。
Tip
我在这里简化。您可能会遇到另外三种原语。已经声明但没有赋值的变量是undefined,而null值用来表示一个变量没有值,就像其他语言一样。最后一个原语类型是Symbol,它是一个不可变的值,表示一个惟一的 ID,但是在编写本文时还没有广泛使用。
使用布尔值
boolean类型有两个值:true和false。清单 5-17 显示了正在使用的两个值,但是这种类型在条件语句中使用时最有用,比如一个if语句。该清单中没有控制台输出。
let firstBool = true;
let secondBool = false;
Listing 5-17.Defining boolean Values in the main.ts File in the src Folder
使用字符串
您可以使用双引号或单引号字符来定义string值,如清单 5-18 所示。
let firstString = "This is a string";
let secondString = 'And so is this';
Listing 5-18.Defining string Variables in the main.ts File in the src Folder
您使用的引号字符必须匹配。例如,你不能用单引号开始一个字符串,然后用双引号结束。这个清单没有输出。JavaScript 为string对象提供了一组基本的属性和方法,其中最有用的在表 5-2 中有描述。
表 5-2。
有用的字符串属性和方法
|名字
|
描述
|
| --- | --- |
| length | 此属性返回字符串中的字符数。 |
| charAt(index) | 此方法返回包含指定索引处的字符的字符串。 |
| concat(string) | 此方法返回一个新字符串,该字符串将调用该方法的字符串和作为参数提供的字符串连接在一起。 |
| indexOf(term, start) | 该方法返回第一个索引,在该索引处term出现在字符串中,如果没有匹配,则返回-1。可选的start参数指定搜索的起始索引。 |
| replace(term, newTerm) | 该方法返回一个新字符串,其中所有的term实例都被替换为newTerm。 |
| slice(start, end) | 此方法返回包含起始和结束索引之间的字符的子字符串。 |
| split(term) | 这个方法将一个字符串分割成一个由term分隔的值数组。 |
| toUpperCase()toLowerCase() | 这些方法返回所有字符都是大写或小写的新字符串。 |
| trim() | 此方法返回一个新字符串,其中所有的前导和尾随空白字符都已被删除。 |
使用模板字符串
一个常见的编程任务是将静态内容与数据值结合起来,以生成可以呈现给用户的字符串。传统的方法是通过字符串连接,这是我在本章的例子中一直使用的方法,如下所示:
...
let message = "It is " + weather + " today";
...
JavaScript 还支持模板字符串,它允许内联指定数据值,这有助于减少错误,带来更自然的开发体验。清单 5-19 显示了模板字符串的使用。
let messageFunction = function (weather) {
let message = `It is ${weather} today`;
console.log(message);
}
messageFunction("raining");
Listing 5-19.Using a Template String in the main.ts File
模板字符串以反斜杠(```ts 字符)开始和结束,数据值由花括号表示,前面有一个美元符号。例如,这个字符串将变量weather的值合并到模板字符串中:
...
let message = `It is ${weather} today`;
...
```ts
此示例产生以下输出:
It is raining today
#### 使用数字
`number`类型用于表示*整数*和*浮点*(也称为*实数*)。清单 5-20 提供了一个演示。
let daysInWeek = 7; let pi = 3.14; let hexValue = 0xFFFF;
Listing 5-20.Defining number Values in the main.ts File in the src Folder
您不必指定使用哪种号码。您只需表达您需要的值,JavaScript 就会相应地执行。在清单中,我定义了一个整数值、一个浮点值,并在一个值前面加上了`0x`来表示一个十六进制值。
## 使用 JavaScript 运算符
JavaScript 定义了一组非常标准的操作符。我在表 5-3 中总结了最有用的。
表 5-3。
有用的 JavaScript 运算符
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
操作员
|
描述
|
| --- | --- |
| `++, --` | 前或后递增和递减 |
| `+, -, *, /, %` | 加法、减法、乘法、除法、余数 |
| `<, <=, >, >=` | 小于,小于等于,大于,大于等于 |
| `==, !=` | 平等和不平等测试 |
| `===, !==` | 同一性和非同一性测试 |
| `&&, ||` | 逻辑 AND 和 OR (||用于合并空值) |
| `=` | 分配 |
| `+` | 串并置 |
| `?:` | 三操作数条件语句 |
### 使用条件语句
许多 JavaScript 操作符与条件语句一起使用。在本书中,我倾向于使用`if/else`和`switch`语句。清单 5-21 展示了两者的用法,如果你使用过几乎任何一种编程语言,你都会很熟悉。
let firstName = "Adam";
if (firstName == "Adam") { console.log("firstName is Adam"); } else if (firstName == "Jacqui") { console.log("firstName is Jacqui"); } else { console.log("firstName is neither Adam or Jacqui"); }
switch (firstName) { case "Adam": console.log("firstName is Adam"); break; case "Jacqui": console.log("firstName is Jacqui"); break; default: console.log("firstName is neither Adam or Jacqui"); break; }
Listing 5-21.Using the if/else and switch Conditional Statements in the main.ts File in the src Folder
清单中的结果如下:
firstName is Adam firstName is Adam
### 相等运算符与相同运算符
等式和等式运算符特别值得注意。相等运算符将尝试将操作数强制(转换)为相同的类型来评估相等性。这是一个方便的特性,只要你意识到它正在发生。清单 5-22 展示了等式操作符的作用。
let firstVal = 5; let secondVal = "5";
if (firstVal == secondVal) { console.log("They are the same"); } else { console.log("They are NOT the same"); }
Listing 5-22.Using the Equality Operator in the main.ts File in the src Folder
该脚本的输出如下:
They are the same
JavaScript 将两个操作数转换成相同的类型,并对它们进行比较。本质上,相等运算符测试值是否相同,而不管它们的类型如何。为了帮助防止这种错误,TypeScript 编译器将生成一个警告,尽管它仍然会生成 JavaScript 代码,因为这种类型的比较是合法的:
ERROR in src/main.ts:4:5 - error TS2367: This condition will always return 'false' since the types 'number' and 'string' have no overlap.
如果你想测试确保值*和*的类型是相同的,那么你需要使用恒等运算符(`===`,三个等号,而不是两个等号的运算符),如清单 5-23 所示。
let firstVal = 5; let secondVal = "5";
if (firstVal === secondVal) { console.log("They are the same"); } else { console.log("They are NOT the same"); }
Listing 5-23.Using the Identity Operator in the main.ts File in the src Folder
在本例中,identity 运算符将认为这两个变量是不同的。该运算符不强制类型。该脚本的结果如下:
They are NOT the same
### 显式转换类型
字符串连接操作符(`+`)比加法操作符(`+`)具有更高的优先级,这意味着 JavaScript 将优先于加法连接变量。这可能会造成混乱,因为 JavaScript 也会自由地转换类型以产生结果——而不总是预期的结果,如清单 5-24 所示。
let myData1 = 5 + 5; let myData2 = 5 + "5";
console.log("Result 1: " + myData1); console.log("Result 2: " + myData2);
Listing 5-24.String Concatenation Operator Precedence in the main.ts File
该脚本的结果如下:
Result 1: 10 Result 2: 55
第二种结果是引起混乱的那种。通过运算符优先级和类型转换的组合,原本应该是加法运算的操作被解释为字符串串联。为了避免这种情况,可以显式转换值的类型,以确保执行正确的操作,如以下部分所述。
#### 将数字转换为字符串
如果您正在处理多个数字变量,并希望将它们连接成字符串,那么您可以使用`toString`方法将数字转换成字符串,如清单 5-25 所示。
let myData1 = (5).toString() + String(5); console.log("Result: " + myData1);
Listing 5-25.Using the number.toString Method in the main.ts File in the src Folder
注意,我将数值放在括号中,然后调用了`toString`方法。这是因为在调用`number`类型定义的方法之前,您必须允许 JavaScript 将文字值转换成`number`。我还展示了实现相同效果的另一种方法,即调用`String`函数,并将数值作为参数传入。这两种技术具有相同的效果,都是将一个`number`转换成一个`string`,这意味着`+`操作符用于字符串连接而不是加法。该脚本的输出如下:
Result: 55
还有一些其他的方法可以让你更好地控制一个数字如何被表示成一个字符串。我在表 5-4 中简要描述了这些方法。表格中显示的所有方法都由`number`类型定义。
表 5-4。
有用的数字到字符串的方法
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
方法
|
描述
|
| --- | --- |
| `toString()` | 此方法返回一个表示以 10 为基数的数字的字符串。 |
| `toString(2)toString(8)toString(16)` | 此方法返回以二进制、八进制或十六进制表示法表示数字的字符串。 |
| `toFixed(n)` | 该方法返回一个表示小数点后有`n`位的实数的字符串。 |
| `toExponential(n)` | 该方法返回一个字符串,该字符串使用指数表示法表示一个数字,小数点前有一位数字,小数点后有`n`位数字。 |
| `toPrecision(n)` | 该方法返回一个字符串,该字符串表示一个具有`n`个有效数字的数字,如果需要,可以使用指数符号。 |
#### 将字符串转换为数字
补充技术是将字符串转换为数字,这样您就可以执行加法而不是连接。你可以用`Number`函数来实现,如清单 5-26 所示。
let firstVal = "5"; let secondVal = "5";
let result = Number(firstVal) + Number(secondVal); console.log("Result: " + result);
Listing 5-26.Converting Strings to Numbers in the main.ts File in the src Folder
该脚本的输出如下:
Result: 10
`Number`函数解析字符串值的方式很严格,但是您可以使用另外两个更灵活的函数,它们会忽略后面的非数字字符。这些功能是`parseInt`和`parseFloat`。我已经在表 5-5 中描述了所有三种方法。
表 5-5。
对数字方法有用的字符串
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
方法
|
描述
|
| --- | --- |
| `Number(str)` | 此方法分析指定的字符串以创建整数或实数值。 |
| `parseInt(str)` | 此方法分析指定的字符串以创建整数值。 |
| `parseFloat(str)` | 此方法分析指定的字符串以创建整数或实数值。 |
## 使用数组
JavaScript 数组的工作方式类似于大多数其他编程语言中的数组。清单 5-27 展示了如何创建和填充一个数组。
let myArray = new Array(); myArray[0] = 100; myArray[1] = "Adam"; myArray[2] = true;
Listing 5-27.Creating and Populating an Array in the main.ts File in the src Folder
我通过调用`new Array()`创建了一个新数组。这创建了一个空数组,我将它赋给了变量`myArray`。在随后的语句中,我为数组中的不同索引位置赋值。(这个清单中没有控制台输出。)
在这个例子中有一些事情需要注意。首先,在创建数组时,我不需要声明数组中的项数。JavaScript 数组会自动调整大小以容纳任意数量的项目。第二点是,我不必声明数组将保存的数据类型。任何 JavaScript 数组都可以包含任何混合的数据类型。在这个例子中,我给数组分配了三个项目:一个`number`、一个`string`和一个`boolean`。
### 使用数组文本
array literal 样式允许您在一条语句中创建和填充一个数组,如清单 5-28 所示。
let myArray = [100, "Adam", true];
Listing 5-28.Using the Array Literal Style in the main.ts File in the src Folder
在这个例子中,我通过在方括号(`[`和`]`)之间指定我想要的数组中的项目,指定了应该给`myArray`变量分配一个新的数组。(这个清单中没有控制台输出。)
### 读取和修改数组的内容
使用方括号(`[`和`]`)读取给定索引处的值,将所需的索引放在括号之间,如清单 5-29 所示。
let myArray = [100, "Adam", true]; console.log("Index 0: " + myArray[0]);
Listing 5-29.Reading the Data from an Array Index in the main.ts File in the src Folder
只需给索引赋值,就可以修改 JavaScript 数组中任何位置的数据。就像常规变量一样,您可以在索引处切换数据类型,不会有任何问题。清单的输出如下所示:
Index 0: 100
清单 5-30 展示了如何修改一个数组的内容。
let myArray = [100, "Adam", true]; myArray[0] = "Tuesday"; console.log("Index 0: " + myArray[0]);
Listing 5-30.Modifying the Contents of an Array in the main.ts File in the src Folder
在这个例子中,我将一个`string`赋值给数组中的位置`0`,这个位置以前是由一个`number`持有的,并产生以下输出:
Index 0: Tuesday
### 枚举数组的内容
使用一个`for`循环或者使用`forEach`方法来枚举数组的内容,该方法接收一个被调用来处理数组中每个元素的函数。清单 5-31 显示了这两种方法。
let myArray = [100, "Adam", true];
for (let i = 0; i < myArray.length; i++) { console.log("Index " + i + ": " + myArray[i]); }
console.log("---");
myArray.forEach((value, index) => console.log("Index " + index + ": " + value));
Listing 5-31.Enumerating the Contents of an Array in the main.ts File in the src Folder
JavaScript `for`循环的工作方式与许多其他语言中的循环一样。使用`length`属性确定数组中有多少个元素。
传递给`forEach`方法的函数有两个参数:要处理的当前项的值和该项在数组中的位置。在这个清单中,我使用了一个 arrow 函数作为`forEach`方法的参数,这是它们擅长的一种用法(在本书中你会看到它的使用)。清单的输出如下所示:
Index 0: 100 Index 1: Adam Index 2: true
Index 0: 100 Index 1: Adam Index 2: true
### 使用扩展运算符
spread 运算符用于扩展数组,以便其内容可以用作函数参数或与其他数组组合。在清单 5-32 中,我使用了 spread 操作符来扩展一个数组,这样它的条目就可以合并到另一个数组中。
let myArray = [100, "Adam", true]; let otherArray = [...myArray, 200, "Bob", false];
for (let i = 0; i < otherArray.length; i++) {
console.log(Array item ${i}: ${otherArray[i]});
}
Listing 5-32.Using the Spread Operator in the main.ts File in the src Folder
spread 操作符是一个省略号(三个句点的序列),它导致数组被解包。
... let otherArray = [...myArray, 200, "Bob", false]; ...
使用 spread 操作符,我可以在定义`otherArray`时将`myArray`指定为一个项,结果是第一个数组的内容将被解包并作为项添加到第二个数组中。此示例产生以下结果:
Array item 0: 100 Array item 1: Adam Array item 2: true Array item 3: 200 Array item 4: Bob Array item 5: false
### 使用内置数组方法
JavaScript `Array`对象定义了许多可以用来处理数组的方法,表 5-6 中描述了其中最有用的方法。
表 5-6。
有用的数组方法
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup>
|
方法
|
描述
|
| --- | --- |
| `concat(otherArray)` | 此方法返回一个新数组,该数组将调用它的数组与指定为参数的数组连接起来。可以指定多个数组。 |
| `join(separator)` | 该方法将数组中的所有元素连接起来形成一个字符串。该参数指定用于分隔各项的字符。 |
| `pop()` | 此方法移除并返回数组中的最后一项。 |
| `shift()` | 此方法移除并返回数组中的第一个元素。 |
| `push(item)` | 此方法将指定的项追加到数组的末尾。 |
| `unshift(item)` | 此方法在数组的开头插入一个新项。 |
| `reverse()` | 此方法返回一个新数组,该数组包含逆序排列的项。 |
| `slice(start,end)` | 此方法返回数组的一部分。 |
| `sort()` | 此方法对数组进行排序。可选的比较功能可用于执行自定义比较。 |
| `splice(index, count)` | 该方法从指定的`index`开始,从数组中移除`count`项。移除的项作为方法的结果返回。 |
| `unshift(item)` | 此方法在数组的开头插入一个新项。 |
| `every(test)` | 该方法为数组中的每一项调用`test`函数,如果函数为所有项返回`true`,则返回`true`,否则返回`false`。 |
| `some(test)` | 如果为数组中的每一项调用`test`函数至少返回一次`true`,则该方法返回`true`。 |
| `filter(test)` | 该方法返回一个新数组,其中包含了`test`函数返回的`true`项。 |
| `find(test)` | 该方法返回数组中第一个项目,对于该项目,`test`函数返回`true`。 |
| `findIndex(test)` | 该方法返回数组中第一项的索引,对于该数组,`test`函数返回`true`。 |
| `foreach(callback)` | 这个方法为数组中的每一项调用`callback`函数,如前一节所述。 |
| `includes(value)` | 如果数组包含指定的值,这个方法返回`true`。 |
| `map(callback)` | 该方法返回一个新数组,其中包含为数组中的每一项调用`callback`函数的结果。 |
| `reduce(callback)` | 该方法返回通过调用回调函数为数组中的每一项生成的累计值。 |
由于表 5-6 中的许多方法返回一个新数组,这些方法可以链接在一起处理一个过滤后的数据数组,如清单 5-33 所示。
let products = [ { name: "Hat", price: 24.5, stock: 10 }, { name: "Kayak", price: 289.99, stock: 1 }, { name: "Soccer Ball", price: 10, stock: 0 }, { name: "Running Shoes", price: 116.50, stock: 20 } ];
let totalValue = products .filter(item => item.stock > 0) .reduce((prev, item) => prev + (item.price * item.stock), 0);
console.log("Total value: $" + totalValue.toFixed(2));
Listing 5-33.Processing a Data Array in the main.ts File in the src Folder
我使用`filter`方法选择数组中`stock`值大于零的项目,并使用`reduce`方法确定这些项目的总值,产生以下输出:
Total value: $2864.99
## 摘要
在这一章中,我提供了一个关于 JavaScript 的简单入门,重点放在核心功能上,它将帮助你开始学习这门语言。我在本章中描述的一些特性是 JavaScript 规范中最近增加的,需要 TypeScript 编译器将它们转换成可以在旧浏览器中运行的代码。我将在下一章继续这个主题,并介绍一些在 Angular 开发中使用的更高级的 JavaScript 特性。
# 六、JavaScript 和 TypeScript:第二部分
在这一章中,我描述了一些对 Angular 开发有用的更高级的 JavaScript 特性。我解释了 JavaScript 如何处理对象,包括对类的支持,还解释了 JavaScript 功能如何打包到 JavaScript 模块中。我还介绍了 TypeScript 提供的一些特性,这些特性不是 JavaScript 规范的一部分,我在本书后面的一些示例中依赖这些特性。表 6-1 总结了本章内容。
表 6-1。
章节总结
<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"> <col class="tcol3 align-left"></colgroup>
|
问题
|
解决办法
|
列表
|
| --- | --- | --- |
| 通过指定属性和值创建对象 | 使用`new`关键字或使用对象文字 | 1–3 |
| 使用模板创建对象 | 定义一个类 | 4, 5 |
| 从另一个类继承行为 | 使用`extends`关键字 | six |
| 将 JavaScript 特性打包在一起 | 创建一个 JavaScript 模块 | seven |
| 声明对模块的依赖 | 使用`import`关键字 | 8–11 |
| 声明属性、参数和变量使用的类型 | 使用 TypeScript 类型批注 | 12–17 |
| 指定多种类型 | 使用联合类型 | 18–20 |
| 创建临时类型组 | 使用元组 | Twenty-one |
| 按关键字对值进行分组 | 使用可索引类型 | Twenty-two |
| 控制对类中方法和属性的访问 | 使用访问控制修饰符 | Twenty-three |
## 准备示例项目
对于这一章,我继续使用第五章的 JavaScriptPrimer 项目。准备本章不需要做任何更改,在`JavaScriptPrimer`文件夹中运行以下命令将启动 TypeScript 编译器和开发 HTTP 服务器:
```ts
ng serve --open
一个新的浏览器窗口将会打开,但它将是空的,因为我在上一章中删除了占位符内容。本章中的示例依靠浏览器的 JavaScript 控制台来显示消息。如果查看控制台,您会看到以下结果:
Total value: $2864.99
Tip
你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。
使用对象
有几种方法可以在 JavaScript 中创建对象。清单 6-1 给出了一个简单的例子。
Note
本章中的一些示例会导致 TypeScript 编译器报告错误。这些示例仍然有效,您可以忽略这些消息,因为 TypeScript 提供了一些额外的功能,我将在本章的后面介绍这些功能。
let myData = new Object();
myData.name = "Adam";
myData.weather = "sunny";
console.log("Hello " + myData.name + ".");
console.log("Today is " + myData.weather + ".");
Listing 6-1.Creating an Object in the main.ts File in the src Folder
我通过调用new Object()创建一个对象,并将结果(新创建的对象)赋给一个名为myData的变量。一旦创建了对象,我就可以通过赋值来定义对象的属性,就像这样:
...
myData.name = "Adam";
...
在这个语句之前,我的对象没有名为name的属性。当语句执行后,该属性确实存在,并被赋予了值Adam。您可以通过将变量名和属性名与句点组合来读取属性值,如下所示:
...
console.log("Hello " + myData.name + ".");
...
清单的结果如下:
Hello Adam.
Today is sunny.
使用对象文字
您可以使用对象文字格式在一个步骤中定义一个对象及其属性,如清单 6-2 所示。
let myData = {
name: "Adam",
weather: "sunny"
};
console.log("Hello " + myData.name + ". ");
console.log("Today is " + myData.weather + ".");
Listing 6-2.Using the Object Literal Format in the main.ts File in the src Folder
使用冒号(:)将您要定义的每个属性与其值分开,使用逗号(,)将属性分开。效果与前面的示例相同,清单的结果如下:
Hello Adam.
Today is sunny.
将函数用作方法
我最喜欢 JavaScript 的一个特性是可以向对象添加函数。定义在对象上的函数被称为方法。清单 6-3 展示了如何以这种方式添加方法。
let myData = {
name: "Adam",
weather: "sunny",
printMessages: function () {
console.log("Hello " + this.name + ". ");
console.log("Today is " + this.weather + ".");
}
};
myData.printMessages();
Listing 6-3.Adding Methods to an Object in the main.ts File in the src Folder
在这个例子中,我使用了一个函数来创建一个名为printMessages的方法。注意,为了引用对象定义的属性,我必须使用this关键字。当一个函数作为一个方法使用时,该函数通过特殊变量this被隐式传递给调用该方法的对象作为参数。清单的输出如下所示:
Hello Adam.
Today is sunny.
定义类别
类是用于创建具有相同功能的对象的模板。对类的支持是 JavaScript 规范中最近增加的内容,旨在使 JavaScript 与其他主流编程语言的工作更加一致,在整个 angle 开发中都使用了类。清单 6-4 展示了如何使用类来表达上一节中对象定义的功能。
class MyClass {
constructor(name, weather) {
this.name = name;
this.weather = weather;
}
printMessages() {
console.log("Hello " + this.name + ". ");
console.log("Today is " + this.weather + ".");
}
}
let myData = new MyClass("Adam", "sunny");
myData.printMessages();
Listing 6-4.Defining a Class in the main.ts File in the src Folder
如果您使用过另一种主流语言,如 Java 或 C#,那么 JavaScript 类将会很熟悉。class关键字用于声明一个类,后跟类名,在本例中是MyClass。
当使用该类创建一个新对象时,调用constructor函数,它提供了一个接收数据值和进行该类所需的任何初始设置的机会。在这个例子中,构造函数定义了用于创建同名变量的name和weather参数。像这样定义的变量被称为属性。
类可以有被定义为函数的方法,尽管不需要使用function k关键字。示例中有一个方法叫做printMessages,它使用name和weather属性的值将消息写入浏览器的 JavaScript 控制台。
Tip
类也可以有静态方法,用关键字static表示。静态方法属于类,而不是它们创建的对象。我在清单 6-14 中包含了一个静态方法的例子。
new关键字用于从一个类中创建一个对象,如下所示:
...
let myData = new MyClass("Adam", "sunny");
...
该语句使用MyClass类作为模板创建一个新对象。在这种情况下,MyClass被用作函数,传递给它的参数将被该类定义的constructor函数接收。这个表达式的结果是一个新对象,它被分配给一个名为myData的变量。一旦创建了一个对象,就可以通过为其赋值的变量来访问它的属性和方法,如下所示:
...
myData.printMessages();
...
此示例在浏览器的 JavaScript 控制台中产生以下结果:
Hello Adam.
Today is sunny.
JavaScript Classes vs. Prototypes
类特性没有改变 JavaScript 处理类型的基本方式。相反,它只是提供了一种大多数程序员更熟悉的使用它们的方法。在幕后,JavaScript 仍然使用传统的类型系统,这是基于原型的。举个例子,清单 6-4 中的代码也可以写成这样:
var MyClass = function MyClass(name, weather) {
this.name = name;
this.weather = weather;
}
MyClass.prototype.printMessages = function () {
console.log("Hello " + this.name + ". ");
console.log("Today is " + this.weather + ".");
};
var myData = new MyClass("Adam", "sunny");
myData.printMessages();
使用类时,Angular 开发更容易,这是我在本书中一直采用的方法。ES6 中引入的许多特性被归类为语法糖,这意味着它们使 JavaScript 的某些方面更容易理解和使用。术语句法糖可能看起来带有贬义,但是 JavaScript 有一些奇怪的特性,其中许多特性帮助开发人员避免常见的陷阱。
定义类 Getter 和 Setter 属性
JavaScript 类可以在其构造函数中定义属性,从而产生一个可以在应用的其他地方读取和修改的变量。Getters 和 setters 作为常规属性出现在类之外,但它们允许引入额外的逻辑,这对于验证或转换新值或以编程方式生成值很有用,如清单 6-5 所示。
class MyClass {
constructor(name, weather) {
this.name = name;
this._weather = weather;
}
set weather(value) {
this._weather = value;
}
get weather() {
return `Today is ${this._weather}`;
}
printMessages() {
console.log("Hello " + this.name + ". ");
console.log(this.weather);
}
}
let myData = new MyClass("Adam", "sunny");
myData.printMessages();
Listing 6-5.Using Getters and Setters in the main.ts File in the src Folder
getter 和 setter 被实现为前面带有关键字get或set的函数。JavaScript 类中没有访问控制的概念,惯例是在内部属性的名称前加上下划线(_字符)。在清单中,weather属性是用一个更新名为_weather的属性的 setter 和一个将_weather值合并到模板字符串中的 getter 实现的。此示例在浏览器的 JavaScript 控制台中产生以下结果:
Hello Adam.
Today is sunny
使用类继承
使用extends关键字,类可以从其他类继承行为,如清单 6-6 所示。
class MyClass {
constructor(name, weather) {
this.name = name;
this._weather = weather;
}
set weather(value) {
this._weather = value;
}
get weather() {
return `Today is ${this._weather}`;
}
printMessages() {
console.log("Hello " + this.name + ". ");
console.log(this.weather);
}
}
class MySubClass extends MyClass {
constructor(name, weather, city) {
super(name, weather);
this.city = city;
}
printMessages() {
super.printMessages();
console.log(`You are in ${this.city}`);
}
}
let myData = new MySubClass("Adam", "sunny", "London");
myData.printMessages();
Listing 6-6.Using Class Inheritance in the main.ts File in the src Folder
extends关键字用于声明将被继承的类,称为超类或基类。在清单中,MySubClass继承了MyClass。super关键字用于调用超类的构造函数和方法。MySubClass在MyClass功能的基础上增加了对城市的支持,在浏览器的 JavaScript 控制台中产生了以下结果:
Hello Adam.
Today is sunny
You are in London
使用 JavaScript 模块
JavaScript 模块用于管理 web 应用中的依赖关系,这意味着您不需要管理大量单独的代码文件来确保浏览器下载应用的所有代码。相反,在编译过程中,应用需要的所有 JavaScript 文件被组合成一个更大的文件,称为包,浏览器下载的就是这个文件。
Note
早期版本的 Angular 依赖于一个模块加载器,它会发送单独的 HTTP 请求来获取应用所需的 JavaScript 文件。对开发工具的更改通过切换到使用在构建过程中创建的包简化了这个过程。
创建和使用模块
添加到项目中的每个 TypeScript 或 JavaScript 文件都被视为一个模块。为了演示,我在src文件夹中创建了一个名为modules的文件夹,向其中添加了一个名为NameAndWeather.ts的文件,并添加了清单 6-7 中所示的代码。
export class Name {
constructor(first, second) {
this.first = first;
this.second = second;
}
get nameMessage() {
return `Hello ${this.first} ${this.second}`;
}
}
export class WeatherLocation {
constructor(weather, city) {
this.weather = weather;
this.city = city;
}
get weatherMessage() {
return `It is ${this.weather} in ${this.city}`;
}
}
Listing 6-7.The Contents of the NameAndWeather.ts File in the src/modules Folder
默认情况下,JavaScript 或 TypeScript 文件中定义的类、函数和变量只能在该文件中访问。export关键字用于使特性在文件之外可访问,以便它们可以被应用的其他部分使用。在这个例子中,我将关键字export应用到了Name和WeatherLocation类,这意味着它们可以在模块之外使用。
Tip
我在NameAndWeather.ts文件中定义了两个类,其效果是创建了一个包含两个类的模块。Angular 应用中的惯例是将每个类放入它自己的文件中,这意味着每个类都在它自己的模块中定义,并且你会在本书的清单中看到关键字export。
import关键字用于声明对模块提供的特性的依赖。在清单 6-8 中,我在main.ts文件中使用了Name和WeatherLocation类,这意味着我必须使用import关键字来声明对它们和它们所来自的模块的依赖。
import { Name, WeatherLocation } from "./modules/NameAndWeather";
let name = new Name("Adam", "Freeman");
let loc = new WeatherLocation("raining", "London");
console.log(name.nameMessage);
console.log(loc.weatherMessage);
Listing 6-8.Importing Specific Types in the main.ts File in the src Folder
这是我在本书的大多数例子中使用import关键字的方式。该关键字后面是花括号,其中包含当前文件中的代码所依赖的特性的逗号分隔列表,后面是from关键字,再后面是模块名。在本例中,我从modules文件夹中的NameAndWeather模块中导入了Name和WeatherLocation类。请注意,指定模块时不包括文件扩展名。
停止 Angular 开发工具,并运行以下命令再次启动它们,这将确保新文件夹包含在构建过程中:
ng serve
当main.ts文件被编译时,Angular 开发工具检测到对NameAndWeather.ts文件中代码的依赖。这种依赖性确保了 JavaScript 包文件中包含了Name和WeatherLocation类,您将在浏览器的 JavaScript 控制台中看到以下输出,显示模块中的代码用于产生结果:
Hello Adam Freeman
It is raining in London
请注意,我不必将NaneAndWeather.ts文件包含在要发送到浏览器的文件列表中。仅仅使用import关键字就足以声明依赖关系,并确保应用所需的代码包含在发送给浏览器的 JavaScript 文件中。
(您将看到警告您尚未定义属性的错误。暂时忽略这些警告;我将在本章的后面解释它们是如何解决的。)
Understanding Module Resolution
在本书的import语句中,你会看到两种不同的指定模块的方式。第一个是相对模块,其中模块名以./为前缀,如清单 6-8 中的例子:
...
import { Name, WeatherLocation } from "./modules/NameAndWeather";
...
该语句指定了相对于包含import语句的文件定位的模块。在这种情况下,NameAndWeather.ts文件位于modules目录中,该目录与main.ts文件位于同一目录中。另一种类型的导入是不相关的。这是第二章的一个非相关的例子,你会在整本书中看到:
...
import { Component } from "@angular/core";
...
这个import语句中的模块不是以./开始的,构建工具通过在node_modules文件夹中寻找一个包来解决依赖关系。在这种情况下,依赖关系是由@angular/core包提供的特性,它是在由ng new命令创建时添加到项目中的。
重命名导入
在有许多依赖项的复杂项目中,您可能需要使用来自不同模块的同名的两个类。为了重现这种情况,我在src/modules文件夹中创建了一个名为DuplicateName.ts的文件,并定义了清单 6-9 中所示的类。
export class Name {
get message() {
return "Other Name";
}
}
Listing 6-9.The Contents of the DuplicateName.ts File in the src/modules Folder
这个类不做任何有用的事情,但是它被称为Name,这意味着使用清单 6-8 中的方法导入它将会导致冲突,因为编译器将无法区分这两个同名的类。解决方案是使用as关键字,它允许在从模块导入类时为类创建别名,如清单 6-10 所示。
import { Name, WeatherLocation } from "./modules/NameAndWeather";
import { Name as OtherName } from "./modules/DuplicateName";
let name = new Name("Adam", "Freeman");
let loc = new WeatherLocation("raining", "London");
let other = new OtherName();
console.log(name.nameMessage);
console.log(loc.weatherMessage);
console.log(other.message);
Listing 6-10.Using a Module Alias in the main.ts File in the src Folder
DuplicateName模块中的Name类被导入为OtherName,这使得它在使用时不会与NameAndWeather模块中的Name类发生冲突。此示例产生以下输出:
Hello Adam Freeman
It is raining in London
Other Name
导入模块中的所有类型
另一种方法是将模块作为一个对象导入,该对象具有它所包含的每种类型的属性,如清单 6-11 所示。
import * as NameAndWeatherLocation from "./modules/NameAndWeather";
import { Name as OtherName } from "./modules/DuplicateName";
let name = new NameAndWeatherLocation.Name("Adam", "Freeman");
let loc = new NameAndWeatherLocation.WeatherLocation("raining", "London");
let other = new OtherName();
console.log(name.nameMessage);
console.log(loc.weatherMessage);
console.log(other.message);
Listing 6-11.Importing a Module as an Object in the main.ts File in the src Folder
本例中的import语句导入NameAndWeather模块的内容,并创建一个名为NameAndWeatherLocation的对象。这个对象具有与模块中定义的类相对应的Name和Weather属性。这个例子产生与清单 6-10 相同的输出。
有用的 TypeScript 功能
TypeScript 是 JavaScript 的超集,提供的语言特性建立在 JavaScript 规范所提供的特性之上。在接下来的几节中,我将展示对于 Angular 开发最有用的 TypeScript 特性,我在本书的示例中使用了其中的许多特性。
Tip
TypeScript 支持比我在本章中描述的更多的特性。我将在后面的章节中介绍一些额外的特性,但是要获得完整的参考资料,请查看位于 www.typescriptlang.org 的 TypeScript 主页。
使用类型注释
headline TypeScript 特性支持类型注释,这有助于通过在编译代码时应用类型检查来减少常见的 JavaScript 错误,这种方式让人想起 C#或 Java 等语言。如果您很难接受 JavaScript 类型系统(或者甚至没有意识到有这样一个系统),那么类型注释可以在很大程度上防止最常见的错误。(另一方面,如果您喜欢常规 JavaScript 类型的自由,您可能会发现 TypeScript 类型注释的限制性和烦人性。)
为了展示类型注释解决的问题,我在JavaScriptPrimer文件夹中创建了一个名为tempConverter.ts的文件,并添加了清单 6-12 中的代码。
export class TempConverter {
static convertFtoC(temp) {
return ((parseFloat(temp.toPrecision(2)) - 32) / 1.8).toFixed(1);
}
}
Listing 6-12.The Contents of the tempConverter.ts File in the src Folder
TempConverter类包含一个名为convertFtoC的简单静态方法,该方法接受以华氏度表示的温度值,并返回以摄氏度表示的相同温度。
本代码中有一些不明确的假设。convertFtoC方法期望接收一个number值,在此基础上调用toPrecision方法来设置浮点数字的个数。该方法返回一个string,尽管在没有仔细检查代码的情况下很难判断出来(toFixed方法的结果是一个string)。
这些隐含的假设会导致问题,尤其是当一个开发人员使用另一个开发人员编写的 JavaScript 代码时。在清单 6-13 中,我故意制造了一个错误,将温度作为string值传递,而不是该方法期望的number。
import { Name, WeatherLocation } from "./modules/NameAndWeather";
import { Name as OtherName } from "./modules/DuplicateName";
import { TempConverter } from "./tempConverter";
let name = new Name("Adam", "Freeman");
let loc = new WeatherLocation("raining", "London");
let other = new OtherName();
let cTemp = TempConverter.convertFtoC("38");
console.log(name.nameMessage);
console.log(loc.weatherMessage);
console.log(`The temp is ${cTemp}C`);
Listing 6-13.Using the Wrong Type in the main.ts File in the src Folder
当浏览器执行代码时,您将在浏览器的 JavaScript 控制台中看到以下消息(具体工作方式可能因您使用的浏览器而异):
temp.toPrecision is not a function
当然,不使用 TypeScript 也可以解决这种问题,但这意味着任何 JavaScript 应用中的大量代码都被用来检查正在使用的类型。TypeScript 解决方案是使用添加到 JavaScript 代码中的类型注释,使类型强制成为编译器的工作。在清单 6-14 中,我给TempConverter类添加了类型注释。
export class TempConverter {
static convertFtoC(temp: number) : string {
return ((parseFloat(temp.toPrecision(2)) - 32) / 1.8).toFixed(1);
}
}
Listing 6-14.Adding Type Annotations in the tempConverter.ts File in the src Folder
类型注释使用冒号(:字符)后跟类型来表示。示例中有两个注释。第一个指定了convertFtoC方法的参数应该是一个number。
...
static convertFtoC(temp: number) : string {
...
另一个注释指定该方法的结果是一个字符串。
...
static convertFtoC(temp: number) : string {
...
当您保存对文件的更改时,TypeScript 编译器将运行。报告的错误包括:
Argument of type '"38"' is not assignable to parameter of type 'number'.
TypeScript 编译器检查到传递给文件main.ts中的convertFtoC方法的值的类型与类型注释不匹配,并报告了一个错误。这是 TypeScript 类型系统的核心;这意味着您不必在您的类中编写额外的代码来检查您是否收到了预期的类型,并且它还使得确定方法结果的类型变得容易。为了解决报告给编译器的错误,清单 6-15 更新了调用convertFtoC方法的语句,以便它使用一个number。
import { Name, WeatherLocation } from "./modules/NameAndWeather";
import { Name as OtherName } from "./modules/DuplicateName";
import { TempConverter } from "./tempConverter";
let name = new Name("Adam", "Freeman");
let loc = new WeatherLocation("raining", "London");
let other = new OtherName();
let cTemp = TempConverter.convertFtoC(38);
console.log(name.nameMessage);
console.log(loc.weatherMessage);
console.log(other.message);
console.log(`The temp is ${cTemp}C`);
Listing 6-15.Using a Number Argument in the main.ts File in the src Folder
保存更改时,您将看到浏览器的 JavaScript 控制台中显示以下消息:
Hello Adam Freeman
It is raining in London
Other Name
The temp is 3.3C
类型注释属性和变量
类型注释也可以应用于属性和变量,确保编译器可以验证应用中使用的所有类型。在清单 6-16 中,我给NameAndWeather模块中的类添加了类型注释。
export class Name {
first: string;
second: string;
constructor(first: string, second: string) {
this.first = first;
this.second = second;
}
get nameMessage() : string {
return `Hello ${this.first} ${this.second}`;
}
}
export class WeatherLocation {
weather: string;
city: string;
constructor(weather: string, city: string) {
this.weather = weather;
this.city = city;
}
get weatherMessage() : string {
return `It is ${this.weather} in ${this.city}`;
}
}
Listing 6-16.Adding Annotations in the NameAndWeather.ts File in the src/modules Folder
属性是用类型注释声明的,遵循与参数和结果注释相同的模式。清单 6-17 中的变化解决了 TypeScript 编译器报告的其余错误,该编译器抱怨是因为它不知道构造函数中创建的属性的类型。
接收构造函数参数并将它们的值赋给变量的模式如此常见,以至于 TypeScript 包含了一个优化,如清单 6-17 所示。
export class Name {
constructor(private first: string, private second: string) {}
get nameMessage() : string {
return `Hello ${this.first} ${this.second}`;
}
}
export class WeatherLocation {
constructor(private weather: string, private city: string) {}
get weatherMessage() : string {
return `It is ${this.weather} in ${this.city}`;
}
}
Listing 6-17.Using Parameters in the NameAndWeather.ts File in the src/modules Folder
关键字private是访问控制修饰符的一个例子,我在“使用访问修饰符”一节中描述了它。将关键字应用于构造函数参数具有自动定义 class 属性并为其分配参数值的效果。清单 6-17 中的代码是清单 6-16 的更简洁版本。
指定多种类型或任何类型
TypeScript 允许指定多种类型,用一个竖线(|字符)分隔。当一个方法可以接受或返回多种类型时,或者当一个变量可以被赋予不同类型的值时,这是很有用的。清单 6-18 修改了convertFtoC方法,使其接受number或string值。
export class TempConverter {
static convertFtoC(temp: number | string): string {
let value: number = (<number>temp).toPrecision
? <number>temp : parseFloat(<string>temp);
return ((parseFloat(value.toPrecision(2)) - 32) / 1.8).toFixed(1);
}
}
Listing 6-18.Accepting Multiple Types in the tempConverter.ts File in the src Folder
temp参数的type声明已经更改为number | string,这意味着该方法可以接受任何一种类型。这叫做联体式。在该方法中,类型断言用于确定接收到的是哪种类型。这是一个有点尴尬的过程,但是参数值被转换成一个数字值,以检查结果上是否定义了一个toPrecision方法,如下所示:
...
(<number>temp).toPrecision
...
尖括号(<和>字符)用于声明一个类型断言,该断言试图将一个对象转换为指定的类型。您也可以使用as关键字获得相同的结果,如清单 6-19 所示。
export class TempConverter {
static convertFtoC(temp: number | string): string {
let value: number = (temp as number).toPrecision
? temp as number : parseFloat(<string>temp);
return ((parseFloat(value.toPrecision(2)) - 32) / 1.8).toFixed(1);
}
}
Listing 6-19.Using the as Keyword in the tempConverter.ts File in the src Folder
指定联合类型的另一种方法是使用any关键字,它允许将任何类型赋给变量、用作参数或从方法返回。清单 6-20 用any关键字替换了convertFtoC方法中的联合类型。
Tip
当您省略类型注释时,TypeScript 编译器将隐式应用any关键字。
export class TempConverter {
static convertFtoC(temp: any): string {
let value: number;
if ((temp as number).toPrecision) {
value = temp;
} else if ((temp as string).indexOf) {
value = parseFloat(<string>temp);
} else {
value = 0;
}
return ((parseFloat(value.toPrecision(2)) - 32) / 1.8).toFixed(1);
}
}
Listing 6-20.Specifying Any Type in the tempConverter.ts File in the src Folder
使用元组
元组是固定长度的数组,数组中的每一项都是指定的类型。这是一个听起来模糊的描述,因为元组是如此灵活。例如,清单 6-21 使用一个元组来表示一个城市及其当前的天气和温度。
import { Name, WeatherLocation } from "./modules/NameAndWeather";
import { Name as OtherName } from "./modules/DuplicateName";
import { TempConverter } from "./tempConverter";
let name = new Name("Adam", "Freeman");
let loc = new WeatherLocation("raining", "London");
let other = new OtherName();
let cTemp = TempConverter.convertFtoC("38");
let tuple: [string, string, string];
tuple = ["London", "raining", TempConverter.convertFtoC("38")]
console.log(`It is ${tuple[2]} degrees C and ${tuple[1]} in ${tuple[0]}`);
Listing 6-21.Using a Tuple in the main.ts File in the src Folder
元组被定义为一个类型数组,使用数组索引器访问单个元素。此示例在浏览器的 JavaScript 控制台中生成以下消息:
It is 3.3 degrees C and raining in London
使用可索引类型
可索引类型将一个键与一个值相关联,创建一个类似地图的集合,可用于将相关的数据项收集在一起。在清单 6-22 中,我使用了一个可索引类型来收集多个城市的信息。
import { Name, WeatherLocation } from "./modules/NameAndWeather";
import { Name as OtherName } from "./modules/DuplicateName";
import { TempConverter } from "./tempConverter";
let cities: { [index: string]: [string, string] } = {};
cities["London"] = ["raining", TempConverter.convertFtoC("38")];
cities["Paris"] = ["sunny", TempConverter.convertFtoC("52")];
cities["Berlin"] = ["snowing", TempConverter.convertFtoC("23")];
for (let key in cities) {
console.log(`${key}: ${cities[key][0]}, ${cities[key][1]}`);
}
Listing 6-22.Using Indexable Types in the main.ts File in the src Folder
cities变量定义为可索引类型,键为字符串,数据值为[string, string]元组。使用数组风格的索引器来赋值和读取,比如cities["London"]。可以使用一个for...in循环访问可索引类型的键集合,如示例所示,它在浏览器的 JavaScript 控制台中产生以下输出:
London: raining, 3.3
Paris: sunny, 11.1
Berlin: snowing, -5.0
只有number和string值可以用作可索引类型的键,但这是一个有用的特性,我将在后面章节的示例中使用。
使用访问修饰符
JavaScript 不支持访问保护,这意味着可以从应用的任何部分访问类、它们的属性和它们的方法。有一个惯例是在实现成员的名字前加一个下划线(_字符),但是这只是对其他开发人员的一个警告,并不是强制的。
TypeScript 提供了三个关键字,用于管理访问,并由编译器强制执行。表 6-2 描述了关键字。
表 6-2。
TypeScript 访问修饰符关键字
|关键字
|
描述
|
| --- | --- |
| public | 该关键字用于表示可以在任何地方访问的属性或方法。如果没有使用关键字,这是默认的访问保护。 |
| private | 该关键字用于表示只能在定义它的类中访问的属性或方法。 |
| protected | 该关键字用于表示只能在定义它的类中或由扩展该类的类访问的属性或方法。 |
清单 6-23 向TempConverter类添加了一个private方法。
export class TempConverter {
static convertFtoC(temp: any): string {
let value: number;
if ((temp as number).toPrecision) {
value = temp;
} else if ((temp as string).indexOf) {
value = parseFloat(<string>temp);
} else {
value = 0;
}
return TempConverter.performCalculation(value).toFixed(1);
}
private static performCalculation(value: number): number {
return (parseFloat(value.toPrecision(2)) - 32) / 1.8;
}
}
Listing 6-23.Using an Access Modifier in the tempConverter.ts File in the src Folder
performCalculation方法被标记为private,这意味着如果应用的任何其他部分试图调用该方法,TypeScript 编译器将报告一个错误代码。
摘要
在这一章中,我描述了 JavaScript 支持处理对象和类的方式,解释了 JavaScript 模块如何工作,并介绍了对 angle 开发有用的 TypeScript 特性。在下一章中,我将开始创建一个现实项目的过程,在本书第二部分深入探讨各个细节之前,该项目将概述不同的 Angular 特征如何共同创建应用。
七、SportsStore:一个真正的应用
在第二章,我构建了一个快速简单的 Angular 应用。小而集中的例子允许我展示特定的 Angular 特征,但它们可能缺乏上下文。为了帮助克服这个问题,我将创建一个简单但现实的电子商务应用。
我的应用名为 SportsStore,将遵循各地在线商店采用的经典方法。我将创建一个客户可以按类别和页面浏览的在线产品目录,一个用户可以添加和删除产品的购物车,以及一个客户可以输入送货细节和下订单的收银台。我还将创建一个管理区域,其中包括用于管理目录的创建、读取、更新和删除(CRUD)工具——我将保护它,以便只有登录的管理员才能进行更改。最后,我将向您展示如何准备和部署 Angular 应用。
我在这一章和接下来的章节中的目标是通过创建尽可能真实的例子来给你一个真实的 Angular 发展的感觉。当然,我想把重点放在 Angular 上,所以我简化了与外部系统的集成,比如数据存储,并完全省略了其他部分,比如支付处理。
SportsStore 是我在几本书中使用的一个例子,尤其是因为它展示了使用不同的框架、语言和开发风格来实现相同结果的方法。你不需要阅读我的任何其他书籍来理解这一章,但是如果你已经拥有我的Pro ASP.NET Core 3书,你会发现这种对比很有趣。
我在 SportsStore 应用中使用的 Angular 特征将在后面的章节中详细介绍。我不会在这里重复所有的内容,我告诉您的内容足以让您理解示例应用,并让您参考其他章节以获得更深入的信息。你可以从头到尾阅读 SportsStore 章节,了解 Angular 的工作原理,也可以从细节章节跳转到深度章节。无论哪种方式,都不要指望马上理解所有的东西——Angular 有许多活动的部分,SportsStore 应用旨在向您展示它们是如何组合在一起的,而不是深入到我在本书剩余部分描述的细节中。
准备项目
要创建 SportsStore 项目,请打开命令提示符,导航到一个方便的位置,然后运行以下命令:
ng new SportsStore --routing false --style css --skip-git --skip-tests
angular-cli包将为 Angular 开发创建一个新项目,包含配置文件、占位符内容和开发工具。项目设置过程可能需要一些时间,因为有许多 NPM 软件包需要下载和安装。
Tip
你可以从 https://github.com/Apress/pro-angular-9 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第一章获取帮助。
安装附加的 NPM 软件包
除了核心 Angular 包和由ng new命令设置的构建工具之外,SportsStore 项目还需要额外的包。运行以下命令导航到SportsStore文件夹并添加所需的包:
cd SportsStore
npm install bootstrap@4.4.1
npm install @fortawesome/fontawesome-free@5.12.1
npm install --save-dev json-server@0.16.0
npm install --save-dev jsonwebtoken@8.5.1
使用清单中显示的版本号很重要。在添加包时,您可能会看到关于未满足对等依赖关系的警告,但是您可以忽略它们。有些包是使用--save-dev参数安装的,这表明它们是在开发过程中使用的,不会成为 SportsStore 应用的一部分。
向应用添加 CSS 样式表
一旦安装了这些包,将清单 7-1 中所示的语句添加到angular.json文件中,将来自引导 CSS 框架和字体 Awesome 包的 CSS 文件合并到应用中。我将对 SportsStore 应用中的所有 HTML 内容使用 Bootstrap CSS 样式,并且我将使用字体 Awesome 包中的图标向用户呈现购物车的摘要。
...
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/SportsStore",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/@fortawesome/fontawesome-free/css/all.min.css"
],
"scripts": []
},
...
Listing 7-1.Adding CSS to the angular.json File in the SportsStore Folder
准备 RESTful Web 服务
SportsStore 应用将使用异步 HTTP 请求来获取由 RESTful web 服务提供的模型数据。正如我在第二十四章中所描述的,REST 是一种设计 web 服务的方法,它使用 HTTP 方法或动词来指定操作和 URL 来选择操作所应用的数据对象。
在前一节中,我将json-server包添加到项目中。这是一个从 JSON 数据或 JavaScript 代码创建 web 服务的优秀包。将清单 7-2 中所示的语句添加到package.json文件的scripts部分,这样json-server包就可以从命令行启动。
...
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"json": "json-server data.js -p 3500 -m authMiddleware.js"
},
...
Listing 7-2.Adding a Script in the package.json File in the SportsStore Folder
为了给json-server包提供要处理的数据,我在SportsStore文件夹中添加了一个名为data.js的文件,并添加了如清单 7-3 所示的代码,这将确保无论何时启动json-server包都可以获得相同的数据,这样我在开发过程中就有了一个固定的参考点。
Tip
创建配置文件时,注意文件名很重要。有些具有.json扩展名,这意味着它们包含 JSON 格式的静态数据。其他文件的扩展名为.js,这意味着它们包含 JavaScript 代码。Angular 开发所需的每个工具都有关于其配置文件的预期。
module.exports = function () {
return {
products: [
{ id: 1, name: "Kayak", category: "Watersports",
description: "A boat for one person", price: 275 },
{ id: 2, name: "Lifejacket", category: "Watersports",
description: "Protective and fashionable", price: 48.95 },
{ id: 3, name: "Soccer Ball", category: "Soccer",
description: "FIFA-approved size and weight", price: 19.50 },
{ id: 4, name: "Corner Flags", category: "Soccer",
description: "Give your playing field a professional touch",
price: 34.95 },
{ id: 5, name: "Stadium", category: "Soccer",
description: "Flat-packed 35,000-seat stadium", price: 79500 },
{ id: 6, name: "Thinking Cap", category: "Chess",
description: "Improve brain efficiency by 75%", price: 16 },
{ id: 7, name: "Unsteady Chair", category: "Chess",
description: "Secretly give your opponent a disadvantage",
price: 29.95 },
{ id: 8, name: "Human Chess Board", category: "Chess",
description: "A fun game for the family", price: 75 },
{ id: 9, name: "Bling King", category: "Chess",
description: "Gold-plated, diamond-studded King", price: 1200 }
],
orders: []
}
}
Listing 7-3.The Contents of the data.js File in the SportsStore Folder
这段代码定义了将由 RESTful web 服务呈现的两个数据集合。products集合包含销售给客户的产品,而orders集合将包含客户已经下的订单(但是目前是空的)。
RESTful web 服务存储的数据需要受到保护,这样普通用户就不能修改产品或更改订单的状态。json-server包不包含任何内置的认证特性,所以我在SportsStore文件夹中创建了一个名为authMiddleware.js的文件,并添加了清单 7-4 中所示的代码。
const jwt = require("jsonwebtoken");
const APP_SECRET = "myappsecret";
const USERNAME = "admin";
const PASSWORD = "secret";
const mappings = {
get: ["/api/orders", "/orders"],
post: ["/api/products", "/products", "/api/categories", "/categories"]
}
function requiresAuth(method, url) {
return (mappings[method.toLowerCase()] || [])
.find(p => url.startsWith(p)) !== undefined;
}
module.exports = function (req, res, next) {
if (req.url.endsWith("/login") && req.method == "POST") {
if (req.body && req.body.name == USERNAME && req.body.password == PASSWORD) {
let token = jwt.sign({ data: USERNAME, expiresIn: "1h" }, APP_SECRET);
res.json({ success: true, token: token });
} else {
res.json({ success: false });
}
res.end();
return;
} else if (requiresAuth(req.method, req.url)) {
let token = req.headers["authorization"] || "";
if (token.startsWith("Bearer<")) {
token = token.substring(7, token.length - 1);
try {
jwt.verify(token, APP_SECRET);
next();
return;
} catch (err) { }
}
res.statusCode = 401;
res.end();
return;
}
next();
}
Listing 7-4.The Contents of the authMiddleware.js File in the SportsStore Folder
这段代码检查发送到 RESTful web 服务的 HTTP 请求,并实现一些基本的安全特性。这是与 Angular 开发没有直接关系的服务器端代码,所以如果它的目的不是很明显,也不用担心。我在第九章解释认证授权过程,包括如何用 Angular 认证用户。
Caution
除了 SportsStore 应用之外,不要使用清单 7-4 中的代码。它包含硬连线到代码中的弱密码。这对于 SportsStore 项目来说很好,因为重点是用 Angular 进行客户端开发,但是这并不适合真实的项目。
准备 HTML 文件
每个 Angular web 应用都依赖于浏览器加载的 HTML 文件,该文件用于加载和启动应用。编辑SportsStore/src文件夹中的index.html文件,删除占位符内容,并添加清单 7-5 中所示的元素。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>SportsStore</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body class="m-2">
<app>SportsStore Will Go Here</app>
</body>
</html>
Listing 7-5.Preparing the index.html File in the src Folder
HTML 文档包含一个app元素,它是 SportsStore 功能的占位符。还有一个base元素,这是 Angular URL 路由特性所需要的,我在第八章将它添加到了 SportsStore 项目中。
创建文件夹结构
设置 Angular 应用的一个重要部分是创建文件夹结构。ng new命令建立一个项目,将应用的所有文件放在src文件夹中,Angular 文件放在src/app文件夹中。为了给项目添加一些结构,创建如表 7-1 所示的附加文件夹。
表 7-1。
SportsStore 项目所需的附加文件夹
|文件夹
|
描述
|
| --- | --- |
| SportsStore/src/app/model | 该文件夹将包含数据模型的代码。 |
| SportsStore/src/app/store | 该文件夹将包含基本购物功能。 |
| SportsStore/src/app/admin | 该文件夹将包含管理功能。 |
运行示例应用
确保所有更改都已保存,并在 SportsStore 文件夹中运行以下命令:
ng serve --open
该命令将启动由ng new命令设置的开发工具,每当检测到变更时,该工具将自动编译并打包src文件夹中的代码和内容文件。一个新的浏览器窗口将会打开并显示如图 7-1 所示的内容。
图 7-1。
运行示例应用
开发 web 服务器将在端口 4200 上启动,因此应用的 URL 将是http://localhost:4200。您不必包含 HTML 文档的名称,因为index.html是服务器响应的默认文件。(您将在浏览器的 JavaScript 控制台中看到错误,暂时可以忽略。)
启动 RESTful Web 服务
要启动 RESTful web 服务,打开一个新的命令提示符,导航到SportsStore文件夹,并运行以下命令:
npm run json
RESTful web 服务被配置为在端口 3500 上运行。为了测试 web 服务请求,使用浏览器请求 URL http://localhost:3500/products/1。浏览器将显示清单 7-3 中定义的产品之一的 JSON 表示,如下所示:
{
"id": 1,
"name": "Kayak",
"category": "Watersports",
"description": "A boat for one person",
"price": 275
}
准备 Angular 投影特征
每个 Angular 的项目都需要一些基本的准备。在接下来的小节中,我将替换占位符内容来构建 SportsStore 应用的基础。
更新根组件
根组件是 Angular 构建块,它将管理清单 7-5 中 HTML 文档中app元素的内容。一个应用可以包含许多组件,但是总有一个根组件负责呈现给用户的顶层内容。我在SportsStore/src/app文件夹中编辑了名为app.component.ts的文件,并用清单 7-6 中的语句替换了现有代码。
import { Component } from "@angular/core";
@Component({
selector: "app",
template: `<div class="bg-success p-2 text-center text-white">
This is SportsStore
</div>`
})
export class AppComponent { }
Listing 7-6.The Contents of the app.component.ts File in the src/app Folder
@Component装饰器告诉 Angular,AppComponent类是一个组件,它的属性配置如何应用组件。所有组件属性都在第十七章中描述,但是列表中显示的属性是最基本和最常用的。selector属性告诉 Angular 如何在 HTML 文档中应用组件,而template属性定义了组件将显示的 HTML 内容。组件可以定义内联模板,就像这样,或者它们使用外部 HTML 文件,这样可以更容易地管理复杂的内容。
在AppComponent类中没有代码,因为 Angular 项目中的根组件只是用来管理显示给用户的内容。最初,我将手动管理根组件显示的内容,但是在第八章中,我使用了一个名为 URL 路由的特性来根据用户动作自动调整内容。
更新根模块
有两种 Angular 模块:特征模块和根模块。功能模块用于对相关的应用功能进行分组,以使应用更易于管理。我为应用的每个主要功能区域创建功能模块,包括数据模型、呈现给用户的商店界面和管理界面。
根模块用于描述对 Angular 的应用。描述包括运行应用需要哪些特性模块,应该加载哪些定制特性,以及根组件的名称。根组件文件的常规名称是app.module.ts,它创建在SportsStore/src/app文件夹中。目前不需要对此文件进行任何更改;清单 7-7 显示了它的初始内容。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Listing 7-7.The Contents of the app.module.ts File in the src/app Folder
与根组件类似,根模块的类中没有代码。这是因为根模块的真正存在只是为了通过@NgModule装饰器提供信息。属性imports告诉 Angular 它应该加载BrowserModule特性模块,该模块包含 web 应用所需的核心 Angular 特性。
declarations属性告诉 Angular 它应该加载根组件,providers属性告诉 Angular 应用使用的共享对象,bootstrap属性告诉 Angular 根组件是AppComponent类。当我向 SportsStore 应用添加特性时,我将向这个装饰器的属性添加信息,但是这个基本配置足以启动应用。
检查引导文件
下一步是启动应用的引导文件。这本书的重点是使用 Angular 来创建在 web 浏览器中工作的应用,但是 Angular 平台可以移植到不同的环境中。引导文件使用 Angular 浏览器平台加载根模块并启动应用。不需要对SportsStore/src文件夹中的main.ts文件的内容进行修改,如清单 7-8 所示。
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
Listing 7-8.The Contents of the main.ts File in the src Folder
开发工具检测项目文件的变化,编译代码文件,并自动重新加载浏览器,产生如图 7-2 所示的内容。
图 7-2。
启动 SportsStore 应用
启动数据模型
开始任何新项目的最佳地方是数据模型。我想让您看到一些工作中的 Angular 特性,因此,我将使用虚拟数据来实现一些基本功能,而不是从头到尾定义数据模型。在第八章中,我将使用这些数据创建面向用户的特性,然后返回到数据模型,将其连接到 RESTful web 服务。
创建模型类
每个数据模型都需要描述数据模型中包含的数据类型的类。对于 SportsStore 应用,这意味着描述商店中销售的产品和从客户那里收到的订单的类。
能够描述产品就足以开始使用 SportsStore 应用,我将创建其他模型类来支持我实现的特性。我在SportsStore/src/app/model文件夹中创建了一个名为product.model.ts的文件,并添加了清单 7-9 中所示的代码。
export class Product {
constructor(
public id?: number,
public name?: string,
public category?: string,
public description?: string,
public price?: number) { }
}
Listing 7-9.The Contents of the product.model.ts File in the src/app/model Folder
Product类定义了一个接受id、name、category、description和price属性的构造函数,这些属性对应于用来填充清单 7-3 中 RESTful web 服务的数据结构。参数名称后面的问号(?字符)表示这些是可选参数,在使用Product类创建新对象时可以省略,这在编写使用 HTML 表单填充模型对象属性的应用时很有用。
创建虚拟数据源
为了准备从虚拟数据到真实数据的转换,我将使用数据源提供应用数据。应用的其余部分不知道数据来自哪里,这将无缝地切换到使用 HTTP 请求获取数据。
我在SportsStore/src/app/model文件夹中添加了一个名为static.datasource.ts的文件,并定义了清单 7-10 中所示的类。
import { Injectable } from "@angular/core";
import { Product } from "./product.model";
import { Observable, from } from "rxjs";
@Injectable()
export class StaticDataSource {
private products: Product[] = [
new Product(1, "Product 1", "Category 1", "Product 1 (Category 1)", 100),
new Product(2, "Product 2", "Category 1", "Product 2 (Category 1)", 100),
new Product(3, "Product 3", "Category 1", "Product 3 (Category 1)", 100),
new Product(4, "Product 4", "Category 1", "Product 4 (Category 1)", 100),
new Product(5, "Product 5", "Category 1", "Product 5 (Category 1)", 100),
new Product(6, "Product 6", "Category 2", "Product 6 (Category 2)", 100),
new Product(7, "Product 7", "Category 2", "Product 7 (Category 2)", 100),
new Product(8, "Product 8", "Category 2", "Product 8 (Category 2)", 100),
new Product(9, "Product 9", "Category 2", "Product 9 (Category 2)", 100),
new Product(10, "Product 10", "Category 2", "Product 10 (Category 2)", 100),
new Product(11, "Product 11", "Category 3", "Product 11 (Category 3)", 100),
new Product(12, "Product 12", "Category 3", "Product 12 (Category 3)", 100),
new Product(13, "Product 13", "Category 3", "Product 13 (Category 3)", 100),
new Product(14, "Product 14", "Category 3", "Product 14 (Category 3)", 100),
new Product(15, "Product 15", "Category 3", "Product 15 (Category 3)", 100),
];
getProducts(): Observable<Product[]> {
return from([this.products]);
}
}
Listing 7-10.The Contents of the static.datasource.ts File in the src/app/model Folder
StaticDataSource类定义了一个名为getProducts的方法,它返回虚拟数据。调用getProducts方法的结果是一个Observable<Product[]>,它是一个产生Product对象数组的Observable。
Observable类由 Reactive Extensions 包提供,Angular 使用它来处理应用中的状态变化。我在第二十三章中描述了Observable类,但是对于这一章来说,知道一个Observable对象代表一个将在未来某个时刻产生结果的异步任务就足够了。Angular 公开了它对某些特性的Observable对象的使用,包括发出 HTTP 请求,这就是为什么getProducts方法返回一个Observable<Product[]>而不是简单地同步返回数据。
@Injectable装饰器已经应用于StaticDataSource类。这个装饰器用来告诉 Angular 这个类将被用作一个服务,它允许其他类通过一个叫做依赖注入的特性来访问它的功能,这个特性在第 19 和 20 章中有描述。随着应用的形成,您将看到服务是如何工作的。
Tip
注意,我必须从@angular/core JavaScript 模块导入Injectable,这样我就可以应用@Injectable装饰器。我不会强调我为 SportsStore 示例导入的所有不同的 Angular 类,但是您可以在描述它们相关特性的章节中获得完整的细节。
创建模型库
数据源负责向应用提供它所需要的数据,但是对这些数据的访问通常是通过一个库来完成的,这个库负责将这些数据分发到各个应用构建模块,这样就可以隐藏如何获得数据的细节。我在SportsStore/src/app/model文件夹中添加了一个名为product.repository.ts的文件,并定义了清单 7-11 中所示的类。
import { Injectable } from "@angular/core";
import { Product } from "./product.model";
import { StaticDataSource } from "./static.datasource";
@Injectable()
export class ProductRepository {
private products: Product[] = [];
private categories: string[] = [];
constructor(private dataSource: StaticDataSource) {
dataSource.getProducts().subscribe(data => {
this.products = data;
this.categories = data.map(p => p.category)
.filter((c, index, array) => array.indexOf(c) == index).sort();
});
}
getProducts(category: string = null): Product[] {
return this.products
.filter(p => category == null || category == p.category);
}
getProduct(id: number): Product {
return this.products.find(p => p.id == id);
}
getCategories(): string[] {
return this.categories;
}
}
Listing 7-11.The Contents of the product.repository.ts File in the src/app/model Folder
当 Angular 需要创建一个新的存储库实例时,它将检查这个类,并发现它需要一个StaticDataSource对象来调用ProductRepository构造函数并创建一个新对象。
存储库构造器调用数据源的getProducts方法,然后对返回的Observable对象使用subscribe方法来接收产品数据。关于Observable物体如何工作的细节,参见第二十三章。
创建特征模块
我将定义一个 Angular 特征模型,它将允许数据模型功能在应用中的其他地方轻松使用。我在SportsStore/src/app/model文件夹中添加了一个名为model.module.ts的文件,并定义了清单 7-12 中所示的类。
Tip
不要担心所有的文件名似乎相似和混乱。在阅读本书的其他章节时,您将会习惯 Angular 应用的结构方式,并且很快就能看到 Angular 项目中的文件,知道它们的用途。
import { NgModule } from "@angular/core";
import { ProductRepository } from "./product.repository";
import { StaticDataSource } from "./static.datasource";
@NgModule({
providers: [ProductRepository, StaticDataSource]
})
export class ModelModule { }
Listing 7-12.The Contents of the model.module.ts File in the src/app/model Folder
@NgModule装饰器用于创建特性模块,它的属性告诉 Angular 应该如何使用模块。这个模块中只有一个属性providers,它告诉 Angular 哪些类应该被用作依赖注入特性的服务,这在第 19 和 20 章中有描述。特征模块和@NgModule装饰器在第二十一章中描述。
开店
既然数据模型已经就绪,我可以开始构建商店功能,这将让用户看到待售的产品并为它们下订单。商店的基本结构将是一个两列布局,带有允许过滤产品列表的类别按钮和一个包含产品列表的表格,如图 7-3 所示。
图 7-3。
商店的基本结构
在接下来的部分中,我将使用 Angular 特征和模型中的数据来创建图中所示的布局。
创建商店组件和模板
随着您对 Angular 的熟悉,您将会了解到,可以将各种功能组合起来,以不同的方式解决同一问题。我试图在 SportsStore 项目中引入一些变化,以展示一些重要的 Angular 特征,但为了能够快速启动项目,我暂时将事情保持简单。
考虑到这一点,store 功能的起点将是一个新的组件,它是一个向 HTML 模板提供数据和逻辑的类,该模板包含动态生成内容的数据绑定。我在SportsStore/src/app/store文件夹中创建了一个名为store.component.ts的文件,并定义了清单 7-13 中所示的类。
import { Component } from "@angular/core";
import { Product } from "../model/product.model";
import { ProductRepository } from "../model/product.repository";
@Component({
selector: "store",
templateUrl: "store.component.html"
})
export class StoreComponent {
constructor(private repository: ProductRepository) { }
get products(): Product[] {
return this.repository.getProducts();
}
get categories(): string[] {
return this.repository.getCategories();
}
}
Listing 7-13.The Contents of the store.component.ts File in the src/app/store Folder
@Component装饰器已经应用于StoreComponent类,它告诉 Angular 它是一个组件。装饰器的属性告诉 Angular 如何将组件应用于 HTML 内容(使用一个叫做store的元素)以及如何找到组件的模板(在一个叫做store.component.html的文件中)。
StoreComponent类提供了支持模板内容的逻辑。构造函数接收一个ProductRepository对象作为参数,它是通过第 20 和 21 章中描述的依赖注入特性提供的。该组件定义了products和categories属性,这些属性将用于使用从存储库中获得的数据在模板中生成 HTML 内容。为了给组件提供模板,我在SportsStore/src/app/store文件夹中创建了一个名为store.component.html的文件,并添加了清单 7-14 中所示的 HTML 内容。
<div class="container-fluid">
<div class="row">
<div class="col bg-dark text-white">
<a class="navbar-brand">SPORTS STORE</a>
</div>
</div>
<div class="row text-white">
<div class="col-3 bg-info p-2">
{{categories.length}} Categories
</div>
<div class="col-9 bg-success p-2">
{{products.length}} Products
</div>
</div>
</div>
Listing 7-14.The Contents of the store.component.html File in the src/app/store Folder
模板很简单,只是入门。大多数元素为商店布局提供了结构,并应用了一些引导 CSS 类。目前只有两个 Angular 数据绑定,分别用{{和}}字符表示。这些是字符串插值绑定,它们告诉 Angular 计算绑定表达式并将结果插入到元素中。这些绑定中的表达式显示了商店组件提供的产品和类别的数量。
创建商店功能模块
目前还没有太多的商店功能,但即使如此,还需要做一些额外的工作来将其连接到应用的其余部分。为了创建商店功能的 Angular 特征模块,我在SportsStore/src/app/store文件夹中创建了一个名为store.module.ts的文件,并添加了清单 7-15 中所示的代码。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { ModelModule } from "../model/model.module";
import { StoreComponent } from "./store.component";
@NgModule({
imports: [ModelModule, BrowserModule, FormsModule],
declarations: [StoreComponent],
exports: [StoreComponent]
})
export class StoreModule { }
Listing 7-15.The Contents of the store.module.ts File in the src/app/store Folder
@NgModule装饰器配置模块,使用imports属性告诉 Angular 存储模块依赖于模型模块以及BrowserModule和FormsModule,它们包含 web 应用和 HTML 表单元素的标准 Angular 特性。装饰器使用declarations属性告诉 Angular 关于StoreComponent类的信息,而exports属性告诉 Angular 该类也可以用于应用的其他部分,这很重要,因为它将被根模块使用。
更新根组件和根模块
应用基本模型和商店功能需要更新应用的根模块以导入两个特征模块,还需要更新根模块的模板以添加商店模块中的组件将应用到的 HTML 元素。清单 7-16 显示了对根组件模板的更改。
import { Component } from "@angular/core";
@Component({
selector: "app",
template: "<store></store>"
})
export class AppComponent { }
Listing 7-16.Adding an Element in the app.component.ts File in the src/app Folder
store元素替换了根组件模板中先前的内容,并对应于清单 7-13 中@Component装饰器的selector属性值。清单 7-17 显示了根模块所需的更改,以便对包含商店功能的特征模块进行 Angular 加载。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppComponent } from "./app.component";
import { StoreModule } from "./store/store.module";
@NgModule({
imports: [BrowserModule, StoreModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
Listing 7-17.Importing Feature Modules in the app.module.ts File in the src/app Folder
当您将更改保存到根模块时,Angular 将拥有加载应用和显示来自 store 模块的内容所需的所有细节,如图 7-4 所示。
如果您没有看到预期的结果,那么停止 Angular 开发工具,并使用ng serve命令再次启动它们。这将重复项目的构建过程,并且应该反映您所做的更改。
上一节中创建的所有构件一起工作来显示内容——诚然很简单——显示有多少产品以及它们属于多少类别。
图 7-4。
SportsStore 应用的基本功能
添加商店特色产品详情
Angular 开发的本质是从一个缓慢的开始开始,因为项目的基础已经就位,基本的构建块已经创建。但是一旦这样做了,就可以相对容易地创建新的功能。在接下来的部分中,我将向商店添加一些功能,以便用户可以看到提供的产品。
显示产品详情
显而易见的起点是展示产品的细节,这样顾客就可以看到有什么优惠。清单 7-18 通过数据绑定将 HTML 元素添加到商店组件的模板中,为组件提供的每个产品生成内容。
<div class="container-fluid">
<div class="row">
<div class="col bg-dark text-white">
<a class="navbar-brand">SPORTS STORE</a>
</div>
</div>
<div class="row">
<div class="col-3 bg-info p-2 text-white">
{{categories.length}} Categories
</div>
<div class="col-9 p-2">
<div *ngFor="let product of products" class="card m-1 p-1 bg-light">
<h4>
{{product.name}}
<span class="badge badge-pill badge-primary float-right">
{{ product.price | currency:"USD":"symbol":"2.2-2" }}
</span>
</h4>
<div class="card-text bg-white p-1">{{product.description}}</div>
</div>
</div>
</div>
Listing 7-18.Adding Elements in the store.component.html File in the src/app/store Folder
大多数元素控制内容的布局和外观。最重要的变化是添加了 Angular 数据绑定表达式。
...
<div *ngFor="let product of products" class="card m-1 p-1 bg-light">
...
这是一个指令的例子,它转换应用它的 HTML 元素。这个特定的指令叫做ngFor,它通过为组件的products属性返回的每个对象复制div元素来转换它。Angular 包括一系列内置指令,执行最常见的所需任务,如第十三章所述。
因为它复制了div元素,所以当前对象被赋给一个名为product的变量,这允许它在其他数据绑定中被引用,比如这个,它将当前产品的name描述属性的值作为div元素的内容插入:
...
<div class="card-text p-1 bg-white">{{product.description}}</div>
...
并非应用数据模型中的所有数据都可以直接显示给用户。Angular 包含一个名为 pipes 的特性,这些类用于转换或准备数据值,以便在数据绑定中使用。Angular 包含了几个内置管道,包括currency管道,它将数字值格式化为货币,如下所示:
...
{{ product.price | currency:"USD":"symbol":"2.2-2" }}
...
应用管道的语法可能有点笨拙,但是这个绑定中的表达式告诉 Angular 使用currency管道格式化当前产品的price属性,使用美国的货币惯例。保存对模板的修改,您将看到数据模型中的产品列表显示为一个长列表,如图 7-5 所示。
图 7-5。
显示产品信息
添加类别选择
添加对按类别过滤产品列表的支持需要准备 store 组件,以便它跟踪用户想要显示的类别,并需要更改检索数据的方式以使用该类别,如清单 7-19 所示。
import { Component } from "@angular/core";
import { Product } from "../model/product.model";
import { ProductRepository } from "../model/product.repository";
@Component({
selector: "store",
templateUrl: "store.component.html"
})
export class StoreComponent {
public selectedCategory = null;
constructor(private repository: ProductRepository) {}
get products(): Product[] {
return this.repository.getProducts(this.selectedCategory);
}
get categories(): string[] {
return this.repository.getCategories();
}
changeCategory(newCategory?: string) {
this.selectedCategory = newCategory;
}
}
Listing 7-19.Adding Category Filtering in the store.component.ts File in the src/app/store Folder
这些变化很简单,因为它们建立在本章开始时花了很长时间创建的基础上。selectedCategory属性被分配给用户选择的类别(其中null表示所有类别),并在updateData方法中用作getProducts方法的参数,将过滤委托给数据源。changeCategory方法将这两个成员放在一个方法中,当用户选择类别时可以调用这个方法。
清单 7-20 显示了对组件模板的相应更改,为用户提供了一组按钮,用于更改所选的类别,并显示选择了哪个类别。
<div class="container-fluid">
<div class="row">
<div class="col bg-dark text-white">
<a class="navbar-brand">SPORTS STORE</a>
</div>
</div>
<div class="row">
<div class="col-3 p-2">
<button class="btn btn-block btn-outline-primary" (click)="changeCategory()">
Home
</button>
<button *ngFor="let cat of categories"
class="btn btn-outline-primary btn-block"
[class.active]="cat == selectedCategory" (click)="changeCategory(cat)">
{{cat}}
</button>
</div>
<div class="col-9 p-2">
<div *ngFor="let product of products" class="card m-1 p-1 bg-light">
<h4>
{{product.name}}
<span class="badge badge-pill badge-primary float-right">
{{ product.price | currency:"USD":"symbol":"2.2-2" }}
</span>
</h4>
<div class="card-text bg-white p-1">{{product.description}}</div>
</div>
</div>
</div>
</div>
Listing 7-20.Adding Category Buttons in the store.component.html File in the src/app/store Folder
模板中有两个新的button元素。第一个是 Home 按钮,它有一个事件绑定,当按钮被单击时调用组件的changeCategory方法。该方法没有提供任何参数,其效果是将类别设置为null,并选择所有产品。
ngFor绑定已经应用于另一个button元素,其中一个表达式将为组件的categories属性返回的数组中的每个值重复该元素。button有一个click事件绑定,其表达式调用changeCategory方法选择当前类别,该类别将过滤显示给用户的产品。还有一个class绑定,当与按钮关联的类别是选中的类别时,它将button元素添加到active类中。当类别被过滤时,这为用户提供了视觉反馈,如图 7-6 所示。
图 7-6。
选择产品类别
添加产品分页
按类别过滤产品有助于使产品列表更易于管理,但更典型的方法是将列表分成更小的部分,并将每个部分显示为一个页面,并带有在页面之间移动的导航按钮。清单 7-21 增强了商店组件,这样它就可以跟踪当前页面和页面上的商品数量。
import { Component } from "@angular/core";
import { Product } from "../model/product.model";
import { ProductRepository } from "../model/product.repository";
@Component({
selector: "store",
templateUrl: "store.component.html"
})
export class StoreComponent {
public selectedCategory = null;
public productsPerPage = 4;
public selectedPage = 1;
constructor(private repository: ProductRepository) {}
get products(): Product[] {
let pageIndex = (this.selectedPage - 1) * this.productsPerPage
return this.repository.getProducts(this.selectedCategory)
.slice(pageIndex, pageIndex + this.productsPerPage);
}
get categories(): string[] {
return this.repository.getCategories();
}
changeCategory(newCategory?: string) {
this.selectedCategory = newCategory;
}
changePage(newPage: number) {
this.selectedPage = newPage;
}
changePageSize(newSize: number) {
this.productsPerPage = Number(newSize);
this.changePage(1);
}
get pageNumbers(): number[] {
return Array(Math.ceil(this.repository
.getProducts(this.selectedCategory).length / this.productsPerPage))
.fill(0).map((x, i) => i + 1);
}
}
Listing 7-21.Adding Pagination Support in the store.component.ts File in the src/app/store Folder
这个清单中有两个新特性。第一个是获取产品页面的能力,第二个是改变页面的大小,允许改变每个页面包含的产品数量。
奇怪的是,该组件必须解决这个问题。Angular 提供的内置ngFor指令有一个限制,它只能为数组或集合中的对象生成内容,而不能使用计数器。由于我需要生成带编号的页面导航按钮,这意味着我需要创建一个包含我需要的数字的数组,如下所示:
...
return Array(Math.ceil(this.repository.getProducts(this.selectedCategory).length
/ this.productsPerPage)).fill(0).map((x, i) => i + 1);
...
该语句创建一个新数组,用值0填充它,然后使用map方法生成一个带有数字序列的新数组。这足以很好地实现分页特性,但感觉有些笨拙,我将在下一节演示一种更好的方法。清单 7-22 展示了对商店组件模板的更改,以实现分页特性。
<div class="container-fluid">
<div class="row">
<div class="col bg-dark text-white">
<a class="navbar-brand">SPORTS STORE</a>
</div>
</div>
<div class="row">
<div class="col-3 p-2">
<button class="btn btn-block btn-outline-primary" (click)="changeCategory()">
Home
</button>
<button *ngFor="let cat of categories"
class="btn btn-outline-primary btn-block"
[class.active]="cat == selectedCategory" (click)="changeCategory(cat)">
{{cat}}
</button>
</div>
<div class="col-9 p-2">
<div *ngFor="let product of products" class="card m-1 p-1 bg-light">
<h4>
{{product.name}}
<span class="badge badge-pill badge-primary float-right">
{{ product.price | currency:"USD":"symbol":"2.2-2" }}
</span>
</h4>
<div class="card-text bg-white p-1">{{product.description}}</div>
</div>
<div class="form-inline float-left mr-1">
<select class="form-control" [value]="productsPerPage"
(change)="changePageSize($event.target.value)">
<option value="3">3 per Page</option>
<option value="4">4 per Page</option>
<option value="6">6 per Page</option>
<option value="8">8 per Page</option>
</select>
</div>
<div class="btn-group float-right">
<button *ngFor="let page of pageNumbers" (click)="changePage(page)"
class="btn btn-outline-primary"
[class.active]="page == selectedPage">
{{page}}
</button>
</div>
</div>
</div>
</div>
Listing 7-22.Adding Pagination in the store.component.html File in the src/app/store Folder
新元素添加了一个select元素,允许改变页面的大小,以及一组在产品页面中导航的按钮。新元素具有数据绑定,将它们绑定到组件提供的属性和方法。结果是一组更易于管理的产品,如图 7-7 所示。
Tip
清单 7-22 中的select元素由静态定义的option元素填充,而不是使用来自组件的数据创建的。这样做的一个影响是,当选择的值被传递给changePageSize方法时,它将是一个string值,这就是为什么在用于设置清单 7-21 中的页面大小之前,参数被解析为一个number。从 HTML 元素接收数据值时必须小心,以确保它们是预期的类型。在这种情况下,TypeScript 类型批注没有帮助,因为数据绑定表达式是在运行时计算的,而此时 TypeScript 编译器已经生成了不包含额外类型信息的 JavaScript 代码。
图 7-7。
产品分页
创建自定义指令
在本节中,我将创建一个自定义指令,这样我就不必生成一个充满数字的数组来创建页面导航按钮。Angular 提供了大量的内置指令,但是创建您自己的指令来解决特定于您的应用的问题或者支持内置指令没有的特性是一个简单的过程。我在src/app/store文件夹中添加了一个名为counter.directive.ts的文件,并用它来定义清单 7-23 中所示的类。
import {
Directive, ViewContainerRef, TemplateRef, Input, Attribute, SimpleChanges
} from "@angular/core";
@Directive({
selector: "[counterOf]"
})
export class CounterDirective {
constructor(private container: ViewContainerRef,
private template: TemplateRef<Object>) {
}
@Input("counterOf")
counter: number;
ngOnChanges(changes: SimpleChanges) {
this.container.clear();
for (let i = 0; i < this.counter; i++) {
this.container.createEmbeddedView(this.template,
new CounterDirectiveContext(i + 1));
}
}
}
class CounterDirectiveContext {
constructor(public $implicit: any) { }
}
Listing 7-23.The Contents of the counter.directive.ts File in the src/app/store Folder
这是一个结构指令的例子,在第十六章中有详细描述。这个指令通过一个counter属性应用于元素,并依赖 Angular 提供的特殊特性来重复创建内容,就像内置的ngFor指令一样。在这种情况下,自定义指令不会生成集合中的每个对象,而是生成一系列可用于创建页面导航按钮的数字。
Tip
该指令删除它创建的所有内容,并在页数改变时重新开始。在更复杂的指令中,这可能是一个昂贵的过程,我会在第十六章解释如何提高性能。
要使用该指令,必须将其添加到其特性模块的declarations属性中,如清单 7-24 所示。
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { ModelModule } from "../model/model.module";
import { StoreComponent } from "./store.component";
import { CounterDirective } from "./counter.directive";
@NgModule({
imports: [ModelModule, BrowserModule, FormsModule],
declarations: [StoreComponent, CounterDirective],
exports: [StoreComponent]
})
export class StoreModule { }
Listing 7-24.Registering the Custom Directive in the store.module.ts File in the src/app/store Folder
现在该指令已经注册,可以在商店组件的模板中使用它来替换ngFor指令,如清单 7-25 所示。
<div class="container-fluid">
<div class="row">
<div class="col bg-dark text-white">
<a class="navbar-brand">SPORTS STORE</a>
</div>
</div>
<div class="row">
<div class="col-3 p-2">
<button class="btn btn-block btn-outline-primary" (click)="changeCategory()">
Home
</button>
<button *ngFor="let cat of categories"
class="btn btn-outline-primary btn-block"
[class.active]="cat == selectedCategory" (click)="changeCategory(cat)">
{{cat}}
</button>
</div>
<div class="col-9 p-2">
<div *ngFor="let product of products" class="card m-1 p-1 bg-light">
<h4>
{{product.name}}
<span class="badge badge-pill badge-primary float-right">
{{ product.price | currency:"USD":"symbol":"2.2-2" }}
</span>
</h4>
<div class="card-text bg-white p-1">{{product.description}}</div>
</div>
<div class="form-inline float-left mr-1">
<select class="form-control" [value]="productsPerPage"
(change)="changePageSize($event.target.value)">
<option value="3">3 per Page</option>
<option value="4">4 per Page</option>
<option value="6">6 per Page</option>
<option value="8">8 per Page</option>
</select>
</div>
<div class="btn-group float-right">
<button *counter="let page of pageCount" (click)="changePage(page)"
class="btn btn-outline-primary" [class.active]="page == selectedPage">
{{page}}
</button>
</div>
</div>
</div>
</div>
Listing 7-25.Replacing the Built-in Directive in the store.component.html File in the src/app/store Folder
新的数据绑定依赖于一个名为pageCount的属性来配置自定义指令。在清单 7-26 中,我用一个提供表达式值的简单的number代替了数字数组。
import { Component } from "@angular/core";
import { Product } from "../model/product.model";
import { ProductRepository } from "../model/product.repository";
@Component({
selector: "store",
templateUrl: "store.component.html"
})
export class StoreComponent {
public selectedCategory = null;
public productsPerPage = 4;
public selectedPage = 1;
constructor(private repository: ProductRepository) {}
get products(): Product[] {
let pageIndex = (this.selectedPage - 1) * this.productsPerPage
return this.repository.getProducts(this.selectedCategory)
.slice(pageIndex, pageIndex + this.productsPerPage);
}
get categories(): string[] {
return this.repository.getCategories();
}
changeCategory(newCategory?: string) {
this.selectedCategory = newCategory;
}
changePage(newPage: number) {
this.selectedPage = newPage;
}
changePageSize(newSize: number) {
this.productsPerPage = Number(newSize);
this.changePage(1);
}
get pageCount(): number {
return Math.ceil(this.repository
.getProducts(this.selectedCategory).length / this.productsPerPage)
}
//get pageNumbers(): number[] {
// return Array(Math.ceil(this.repository
// .getProducts(this.selectedCategory).length / this.productsPerPage))
// .fill(0).map((x, i) => i + 1);
//}
}
Listing 7-26.Supporting the Custom Directive in the store.component.ts File in the src/app/store Folder
SportsStore 应用在视觉上没有变化,但本节已经演示了可以使用定制代码来补充内置的 Angular 功能,这些代码是根据特定项目的需求定制的。
摘要
在这一章中,我启动了 SportsStore 项目。本章的前一部分花在创建项目的基础上,包括创建应用的根构建块和开始特性模块的工作。一旦有了基础,我就能够快速地添加向用户显示虚拟模型数据的特性,添加分页,并按类别过滤产品。在本章的最后,我创建了一个自定义指令来演示如何通过自定义代码来补充 Angular 提供的内置特性。在下一章,我将继续构建 SportsStore 应用。