Angular 企业级开发指南 - 给 Vue3/React 开发者的从入门到精通

知识结构


一、项目搭建与 CLI

1.1 快速开始

Terminal window
# 安装 Angular CLI
npm install -g @angular/cli
# 创建新项目 (Angular 19+ 默认 standalone)
ng new my-app --style=scss --ssr=false
cd my-app && ng serve # 启动 http://localhost:4200

1.2 CLI 常用命令对比

任务Angular CLIVue (Vite)React (Vite)
创建项目ng newnpm create vue@latestnpm create vite@latest
开发服务器ng servenpm run devnpm run dev
生成组件ng g c features/user手动创建手动创建
生成服务ng g s core/auth手动创建手动创建
生成管道ng g p shared/truncate
生成指令ng g d shared/highlight
生成 Guardng g guard core/auth
构建ng buildnpm run buildnpm run build
添加库ng add @angular/materialnpm installnpm install
代码检查ng lintnpm run lintnpm run lint

Angular CLI 是三大框架中最强大的脚手架工具,ng generate 自动创建文件、测试、目录结构。

1.3 项目结构

my-app/
src/
app/
app.component.ts # 根组件
app.config.ts # 应用配置 (providers)
app.routes.ts # 路由定义
core/ # 全局单例服务、守卫、拦截器
services/
guards/
interceptors/
features/ # 功能模块 (按业务领域划分)
user/
user.component.ts
user.service.ts
user.routes.ts
dashboard/
shared/ # 可复用组件、管道、指令
components/
pipes/
directives/
assets/
styles.scss
main.ts # 启动入口
angular.json # CLI 工作区配置
tsconfig.json
package.json

与 Vue3/React 对比:

方面Angular (v19+)Vue 3React
入口main.ts + bootstrapApplication()main.ts + createApp()main.tsx + createRoot()
全局配置app.config.ts (providers)Plugins in main.tsContext/Providers in JSX
路由配置app.routes.ts (独立文件)router/index.tsApp.tsx 或 router 配置
构建配置angular.jsonvite.config.tsvite.config.ts
组件组织Feature-based 目录Feature-based 或按类型Feature-based 或按类型

二、组件系统

2.1 Standalone 组件 (现代标准)

Starting in v19, standalone: true is the default. Components, directives, and pipes are all standalone by default.

Angular Official Docs

user-profile.component.ts
@Component({
selector: 'app-user-profile',
imports: [DatePipe, UserAvatarComponent], // 直接声明依赖
template: `
<div class="profile">
<app-user-avatar [src]="user().avatar" />
<h2>{{ user().name }}</h2>
<p>Joined: {{ user().createdAt | date:'mediumDate' }}</p>
</div>
`,
styles: `.profile { padding: 16px; border: 1px solid #eee; }`
})
export class UserProfileComponent {
user = input.required<User>();
}

三框架组件定义对比:

概念AngularVue 3 (<script setup>)React
定义组件@Component 装饰器 + class<script setup> SFC函数组件
模板template 属性 / templateUrl<template>JSX return
样式隔离ViewEncapsulation (默认 emulated)<style scoped>CSS Modules / styled
选择器selector: 'app-xxx'文件名自动注册import 导入使用
依赖声明imports 数组自动导入import 直接使用

2.2 组件通信 - input / output / model

Angular 19+ 推荐基于 Signal 的新 API:

todo-item.component.ts
@Component({
selector: 'app-todo-item',
template: `
<li [class.done]="todo().completed">
<span>{{ todo().title }}</span>
<button (click)="onToggle()">Toggle</button>
<button (click)="onDelete()">Delete</button>
</li>
`
})
export class TodoItemComponent {
// Signal 写法 (推荐)
todo = input.required<Todo>(); // 必传 prop
showDetails = input(false); // 可选, 默认 false
toggle = output<string>(); // 事件
delete = output<string>(); // 事件
onToggle() { this.toggle.emit(this.todo().id); }
onDelete() { this.delete.emit(this.todo().id); }
}
<!-- 父组件使用 -->
<app-todo-item
[todo]="myTodo"
[showDetails]="true"
(toggle)="handleToggle($event)"
(delete)="handleDelete($event)"
/>

三框架组件通信对比:

概念AngularVue 3React
传入数据input() / @Input()defineProps()props 参数
事件上报output() / @Output()defineEmits()回调函数 props
双向绑定model() / [(ngModel)]defineModel() / v-model受控组件模式
必填校验input.required()required: trueTypeScript
默认值input(defaultValue)withDefaults()参数默认值

2.3 模板语法

特性AngularVue 3React JSX
文本插值{{ value }}{{ value }}{value}
属性绑定[src]="imgUrl":src="imgUrl"src={imgUrl}
事件绑定(click)="handler()"@click="handler"onClick={handler}
双向绑定[(ngModel)]="val"v-model="val"手动 (value + onChange)
条件渲染@if (cond) { }v-if="cond"{cond && <X/>}
列表渲染@for (x of list; track x.id)v-for="x in list" :key="x.id"{list.map(x => ...)}
Class 绑定[class.active]="isActive":class="{ active: isActive }"className={...}
Style 绑定[style.color]="color":style="{ color }"style={{ color }}
元素引用#myRefviewChild()ref="myRef"useRef()
内容投影<ng-content><slot>{children}

2.4 新控制流语法 (Angular 17+)

<!-- @if / @else -->
@if (user.isAdmin) {
<admin-panel />
} @else if (user.isMod) {
<mod-panel />
} @else {
<user-panel />
}
<!-- @for with track (必需) and @empty -->
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
} @empty {
<div>No items found</div>
}
<!-- @switch -->
@switch (status) {
@case ('loading') { <spinner /> }
@case ('error') { <error-message /> }
@default { <content /> }
}
<!-- @defer 延迟加载 -->
@defer (on viewport) {
<heavy-chart-component />
} @loading {
<spinner />
} @placeholder {
<div>Scroll down to see chart</div>
}

新语法优势:无需导入 CommonModule,更好的类型检查,更少 DOM 节点,性能更优。

2.5 生命周期

目的AngularVue 3 Composition APIReact
初始化ngOnInitonMounteduseEffect(() => {}, [])
属性变更ngOnChangeswatch(props, ...)useEffect(() => {}, [prop])
销毁前ngOnDestroyonUnmounteduseEffect cleanup
DOM 渲染后ngAfterViewInitonMounteduseLayoutEffect
每次渲染后afterRender()onUpdateduseEffect (无依赖)
一次性渲染后afterNextRender()nextTick()useEffect(() => {}, [])
@Component({ /* ... */ })
export class UserComponent implements OnInit, OnDestroy {
private userService = inject(UserService);
userId = input.required<string>();
ngOnInit() {
// 组件初始化,inputs 已可用
console.log('User ID:', this.userId());
}
ngOnDestroy() {
// 清理资源
console.log('Component destroyed');
}
constructor() {
// afterNextRender 必须在注入上下文中调用
afterNextRender(() => {
// DOM 已渲染,可安全操作 DOM
});
}
}

三、Signals 响应式系统

Angular Signals 是 Angular 的现代响应式原语,对 Vue3 开发者来说非常熟悉。

Signals are Angular’s go-to mechanism for component-level reactivity.

Angular Official Docs - Signals

3.1 核心 API

export class CounterComponent {
// 创建可写 Signal -- 相当于 Vue ref(0) / React useState(0)
count = signal(0);
// 计算属性 -- 相当于 Vue computed() / React useMemo()
doubled = computed(() => this.count() * 2);
increment() {
this.count.update(c => c + 1); // 相当于 Vue count.value++
}
reset() {
this.count.set(0); // 相当于 Vue count.value = 0
}
constructor() {
// 副作用 -- 相当于 Vue watchEffect() / React useEffect()
effect(() => {
console.log('Count changed:', this.count());
});
}
}

3.2 Signal API 全览

API类型说明Vue 3 等价React 等价
signal(val)可写基础响应式值ref(val)useState(val)
computed(fn)只读派生值,自动追踪computed(fn)useMemo(fn, deps)
effect(fn)副作用Signal 变化时执行watchEffect(fn)useEffect(fn)
linkedSignal(fn)可写+派生源变化时重置,可手动覆盖ref + watchuseState + useEffect
input()只读 SignalSignal 版 @InputdefinePropsprops
input.required()只读 Signal必填 Signal Inputrequired: trueTypeScript
model()可写 Signal双向绑定defineModel()受控组件
output()事件发射Signal 版 @OutputdefineEmits()callback
viewChild()只读 SignalSignal 版 @ViewChildref()useRef()
resource()异步 Signal管理异步数据加载useFetch (VueUse)React Query

3.3 linkedSignal - Angular 独有

linkedSignal 解决了一个 Vue/React 都需要组合多个 API 才能实现的场景:

export class FilterComponent {
options = input.required<string[]>();
// 当 options 变化时自动重置为第一项,但用户可以手动选择
selectedOption = linkedSignal(() => this.options()[0]);
selectOption(opt: string) {
this.selectedOption.set(opt); // 手动覆盖仍然有效
}
}

在 Vue 3 中需要 ref + watch 组合,React 中需要 useState + useEffect

3.4 resource / httpResource - 异步数据加载

export class UserDetailComponent {
userId = input.required<number>();
// 当 userId 变化时自动重新请求
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
// userResource.value() -- 数据
// userResource.isLoading() -- 加载状态
// userResource.error() -- 错误信息
}

3.5 Signals vs RxJS - 如何选择

场景推荐方案原因
组件局部状态Signal简单直观,自动触发 UI 更新
派生计算computed() Signal自带缓存,依赖自动追踪
简单异步请求httpResource() / resource()声明式,自动管理生命周期
搜索防抖RxJS (debounceTime + switchMap)需要操作符组合
WebSocket / SSERxJS持续数据流
复杂异步编排RxJS (forkJoin, combineLatest)强大的流组合能力
全局状态Signal + Service 或 NgRx SignalStore取决于复杂度

互操作桥接:

import { toSignal, toObservable } from '@angular/core/rxjs-interop';
// Observable -> Signal
const users = toSignal(this.userService.getUsers$(), { initialValue: [] });
// Signal -> Observable
const count$ = toObservable(this.count);

四、依赖注入 (DI) 系统

依赖注入是 Angular 最独特的核心特性,Vue/React 中没有同等复杂度的内置方案。

Angular’s DI framework provides dependencies to a class upon instantiation. You can use Angular DI to increase flexibility and modularity in your applications.

Angular Official Docs - DI

4.1 基础用法

auth.service.ts
@Injectable({ providedIn: 'root' }) // 全局单例,支持 Tree-shaking
export class AuthService {
private http = inject(HttpClient);
private currentUser = signal<User | null>(null);
login(credentials: LoginForm): Observable<User> {
return this.http.post<User>('/api/login', credentials).pipe(
tap(user => this.currentUser.set(user))
);
}
isLoggedIn = computed(() => this.currentUser() !== null);
}
// 在组件中使用
@Component({ /* ... */ })
export class DashboardComponent {
// inject() 函数 (推荐)
private authService = inject(AuthService);
private router = inject(Router);
}

4.2 注入层级

级别方式作用域
全局单例@Injectable({ providedIn: 'root' })整个应用一个实例
路由级路由配置中的 providers每个懒加载路由一个实例
组件级@Component({ providers: [...] })每个组件实例一个

4.3 InjectionToken - 注入非类值

tokens.ts
export const API_URL = new InjectionToken<string>('API_URL');
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
// app.config.ts - 提供值
export const appConfig: ApplicationConfig = {
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' },
{ provide: APP_CONFIG, useFactory: () => loadConfig() },
]
};
// 使用
export class ApiService {
private apiUrl = inject(API_URL);
}

4.4 三框架 DI 对比

概念AngularVue 3React
提供依赖providers / @Injectableprovide()Context.Provider
消费依赖inject() 函数inject() 函数useContext()
全局单例providedIn: 'root'app.provide()模块变量
子树作用域组件级 providers组件级 provide嵌套 Provider
自动实例化是 (DI 创建实例)否 (手动)否 (手动)
Tree-shakingN/AN/A
层级查找自动沿注入器树向上查找自动沿组件树向上自动沿组件树向上

五、Decorators 装饰器

Angular 使用 TypeScript 装饰器为类添加元数据。这是 Angular 和 Vue/React 最大的设计差异之一。

Decorators are functions that modify JavaScript classes. Angular defines a number of decorators that attach specific kinds of metadata to classes.

Angular Official Docs

5.1 @ViewChild / @ContentChild

@Component({
selector: 'app-form',
template: `
<input #nameInput type="text" />
<app-submit-button />
<ng-content></ng-content>
`
})
export class FormComponent implements AfterViewInit {
// 查询自己模板中的元素
@ViewChild('nameInput') nameInput!: ElementRef;
@ViewChild(SubmitButtonComponent) submitBtn!: SubmitButtonComponent;
// Signal 写法 (推荐)
nameInputRef = viewChild<ElementRef>('nameInput');
// 查询投影内容 (父组件通过 ng-content 传入的)
@ContentChild(FormFieldComponent) firstField!: FormFieldComponent;
ngAfterViewInit() {
this.nameInput.nativeElement.focus();
}
}

5.2 @HostListener / @HostBinding (及现代替代)

// 装饰器写法 (兼容)
@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
@HostBinding('style.backgroundColor') bgColor = '';
@HostListener('mouseenter') onEnter() { this.bgColor = 'yellow'; }
@HostListener('mouseleave') onLeave() { this.bgColor = ''; }
}
// host 属性写法 (推荐, Angular 19+)
@Directive({
selector: '[appHighlight]',
host: {
'(mouseenter)': 'onEnter()',
'(mouseleave)': 'onLeave()',
'[style.backgroundColor]': 'bgColor',
}
})
export class HighlightDirective {
bgColor = '';
onEnter() { this.bgColor = 'yellow'; }
onLeave() { this.bgColor = ''; }
}

六、RxJS 核心指南

RxJS 是 Angular 的响应式编程基石,也是 Vue/React 开发者学习 Angular 最大的门槛。

Think of RxJS as Lodash for events.

RxJS Official Documentation

6.1 Observable vs Promise

// Promise: 单值、立即执行、不可取消
const promise = fetch('/api/users').then(res => res.json());
// Observable: 多值、惰性执行、可取消
const users$ = this.http.get<User[]>('/api/users');
// 不 subscribe 就不会发请求!
const sub = users$.subscribe({
next: (users) => console.log(users),
error: (err) => console.error(err),
complete: () => console.log('done')
});
sub.unsubscribe(); // 可以取消
特性ObservablePromise
值数量多个值随时间推送单个值
执行惰性 (订阅后才执行)立即执行
取消unsubscribe()不可取消
操作符丰富 (map, filter, switchMap…).then() / .catch()
组合combineLatest, merge, forkJoinPromise.all(), Promise.race()

6.2 Subject 家族

// Subject: 多播,无初始值
const subject = new Subject<string>();
subject.subscribe(v => console.log('A:', v));
subject.next('hello'); // A: hello
subject.subscribe(v => console.log('B:', v));
subject.next('world'); // A: world, B: world (B 收不到 hello)
// BehaviorSubject: 有初始值,新订阅者立即收到最新值
const auth$ = new BehaviorSubject<User | null>(null);
auth$.subscribe(v => console.log(v)); // 立即输出 null
auth$.next({ name: 'Alice' });
// ReplaySubject: 缓存 N 个历史值
const messages$ = new ReplaySubject<string>(3); // 缓存最近 3 条

与 Vue3/React 对比:

RxJSVue 3 等价React 等价
BehaviorSubjectref() / reactive()useState()
Subject自定义 EventBusEventEmitter / callback
Observable (HTTP)useFetch / axios + refuseEffect + fetch
.subscribe()watch() / watchEffect()useEffect()

6.3 四大 Map 操作符 (高频考点)

flowchart LR
    subgraph switchMap["switchMap 取消旧的"]
        A1[请求1] -.取消.-> X1[x]
        A2[请求2] -.取消.-> X2[x]
        A3[请求3] --> R1[结果3]
    end
    subgraph mergeMap["mergeMap 全部并行"]
        B1[请求1] --> R2[结果1]
        B2[请求2] --> R3[结果2]
        B3[请求3] --> R4[结果3]
    end
    subgraph concatMap["concatMap 排队执行"]
        C1[请求1] --> R5[结果1]
        R5 --> C2[请求2]
        C2 --> R6[结果2]
    end
    subgraph exhaustMap["exhaustMap 忽略新的"]
        D1[请求1] --> R8[结果1]
        D2[请求2] -.忽略.-> X3[x]
    end
// switchMap: 取消旧请求,用最新的 (搜索框首选)
this.searchInput.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(keyword => this.api.search(keyword))
).subscribe(results => this.results.set(results));
// mergeMap: 全部并行,不取消 (批量上传)
this.files$.pipe(
mergeMap(file => this.uploadService.upload(file))
).subscribe();
// concatMap: 排队执行,保证顺序 (顺序依赖操作)
this.actions$.pipe(
concatMap(action => this.api.execute(action))
).subscribe();
// exhaustMap: 忽略后续,防重复提交 (表单提交按钮)
this.submitBtn.clicks$.pipe(
exhaustMap(() => this.api.submitForm(this.form.value))
).subscribe();

速记口诀:

Operator行为场景
switchMap取消旧的,用新的搜索、路由切换、自动补全
mergeMap全部并行批量上传、并行请求
concatMap排队执行顺序操作、消息队列
exhaustMap忽略新的表单提交、登录按钮

6.4 常用操作符组合

// 搜索标配:debounce + distinct + switchMap
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(term => term.length > 2),
switchMap(term => this.searchService.search(term))
).subscribe(results => this.results.set(results));
// combineLatest: 多个流组合 (任一变化就触发)
combineLatest([
this.route.params,
this.authService.currentUser$
]).pipe(
switchMap(([params, user]) =>
this.api.getPost(params['id'], user.token)
)
).subscribe(post => this.post.set(post));
// forkJoin: 等所有请求完成 (类似 Promise.all)
forkJoin({
users: this.api.getUsers(),
roles: this.api.getRoles(),
permissions: this.api.getPermissions()
}).subscribe(({ users, roles, permissions }) => {
this.initDashboard(users, roles, permissions);
});
// shareReplay: 缓存结果,避免重复请求
@Injectable({ providedIn: 'root' })
export class ConfigService {
readonly config$ = this.http.get<AppConfig>('/api/config').pipe(
shareReplay(1) // 多个组件订阅只发一次请求
);
constructor(private http: HttpClient) {}
}
// catchError + retry: 错误处理和重试
this.http.get<Data>('/api/data').pipe(
retry(3),
catchError(error => {
console.error('API Error:', error);
return of(DEFAULT_DATA); // 返回降级数据
})
).subscribe(data => this.data.set(data));

6.5 订阅管理 - 避免内存泄漏

// 方式 1: takeUntilDestroyed (Angular 16+, 推荐)
@Component({ /* ... */ })
export class UserListComponent {
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.userService.getUsers().pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(users => this.users = users);
}
// 在构造函数/字段初始化中可以省略参数
private data$ = this.api.getData().pipe(
takeUntilDestroyed()
);
}
// 方式 2: async pipe (模板中使用,自动管理)
@Component({
template: `
@if (users$ | async; as users) {
@for (user of users; track user.id) {
<app-user-card [user]="user" />
}
} @else {
<p>Loading...</p>
}
`
})
export class UserListComponent {
users$ = inject(UserService).getUsers();
}
// 方式 3: toSignal (推荐用于新项目)
@Component({
template: `
@for (user of users(); track user.id) {
<app-user-card [user]="user" />
}
`
})
export class UserListComponent {
users = toSignal(inject(UserService).getUsers(), { initialValue: [] });
}

不需要手动取消订阅的场景:

  • HttpClient 的请求 (完成后自动 complete)
  • 使用 async pipe 的模板绑定
  • 使用 toSignal() 转换的 Signal
  • 使用 first() / take(1) 限定的流

七、路由系统

7.1 路由配置

app.routes.ts
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{
path: 'dashboard',
loadComponent: () => import('./features/dashboard/dashboard.component')
.then(m => m.DashboardComponent), // 组件级懒加载
},
{
path: 'admin',
canActivate: [authGuard], // 路由守卫
loadChildren: () => import('./features/admin/admin.routes')
.then(m => m.ADMIN_ROUTES), // 路由级懒加载
},
{
path: 'users/:id',
loadComponent: () => import('./features/user/user-detail.component')
.then(m => m.UserDetailComponent),
resolve: { user: userResolver }, // 数据预加载
},
{ path: '**', component: NotFoundComponent },
];
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)]
};

7.2 路由守卫 (函数式, 推荐)

auth.guard.ts
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
return authService.isLoggedIn()
? true
: router.createUrlTree(['/login']);
};
// 使用: canActivate: [authGuard]

7.3 数据预加载 (Resolver)

export const userResolver: ResolveFn<User> = (route) => {
return inject(UserService).getUser(route.paramMap.get('id')!);
};
// 使用: resolve: { user: userResolver }

7.4 三框架路由对比

特性Angular RouterVue RouterReact Router
懒加载loadComponent / loadChildren动态 import()React.lazy() + Suspense
路由守卫CanActivateFn 等内置beforeEach / beforeEnter无内置,用包装组件
数据预加载内置 ResolveFn无内置,用 beforeEnterv6.4+ loader
配置方式集中式数组配置集中式数组配置JSX 或配置对象

八、表单系统

Angular 提供两种表单方案,企业级项目推荐 Reactive Forms

8.1 Reactive Forms vs Template-driven

方面Reactive FormsTemplate-driven Forms
场景复杂表单、动态字段、企业级简单表单 (登录、联系)
数据流同步、不可变异步、可变
校验编程式 (Validators)指令式 (模板中)
测试简单 (无需 DOM)需要深度渲染
可扩展性有限

8.2 Reactive Forms 实战

@Component({
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div>
<label>Name</label>
<input formControlName="name" />
@if (userForm.get('name')?.errors?.['required']) {
<span class="error">Name is required</span>
}
</div>
<div>
<label>Email</label>
<input formControlName="email" type="email" />
@if (userForm.get('email')?.errors?.['email']) {
<span class="error">Invalid email</span>
}
</div>
<div formGroupName="address">
<input formControlName="street" placeholder="Street" />
<input formControlName="city" placeholder="City" />
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
`
})
export class UserFormComponent {
private fb = inject(FormBuilder);
userForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
address: this.fb.group({
street: [''],
city: ['']
})
});
onSubmit() {
if (this.userForm.valid) {
console.log(this.userForm.value);
}
}
}

九、HttpClient

9.1 基础配置

app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, errorInterceptor])
)
]
};

9.2 函数式拦截器 (推荐)

auth.interceptor.ts
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
}
return next(req);
};
// error.interceptor.ts
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
inject(Router).navigate(['/login']);
}
return throwError(() => error);
})
);
};

9.3 三框架 HTTP 对比

特性Angular HttpClientAxios (Vue/React)Fetch API
拦截器内置 withInterceptors内置 interceptors无原生支持
错误处理RxJS catchError.catch()手动 response.ok
JSON 解析自动自动手动 .json()
取消请求takeUntil / switchMapAbortControllerAbortController
TypeScript一等泛型支持良好基础
返回类型ObservablePromisePromise

十、Content Projection (内容投影)

Angular 的 Content Projection 等价于 Vue 的 Slots 和 React 的 children。

Content projection is a pattern in which you insert, or project, the content you want to use inside another component.

Angular Official Docs

10.1 单 Slot 与多 Slot

// modal.component.ts - 多 slot 投影
@Component({
selector: 'app-modal',
template: `
<div class="modal">
<header><ng-content select="[modal-header]" /></header>
<main><ng-content select="[modal-body]" /></main>
<footer><ng-content select="[modal-footer]" /></footer>
</div>
`
})
export class ModalComponent {}
// 使用
@Component({
template: `
<app-modal>
<div modal-header><h2>Confirm Delete</h2></div>
<div modal-body><p>Are you sure?</p></div>
<div modal-footer>
<button (click)="cancel()">Cancel</button>
<button (click)="confirm()">OK</button>
</div>
</app-modal>
`
})
export class PageComponent {}

10.2 ng-template + ngTemplateOutlet (高级用法)

// data-table.component.ts - 可自定义模板的组件
@Component({
selector: 'app-data-table',
template: `
<table>
@for (row of data(); track row.id) {
<tr>
<ng-container
*ngTemplateOutlet="rowTemplate() || defaultRow; context: { $implicit: row }"
/>
</tr>
}
</table>
<ng-template #defaultRow let-row>
<td>{{ row.id }}</td>
<td>{{ row.name }}</td>
</ng-template>
`
})
export class DataTableComponent {
data = input.required<any[]>();
rowTemplate = input<TemplateRef<any>>();
}
// 使用 - 传入自定义行模板
@Component({
template: `
<ng-template #customRow let-row>
<td><strong>{{ row.name }}</strong></td>
<td>{{ row.email }}</td>
<td><button (click)="edit(row)">Edit</button></td>
</ng-template>
<app-data-table [data]="users" [rowTemplate]="customRow" />
`
})
export class UserPageComponent {}

10.3 三框架对比

概念AngularVue 3React
默认 slot<ng-content /><slot />{children}
具名 slot<ng-content select="[name]" /><slot name="xxx" />命名 props
作用域 slotngTemplateOutlet + contextv-slot="{ data }"render props
逻辑容器<ng-container><template><Fragment>

十一、Pipes 管道

11.1 内置管道

管道用途示例
DatePipe日期格式化{{ date | date:'yyyy-MM-dd' }}
CurrencyPipe货币格式化{{ price | currency:'CNY' }}
DecimalPipe数字格式化{{ num | number:'1.2-2' }}
PercentPipe百分比{{ ratio | percent:'1.0-0' }}
AsyncPipe自动订阅 Observable/Promise{{ data$ | async }}
JsonPipe调试用 JSON{{ obj | json }}
UpperCasePipe大写{{ text | uppercase }}
KeyValuePipe遍历对象@for (kv of obj | keyvalue)

11.2 自定义管道

@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
transform(value: string, maxLength = 50): string {
return value.length > maxLength
? value.substring(0, maxLength) + '...'
: value;
}
}
// 使用: {{ longText | truncate:30 }}

与 Vue3 对比: Vue 3 移除了 filters,用 computed() 或方法替代。Angular Pipes 本质上就是模板中的纯函数转换。


十二、状态管理

12.1 方案选择

方案复杂度适用场景类比
Service + Signals中小型应用Vue composables
NgRx SignalStore中大型应用Pinia
NgRx Store大型企业应用Redux Toolkit
NGXS装饰器爱好者Vuex

12.2 Service + Signals (推荐起步方案)

@Injectable({ providedIn: 'root' })
export class TodoStore {
// State
private todos = signal<Todo[]>([]);
// Selectors (只读)
readonly allTodos = this.todos.asReadonly();
readonly completedCount = computed(() =>
this.todos().filter(t => t.completed).length
);
readonly pendingCount = computed(() =>
this.todos().filter(t => !t.completed).length
);
// Actions
addTodo(title: string) {
this.todos.update(todos => [
...todos,
{ id: crypto.randomUUID(), title, completed: false }
]);
}
toggleTodo(id: string) {
this.todos.update(todos =>
todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
removeTodo(id: string) {
this.todos.update(todos => todos.filter(t => t.id !== id));
}
}

12.3 NgRx SignalStore (企业级推荐)

import { signalStore, withState, withMethods, withComputed, patchState } from '@ngrx/signals';
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState({ todos: [] as Todo[], loading: false }),
withComputed(({ todos }) => ({
completedCount: computed(() => todos().filter(t => t.completed).length),
})),
withMethods((store, todoApi = inject(TodoApiService)) => ({
async loadTodos() {
patchState(store, { loading: true });
const todos = await firstValueFrom(todoApi.getAll());
patchState(store, { todos, loading: false });
},
addTodo(title: string) {
patchState(store, { todos: [...store.todos(), { id: Date.now(), title, completed: false }] });
},
}))
);

12.4 三框架状态管理对比

特性NgRxPinia (Vue)Redux Toolkit (React)
样板代码多 (Action, Reducer, Effect)中等
学习曲线陡峭平缓中等
DevTools
异步处理RxJS Effects内置 actionscreateAsyncThunk
TypeScript
推荐入门SignalStoredefineStorecreateSlice

十三、变更检测 (Change Detection)

13.1 策略对比

策略行为何时使用
Default每次事件检查整个组件树老项目/简单应用
OnPush仅 inputs 变化/事件/signals 时检查推荐所有新代码
Zoneless无 Zone.js,完全依赖 Signals未来默认 (v20+ 稳定)

13.2 三框架变更检测对比

方面AngularVue 3React
机制Zone.js + 组件树遍历 (或 Zoneless + Signals)Proxy 响应式 (自动细粒度追踪)Virtual DOM diffing
粒度组件级 (Signal 可达属性级)属性级 (细粒度)组件级
优化方式OnPush 策略不需要 (自动)React.memo / useMemo
未来方向Zoneless + Signals (类似 Vue)已是细粒度React Compiler

13.3 Zoneless 配置

app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(), // 无 Zone.js
provideRouter(routes),
provideHttpClient()
]
};

Zoneless 模式下,Angular 完全依赖 Signals 驱动 UI 更新,减少约 18% 的包体积。


十四、性能优化

14.1 优化清单

技术效果难度
OnPush 变更检测减少不必要的组件检查
track 表达式 (@for)避免列表项重建
路由懒加载 (loadComponent)减小首屏包体积
@defer 延迟加载组件级按需加载,首屏减 30-50%
Zoneless减少 18% 包体积,提升 12% 首屏速度
AOT 编译更快渲染,更小包体积默认开启
shareReplay避免重复 HTTP 请求
CDK Virtual Scrolling高效渲染大列表

14.2 @defer 延迟加载 (Angular 17+)

<!-- 进入视口时加载 -->
@defer (on viewport) {
<heavy-chart [data]="chartData" />
} @loading (minimum 200ms) {
<skeleton-loader />
} @placeholder {
<div style="height: 400px">Chart will appear here</div>
} @error {
<p>Failed to load chart</p>
}
<!-- 其他触发条件 -->
@defer (on idle) { ... } <!-- 浏览器空闲时 -->
@defer (on interaction) { ... } <!-- 用户交互时 -->
@defer (on hover) { ... } <!-- 鼠标悬停时 -->
@defer (on timer(5s)) { ... } <!-- 5秒后 -->
@defer (when condition) { ... } <!-- 条件为真时 -->

十五、测试

15.1 单元测试 (Jest)

describe('AuthService', () => {
let service: AuthService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
AuthService,
provideHttpClient(),
provideHttpClientTesting(),
]
});
service = TestBed.inject(AuthService);
httpMock = TestBed.inject(HttpTestingController);
});
it('should login successfully', () => {
const mockUser = { id: '1', name: 'Alice' };
service.login({ email: 'a@b.com', password: '123' }).subscribe(user => {
expect(user).toEqual(mockUser);
});
const req = httpMock.expectOne('/api/login');
expect(req.request.method).toBe('POST');
req.flush(mockUser);
});
});

15.2 组件测试

describe('UserCardComponent', () => {
it('should display user name', async () => {
await TestBed.configureTestingModule({
imports: [UserCardComponent]
}).compileComponents();
const fixture = TestBed.createComponent(UserCardComponent);
fixture.componentRef.setInput('user', { name: 'Alice', email: 'a@b.com' });
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Alice');
});
});

15.3 测试工具对比

工具用途状态
Jest单元/组件测试主流推荐
Vitest单元/组件测试Angular 21+ 支持
Jasmine + Karma单元测试传统,逐渐淘汰
PlaywrightE2E 测试推荐
CypressE2E 测试仍流行
Component Harness (CDK)组件交互测试官方推荐

十六、企业级架构模式

16.1 Feature-based 架构 (推荐)

src/app/
core/ # 全局单例 (加载一次)
services/auth.service.ts
interceptors/auth.interceptor.ts
guards/auth.guard.ts
features/ # 业务模块 (各自独立)
user/
components/
services/
user.routes.ts
order/
components/
services/
order.routes.ts
shared/ # 跨模块复用
components/button/
pipes/truncate.pipe.ts
directives/tooltip.directive.ts

16.2 Nx Monorepo (大型团队)

apps/
web-app/ # 主应用
admin-app/ # 管理后台
libs/
feature-user/ # 用户功能库
data-access-api/ # API 数据层
ui-components/ # UI 组件库
util-validators/ # 工具函数库

Nx 库类型及依赖规则:

库类型用途可依赖
feature业务功能data-access, ui, util
data-accessAPI 和状态管理util
ui展示组件util
util纯工具函数

十七、SSR / 安全 / i18n

17.1 SSR (服务端渲染)

Terminal window
ng add @angular/ssr

Angular SSR 特性:

  • Hydration: 复用服务端 DOM,不重新渲染
  • 混合渲染: 同一应用支持 SSR + SSG + CSR
  • 增量 Hydration: 配合 @defer 按需水合
  • 3x 更快的首屏感知速度

17.2 安全最佳实践

  • Angular 默认将所有值视为不可信,自动 sanitize HTML/URL/Style
  • 永远不要使用 innerHTMLElementRef.nativeElementeval()
  • 仅在绝对必要时使用 DomSanitizer.bypassSecurityTrustHtml()
  • HttpClient 自动支持 CSRF 双重提交 Cookie 模式
  • 使用 CSP 作为第二层防御

来源: Angular Security

17.3 i18n 国际化

方案类型动态切换特点
@angular/localize编译时否 (需重建)官方,零运行时开销
Transloco运行时DX 最佳,推荐
ngx-translate运行时成熟稳定

十八、Angular Material 和 CDK

18.1 Angular Material

企业级 UI 组件库,Angular 团队官方维护,Material Design 规范:

Terminal window
ng add @angular/material

提供: Button, Card, Table, Form Controls, Dialog, Snackbar, DatePicker, Tree, Toolbar 等完整组件集。

18.2 CDK (Component Dev Kit)

不含样式的行为原语,适合自定义 UI 设计系统:

模块能力
Overlay浮层、下拉、Modal 定位
Drag & Drop拖拽排序、自由拖拽
Virtual Scrolling大列表虚拟滚动
A11y焦点管理、键盘导航、屏幕阅读器
Layout断点观察器 (响应式)
Clipboard程序化复制

十九、常见陷阱 (Vue3/React 转 Angular)

陷阱说明解决方案
忘记取消订阅Observable 不取消会内存泄漏takeUntilDestroyed / async pipe / toSignal
不理解 DIAngular DI 远比 Vue provide/inject 复杂providedIn: 'root' + inject() 开始
还在学 NgModule新项目不需要 Module直接用 Standalone 组件
Zone.js 心智负担不理解变更检测何时触发用 OnPush + Signals,迁向 Zoneless
RxJS 过度使用简单场景不需要 Observable简单状态用 Signal,复杂异步才用 RxJS
装饰器写法混乱新旧 API 混用统一用 input() / output() / inject()
模板语法混乱*ngIf@if 混用统一用新控制流 @if / @for / @switch

二十、2025-2026 最佳实践速查

The Angular team conducted a public RFC to update the style guide for 2025.

Angular Style Guide RFC

实践推荐避免
组件类型Standalone (默认)NgModule
输入输出input() / output() / model()@Input / @Output
依赖注入inject() 函数构造函数注入
状态管理Signal简单场景用 BehaviorSubject
变更检测OnPush (或 Zoneless)Default
控制流@if / @for / @switch*ngIf / *ngFor
拦截器函数式 HttpInterceptorFn类式 HttpInterceptor
路由守卫函数式 CanActivateFn类式 CanActivate
表单类型Typed Reactive FormsUntypedFormGroup
文件命名kebab-case其他命名风格

推荐学习路径


参考资料

官方文档

RxJS

生态

社区资源

Read Next

Go Web 框架对决:Gin vs Fiber 全面深度对比

Read Previous

MyBatis vs MyBatis-Plus vs JPA:Java ORM 框架深度对比