DevToolBox免费
博客

Angular指南:组件、服务、RxJS、NgRx和Angular 17+信号

14 分钟阅读作者 DevToolBox
TL;DR

Angular 是由 Google 维护的完整、强约定 TypeScript 优先前端框架。与 React(库)或 Vue(渐进式框架)不同,Angular 开箱即用提供一切:组件系统、双向数据绑定、依赖注入、HTTP 客户端、路由器、响应式表单和强大的 CLI。学习曲线较陡,但在结构和一致性至关重要的大型企业应用中表现出色。Angular 17+ 引入了独立组件、信号和新的控制流语法,大幅简化了开发。

Angular 是由 Google 构建和维护的经过实战检验的 TypeScript 优先 Web 应用框架。最初于 2010 年作为 AngularJS 发布,并于 2016 年作为 Angular 2 完全重写,现在已发展到 17+ 版本,每年发布两个主要版本。Angular 是 Google、Microsoft、IBM 和众多财富 500 强公司大型企业应用的首选框架。其强约定性——强制使用 TypeScript、提供完整工具链、要求架构模式——使其非常适合需要一致性和可维护性的大型团队。本指南涵盖从组件和指令到 NgRx 状态管理以及最新 Angular 17+ 特性的所有内容。

Key Takeaways
  • Angular 是完整框架,而非库——它为路由、表单、HTTP、测试和状态管理提供有主见的解决方案,无需第三方决策。
  • 每个 Angular 应用都由组件构建——组件是带有 @Component 装饰器、模板和可选样式的 TypeScript 类。
  • 依赖注入是 Angular 的核心——服务是在模块或根级别提供的单例类,通过构造函数参数注入。
  • 对于复杂场景,优先使用响应式表单(FormGroup/FormControl)而非模板驱动表单——它们更易于测试,并提供对表单值的同步访问。
  • RxJS Observable 驱动 Angular——async 管道自动处理订阅,防止模板中的内存泄漏。
  • 独立组件(Angular 14+)消除了大多数用例中对 NgModule 的需求,使 Angular 应用更小、更易于理解。

Angular 是什么?完整框架理念

Angular 与 React 和 Vue 在设计理念上根本不同。React 是一个 UI 渲染库——你需要用第三方工具来组合路由(React Router)、状态(Redux/Zustand)和 HTTP(Axios/fetch)。Vue 是一个渐进式框架,从简单开始逐步扩展。Angular 是一个完整的、内置电池的框架:它附带路由器、HTTP 客户端、响应式表单、动画系统、国际化、测试工具和 CLI。这意味着初始复杂性更高,但在大型代码库中决策疲劳更少,一致性更强。

Angular CLI:核心工具链

Angular CLI(@angular/cli)是创建、构建、测试和部署 Angular 应用的主要方式。它提供代码生成(ng generate)、带热模块替换的开发服务器、带树摇和差异加载的优化生产构建、单元测试运行器(Karma/Jest)和端到端测试运行器(Cypress/Playwright)。

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

# Create a new Angular application
ng new my-app --routing --style=scss

# Start development server
ng serve --open

# Generate a component
ng generate component features/user-profile
ng g c features/user-profile  # shorthand

# Generate a service
ng generate service services/auth

# Build for production
ng build --configuration production

# Run unit tests
ng test

# Run end-to-end tests
ng e2e

TypeScript 优先架构

Angular 从零开始以 TypeScript 为核心设计。类型安全不是可选的——Angular 依赖 TypeScript 装饰器(@Component、@Injectable、@Input、@Output)进行元数据系统。Angular 编译器(Ivy)执行预先编译(AOT),在浏览器运行之前将 TypeScript 和模板转换为优化的 JavaScript。这使得在构建时能更好地检测错误,减小包大小,并提高运行时性能。

Angular 组件:构建块

组件是 Angular 应用的基本构建块。Angular 中的每个可见元素都是一个组件。组件由三个部分组成:定义逻辑和数据的 TypeScript 类、定义视图的 HTML 模板以及定义外观的 CSS/SCSS 样式。组件用 @Component 装饰,它向 Angular 提供创建和渲染组件所需的元数据。

// user-card.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
      <span>{{ user.role | uppercase }}</span>
      <button (click)="onSelect()">Select User</button>
    </div>
  `,
  styles: [`
    .card { border: 1px solid #e2e8f0; padding: 1rem; border-radius: 8px; }
  `]
})
export class UserCardComponent implements OnInit {
  @Input({ required: true }) user!: User;
  @Output() userSelected = new EventEmitter<User>();

  ngOnInit(): void {
    console.log('Component initialized for user:', this.user.name);
  }

  onSelect(): void {
    this.userSelected.emit(this.user);
  }
}

数据绑定:四种类型

Angular 支持将组件类连接到模板的四种数据绑定。插值({{ }})将类属性渲染为文本。属性绑定([property]="expression")设置 DOM 或组件属性。事件绑定((event)="handler()")响应用户操作。双向绑定([(ngModel)]="property")结合属性和事件绑定,保持视图和模型同步——这需要导入 FormsModule。

<!-- app.component.html -->

<!-- 1. Interpolation: renders text -->
<h1>{{ title }}</h1>
<p>Hello, {{ user.name }}!</p>

<!-- 2. Property binding: sets DOM property -->
<img [src]="user.avatarUrl" [alt]="user.name">
<button [disabled]="isLoading">Submit</button>
<app-user-card [user]="currentUser"></app-user-card>

<!-- 3. Event binding: responds to DOM events -->
<button (click)="handleClick()">Click Me</button>
<input (input)="onInputChange($event)">
<form (ngSubmit)="onSubmit()">...</form>

<!-- 4. Two-way binding: syncs view and model -->
<input [(ngModel)]="searchTerm" placeholder="Search...">
<p>You typed: {{ searchTerm }}</p>

<!-- Template reference variable (#) -->
<input #emailInput type="email">
<button (click)="handleEmail(emailInput.value)">Submit</button>

组件通信:@Input 和 @Output

父组件和子组件通过 @Input 和 @Output 装饰器进行通信。@Input 允许父组件通过属性绑定向子组件传递数据。@Output 与 EventEmitter 配对允许子组件发出父组件可以监听的事件。这创建了清晰的单向数据流模式,易于测试和理解。

// parent.component.ts
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [UserCardComponent, NgFor],
  template: `
    <h2>Users ({{ selectedCount }} selected)</h2>
    <app-user-card
      *ngFor="let user of users"
      [user]="user"
      (userSelected)="onUserSelected($event)">
    </app-user-card>
  `
})
export class ParentComponent {
  users: User[] = [...];
  selectedCount = 0;

  onUserSelected(user: User): void {
    this.selectedCount++;
    console.log('Selected:', user.name);
  }
}

组件生命周期钩子

Angular 组件具有由 Angular 管理的定义生命周期。关键钩子有:ngOnChanges(输入属性变化时调用)、ngOnInit(第一次 ngOnChanges 后调用一次——用于初始化逻辑)、ngDoCheck(自定义变更检测)、ngAfterContentInit(内容投影后)、ngAfterViewInit(视图和子视图初始化后)和 ngOnDestroy(组件销毁前清理——在此处取消订阅以防止内存泄漏)。

import {
  Component, OnInit, OnDestroy, OnChanges,
  Input, SimpleChanges
} from '@angular/core';
import { Subscription } from 'rxjs';

@Component({ selector: 'app-lifecycle', template: '' })
export class LifecycleComponent implements OnInit, OnChanges, OnDestroy {
  @Input() userId!: number;
  private subscription = new Subscription();

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['userId'] && !changes['userId'].firstChange) {
      console.log('userId changed to:', changes['userId'].currentValue);
      this.loadUser(changes['userId'].currentValue);
    }
  }

  ngOnInit(): void {
    // Safe to access @Input values here
    this.loadUser(this.userId);
  }

  private loadUser(id: number): void { /* ... */ }

  ngOnDestroy(): void {
    // Cleanup: unsubscribe to prevent memory leaks
    this.subscription.unsubscribe();
  }
}

Angular 指令:扩展 HTML

指令是向 DOM 元素添加行为的类。Angular 有三种类型:组件(带模板的指令)、结构指令(通过添加/删除元素改变 DOM 布局)和属性指令(改变元素的外观或行为)。内置指令涵盖了最常见的用例,但你可以创建自定义指令实现可复用行为。

结构指令:ngIf、ngFor、ngSwitch

结构指令操纵 DOM 结构。星号(*)前缀是 Angular 展开为更长形式的语法糖。*ngIf 有条件地包含/排除元素,*ngFor 遍历集合并为每个项目生成模板,*ngSwitch 根据条件在一组视图之间切换。在 Angular 17+ 中,新的控制流语法(@if、@for、@switch)以更好的性能和类型安全替代了这些指令。

<!-- ngIf: conditional rendering -->
<div *ngIf="isLoggedIn; else loginBlock">
  <p>Welcome back, {{ user.name }}!</p>
</div>
<ng-template #loginBlock>
  <app-login-form></app-login-form>
</ng-template>

<!-- ngFor: list rendering with index and trackBy -->
<ul>
  <li *ngFor="let item of items; let i = index; trackBy: trackById">
    {{ i + 1 }}. {{ item.name }}
  </li>
</ul>

<!-- Angular 17+ new control flow syntax -->
@if (isLoggedIn) {
  <p>Welcome, {{ user.name }}!</p>
} @else {
  <app-login-form />
}

@for (item of items; track item.id; let i = $index) {
  <li>{{ i + 1 }}. {{ item.name }}</li>
} @empty {
  <li>No items found.</li>
}

@switch (user.role) {
  @case ('admin') { <app-admin-panel /> }
  @case ('editor') { <app-editor-panel /> }
  @default { <app-viewer-panel /> }
}

属性指令:ngClass、ngStyle、自定义

属性指令修改元素的外观或行为,而不改变 DOM 结构。ngClass 动态添加或删除 CSS 类,ngStyle 动态设置内联样式。自定义属性指令使用 @Directive 装饰器,可以通过 ElementRef 访问宿主元素,并使用 @HostListener 监听宿主事件。

<!-- ngClass: dynamic CSS classes -->
<div [ngClass]="{
  'active': isActive,
  'error': hasError,
  'loading': isLoading
}">
  Status indicator
</div>

<!-- ngStyle: dynamic inline styles -->
<div [ngStyle]="{
  'color': textColor,
  'font-size': fontSize + 'px',
  'background-color': isHighlighted ? '#fef3c7' : 'transparent'
}">
  Styled content
</div>

<!-- Built-in pipes -->
<p>{{ price | currency:'USD' }}</p>
<p>{{ date | date:'longDate' }}</p>
<p>{{ name | uppercase }}</p>
<p>{{ longText | slice:0:100 }}...</p>

创建自定义指令

自定义指令用 @Directive 装饰器创建。它们接收宿主 ElementRef 进行直接 DOM 操作,可以接受 @Input 属性来配置行为。@HostListener 装饰器绑定到宿主元素事件。@HostBinding 装饰器绑定到宿主元素属性或特性。自定义指令非常适合自动聚焦、悬停高亮、拖放和输入掩码等可复用行为。

// highlight.directive.ts
import {
  Directive, ElementRef, HostListener,
  Input, OnInit
} from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective implements OnInit {
  @Input() appHighlight = '#fef3c7';
  @Input() defaultColor = 'transparent';

  constructor(private el: ElementRef) {}

  ngOnInit(): void {
    this.el.nativeElement.style.transition = 'background-color 0.2s';
  }

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

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

// Usage in template:
// <p appHighlight="#d1fae5">Hover over me!</p>
// <p [appHighlight]="userColor" defaultColor="#f0f9ff">Custom color</p>

服务与依赖注入

服务是 Angular 中封装业务逻辑、数据访问和共享功能的单例类。它们遵循单一职责原则:组件处理 UI,服务处理其他一切。Angular 依赖注入(DI)系统通过构造函数注入自动向需要它们的类提供服务实例。

@Injectable 和 DI 系统

服务用 @Injectable 装饰。providedIn 属性确定注入范围。providedIn: "root" 创建全应用程序可用的单例(最常见的选择)。providedIn: "any" 为每个惰性加载模块创建单独的实例。在特定模块中提供会创建作用域单例。Angular 14+ 支持 inject() 函数作为构造函数注入的替代方案,允许在独立函数中进行依赖注入。

// auth.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, tap } from 'rxjs';

interface AuthUser {
  id: number;
  email: string;
  token: string;
}

@Injectable({
  providedIn: 'root', // singleton for entire app
})
export class AuthService {
  private http = inject(HttpClient);
  private currentUserSubject = new BehaviorSubject<AuthUser | null>(null);

  // Expose as read-only Observable
  currentUser$ = this.currentUserSubject.asObservable();

  get isLoggedIn(): boolean {
    return this.currentUserSubject.value !== null;
  }

  login(email: string, password: string): Observable<AuthUser> {
    return this.http.post<AuthUser>('/api/auth/login', { email, password }).pipe(
      tap(user => {
        localStorage.setItem('token', user.token);
        this.currentUserSubject.next(user);
      })
    );
  }

  logout(): void {
    localStorage.removeItem('token');
    this.currentUserSubject.next(null);
  }
}

HttpClient:发起 API 调用

Angular 在 @angular/common/http 中提供 HttpClient 用于发起 HTTP 请求。它返回 Observable(而非 Promise),与 Angular 变更检测集成,支持拦截器处理横切关注点(认证头、错误处理、日志记录),并通过泛型提供类型安全的响应。在 AppModule 中导入 HttpClientModule 或在独立引导中使用 provideHttpClient()。

// products.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

interface ProductsResponse {
  data: Product[];
  total: number;
  page: number;
}

@Injectable({ providedIn: 'root' })
export class ProductsService {
  private http = inject(HttpClient);
  private baseUrl = '/api/products';

  getProducts(page = 1, category?: string): Observable<ProductsResponse> {
    let params = new HttpParams().set('page', page);
    if (category) params = params.set('category', category);

    return this.http.get<ProductsResponse>(this.baseUrl, { params });
  }

  getProduct(id: number): Observable<Product> {
    return this.http.get<Product>(this.baseUrl + '/' + id);
  }

  createProduct(product: Omit<Product, 'id'>): Observable<Product> {
    return this.http.post<Product>(this.baseUrl, product);
  }

  updateProduct(id: number, updates: Partial<Product>): Observable<Product> {
    return this.http.patch<Product>(this.baseUrl + '/' + id, updates);
  }

  deleteProduct(id: number): Observable<void> {
    return this.http.delete<void>(this.baseUrl + '/' + id);
  }
}

// HTTP Interceptor (Angular 15+ functional style)
// app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';

export const authInterceptor = (req, next) => {
  const token = localStorage.getItem('token');
  if (token) {
    req = req.clone({
      setHeaders: { Authorization: 'Bearer ' + token }
    });
  }
  return next(req);
};

// Bootstrap:
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
});

响应式表单与模板驱动表单

Angular 提供两种构建表单的方法。模板驱动表单在模板中使用 NgModel 指令,对基本用例更简单但难以测试和验证。响应式表单(也称模型驱动表单)使用 FormGroup 和 FormControl 在组件类中定义表单结构——它们是任何具有复杂验证、动态字段或需要单元测试的表单的推荐方法。

FormGroup 和 FormControl

响应式表单由 FormGroup 和 FormControl 实例树构建。FormControl 跟踪单个输入的值和验证状态。FormGroup 整体跟踪一组控件的值和有效性。FormArray 管理控件数组。FormBuilder 服务提供方便的简写语法以编程方式创建这些。每个控件跟踪其 dirty、pristine、touched、untouched、valid 和 invalid 状态。

// registration.component.ts
import { Component, inject } from '@angular/core';
import {
  FormBuilder, FormGroup, FormControl,
  Validators, ReactiveFormsModule
} from '@angular/forms';

@Component({
  selector: 'app-registration',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div>
        <label>Email</label>
        <input formControlName="email" type="email">
        @if (email.invalid && email.touched) {
          @if (email.errors?.['required']) { <span>Email is required</span> }
          @if (email.errors?.['email']) { <span>Invalid email format</span> }
        }
      </div>
      <div formGroupName="password">
        <input formControlName="value" type="password">
        <input formControlName="confirm" type="password">
        @if (form.get('password')?.errors?.['mismatch']) {
          <span>Passwords do not match</span>
        }
      </div>
      <button type="submit" [disabled]="form.invalid">Register</button>
    </form>
  `
})
export class RegistrationComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    name: ['', [Validators.required, Validators.minLength(2)]],
    password: this.fb.group({
      value: ['', [Validators.required, Validators.minLength(8)]],
      confirm: ['', Validators.required],
    }, { validators: passwordMatchValidator }),
  });

  get email() { return this.form.get('email') as FormControl; }

  onSubmit(): void {
    if (this.form.valid) {
      console.log(this.form.value);
    }
  }
}

// Custom cross-field validator
function passwordMatchValidator(group: AbstractControl) {
  const value = group.get('value')?.value;
  const confirm = group.get('confirm')?.value;
  return value === confirm ? null : { mismatch: true };
}

Angular 中的 RxJS 和 Observable

RxJS(JavaScript 响应式扩展)深度集成到 Angular 中。HTTP 请求、路由器事件、表单值更改和 async 管道都使用 Observable。理解 RxJS 对于有效的 Angular 开发至关重要。Observable 表示随时间流动的值流,它是惰性的(在订阅之前什么都不会发生)、可取消的(与 Promise 不同)和可用运算符组合的。

async 管道:自动订阅管理

async 管道是 Angular 最强大的功能之一。它在模板中订阅 Observable 或 Promise,在组件销毁时自动取消订阅(防止内存泄漏),并在新值到达时触发变更检测。将数据作为 Observable 从服务中暴露并在模板中使用 async 管道的模式是推荐的 Angular 模式——它保持组件精简并自动处理订阅生命周期。

// products-list.component.ts
import { Component, inject } from '@angular/core';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { ProductsService } from '../services/products.service';

@Component({
  selector: 'app-products-list',
  standalone: true,
  imports: [AsyncPipe, NgFor, NgIf],
  template: `
    @if (products$ | async; as response) {
      <p>Total: {{ response.total }}</p>
      @for (product of response.data; track product.id) {
        <div>{{ product.name }} - {{ product.price | currency }}</div>
      }
    } @else {
      <app-loading-spinner />
    }
  `
})
export class ProductsListComponent {
  private productService = inject(ProductsService);

  // No manual subscribe/unsubscribe needed!
  products$ = this.productService.getProducts();
}

核心 RxJS 运算符

RxJS 提供 100+ 个运算符,但少数几个涵盖了 90% 的用例。map 转换每个发出的值。filter 丢弃不匹配谓词的值。switchMap 取消之前的内部 Observable 并订阅新的(非常适合搜索自动完成:当新输入到达时取消之前的 HTTP 请求)。mergeMap 并发订阅内部 Observable。combineLatest 在任何源发出时发出,结合最新值。debounceTime 延迟发出指定时间,忽略中间值(非常适合搜索输入以避免每次击键都触发)。

// search.component.ts — debounced search with switchMap
import { Component, inject, OnInit } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import {
  debounceTime, distinctUntilChanged,
  switchMap, filter, map, catchError
} from 'rxjs/operators';
import { of } from 'rxjs';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [ReactiveFormsModule, AsyncPipe, NgFor],
  template: `
    <input [formControl]="searchControl" placeholder="Search products...">
    @for (result of results$ | async; track result.id) {
      <div>{{ result.name }}</div>
    }
  `
})
export class SearchComponent {
  private http = inject(HttpClient);
  searchControl = new FormControl('');

  results$ = this.searchControl.valueChanges.pipe(
    debounceTime(300),          // Wait 300ms after last keystroke
    distinctUntilChanged(),     // Skip if same value
    filter(term => term!.length >= 2), // Min 2 chars
    switchMap(term =>           // Cancel previous request
      this.http.get<Product[]>('/api/search?q=' + term).pipe(
        catchError(() => of([]))  // Handle errors gracefully
      )
    ),
    map(results => results.slice(0, 10)) // Top 10 results
  );
}

用于状态的 Subject 和 BehaviorSubject

Subject 既是 Observable 又是 Observer——它可以发出值并被订阅。BehaviorSubject 保存当前值并立即向新订阅者发出它(非常适合当前用户状态、主题设置)。ReplaySubject 缓冲指定数量的发出内容并向新订阅者重放。在服务中,将 Subject 作为 Observable 暴露(使用 .asObservable())以防止外部代码直接调用 next()。

// theme.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject, ReplaySubject } from 'rxjs';

type Theme = 'light' | 'dark';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  // BehaviorSubject: has initial value, emits immediately to new subscribers
  private themeSubject = new BehaviorSubject<Theme>('light');
  theme$ = this.themeSubject.asObservable(); // read-only

  // Subject: no initial value, emits to current subscribers only
  private notificationSubject = new Subject<string>();
  notification$ = this.notificationSubject.asObservable();

  // ReplaySubject: buffers last N emissions for new subscribers
  private activityLog = new ReplaySubject<string>(5); // last 5 actions
  activityLog$ = this.activityLog.asObservable();

  get currentTheme(): Theme {
    return this.themeSubject.value;
  }

  toggleTheme(): void {
    const next: Theme = this.themeSubject.value === 'light' ? 'dark' : 'light';
    this.themeSubject.next(next);
    this.activityLog.next('Theme changed to ' + next);
  }

  notify(message: string): void {
    this.notificationSubject.next(message);
  }
}

Angular 路由器:导航和惰性加载

Angular 路由器支持单页应用中视图之间的导航。路由将 URL 路径映射到组件。路由器处理浏览器历史、URL 参数、查询参数、路由守卫和功能模块的惰性加载。在应用程序级别使用 RouterModule.forRoot() 配置路由,在功能模块中使用 RouterModule.forChild()。

惰性加载:规模化性能

惰性加载将应用程序分割成单独的 JavaScript 包,在用户导航到该路由时按需加载。这大幅减少初始包大小和可交互时间。在 Angular 17+ 中,惰性加载使用 loadComponent(对于独立组件)或 loadChildren(对于路由配置文件)进行配置。Angular CLI 自动处理代码分割。预加载策略(PreloadAllModules)可以在初始加载后在后台预加载惰性模块。

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

export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./home/home.component')
      .then(m => m.HomeComponent)
  },
  {
    path: 'products',
    loadComponent: () => import('./products/products-list.component')
      .then(m => m.ProductsListComponent)
  },
  {
    path: 'products/:id',
    loadComponent: () => import('./products/product-detail.component')
      .then(m => m.ProductDetailComponent)
  },
  {
    path: 'admin',
    canActivate: [authGuard],
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.adminRoutes)
  },
  {
    path: '**',
    loadComponent: () => import('./not-found/not-found.component')
      .then(m => m.NotFoundComponent)
  }
];

// app.config.ts
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

export const appConfig = {
  providers: [
    provideRouter(routes, withPreloading(PreloadAllModules))
  ]
};

路由守卫:保护路由

路由守卫控制到路由和从路由的导航。CanActivate 防止未经授权导航到路由(例如,身份验证检查)。CanDeactivate 防止离开有未保存更改的路由。CanActivateChild 守护子路由。Resolve 在路由激活之前预取数据,消除组件中的加载状态。在 Angular 15+ 中,守卫是函数(非类),使用 inject() 函数处理依赖项,使其更简单易写和测试。

// auth.guard.ts (Angular 15+ functional style)
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

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

  if (authService.isLoggedIn) {
    return true;
  }

  // Redirect to login, preserve intended URL
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

// Unsaved changes guard
export const unsavedChangesGuard: CanDeactivateFn<EditFormComponent> =
  (component) => {
    if (component.hasUnsavedChanges()) {
      return window.confirm('You have unsaved changes. Leave anyway?');
    }
    return true;
  };

ActivatedRoute:读取 URL 参数

ActivatedRoute 提供对路由信息的访问:URL 参数(route.params 或 route.paramMap)、查询参数(route.queryParams 或 route.queryParamMap)、路由数据(route.data)和 URL 片段(route.url)。所有这些都作为 Observable 暴露——订阅以处理使用不同参数在同一组件之间的导航。当你只需要初始值时,使用快照进行一次性访问。

// product-detail.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { switchMap } from 'rxjs/operators';

@Component({ selector: 'app-product-detail', standalone: true, template: '' })
export class ProductDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  private router = inject(Router);
  private productService = inject(ProductsService);

  // Reactive: re-fetches when :id changes without re-creating the component
  product$ = this.route.paramMap.pipe(
    map(params => Number(params.get('id'))),
    switchMap(id => this.productService.getProduct(id))
  );

  ngOnInit(): void {
    // Query params
    this.route.queryParams.subscribe(params => {
      const tab = params['tab'] || 'details';
      console.log('Active tab:', tab);
    });
  }

  goBack(): void {
    this.router.navigate(['/products']);
  }

  goToEdit(id: number): void {
    this.router.navigate(['/products', id, 'edit'], {
      queryParams: { mode: 'full' }
    });
  }
}

状态管理:NgRx 和存储模式

对于复杂应用程序,组件 @Input/@Output 通信和带 Subject 的服务变得难以管理。NgRx(受 Redux 启发)提供可预测的状态容器:单一不可变存储、描述状态更改的动作、从动作计算新状态的 reducer、从存储派生数据的选择器,以及处理副作用(HTTP 调用)的效果。这种模式使状态更改明确、可追溯和可测试。

NgRx 动作和 Reducer

动作是描述发生了什么的简单对象,带有类型字符串(例如,"[Product List] Load Products"、"[Cart] Add Item")。它们可以携带载荷。createAction 函数创建类型安全的动作创建器。Reducer 是纯函数,接受当前状态和动作并返回新状态——永远不要改变现有状态。带有 on() 的 createReducer 函数处理特定动作。初始状态定义状态形状和默认值。

// store/products/products.actions.ts
import { createAction, props } from '@ngrx/store';

// Load actions
export const loadProducts = createAction('[Product List] Load Products');
export const loadProductsSuccess = createAction(
  '[Product List] Load Products Success',
  props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction(
  '[Product List] Load Products Failure',
  props<{ error: string }>()
);

// CRUD actions
export const addToCart = createAction(
  '[Cart] Add Item',
  props<{ product: Product; quantity: number }>()
);

// store/products/products.reducer.ts
import { createReducer, on } from '@ngrx/store';

interface ProductsState {
  products: Product[];
  loading: boolean;
  error: string | null;
}

const initialState: ProductsState = {
  products: [],
  loading: false,
  error: null,
};

export const productsReducer = createReducer(
  initialState,
  on(loadProducts, state => ({ ...state, loading: true, error: null })),
  on(loadProductsSuccess, (state, { products }) => ({
    ...state,
    loading: false,
    products,
  })),
  on(loadProductsFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error,
  }))
);

选择器:派生状态

选择器是选择、派生和记忆状态的纯函数。createSelector 结合多个选择器并记忆结果——只有当输入选择器产生新值时,投影函数才会重新运行。这使选择器在复杂应用程序中也非常高效。在组件中使用 store.select(mySelector) 选择状态,它返回一个 Observable,当选定状态更改时发出。组合选择器以最大化重用。

// store/products/products.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';

const selectProductsState = createFeatureSelector<ProductsState>('products');

export const selectAllProducts = createSelector(
  selectProductsState,
  state => state.products
);

export const selectProductsLoading = createSelector(
  selectProductsState,
  state => state.loading
);

// Derived selector: memoized, only recalculates when inputs change
export const selectAvailableProducts = createSelector(
  selectAllProducts,
  products => products.filter(p => p.stock > 0)
);

export const selectProductById = (id: number) => createSelector(
  selectAllProducts,
  products => products.find(p => p.id === id)
);

// Using in component:
@Component({ template: '' })
export class ProductsComponent {
  private store = inject(Store);

  products$ = this.store.select(selectAvailableProducts);
  loading$ = this.store.select(selectProductsLoading);

  ngOnInit() {
    this.store.dispatch(loadProducts());
  }
}

NgRx 效果:副作用

效果处理由动作触发的异步副作用(HTTP 调用、localStorage、定时器)。效果使用 Actions 和 ofType() 监听特定动作,执行副作用,并调度成功或失败动作。效果保持 reducer 纯净,组件不含异步逻辑。createEffect 函数包装效果 Observable 并使用 catchError 处理错误恢复,以防止效果因错误而停止。

// store/products/products.effects.ts
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { switchMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable()
export class ProductsEffects {
  private actions$ = inject(Actions);
  private productService = inject(ProductsService);

  loadProducts$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadProducts),
      switchMap(() =>
        this.productService.getProducts().pipe(
          map(response => loadProductsSuccess({ products: response.data })),
          catchError(error =>
            of(loadProductsFailure({ error: error.message }))
          )
        )
      )
    )
  );

  // Effect that dispatches no action
  logProductLoad$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadProductsSuccess),
      tap(({ products }) => console.log('Loaded ' + products.length + ' products'))
    ),
    { dispatch: false }
  );
}

Angular vs React vs Vue:框架对比

选择前端框架取决于你的团队规模、项目复杂性和组织需求。以下是 Angular、React 和 Vue 在关键维度上的详细对比。

DimensionAngularReactVue
TypeFull frameworkUI libraryProgressive framework
LanguageTypeScript (mandatory)JS or TypeScriptJS or TypeScript
Maintained byGoogleMeta (Facebook)Community / Evan You
Learning curveSteep (DI, RxJS, decorators)Moderate (hooks, JSX)Gentle (Options/Composition API)
RoutingBuilt-in (Angular Router)Third-party (React Router)Built-in (Vue Router)
State managementServices/NgRx/SignalsRedux/Zustand/RecoilVuex / Pinia
FormsBuilt-in (reactive + template)Third-party (React Hook Form)Built-in (v-model)
HTTP clientBuilt-in (HttpClient)Third-party (Axios/fetch)Third-party (Axios/fetch)
Render approachIncremental DOM / Zone.jsVirtual DOMVirtual DOM (VDOM)
Min bundle (gzip)~30KB (standalone)~45KB (React + ReactDOM)~16KB (Vue 3)
Enterprise adoptionVery high (banking, gov, Google)Very high (startups + enterprise)High (Asia, mid-market)
TestingTestBed (built-in DI testing)React Testing LibraryVue Testing Library
SSR supportAngular Universal (built-in)Next.jsNuxt.js
Best forLarge enterprise apps, big teamsAny scale, flexible architectureRapid development, gentle onboarding

Angular 17+ 新特性

Angular 17(2023 年 11 月发布)引入了自 Angular 2 以来最重要的开发者体验改进。内置于模板编译器的新控制流语法(@if、@for、@switch)以更好的性能和类型安全替代了 *ngIf 和 *ngFor。独立组件现在是默认的——NgModule 是可选的。信号提供了与 Angular 变更检测集成的新的细粒度响应性原语。新的应用构建器(Vite + esbuild)提供了 87% 更快的构建时间。

Angular 信号:细粒度响应性

信号(Angular 16+)是自动跟踪依赖关系的响应性原语。信号保存一个值,并在值更改时通知消费者。signal() 创建可写信号,computed() 创建派生信号(类似 Vue 的计算属性),effect() 在信号更改时运行副作用。信号与 Angular 的变更检测集成,在未来版本中无需 Zone.js 即可实现细粒度更新。对于同步状态,它们比 RxJS 更易于理解。

// counter.component.ts — Signals example
import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ doubled() }}</p>
    <button (click)="increment()">+</button>
    <button (click)="decrement()">-</button>
  `
})
export class CounterComponent {
  count = signal(0);

  // Computed signal: automatically updates when count changes
  doubled = computed(() => this.count() * 2);

  constructor() {
    // Effect: runs side effect when signal changes
    effect(() => {
      console.log('Count is now:', this.count());
      // Automatically tracks this.count() as a dependency
    });
  }

  increment(): void { this.count.update(v => v + 1); }
  decrement(): void { this.count.update(v => v - 1); }
}

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

@Component({ standalone: true, template: '' })
export class ExampleComponent {
  private userService = inject(UserService);

  // Convert Observable to Signal
  currentUser = toSignal(this.userService.currentUser$);

  // Use in template without async pipe
  // {{ currentUser()?.name }}
}

独立组件

独立组件(Angular 15 中稳定,17 中默认)不属于任何 NgModule。@Component 中的 standalone: true 标志允许在 imports 数组中直接导入依赖项(其他组件、管道、指令),而不是在模块中声明它们。bootstrapApplication() 取代 platformBrowserDynamic().bootstrapModule() 用于根应用程序。独立 API 使 Angular 应用更小(无模块样板代码)、更易于理解,并且更好地支持树摇。

// main.ts — Angular 17+ bootstrap (no AppModule!)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideStore } from '@ngrx/store';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptors([authInterceptor])),
    provideAnimations(),
    provideStore({ products: productsReducer }),
  ]
});

// app.component.ts — standalone root component
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RouterOutlet,
    RouterLink,
    HeaderComponent,
    FooterComponent,
  ],
  template: `
    <app-header />
    <main>
      <router-outlet />
    </main>
    <app-footer />
  `
})
export class AppComponent {}

常见问题

Angular 适合小型项目吗?

与 React 或 Vue 相比,Angular 的初始设置更复杂,对于小型项目或原型可能过于复杂。CLI、TypeScript 要求和架构模式在前期增加了复杂性。但是,对于预期会增长的项目,从 Angular 的结构开始可以避免后来痛苦的重构。Angular 17 中的独立组件显著减少了样板代码。对于真正的小型项目(落地页、简单 SPA),React 或 Vue 可能更合适。

Angular 和 AngularJS 有什么区别?

AngularJS(Angular 1.x)和 Angular(2+)是完全不同的框架,只是共享名称。AngularJS 于 2010 年发布,使用 JavaScript、基于作用域的模型和带脏检查的双向数据绑定。Angular 2 于 2016 年完全重写,使用 TypeScript、基于组件的架构、单向数据流、预先编译和完全不同的 API。AngularJS 于 2021 年 12 月停止维护。今天人们说"Angular"时,总是指 Angular 2+。

Angular 变更检测是如何工作的?

Angular 使用 Zone.js 拦截异步操作(setTimeout、HTTP 调用、DOM 事件)并自动触发变更检测。默认情况下,Angular 在任何异步操作后检查树中的每个组件。OnPush 变更检测策略通过仅在输入变化或 Observable 通过 async 管道发出时检查组件来优化这一点。信号(Angular 16+)提供了更细粒度的响应模型,最终可以完全替代 Zone.js(Angular 正在朝着"无 zone"模式努力)。

Angular 17 中的独立组件是什么?

独立组件是不属于任何 NgModule 的组件。在 Angular 14 中作为实验性功能引入,在 Angular 15 中稳定,在 Angular 17 中成为默认。在 @Component 中使用 standalone: true,你在组件的 imports 数组中直接导入依赖项,而不是在模块中声明它们。这消除了冗长的 NgModule 模式,减少了样板代码,改进了树摇,并使组件更加自包含和可复用。

我应该何时使用 NgRx vs Angular 服务进行状态管理?

对于大多数应用程序,使用带有 BehaviorSubject 或信号的 Angular 服务。NgRx 增加了显著的复杂性(样板代码、学习曲线、额外依赖项),只有在以下情况才值得:具有多个团队接触的复杂共享状态的大型应用程序、需要记录/调试/时间旅行的状态更改,或者代码库需要让许多受益于可预测模式的开发人员加入。NgRx 团队本身建议在没有 NgRx 的情况下开始,只在需要时添加它。

Angular 信号与 RxJS 相比如何?

信号和 RxJS 服务于不同目的,是互补的,而非替代品。信号是为组件状态设计的同步响应性原语——对于不涉及异步操作的 UI 状态,它们更易于学习和使用。RxJS 对于复杂的异步流、事件流和多步转换(防抖、重试、竞争条件)非常强大。Angular 团队提供了它们之间的互操作:toSignal() 将 Observable 转换为信号,toObservable() 做相反的事情。

Angular 中的树摇是什么,为什么重要?

树摇是在构建期间从最终包中消除死代码(未使用的导出)的过程。Angular 的 Ivy 编译器和 CLI 的生产构建(使用 webpack 或 esbuild)执行积极的树摇。独立组件比 NgModules 更好地支持树摇,因为它们明确声明其依赖项,使打包器更容易识别未使用的代码。这导致更小的包大小:最小独立 Angular 应用现在压缩后不到 30KB。

2024/2025 年应该使用哪个版本的 Angular?

新项目使用 Angular 17 或 18。Angular 17 引入了新的模板控制流、默认独立、新的应用构建器(Vite + esbuild)和重新设计的 angular.dev 文档。Angular 18 添加了无 zone 变更检测(实验性)、内置国际化改进和 Material 3 组件。Angular 遵循 6 个月主要发布周期,发布后 18 个月内提供 LTS 支持,因此截至 2024 年,Angular 16 和 17 都处于 LTS 中。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON Formatter.*Regex TesterB→Base64 Encoder

相关文章

React Hooks 完全指南:useState、useEffect 和自定义 Hooks

通过实际示例掌握 React Hooks。学习 useState、useEffect、useContext、useReducer、useMemo、useCallback、自定义 Hooks 和 React 18+ 并发 Hooks。

JavaScript Promises 和 Async/Await 完全指南

掌握 JavaScript Promises 和 async/await:创建、链式调用、Promise.all、错误处理和并发策略。

TypeScript 泛型完全指南 2026:从基础到高级模式

全面掌握 TypeScript 泛型:类型参数、约束、条件类型、映射类型、工具类型,以及事件发射器和 API 客户端等实战模式。