Angular 的构建块






4.24/5 (4投票s)
在这里,我们将探讨 Angular 的构建块,如组件、指令、模块、服务、组件模板。我们将探讨如何手动创建组件以及组件在应用程序中的工作方式。我们还将了解 Angular 中的依赖注入。
引言
今天,我们讨论 Angular 的构建块。基本上,Angular 本身是用 Typescript 编写的。因此,在开始学习 Angular 之前,了解 Typescript 是一个先决条件。显然,由于 Angular 是用 Typescript 编写的,如果我们想编写自己的自定义代码,那么我们肯定会用 Typescript 来编写。这是 Angular 系列的路线图
目录
在本文中,我们将涵盖很多内容
Angular 的构建块
Components
在每个 Angular 应用程序的核心,我们都有一个或多个组件。事实上,在实际应用中,我们用数十个组件来开发复杂的应用程序。组件封装了视图背后的数据、HTML 标记和逻辑。Angular 采用基于组件的架构,允许我们处理更小、更易于维护且可在不同地方重用的部分。
每个应用程序都必须有一个组件,我们称之为 appcomponent 或根组件。真实的 Angular 应用本质上是一个组件树,以 appcomponent 为起点。
模块
模块是相关组件的容器。每个 Angular 应用至少有一个模块,我们称之为 app module。随着应用程序的增长,我们可能希望将模块分解成更小、更易于维护的模块。随着应用程序的增长,我们需要将我们的 app module 分解成更小的子模块,每个模块负责特定的部分。它们里面有相关的组件。
组件
让我们从一些实际操作开始。
实际上,我们需要遵循 3 个步骤
- 创建组件。
- 在模块中注册组件。
- 将元素添加到 HTML 标记中。
打开 Visual Studio Code 并构建项目。
PS > ng serve
在浏览器中打开 URL (https://:4200/)。
现在让我们创建组件。
创建组件
在 Visual Studio Code 中打开文件面板,然后在项目目录中,打开 'src' 文件夹 > 'app' 文件夹
在这里,我们想添加一个显示courses
的组件。所以,我们创建一个文件并将其命名为 'courses.component.ts'。这是我们使用 Angular 应用程序时的约定。如果组件有多个名称,如 'course form
',那么我们将用连字符 'course-form.component.ts' 来分隔它们。
在这里,我们开始在 'courses.component.ts' 中创建一个纯粹的 TypeScript 类。
class CoursesComponent {
}
所以为了让 Angular 识别这个类,我们需要导出它。
export class CoursesComponent {
}
到目前为止,我们有这个纯粹的 TypeScript 类。它还不是一个组件。为了将其转换为组件,我们需要为其添加一些 Angular 可识别的元数据。我们通过装饰器来实现这一点。在 Angular 中,我们有一个名为 component 的装饰器,可以将其附加到类上,从而使该类成为一个组件。所以我们需要在顶部导入这个装饰器。
正如我们所见,这个@Component()
装饰器函数需要 1 个参数。在这里,我们将创建一个对象。在这个对象中,我们将创建 1 个或多个属性来告诉这个组件如何工作。例如,我们经常使用的一个属性是selector
,我们将其选择为 CSS 选择器。如果我们想引用一个元素,比如
元素标签 | 选择符 |
<courses> | “courses” |
<div class=”courses”> | “.courses” |
<div id=”courses”> | “#courses” |
所以在这里,我们想引用一个名为<courses>
的元素,因为通过组件,我们可以扩展 HTML 的词汇。所以我们可以定义新元素,如courses
,在其中,我们将有课程列表,或者将来,我们可以定义一个自定义元素,一个自定义 HTML 元素,名为<rating>
。所以最终,我们的组件选择器是courses
。而模板是我们通过调用该模板在网页上渲染的标记。
// Import Component Decorator
import { Component } from '@angular/core';
// Apply Decorator Function to the Typescript class
@Component({
selector: 'courses',
template: '<h2>Angular</h2>'
})
export class CoursesComponent {
}
所以这就是 Angular 中的基本组件。
在模块中注册组件
现在第二步是在模块中注册组件。目前,我们只有一个名为 'appmodule
' 的模块。
这里我们有 3 个import
语句和底部 1 个export
语句。请注意,这个类被另一个名为@NgModule
的装饰器函数装饰。现在不要担心装饰器中使用的属性。我们稍后会讨论它们。这里,我们只关注declarations,这是我们添加属于该模块的所有组件的地方。所以默认情况下,当我们生成一个应用程序时,我们有一个名为appcomponent
的组件,我们可以在 app module 中看到它。在这里,我们需要添加我们的自定义组件到 declarations 中,如果你使用 VS Code,那么我们有一个扩展(自动导入)。它会在你创建任何类对象时自动导入头部引用语句,并且当你提供引用名称时,就像我们在 declarations 中那样。
import { CoursesComponent } from './courses.component';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent,
CoursesComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
将元素添加到 HTML 标记中
现在,是时候在 HTML 文件中使用组件了。从src >app>app.component.html打开app.component.html文件。
这个 HTML 文件实际上是用于在浏览器中打开 localhost:4200 时渲染主页的。现在只需注释掉这个 HTML 代码,然后让我们在 HTML 中尝试我们的组件。
<h1>My First App With</h1>
<courses></courses>
当 Angular 看到这个自定义元素时,它会渲染我们的courses
组件的模板。现在只需保存文件,webpack 就会自动执行。你可以在网页上看到区别。
看,它是这样工作的。如果我们检查浏览器中的元素,那么你就会知道 Angular 应用程序的结构以及它在后台如何工作。
现在你可能会想,如果你仔细查看上图,你会看到
<h1 _ngcontent-c0>My First App With</h1>
ngcontent
是从哪里来的?所以,现在打开你src文件夹中的index.html页面,你会看到<app-root></app-root>
自定义 HTML 元素,它正在使用我们的appcomponent
,你可以验证它。只需打开app.component.ts,在这里,你会看到
selector: ‘app-root’
所以每当 Angular 看到这样的元素时,它就会在这个元素内部渲染该组件的模板。
Point
你可能会想到视图,我们有应用程序中的不同 HTML 页面。我们在src文件夹中有一个index.html页面,我们也有自定义组件的 HTML 文件。而且我们也有@Component({ template: ‘’ })
装饰器来在其中编写 HTML。实际上,当我们运行应用程序时,我们的主视图实际上是index.html,在那里我们使用了<app-root></app-root>
app component。如果你在app.component.ts中,你定义了app.component.html的templateUrl
,而在app.component.html中,我们使用了CoursesComponent
的选择器<courses></courses>
。
并且我们已经编写了我们的CoursesComponent
,并在上面定义了它的选择器,即courses
。
使用 Angular CLI 生成组件
现在,对于我们之前讨论的创建自定义组件的方法,有两个问题。
- 这种方法有点繁琐。我们需要记住很多步骤。
- 如果我们忘记了第二步(将组件注册到
appmodule
),那么我们的应用程序就会崩溃。
我们可以做一个实验,针对appmodule
- 只需从app.module.ts中的 declaration 中删除CoursesComponent
。
现在保存文件并打开 URL (localhost:4200/),你将看到一个空白的白色浏览器屏幕。现在,打开浏览器控制台。
在这里,你会看到错误。
现在让我们以更快、更可靠的方式创建 Angular 组件。在这里,我们使用 Angular CLI 来生成组件。打开 VS Code 终端。
就像我们使用ng new
命令创建新应用程序一样,我们也可以使用ng
来生成组件
语句语法:ng g c nameofcomponent
g
代表生成,c
代表组件,然后是组件的名称。让我们创建一个名为course
的组件。
PS > ng g c course
看看 Angular CLI 是如何创建名为course的目录,然后在该目录中创建 4 个文件的。(.css)文件用于组件样式,(.html)用于 HTML,(.spec.ts)用于组件单元测试,(.ts)是实际的组件文件。另一件重要的事情是,当我们创建组件时,它会自动更新我们的 app module 并在此处注册我们的新组件。让我们在 app module 中验证自动更新。
现在打开 app module。
看,它已经自动更新了。你可以看到,这大大减少了工作量。如果你打开 course component
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-course',
templateUrl: './course.component.html',
styleUrls: ['./course.component.css']
})
export class CourseComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
这里是所有的样板代码。这就是 Angular CLI 如何为我们节省大量时间和精力。
模板
组件可以封装视图的数据、逻辑、HTML 标记。如果我们打开coursescomponent
,这里只有 HTML 标记,但没有数据或逻辑。所以,让我们扩展这个例子。
import { Component } from '@angular/core';
@Component({
selector: 'courses',
template: '<h2>Angular</h2>'
})
export class CoursesComponent {
}
现在我们想要的是将数据封装在容器中,并在模板标记中动态显示。在模板中,我们使用双大括号语法来封装数据,当运行时数据发生变化时,Angular 会自动在浏览器视图中更新。
import { Component } from '@angular/core';
@Component({
selector: 'courses',
template: '<h2>{{ name }}</h2>'
})
export class CoursesComponent {
name = "My Name Is Usama";
}
这就是我们所说的数据绑定。
我们不仅可以将变量放在 HTML 模板中,还可以编写简单的 JavaScript 表达式,并且还可以调用方法。让我们举个例子。
在 JavaScript 中,我们可以在模板中添加string
并连接string
。
import { Component } from '@angular/core';
@Component({
selector: 'courses',
template: '<h2>{{ "My Name Is: " + name }}</h2>'
})
export class CoursesComponent {
name = "Usama";
}
我们也可以在模板中调用函数。
import { Component } from '@angular/core';
@Component({
selector: 'courses',
template: '<h2>{{ "My Name Is: " + myName() }}</h2>'
})
export class CoursesComponent {
name = "Usama";
myName(){
return this.name;
}
}
这就是我们在模板标记中进行的操作,称为string
插值。
指令
现在让我们显示courses
列表。
export class CoursesComponent {
name = "Usama";
courses = ["BIO", "MTH", "CHE", "PHY"];
}
要在模板中显示这个课程数组,我们需要做一些修改。
- 将单引号(
‘
)更改为反引号(`
),用于模板。
现在使用反引号的好处是我们可以在模板中将模板分成多行,使其更易读。
现在让我们在模板的ul
中打印课程的名称
@Component({
selector: 'courses',
template: `
<h2>{{ name }}</h2>
<ul>
<li></li>
</ul>
`
})
在这里,我们需要循环遍历我们的courses
数组,以便在单个li
中打印它们。在这里,我们使用指令。
指令用于操作 DOM。我们可以使用它们来添加 DOM 元素、删除现有 DOM 元素、更改 DOM 元素的类或样式等等。
在这里,我们使用指令ngFor
import { Component } from '@angular/core';
@Component({
selector: 'courses',
template: `
<h2>{{ "List of Courses" }}</h2>
<ul>
<li *ngFor="let course of courses">
{{ course }}
</li>
</ul>
`
})
export class CoursesComponent {
courses = ["BIO", "MTH", "CHE", "PHY"];
}
这是 Angular 的特殊语法,我们像在 JavaScript 或 C# 中那样迭代 courses 数组foreach
循环。然后,我们在string
插值中显示数据。
这里,我们在屏幕上显示了数据。但是请记住,它显示在屏幕上是因为我在app.component.html中使用了 courses 组件选择器。
<courses></courses>
服务
在我们实际的应用程序中,数据显然来自服务器。这里是 Angular 中的service
,用于了解如何在 Angular 中使用来自服务器的数据。
这里我们有两个选项
- 在组件中编写调用 HTTP 服务的逻辑
但是这种方法有一些问题。第一个问题是,这个逻辑与该组件和该 HTTP 端点是紧密耦合的。将来,当我们在此类中编写单元测试时,我们不想依赖于实时 HTTP 端点,因为这会使执行单元测试变得更加困难。
所以我们需要制作一个 HTTP 服务的假 HTTP 实现。
第二个问题是,应用程序中的其他地方可能需要显示课程列表,比如在仪表板、管理面板或主页上。使用这种实现,我们需要在多个地方使用我们的 HTTP 服务。
第三个问题是,组件不应包含除表示逻辑以外的任何逻辑。细节应该委托给应用程序中的其他地方。
- 所以解决方案是,制作一个单独的服务类,我们在其中编写检索数据的逻辑,然后在应用程序的多个地方重用这个类。
在app文件夹中添加新文件courses.service.ts
再一次,我们导出 TypeScript 类。但是我们没有用于服务类的装饰器。它们只是纯粹的 Angular 类。
export class CoursesService { getCourses() { return ["BIO", "MTH", "CHE", "PHY"]; } }
现在回到组件,这里我们不打算使用我们的 HTTP 服务,这允许我们在不依赖 HTTP 端点的情况下进行单元测试。所以在单元测试这个类时,我们可以提供服务的假实现。但这相当复杂,我们稍后会看到。
依赖注入
现在,我们有了从服务器获取课程列表的服务。我们需要在CoursesComponent
中使用这个服务。所以,首先,我们需要在这里的组件类中添加构造函数。通过构造函数,我们初始化一个对象。所以,这里,我们需要在构造函数中创建一个服务的实例。如果你不使用 VS Code 的自动导入插件,你需要手动导入服务文件到CoursesComponent
。
然后,我们需要在构造函数中的service
方法中初始化CoursesComponent
的courses
。
import { Component } from '@angular/core';
import { CoursesService } from './courses.service';
@Component({
selector: 'courses',
template: `
<h2>{{ "List of Courses" }}</h2>
<ul>
<li *ngFor="let course of courses">
{{ course }}
</li>
</ul>
`
})
export class CoursesComponent {
courses;
constructor(){
let service = new CoursesService();
this.courses = service.getCourses();
}
}
现在让我们测试应用程序,看看发生了什么。是的,它工作正常。
然而,这种实现存在一个问题,第一个问题是CoursesComponent
与CoursesService
是紧密耦合的,如果它们相互紧密耦合。我们无法对其进行单元测试。主要问题是我们正在构造函数中创建CoursesService()
对象。第二个问题是,如果在将来,我们决定向CoursesService
的构造函数添加参数,我们就必须回到这里,并且在应用程序中任何使用CoursesService
的地方,我们都需要在那里添加一个新的参数。所以每次我们在CoursesServices
中添加新参数时,我们都需要在整个应用程序中同时添加其他更改。
我们该怎么做?
与其重新创建CoursesService
的实例,不如让 Angular 为我们做。所以删除对象创建行,并这样进行更改。
export class CoursesComponent {
courses;
constructor(service: CoursesService){
this.courses = service.getCourses();
}
}
通过这样做,Angular 将会创建一个CoursesComponent
的实例,它会查看这个构造函数,并且看到这个构造函数依赖于CoursesService
类型。所以,它首先会自动创建一个CoursesService
的实例并将其传递给这个构造函数。现在,如果你对CoursesService
构造函数进行任何更改,我们就不需要修改整个应用程序中的所有更改。第二种实现的优点是,当我们对CoursesComponent
进行单元测试时,而不是向该构造函数提供实际的课程,我们可以创建一个服务的假实现,该实现不使用后端的 HTTP 服务。换句话说,我们已经将courses
组件与courses
服务解耦。
所以教训是,如果我们正在函数或另一个类中创建类的一个实例,那么我们将这个类与那个类紧密耦合。我们无法在运行时更改它,但当你将该依赖项作为构造函数的参数添加时,我们就将该类与该依赖项解耦了。
现在还没完,我们还需要明确指示 Angular 创建CoursesService
的实例并将其传递给我们的CoursesComponent
。这个概念称为依赖注入。所以我们应该指示 Angular 将组件的依赖注入到其构造函数中。
很多人认为依赖注入非常复杂,但它实际上是一个 25 美元术语,代表 5 美分的概念。所以依赖注入意味着将类的依赖项注入或提供到其构造函数中。
Angular 作为一个内置的 DI(依赖注入)框架。所以当我们创建组件的实例时,它可以注入依赖项,但为了使其正常工作,我们需要注册依赖项。
(service: CoursesService)
在我们模块的某个地方。现在打开app.module.ts,看看这个NgModule
声明器,这里我们有一个名为 providers 的属性,它被设置为一个空数组。在这个数组中,我们需要注册该模块中的组件所依赖的所有依赖项,即CoursesComponent
依赖于CoursesService
,所以我们需要在模块中将CoursesService
注册为提供者。
如果你使用自动导入,它会自动将CoursesService
类的引用添加到此文件中。
如果你忘记了将引用添加到 providers 数组的这一步,那么它将不起作用。你可以通过注释掉或从 providers 数组中删除此项来自己进行实验,并在浏览器中运行 URL,当你打开控制台时,你会看到错误。
实际上,后台发生了什么?
当你在模块中注册依赖项提供者时,Angular 会为该整个模块创建一个该类的单个实例。所以想象一下,在这个模块中,我们有 100 个组件,其中一半需要CoursesService
。在内存中,我们只有一个CoursesService
的实例,Angular 会将同一个实例传递给所有这些组件。这就是我们所说的单例模式。
所以给定对象的单个实例存在于内存中。
使用 Angular CLI 生成服务
现在让我告诉你使用 Angular CLI 生成服务的快速方法。在 Visual Studio Code 中打开终端窗口。
语句:ng g s NameOfService
它为我们生成了 2 个文件,1 个是实际的服务文件,另一个(.spec.ts)包含用于编写该服务的单元测试的样板代码。
这里有一个我们以前没见过的新东西:@Injectable
声明器函数。只有当服务在其构造函数中有依赖项时,我们才需要这个装饰器函数,也就是说。
我们在构造函数中有logService
的依赖。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class EmailService {
constructor(log: LogService) { }
}
在这种情况下,我们需要将这个Injectable()
函数应用于该类,这就告诉 Angular,这是一个可注入类,这意味着 Angular 应该能够将其类依赖项注入到其构造函数中。现在,我们在定义组件时没有使用这个装饰器,因为当我们使用组件装饰器时,该装饰器内部包含了 Injectable 装饰器。属性...
providedIn: ‘root’
...意味着该服务应该由根应用程序注入器创建。
我们学到了什么
让我们更实际一些,做一个新的。在这里,我们将通过 CLI 生成的组件和服务的组合来探索这个结果
所以,首先,让我们通过 Angular CLI 创建组件和服务
PM > ng g c author
PM > ng g s author
首先,让我们为 author service 编写代码。
@Injectable({
providedIn: 'root'
})
export class AuthorService {
constructor() { }
getAuthors(){
return ["Bob", "Adam", "Joff", "Scott"];
}
}
组件会自动在app.module.ts中注册,但如果我们想注册authorservice
,那么我们需要手动在这里注册服务。
@NgModule({
declarations: [
AppComponent,
CoursesComponent,
CourseComponent,
AuthorComponent
],
imports: [
BrowserModule
],
providers: [CoursesService, AuthorService],
bootstrap: [AppComponent]
})
export class AppModule { }
现在是时候在我们的组件中使用author
服务了。
@Component({
selector: 'app-author',
templateUrl: './author.component.html',
styleUrls: ['./author.component.css']
})
export class AuthorComponent implements OnInit {
authors;
constructor(author: AuthorService) {
this.authors = author.getAuthors();
}
ngOnInit() {
}
}
这里Component
装饰器有templateUrl
author.component.html,现在进入author.component.html。
<h2> {{ authors.length }} Authors</h2>
<ul>
<li *ngFor="let author of authors">
{{ author }}
</li>
</ul>
.length
属性是一个 JavaScript 属性,通过它可以获得数组的总数。而在<li>
中,我们使用了ngFor
装饰器,因为它改变了我们的 DOM,所以它前面加上了星号(*)。
现在,我们知道我们的基础组件是app
。我们在基础组件中使用我们的子组件。所以进入app.component.html,然后放置你的author
组件选择器来使用它。
<app-author></app-author>
结论
这里,我们讨论了 Angular 的构建块。组件是我们编写逻辑、定义选择器和 HTML 标记的地方,服务只不过是我们获取数据并在组件中使用的东西。这里,我们讨论了如何消除类之间的紧密耦合并在它们之间注入依赖项。我们学习了如何通过 Angular CLI 创建组件和服务,以使我们的代码更少出错,并在几秒钟内准备好一切。这就是我们在 Angular 中的工作方式。