| name | angular |
| description | Angular 框架编程助手,提供组件开发、路由、表单、HTTP、依赖注入等方面的指导和代码生成 |
| disable-model-invocation | false |
| allowed-tools | Read, Write, Edit, Grep, Glob |
| argument-hint | ["Angular 开发任务"] |
Angular 编程助手
本技能提供 Angular 框架开发的全面支持,包括组件、指令、服务、路由、表单、HTTP 通信等核心功能的指导和代码生成。
核心概念
组件 (Components)
组件是 Angular 应用的基本构建块,控制屏幕上的一块区域(称为视图)。
创建基本组件
import { Component } from '@angular/core';
@Component({
selector: 'app-my-component',
template: `
<h1>{{ title }}</h1>
<p>{{ description }}</p>
`,
styles: [`
h1 { color: #333; }
p { color: #666; }
`],
standalone: true
})
export class MyComponent {
title = 'Hello Angular';
description = 'This is a simple component';
}
组件元数据属性
selector: CSS 选择器,用于在模板中标识组件(如 'app-my-component')
template: 内联 HTML 模板
templateUrl: 外部 HTML 模板文件路径
styles: 内联 CSS 样式
styleUrls: 外部 CSS 样式文件路径
standalone: 标记为独立组件(Angular 14+)
组件生命周期钩子
import { Component, OnInit, OnChanges, OnDestroy } from '@angular/core';
@Component({...})
export class MyComponent implements OnInit, OnChanges, OnDestroy {
ngOnInit(): void {
console.log('Component initialized');
}
ngOnChanges(changes: SimpleChanges): void {
console.log('Input changed', changes);
}
ngOnDestroy(): void {
console.log('Component destroyed');
}
}
主要生命周期钩子:
OnInit: 组件初始化完成后调用
OnChanges: 输入属性变化时调用
DoCheck: 每次变更检测时调用
AfterContentInit: 内容投影完成后调用
AfterViewInit: 视图初始化完成后调用
AfterContentChecked: 内容检查完成后调用
AfterViewChecked: 视图检查完成后调用
OnDestroy: 组件销毁前调用
组件交互
输入属性 (@Input): 从父组件接收数据
import { Component, Input } from '@angular/core';
@Component({...})
export class ChildComponent {
@Input() message: string = '';
@Input() user?: User;
}
输出属性 (@Output): 向父组件发送事件
import { Component, Output, EventEmitter } from '@angular/core';
@Component({...})
export class ChildComponent {
@Output() messageChanged = new EventEmitter<string>();
@Output() buttonClicked = new EventEmitter<void>();
sendMessage() {
this.messageChanged.emit('Hello from child');
}
handleClick() {
this.buttonClicked.emit();
}
}
父组件使用:
<app-child
[message]="parentMessage"
[user]="currentUser"
(messageChanged)="handleMessageChange($event)"
(buttonClicked)="handleButtonClick()">
</app-child>
独立组件 (Standalone Components)
独立组件不依赖 NgModule,更加简洁和模块化。
创建独立组件
@Component({
selector: 'app-standalone',
template: `
<h1>{{ title }}</h1>
<button (click)="increment()">Count: {{ count() }}</button>
`,
imports: [CommonModule, FormsModule],
standalone: true
})
export class StandaloneComponent {
title = 'Standalone Component';
count = signal(0);
increment() {
this.count.update(c => c + 1);
}
}
Bootstrap 独立应用
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, Routes } from '@angular/router';
import { AppComponent } from './app/app.component';
const routes: Routes = [
{ path: '', component: AppComponent },
];
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes)
]
});
依赖注入 (Dependency Injection)
创建服务
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com';
getData(): Observable<any> {
return this.http.get(this.apiUrl);
}
constructor(private http: HttpClient) {}
}
注入服务
import { Component, inject } from '@angular/core';
import { DataService } from './data.service';
@Component({...})
export class MyComponent {
constructor(private dataService: DataService) {}
private http = inject(HttpClient);
}
Provider 类型
{ provide: DataService, useClass: DataService }
{ provide: 'API_URL', useValue: 'https://api.example.com' }
{
provide: 'CONFIG',
useFactory: (dataService: DataService) => {
return dataService.getConfig();
},
deps: [DataService]
}
{ provide: AbstractService, useExisting: ConcreteService }
InjectionToken
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');
{ provide: API_URL, useValue: 'https://api.example.com' }
constructor(@Inject(API_URL) private apiUrl: string) {}
路由 (Routing)
基本路由配置
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'users/:id', component: UserDetailComponent },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)]
});
路由导航
模板导航:
<a routerLink="/about">About</a>
<a [routerLink]="['/users', userId]">User Details</a>
<router-outlet></router-outlet>
编程导航:
import { Router, ActivatedRoute } from '@angular/router';
constructor(
private router: Router,
private route: ActivatedRoute
) {}
navigateToUsers() {
this.router.navigate(['/users']);
}
navigateToDetails(id: string) {
this.router.navigate(['../', id], { relativeTo: this.route });
}
navigateWithParams() {
this.router.navigate(['/search'], { queryParams: { q: 'angular' } });
}
路由参数
import { ActivatedRoute } from '@angular/router';
constructor(private route: ActivatedRoute) {}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
this.route.paramMap.subscribe(params => {
const id = params.get('id');
});
this.route.queryParamMap.subscribe(params => {
const page = params.get('page');
});
}
路由守卫 (Route Guards)
export const canActivateGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
if (authService.isAuthenticated()) {
return true;
}
return inject(Router).createUrlTree(['/login']);
};
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [canActivateGuard]
}
];
表单 (Forms)
Angular 支持两种表单方式:响应式表单和模板驱动表单。
响应式表单 (Reactive Forms)
更适合复杂场景,提供更好的可测试性和可扩展性。
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({...})
export class ReactiveFormComponent {
userForm: FormGroup;
constructor(private fb: FormBuilder) {
this.userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
age: [null, [Validators.min(18), Validators.max(120)]],
address: this.fb.group({
street: [''],
city: [''],
zip: ['']
}),
hobbies: this.fb.array([])
});
}
addHobby() {
const hobbies = this.userForm.get('hobbies') as FormArray;
hobbies.push(this.fb.control(''));
}
onSubmit() {
if (this.userForm.valid) {
console.log(this.userForm.value);
}
}
}
模板:
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<input formControlName="name" placeholder="Name">
<div *ngIf="userForm.get('name')?.errors?.['required']">Name is required</div>
<div *ngIf="userForm.get('name')?.errors?.['minlength']">Name must be at least 3 characters</div>
<input formControlName="email" placeholder="Email">
<div *ngIf="userForm.get('email')?.errors?.['email']">Invalid email format</div>
<div formGroupName="address">
<input formControlName="street" placeholder="Street">
<input formControlName="city" placeholder="City">
<input formControlName="zip" placeholder="Zip">
</div>
<div formArrayName="hobbies">
<div *ngFor="let hobby of userForm.get('hobbies')?.controls; let i = index">
<input [formControlName]="i" placeholder="Hobby">
</div>
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
模板驱动表单 (Template-Driven Forms)
更简单,适合小型表单。
import { Component } from '@angular/core';
@Component({...})
export class TemplateFormComponent {
user = {
name: '',
email: ''
};
onSubmit() {
console.log(this.user);
}
}
模板:
<form #userForm="ngForm" (ngSubmit)="onSubmit()">
<input [(ngModel)]="user.name" name="name" required minlength="3" #name="ngModel">
<div *ngIf="name.invalid && name.touched">
<div *ngIf="name.errors?.['required']">Name is required</div>
<div *ngIf="name.errors?.['minlength']">Name must be at least 3 characters</div>
</div>
<input [(ngModel)]="user.email" name="email" required email #email="ngModel">
<div *ngIf="email.invalid && email.touched">
<div *ngIf="email.errors?.['required']">Email is required</div>
<div *ngIf="email.errors?.['email']">Invalid email format</div>
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
</form>
HTTP 通信
HttpClient 基本使用
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({...})
export class ApiService {
private apiUrl = 'https://api.example.com';
constructor(private http: HttpClient) {}
getData(): Observable<any[]> {
return this.http.get<any[]>(`${this.apiUrl}/data`);
}
getDataById(id: string): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/data/${id}`);
}
createData(data: any): Observable<any> {
return this.http.post<any>(`${this.apiUrl}/data`, data);
}
updateData(id: string, data: any): Observable<any> {
return this.http.put<any>(`${this.apiUrl}/data/${id}`, data);
}
deleteData(id: string): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/data/${id}`);
}
searchData(params: any): Observable<any[]> {
return this.http.get<any[]>(`${this.apiUrl}/data`, { params });
}
}
使用 RxJS 操作符
import { Observable, of, throwError } from 'rxjs';
import { catchError, map, tap, retry } from 'rxjs/operators';
getData(): Observable<any[]> {
return this.http.get<any[]>(`${this.apiUrl}/data`).pipe(
retry(3),
map(data => data.map(item => ({ ...item, formatted: true }))),
tap(data => console.log('Data loaded:', data)),
catchError(error => {
console.error('Error occurred:', error);
return throwError(() => error);
})
);
}
HTTP 拦截器
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('authToken');
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
});
return next.handle(authReq);
}
}
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
指令 (Directives)
属性指令
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@Input('appHighlight') highlightColor = 'yellow';
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.highlightColor);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string | null) {
this.el.nativeElement.style.backgroundColor = color;
}
}
使用:
<p appHighlight>Hover over me!</p>
<p [appHighlight]="'red'">Custom color!</p>
结构指令
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appUnless]'
})
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
使用:
<div *appUnless="condition">Show unless condition is true</div>
内置指令
条件渲染 (@if)
@if (isLoggedIn) {
<p>Welcome back!</p>
} @else if (isGuest) {
<p>Welcome guest!</p>
} @else {
<p>Please login</p>
}
循环 (@for)
@for (item of items; track item.id; let i = $index) {
<div>{{ i + 1 }}. {{ item.name }}</div>
} @empty {
<div>No items found</div>
}
条件切换 (@switch)
@switch (status) {
@case ('success') {
<p>Success!</p>
}
@case ('error') {
<p>Error!</p>
}
@default {
<p>Unknown status</p>
}
}
管道 (Pipes)
AsyncPipe
自动订阅 observable 或 promise。
@Component({
template: `
@if (data$ | async; as data) {
<p>{{ data.name }}</p>
}
`
})
export class MyComponent {
data$: Observable<Data>;
constructor(private service: DataService) {
this.data$ = service.getData();
}
}
自定义管道
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit: number = 20): string {
if (!value) return '';
return value.length > limit ? value.substring(0, limit) + '...' : value;
}
}
使用:
<p>{{ longText | truncate:50 }}</p>
Signals (Angular 16+)
Signals 提供了新的响应式方式来管理状态。
import { Component, signal, computed, effect } from '@angular/core';
@Component({...})
export class SignalComponent {
count = signal(0);
name = signal('Angular');
doubleCount = computed(() => this.count() * 2);
greeting = computed(() => `Hello, ${this.name()}!`);
constructor() {
effect(() => {
console.log('Count changed:', this.count());
});
}
increment() {
this.count.update(c => c + 1);
}
setName(newName: string) {
this.name.set(newName);
}
}
测试
组件测试
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent]
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display title', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello Angular');
});
});
HTTP 测试
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('ApiService', () => {
let service: ApiService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ApiService]
});
service = TestBed.inject(ApiService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should fetch data', () => {
const mockData = [{ id: 1, name: 'Test' }];
service.getData().subscribe(data => {
expect(data).toEqual(mockData);
});
const req = httpMock.expectOne('https://api.example.com/data');
expect(req.request.method).toBe('GET');
req.flush(mockData);
});
});
最佳实践
- 使用独立组件: 优先使用 standalone 组件,减少 NgModules 的复杂性
- 类型安全: 充分利用 TypeScript 的类型系统
- 变更检测策略: 对于不需要频繁变更检测的组件,使用
OnPush 策略
- 使用 Signals: 优先使用 Signals 管理状态,而不是传统的可观察对象
- 模块化服务: 将服务按功能域分组,使用 providedIn 限制作用域
- 异步处理: 始终正确处理 Observables 和 Promises 的取消订阅
- 响应式表单: 对于复杂表单,使用响应式表单而不是模板驱动表单
- 单元测试: 为组件、服务和管道编写单元测试
- 性能优化: 使用 trackBy 优化 *ngFor,避免不必要的 DOM 操作
- 错误处理: 在 HTTP 请求和异步操作中添加适当的错误处理
常见问题
如何处理异步数据
使用 AsyncPipe 或在组件中订阅:
data$ = this.http.get('/api/data');
private destroy$ = new Subject<void>();
ngOnInit() {
this.http.get('/api/data')
.pipe(takeUntil(this.destroy$))
.subscribe(data => {
this.data = data;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
如何优化性能
- 使用
ChangeDetectionStrategy.OnPush
- 使用 trackBy 优化列表渲染
- 使用 OnPush + Signals
- 避免在模板中使用复杂表达式
- 使用虚拟滚动处理大型列表
如何调试 Angular 应用
- 使用 Angular DevTools 浏览器扩展
- 在组件中添加 console.log
- 使用 Angular 的内置调试工具
- 查看 Chrome DevTools 的 Performance 面板
常用模块和包
import { Component, Directive, Pipe, Injectable, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule, Routes } from '@angular/router';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Observable, of, Subject } from 'rxjs';
import { map, filter, switchMap, tap, catchError, retry } from 'rxjs/operators';
参考资源