Angular 企业级开发指南 - 给 Vue3/React 开发者的从入门到精通
知识结构
一、项目搭建与 CLI
1.1 快速开始
# 安装 Angular CLInpm install -g @angular/cli
# 创建新项目 (Angular 19+ 默认 standalone)ng new my-app --style=scss --ssr=falsecd my-app && ng serve # 启动 http://localhost:42001.2 CLI 常用命令对比
| 任务 | Angular CLI | Vue (Vite) | React (Vite) |
|---|---|---|---|
| 创建项目 | ng new | npm create vue@latest | npm create vite@latest |
| 开发服务器 | ng serve | npm run dev | npm run dev |
| 生成组件 | ng g c features/user | 手动创建 | 手动创建 |
| 生成服务 | ng g s core/auth | 手动创建 | 手动创建 |
| 生成管道 | ng g p shared/truncate | — | — |
| 生成指令 | ng g d shared/highlight | — | — |
| 生成 Guard | ng g guard core/auth | — | — |
| 构建 | ng build | npm run build | npm run build |
| 添加库 | ng add @angular/material | npm install | npm install |
| 代码检查 | ng lint | npm run lint | npm 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.jsonpackage.json与 Vue3/React 对比:
| 方面 | Angular (v19+) | Vue 3 | React |
|---|---|---|---|
| 入口 | main.ts + bootstrapApplication() | main.ts + createApp() | main.tsx + createRoot() |
| 全局配置 | app.config.ts (providers) | Plugins in main.ts | Context/Providers in JSX |
| 路由配置 | app.routes.ts (独立文件) | router/index.ts | App.tsx 或 router 配置 |
| 构建配置 | angular.json | vite.config.ts | vite.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.
@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>();}三框架组件定义对比:
| 概念 | Angular | Vue 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:
@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)"/>三框架组件通信对比:
| 概念 | Angular | Vue 3 | React |
|---|---|---|---|
| 传入数据 | input() / @Input() | defineProps() | props 参数 |
| 事件上报 | output() / @Output() | defineEmits() | 回调函数 props |
| 双向绑定 | model() / [(ngModel)] | defineModel() / v-model | 受控组件模式 |
| 必填校验 | input.required() | required: true | TypeScript |
| 默认值 | input(defaultValue) | withDefaults() | 参数默认值 |
2.3 模板语法
| 特性 | Angular | Vue 3 | React 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 }} |
| 元素引用 | #myRef 或 viewChild() | 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 生命周期
| 目的 | Angular | Vue 3 Composition API | React |
|---|---|---|---|
| 初始化 | ngOnInit | onMounted | useEffect(() => {}, []) |
| 属性变更 | ngOnChanges | watch(props, ...) | useEffect(() => {}, [prop]) |
| 销毁前 | ngOnDestroy | onUnmounted | useEffect cleanup |
| DOM 渲染后 | ngAfterViewInit | onMounted | useLayoutEffect |
| 每次渲染后 | afterRender() | onUpdated | useEffect (无依赖) |
| 一次性渲染后 | 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.
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 + watch | useState + useEffect |
input() | 只读 Signal | Signal 版 @Input | defineProps | props |
input.required() | 只读 Signal | 必填 Signal Input | required: true | TypeScript |
model() | 可写 Signal | 双向绑定 | defineModel() | 受控组件 |
output() | 事件发射 | Signal 版 @Output | defineEmits() | callback |
viewChild() | 只读 Signal | Signal 版 @ViewChild | ref() | 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 / SSE | RxJS | 持续数据流 |
| 复杂异步编排 | RxJS (forkJoin, combineLatest) | 强大的流组合能力 |
| 全局状态 | Signal + Service 或 NgRx SignalStore | 取决于复杂度 |
互操作桥接:
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
// Observable -> Signalconst users = toSignal(this.userService.getUsers$(), { initialValue: [] });
// Signal -> Observableconst 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.
4.1 基础用法
@Injectable({ providedIn: 'root' }) // 全局单例,支持 Tree-shakingexport 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 - 注入非类值
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 对比
| 概念 | Angular | Vue 3 | React |
|---|---|---|---|
| 提供依赖 | providers / @Injectable | provide() | Context.Provider |
| 消费依赖 | inject() 函数 | inject() 函数 | useContext() |
| 全局单例 | providedIn: 'root' | app.provide() | 模块变量 |
| 子树作用域 | 组件级 providers | 组件级 provide | 嵌套 Provider |
| 自动实例化 | 是 (DI 创建实例) | 否 (手动) | 否 (手动) |
| Tree-shaking | 是 | N/A | N/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.
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.
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(); // 可以取消| 特性 | Observable | Promise |
|---|---|---|
| 值数量 | 多个值随时间推送 | 单个值 |
| 执行 | 惰性 (订阅后才执行) | 立即执行 |
| 取消 | unsubscribe() | 不可取消 |
| 操作符 | 丰富 (map, filter, switchMap…) | .then() / .catch() |
| 组合 | combineLatest, merge, forkJoin | Promise.all(), Promise.race() |
6.2 Subject 家族
// Subject: 多播,无初始值const subject = new Subject<string>();subject.subscribe(v => console.log('A:', v));subject.next('hello'); // A: hellosubject.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)); // 立即输出 nullauth$.next({ name: 'Alice' });
// ReplaySubject: 缓存 N 个历史值const messages$ = new ReplaySubject<string>(3); // 缓存最近 3 条与 Vue3/React 对比:
| RxJS | Vue 3 等价 | React 等价 |
|---|---|---|
BehaviorSubject | ref() / reactive() | useState() |
Subject | 自定义 EventBus | EventEmitter / callback |
Observable (HTTP) | useFetch / axios + ref | useEffect + 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 + switchMapthis.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)- 使用
asyncpipe 的模板绑定 - 使用
toSignal()转换的 Signal - 使用
first()/take(1)限定的流
七、路由系统
7.1 路由配置
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.tsexport const appConfig: ApplicationConfig = { providers: [provideRouter(routes)]};7.2 路由守卫 (函数式, 推荐)
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 Router | Vue Router | React Router |
|---|---|---|---|
| 懒加载 | loadComponent / loadChildren | 动态 import() | React.lazy() + Suspense |
| 路由守卫 | CanActivateFn 等内置 | beforeEach / beforeEnter | 无内置,用包装组件 |
| 数据预加载 | 内置 ResolveFn | 无内置,用 beforeEnter | v6.4+ loader |
| 配置方式 | 集中式数组配置 | 集中式数组配置 | JSX 或配置对象 |
八、表单系统
Angular 提供两种表单方案,企业级项目推荐 Reactive Forms。
8.1 Reactive Forms vs Template-driven
| 方面 | Reactive Forms | Template-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 基础配置
export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([authInterceptor, errorInterceptor]) ) ]};9.2 函数式拦截器 (推荐)
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.tsexport 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 HttpClient | Axios (Vue/React) | Fetch API |
|---|---|---|---|
| 拦截器 | 内置 withInterceptors | 内置 interceptors | 无原生支持 |
| 错误处理 | RxJS catchError | .catch() | 手动 response.ok |
| JSON 解析 | 自动 | 自动 | 手动 .json() |
| 取消请求 | takeUntil / switchMap | AbortController | AbortController |
| TypeScript | 一等泛型支持 | 良好 | 基础 |
| 返回类型 | Observable | Promise | Promise |
十、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.
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 三框架对比
| 概念 | Angular | Vue 3 | React |
|---|---|---|---|
| 默认 slot | <ng-content /> | <slot /> | {children} |
| 具名 slot | <ng-content select="[name]" /> | <slot name="xxx" /> | 命名 props |
| 作用域 slot | ngTemplateOutlet + context | v-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 三框架状态管理对比
| 特性 | NgRx | Pinia (Vue) | Redux Toolkit (React) |
|---|---|---|---|
| 样板代码 | 多 (Action, Reducer, Effect) | 少 | 中等 |
| 学习曲线 | 陡峭 | 平缓 | 中等 |
| DevTools | 有 | 有 | 有 |
| 异步处理 | RxJS Effects | 内置 actions | createAsyncThunk |
| TypeScript | 强 | 强 | 强 |
| 推荐入门 | SignalStore | defineStore | createSlice |
十三、变更检测 (Change Detection)
13.1 策略对比
| 策略 | 行为 | 何时使用 |
|---|---|---|
Default | 每次事件检查整个组件树 | 老项目/简单应用 |
OnPush | 仅 inputs 变化/事件/signals 时检查 | 推荐所有新代码 |
Zoneless | 无 Zone.js,完全依赖 Signals | 未来默认 (v20+ 稳定) |
13.2 三框架变更检测对比
| 方面 | Angular | Vue 3 | React |
|---|---|---|---|
| 机制 | Zone.js + 组件树遍历 (或 Zoneless + Signals) | Proxy 响应式 (自动细粒度追踪) | Virtual DOM diffing |
| 粒度 | 组件级 (Signal 可达属性级) | 属性级 (细粒度) | 组件级 |
| 优化方式 | OnPush 策略 | 不需要 (自动) | React.memo / useMemo |
| 未来方向 | Zoneless + Signals (类似 Vue) | 已是细粒度 | React Compiler |
13.3 Zoneless 配置
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 | 单元测试 | 传统,逐渐淘汰 |
| Playwright | E2E 测试 | 推荐 |
| Cypress | E2E 测试 | 仍流行 |
| 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.ts16.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-access | API 和状态管理 | util |
| ui | 展示组件 | util |
| util | 纯工具函数 | 无 |
十七、SSR / 安全 / i18n
17.1 SSR (服务端渲染)
ng add @angular/ssrAngular SSR 特性:
- Hydration: 复用服务端 DOM,不重新渲染
- 混合渲染: 同一应用支持 SSR + SSG + CSR
- 增量 Hydration: 配合
@defer按需水合 - 3x 更快的首屏感知速度
17.2 安全最佳实践
- Angular 默认将所有值视为不可信,自动 sanitize HTML/URL/Style
- 永远不要使用
innerHTML、ElementRef.nativeElement、eval() - 仅在绝对必要时使用
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 规范:
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 |
| 不理解 DI | Angular 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.
| 实践 | 推荐 | 避免 |
|---|---|---|
| 组件类型 | Standalone (默认) | NgModule |
| 输入输出 | input() / output() / model() | @Input / @Output |
| 依赖注入 | inject() 函数 | 构造函数注入 |
| 状态管理 | Signal | 简单场景用 BehaviorSubject |
| 变更检测 | OnPush (或 Zoneless) | Default |
| 控制流 | @if / @for / @switch | *ngIf / *ngFor |
| 拦截器 | 函数式 HttpInterceptorFn | 类式 HttpInterceptor |
| 路由守卫 | 函数式 CanActivateFn | 类式 CanActivate |
| 表单类型 | Typed Reactive Forms | UntypedFormGroup |
| 文件命名 | kebab-case | 其他命名风格 |
推荐学习路径
参考资料
官方文档
- Angular Dev (主文档站)
- Angular Signals Guide
- Angular Components Lifecycle
- Angular Dependency Injection
- Angular Template Control Flow
- Angular Template Binding
- Angular Content Projection
- Angular Routing
- Angular Route Guards
- Angular Forms
- Angular HttpClient Interceptors
- Angular Pipes
- Angular Zoneless
- Angular @defer Blocks
- Angular SSR
- Angular Security
- Angular Style Guide
- Angular i18n
- Angular Standalone Migration
- Angular Roadmap