1. 简介
构建内容
在此 Codelab 中,您将使用 Angular 构建一个外壳应用。完成后的应用将能够根据用户搜索来查看住宅详情以及查看住宅位置的详细信息。
您可以利用 Angular 强大的工具和出色的浏览器集成功能,通过 Angular 构建所有内容。
这是您今天将构建的应用
学习内容
- 如何使用 Angular CLI 构建新项目。
- 如何使用 Angular 组件构建界面。
- 如何在应用的组件和其他部分中共享数据。
- 如何在 Angular 中使用事件处理程序。
- 如何使用 Angular CLI 将应用部署到 Firebase Hosting。
您需要满足的条件
- 具备 HTML、CSS、TypeScript(或 JavaScript)、Git 和命令行方面的基础知识。
2. 环境设置
设置本地环境
为完成此 Codelab,您需要在本地机器上安装以下软件:
- Node 版本 ^12.20.2、^14.15.5 或 ^16.10.0。
- 代码编辑器 - VS Code 或您选择的其他代码编辑器。
- 适用于 VS Code 的 Angular 语言服务插件。
安装 Angular CLI
配置完所有依赖项后,您可以通过计算机上的命令行窗口安装 Angular CLI:
npm install -g @angular/cli
要确认您的配置是否正确,请在您的计算机命令行中运行以下命令:
ng –version
如果该命令运行成功,您将看到类似于以下屏幕截图的消息。
获取代码
此 Codelab 的代码包含不同分支中的中间步骤和最终解决方案。首先,从 GitHub 下载代码
- 打开新的浏览器标签页,然后转到
https://github.com/angular/introduction-to-angular
。 - 从命令行窗口分支,将代码库和
cd introduction-to-angular/
克隆到代码库中。 - 在起始代码分支中,输入
git checkout get-started
。 - 在您的首选代码编辑器中打开该代码,然后打开
introduction-to-angular
项目文件夹。 - 从命令行窗口中,运行
npm install
以安装运行服务器所需的依赖项。 - 要在后台运行 Angular 网络服务器,请打开一个单独的命令行窗口,然后运行
ng serve
来启动服务器。 - 打开一个浏览器标签页以
http://localhost:4200
。
随着应用正常运行,您可以开始构建 Fairhouse 应用。
3.创建您的首个组件
组件是 Angular 应用的核心构建块。把组件看作用于建造的砖块。一开始,砖块的能量并不是很大,但与其他砖块结合在一起,就能打造出令人惊叹的建筑。
使用 Angular 构建的应用也是如此。
组件有 3 个主要方面:
- 模板的 HTML 文件。
- 样式的 CSSfile。
- 与应用行为对应的 TypeScript 文件。
您要更新的第一个组件是 AppComponent
。
- 在代码编辑器中打开
app.component.html
;这是AppComponent
的模板文件。 - 删除此文件中的所有代码,并将其替换为以下代码:
<main>
<header><img src="../assets/logo.svg" alt="fairhouse">Fairhouse</header>
<section>
</section>
</main>
- 保存代码并检查浏览器。在开发服务器运行时,这些更改在我们保存时反映在浏览器中。
恭喜!您已成功更新第一个 Angular 应用。敬请期待更多精彩应用。让我们继续。
接下来,您将为搜索添加一个文本字段,并向界面添加一个按钮。
组件有许多优势,其中一个是能够组织界面。您将要创建一个包含文本字段、搜索按钮并最终包含位置列表的组件。
如需创建此新组件,请使用 Angular CLI。Angular CLI 是一套命令行工具,可协助构建基架、部署等。
- 从命令行输入:
ng generate component housing-list
以下是此命令的部分内容:
ng
是 Angular CLI。- 该命令会生成要执行的操作类型。在这种情况下,为某些内容生成基架。
- 该组件表示我们要创建的“内容”。
- home-list 是组件的名称。
- 接下来,将新组件添加到
AppComponent
模板中。在app.component.html
中,更新模板代码:
<main>
<header><img src="../assets/logo.svg" alt="fairhouse">Fairhouse</header>
<section>
<app-housing-list></app-housing-list>
</section>
</main>
- 保存所有文件并返回浏览器,以确认是否显示了消息
housing-list works
。 - 在代码编辑器中,转到
housing-list.component.html
,移除现有代码,然后将其替换为:
<label for="location-search">Search for a new place</label>
<input id="location-search" placeholder="Ex: Chicago"><button>Search</button>
- 在
housing-list.component.css
中,添加以下样式:
input, button {
border: solid 1px #555B6E;
border-radius: 2px;
display: inline-block;
padding: 0;
}
input {
width: 400px;
height: 40px;
border-radius: 2px 0 0 2px;
color: #888c9c;
border: solid 1px #888c9c;
padding: 0 5px 0 10px;
}
button {
width: 70px;
height: 42px;
background-color: #4468e8;
color: white;
border: solid 1px #4468e8;
border-radius: 0 2px 2px 0;
}
article {
margin: 40px 0 10px 0;
color: #202845;
}
article, article > p {
color: #202845;
}
article> p:first-of-type {
font-weight: bold;
font-size: 22pt;
}
img {
width: 490px;
border-radius: 5pt;
}
label {
display: block;
color: #888c9c;
}
- 保存文件,然后返回浏览器。
我们的 Angular 应用即将开始塑造。
4.事件处理
应用有输入字段和按钮,但没有互动。在网页上,您通常需要与控件互动并调用事件和事件处理程序的使用。您将使用此策略来构建您的应用。
您将在housing-list.component.html
中进行这些更改。
要添加点击处理程序,您需要向该按钮添加事件监听器。在 Angular 中,语法是将事件名称括在圆括号中并为其分配一个值。在这里,您可以为点击按钮时调用的方法命名。我们将其命名为 searchHousingLocations
,请务必将括号添加到此函数名称的末尾以调用该函数。
- 更新按钮代码以匹配以下代码:
<button (click)="searchHousingLocations()">Search</button>
- 保存此代码并检查浏览器。现在存在编译错误:
由于 searchHousingLocations
方法不存在,应用会抛出此错误,因此您需要进行更改。
- 在
housing-list.component.ts
中,在HousingListComponent
类的正文末尾添加一个新方法:
searchHousingLocations() {}
您很快便会填写此方法的详细信息。
- 保存此代码即可更新浏览器并解决相应错误。
下一步是获取输入字段的值,并将其作为参数传递给 searchHousingLocations
方法。您将使用名为模板变量的 Angular 功能,该功能提供对模板中的元素的引用并与其交互。
- 在
housing-list.component.html
中,添加一个名为search
的属性,将 # 标签作为输入的前缀。
<label for="location-search">Search for a new place</label>
<input id="location-search" #search><button (click)="searchHousingLocations()">Search</button>
现在,我们有了对输入的引用。我们还可以访问输入的 .value
属性。
- 将输入的值传递给
searchHousingLocations
方法。
<input id="location-search" #search><button (click)="searchHousingLocations(search.value)">Search</button>
到目前为止,您一直在传递该参数作为参数,但让我们更新一下使用该参数的方法。目前,该参数会在 console.log
命令中使用,之后还会用作搜索参数。
- 在
housing-list.component.ts
中,添加以下代码:
searchHousingLocations(searchText: string) {
console.log(searchText);
}
- 保存代码,然后在浏览器中打开 Chrome 开发者工具,然后转到 Console(控制台)标签页。在输入中输入任意值。选择搜索,并验证该值是否显示在 Chrome 开发者工具的控制台标签页中。
您已成功添加事件处理程序,应用可以接受用户的输入。
5. 搜索结果
下一步是根据用户输入显示结果。每个位置都具有以下属性:字符串、名称、城市、州/省、照片、availableUnits 的数字属性以及两个洗衣属性和 Wi-Fi 布尔值属性:
name: "Location One",
city: "Chicago",
state: "IL",
photo: "/path/to/photo.jpg",
availableUnits: 4,
wifi: true,
laundry: true
您可以将这些数据表示为普通 JavaScript 对象,但最好在 Angular 中使用 TypeScript 支持。使用类型有助于避免构建时发生错误。
我们可以使用类型来定义数据的特征,也称为“塑造数据”。在 TypeScript 中,接口通常用于此目的。我们来创建一个用于表示住房位置数据的接口。在编辑器的终端中,使用 Angular CLI 创建 HousingLocation 类型。
- 为此,请输入:
ng generate interface housing-location
- 在
housing-location.ts
中,添加接口的类型详细信息。根据我们的设计,为每个属性指定适当的类型:
export interface HousingLocation {
name: string,
city: string,
state: string,
photo: string,
availableUnits: number,
wifi: boolean,
laundry: boolean,
}
- 保存文件并打开
app.component.ts
。 - 通过从
./housing-location
导入住址位置接口来创建包含代表住房位置数据的数组。
import { HousingLocation } from './housing-location';
- 更新
AppComponent
类,使其包含名为housingLocationList
、类型为HousingLocation[]
的属性。使用以下值填充数组:
housingLocationList: HousingLocation[] = [
{
name: "Acme Fresh Start Housing",
city: "Chicago",
state: "IL",
photo: "../assets/housing-1.jpg",
availableUnits: 4,
wifi: true,
laundry: true,
},
{
name: "A113 Transitional Housing",
city: "Santa Monica",
state: "CA",
photo: "../assets/housing-2.jpg",
availableUnits: 0,
wifi: false,
laundry: true,
},
{
name: "Warm Beds Housing Support",
city: "Juneau",
state: "AK",
photo: "../assets/housing-3.jpg",
availableUnits: 1,
wifi: false,
laundry: false,
}
];
您无需通过实例化类的新实例来获取对象,我们便可利用该接口提供的类型信息。对象中的数据必须具有相同的“形状”,也就是说,它必须与接口上定义的属性匹配。
数据存储在 app.component.ts
中,但我们需要与其他组件共享这些数据。一种解决方案是使用 Angular 中的服务,但为了降低应用的复杂性,我们将使用 Angular 提供的输入修饰器。输入修饰器允许组件从模板接收值。您将使用它与 HousingListComponent
共享 housingLocationList
数组。
- 在
housing-list.component.ts
中,从@angular/core
导入input
,并从./housingLocation
导入HousingLocation
。
import { Component, OnInit, Input } from '@angular/core';
import {HousingLocation } from '../housing-location';
- 在组件类的正文中创建一个名为
locationList
的属性。您将使用Input
作为locationList
的修饰器。
export class HousingListComponent implements OnInit {
@Input() locationList: HousingLocation[] = [];
...
}
此属性的类型设置为 HousingLocation[]
。
- 在
app.component.html
中,更新app-housing-list
元素以包含名为locationList
的属性,并将值设置为housingLocationList
。
<main>
...
<app-housing-list [locationList]="housingLocationList"></app-housing-list>
</main>
locationList
属性必须用方括号 ( [ ]
) 括起来,以便 Angular 可以将 locationList 属性的值动态绑定到变量或表达式。否则,Angular 将等号右侧的值视为字符串。
如果您此时遇到任何错误,请检查:
- 输入属性名称拼写与 TypeScript 类中属性的拼写一致。这种情况也很重要。
- 等号右侧的属性名称准确无误。
- 输入属性用方括号括起来。
数据共享配置已完成!下一步是在浏览器中显示结果。由于数据采用数组格式,我们需要使用 Angular 功能,让您可以循环遍历模板 *ngFor
中的数据和重复元素。
- 在
housing-list.component.html
中,更新模板中的文章元素以使用*ngFor
,以便在浏览器中显示数组条目:
<article *ngFor="let location of locationList"></article>
分配给 ngFor
属性的值是 Angular 模板语法。它会在模板中创建一个局部变量。Angular 会在开始标记和结束标记之间的 article
元素范围内使用局部变量。
如需详细了解 ngFor 和模板语法,请参阅 Angular 文档。
ngFor
为 locationList
数组的每个条目重复一篇文章元素。接下来,将显示位置变量中的值。
- 更新此模板以添加段落元素 (
<p>
)。段落元素的子元素是 location 属性中的插值:
<input #search><button (click)="searchHousingLocations(search.value)">Search</button>
<article *ngFor="let location of locationList">
<p>{{location.name}}</p>
</article>
在 Angular 模板中,您可以使用文本插值来显示带有双大括号 ({{ }}
) 语法的值。
- 保存并返回浏览器。现在,应用会为
locationList
数组中的每个数组条目显示一个标签。
数据从应用组件分享到住房列表组件,我们会逐一迭代上述各个值,以在浏览器中显示它们。
我们刚刚介绍了一些可在组件之间共享数据的方法,使用了某些新的模板语法和 ngFor 指令。
6. 过滤搜索结果
目前,该应用根据用户的搜索内容显示所有结果,而不是显示结果。如需更改此行为,您需要更新 HousingListComponent
,以使应用按预期运行。
- 在
housing-list.component.ts
中,更新HousingListComponent
以创建一个名为results
且类型为HousingLocation[]
的新属性:
export class HousingListComponent implements OnInit {
@Input() locationList: HousingLocation[] = [];
results: HousingLocation[] = [];
...
results 数组表示与用户搜索匹配的住址。下一步是更新 searchHousingLocations
方法以过滤值。
- 移除
console.log
并更新代码,将结果属性分配给对locationList
进行过滤的输出(按searchText
过滤):
searchHousingLocations(searchText: string) {
this.results = this.locationList.filter(
(location: HousingLocation) => location.city
.toLowerCase()
.includes(
searchText.toLowerCase()
));
}
在此代码中,我们使用数组过滤方法,只接受包含 searchText
的值。所有值均使用小写形式的字符串。
两点注意事项:
- 在方法内引用某个类的属性时,必须使用
this
前缀。这就是我们使用this.results
和this.locationList
的原因。 - 此处的搜索功能仅与营业地点的 city 属性匹配,但您可以更新代码以包含更多属性。
尽管此代码按原样运行,但您可以对其进行改进。
- 更新代码,以防止
searchText
为空时在数组中进行搜索:
searchHousingLocations(searchText: string) {
if (!searchText) return;
...
}
方法已更新,您需要对模板做出修改,然后结果在浏览器中显示。
- 在
housing-location.component.html
中,将ngFor
中的locationList
替换为results
:
<article *ngFor="let location of results">...</article>
- 保存代码并返回浏览器。使用输入值,从示例数据中搜索位置(例如,芝加哥)。
该应用仅显示匹配的结果:
您已完成将用户输入与搜索结果完全链接所需的其他功能。应用即将完成。
接下来,系统将显示关于该应用的更多详细信息,以完成保存。
7. 显示详细信息
应用需要支持点击搜索结果以及在详细信息面板中显示信息。HousingListComponent
知道点击哪个结果,因为在该组件中显示数据。我们需要一种方法来将 HousingListComponent
中的数据共享给父级组件 AppComponent
。
在 Angular 中,@Input()
将数据从父项发送到子项,而 @Output()
允许组件将数据从子项发送到其父组件。输出修饰器使用 EventEmitter
将任何事件通知所有监听器。在这种情况下,您需要发出一个代表搜索结果点击的事件。您要随选择事件一起发送所选内容作为载荷的一部分。
- 在
housing-list.component.ts
中,更新import
以包含@angular/core
和来自其位置的HousingLocation
中的Output
和EventEmitter
:
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { HousingLocation } from '../housing-location';
- 在
HousingListComponent
的正文中,更新代码以添加一个名为locationSelectedEvent
且类型为EventEmitter<HousingLocation>();
的新属性:
@Output() locationSelectedEvent = new EventEmitter<HousingLocation>();
locationSelectedEvent
属性使用 @Output()
进行修饰,这使它成为此组件的 API 的一部分。借助 EventEmitter
,您可以通过为类提供 HousingLocation
类型来利用泛型 API。当 locationSelectedEvent
发出某个事件时,该事件的监听器可以预期任何附加的数据的类型为 HousingLocation
。此类类型可以保障我们的开发并降低出现某些错误的可能性。
当用户点击列表中的营业地点时,我们需要触发 locationSelectedEvent
。
- 更新
HousingListComponent
以添加名为selectLocation
的新方法,该方法接受housingLocation
类型的值作为参数:
selectHousingLocation(location: HousingLocation) { }
- 在该方法的正文中,从
locationSelectedEvent
发出器发出新的事件。发出的值是用户选择的位置。
selectHousingLocation(location: HousingLocation) {
this.locationSelectedEvent.emit(location);
}
让我们将其关联到模板。
- 在
housing-list-component.html
中,更新文章元素以添加一个包含click event
的新按钮子级。此事件调用 TypeScript 类中的selectHousingLocation
方法,并传入对所点击location
的引用作为参数。
<article *ngFor="let location of results" >
<p>{{location.name}}</p>
<button (click)="selectHousingLocation(location)">View</button>
</article>
住房位置现在有一个可点击的按钮,您可以将值传递回组件。
该流程的最后一步是更新 AppComponent
以监听事件,并相应地更新显示内容。
- 在
app.component.html
中,更新app-housing-list
元素以监听locationSelectedEvent
并使用updateSelectedLocation
方法处理事件:
<app-housing-list [locationList]="housingLocationList" (locationSelectedEvent)="updateSelectedLocation($event)"></app-housing-list>
在处理模板中的事件处理脚本时,Angular 提供了 $event
。$event 参数是 HousingLocation
类型的对象,因为这是 EventEmitter
的类型参数。Angular 会为您处理所有这一切。您只需确认您的模板正确无误即可。
- 在
app.component.ts
中,更新代码以包含名为selectedLocation
且类型为HousingLocation | undefined
的新属性。
selectedLocation: HousingLocation | undefined;
它使用称为联合类型的 TypeScript 功能。联合可让变量接受多种类型中的一种。在本例中,您希望 selectedLocation
的值为 HousingLocation
或 undefined
,因为您没有为 selectedLocation
指定默认值。
您需要实现 updateSelectedLocation
。
- 添加一个名为
updateSelection
的新方法,该方法使用一个名为location
且类型为HousingLocation
的参数。
updateSelectedLocation(location: HousingLocation) { } searchHousingLocations() {}
- 在该方法的正文中,将
selectedLocation
的值设置为location
参数:
updateSelectedLocation(location: HousingLocation) {
this.selectedLocation = location;
}
完成此部分后,最后一步是更新模板以显示所选位置。
- 在
app.component.html
中,添加一个新的<article>
元素,我们将使用它来显示所选位置的属性。使用以下代码更新模板:
<article>
<img [src]="selectedLocation?.photo">
<p>{{selectedLocation?.name}}</p>
<p>{{selectedLocation?.availableUnits}}</p>
<p>{{selectedLocation?.city}}, {{selectedLocation?.state}}</p>
<p>{{selectedLocation?.laundry ? "Has laundry" : "Does Not have laundry"}}</p>
<p>{{selectedLocation?.wifi ? "Has wifi" : "Does Not have wifi"}}</p>
</article>
由于 selectedLocation
可以是 undefined
,因此您可以使用可选链运算符从媒体资源中检索值。此外,您还对 wifi
和 laundry
布尔值使用三元语法。这样就可以根据值显示自定义消息。
- 保存代码并检查浏览器。搜索地点,然后点击某个位置以显示详细信息:
看起来不错,但仍有一个问题需要解决。网页最初加载时,详细信息面板中有一些文字工件不应显示。Angular 可通过一些方式有条件地显示您将在下一步中使用的内容。
目前,我们对该应用的进展充满期待。以下是您目前为止已经实现的内容:
- 您可以使用输出修饰器和 EventEmitter 将数据从子组件共享给父组件。
- 您还已成功允许用户输入该值,然后使用该值进行搜索。
- 应用可以显示搜索结果,用户可以点击查看更多详情。
到目前为止,您做得非常好。我们来更新模板并完成应用。
8. 美化模板
界面目前包含应有条件显示的详细信息面板中的文本工件。我们将使用两个 Angular 功能,即 ng-container
和 *ngIf
。
如果您将 ngIf
指令直接应用于 article
元素,它会在用户做出第一次选择时导致布局偏移。为了改善这种体验,您可以将位置详情封装在作为 article
子级的另一个元素中。该元素没有任何样式或函数,只是向 DOM 添加了权重。为避免出现这种情况,您可以使用 ng-container
。您可对其应用指令,但这些指令不会显示在最终 DOM 中。
- 在
app.component.html
中,更新article
元素以匹配以下代码:
<article>
<ng-container>
<img [src]="selectedLocation?.photo">
<p>{{selectedLocation?.name}}</p>
<p>{{selectedLocation?.city}}, {{selectedLocation?.state}}</p>
<p>Available Units: {{selectedLocation?.availableUnits}}</p>
<p>{{selectedLocation?.laundry ? "Has laundry" : "Does Not have laundry"}}</p>
<p>{{selectedLocation?.wifi ? "Has wifi" : "Does Not have wifi"}}</p>
</ng-container>
</article>
- 接下来,将
*ngIf
属性添加到ng-container
元素中。值应当为selectedLocation
。
<article>
<ng-container *ngIf="selectedLocation">
...
</ng-container>
</article>
现在,仅当 selectedLocation
为 Truthy 时,应用才会显示 ng-container
元素的内容。
- 保存此代码,并确认浏览器在网页加载时不再显示文字伪影。
最后一项更新就是,我们可以对应用进行更新。在 housing-location.component.html
的搜索结果中显示更多详细信息。
- 在
housing-location.component.html
中,将代码更新为:
<label for="location-search">Search for a new place</label>
<input id="location-search" #search placeholder="Ex: Chicago"><button
(click)="searchHousingLocations(search.value)">Search</button>
<article *ngFor="let location of results" (click)="selectHousingLocation(location)">
<img [src]="location.photo" [alt]="location.name">
<p>{{location.name}}</p>
<p>{{location.city}}, {{location.state}}</p>
<button (click)="selectHousingLocation(location)">View</button>
</article>
- 保存代码,并返回浏览器以显示已完成的应用。
现在,该应用看起来很棒,并且完全正常运行。非常棒。
9. 恭喜
感谢您走上这段旅程,使用 Angular 构建 Fairhouse。
您使用 Angular 创建了一个界面。您已使用 Angular CLI 创建了组件和接口。然后,您使用 Angular 中强大的模板功能构建了一个功能性应用,用于显示图像、处理事件等。
后续步骤
如果您想继续构建功能,请参考以下建议:
- 数据是在应用中硬编码的。进行的重要重构是添加一项服务来包含这些数据。
- 详情页面目前显示在同一个页面上,但将详细信息移到他们自己的页面上并利用 Angular 路由会很棒。
- 另一个更新是将数据托管在静态端点,并使用 Angular 中的 HTTP 软件包在运行时加载数据。
大量的娱乐机会。