i.active).length > 0 && user.role === 'admin'">`
// ✅ 提取为 computed
filteredItems = computed(() => this.items().filter(i => i.active));
shouldShow = computed(() => this.filteredItems().length > 0 && this.user().role === 'admin');
template: `@if (shouldShow()) {
...
}`
```
### 原生绑定优于 NgClass / NgStyle
```typescript
// ❌ NgClass/NgStyle——额外指令开销
template: `
`
// ✅ 原生 class/style 绑定——性能更好
template: `
`
```
### 模板专用成员标记 protected
```typescript
// ❂ 模板专用方法暴露为 public
export class UserProfile {
formatName(name: string) { return name.trim(); }
}
// ✅ 模板专用成员用 protected
export class UserProfile {
protected formatName(name: string) { return name.trim(); }
}
```
### Angular 管理的属性标记 readonly
```typescript
// ❌ input/output/model 可被意外覆盖
userId = input();
userSaved = output();
// ✅ readonly 防止意外赋值
readonly userId = input();
readonly userSaved = output();
readonly userName = model();
```
### 命名规范:操作名而非事件名
```typescript
// ❌ 以事件命名
template: ``
// ✅ 以操作命名
template: ``
```
---
## 性能优化
### effect 是最后手段——优先 computed
```typescript
// ❌ effect 用于状态同步——触发额外 CD,可能无限循环
effect(() => {
this.filteredItems.set(this.items().filter(i => i.active));
});
// ✅ computed——惰性计算,无副作用,无额外 CD
filteredItems = computed(() => this.items().filter(i => i.active));
```
### afterRenderEffect 分离读写阶段
```typescript
// ❌ 无阶段指定 = mixedReadWrite = 额外 DOM 回流
afterRenderEffect(() => {
const height = el.offsetHeight; // 读
el.style.height = height + 10 + 'px'; // 写
});
// ✅ 分离阶段减少回流
afterRenderEffect({
earlyRead: () => el.offsetHeight,
write: (height) => { el.style.height = height() + 10 + 'px'; },
read: () => verifyLayout(),
});
```
### inject() 优于构造函数注入
```typescript
// ❌ 构造函数注入——多依赖时难以阅读
export class UserService {
constructor(
private http: HttpClient,
private router: Router,
private auth: AuthService,
) {}
}
// ✅ inject()——更好的类型推断和可读性
export class UserService {
private http = inject(HttpClient);
private router = inject(Router);
private auth = inject(AuthService);
}
```
---
---
## 测试
### 组件测试(TestBed)
```typescript
// ✅ 独立组件测试
@Component({
standalone: true,
template: ``,
})
export class CounterComponent {
count = signal(0);
increment() { this.count.update(c => c + 1); }
}
describe('CounterComponent', () => {
let fixture: ComponentFixture;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent],
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
fixture.detectChanges();
});
it('should increment on click', () => {
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(button.textContent.trim()).toBe('1');
});
});
```
### 服务测试(依赖注入 Mock)
```typescript
// ✅ 使用 TestBed.inject + provide 覆盖
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getUser(id: number) {
return this.http.get(`/api/users/${id}`);
}
}
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting()],
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('should fetch user', () => {
const mockUser = { id: 1, name: 'Alice' };
service.getUser(1).subscribe(user => {
expect(user).toEqual(mockUser);
});
const req = httpMock.expectOne('/api/users/1');
expect(req.request.method).toBe('GET');
req.flush(mockUser);
});
});
```
### 集成测试策略
```typescript
// ❌ 过度 Mock——测试的是 Mock 而非真实行为
provideHttpClient: () => ({
get: jasmine.createSpy().and.returnValue(of(mockData)),
}),
// ✅ 使用 HttpTestingController 验证真实 HTTP 交互
TestBed.configureTestingModule({
providers: [
provideHttpClient(),
provideHttpClientTesting(),
],
});
// ✅ 浅渲染:只测试组件本身,Mock 子组件
describe('UserProfile', () => {
it('should pass user to child', () => {
const fixture = TestBed.createComponent(UserProfile);
fixture.componentRef.setInput('user', testUser);
fixture.detectChanges();
const child = fixture.debugElement.query(By.directive(UserAvatar));
expect(child.componentInstance.user()).toEqual(testUser);
});
});
```
---
## 路由守卫
### AuthGuard / CanActivate
```typescript
// ✅ 函数式路由守卫(Angular 15+ 推荐)
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) return true;
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url },
});
};
// 使用
export const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard],
},
];
```
### 延迟加载路由守卫
```typescript
// ✅ 守卫在路由加载时才被解析
// auth.guard.ts
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.isAuthenticated();
};
// routes.ts
export const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/routes').then(m => m.routes),
canActivate: [authGuard],
},
];
```
### 参数化路由守卫
```typescript
// ✅ 带角色参数的守卫
export function roleGuard(allowedRoles: string[]): CanActivateFn {
return (route, state) => {
const auth = inject(AuthService);
const user = auth.currentUser();
return user ? allowedRoles.includes(user.role) : false;
};
}
// 使用
{
path: 'admin',
component: AdminComponent,
canActivate: [roleGuard(['admin', 'superadmin'])],
}
```
### CanDeactivate 守卫
```typescript
// ✅ 防止未保存修改的导航离开
export const unsavedChangesGuard: CanDeactivateFn = (
component
) => {
if (component.hasUnsavedChanges()) {
return confirm('You have unsaved changes. Leave anyway?');
}
return true;
};
```
---
## 依赖注入模式
### InjectionToken 使用
```typescript
// ❌ 使用字符串 token——易冲突且无类型安全
providers: [{ provide: 'API_URL', useValue: 'https://api.example.com' }]
// ✅ InjectionToken 提供类型安全
export const API_URL = new InjectionToken('API_URL');
providers: [{ provide: API_URL, useValue: 'https://api.example.com' }]
// 使用
private apiUrl = inject(API_URL);
```
### 多级提供者
```typescript
// ✅ 不同注入层级
// 根级——全局单例
@Injectable({ providedIn: 'root' })
export class GlobalService {}
// 组件级——每个组件实例独立
@Component({
providers: [LocalService],
})
export class MyComponent {
private local = inject(LocalService);
}
// 路由级——路由及其子路由共享
{
path: 'checkout',
providers: [CheckoutService],
children: [/* ... */],
}
```
### 工厂提供者
```typescript
// ✅ 根据条件动态提供不同实现
export const themeProvider: FactoryProvider = {
provide: ThemeService,
useFactory: () => {
const platform = inject(PLATFORM_ID);
if (isPlatformServer(platform)) {
return new ServerThemeService();
}
return new BrowserThemeService();
},
};
// ✅ 使用环境变量配置
export const apiConfigProvider: FactoryProvider = {
provide: ApiConfig,
useFactory: () => {
const env = inject(ENVIRONMENT);
return env.production
? new ProductionApiConfig()
: new DevelopmentApiConfig();
},
};
```
---
## HttpInterceptor
### 认证 Token 拦截器
```typescript
// ✅ 函数式拦截器——自动附加 Auth Token
export function authInterceptor(
req: HttpRequest,
next: HttpHandlerFn
): Observable> {
const token = inject(AuthService).token();
if (!token) return next(req);
return next(req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
}));
}
// 注册
provideHttpClient(withInterceptors([authInterceptor]))
```
### 错误处理拦截器
```typescript
// ✅ 函数式拦截器——统一错误处理
export function errorInterceptor(
req: HttpRequest,
next: HttpHandlerFn
): Observable> {
const router = inject(Router);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
router.navigate(['/login']);
}
if (error.status === 500) {
console.error('Server error:', error);
}
return throwError(() => error);
})
);
}
```
### 请求/响应转换
```typescript
// ✅ 函数式拦截器——自动 camelCase ↔ snake_case
export function transformInterceptor(
req: HttpRequest,
next: HttpHandlerFn
): Observable> {
const transformedBody = req.body ? toSnakeCase(req.body) : null;
const transformedReq = req.clone({ body: transformedBody });
return next(transformedReq).pipe(
map(event => {
if (event instanceof HttpResponse) {
return event.clone({ body: toCamelCase(event.body) });
}
return event;
})
);
}
```
### 拦截器顺序
```typescript
// ✅ 拦截器按注册顺序执行
// 请求:A → B → C → 后端
// 响应:后端 → C → B → A
provideHttpClient(
withInterceptors([
authInterceptor,
loggingInterceptor,
errorInterceptor,
])
)
```
## Review Checklist
### Signals 与变更检测
- [ ] Signal + OnPush 用于模板状态(非可变对象)
- [ ] `@Input()` 对象通过新引用更新(非变异)
- [ ] 派生状态用 `computed()`,不用 `effect()`
- [ ] `effect()` 中 Signal 读取在 `await` 之前
- [ ] `effect()` 只用于 DOM 操作、日志、外部源订阅
### Standalone 组件
- [ ] 无 `standalone: false`(Angular 19+)
- [ ] 组件通过 `imports` 数组导入依赖
- [ ] 无不必要的 `@NgModule`
### RxJS
- [ ] `.subscribe()` 配 `takeUntilDestroyed` 或 `async` pipe
- [ ] 优先 `toSignal` 而非 `AsyncPipe`
- [ ] 无重复 `toSignal` 调用
### Zoneless
- [ ] 模板状态通过 Signal 管理(非普通属性)
- [ ] 无 `NgZone.onStable` / `NgZone.onMicrotaskEmpty`
- [ ] Reactive Forms 变异后有 `markForCheck()`
### 模板
- [ ] 复杂逻辑提取为 `computed` Signal
- [ ] 使用原生 `[class]`/`[style]` 而非 `NgClass`/`NgStyle`
- [ ] 模板专用成员标记 `protected`
- [ ] `input`/`output`/`model` 属性标记 `readonly`
- [ ] 事件处理器以操作命名(`saveData` 而非 `handleClick`)
### 性能
- [ ] `effect()` 不用于状态同步
- [ ] `afterRenderEffect` 分离读写阶段
- [ ] `inject()` 用于依赖注入