从零开始学 Angular:100 个示例,涵盖组件、模板、信号、表单、路由、HTTP 与各类属性
Angular 是 Google 推出的框架,用于用 TypeScript 构建结构清晰的大型 Web 应用。本文从创建项目开始,一直到 100 个可以运行的示例。你将学习组件与模板、控制流语法、管道、输入与输出、组件可以持有的各种属性、生命周期钩子、依赖注入、路由、响应式表单与模板驱动表单、基于 RxJS 的 HTTP、指令、信号、测试和部署。每个示例都是一小段代码。TypeScript 代码已用 TypeScript 5.6 解析器检查语法;模板和构建命令则按照 Angular 19 项目中的写法展示。
需要准备的东西
Node.js 20 或更新版本(含 npm)、Visual Studio Code 等代码编辑器,以及一个浏览器。用 npm install -g @angular/cli 安装 Angular CLI。需要了解 HTML、CSS 和 JavaScript 的基础知识,知道什么是类和函数会对 TypeScript 示例有帮助。
如何使用示例
TypeScript 示例放入独立的 Angular 项目中对应的组件或服务文件。命令示例在项目终端中运行,HTML 示例则是放入组件 HTML 文件的模板。
知识点 1: 项目设置与第一个组件
Angular 应用是一棵组件树。CLI 负责创建项目,每个组件都是一个带装饰器的类,装饰器提供选择器和模板。
创建项目
ng new 会创建独立应用的项目文件,这意味着组件不需要 NgModule。ng serve 启动带热重载的开发服务器,ng generate 则为新的组件、服务或管道创建文件。
启动与配置
main.ts 调用 bootstrapApplication,传入根组件和应用配置。配置在一个地方列出 providers,例如路由器和 HTTP 客户端。
组件、选择器与样式
@Component 装饰器设置选择器(即 HTML 标签)、模板和样式。Angular 会为组件的元素添加唯一的属性,因此样式只作用于该组件,两个组件都使用名为 card 的类也互不影响。
示意图
示例 1: 用 CLI 创建项目
安装 Angular CLI,并生成一个独立组件的应用。
npm install -g @angular/cli
ng new blog --standalone --routing --style=css
cd blog
ng serve
示例 2: 生成组件
CLI 为新组件创建文件。
ng generate component hello
# short form
ng g c hello --standalone
示例 3: 启动应用
用应用配置启动根组件。
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
示例 4: 独立组件
不需要 NgModule 的组件。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: '<h1>Hello, Angular!</h1>',
})
export class AppComponent {}
示例 5: 多行内联模板
用反引号编写跨多行的模板。
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
standalone: true,
template: `
<section>
<h2>Welcome</h2>
<p>This template spans several lines.</p>
</section>
`,
})
export class CardComponent {}
示例 6: 组件样式
只作用于本组件的样式。
import { Component } from '@angular/core';
@Component({
selector: 'app-badge',
standalone: true,
template: '<span class="badge">New</span>',
styles: [`.badge { background: #3366ff; color: white; padding: 4px 8px; border-radius: 6px; }`],
})
export class BadgeComponent {}
示例 7: 带 providers 的应用配置
在一个地方注册路由器和其他服务。
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};
示例 8: index.html 中的根元素
app-root 是 Angular 渲染应用的位置。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Blog</title>
<base href="/">
</head>
<body>
<app-root></app-root>
</body>
</html>
知识点 2: 模板与数据绑定
模板是带有额外语法的 HTML。它显示组件中的值,把用户操作传回组件,并保持输入与值同步。
插值与表达式
双花括号在文字中显示一个值,表达式可以进行计算、使用三元运算符,或使用安全导航运算符 ?.,这样对象不存在时也不会报错。
属性、特性与类绑定
方括号绑定 DOM 属性,例如 [disabled]。没有对应属性的 HTML 特性(例如 aria 标签)需要使用 attr. 进行特性绑定。类绑定和样式绑定会根据值添加类或内联样式。
事件绑定与双向绑定
圆括号监听事件,例如 (click)。使用 [(ngModel)] 的双向绑定让表单字段与组件中的值保持一致,需要引入 FormsModule。#box 这样的模板引用变量为元素命名,供模板的其他部分使用。
示例 9: 插值
用双花括号把值显示在文字中。
import { Component } from '@angular/core';
@Component({
selector: 'app-greeting',
standalone: true,
template: '<p>Hello, {{ name }}! You have {{ count + 1 }} message(s).</p>',
})
export class GreetingComponent {
name = 'Ada';
count = 2;
}
示例 10: 模板表达式
在模板中计算、使用三元运算符和安全导航。
<p>{{ price * quantity }}</p>
<p>{{ isLoggedIn ? 'Welcome back' : 'Please sign in' }}</p>
<p>{{ user?.name ?? 'Guest' }}</p>
示例 11: 属性绑定
用组件中的值设置元素的属性。
<img [src]="photoUrl" [alt]="photoAlt" width="120">
<button [disabled]="saving">Save</button>
示例 12: 特性绑定
设置没有对应属性的 HTML 特性。
<button [attr.aria-label]="closeLabel">X</button>
<td [attr.colspan]="columns">Total</td>
示例 13: 事件绑定
用户点击时运行组件的方法。
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Clicked {{ clicks }} times</p>
<button (click)="increment()">Click me</button>
`,
})
export class CounterComponent {
clicks = 0;
increment() {
this.clicks++;
}
}
示例 14: 使用 ngModel 的双向绑定
让输入框与组件中的值保持同步。
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-name-field',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="name">
<p>You typed: {{ name }}</p>
`,
})
export class NameFieldComponent {
name = '';
}
示例 15: 类绑定
根据值添加或移除 CSS 类。
<div [class.active]="isActive" [class.error]="hasError">Panel</div>
<div [ngClass]="{ 'big': size === 'lg', 'hidden': !visible }">Box</div>
示例 16: 样式绑定
根据组件中的值设置内联样式。
<p [style.color]="textColor" [style.font-size.px]="fontSize">Styled text</p>
示例 17: 模板引用变量
为元素命名,以便模板的其他部分使用它。
<input #nameBox type="text">
<button (click)="greet(nameBox.value)">Greet</button>
知识点 3: 控制流:@if、@for、@switch 与 @defer
从 Angular 17 开始,模板内置了条件和循环的代码块。它们取代了旧的结构型指令,更易读。
@if 与 @else
@if 仅在条件为真时显示代码块。@else if 与 @else 在多个代码块之间选择一个。代码块可以嵌套,例如在 for 中放入 if。
带 track 与 @empty 的 @for
@for 为每一项重复代码块。track 告诉 Angular 每一项是谁,从而只重绘变化的行。@empty 在列表为空时显示提示。track 是必需的。
@switch 与 @defer
@switch 根据表达式的值选择代码块,并可用 @default 作为后备。@defer 会延迟加载页面的一部分,例如在它进入视口或用户与之交互时加载,在此之前显示 @placeholder 或 @loading。
示例 18: @if
只有条件为真时才显示代码块。
@if (items.length > 0) {
<p>You have {{ items.length }} items.</p>
}
示例 19: @if 与 @else
在多个代码块之间选择一个。
@if (isLoggedIn) {
<p>Welcome back!</p>
} @else if (isGuest) {
<p>Browsing as a guest.</p>
} @else {
<p>Please sign in.</p>
}
示例 20: 带 track 的 @for
为每一项重复代码块,track 为每一项提供稳定的标识。
<ul>
@for (fruit of fruits; track fruit.id) {
<li>{{ fruit.name }}</li>
}
</ul>
示例 21: @for 与 @empty
列表为空时显示提示。
@for (post of posts; track post.id) {
<article>{{ post.title }}</article>
} @empty {
<p>No posts yet.</p>
}
示例 22: @switch
根据表达式的值选择代码块。
@switch (status) {
@case ('draft') { <span>Draft</span> }
@case ('published') { <span>Live</span> }
@default { <span>Unknown</span> }
}
示例 23: 带占位符的 @defer
稍后加载页面的一部分,先显示占位符。
@defer (on viewport) {
<app-heavy-chart />
} @placeholder {
<p>Chart will load when visible.</p>
} @loading {
<p>Loading chart...</p>
}
示例 24: 用 @let 定义局部值
为值命名一次,然后在模板中多次使用。
@let total = price * quantity;
<p>Total: {{ total }}</p>
知识点 4: 管道
管道在模板中转换值,例如格式化日期或金额。组件中的值保持不变。
内置的格式化管道
date、currency、number、uppercase、lowercase、titlecase 和 slice 在模板中格式化值。参数写在冒号后面,例如 date: 'longDate' 或 currency: 'USD'。
json、keyvalue 与 async
json 把对象显示为文本,调试时很有用。keyvalue 遍历对象的键和值。async 订阅一个 Observable 并显示其最新值,组件销毁时会自动取消订阅。
自定义管道
带有 @Pipe 装饰器并实现 transform 方法的类就是一个新的管道。当你需要在多个地方格式化同一类值时编写管道。管道应当没有副作用,这样 Angular 才能复用它的结果。
示例 25: 日期管道
格式化日期以便显示。
<p>{{ today | date: 'longDate' }}</p>
<p>{{ today | date: 'shortTime' }}</p>
示例 26: 货币与数字管道
格式化金额和小数。
<p>{{ 1234.5 | currency: 'USD' }}</p>
<p>{{ 3.14159 | number: '1.2-2' }}</p>
示例 27: 文本管道
在模板中改变大小写或截取字符串。
<p>{{ 'angular' | uppercase }}</p>
<p>{{ 'HELLO' | lowercase }}</p>
<p>{{ 'hello world' | titlecase }}</p>
<p>{{ 'Angular' | slice: 0 : 3 }}</p>
示例 28: json 与 keyvalue 管道
显示对象用于调试,或遍历其键和值。
<pre>{{ user | json }}</pre>
<ul>
@for (item of settings | keyvalue; track item.key) {
<li>{{ item.key }}: {{ item.value }}</li>
}
</ul>
示例 29: async 管道
订阅 Observable 并显示最新的值。
import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Observable, of } from 'rxjs';
@Component({
selector: 'app-clock',
standalone: true,
imports: [AsyncPipe],
template: '<p>Value: {{ value$ | async }}</p>',
})
export class ClockComponent {
value$: Observable<number> = of(42);
}
示例 30: 自定义管道
编写一个缩短文本的管道。
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'shorten', standalone: true })
export class ShortenPipe implements PipeTransform {
transform(value: string, max = 20): string {
return value.length > max ? value.slice(0, max) + '...' : value;
}
}
知识点 5: 组件之间的通信
数据通过输入从父组件流向子组件,事件通过输出向上流动。不是父子关系的组件可以改用共享服务。
输入
input() 声明父组件设置的值。可以给它默认值;当父组件必须提供该值时使用 input.required;当属性名应当不同时添加 alias。旧的 @Input 装饰器仍然可用。
输出与 @Output
output() 声明一个事件,子组件通过 .emit() 发出它。父组件用事件绑定监听,例如 (liked)='save()'。旧的 @Output 配合 EventEmitter 的行为相同。
投影、查询与共享服务
ng-content 允许父组件把自己的内容放进子组件。viewChild 和 contentChild 通过名称查找元素或模板。带有 Subject 或信号的共享服务,让距离较远的组件也能通信。
示例 31: input() 信号
接收父组件的值,并设置默认值。
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: '<p>Hello, {{ name() }}</p>',
})
export class UserCardComponent {
name = input<string>('Guest');
}
示例 32: 必填的 input
强制父组件提供该值。
import { Component, input } from '@angular/core';
@Component({
selector: 'app-price',
standalone: true,
template: '<p>{{ amount() }} THB</p>',
})
export class PriceComponent {
amount = input.required<number>();
}
示例 33: output() 事件
从子组件向父组件发送事件。
import { Component, output } from '@angular/core';
@Component({
selector: 'app-like-button',
standalone: true,
template: '<button (click)="liked.emit()">Like</button>',
})
export class LikeButtonComponent {
liked = output<void>();
}
示例 34: @Output 与 EventEmitter
子组件发出事件的传统方式。
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'app-search-box',
standalone: true,
template: '<input (keyup.enter)="submit($event)">',
})
export class SearchBoxComponent {
@Output() searched = new EventEmitter<string>();
submit(event: Event) {
this.searched.emit((event.target as HTMLInputElement).value);
}
}
示例 35: viewChild 信号查询
获取组件模板中某个元素的引用。
import { Component, ElementRef, viewChild } from '@angular/core';
@Component({
selector: 'app-focus',
standalone: true,
template: '<input #box type="text">',
})
export class FocusComponent {
box = viewChild<ElementRef<HTMLInputElement>>('box');
focusBox() {
this.box()?.nativeElement.focus();
}
}
示例 36: ng-content 内容投影
让父组件把自己的内容放进子组件。
import { Component } from '@angular/core';
@Component({
selector: 'app-panel',
standalone: true,
template: `
<section class="panel">
<ng-content></ng-content>
</section>
`,
})
export class PanelComponent {}
示例 37: contentChild 与 ng-template
接收父组件的模板,并为每一项渲染它。
import { Component, contentChild, TemplateRef } from '@angular/core';
@Component({
selector: 'app-list',
standalone: true,
imports: [],
template: `
@for (item of items; track item) {
<ng-container [ngTemplateOutlet]="itemTpl()" [ngTemplateOutletContext]="{ $implicit: item }"></ng-container>
}
`,
})
export class ListComponent {
items = ['a', 'b'];
itemTpl = contentChild<TemplateRef<unknown>>('itemTpl');
}
示例 38: host binding 与 host listener
绑定宿主元素,并监听它的事件。
import { Component, HostBinding, HostListener } from '@angular/core';
@Component({
selector: 'app-hover-card',
standalone: true,
template: '<p>Hover me</p>',
})
export class HoverCardComponent {
@HostBinding('class.highlighted') highlighted = false;
@HostListener('mouseenter') onEnter() {
this.highlighted = true;
}
@HostListener('mouseleave') onLeave() {
this.highlighted = false;
}
}
示例 39: host 元数据
在装饰器中设置宿主的类和特性。
import { Component } from '@angular/core';
@Component({
selector: 'app-tag',
standalone: true,
template: '<ng-content></ng-content>',
host: {
'role': 'note',
'[class.small]': 'small',
},
})
export class TagComponent {
small = true;
}
示例 40: 用于消息传递的共享服务
在非父子关系的组件之间发送消息。
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class MessageBus {
private messages = new Subject<string>();
messages$ = this.messages.asObservable();
send(text: string) {
this.messages.next(text);
}
}
知识点 6: 各种类型的属性
组件有几种属性,每种适合不同的需求:普通值、只读值、计算值、响应式信号以及双向值。
普通属性、readonly 与可选属性
普通属性是可以改变的字段。readonly 在字段设置之后禁止重新赋值。属性名后加问号,例如 email?,表示该值可能不存在,代码必须处理 undefined。
getter、setter 与静态属性
getter 在读取属性时运行代码,setter 在写入属性时运行代码。静态属性属于类本身,而不属于每个实例,适合像主题名称这样共享的默认值。
信号:signal、computed 与 effect
signal() 保存一个值,并在值变化时通知 Angular,读取时像函数一样调用,例如 count()。computed() 从信号派生出值,信号变化时自动更新。effect() 会在它读取的信号变化时运行代码。
model()、别名与 input.required
model() 是双向输入:子组件可以设置它,父组件的值随之变化,相当于自定义组件中的 ngModel。别名让输入使用不同的属性名。input.required 要求父组件提供值。
箭头函数属性
保存箭头函数的属性在把该函数作为回调传递时,仍然保持组件的 this,因为箭头函数在创建时就捕获了 this。
示例 41: 带默认值的类属性
带有初始值的普通属性。
import { Component } from '@angular/core';
@Component({
selector: 'app-profile',
standalone: true,
template: '<p>{{ username }} ({{ role }})</p>',
})
export class ProfileComponent {
username = 'mali';
role: string = 'editor';
}
示例 42: readonly 属性
只能在声明时设置一次的属性。
import { Component } from '@angular/core';
@Component({
selector: 'app-version',
standalone: true,
template: '<p>Version {{ version }}</p>',
})
export class VersionComponent {
readonly version = '1.4.0';
}
示例 43: getter 与 setter 属性
读取或写入时运行代码的属性。
import { Component } from '@angular/core';
@Component({
selector: 'app-temperature',
standalone: true,
template: '<p>{{ fahrenheit }} F</p>',
})
export class TemperatureComponent {
private celsius = 0;
get fahrenheit(): number {
return this.celsius * 9 / 5 + 32;
}
set fahrenheit(value: number) {
this.celsius = (value - 32) * 5 / 9;
}
}
示例 44: 带 set 与 update 的信号属性
用 set 或 update 修改的响应式值。
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter-signal',
standalone: true,
template: `
<p>{{ count() }}</p>
<button (click)="count.update(n => n + 1)">+1</button>
<button (click)="count.set(0)">Reset</button>
`,
})
export class CounterSignalComponent {
count = signal(0);
}
示例 45: computed 属性
由信号派生、会自动更新的值。
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-cart',
standalone: true,
template: '<p>Total: {{ total() }}</p>',
})
export class CartComponent {
prices = signal([120, 80, 50]);
total = computed(() => this.prices().reduce((sum, p) => sum + p, 0));
}
示例 46: effect
每当它读取的信号变化时运行代码。
import { Component, effect, signal } from '@angular/core';
@Component({
selector: 'app-theme',
standalone: true,
template: '<p>Theme: {{ theme() }}</p>',
})
export class ThemeComponent {
theme = signal('light');
constructor() {
effect(() => {
console.log('Theme changed to', this.theme());
});
}
}
示例 47: model() 双向输入
子组件也可以更新的输入,类似 ngModel。
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
standalone: true,
template: '<button (click)="on.set(!on())">{{ on() ? "On" : "Off" }}</button>',
})
export class ToggleComponent {
on = model(false);
}
示例 48: 带别名的 input
在模板中使用不同的属性名。
import { Component, input } from '@angular/core';
@Component({
selector: 'app-avatar',
standalone: true,
template: '<img [alt]="label()">',
})
export class AvatarComponent {
label = input('Profile picture', { alias: 'caption' });
}
示例 49: 可选属性
可能没有值的属性,用问号标记。
interface Customer {
id: number;
name: string;
email?: string;
}
const shopper: Customer = { id: 1, name: 'Ploy' };
console.log(shopper.email ?? 'no email');
示例 50: static 属性与箭头函数属性
整个类共享的值,以及保持 this 的函数。
import { Component } from '@angular/core';
@Component({
selector: 'app-config',
standalone: true,
template: '<p>{{ appName }}</p>',
})
export class ConfigComponent {
static readonly defaultTheme = 'light';
appName = 'Blog';
logName = () => console.log(this.appName);
}
知识点 7: 生命周期钩子
在组件生命周期的固定时间点,Angular 会调用特定的方法:创建时、输入变化时、视图绘制之后,以及被移除之前。
ngOnInit 与 ngOnChanges
ngOnInit 在 Angular 设置第一批输入之后只运行一次,因此适合加载数据。ngOnChanges 在每次输入变化时运行,并通过 SimpleChanges 接收旧值与新值。
ngOnDestroy
ngOnDestroy 在组件被移除之前运行。在这里取消长时间运行的数据流的订阅,并清除定时器,以防止内存泄漏。
ngAfterViewInit 与 ngDoCheck
ngAfterViewInit 在组件的视图和子视图绘制之后运行,此时视图中的元素可以使用。ngDoCheck 在每一轮变更检测中都会运行,因此应当保持轻量。
示例 51: ngOnInit
在 Angular 设置输入之后,只运行一次的初始化代码。
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-todo-list',
standalone: true,
template: '<p>{{ items.length }} tasks loaded</p>',
})
export class TodoListComponent implements OnInit {
items: string[] = [];
ngOnInit() {
this.items = ['Write', 'Test', 'Ship'];
}
}
示例 52: ngOnChanges
父组件的输入变化时作出响应。
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-badge-count',
standalone: true,
template: '<span>{{ count }}</span>',
})
export class BadgeCountComponent implements OnChanges {
@Input() count = 0;
ngOnChanges(changes: SimpleChanges) {
if (changes['count']) {
console.log('Count changed to', changes['count'].currentValue);
}
}
}
示例 53: ngOnDestroy
组件被移除时取消订阅并清除定时器。
import { Component, OnDestroy, OnInit } from '@angular/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'app-timer',
standalone: true,
template: '<p>Ticks: {{ ticks }}</p>',
})
export class TimerComponent implements OnInit, OnDestroy {
ticks = 0;
private sub?: Subscription;
ngOnInit() {
this.sub = interval(1000).subscribe(() => this.ticks++);
}
ngOnDestroy() {
this.sub?.unsubscribe();
}
}
示例 54: ngAfterViewInit
模板绘制完成后使用视图中的元素。
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
@Component({
selector: 'app-autofocus',
standalone: true,
template: '<input #field>',
})
export class AutofocusComponent implements AfterViewInit {
@ViewChild('field') field!: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
this.field.nativeElement.focus();
}
}
示例 55: ngDoCheck
在每次变更检测时执行检查。
import { Component, DoCheck } from '@angular/core';
@Component({
selector: 'app-watcher',
standalone: true,
template: '<p>Checks run: {{ checks }}</p>',
})
export class WatcherComponent implements DoCheck {
checks = 0;
ngDoCheck() {
this.checks++;
}
}
知识点 8: 依赖注入与服务
服务保存多个组件都需要的逻辑和数据。Angular 负责创建服务,并通过依赖注入传入,因此组件不需要自己创建副本。
@Injectable 与 providedIn root
带有 @Injectable({ providedIn: 'root' }) 的类在整个应用中只创建一次。组件可以用 inject() 或构造函数参数获取它。在服务中使用信号保存模板可以读取的状态。
InjectionToken 与值型 provider
令牌用来标识非类的值,例如一个配置对象。你用 provider 注册它:useValue 用于固定的值,useClass 用于类,useFactory 用于构建值。
组件级 provider
组件上的 providers 数组让该组件及其子组件拥有服务自己的实例。这适合那些应当在每次创建组件时重置的状态。
示例 56: inject() 函数
不用构造函数参数即可获取服务。
import { Component, inject } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-items',
standalone: true,
template: '<p>{{ data.count() }} items</p>',
})
export class ItemsComponent {
data = inject(DataService);
}
示例 57: 可注入的服务
任何组件都可以接收的共享类。
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CartService {
private itemsSignal = signal<string[]>([]);
readonly items = this.itemsSignal.asReadonly();
add(item: string) {
this.itemsSignal.update((list) => [...list, item]);
}
}
示例 58: InjectionToken
提供非类的值,例如配置。
import { InjectionToken } from '@angular/core';
export interface AppSettings {
apiUrl: string;
pageSize: number;
}
export const APP_SETTINGS = new InjectionToken<AppSettings>('app.settings');
示例 59: useValue 的 provider
把固定的对象交给注入器。
import { ApplicationConfig } from '@angular/core';
import { APP_SETTINGS } from './app-settings.token';
export const appConfig: ApplicationConfig = {
providers: [
{
provide: APP_SETTINGS,
useValue: { apiUrl: 'https://api.example.com', pageSize: 20 },
},
],
};
示例 60: useClass 与 useFactory
需要时选择类,或创建一个值。
import { ApplicationConfig, inject } from '@angular/core';
import { LoggerService } from './logger.service';
import { ConsoleLogger } from './console-logger';
export const appConfig: ApplicationConfig = {
providers: [
{ provide: LoggerService, useClass: ConsoleLogger },
{
provide: 'clock',
useFactory: () => () => new Date().getFullYear(),
},
],
};
示例 61: 组件级 provider
让服务在某个组件树中拥有自己的实例。
import { Component, inject } from '@angular/core';
import { CounterStore } from './counter.store';
@Component({
selector: 'app-scoped-counter',
standalone: true,
template: '<p>{{ store.value() }}</p>',
providers: [CounterStore],
})
export class ScopedCounterComponent {
store = inject(CounterStore);
}
知识点 9: 路由
路由器把 URL 路径映射到组件,在 router-outlet 中显示匹配的页面,并且可以只在需要时加载页面。
路由与导航链接
Routes 数组把 '' 和 'about' 等路径映射到组件。routerLink 创建不会重新加载页面的链接,routerLinkActive 为当前页面的链接添加类。** 通配符路由用于处理未知的 URL。
参数与懒加载
像 'post/:id' 这样的路由会传递 id,你可以从 ActivatedRoute 中读取。使用动态导入的 loadComponent 只在用户打开页面时才加载其代码,从而让首次下载更小。
守卫、解析器与代码中的导航
CanActivate 守卫返回 true,或返回一个 UrlTree 来重定向。解析器在页面打开前加载数据。Router.navigate 从代码中导航,并可以传递查询参数。
示例 62: 路由
把 URL 路径映射到组件。
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: '**', redirectTo: '' },
];
示例 63: routerLink 与 router-outlet
在页面之间建立链接,并显示匹配的页面。
<nav>
<a routerLink="/" routerLinkActive="active">Home</a>
<a routerLink="/about" routerLinkActive="active">About</a>
</nav>
<router-outlet></router-outlet>
示例 64: 路由参数
从 URL 中读取 id 等值。
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-post',
standalone: true,
template: '<p>Post id: {{ id }}</p>',
})
export class PostComponent {
private route = inject(ActivatedRoute);
id = this.route.snapshot.paramMap.get('id');
}
示例 65: 懒加载路由
只有用户打开页面时才加载其代码。
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'admin',
loadComponent: () =>
import('./admin/admin.component').then((m) => m.AdminComponent),
},
];
示例 66: CanActivate 守卫
根据检查结果允许或阻止路由。
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.isLoggedIn() ? true : inject(Router).createUrlTree(['/login']);
};
示例 67: 解析器
在页面打开之前加载数据。
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { PostService } from './post.service';
export const postResolver: ResolveFn<string> = (route) => {
return inject(PostService).titleFor(route.paramMap.get('id') ?? '');
};
示例 68: 编程式导航
用代码跳转到其他页面,并带上查询参数。
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-checkout',
standalone: true,
template: '<button (click)="finish()">Finish</button>',
})
export class CheckoutComponent {
private router = inject(Router);
finish() {
this.router.navigate(['/thanks'], { queryParams: { ref: 'cart' } });
}
}
知识点 10: 表单
Angular 有两种表单风格。响应式表单在代码中构建表单,是大型表单的常用选择。模板驱动表单在 HTML 中使用 ngModel,适合小型表单。
FormControl、FormGroup 与 FormBuilder
FormControl 保存一个值,FormGroup 把多个控件组合在一起。FormBuilder 减少代码量,nonNullable 让重置后的值保持为字符串,而不是 null。
验证器与错误信息
Validators.required、minLength、min、max 和 email 是内置的。只有在控件无效且用户已经接触过它时才显示错误,这样用户在输入之前不会看到错误。自定义验证器是返回错误对象或 null 的函数。
FormArray 与 valueChanges
FormArray 是一组可以添加或删除的控件。valueChanges 是一个 Observable,每当值变化时发出新值,适合实时搜索或保存草稿。
示例 69: 响应式 FormControl
值保存在代码中的表单字段。
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-email-input',
standalone: true,
imports: [ReactiveFormsModule],
template: '<input [formControl]="email">',
})
export class EmailInputComponent {
email = new FormControl('', { nonNullable: true });
}
示例 70: FormGroup 与 FormBuilder
把多个字段组合成一个响应式表单。
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-signup',
standalone: true,
imports: [ReactiveFormsModule],
template: '<form [formGroup]="form"></form>',
})
export class SignupComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: [''],
email: [''],
});
}
示例 71: 内置验证器
必填、最小长度、最小值、最大值和邮箱检查。
import { FormBuilder, Validators } from '@angular/forms';
import { inject } from '@angular/core';
const fb = inject(FormBuilder);
const form = fb.nonNullable.group({
name: ['', [Validators.required, Validators.minLength(2)]],
age: [18, [Validators.min(13), Validators.max(120)]],
email: ['', Validators.email],
});
示例 72: 响应式表单模板
把 FormGroup 绑定到 HTML 并显示错误。
<form [formGroup]="form" (ngSubmit)="save()">
<input formControlName="name" placeholder="Name">
@if (form.controls.name.invalid && form.controls.name.touched) {
<p class="error">Name is required.</p>
}
<button [disabled]="form.invalid">Save</button>
</form>
示例 73: FormArray
可以增减的表单控件列表。
import { FormArray, FormControl } from '@angular/forms';
const tags = new FormArray([
new FormControl('angular'),
new FormControl('typescript'),
]);
tags.push(new FormControl('rxjs'));
console.log(tags.length);
示例 74: 自定义验证器
编写自己的规则,例如不允许空格。
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function noSpaces(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = String(control.value ?? '');
return value.includes(' ') ? { noSpaces: true } : null;
};
}
示例 75: 模板驱动表单
直接在模板中用 ngModel 构建表单。
<form #form="ngForm" (ngSubmit)="submit(form.value)">
<input name="title" ngModel required>
<button [disabled]="form.invalid">Post</button>
</form>
示例 76: valueChanges
每当表单的值变化时作出响应。
import { FormControl } from '@angular/forms';
const search = new FormControl('');
search.valueChanges.subscribe((text) => {
console.log('Searching for', text);
});
知识点 11: HTTP、RxJS 与异步数据
HttpClient 发送请求并返回 Observable。RxJS 操作符转换这些数据流,async 管道无需手动订阅即可显示结果。
设置 HttpClient
在应用配置中调用 provideHttpClient()。HttpClient 的 get 和 post 返回 Observable,在有人订阅之前什么也不做,因此在模板中要使用 async 管道或 toSignal。
操作符:map、filter 与 switchMap
map 改变每个值,filter 只保留需要的值。switchMap 在新值到来时取消之前的请求,适合随用户输入进行搜索。加上 debounceTime 可以等待输入停止。
错误与重试
catchError 处理失败的请求,并可以返回一个安全的值。retry 在失败之前再次尝试请求。出错时用户看到什么,必须事先决定好,因为空白页面会让人困惑。
示例 77: provideHttpClient
在应用配置中启用 HttpClient。
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient()],
};
示例 78: 用 async 管道获取数据
无需手动订阅即可显示来自 HTTP 的列表。
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Component({
selector: 'app-users',
standalone: true,
imports: [AsyncPipe],
template: `
@for (user of users$ | async; track user.id) {
<p>{{ user.name }}</p>
}
`,
})
export class UsersComponent {
private http = inject(HttpClient);
users$: Observable<{ id: number; name: string }[]> =
this.http.get<{ id: number; name: string }[]>('/api/users');
}
示例 79: RxJS 的 map 与 filter
从数据流中转换并只保留需要的值。
import { map, filter } from 'rxjs';
const prices$ = of(120, 35, 80);
prices$
.pipe(
filter((p) => p > 50),
map((p) => p * 1.07),
)
.subscribe((total) => console.log(total.toFixed(2)));
示例 80: 用于实时搜索的 switchMap
等待输入停止后,取消之前的请求。
import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
@Component({
selector: 'app-live-search',
standalone: true,
imports: [ReactiveFormsModule],
template: '<input [formControl]="query">',
})
export class LiveSearchComponent {
private http = inject(HttpClient);
query = new FormControl('', { nonNullable: true });
results$ = this.query.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) => this.http.get<string[]>('/api/search', { params: { q } })),
);
}
示例 81: catchError 与 retry
重试失败的请求,然后回退到安全的值。
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, of, retry } from 'rxjs';
const http = inject(HttpClient);
const safeList$ = http.get<string[]>('/api/tags').pipe(
retry(2),
catchError(() => of([] as string[])),
);
示例 82: HTTP POST
用请求体把数据发送到服务器。
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export class PostService {
private http = inject(HttpClient);
createPost(title: string) {
return this.http.post<{ id: number }>('/api/posts', { title });
}
}
知识点 12: 指令
指令改变元素的行为,或决定元素是否出现。属性指令改变元素本身,结构型指令则添加或删除元素。
属性指令
选择器写在方括号中的指令,例如 [appHighlight],可以用 HostListener 响应鼠标事件,并改变元素的样式。应保持逻辑简单,复杂的行为应放在组件中。
结构型指令与 TemplateRef
结构型指令注入 TemplateRef 和 ViewContainerRef。当条件为真时,它用 createEmbeddedView 创建视图;为假时清除视图。Angular 的 *ngIf 和 @if 工作方式相同。
指令中输入的 setter
输入上的 setter 在父组件每次传入新值时运行。可以用它直接响应值,例如重建视图,而无需编写 ngOnChanges。
示例 83: 属性指令
改变元素外观的指令。
import { Directive, ElementRef, HostListener, inject, input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true,
})
export class HighlightDirective {
color = input('yellow', { alias: 'appHighlight' });
private el = inject<ElementRef<HTMLElement>>(ElementRef);
@HostListener('mouseenter') onEnter() {
this.el.nativeElement.style.backgroundColor = this.color();
}
@HostListener('mouseleave') onLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}
示例 84: 结构型指令
在页面中添加或移除元素的指令。
import { Directive, inject, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appUnless]',
standalone: true,
})
export class UnlessDirective {
private template = inject<TemplateRef<unknown>>(TemplateRef);
private view = inject(ViewContainerRef);
@Input() set appUnless(condition: boolean) {
this.view.clear();
if (!condition) {
this.view.createEmbeddedView(this.template);
}
}
}
示例 85: input 的 setter
父组件每次传入新值时运行代码。
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-tag-list',
standalone: true,
template: '<p>{{ tags.join(", ") }}</p>',
})
export class TagListComponent {
private _tags: string[] = [];
@Input()
set tags(value: string[]) {
this._tags = value.map((t) => t.trim());
}
get tags(): string[] {
return this._tags;
}
}
知识点 13: 渲染、信号与变更检测
Angular 通过变更检测决定重绘哪些部分。信号与 OnPush 让你控制这一过程,使大型页面保持快速。
带索引的列表与计算值
在 @for 中可以使用 $index、$first 和 $last 来设置每一行的样式。computed 信号可以过滤列表,因此过滤结果会随搜索文字或列表的变化而更新。
OnPush 变更检测
使用 ChangeDetectionStrategy.OnPush 后,Angular 只在输入变化、组件内发生事件,或它读取的信号变化时才检查该组件。在组件很多的页面上,这能显著提速。
zoneless 与 toSignal
zoneless 应用不使用 zone.js 运行,因此视图通过信号和事件更新,而不是因为浏览器的每个事件而更新。toSignal 把 Observable 转换为信号,可以在模板中直接读取,无需订阅。
示例 86: 带 $index 与 $first 的 @for
使用循环中每一项的位置。
@for (step of steps; track step; let i = $index, first = $first, last = $last) {
<li [class.first]="first" [class.last]="last">{{ i + 1 }}. {{ step }}</li>
}
示例 87: 用 computed 过滤
显示随用户输入更新的过滤列表。
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-fruit-filter',
standalone: true,
template: `
<input (input)="query.set($any($event.target).value)">
@for (fruit of shown(); track fruit) {
<p>{{ fruit }}</p>
}
`,
})
export class FruitFilterComponent {
fruits = ['apple', 'banana', 'cherry'];
query = signal('');
shown = computed(() =>
this.fruits.filter((f) => f.includes(this.query().toLowerCase())),
);
}
示例 88: OnPush 变更检测
只在输入变化时检查组件。
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'app-price-tag',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<p>{{ price }} THB</p>',
})
export class PriceTagComponent {
@Input() price = 0;
}
示例 89: 无 Zone 的变更检测
不使用 zone.js 运行 Angular,并用信号更新视图。
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [provideZonelessChangeDetection()],
};
示例 90: toSignal
把 Observable 转换为可在模板中读取的信号。
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { interval, map } from 'rxjs';
@Component({
selector: 'app-seconds',
standalone: true,
template: '<p>Seconds: {{ seconds() }}</p>',
})
export class SecondsComponent {
seconds = toSignal(interval(1000).pipe(map((n) => n + 1)), { initialValue: 0 });
}
知识点 14: 测试与动画
测试会单独检查组件和服务。动画会在元素状态变化时平滑地改变它。
TestBed 与组件测试
TestBed 在测试模块中创建组件,detectChanges 运行变更检测,测试检查 nativeElement 的文字。每个测试只检查一种行为,这样失败时便能快速找到原因。
HttpTestingController 与 fakeAsync
provideHttpClientTesting 用假的网络替换真实的网络,expectOne 和 flush 让测试获得响应。fakeAsync 配合 tick 推进时间,因此无需等待就能测试延迟执行的操作。
动画触发器
动画触发器定义状态(例如 open 与 closed)以及它们之间的过渡和时间。把触发器绑定到模板中的值,当值变化时 Angular 就会播放过渡效果。
示例 91: 用 TestBed 测试组件
在测试中创建组件并检查它的文字。
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';
describe('CounterComponent', () => {
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({ imports: [CounterComponent] }).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
});
it('starts at zero', () => {
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('0');
});
});
示例 92: HttpTestingController
无需真实服务器即可测试 HTTP 服务。
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { UserService } from './user.service';
describe('UserService', () => {
it('loads users', () => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
const service = TestBed.inject(UserService);
const http = TestBed.inject(HttpTestingController);
service.users().subscribe((list) => expect(list.length).toBe(2));
http.expectOne('/api/users').flush([{ id: 1 }, { id: 2 }]);
http.verify();
});
});
示例 93: fakeAsync 与 tick
在测试中推进时间,检查延迟执行的操作。
import { fakeAsync, tick } from '@angular/core/testing';
it('runs a delayed action', fakeAsync(() => {
let done = false;
setTimeout(() => (done = true), 500);
tick(500);
expect(done).toBeTrue();
}));
示例 94: 动画触发器
在元素状态变化时为其添加动画。
import { Component } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';
@Component({
selector: 'app-drawer',
standalone: true,
animations: [
trigger('slide', [
state('closed', style({ height: '0px' })),
state('open', style({ height: '200px' })),
transition('closed <=> open', animate('300ms ease-in')),
]),
],
template: '<div [@slide]="isOpen ? \'open\' : \'closed\'">Content</div>',
})
export class DrawerComponent {
isOpen = false;
}
知识点 15: 配置、错误与部署
真实的应用需要为每个环境提供配置、应对意外错误的方案,以及可以发布的生产构建。
环境文件与初始化器
把 API 地址等设置保存在环境文件中,开发与生产环境使用不同的值。provideAppInitializer 在第一个页面绘制之前运行代码,例如加载配置。
错误与 ErrorHandler
Angular 会把未捕获的错误交给 ErrorHandler。你可以提供自己的类,把错误记录下来或发送到报告服务,这样就能在一个地方决定如何处理意外错误。
构建、测试与部署
ng build --configuration production 会在 dist/ 中生成优化后的文件。在构建流水线中,ng test --watch=false 只运行一次单元测试。把静态文件托管在支持单页应用的 Web 服务器或静态托管服务上即可。
示例 95: 环境配置
把 API 地址等设置保存在一个文件中。
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
};
示例 96: 自定义 ErrorHandler
在一个地方报告意外的错误。
import { ErrorHandler, Injectable } from '@angular/core';
@Injectable()
export class ReportingErrorHandler implements ErrorHandler {
handleError(error: unknown): void {
console.error('Unexpected error:', error);
}
}
示例 97: provideAppInitializer
在应用启动之前加载配置。
import { ApplicationConfig, provideAppInitializer, inject } from '@angular/core';
import { ConfigService } from './config.service';
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => inject(ConfigService).load()),
],
};
示例 98: 独立组件的 imports
列出组件使用的指令和管道。
import { Component } from '@angular/core';
import { NgClass } from '@angular/common';
import { HighlightDirective } from './highlight.directive';
import { ShortenPipe } from './shorten.pipe';
@Component({
selector: 'app-note',
standalone: true,
imports: [NgClass, HighlightDirective, ShortenPipe],
template: '<p [ngClass]="{ bold: true }" appHighlight="pink">{{ text | shorten: 10 }}</p>',
})
export class NoteComponent {
text = 'A long note about Angular standalone components';
}
示例 99: 交互时的 @defer
只有用户点击之后才加载页面的一部分。
<button #openBtn>Show comments</button>
@defer (on interaction(openBtn)) {
<app-comments />
} @placeholder {
<p>Comments will load after you click.</p>
}
示例 100: 构建与测试命令
日常工作中最常用的 Angular CLI 命令。
# development server
ng serve
# production build (outputs to dist/)
ng build --configuration production
# unit tests
ng test --watch=false
练习项目:任务列表
制作一个小应用,使用每一节的思路。先自己编写,然后与示例进行比较。
1. 创建一个独立组件项目,再创建一个 TaskList 组件,用信号保存任务数组。
2. 添加一个带必填文本框的表单,并用绑定到 (ngSubmit) 的方法把新任务加入列表。
3. 用 @for 和 track 显示任务,并在列表为空时用 @empty 显示提示。
4. 添加一个 computed 信号统计未完成的任务数量,并在模板中显示。
5. 添加一个任务详情页面的路由,用 ActivatedRoute 从 URL 读取 id。
6. 编写一个组件测试,检查列表为空时的提示,然后运行 ng test --watch=false。
下一步
接下来,请深入学习路由器,学习用信号或 NgRx 进行状态管理,学习使用 Angular SSR 进行服务端渲染,以及学习 Angular Material 这类组件库。官方文档 angular.dev 是最好的参考资料。本文的示例遵循 Angular 17 及以后版本推荐的语法。
总结
Angular 用组件构建应用。模板绑定数据和事件,控制流代码块显示或重复内容,管道格式化值,输入和输出连接父子组件。组件把状态保存在普通属性、只读值、getter 和信号中。服务通过依赖注入共享,路由把 URL 映射到页面,表单验证输入,HttpClient 与 RxJS 加载数据。测试、配置和生产构建让整个流程完整。记住这 100 个示例最好的方法,是制作任务列表项目,并且每次只改动一处。