Skip to content

Angular 速查表

Google 开发的基于 TypeScript 的 Web 应用框架。

01

入门

CLI 安装与项目创建

Angular CLI 是开发的主要工具。ng new 脚手架会生成一个预配置路由和样式的完整项目。使用 ng generate(ng g)保持文件创建的一致性。CLI 负责构建、测试和部署。

angular
# install Angular CLI globally
npm install -g @angular/cli

# create a new project
ng new my-app --routing --style=scss
cd my-app

# serve the app (default: localhost:4200)
ng serve --open

# generate components, services, etc.
ng generate component components/user-profile
ng g service services/auth
ng g module modules/admin --routing

项目结构概览

Angular 强制使用清晰的项目结构。src/app 存放所有功能代码。main.ts 引导根组件。angular.json 配置构建、测试和启动选项。environments 文件夹让你按环境切换 API URL。

angular
my-app/
├── src/
│   ├── app/                 # application code lives here
│   │   ├── app.component.ts
│   │   ├── app.component.html
│   │   └── app.config.ts    # standalone bootstrap config
│   ├── assets/              # static images, fonts
│   ├── environments/        # env-specific values
│   ├── index.html           # main HTML entry
│   ├── main.ts              # bootstrap entry point
│   └── styles.scss          # global styles
├── angular.json             # CLI workspace config
├── package.json
└── tsconfig.json

启动与构建

ng serve 启动开发服务器,默认在 4200 端口提供 HMR 式热重载。生产构建会进行摇树优化、压缩并为文件名添加哈希以利于缓存。使用 --stats-json 配合 webpack-bundle-analyzer 查找臃肿的依赖。ng test 默认运行 Karma。

angular
# dev server with live reload
ng serve --port 4300 --open

# production build (outputs to dist/)
ng build --configuration production

# analyze bundle size
ng build --stats-json
npx webpack-bundle-analyzer dist/my-app/stats.json

# run unit tests (Karma + Jasmine)
ng test

# run end-to-end tests
ng e2e

代码生成(Schematics)

Schematics 确保文件命名和样板代码的一致性。--standalone 标志生成独立组件(v17 起推荐)。--route + --module 自动接入懒加载路由。用 --functional 创建的守卫返回函数而非类。

angular
# generate a standalone component
ng g component user-profile --standalone

# generate a service with root provider
ng g service services/auth

# generate a guard (functional by default)
ng g guard guards/auth --functional

# generate a pipe, directive, or module
ng g pipe pipes/shorten
ng g directive directives/highlight
ng g module admin --routing --route admin --module app

# generate an interface or enum
ng g interface models/user
ng g enum models/role

配置(angular.json)

angular.json 是工作区配置。每个项目有 build、serve、test 和 lint 目标。budgets 在包体积超阈值时警告或报错。polyfills 数组包含 Angular 所需的 zone.js。一个工作区可容纳多个项目(应用 + 库)。

angular
{
  "projects": {
    "my-app": {
      "architect": {
        "build": {
          "options": {
            "outputPath": "dist/my-app",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": ["zone.js"],
            "tsConfig": "tsconfig.app.json",
            "assets": ["src/favicon.ico", "src/assets"],
            "styles": ["src/styles.scss"],
            "scripts": []
          },
          "configurations": {
            "production": {
              "budgets": [{ "type": "initial", "maximumWarning": "500kb" }]
            }
          }
        }
      }
    }
  }
}

环境变量

Angular 在构建时根据配置(开发 vs 生产)替换环境文件。文件替换在 angular.json 的 fileReplacements 中配置。在任何地方注入 environment 对象即可访问环境特定值,无需硬编码 URL。切勿将真实密钥提交到版本控制。

angular
// src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api',
  apiKey: 'dev-key'
};

// src/environments/environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.example.com',
  apiKey: 'prod-key'
};

// usage in a service
import { environment } from '../environments/environment';

@Injectable({ providedIn: 'root' })
export class ApiService {
  private baseUrl = environment.apiUrl;
}
02

组件基础

基本组件

组件是用 @Component 装饰的 TypeScript 类。selector 是模板中使用的自定义 HTML 标签。templateUrl/styleUrls 指向外部文件;用 template/styles 使用内联形式。@Input 标记父组件可绑定的属性。保持组件职责单一。

angular
// user-profile.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.scss'],
})
export class UserProfileComponent {
  @Input() name = 'Guest';
  @Input() age = 0;

  greet() {
    return `Hello, ${this.name}!`;
  }
}

内联模板组件

内联模板便于小组件使用,避免额外文件。用反引号书写多行字符串——内部的反引号需用反斜杠转义。styles 接受 CSS 字符串数组。:host 选择器定位组件自身元素。模板超过约 10 行时优先使用外部文件。

angular
@Component({
  selector: 'app-counter',
  template: `
    <button (click)="decrement()">-</button>
    <span>{{ count }}</span>
    <button (click)="increment()">+</button>
  `,
  styles: [`
    :host { display: flex; gap: 8px; align-items: center; }
    button { padding: 4px 12px; }
  `],
})
export class CounterComponent {
  count = 0;
  increment() { this.count++; }
  decrement() { this.count--; }
}

独立组件(v14+)

独立组件不需要 NgModule——它们在 imports 数组中声明自己的依赖(CommonModule、FormsModule、其他组件)。这是现代推荐方式。只导入用到的内容以获得更好的摇树优化。可直接在 main.ts 中引导独立组件。

angular
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <input [(ngModel)]="query" placeholder="Search..." />
    <p>Searching for: {{ query }}</p>
  `,
})
export class SearchComponent {
  query = '';
}

输入与输出

@Input 让父组件通过属性绑定传入数据。@Output 暴露一个 EventEmitter,父组件通过事件绑定订阅。始终为输入设置默认值以避免 undefined 错误。v17+ 可使用新的 input()/output() 信号 API 获得更简洁的代码。

angular
@Component({
  selector: 'app-toggle',
  template: `
    <button (click)="toggle()">{{ label }}: {{ on ? 'On' : 'Off' }}</button>
  `,
})
export class ToggleComponent {
  @Input() label = 'Toggle';
  @Input() on = false;
  @Output() changed = new EventEmitter<boolean>();

  toggle() {
    this.on = !this.on;
    this.changed.emit(this.on);
  }
}

<!-- parent template -->
<app-toggle [label]="'Power'" [on]="isOn" (changed)="onToggle($event)" />

基于信号的输入(v17.1+)

input() 函数创建基于信号的、只读的输入,像函数一样调用。它通过 input.required<T>() 支持必填输入,支持转换器,并与 computed() 和 effect() 无缝协作。output() 是 @Output EventEmitter 的现代替代品。这是 Angular 组件 API 的未来方向。

angular
import { Component, input, output } from '@angular/core';

@Component({
  selector: 'app-card',
  template: `
    <div>{{ title() }} - {{ count() }}</div>
    <button (click)="clicked.emit()">Click</button>
  `,
})
export class CardComponent {
  // readonly signal inputs with defaults
  title = input<string>('Default');
  count = input<number>(0);

  // output() function replaces @Output
  clicked = output<void>();

  // computed derived from inputs
  // requires import { computed }
  // label = computed(() => this.title().toUpperCase());
}

视图封装

Emulated(默认)通过添加唯一属性将样式作用域限定到组件,无需真正的 Shadow DOM。None 使样式全局生效(慎用)。ShadowDom 使用原生 Shadow DOM 实现真正隔离,但会影响样式穿透和某些第三方库。除非有特殊需求,否则坚持使用 Emulated。

angular
@Component({
  selector: 'app-styles',
  template: `<p class="local">Emulated (default)</p>`,
  styles: [`p.local { color: red; }`],
  encapsulation: ViewEncapsulation.Emulated, // default
})
export class StylesComponent {}

// ViewEncapsulation.None    -> styles leak globally
// ViewEncapsulation.ShadowDom -> real Shadow DOM (strong isolation)
// ViewEncapsulation.Emulated -> scoped attributes (default, recommended)
03

模板语法

文本插值

双花括号 {{ }} 将表达式渲染为文本(HTML 转义)。可使用大多数 JS 表达式:算术、三元、方法调用、属性访问。不支持 new、typeof 或赋值运算符。管道(|)转换输出。插值在每次变更检测周期重新求值。

angular
<p>{{ title }}</p>                       <!-- renders the value -->
<p>{{ 1 + 2 }}</p>                        <!-- expressions allowed -->
<p>{{ user.name }}</p>                    <!-- property access -->
<p>{{ items.length }}</p>                 <!-- method/property -->
<p>{{ price | currency:'USD' }}</p>       <!-- pipe in interpolation -->
<p>{{ isReady ? 'Yes' : 'No' }}</p>       <!-- ternary -->

<!-- one-time interpolation updates when the source changes -->
<!-- new control flow @if, @for work without *ngIf/*ngFor directives -->

属性与特性绑定

[prop] 绑定到 DOM 属性(首选)。[attr.name] 绑定到 HTML 特性,用于没有 DOM 属性的场景(colspan、SVG 属性)。[class.x] 按真值切换单个类;[style.prop] 设置样式,支持 .px 等单位后缀。避免用 innerHTML 绑定不可信内容。

angular
<!-- property binding: [prop]="expression" -->
<img [src]="user.avatar" [alt]="user.name" />
<button [disabled]="isSaving">Save</button>
<my-comp [data]="items"></my-comp>

<!-- attribute binding (for attrs without DOM props) -->
<td [attr.colspan]="2">Merged cell</td>
<svg [attr.viewBox]="box"></svg>

<!-- class & style binding -->
<div [class.active]="isActive">Active?</div>
<div [class]="dynamicClassString">
<div [style.color]="colorVar">
<div [style.width.px]="size">Width</div>

事件绑定

(event) 绑定 DOM 事件和自定义 EventEmitter 输出。$event 是事件载荷(原生事件为 DOM Event,自定义事件为发出的值)。事件修饰符如 .enter、.esc、.prevent、.stop 用于过滤或转换事件。括号中的目标决定什么触发处理程序。

angular
<button (click)="save()">Save</button>
<input (input)="onInput($event)" (keyup)="onKey($event)" />
<div (mouseenter)="hover=true" (mouseleave)="hover=false">Hover</div>

<!-- $event is the DOM event object -->
<input (keyup.enter)="submit()" (keyup.esc)="cancel()" />

<!-- passive events (scroll, touchmove) -->
<div (scroll.passive)="onScroll($event)">...</div>

<!-- binding to custom component outputs -->
<app-child (changed)="onChanged($event)"></app-child>

模板引用变量

#var(或 ref-var)引用其声明所在的 DOM 元素或组件实例。在模板中任意位置可访问。对于组件,值是组件实例;对于元素,是 HTMLElement。在 @for 内部,$index、$first、$last、$even、$odd 是隐式上下文变量。

angular
<input #nameInput type="text" />
<button (click)="nameInput.focus()">Focus</button>
<p>You typed: {{ nameInput.value }}</p>

<!-- ref to a component instance -->
<app-form #f="appForm"></app-form>
<button [disabled]="!f.valid" (click)="f.submit()">Submit</button>

<!-- ref inside @for -->
<ul>
  @for (item of items; track item.id) {
    <li>{{ item.name }} (index: {{ $index }})</li>
  }
</ul>

双向绑定(盒子里的香蕉)

[(ngModel)] 需要 FormsModule。[(x)] 语法解构为 [x] + (xChange),因此自定义双向绑定需要一个名为 x 的输入和一个名为 xChange 的输出。记住口诀'盒子里的香蕉'——圆括号在方括号里面。v17.2+ 的 model() 信号函数简化了这一过程。

angular
<!-- [()] = "banana in a box" - property + event combined -->
<input [(ngModel)]="username" />

<!-- equivalent to -->
<input [ngModel]="username" (ngModelChange)="username = $event" />

<!-- two-way binding on custom components -->
<app-counter [(value)]="count"></app-counter>
<!-- requires: @Input() value; @Output() valueChange = new EventEmitter() -->

<!-- signals: model() for two-way (v17.2+) -->
// value = model<number>(0);

安全导航与非空断言

?. 操作符安全导航可空链,返回 undefined 而非抛出错误。用于尚未加载的异步数据。! 操作符仅是编译时断言——不添加运行时检查,仅在你确定时使用。对于可能为 null 的数组,使用 ?? [] 作为兜底。

angular
<!-- safe navigation: short-circuits to undefined if null -->
<p>{{ user?.profile?.bio }}</p>
<p>{{ user?.getName?.() }}</p>

<!-- non-null assertion: tells compiler it's not null -->
<p>{{ user!.name }}</p>

<!-- in property bindings -->
<img [src]="user?.avatar" />
<app-detail [data]="user!"></app-detail>

<!-- safe navigation in @for doesn't replace track -->
@for (item of items ?? []; track item.id) { ... }
04

指令

@if / @else(新控制流)

新的 @if/@else if/@else 块语法取代 *ngIf。无需导入 CommonModule 或 NgIf,无星号,且 @else 原生支持(不像 ngIf 需要 ng-template 的 hack)。这是新代码的推荐语法。它是 v17 引入、v18 稳定的内置控制流的一部分。

angular
@if (user) {
  <p>Hello, {{ user.name }}</p>
} @else if (loading) {
  <p>Loading...</p>
} @else {
  <p>No user found.</p>
}

<!-- no structural directive import needed, no * prefix -->
<!-- works in standalone templates automatically (v17+) -->

@for / @empty(新控制流)

@for 以强制的 track 表达式取代 *ngFor,实现高效 DOM diff。@empty 在集合为空时渲染兜底内容。隐式上下文变量($index、$first、$last 等)始终可用。用稳定的唯一 id 进行追踪对大列表性能至关重要。

angular
<ul>
  @for (item of items; track item.id) {
    <li>{{ item.name }} ({{ $index }})</li>
  } @empty {
    <li>No items yet.</li>
  }
</ul>

<!-- 'track' is REQUIRED for performance -->
<!-- implicit vars: $index, $first, $last, $even, $odd, $count -->

<!-- @for over a range -->
@for (i of [1,2,3,4,5]; track i) {
  <span>{{ i }}</span>
}

ngClass 与 ngStyle

ngClass 接受对象(真值键应用)、数组或字符串。ngStyle 接受驼峰或连字符键的对象,支持单位后缀。需要导入 CommonModule 或具体指令。对于单个类/样式,[class.x] 和 [style.x] 更轻量且无需导入。

angular
<div [ngClass]="{ active: isActive, disabled: !enabled }">Box</div>
<div [ngClass]="['card', 'shadow', themeClass]">Card</div>
<div [ngClass]="currentClass">String</div>

<div [ngStyle]="{ color: textColor, 'font-size.px': size }">Text</div>
<div [ngStyle]="styleObject">Styled</div>

<!-- import CommonModule (or NgClass/NgStyle) for these -->
<!-- prefer [class.x] and [style.x] for single value bindings -->

@switch(新控制流)

@switch 取代 *ngSwitch(及 ngSwitchCase/ngSwitchDefault 指令)。更简洁——无需包裹容器,无需导入。每个 @case 仅在其值匹配 @switch 表达式时渲染。@default 是兜底。这是 v17+ 提供的内置控制流。

angular
@switch (status) {
  @case ('loading') {
    <spinner>Loading...</spinner>
  }
  @case ('error') {
    <p class="error">Something went wrong</p>
  }
  @case ('success') {
    <p>Done!</p>
  }
  @default {
    <p>Idle</p>
  }
}

自定义属性指令

属性指令改变 DOM 元素的外观或行为。selector 中的[方括号]匹配属性用法。ElementRef 提供直接 DOM 访问;HostListener 绑定 DOM 事件。注入 ElementRef<HTMLElement> 以获得类型安全。在服务端渲染场景中优先使用 Renderer2 操作 DOM。

angular
import { Directive, ElementRef, Input, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
})
export class HighlightDirective {
  @Input('appHighlight') highlightColor = 'yellow';

  constructor(private el: ElementRef<HTMLElement>) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.el.nativeElement.style.backgroundColor = this.highlightColor;
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

<!-- usage: <p appHighlight="lightblue">Hover me</p> -->

结构指令内部原理

结构指令通过 TemplateRef(被包裹的模板)和 ViewContainerRef(插入位置)添加/移除 DOM 节点。星号(*)是语法糖,将元素包裹在 <ng-template> 中。新的 @if/@for/@switch 块取代了大多数自定义结构指令,但此模式在高级场景中仍有用。

angular
import { Directive, TemplateRef, ViewContainerRef, Input } from '@angular/core';

@Directive({ selector: '[appUnless]' })
export class UnlessDirective {
  private hasView = false;

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.vc.createEmbeddedView(this.tpl);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.vc.clear();
      this.hasView = false;
    }
  }

  constructor(private tpl: TemplateRef<unknown>, private vc: ViewContainerRef) {}
}

<!-- usage: <div *appUnless="hide">Shown when hide is false</div> -->
05

管道

内置管道

管道转换模板显示值。常用内置管道:date、number、currency、percent、uppercase、lowercase、titlecase、slice、json、async。管道默认是纯的——仅当输入引用变化时才重新计算。格式字符串遵循 Angular 的区域感知规则。

angular
<!-- date formatting -->
<p>{{ today | date }}</p>                          <!-- Sep 10, 2024 -->
<p>{{ today | date:'short' }}</p>
<p>{{ today | date:'yyyy-MM-dd HH:mm' }}</p>

<!-- number & currency -->
<p>{{ 3.14159 | number:'1.2-2' }}</p>               <!-- 3.14 -->
<p>{{ price | currency:'USD':'symbol':'1.2-2' }}</p>
<p>{{ ratio | percent }}</p>

<!-- text transforms -->
<p>{{ 'hello world' | uppercase }}</p>              <!-- HELLO WORLD -->
<p>{{ 'HELLO' | lowercase }}</p>
<p>{{ title | titlecase }}</p>
<p>{{ 'a/b/c' | slice:0:2 }}</p>

<!-- json (for debugging) -->
<pre>{{ obj | json }}</pre>

管道链与参数

管道从左到右链式调用,每个管道接收前一个的输出。参数以冒号分隔且按位置传递。用于纯转换(格式化、过滤)以保持模板声明式。避免在性能敏感数据上使用非纯管道;应在组件中计算。

angular
<!-- chain: left to right, output of one feeds the next -->
<p>{{ '  hello  ' | trim | uppercase }}</p>         <!-- HELLO -->
<p>{{ birthday | date:'fullDate' | uppercase }}</p>
<p>{{ items | slice:0:3 | json }}</p>

<!-- parameters separated by colons -->
<p>{{ amount | number:'1.2-2':'en-US' }}</p>
<p>{{ meeting | date:'shortTime' }}</p>
<p>{{ list | slice:1:5 }}</p>

<!-- multiple parameters -->
<p>{{ value | myPipe:arg1:arg2:arg3 }}</p>

自定义管道

自定义管道实现 PipeTransform.transform()。@Pipe 的 name 在模板中使用。在组件的 imports 数组中注册独立管道(v15+),或在 NgModule 中声明。纯管道(默认)仅在输入引用变化时重新运行——传递新引用以触发更新。

angular
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 20, ellipsis = '...'): string {
    if (!value) return '';
    return value.length > limit
      ? value.slice(0, limit) + ellipsis
      : value;
  }
}

// register in a standalone component imports:
// imports: [TruncatePipe]

<!-- usage -->
<p>{{ longText | truncate:50 }}</p>
<p>{{ longText | truncate:10:'>>' }}</p>

Async 管道

async 管道订阅 Observable 或 Promise,渲染值,并在销毁时自动取消订阅——无需手动订阅/取消。结合 *ngIf as 或 @for,可优雅处理加载时的 null。当新值到达时它会标记组件进行检查,与 OnPush 集成。

angular
@Component({
  selector: 'app-user',
  template: `
    <div *ngIf="user$ | async as user">
      {{ user.name }}
    </div>
    <ul>
      @for (item of items$ | async; track item.id) {
        <li>{{ item.name }}</li>
      }
    </ul>
  `,
  imports: [AsyncPipe, NgIf],
})
export class UserComponent {
  user$ = this.http.get<User>('/api/user');
  items$ = this.http.get<Item[]>('/api/items');

  constructor(private http: HttpClient) {}
}

纯管道 vs 非纯管道

纯管道缓存结果直到输入引用变化;变更数组或对象的属性不会触发它们。非纯管道(pure: false)每次变更检测都重新运行——便于过滤变更的数组但代价高昂。优先保持数据不可变(替换数组而非变更)并使用纯管道以获得性能。

angular
// PURE (default): re-runs only when input reference changes
@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {
  transform(items: Item[], term: string): Item[] {
    return items.filter(i => i.name.includes(term));
  }
}
// Issue: mutating items.push() won't trigger re-evaluation.

// IMPURE: re-runs on every change detection cycle
@Pipe({ name: 'filter', pure: false })
export class ImpureFilterPipe implements PipeTransform { ... }
// ⚠️ Expensive! Use sparingly — affects performance.

i18n 与区域管道

date、number、currency 等管道遵循 LOCALE_ID。通过 registerLocaleData 在引导前注册区域数据。货币/数字格式遵循当前区域。对于多语言应用,按用户提供 LOCALE_ID 或使用 i18n 工具进行模板翻译。缺失的区域数据回退到 en-US。

angular
// register locale data in app.config.ts
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
registerLocaleData(localeFr);

// provide a default locale
providers: [{ provide: LOCALE_ID, useValue: 'fr-FR' }]

// usage - now locale-aware
<p>{{ price | currency:'EUR' }}</p>          <!-- 1 234,56 € -->
<p>{{ today | date:'fullDate' }}</p>          <!-- lundi 10 septembre 2024 -->
<p>{{ number | number:'1.2-2' }}</p>          <!-- 1 234,56 -->
<p>{{ amount | currency:'JPY' }}</p>          <!-- ¥1,235 -->
06

服务与依赖注入

基本服务

providedIn: 'root' 创建一个全应用可用的单例,无需模块注册。该服务可摇树优化——未使用则被移除。使用 inject() 函数(v14+)代替构造函数注入以获得更简洁的代码和更易测试。服务持有共享状态和业务逻辑;组件保持展示性。

angular
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UserService {
  private users = ['Alice', 'Bob'];

  getUsers() { return this.users; }
  addUser(name: string) { this.users.push(name); }
}

// usage in a component
export class UserListComponent {
  // Angular injects the singleton instance
  constructor(private users: UserService) {}

  get users() { return this.usersService.getUsers(); }
  private usersService = inject(UserService);
}

inject() 函数

inject() 是构造函数注入的现代替代品。它可在字段初始化器中使用,使代码更可读并启用更好的 TypeScript 推断。它必须在构造期间运行(不能在之后的异步回调中)。一致地使用它——混用构造函数参数和 inject() 也可以,但 inject() 更简洁。

angular
import { Injectable, inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private http = inject(HttpClient);
  private router = inject(Router);

  login(creds: Credentials) {
    return this.http.post('/api/login', creds);
  }
}

// in a component
export class DashboardComponent {
  private auth = inject(AuthService);
  user = this.auth.currentUser;   // works in field initializers
}

Provider 配置

Provider 可在任何层级配置(root、模块、组件)。useClass 切换实现(非常适合模拟)。useValue 提供常量。useExisting 将一个服务别名到另一个。useFactory 动态创建实例。InjectionToken 用于非类值,如字符串或配置对象。

angular
@Component({
  // ... provide at component level for instance-per-component
  providers: [
    UserService,                              // shorthand
    { provide: Logger, useClass: ConsoleLogger },
    { provide: API_URL, useValue: 'https://api.example.com' },
    { provide: Storage, useExisting: LocalStorageService },
    {
      provide: Config,
      useFactory: () => new Config(env.production)
    },
  ],
})
export class AdminComponent {}

// InjectionToken for non-class values
export const API_URL = new InjectionToken<string>('API_URL');

分层注入器

Angular 的 DI 树镜像组件树。providedIn: 'root' = 全应用一个实例。组件级 provider 创建子注入器——每个实例各有自己的。这对编辑器等有状态组件很有用。'any' 作用域到懒加载模块。'platform' 较罕见,用于多应用场景。

angular
// root injector (singleton app-wide)
@Injectable({ providedIn: 'root' })
export class AuthService {}

// component-level provider: new instance for this component and children
@Component({
  providers: [UserService],   // each AdminComponent gets its own UserService
})
export class AdminComponent {}

// 'any' creates a lazy singleton per lazy module
@Injectable({ providedIn: 'any' })
export class SharedService {}

// 'platform' shared across all Angular apps on the page
@Injectable({ providedIn: 'platform' })
export class GlobalConfig {}

多 Provider 与 Token

multi: true 在一个 token 下注册多个 provider,作为数组注入。适用于插件系统、拦截器、验证器或任何可扩展集合。注入顺序遵循注册顺序。结合 InjectionToken 正确地为数组指定类型。

angular
// define a token for a list of plugins
export const PLUGIN = new InjectionToken<Plugin>('PLUGIN');

// provide multiple plugins
@Component({
  providers: [
    { provide: PLUGIN, useClass: AuthPlugin, multi: true },
    { provide: PLUGIN, useClass: AnalyticsPlugin, multi: true },
  ],
})
export class AppComponent {}

// inject all of them as an array
export class PluginManager {
  plugins = inject(PLUGIN);   // Plugin[]
}

可选与默认 Provider

@Optional() 或 inject(token, { optional: true }) 在未找到 provider 时返回 null 而非抛出错误。用于非关键依赖。其他修饰符:@SkipSelf()(不检查当前注入器)、@Self()(仅当前)、@Host()(上溯到宿主组件)。inject() 的等价物在选项对象中接受标志。

angular
import { Injectable, inject, Optional, Inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class Logger {
  constructor(@Optional() @Inject(CONFIG) private cfg?: Config) {
    // cfg is null if no provider exists — handle gracefully
  }
}

// or with inject()
export class Service {
  cfg = inject(CONFIG, { optional: true });   // Config | null
  baseUrl = inject(API_URL, { optional: true }) ?? '/api';
}

// skipSelf: look in parent injectors only
// self: only this component's injector
// host: limit to host component
07

路由

路由配置

路由将 URL 路径映射到组件。loadComponent/loadChildren 启用懒加载(更小的初始包)。:id 是路由参数。通配符 '**' 捕获未匹配的 URL——放在最后。provideRouter 在独立应用中设置路由器。设置 title 用于浏览器标签名;可以是字符串或 ResolveFn。

angular
// app.routes.ts (standalone)
import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', component: HomeComponent, title: 'Home' },
  { path: 'about', loadComponent: () => import('./about/about.component')
      .then(m => m.AboutComponent) },
  { path: 'users/:id', component: UserDetailComponent },
  { path: 'admin', loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES) },
  { path: '**', component: NotFoundComponent },   // wildcard
];

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)]
};

路由出口与链接

router-outlet 标记路由组件渲染的位置。routerLink 创建导航链接(参数用数组形式)。routerLinkActive 在路由匹配时应用类。编程式导航使用 router.navigate() 或 routerLink。命名出口支持辅助路由(如主视图旁的弹窗)。

angular
<!-- app.component.html -->
<nav>
  <a routerLink="/" routerLinkActive="active">Home</a>
  <a routerLink="/about" routerLinkActive="active">About</a>
  <a [routerLink]="['/users', userId]">My Profile</a>
</nav>

<router-outlet></router-outlet>

<!-- named outlet (secondary route) -->
<router-outlet name="popup"></router-outlet>

<!-- navigate programmatically -->
<button (click)="go()">Go</button>
// constructor(private router: Router) {}
// go() { this.router.navigate(['/users', 42]); }

路由参数与查询参数

当组件在每次导航中销毁和重建时使用 snapshot 一次性读取。当同一组件实例处理不同参数(如从 /users/1 导航到 /users/2)时使用可观察的 param/queryParamMap。始终取消订阅或使用 async 管道以避免泄漏。

angular
// reading route params
export class UserDetailComponent {
  constructor(private route: ActivatedRoute) {
    // snapshot: one-time read (good for static links)
    const id = this.route.snapshot.paramMap.get('id');

    // observable: reacts to param changes (same component reused)
    this.route.paramMap.subscribe(params => {
      console.log(params.get('id'));
    });
  }
}

// query params: /search?q=angular&page=2
this.route.queryParamMap.subscribe(qp => {
  this.q = qp.get('q');
});

<!-- link with query params -->
<a [routerLink]="['/search']" [queryParams]="{ q: 'angular', page: 2 }">

子路由与嵌套路由

子路由在父组件内的嵌套 router-outlet 中渲染。带 pathMatch: 'full' 的 redirectTo 重定向空子路径。此模式构建主从布局(父外壳,子内容)。每层嵌套在父模板中需要自己的 router-outlet。

angular
export const routes: Routes = [
  {
    path: 'admin',
    component: AdminLayoutComponent,
    children: [
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
      { path: 'dashboard', component: DashboardComponent },
      { path: 'users', component: AdminUsersComponent },
      { path: 'settings', component: SettingsComponent },
    ],
  },
];

<!-- AdminLayoutComponent template -->
<h2>Admin Panel</h2>
<nav>...</nav>
<router-outlet></router-outlet>   <!-- child routes render here -->

懒加载

懒加载将应用拆分为按需加载的块,缩小初始包。loadChildren 加载路由文件(Routes 的默认导出);loadComponent 加载单个独立组件。动态 import() 字符串必须是静态路径以便打包器拆分。用于导航后的功能区域。

angular
// main routes - load feature modules on demand
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
  },
  {
    path: 'reports',
    loadComponent: () => import('./reports/reports.component')
      .then(m => m.ReportsComponent),
  },
];

// admin.routes.ts
export const ADMIN_ROUTES: Routes = [
  { path: '', component: AdminComponent },
  { path: 'users', component: AdminUsersComponent },
];

路由事件与导航

路由事件(NavigationStart、NavigationEnd、GuardsCheckStart 等)让你跟踪导航生命周期。始终过滤到特定事件并取消订阅。router.navigate 接受 extras:queryParams、fragment、state(不在 URL 中的瞬态数据)、replaceUrl。state 可通过 router.getCurrentNavigation().extras.state 读取。

angular
// subscribe to router events
export class AppComponent implements OnDestroy {
  private sub = this.router.events.pipe(
    filter((e): e is NavigationEnd => e instanceof NavigationEnd)
  ).subscribe(e => {
    console.log('Navigated to:', e.url);
    this.analytics.track(e.url);
  });

  constructor(private router: Router) {}
  ngOnDestroy() { this.sub.unsubscribe(); }
}

// navigation with extras
this.router.navigate(['/users'], {
  queryParams: { sort: 'name' },
  fragment: 'top',
  state: { from: 'login' },   // passed to next route, not in URL
});
08

表单

模板驱动表单

模板驱动表单使用 ngModel 和模板引用。需要 FormsModule。表单状态(有效性、touched、dirty)通过 ngForm 和 ngModel 指令跟踪。适合简单表单。验证在模板中声明(required、minlength、pattern)。通过 #e='ngModel' 等模板引用访问控件状态。

angular
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #f="ngForm" (ngSubmit)="save(f.value)">
      <input name="email" [(ngModel)]="email" required email #e="ngModel" />
      <span *ngIf="e.invalid && e.touched">Email required</span>

      <input name="pwd" type="password" [(ngModel)]="pwd" required minlength="6" />
      <button [disabled]="f.invalid">Submit</button>
    </form>
  `,
})
export class LoginComponent {
  email = '';
  pwd = '';
  save(v: any) { console.log(v); }
}

响应式表单设置

响应式表单在组件类中定义为 FormControl/FormGroup 实例。需要 ReactiveFormsModule。formControlName 将输入绑定到控件。表单模型是可观察的,可脱离 DOM 测试。nonNullable: true 防止清空输入时出现 null 值。复杂表单首选。

angular
import { ReactiveFormsModule, FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-profile',
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="save()">
      <input formControlName="name" />
      <input formControlName="email" />
      <button [disabled]="form.invalid">Save</button>
    </form>
    <p>Valid: {{ form.valid }}</p>
  `,
})
export class ProfileComponent {
  form = new FormGroup({
    name: new FormControl('', { nonNullable: true }),
    email: new FormControl('', { nonNullable: true }),
  });
  save() { console.log(this.form.value); }
}

FormBuilder 与类型化表单

FormBuilder 减少样板代码。fb.group() 从默认值推断类型。嵌套 group 建模嵌套数据。FormArray 管理动态控件列表。使用 nonNullable,控件保持其声明类型而非 null。用 get() 或 controls 进行类型安全访问。类型化表单(v14+)提供值自动补全和类型检查。

angular
import { FormBuilder, FormGroup } from '@angular/forms';

export class CheckoutComponent {
  private fb = inject(FormBuilder);

  // typed form (v14+ nonNullable)
  form = this.fb.group({
    name: ['', { nonNullable: true }],
    address: this.fb.group({
      street: [''],
      city: [''],
      zip: [''],
    }),
    items: this.fb.array<CartItem>([]),
  });

  // type-safe access
  get items() { return this.form.controls.items; }
  addItem(item: CartItem) { this.items.push(this.fb.control(item)); }
}

表单验证

Validators.required、.minLength、.maxLength、.pattern、.email、.min、.max 是内置的。通过数组应用多个。跨字段验证放在 FormGroup 上,验证器函数接收整个组。显示错误前检查 control.touched/dirty 以避免首次渲染就责备用户。用 .hasError('key') 测试特定错误。

angular
import { Validators } from '@angular/forms';

// built-in validators
const email = new FormControl('', [
  Validators.required,
  Validators.email,
  Validators.maxLength(100),
]);

const password = new FormControl('', [
  Validators.required,
  Validators.minLength(8),
  Validators.pattern(/[A-Z]/),
]);

// cross-field validation at group level
const form = this.fb.group({
  pwd: ['', Validators.required],
  confirm: ['', Validators.required],
}, { validators: matchPassword });   // custom group validator

// reading state in template
<!-- <span *ngIf="email.touched && email.hasError('required')">Required</span> -->

自定义验证器

自定义验证器是返回 ValidationErrors(一个映射)或 null(有效)的函数。异步验证器返回 Observable 或 Promise——用于服务端检查如用户名可用性。始终对异步验证器去抖动(通过 timer/switchMap)以避免每次按键都请求服务器。

angular
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// function validator
export const matchPassword: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
  const pwd = group.get('pwd')?.value;
  const confirm = group.get('confirm')?.value;
  return pwd === confirm ? null : { mismatch: true };
};

// async validator (returns Observable/Promise)
export function uniqueUsername(http: HttpClient): AsyncValidatorFn {
  return (control: AbstractControl) => {
    return timer(500).pipe(
      switchMap(() => http.get(`/api/check/${control.value}`)),
      map(exists => exists ? { taken: true } : null),
    );
  };
}

表单值变化与状态

valueChanges 和 statusChanges 是在每次输入时发出的 Observable。debounceTime 防止快速输入时过度工作。takeUntil 配合 Subject 是销毁时取消订阅的惯用方式。patchValue 更新部分表单值;setValue 需要所有字段。使用 { emitEvent: false } 静默更新而不触发 valueChanges。

angular
export class FilterComponent implements OnDestroy {
  private destroy = new Subject<void>();
  form = this.fb.group({ q: [''], category: ['all'] });

  ngOnInit() {
    // react to entire form changes (debounced)
    this.form.valueChanges.pipe(
      debounceTime(300),
      takeUntil(this.destroy),
    ).subscribe(v => this.search(v));

    // react to a single control
    this.form.controls.q.valueChanges.subscribe(q => {
      console.log('q changed:', q);
    });
  }

  ngOnDestroy() { this.destroy.next(); }
}
09

HttpClient

设置与基本 GET

provideHttpClient() 全应用注册客户端。HttpClient 方法返回冷的 Observable——仅在订阅时执行(在模板中用 async 管道或在服务中 subscribe)。响应通过泛型类型化。始终集中或逐调用处理错误。withFetch()(v18)启用基于 fetch 的现代后端。

angular
// app.config.ts
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient()],
};

// usage in a service
@Injectable({ providedIn: 'root' })
export class PostService {
  private http = inject(HttpClient);
  private api = 'https://jsonplaceholder.typicode.com/posts';

  getPosts() {
    return this.http.get<Post[]>(this.api);
  }
}

// in a component (use async pipe to auto-unsubscribe)
export class PostListComponent {
  posts$ = this.posts.getPosts();
  constructor(private posts: PostService) {}
}

POST / PUT / DELETE

post/put/patch 返回发出一次响应后完成的 Observable。第二个参数是请求体。泛型类型收窄响应。URL 可以是带参数的模板字符串。始终订阅——Observable 是冷的,否则不会触发。考虑 toPromise()(已弃用)或 firstValueFrom() 用于一次性读取。

angular
export class PostService {
  private http = inject(HttpClient);

  create(post: Omit<Post, 'id'>) {
    return this.http.post<Post>('/api/posts', post);
  }

  update(id: number, patch: Partial<Post>) {
    return this.http.put<Post>(`/api/posts/${id}`, patch);
  }

  remove(id: number) {
    return this.http.delete<void>(`/api/posts/${id}`);
  }
}

// all return Observables — subscribe or use async pipe
// HTTP methods return a single value then complete

查询参数与请求头

params 接受对象、HttpParams 或数组——Angular 将它们编码到查询字符串。headers 设置请求头。observe: 'response' 返回完整 HttpResponse(状态、头、体)而非仅体。observe: 'events' 配合 reportProgress 流式传输文件上传/下载进度事件。

angular
export class SearchService {
  private http = inject(HttpClient);

  search(term: string, page = 1) {
    return this.http.get<Result[]>('/api/search', {
      params: { q: term, page, sort: 'desc' },
      headers: { 'X-Api-Key': 'secret' },
      observe: 'response',   // get full HttpResponse
      responseType: 'json',
    });
  }

  // observe: 'events' streams upload/download progress
  upload(file: File) {
    return this.http.post('/api/upload', file, {
      reportProgress: true,
      observe: 'events',
    });
  }
}

拦截器

函数式拦截器(v15+)是通过 withInterceptors() 链接的普通函数。它们对每个请求按顺序运行,非常适合认证令牌、日志、重试逻辑或缓存。clone() 创建修改后的请求(请求不可变)。next(req) 函数返回响应流——retry、catchError 等操作符在此处应用。

angular
// functional interceptor (v15+)
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('token');
  const authReq = token
    ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
    : req;
  return next(authReq);
};

// register in app.config.ts
providers: [provideHttpClient(withInterceptors([authInterceptor]))]

// error/logging interceptor
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  console.log('Request:', req.url);
  return next(req).pipe(tap({
    error: e => console.error('Failed:', req.url, e),
  }));
};

错误处理

使用 RxJS 操作符实现 HTTP 韧性:retry 在出错时重新订阅,timeout 中止慢请求,catchError 将错误映射为兜底值或重新抛出。也始终在消费端处理错误——未捕获的 Observable 错误会停止流。用 next/error/complete 回调订阅以完整处理生命周期。

angular
export class PostService {
  getPost(id: number) {
    return this.http.get<Post>(`/api/posts/${id}`).pipe(
      retry(2),                              // retry on failure
      timeout(5000),                         // abort after 5s
      catchError(err => {
        if (err.status === 404) return of(null);   // graceful fallback
        return throwError(() => new Error('Network error'));
      }),
    );
  }
}

// component-side
this.posts.getPost(1).subscribe({
  next: p => this.post = p,
  error: e => this.error = e.message,
  complete: () => this.loading = false,
});

类型化响应与 withFetch

泛型为响应体提供类型,但运行时不验证——为安全起见,使用 zod 等运行时验证器。observe: 'response' 暴露状态、头和体。withFetch()(v18)使用 Fetch API 代替 XHR,启用流式传输和更好的服务端渲染。withXsrfConfiguration 添加自动 CSRF 令牌处理。

angular
// typed generic — response is typed, runtime NOT validated
this.http.get<User[]>('/api/users');

// full HttpResponse observation
this.http.get<User>('/api/users/1', { observe: 'response' })
  .subscribe(res => {
    console.log(res.status);   // 200
    console.log(res.headers.get('X-Total-Count'));
    console.log(res.body);     // User
  });

// opt into fetch() backend (v18+, recommended)
// provideHttpClient(withFetch())

// with XSRF protection enabled
// provideHttpClient(withXsrfConfiguration({ cookieName: 'XSRF-TOKEN' }))
10

Observables 与 RxJS

基本 Observable

Observable 是惰性的——生产者仅在订阅时运行。它们可能发出零个或多个值,然后完成或出错。subscribe 回调接收 next、error、complete。始终取消订阅长生命周期的订阅(或使用 async 管道 / takeUntilDestroyed)。of() 同步发出值;from() 展平数组或 promise。

angular
import { Observable } from 'rxjs';

const obs = new Observable<number>(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  setTimeout(() => {
    subscriber.next(3);
    subscriber.complete();
  }, 1000);
});

const sub = obs.subscribe({
  next: v => console.log(v),
  error: e => console.error(e),
  complete: () => console.log('done'),
});

// cleanup
sub.unsubscribe();

// use of(...) for synchronous values
of(1, 2, 3).subscribe(v => console.log(v));
// from(iterable) for arrays/async iterables
from([1, 2, 3]).subscribe(console.log);

常用操作符

操作符转换流。map/filter 类似数组方法。debounceTime 等待安静期(非常适合搜索输入)。distinctUntilChanged 跳过连续重复值。switchMap 在新值到达时取消之前的内部 Observable——非常适合边输入边搜索。操作符从左到右管道。

angular
import { of, from, interval } from 'rxjs';
import { map, filter, switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';

of(1, 2, 3, 4).pipe(
  map(x => x * 10),                          // 10, 20, 30, 40
  filter(x => x > 15),                       // 20, 30, 40
).subscribe(console.log);

// search input with debounce
input$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => http.get(`/api/search?q=${term}`)),
).subscribe(results => this.results = results);

组合流

combineLatest 在任意源发出时发出(所有源至少发出一次后)——适合派生视图状态。forkJoin 等待所有源完成,返回每个的最后值——用于并行 HTTP 请求。merge 交错发出。zip 按索引配对。race 选择第一个发出的 Observable 并忽略其余。

angular
import { combineLatest, forkJoin, merge, race, zip } from 'rxjs';

// combineLatest: emits when ANY source emits, after all have emitted
combineLatest([user$, settings$]).subscribe(([u, s]) => ...);

// forkJoin: waits for all to complete, emits last values (like Promise.all)
forkJoin({ user: user$, posts: posts$ }).subscribe(r => ...);

// merge: emits values from all sources as they come
merge(clicks$, keys$).subscribe(e => ...);

// zip: pairs emissions by index
zip(range(1, 3), ['a', 'b', 'c']).subscribe(([n, letter]) => ...);

// race: takes the first to emit, ignores the rest
race(primary$, fallback$).subscribe(v => ...);

Subject(多播)

Subject 既是 Observable 又是 Observer——调用 .next() 推送值。Subject:无初始值,迟到订阅者错过之前发出的值。BehaviorSubject:需要初始值,新订阅者立即获得当前值(用于状态)。ReplaySubject:为新订阅者缓冲最后 N 个值(用于重放事件)。

angular
import { Subject, BehaviorSubject, ReplaySubject } from 'rxjs';

// Subject: emits to current subscribers only
const click$ = new Subject<MouseEvent>();
click$.subscribe(x => console.log('A:', x));
click$.next({} as MouseEvent);   // A: logs it

// BehaviorSubject: keeps last value, new subs get it immediately
const count$ = new BehaviorSubject(0);
count$.subscribe(v => console.log(v));   // logs 0 immediately
count$.next(1);                          // logs 1 to subscribers

// ReplaySubject: replays last N values to new subscribers
const recent$ = new ReplaySubject<string>(2);
recent$.next('a'); recent$.next('b'); recent$.next('c');
recent$.subscribe(v => console.log(v));  // logs 'b', then 'c'

takeUntil 与清理

takeUntil(unsubscribe$) 在通知器发出时自动完成订阅——避免组件泄漏的标准模式。takeUntilDestroyed()(v16+)使用 Angular 的 DestroyRef 自动取消订阅,无需 OnDestroy 样板。必须在注入上下文中调用(构造函数或字段初始化器)或显式接受 DestroyRef。

angular
import { Subject, takeUntil, takeUntilDestroyed } from 'rxjs';

export class DemoComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    this.svc.stream$.pipe(
      takeUntil(this.destroy$),
    ).subscribe(data => this.data = data);
  }

  ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
}

// v16+: takeUntilDestroyed (uses DestroyRef, no boilerplate)
export class DemoComponent {
  data: any;
  constructor() {
    inject(SomeService).stream$.pipe(
      takeUntilDestroyed(),
    ).subscribe(d => this.data = d);
  }
}

switchMap vs mergeMap vs concatMap

这些展平操作符在处理重叠内部 Observable 时不同。switchMap 取消(边输入边搜索)。mergeMap 并行(加载多个)。concatMap 排队(保持顺序)。exhaustMap 丢弃(防止重复提交)。选错会导致竞态条件——将操作符与用例的取消语义匹配。

angular
import { from, interval } from 'rxjs';
import { switchMap, mergeMap, concatMap, exhaustMap } from 'rxjs/operators';

// switchMap: cancels the previous inner stream on new value
input$.pipe(switchMap(term => search(term)));
// Best for: typeahead search (discard stale results)

// mergeMap: runs all inner streams in parallel
ids$.pipe(mergeMap(id => fetchDetail(id)));
// Best for: parallel requests, order doesn't matter

// concatMap: queues inner streams, runs one at a time
queue$.pipe(concatMap(task => runTask(task)));
// Best for: ordered sequential operations

// exhaustMap: ignores new values while inner stream is active
save$.pipe(exhaustMap(() => saveForm()));
// Best for: prevent double-submit (ignore clicks while saving)
11

生命周期钩子

钩子顺序概览

钩子按严格顺序触发。ngOnChanges 在 @Input 变化时触发(包括初始)。ngOnInit 在第一次 ngOnChanges 后触发一次。ngDoCheck 每个变更检测周期运行。After* 钩子表示内容/视图已就绪可查询 DOM。ngOnDestroy 是清理点——取消订阅、释放资源、清除定时器。

angular
export class LifecycleComponent implements
    OnChanges, OnInit, DoCheck, AfterContentInit,
    AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {

  ngOnChanges()    { console.log('1. input changes'); }
  ngOnInit()       { console.log('2. component init (once)'); }
  ngDoCheck()      { console.log('3. custom change detection'); }
  ngAfterContentInit()      { console.log('4. projected content ready'); }
  ngAfterContentChecked()   { console.log('5. projected content checked'); }
  ngAfterViewInit()         { console.log('6. view (template) ready'); }
  ngAfterViewChecked()      { console.log('7. view checked'); }
  ngOnDestroy()    { console.log('8. cleanup before destroy'); }
}

ngOnInit 与 ngOnDestroy

ngOnInit 在输入设置后运行一次——适合昂贵初始化(获取、订阅)的位置,因为构造函数应保持轻量。ngOnDestroy 是组件销毁时调用的唯一钩子——切勿跳过此处清理,否则会泄漏订阅、定时器和事件监听器。构造函数仅用于 DI;逻辑放 ngOnInit。

angular
export class ChartComponent implements OnInit, OnDestroy {
  private timer?: ReturnType<typeof setInterval>;

  ngOnInit() {
    // good for: data fetching, initialization, subscriptions
    this.timer = setInterval(() => this.refresh(), 5000);
    this.loadInitialData();
  }

  ngOnDestroy() {
    // cleanup: clear timers, unsubscribe, release resources
    if (this.timer) clearInterval(this.timer);
    // also: this.sub.unsubscribe();
  }

  private refresh() { /* ... */ }
  private loadInitialData() { /* ... */ }
}

ngOnChanges(SimpleChange)

ngOnChanges 在 @Input 引用变化时触发,带有 SimpleChanges 映射:每个条目有 previousValue、currentValue、firstChange 和 isFirstChange()。当你变更对象属性时它不会触发——仅引用变化时。对于深层变化,使用带 setter 的 ngOnChanges 或带 IterableDiffers 的 ngDoCheck。

angular
export class PaginationComponent implements OnChanges {
  @Input() page = 1;
  @Input() total = 0;

  ngOnChanges(changes: SimpleChanges) {
    if (changes['page'] && !changes['page'].firstChange) {
      // react only when page actually changed (not on init)
      console.log('page:', changes['page'].previousValue, '->', changes['page'].currentValue);
      this.fetchPage(this.page);
    }
  }

  private fetchPage(p: number) { /* ... */ }
}

ngAfterViewInit 与 ViewChild

ViewChild/ContentChild 查询在各自的 init 钩子之后解析。ngAfterViewInit 用于组件自身模板中的元素;ngAfterContentInit 用于通过 <ng-content> 投影的内容。在 ngOnInit 中访问这些 ref 返回 undefined。! 非空断言告诉编译器它在 init 后已设置——但仅在 After* 钩子中安全使用。

angular
export class CanvasComponent implements AfterViewInit {
  @ViewChild('canvas') canvasRef!: ElementRef<HTMLCanvasElement>;

  // ❌ canvasRef is undefined in ngOnInit
  ngOnInit() { /* this.canvasRef is undefined here */ }

  // ✅ available after view init
  ngAfterViewInit() {
    const canvas = this.canvasRef.nativeElement;
    const ctx = canvas.getContext('2d');
    ctx?.fillText('Hello', 10, 50);
  }
}

// template: <canvas #canvas></canvas>

ngDoCheck(自定义检测)

ngDoCheck 每个变更检测周期运行——保持轻量。使用 IterableDiffers/KeyValueDiffers 检测 ngOnChanges 遗漏的数组/对象变更(因为引用未变)。这是跟踪内部变更的逃生舱。避免在此处放昂贵逻辑;它频繁运行可能导致性能问题。

angular
export class ListComponent implements DoCheck {
  @Input() items: Item[] = [];
  private differ: IterableDiffer<Item>;

  constructor(differs: IterableDiffers) {
    this.differ = differs.find(items).create<Item>((i) => i.id);
  }

  ngDoCheck() {
    const changes = this.differ.diff(this.items);
    if (changes) {
      changes.forEachAddedItem(r => console.log('Added:', r.item));
      changes.forEachRemovedItem(r => console.log('Removed:', r.item));
    }
  }
}

内容 vs 视图钩子

内容钩子(AfterContentInit/Checked)针对通过 <ng-content> 投影的内容触发。视图钩子(AfterViewInit/Checked)针对组件自身模板触发。内容钩子在视图钩子之前触发,因为内容在视图完成前初始化。@ContentChild 查询投影元素;@ViewChild 查询自身模板元素。

angular
export class CardComponent implements
    AfterContentInit, AfterViewInit, AfterContentChecked, AfterViewChecked {

  @ContentChild('header') headerTpl?: TemplateRef<unknown>;
  @ViewChild('body') bodyEl?: ElementRef<HTMLElement>;

  // projected content (<ng-content>) is ready
  ngAfterContentInit() {
    console.log('content ready:', this.headerTpl);
  }

  // own template view is ready
  ngAfterViewInit() {
    console.log('view ready:', this.bodyEl?.nativeElement);
  }

  // ContentChecked/ViewChecked run every CD cycle after init — beware perf
}
12

内容投影

基本 ng-content

<ng-content> 是父组件提供内容投影的占位符。可看作 Angular 的 <slot>。不带 select 时,所有投影内容进入此处。投影内容在父组件上下文中编译——如 {{ }} 的绑定针对父组件而非 CardComponent 解析。

angular
// card.component.ts
@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <ng-content></ng-content>
    </div>
  `,
  styles: ['.card { border: 1px solid #ccc; padding: 16px; }'],
})
export class CardComponent {}

<!-- usage: anything inside projects into <ng-content> -->
<app-card>
  <h3>Title</h3>
  <p>Body content here.</p>
</app-card>

多插槽投影

ng-content 上的 select 属性定位特定投影内容:按元素('header')、属性('[header]')、类('.active')或组合('div.foo[bar]')。未匹配的内容落入未带 select 的 ng-content。投影内容保持父组件的样式和上下文——仅其在 DOM 中的位置改变。

angular
@Component({
  selector: 'app-layout',
  template: `
    <header><ng-content select="[header]"></ng-content></header>
    <main><ng-content></ng-content></main>
    <footer><ng-content select="footer"></ng-content></footer>
  `,
})
export class LayoutComponent {}

<!-- usage: select matches attributes, elements, classes -->
<app-layout>
  <nav header>Top nav</nav>
  <p>Main body</p>
  <footer>Bottom</footer>
</app-layout>

条件投影(ng-template)

对于条件或重复投影,ng-template 配合 ngTemplateOutlet 提供完全控制。ContentChildren 查询投影的指令。ngTemplateOutlet 在选定位置渲染 TemplateRef——适用于选项卡、手风琴或条件插槽。此模式将内容声明与其渲染位置/时机解耦。

angular
@Component({
  selector: 'app-tabs',
  template: `
    <div class="tabs">
      @for (t of tabs; track t) {
        <button (click)="active = t">{{ t.title }}</button>
      }
    </div>
    @for (t of tabs; track t) {
      @if (t === active) {
        <ng-container [ngTemplateOutlet]="t.template" />
      }
    }
  `,
})
export class TabsComponent {
  @ContentChildren(TabDirective) tabs!: QueryList<TabDirective>;
  active: any;
}

ContentChild 与 ContentChildren

@ContentChild / @ContentChildren 查询投影内容(vs ViewChild 查询自身模板)。ContentChildren 返回 QueryList,内容变化时更新。在 ngAfterContentInit 中解析。{ descendants: true } 选项(默认)遍历所有投影内容。用 { read: ... } 查询特定类型(ElementRef、TemplateRef 等)。

angular
@Directive({ selector: 'tab' })
export class TabDirective {
  @Input() title = '';
  @Input() active = false;
  @ContentChild(TemplateRef) template!: TemplateRef<unknown>;
}

@Component({
  selector: 'app-tabset',
  template: `
    @for (tab of tabs; track tab) {
      <button (click)="select(tab)">{{ tab.title }}</button>
    }
    <ng-container *ngTemplateOutlet="activeTab?.template" />
  `,
})
export class TabsetComponent implements AfterContentInit {
  @ContentChildren(TabDirective) tabs!: QueryList<TabDirective>;
  activeTab?: TabDirective;

  ngAfterContentInit() {
    this.activeTab = this.tabs.find(t => t.active) ?? this.tabs.first;
  }
}

带上下文的 ngTemplateOutlet

ngTemplateOutlet 渲染 TemplateRef 并带可选上下文。let-x 绑定到 context.$implicit(默认值);let-y='key' 绑定到 context.key。这就是 *ngFor 和 ngTemplateOutlet 传递循环变量的方式。用于父组件可自定义的可复用列表/网格/项模板,同时组件控制迭代逻辑。

angular
@Component({
  selector: 'app-list',
  template: `
    @for (item of items; track item.id) {
      <ng-container *ngTemplateOutlet="itemTpl; context: { $implicit: item, i: $index }" />
    }
    <ng-template #itemTpl let-item let-i="i">
      <li>{{ i }}: {{ item.name }}</li>
    </ng-template>
  `,
})
export class ListComponent {
  items = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
}

ng-content select 模式

select 接受类 CSS 选择器:类(.x)、属性([x])、元素(header)或逗号分隔列表('a, b')。未匹配任何选择器的内容落入未带 select 的 ng-content。若无兜底,未匹配内容被丢弃。选择器匹配投影元素自身的属性,而非其内部结构。

angular
@Component({
  selector: 'app-modal',
  template: `
    <div class="modal">
      <ng-content select=".modal-header"></ng-content>
      <ng-content select="[body]"></ng-content>
      <ng-content select="app-modal-footer, footer"></ng-content>
      <!-- fallback for unmatched content -->
      <ng-content></ng-content>
    </div>
  `,
})

<!-- matches by class, attribute, element, or selector list -->
<app-modal>
  <div class="modal-header">Title</div>
  <section body>Body text</section>
  <app-modal-footer>OK</app-modal-footer>
  <p>This falls into the unselect'd slot.</p>
</app-modal>
13

路由守卫

CanActivate(函数式)

函数式守卫(v14.2+)返回 true/false/UrlTree 或它们的 Observable/Promise。返回 UrlTree 重定向到该路由。inject() 可用,因为守卫在注入上下文中运行。CanActivate 阻止导航到路由。在每条路由的 canActivate 数组中注册守卫。它们在每次导航到该路由时运行。

angular
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';

export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isLoggedIn) return true;

  // redirect to login, preserving the attempted URL
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url },
  });
};

// register in routes
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }

CanActivateChild

CanActivateChild 在父路由下的每个子路由之前运行——比在每个子路由上重复 canActivate 更简洁。用于功能区域的共享保护。返回与 CanActivate 相同的值类型。若也要保护父路由本身,也在父路由上添加 canActivate。

angular
export const adminGuard: CanActivateChildFn = (route, state) => {
  const auth = inject(AuthService);

  if (!auth.isAdmin) {
    return inject(Router).parseUrl('/forbidden');
  }
  return true;
};

// applies to all children of this route
{
  path: 'admin',
  canActivateChild: [adminGuard],
  children: [
    { path: 'users', component: AdminUsersComponent },
    { path: 'settings', component: AdminSettingsComponent },
  ],
}

CanDeactivate(守卫未保存更改)

CanDeactivate 在离开路由前运行——非常适合警告未保存的表单更改。守卫接收组件实例,因此调用其上的方法。返回 false 取消导航,true 继续。对于异步(模态确认),返回 Observable 或 Promise。组件必须实现守卫期望的契约。

angular
import { CanDeactivateFn } from '@angular/router';
import { inject } from '@angular/core';

export interface CanComponentDeactivate {
  canDeactivate: () => boolean | Observable<boolean>;
}

export const unsavedGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
  return component?.canDeactivate() ?? true;
};

// component
export class EditComponent implements CanComponentDeactivate {
  canDeactivate() {
    if (this.form.dirty) return confirm('Discard changes?');
    return true;
  }
}

// route: { path: 'edit/:id', canDeactivate: [unsavedGuard] }

CanMatch(条件加载)

CanMatch(v14.1+)在懒加载路由的块下载之前运行——对于付费功能门控比 CanActivate 更高效。若返回 false,路由被完全跳过(其他匹配路由有机会)。CanActivate 会先加载块再拒绝。用 CanMatch 阻止下载用户无法访问的代码。

angular
import { CanMatchFn } from '@angular/router';

export const paidFeatureGuard: CanMatchFn = (route, segments) => {
  const auth = inject(AuthService);
  // runs BEFORE the lazy chunk is even loaded
  if (auth.hasProPlan) return true;

  return inject(Router).createUrlTree(['/upgrade']);
};

// route configuration
{
  path: 'premium',
  canMatch: [paidFeatureGuard],
  loadComponent: () => import('./premium/premium.component'),
}

Resolve(预取数据)

Resolve 在路由激活前运行,延迟导航直到数据到达。解析的数据放入 route.snapshot.data。用于确保组件渲染时有数据(无空状态闪烁)。在解析器内处理错误——返回 EMPTY 取消导航。对于非阻塞加载,改为在 ngOnInit 中获取。

angular
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';

export const userResolver: ResolveFn<User> = (route) => {
  const id = route.paramMap.get('id')!;
  return inject(UserService).getUser(+id).pipe(
    catchError(() => {
      inject(Router).navigate(['/users']);
      return EMPTY;
    }),
  );
};

// route
{ path: 'users/:id', resolve: { user: userResolver }, component: UserComponent }

// component reads pre-fetched data
export class UserComponent {
  user = this.route.snapshot.data['user'] as User;
  constructor(private route: ActivatedRoute) {}
}

守卫组合与顺序

多个守卫按数组顺序运行;第一个失败者取消导航。canMatch 在懒加载块加载前运行;canActivate 在之后。canDeactivate 在离开时运行。所有守卫可同步(返回 boolean)或异步(Observable/Promise)。保持快速——它们阻塞 UI。繁重的服务端检查应去抖动或缓存结果。

angular
// multiple guards run in declaration order, all must pass
{
  path: 'admin',
  canActivate: [authGuard, roleGuard],
  canMatch: [paidFeatureGuard],
  canDeactivate: [unsavedGuard],
  loadChildren: () => import('./admin/admin.routes'),
}

// guard returning Observable (async check)
export const roleGuard: CanActivateFn = () => {
  return inject(AuthService).hasRole('admin').pipe(
    map(ok => ok || inject(Router).parseUrl('/forbidden')),
  );
};

// ordering: canMatch → resolve → canActivate → canActivateChild
14

解析器

基本解析器(ResolveFn)

ResolveFn 是函数式解析器 API。它在导航完成前运行,返回值放在你分配的键下的 route.data 中。组件可从 snapshot.data 同步读取——无需加载状态。订阅 route.data 以在同组件路由间导航时响应式更新。

angular
import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';

export const postResolver: ResolveFn<Post> = (route, state) => {
  const id = Number(route.paramMap.get('id'));
  return inject(PostService).getPost(id);
};

// route
{
  path: 'posts/:id',
  resolve: { post: postResolver },
  component: PostDetailComponent,
}

// reading the resolved data
export class PostDetailComponent {
  post = this.route.snapshot.data['post'] as Post;
  constructor(private route: ActivatedRoute) {}

  // reactive: re-fetch when :id changes (same component reused)
  ngOnInit() {
    this.route.data.subscribe(d => this.post = d['post']);
  }
}

多个解析器

多个解析器并行运行——所有必须在路由激活前完成。resolve 中的每个键成为 route.data 中的一个键。若一个失败,导航被取消。对于独立数据,这很高效。若一个依赖另一个,嵌套它们或在单个解析器中链式调用。权衡解析器与 ngOnInit 获取。

angular
{
  path: 'dashboard',
  component: DashboardComponent,
  resolve: {
    user: userResolver,
    stats: statsResolver,
    notifications: notifResolver,
  },
}

// component
export class DashboardComponent {
  constructor(private route: ActivatedRoute) {}
  ngOnInit() {
    const { user, stats, notifications } = this.route.snapshot.data;
    // all three resolved before component loads
  }
}

解析器错误处理

显式处理解析器错误——未捕获的错误会静默取消导航。EMPTY 完成流而不发出,取消导航。返回 null 让组件优雅处理缺失数据。对于 404,重定向到未找到路由。始终记录或暴露错误;静默失败对用户难以调试。

angular
export const userResolver: ResolveFn<User | null> = (route) => {
  const id = route.paramMap.get('id')!;
  const router = inject(Router);
  const service = inject(UserService);

  return service.getUser(+id).pipe(
    catchError(err => {
      if (err.status === 404) {
        // redirect to a 'not found' page, cancel navigation
        router.navigate(['/404']);
        return EMPTY;
      }
      // re-throw other errors
      return throwError(() => err);
    }),
  );
};

解析器 vs ngOnInit 获取

解析器阻塞导航直到数据到达——UX 干净但感觉较慢。ngOnInit 获取是非阻塞的但要求组件中有加载状态。现代 Angular 指导倾向于非阻塞:在组件中获取并显示加载骨架以获得更好的感知性能。仅在组件确实无法在没有数据时渲染时才使用解析器。

angular
// Approach A: Resolver (blocks navigation until data arrives)
// ✅ Component renders with data — no flicker
// ❌ Slower navigation feel, blocks route transition
{
  path: 'user/:id',
  resolve: { user: userResolver },
  component: UserComponent,
}

// Approach B: Fetch in ngOnInit (non-blocking)
// ✅ Snappy navigation, show loading skeleton
// ❌ Component must handle null/loading state
export class UserComponent implements OnInit {
  user?: User;
  loading = true;
  ngOnInit() {
    this.route.params.subscribe(p => {
      this.loading = true;
      this.svc.getUser(+p['id']).subscribe(u => {
        this.user = u;
        this.loading = false;
      });
    });
  }
}

带查询参数与信号的解析器

解析器可从路由读取 paramMap 和 queryParamMap。当同一组件处理不同参数时,响应式订阅 route.data 而非使用 snapshot。使用信号时,从 data 订阅更新信号。将解析器与信号结合实现干净、响应式的数据流。避免繁重的解析器——它们阻塞导航。

angular
// read query params inside a resolver
export const searchResolver: ResolveFn<SearchResult[]> = (route) => {
  const q = route.queryParamMap.get('q') ?? '';
  const page = Number(route.queryParamMap.get('page') ?? 1);
  return inject(SearchService).search(q, page);
};

// resolver feeding a signal-based component
export class SearchComponent {
  private route = inject(ActivatedRoute);
  results = signal<SearchResult[]>([]);

  constructor() {
    this.route.data.subscribe(d => this.results.set(d['results']));
  }
}
15

动画

设置与基本动画

provideAnimations() 注册动画模块。trigger() 定义命名动画;transition() 声明状态间的变化。:enter 和 :leave 是通过 *ngIf 或 @if 添加/移除元素的内置伪状态。在模板中通过 @triggerName 应用。对于 v17+ 控制流,动画仍与 @if/@for 一起工作。

angular
// app.config.ts
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
  providers: [provideAnimations()],
};

// component
@Component({
  selector: 'app-fade',
  animations: [
    trigger('fadeIn', [
      transition(':enter', [
        style({ opacity: 0 }),
        animate('300ms ease-in', style({ opacity: 1 })),
      ]),
      transition(':leave', [
        animate('300ms ease-out', style({ opacity: 0 })),
      ]),
    ]),
  ],
  template: `<div *ngIf="show" @fadeIn>Hello</div>`,
})

状态与过渡

state() 定义静止时的命名样式。transition('a => b') 从状态 a 动画到 b;'<=>' 是双向的。将触发器绑定到返回状态名称的表达式。状态样式立即应用(无动画);过渡在它们之间动画。使用 'void' 表示尚未在 DOM 中的元素,'*' 作为通配符。

angular
@Component({
  selector: 'app-toggle',
  animations: [
    trigger('openClose', [
      state('open', style({ height: '*', opacity: 1 })),
      state('closed', style({ height: '0', opacity: 0 })),
      transition('open => closed', animate('200ms ease-out')),
      transition('closed => open', animate('300ms ease-in')),
      // bidirectional shortcut: 'open <=> closed'
    ]),
  ],
  template: `
    <button (click)="isOpen = !isOpen">Toggle</button>
    <div [@openClose]="isOpen ? 'open' : 'closed'">Panel</div>
  `,
})
export class ToggleComponent { isOpen = false; }

关键帧与并行动画

keyframes() 定义带 offset(0 到 1)的中间步骤以进行细粒度控制——类似 CSS @keyframes。group() 并行运行动画;sequence() 依次运行。用这些组合复杂的入场/出场效果。保持动画简短(200-400ms)并为无障碍尊重 prefers-reduced-motion。

angular
trigger('wiggle', [
  transition('* => *', [
    animate('600ms ease-in-out', keyframes([
      style({ transform: 'translateX(0)', offset: 0 }),
      style({ transform: 'translateX(-10px)', offset: 0.25 }),
      style({ transform: 'translateX(10px)', offset: 0.75 }),
      style({ transform: 'translateX(0)', offset: 1 }),
    ])),
  ]),
]),

// parallel/group animations
transition(':enter', [
  group([
    animate('300ms', style({ opacity: 1 })),
    animate('300ms', style({ transform: 'translateY(0)' })),
  ]),
]),

动画列表(@for)

将 :enter/:leave 动画应用于 @for 项——它们在项添加或移除时触发。track 确保 Angular 跨变更正确识别项,因此动画在实际添加/移除时触发(而非重排)。无正确追踪的列表动画可能卡顿。对于重排动画,使用 query() 和 animateChild()。

angular
@Component({
  selector: 'app-list',
  animations: [
    trigger('itemAnim', [
      transition(':enter', [
        style({ height: 0, opacity: 0 }),
        animate('200ms', style({ height: '*', opacity: 1 })),
      ]),
      transition(':leave', [
        animate('200ms', style({ height: 0, opacity: 0 })),
      ]),
    ]),
  ],
  template: `
    @for (item of items; track item.id) {
      <div @itemAnim>{{ item.name }}</div>
    }
  `,
})
export class AnimListComponent { items = [...]; }

路由动画

将动画触发器绑定到 router-outlet 的激活状态。query(':enter'/:leave) 定位传入/传出的路由组件。{ optional: true } 避免一侧不存在时出错(初始加载无 :leave)。position: absolute 在过渡期间堆叠新旧组件。路由动画在视图间提供应用般的过渡。

angular
@Component({
  selector: 'app-root',
  animations: [
    trigger('routeAnim', [
      transition('* => *', [
        query(':enter', [
          style({ opacity: 0, position: 'absolute', width: '100%' }),
          animate('300ms', style({ opacity: 1 })),
        ], { optional: true }),
        query(':leave', [
          animate('300ms', style({ opacity: 0 })),
        ], { optional: true }),
      ]),
    ]),
  ],
  template: `
    <div [@routeAnim]="o.isActivated ? o.activatedRoute : ''">
      <router-outlet #o="outlet"></router-outlet>
    </div>
  `,
})

减弱动画与 CSS 动画

始终为无障碍尊重 prefers-reduced-motion——一些用户会晕动症。Angular 动画为包增加约 50KB;对于简单淡入,普通 CSS 动画可能足够。需要状态化过渡(状态机)、父子协调或路由动画时使用 Angular 动画。CSS 对于悬停/淡入效果更轻量。

angular
// respect prefers-reduced-motion
@Component({
  selector: 'app-safe-anim',
  animations: [
    trigger('fade', [
      transition(':enter', [
        style({ opacity: 0 }),
        animate('300ms', style({ opacity: 1 })),
      ]),
    ]),
  ],
  // or just use CSS — Angular animations add bundle weight
  template: `<div class="fade-in" @fade>Content</div>`,
  styles: [`
    @media (prefers-reduced-motion: reduce) {
      .fade-in { animation: none !important; transition: none !important; }
    }
  `],
})
16

测试

组件测试设置

TestBed 配置测试模块。对于独立组件,直接导入(无需 TestBed.configureTestingModule 声明)。createComponent 返回 ComponentFixture:用 .componentInstance 获取类,.nativeElement 获取 DOM,.detectChanges() 触发变更检测。带 async/await 的 beforeEach 处理 compileComponents。

angular
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';

describe('MyComponent', () => {
  let fixture: ComponentFixture<MyComponent>;
  let component: MyComponent;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [MyComponent],   // standalone components are imported
    }).compileComponents();

    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();   // runs initial change detection
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

用 Mock 服务测试

提供服务的 Mock 实现以隔离被测组件。jasmine.createSpy() 让你控制返回值并断言调用。useValue 用于对象 Mock,useClass 用于基于类的假实现。用 HttpTestingController 覆盖 HttpClient 进行 HTTP 测试。Mocking 保持测试快速和确定——无真实网络调用。

angular
describe('UserComponent', () => {
  let mockUserService = {
    getUsers: jasmine.createSpy().and.returnValue(of([...])),
    addUser: jasmine.createSpy().and.returnValue(of({})),
  };

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserComponent],
      providers: [
        { provide: UserService, useValue: mockUserService },
      ],
    }).compileComponents();
  });

  it('loads users on init', () => {
    const fixture = TestBed.createComponent(UserComponent);
    fixture.detectChanges();
    expect(mockUserService.getUsers).toHaveBeenCalled();
    expect(fixture.componentInstance.users.length).toBe(2);
  });
});

HTTP 测试

HttpTestingController 在测试中拦截 HttpClient 调用——无真实网络。expectOne 断言请求已发出并返回它;flush() 模拟服务器响应。verify() 确保无未匹配请求剩余。测试成功和错误路径:用状态码 flush(如 { status: 500, statusText: 'Server Error' })模拟错误。

angular
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';

describe('PostService', () => {
  let service: PostService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        PostService,
        provideHttpClient(),
        provideHttpClientTesting(),
      ],
    });
    service = TestBed.inject(PostService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  it('fetches posts', () => {
    service.getPosts().subscribe(posts => expect(posts.length).toBe(2));

    const req = httpMock.expectOne('/api/posts');
    expect(req.request.method).toBe('GET');
    req.flush([{ id: 1 }, { id: 2 }]);   // simulate response
    httpMock.verify();                    // no outstanding requests
  });
});

测试模板与交互

fixture.nativeElement 提供原始 DOM 访问用于断言。设置属性后 detectChanges() 同步模板绑定。spyOn 让你断言方法调用。对于组件内部查询,debugElement.query(By.css()) 是类型安全的。通过原生 DOM 方法(click())或 DebugElement.triggerEventHandler 触发自定义事件。

angular
it('renders the title', () => {
  const fixture = TestBed.createComponent(MyComponent);
  fixture.componentInstance.title = 'Hello';
  fixture.detectChanges();
  const el: HTMLElement = fixture.nativeElement;
  expect(el.querySelector('h1')?.textContent).toContain('Hello');
});

it('calls save on button click', () => {
  const fixture = TestBed.createComponent(MyComponent);
  const spy = spyOn(fixture.componentInstance, 'save');
  const button = fixture.nativeElement.querySelector('button');
  button.click();
  expect(spy).toHaveBeenCalled();
});

// By.css for debug element queries (faster than nativeElement)
const debugEl = fixture.debugElement.query(By.css('.submit'));

测试信号

信号是函数——在测试中调用它们(带括号)读取值。set() 直接更新可写信号,避免间接操作的需要。detectChanges() 同步信号驱动的模板。对于 computed/effect,detectChanges 或 TestBed.flushEffects() 确保依赖 effect 运行。信号使测试更同步和可预测。

angular
describe('CounterComponent', () => {
  it('increments count', () => {
    const fixture = TestBed.createComponent(CounterComponent);
    const comp = fixture.componentInstance;

    expect(comp.count()).toBe(0);
    comp.increment();
    expect(comp.count()).toBe(1);

    // force signal effects to run in tests
    fixture.detectChanges();
  });

  it('renders count in template', () => {
    const fixture = TestBed.createComponent(CounterComponent);
    fixture.componentInstance.count.set(5);
    fixture.detectChanges();   // syncs signal to template
    expect(fixture.nativeElement.textContent).toContain('5');
  });
});

Cypress / Playwright E2E 测试

E2E 测试通过真实浏览器演练整个应用。使用 data-cy 属性作为选择器——稳定且不耦合 CSS 类。Cypress 在浏览器中运行;Playwright 跨浏览器运行。使用 ng e2e 或手动配置 Cypress/Playwright。E2E 测试捕获单元测试遗漏的集成问题但较慢——保持套件聚焦。

angular
// cypress test (cypress/e2e/login.cy.ts)
describe('Login flow', () => {
  beforeEach(() => cy.visit('/login'));

  it('logs in with valid credentials', () => {
    cy.get('[data-cy=email]').type('[email protected]');
    cy.get('[data-cy=password]').type('secret123');
    cy.get('[data-cy=submit]').click();

    cy.url().should('include', '/dashboard');
    cy.contains('Welcome, User');
  });
});

// run with: npx cypress open  (interactive)
//           npx cypress run   (headless CI)
17

PWA

添加 PWA 支持

ng add @angular/pwa 脚手架生成一切:Service Worker 注册、Web Manifest 和图标。Service Worker 仅在生产构建中运行(ng build --configuration production)——在开发中禁用以避免缓存意外。registrationStrategy 控制 SW 何时注册;registerWhenStable 等待应用稳定。

angular
# add @angular/service-worker
ng add @angular/pwa --project my-app

# this generates:
# - ngsw-config.json (service worker config)
# - manifest.webmanifest
# - icons in src/assets/icons/
# - registers service worker in app.config.ts

# app.config.ts (auto-updated)
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideServiceWorker('ngsw-worker.js', {
      enabled: environment.production,
      registrationStrategy: 'registerWhenStable:30000',
    }),
  ],
};

Service Worker 配置(ngsw-config.json)

assetGroups 缓存应用外壳(prefetch)和静态资源(lazy)。dataGroups 用 freshness/performance 策略和 TTL 缓存 API 响应。prefetch 急切下载;lazy 按需下载。freshness 先尝试网络,回退到缓存(适合新鲜数据);performance 先用缓存(适合较静态的数据)。

angular
{
  "$schema": "./node_modules/@angular/service-worker/config/schema.json",
  "index": "/index.html",
  "assetGroups": [
    {
      "name": "app",
      "installMode": "prefetch",
      "updateMode": "prefetch",
      "resources": {
        "files": ["/favicon.ico", "/index.html", "/manifest.webmanifest", "/*.css", "/*.js"]
      }
    },
    {
      "name": "assets",
      "installMode": "lazy",
      "updateMode": "prefetch",
      "resources": { "files": ["/assets/**", "/*.(svg|png|jpg|jpeg|webp|woff2)"] }
    }
  ],
  "dataGroups": [
    {
      "name": "api",
      "urls": ["https://api.example.com/**"],
      "cacheConfig": {
        "maxSize": 100, "maxAge": "1d", "timeout": "10s", "strategy": "freshness"
      }
    }
  ]
}

处理更新

SwUpdate 在新版本可用时通知。VERSION_READY 表示新包已下载并准备激活。activateUpdate() 切换到新版本;重载以加载它。对于关键更新,立即强制激活。对于非关键,提示用户。订阅 versionUpdates 处理 VERSION_DETECTED、VERSION_READY 和 NO_NEW_VERSION_DETECTED。

angular
import { SwUpdate } from '@angular/service-worker';
import { inject } from '@angular/core';

export class UpdatePromptComponent {
  private updates = inject(SwUpdate);
  updateAvailable = false;

  constructor() {
    this.updates.versionUpdates.subscribe(evt => {
      if (evt.type === 'VERSION_READY') {
        this.updateAvailable = true;
      }
    });
  }

  applyUpdate() {
    this.updates.activateUpdate().then(() => document.location.reload());
  }
}

离线支持与缓存

SwPush 通过 Service Worker 启用 Web 推送通知。requestSubscription 请求用户权限并返回要发送到服务器的订阅对象。即使标签页关闭,SW 也会接收推送事件。VAPID 密钥验证你的服务器。配合 Notification API 显示消息;处理点击以在应用内导航。

angular
import { SwPush } from '@angular/service-worker';

export class NotificationComponent {
  private swPush = inject(SwPush);

  subscribeToPush() {
    this.swPush.requestSubscription({
      serverPublicKey: this.VAPID_PUBLIC,
    }).then(sub => this.sendToServer(sub))
      .catch(err => console.error('Push denied', err));
  }

  // check if SW is enabled
  ngOnInit() {
    if (!this.swPush.isEnabled) {
      console.warn('Push notifications not supported');
    }
  }
}

应用清单

Web Manifest 使应用可安装(添加到主屏幕)。name/short_name 显示在主屏幕;theme_color 着色浏览器 UI;display: standalone 移除浏览器外壳获得应用般体验。提供多种图标尺寸(192、512)以及 Android 的可遮罩图标。用 Lighthouse 的 PWA 审计验证可安装性。

angular
<!-- index.html (added by ng add @angular/pwa) -->
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1976d2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="assets/icons/icon-152x152.png">

// manifest.webmanifest
{
  "name": "My App",
  "short_name": "MyApp",
  "theme_color": "#1976d2",
  "background_color": "#fafafa",
  "display": "standalone",
  "scope": "/",
  "start_url": "/",
  "icons": [
    { "src": "assets/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "assets/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" }
  ]
}

可安装性与 Lighthouse

beforeinstallprompt 在 Chrome 中当应用可安装时触发,但让你延迟原生提示。保存事件并从用户手势(按钮点击)触发 prompt()。iOS Safari 不触发此事件——用户通过分享菜单安装。运行 Lighthouse 的 PWA 审计验证可安装性。PWA 仅在 Service Worker 缓存应用外壳时离线工作。

angular
// detect install prompt (Chrome)
let deferredPrompt: any;
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferredPrompt = e;
  this.canInstall = true;   // show "Install App" button
});

// user-triggered install (must be from user gesture)
async installApp() {
  if (!deferredPrompt) return;
  deferredPrompt.prompt();
  const { outcome } = await deferredPrompt.userChoice;
  console.log('Install:', outcome);
  deferredPrompt = null;
}

// Lighthouse PWA checklist:
// ✅ served over HTTPS
// ✅ registers a service worker
// ✅ has a web manifest with icons
// ✅ start_url loads offline
18

独立组件

引导独立组件

bootstrapApplication 启动独立应用——无需 NgModule。根 AppComponent 必须是独立的。app.config.ts 集中 providers(路由器、HTTP、动画、zone 配置)。这是 v17 起的默认方式。每个 provideX() 函数接入一个子系统。比 AppModule + AppModule providers 干净得多。

angular
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));

// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(),
  ],
};

独立组件导入

standalone: true 标记组件为自包含。imports 数组列出模板所需的一切:模块、组件、指令、管道。使用 v17+ 控制流(@if/@for)时,不需要 CommonModule。只导入用到的内容以获得更好的摇树优化。独立组件可通过 exports 在 NgModule 中使用。

angular
@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [
    CommonModule,          // *ngIf, *ngFor, pipes (or use built-in @if/@for)
    FormsModule,           // ngModel
    UserCardComponent,     // another standalone component
    HighlightDirective,    // standalone directive
    TruncatePipe,          // standalone pipe
  ],
  template: `
    <input [(ngModel)]="filter" />
    @for (u of users; track u.id) {
      <app-user-card [user]="u" appHighlight>{{ u.name | truncate:20 }}</app-user-card>
    }
  `,
})
export class UserListComponent { ... }

独立路由与懒加载

loadComponent 懒加载单个独立组件——最小的可能块。loadChildren 懒加载路由文件(Routes 的默认导出)。两者都使用带静态路径的动态 import() 让打包器拆分。这完全取代功能模块。每个懒加载路由获得自己的 JS 文件,首次导航时加载。

angular
export const routes: Routes = [
  { path: '', loadComponent: () => import('./home/home.component')
      .then(m => m.HomeComponent) },
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin.component')
      .then(m => m.AdminComponent),
    canMatch: [authGuard],
  },
  {
    path: 'reports',
    loadChildren: () => import('./reports/reports.routes')
      .then(m => m.REPORTS_ROUTES),
  },
];

指令与管道作为独立

指令和管道也可以是独立的(v15+)。添加 standalone: true 并导入到需要它们的任何组件。这消除了在共享模块中声明的需要。为向后兼容,独立指令/管道仍可添加到 NgModule exports 中以在基于 NgModule 的组件中使用。

angular
@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective { ... }

@Pipe({
  name: 'truncate',
  standalone: true,
})
export class TruncatePipe implements PipeTransform { ... }

// use directly in a standalone component's imports
@Component({
  imports: [HighlightDirective, TruncatePipe],
  template: `<p appHighlight>{{ text | truncate:10 }}</p>`,
})

从 NgModule 迁移

原理图自动化迁移:使所有声明独立,将 NgModule 导入移动到每个组件的 imports 数组,并移除过时的模块。增量运行——可部分迁移。迁移后,可删除 AppRoutingModule、AppModule 并使用 bootstrapApplication。结果是更精简、更可摇树优化的应用。

angular
# automatic migration (v15.2+)
ng generate @angular/core:standalone

# options:
# 1. Convert all components/directives/pipes to standalone
# 2. Remove unnecessary NgModule classes
# 3. Bootstrap the app using bootstrapApplication

// before: declared in a module
@NgModule({
  declarations: [OldComponent],
  imports: [CommonModule],
  exports: [OldComponent],
})
export class OldModule {}

// after: standalone component
@Component({
  standalone: true,
  imports: [CommonModule],
  // ...
})
export class OldComponent {}

在独立 Provider 中注入

独立应用在 app.config.ts 中配置 providers 而非 AppModule。APP_INITIALIZER 在引导前运行设置。组件级 provider 作用域到该组件子树。对于服务,providedIn: 'root' 仍是首选——可摇树优化。providers 数组用于非服务值(token、配置、Mock)。

angular
// app.config.ts — global providers
export const appConfig: ApplicationConfig = {
  providers: [
    { provide: API_URL, useValue: 'https://api.example.com' },
    { provide: ErrorHandler, useClass: GlobalErrorHandler },
    { provide: APP_INITIALIZER, multi: true, useFactory: () => initApp },
  ],
};

// component-level providers (scoped to this component and children)
@Component({
  providers: [
    { provide: UserService, useClass: MockUserService },
  ],
})
export class AdminComponent {}

// tree-shakable service
@Injectable({ providedIn: 'root' })  // preferred over providers array
export class AuthService {}
19

信号

创建信号

signal() 创建响应式值。通过调用读取:count()。用 set()(替换)或 update()(从前值计算)更新。信号是同步的,避免了 Observable 对简单状态的开销。在模板中可用 () 调用——v17.2+ 在某些上下文中也自动解包。信号与 OnPush 变更检测集成。

angular
import { signal, computed, effect } from '@angular/core';

export class CounterComponent {
  // writable signal with initial value
  count = signal(0);
  user = signal<User | null>(null);

  // reading: call the signal as a function
  current = this.count();    // 0

  // writing
  increment() { this.count.update(v => v + 1); }
  setFive() { this.count.set(5); }

  // in templates, signals are called automatically in v17.2+
  // {{ count }} or {{ count() }} both work in templates
}

计算信号

computed() 创建派生的、只读的信号,自动追踪其依赖并缓存值。仅当依赖变化且再次被读取时才重新计算——惰性求值。计算信号可依赖其他计算信号,形成图。与方法不同,它们缓存结果。这取代了许多 RxJS combineLatest 的用途。

angular
export class CartComponent {
  items = signal<Item[]>([]);
  taxRate = signal(0.08);

  // derived state — recomputes only when items or taxRate change
  subtotal = computed(() =>
    this.items().reduce((sum, i) => sum + i.price * i.qty, 0)
  );
  tax = computed(() => Math.round(this.subtotal() * this.taxRate() * 100) / 100);
  total = computed(() => this.subtotal() + this.tax());

  // computed signals are readonly and cached
  // they recompute lazily only when read AND a dependency changed
}

Effect(副作用)

effect() 在其信号依赖变化时运行副作用。用于日志、同步到 localStorage、获取数据或 DOM 操作。Effect 在变更检测后运行。避免在同一 effect 中写入读取的信号——那会创建无限循环。onCleanup 为下次运行注册清理逻辑。Effect 必须在注入上下文中创建。

angular
export class SearchComponent {
  query = signal('');
  results = signal<Result[]>([]);

  constructor() {
    // effect runs when its signal dependencies change
    effect(() => {
      const q = this.query();
      // ⚠️ avoid setting signals read in this effect (causes loops)
      this.search(q).then(r => this.results.set(r));
    });
  }

  // effect with cleanup
  private log = effect((onCleanup) => {
    const id = setInterval(() => console.log(this.count()), 1000);
    onCleanup(() => clearInterval(id));
  });
}

模板中的信号(OnPush)

信号与 OnPush 无缝集成——Angular 知道哪些视图依赖信号,并在它变化时仅标记那些进行检查。无需手动 markForCheck()。在模板中用 () 调用信号读取。OnPush + 信号是最有效的变更检测设置,通常可完全放弃 zone.js(无 zone 模式)。

angular
@Component({
  selector: 'app-todo',
  // signals work great with OnPush — automatic efficiency
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <input (input)="text.set($any($event.target).value)" />
    <p>You typed: {{ text() }}</p>
    <p>Length: {{ length() }}</p>
    <button (click)="clear()">Clear</button>
  `,
})
export class TodoComponent {
  text = signal('');
  length = computed(() => this.text().length);
  clear() { this.text.set(''); }
}

信号输入与查询

input()/input.required() 创建只读信号输入。output() 创建类型化输出。model() 创建可双向绑定的信号输入(自动生成 xChange 输出)。viewChild()/contentChild() 返回在视图 init 后解析的信号——用 () 调用读取。这些现代 API 用响应式信号取代 @Input/@Output/@ViewChild 装饰器。

angular
import { input, output, viewChild, contentChild, model } from '@angular/core';

export class EditComponent {
  // signal input (readonly, v17.1+)
  id = input<number>(0);
  requiredName = input.required<string>();

  // signal output (v17.3+)
  saved = output<User>();

  // two-way signal (v17.2+)
  value = model<number>(0);   // creates input + output named valueChange

  // signal queries
  inputEl = viewChild<ElementRef>('input');      // in own template
  header = contentChild<TemplateRef>('header');  // projected content

  focus() { this.inputEl()?.nativeElement.focus(); }
}

信号与 RxJS 互操作

toSignal 将 Observable 转换为信号(订阅一次,销毁时取消订阅)。toObservable 反之。用互操作混合信号(用于同步状态)与 RxJS(用于复杂异步流如去抖动搜索)。takeUntilDestroyed 通过 DestroyRef 自动取消订阅。initialValue 防止首次发出前读取 undefined。需要注入上下文。

angular
import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class SearchComponent {
  private http = inject(HttpClient);

  // Observable → Signal (one-way)
  results = toSignal(this.http.get<Result[]>('/api/results'), {
    initialValue: [] as Result[],
  });

  // Signal → Observable (for RxJS operators)
  private query$ = toObservable(this.query);

  // combine the best of both worlds
  filtered = toSignal(
    this.query$.pipe(
      debounceTime(300),
      switchMap(q => this.http.get(`/api/search?q=${q}`)),
      takeUntilDestroyed(),
    ),
    { initialValue: [] as Result[] },
  );
}
20

NgModule 与架构

NgModule 结构

NgModule 将应用组织为内聚的块。declarations 列出模块拥有的内容。imports 引入其他模块的导出。exports 向导入者暴露声明。只有根模块设置 bootstrap。现代 Angular 倾向独立组件(无 NgModule),但 NgModule 应用仍存在。两者都要理解——旧代码库和某些库使用模块。

angular
@NgModule({
  declarations: [                  // components, directives, pipes owned by this module
    AppComponent,
    UserListComponent,
    HighlightDirective,
    TruncatePipe,
  ],
  imports: [                       // other modules whose exports this module needs
    BrowserModule,
    FormsModule,
    HttpClientModule,
    AppRoutingModule,
  ],
  exports: [                       // what's visible to modules that import this module
    UserListComponent,
    TruncatePipe,
  ],
  providers: [],                   // services (use providedIn: 'root' instead)
  bootstrap: [AppComponent],       // only in the root module
})
export class AppModule {}

功能模块与共享模块

SharedModule 打包常见 UI 组件/指令/管道以跨功能复用——重新导出 CommonModule 和 FormsModule 以便导入者不必再导入。功能模块封装功能区域;forChild() 让同一模块按导入不同地配置自身(如路由)。在独立应用中,优先使用单个独立组件而非共享模块。

angular
// shared.module.ts — reusable UI bits
@NgModule({
  imports: [CommonModule, FormsModule],
  declarations: [ButtonComponent, CardComponent, HighlightDirective],
  exports: [ButtonComponent, CardComponent, HighlightDirective, FormsModule],
})
export class SharedModule {}

// feature module (lazy-loaded)
@NgModule({
  declarations: [AdminComponent, AdminUsersComponent],
  imports: [CommonModule, RouterModule, SharedModule],
  exports: [AdminComponent],
})
export class AdminModule {
  static forChild(): ModuleWithProviders<AdminModule> {
    return { ngModule: AdminModule, providers: [AdminService] };
  }
}

Core 模块与单例服务

CoreModule 持有全应用单例(服务、拦截器、错误处理器),在 AppModule 中仅导入一次。构造函数守卫防止意外重复导入(否则会创建重复服务实例)。在独立应用中,这些 providers 移到 app.config.ts。CoreModule 模式是遗留的,但你会在 v14 前代码库中遇到它。

angular
// core.module.ts — app-wide singletons, imported once in AppModule
@NgModule({
  providers: [
    AuthService,
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    { provide: ErrorHandler, useClass: GlobalErrorHandler },
  ],
})
export class CoreModule {
  // guard against importing twice
  constructor(@Optional() @SkipSelf() parent?: CoreModule) {
    if (parent) throw new Error('CoreModule already loaded');
  }
}

forRoot vs forChild 模式

forRoot 配置根实例(在 AppModule 中调用一次);forChild 注册子路由(在功能模块中调用)。此模式被 RouterModule 和需要配置的库模块使用。现代 Angular 用 app.config.ts 中的 provideX() 函数取代它。该模式仍出现在第三方库中——理解它以便集成。

angular
// classic RouterModule pattern
@NgModule({ imports: [CommonModule], exports: [RouterModule] })
export class RouterModule {
  static forRoot(routes: Routes): ModuleWithProviders<RouterModule> {
    return {
      ngModule: RouterModule,
      providers: [provideRouter(routes)],
    };
  }
  static forChild(routes: Routes): ModuleWithProviders<RouterModule> {
    return { ngModule: RouterModule, providers: [provideRoutes(routes)] };
  }
}

// AppModule imports: RouterModule.forRoot(ROUTES)
// FeatureModule imports: RouterModule.forChild(FEATURE_ROUTES)

// modern equivalent: provideRouter(routes) in app.config.ts

入口组件与动态组件

entryComponents(已弃用)列出动态创建的组件(对话框、模态框)。现代 Angular 使用 ViewContainerRef.createComponent() 或 NgComponentOutlet——无需注册。这与独立组件无缝工作。createComponent 返回带 .instance 和 .destroy() 的 ComponentRef。调用 destroy() 清理以避免泄漏。

angular
// older API: entryComponents for dynamically created components
@NgModule({
  entryComponents: [DialogComponent, ModalComponent],
})
export class AppModule {}

// modern: ViewContainerRef.createComponent (no entryComponents needed)
export class DialogService {
  constructor(private vcr: ViewContainerRef) {}

  open() {
    const ref = this.vcr.createComponent(DialogComponent);
    ref.instance.title = 'Hello';
    ref.instance.close.subscribe(() => ref.destroy());
    return ref;
  }
}

// even simpler: NgComponentOutlet in template
<ng-container *ngComponentOutlet="dialogComponent; inputs: { title: 'Hi' }" />

懒加载模块

懒加载模块创建在导航时加载的单独 JS 块。动态 import() 必须是静态字符串以便打包器拆分。懒加载模块有自己的根注入器——懒加载模块中 providedIn 的服务不是全应用单例。现代独立应用改用带路由文件的 loadComponent/loadChildren,更简单且更细粒度。

angular
// classic lazy module (pre-standalone)
const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
  },
];

// admin-routing.module.ts
const routes: Routes = [
  { path: '', component: AdminComponent },
  { path: 'users', component: AdminUsersComponent },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class AdminRoutingModule {}

// admin.module.ts
@NgModule({
  declarations: [AdminComponent, AdminUsersComponent],
  imports: [CommonModule, AdminRoutingModule],
})
export class AdminModule {}

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。