@pilllesss/yorn 1.0.182 → 1.0.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,768 @@
1
+ # Angular Code Review Guide
2
+
3
+ > Angular 17+ 代码审查指南,覆盖 Signals、Standalone 组件、RxJS 反模式、Zoneless 变更检测、模板最佳实践及性能优化等核心主题。
4
+
5
+ ## 目录
6
+
7
+ - [Signals 与变更检测](#signals-与变更检测)
8
+ - [Standalone 组件迁移](#standalone-组件迁移)
9
+ - [RxJS 反模式](#rxjs-反模式)
10
+ - [Zoneless 变更检测](#zoneless-变更检测)
11
+ - [模板最佳实践](#模板最佳实践)
12
+ - [性能优化](#性能优化)
13
+ - [测试](#测试)
14
+ - [路由守卫](#路由守卫)
15
+ - [依赖注入模式](#依赖注入模式)
16
+ - [HttpInterceptor](#httpinterceptor)
17
+ - [Review Checklist](#review-checklist)
18
+
19
+ ---
20
+
21
+ ## Signals 与变更检测
22
+
23
+ ### Signal + OnPush 自动触发变更检测
24
+
25
+ ```typescript
26
+ // ❌ 可变状态 + OnPush = 界面不更新
27
+ @Component({
28
+ changeDetection: ChangeDetectionStrategy.OnPush,
29
+ template: `<p>{{ data.name }}</p>`,
30
+ })
31
+ export class UserProfile {
32
+ data = { name: 'Alice' };
33
+ changeName() { this.data.name = 'Bob'; } // UI 不会更新!
34
+ }
35
+
36
+ // ✅ Signal + OnPush = 自动变更检测
37
+ @Component({
38
+ changeDetection: ChangeDetectionStrategy.OnPush,
39
+ template: `<p>{{ name() }}</p>`,
40
+ })
41
+ export class UserProfile {
42
+ name = signal('Alice');
43
+ changeName() { this.name.set('Bob'); } // 自动触发 CD
44
+ }
45
+ ```
46
+
47
+ ### @Input() 对象变异不会被 OnPush 检测
48
+
49
+ ```typescript
50
+ // ❌ 变异 Input 对象——引用不变,OnPush 不检测
51
+ @Input() config!: Config;
52
+ updateConfig() { this.config.theme = 'dark'; }
53
+
54
+ // ✅ 创建新引用
55
+ updateConfig() { this.config = { ...this.config, theme: 'dark' }; }
56
+ ```
57
+
58
+ ### computed() 用于派生状态
59
+
60
+ ```typescript
61
+ // ❌ effect 用于同步状态——反模式,可能触发额外 CD 周期
62
+ export class CartComponent {
63
+ total = signal(0);
64
+ discounted = signal(0);
65
+
66
+ constructor() {
67
+ effect(() => this.discounted.set(this.total() * 0.9));
68
+ }
69
+ }
70
+
71
+ // ✅ computed 用于派生状态——惰性计算,无副作用
72
+ export class CartComponent {
73
+ total = signal(0);
74
+ discounted = computed(() => this.total() * 0.9);
75
+ }
76
+ ```
77
+
78
+ ### effect() 中 Signal 读取在 await 后不会被追踪
79
+
80
+ ```typescript
81
+ // ❌ await 之后读取 Signal——依赖未被追踪
82
+ effect(async () => {
83
+ const data = await fetchUserData();
84
+ console.log(`Theme: ${theme()}`); // theme() 未被追踪!
85
+ });
86
+
87
+ // ✅ 在 await 之前同步读取
88
+ effect(async () => {
89
+ const currentTheme = theme(); // 同步读取,被追踪
90
+ const data = await fetchUserData();
91
+ console.log(`Theme: ${currentTheme}`);
92
+ });
93
+ ```
94
+
95
+ ### effect 只在特定场景使用
96
+
97
+ ```typescript
98
+ // ❌ 用 effect 同步两个 Signal——永远用 computed
99
+ effect(() => { this.filtered.set(this.items().filter(i => i.active)); });
100
+
101
+ // ✅ effect 的合理场景:DOM 操作、分析日志、订阅外部源
102
+ effect(() => {
103
+ const canvas = this.canvasRef.nativeElement;
104
+ const ctx = canvas.getContext('2d');
105
+ ctx.fillStyle = this.color();
106
+ ctx.fillRect(0, 0, this.size(), this.size());
107
+ });
108
+
109
+ // 💡 "There are no situations where effect is good,
110
+ // only situations where it is appropriate."
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Standalone 组件迁移
116
+
117
+ ### Angular 19+ standalone 是默认值
118
+
119
+ ```typescript
120
+ // ❌ Legacy NgModule 组件
121
+ @Component({
122
+ selector: 'old-component',
123
+ standalone: false,
124
+ })
125
+ export class OldComponent {}
126
+
127
+ // ✅ 现代 Standalone 组件(Angular 19+ standalone 是默认值)
128
+ @Component({
129
+ selector: 'user-profile',
130
+ imports: [ProfilePhoto, RouterLink],
131
+ template: `<profile-photo /><a routerLink="/edit">Edit</a>`,
132
+ })
133
+ export class UserProfile {}
134
+ ```
135
+
136
+ ### 审查标记
137
+
138
+ ```typescript
139
+ // ⚠️ 需要迁移的信号:
140
+ // 1. standalone: false
141
+ // 2. @NgModule declarations
142
+ // 3. 组件通过 NgModule 而非直接 import
143
+
144
+ // ✅ 迁移路径:
145
+ // 1. 删除 standalone: false
146
+ // 2. 将依赖添加到组件的 imports 数组
147
+ // 3. 如果不再有 declarations,删除 NgModule
148
+ ```
149
+
150
+ ---
151
+
152
+ ## RxJS 反模式
153
+
154
+ ### subscribe() 必须配 takeUntilDestroyed
155
+
156
+ ```typescript
157
+ // ❌ 裸 subscribe——内存泄漏!组件销毁后仍继续接收数据
158
+ @Component({ /* ... */ })
159
+ export class UserProfile implements OnInit {
160
+ ngOnInit() {
161
+ this.data$.subscribe(data => this.processData(data));
162
+ }
163
+ }
164
+
165
+ // ✅ takeUntilDestroyed——自动在组件销毁时取消(需在构造函数或注入上下文中调用)
166
+ @Component({ /* ... */ })
167
+ export class UserProfile {
168
+ constructor() {
169
+ this.data$.pipe(takeUntilDestroyed()).subscribe(data => {
170
+ this.processData(data);
171
+ });
172
+ }
173
+ }
174
+
175
+ // ✅ 在构造函数外使用——传入 DestroyRef
176
+ @Component({ /* ... */ })
177
+ export class UserProfile {
178
+ private destroyRef = inject(DestroyRef);
179
+
180
+ startListening() {
181
+ this.data$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(/* ... */);
182
+ }
183
+ }
184
+ ```
185
+
186
+ ### toSignal 优于 AsyncPipe
187
+
188
+ ```typescript
189
+ // ❌ AsyncPipe——需要导入,模板中有 | async
190
+ @Component({
191
+ imports: [AsyncPipe],
192
+ template: `{{ data$ | async }}`,
193
+ })
194
+
195
+ // ✅ toSignal——自动取消订阅,可在任何地方使用
196
+ export class UserProfile {
197
+ data = toSignal(this.data$, { initialValue: null });
198
+ // 模板直接用 data()
199
+ }
200
+ ```
201
+
202
+ ### 避免重复 toSignal 调用
203
+
204
+ ```typescript
205
+ // ❌ toSignal 每次调用都创建新订阅
206
+ getData() {
207
+ return toSignal(this.http.get('/api/data'));
208
+ }
209
+
210
+ // ✅ 存储结果
211
+ data = toSignal(this.http.get('/api/data'), { initialValue: null });
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Zoneless 变更检测
217
+
218
+ ### 普通属性变异不会被检测(Angular 21+)
219
+
220
+ ```typescript
221
+ // ❌ Zoneless 下普通属性赋值不触发 CD
222
+ export class UserService {
223
+ user: User | null = null;
224
+ loadUser() { this.user = fetchResult; } // 不触发!
225
+ }
226
+
227
+ // ✅ Signal 自动触发 CD
228
+ export class UserService {
229
+ private _user = signal<User | null>(null);
230
+ readonly user = this._user.asReadonly();
231
+ loadUser() { this._user.set(fetchResult); }
232
+ }
233
+ ```
234
+
235
+ ### NgZone API 在 Zoneless 中失效
236
+
237
+ ```typescript
238
+ // ❌ NgZone.onStable 在 zoneless 中永远不会触发
239
+ ngZone.onStable.subscribe(() => { /* 永远不触发 */ });
240
+
241
+ // ✅ 使用 afterNextRender
242
+ afterNextRender({ write: () => { /* CD 之后执行 */ } });
243
+ ```
244
+
245
+ ### Reactive Forms 变异需要 markForCheck
246
+
247
+ ```typescript
248
+ // ❌ Reactive Forms 的 setValue/patchValue 在 zoneless 中不自动调度 CD
249
+ this.form.patchValue({ name: 'Alice' }); // UI 可能不更新
250
+
251
+ // ✅ 手动标记或通过 Signal 反映
252
+ this.form.patchValue({ name: 'Alice' });
253
+ this.cdr.markForCheck();
254
+ ```
255
+
256
+ ### Zoneless 下有效的 CD 触发器
257
+
258
+ | 触发器 | 说明 |
259
+ |--------|------|
260
+ | `signal.set()` / `.update()` | Signal 更新自动触发 |
261
+ | `ChangeDetectorRef.markForCheck()` | 手动标记 |
262
+ | `ComponentRef.setInput()` | 输入绑定 |
263
+ | 模板事件监听器回调 | 用户交互 |
264
+
265
+ ---
266
+
267
+ ## 模板最佳实践
268
+
269
+ ### 复杂逻辑提取为 computed Signal
270
+
271
+ ```typescript
272
+ // ❌ 模板中复杂表达式
273
+ template: `<div *ngIf="items.filter(i => i.active).length > 0 && user.role === 'admin'">`
274
+
275
+ // ✅ 提取为 computed
276
+ filteredItems = computed(() => this.items().filter(i => i.active));
277
+ shouldShow = computed(() => this.filteredItems().length > 0 && this.user().role === 'admin');
278
+ template: `@if (shouldShow()) { <div>...</div> }`
279
+ ```
280
+
281
+ ### 原生绑定优于 NgClass / NgStyle
282
+
283
+ ```typescript
284
+ // ❌ NgClass/NgStyle——额外指令开销
285
+ template: `<div [ngClass]="{active: isActive}" [ngStyle]="{'color': textColor}">`
286
+
287
+ // ✅ 原生 class/style 绑定——性能更好
288
+ template: `<div [class.active]="isActive" [style.color]="textColor">`
289
+ ```
290
+
291
+ ### 模板专用成员标记 protected
292
+
293
+ ```typescript
294
+ // ❂ 模板专用方法暴露为 public
295
+ export class UserProfile {
296
+ formatName(name: string) { return name.trim(); }
297
+ }
298
+
299
+ // ✅ 模板专用成员用 protected
300
+ export class UserProfile {
301
+ protected formatName(name: string) { return name.trim(); }
302
+ }
303
+ ```
304
+
305
+ ### Angular 管理的属性标记 readonly
306
+
307
+ ```typescript
308
+ // ❌ input/output/model 可被意外覆盖
309
+ userId = input<string>();
310
+ userSaved = output<void>();
311
+
312
+ // ✅ readonly 防止意外赋值
313
+ readonly userId = input<string>();
314
+ readonly userSaved = output<void>();
315
+ readonly userName = model<string>();
316
+ ```
317
+
318
+ ### 命名规范:操作名而非事件名
319
+
320
+ ```typescript
321
+ // ❌ 以事件命名
322
+ template: `<button (click)="handleClick()">Save</button>`
323
+
324
+ // ✅ 以操作命名
325
+ template: `<button (click)="saveUserData()">Save</button>`
326
+ ```
327
+
328
+ ---
329
+
330
+ ## 性能优化
331
+
332
+ ### effect 是最后手段——优先 computed
333
+
334
+ ```typescript
335
+ // ❌ effect 用于状态同步——触发额外 CD,可能无限循环
336
+ effect(() => {
337
+ this.filteredItems.set(this.items().filter(i => i.active));
338
+ });
339
+
340
+ // ✅ computed——惰性计算,无副作用,无额外 CD
341
+ filteredItems = computed(() => this.items().filter(i => i.active));
342
+ ```
343
+
344
+ ### afterRenderEffect 分离读写阶段
345
+
346
+ ```typescript
347
+ // ❌ 无阶段指定 = mixedReadWrite = 额外 DOM 回流
348
+ afterRenderEffect(() => {
349
+ const height = el.offsetHeight; // 读
350
+ el.style.height = height + 10 + 'px'; // 写
351
+ });
352
+
353
+ // ✅ 分离阶段减少回流
354
+ afterRenderEffect({
355
+ earlyRead: () => el.offsetHeight,
356
+ write: (height) => { el.style.height = height() + 10 + 'px'; },
357
+ read: () => verifyLayout(),
358
+ });
359
+ ```
360
+
361
+ ### inject() 优于构造函数注入
362
+
363
+ ```typescript
364
+ // ❌ 构造函数注入——多依赖时难以阅读
365
+ export class UserService {
366
+ constructor(
367
+ private http: HttpClient,
368
+ private router: Router,
369
+ private auth: AuthService,
370
+ ) {}
371
+ }
372
+
373
+ // ✅ inject()——更好的类型推断和可读性
374
+ export class UserService {
375
+ private http = inject(HttpClient);
376
+ private router = inject(Router);
377
+ private auth = inject(AuthService);
378
+ }
379
+ ```
380
+
381
+ ---
382
+
383
+ ---
384
+
385
+ ## 测试
386
+
387
+ ### 组件测试(TestBed)
388
+
389
+ ```typescript
390
+ // ✅ 独立组件测试
391
+ @Component({
392
+ standalone: true,
393
+ template: `<button (click)="increment()">{{ count() }}</button>`,
394
+ })
395
+ export class CounterComponent {
396
+ count = signal(0);
397
+ increment() { this.count.update(c => c + 1); }
398
+ }
399
+
400
+ describe('CounterComponent', () => {
401
+ let fixture: ComponentFixture<CounterComponent>;
402
+
403
+ beforeEach(async () => {
404
+ await TestBed.configureTestingModule({
405
+ imports: [CounterComponent],
406
+ }).compileComponents();
407
+
408
+ fixture = TestBed.createComponent(CounterComponent);
409
+ fixture.detectChanges();
410
+ });
411
+
412
+ it('should increment on click', () => {
413
+ const button = fixture.nativeElement.querySelector('button');
414
+ button.click();
415
+ fixture.detectChanges();
416
+ expect(button.textContent.trim()).toBe('1');
417
+ });
418
+ });
419
+ ```
420
+
421
+ ### 服务测试(依赖注入 Mock)
422
+
423
+ ```typescript
424
+ // ✅ 使用 TestBed.inject + provide 覆盖
425
+ @Injectable({ providedIn: 'root' })
426
+ export class UserService {
427
+ private http = inject(HttpClient);
428
+ getUser(id: number) {
429
+ return this.http.get<User>(`/api/users/${id}`);
430
+ }
431
+ }
432
+
433
+ describe('UserService', () => {
434
+ let service: UserService;
435
+ let httpMock: HttpTestingController;
436
+
437
+ beforeEach(() => {
438
+ TestBed.configureTestingModule({
439
+ providers: [provideHttpClient(), provideHttpClientTesting()],
440
+ });
441
+ service = TestBed.inject(UserService);
442
+ httpMock = TestBed.inject(HttpTestingController);
443
+ });
444
+
445
+ afterEach(() => httpMock.verify());
446
+
447
+ it('should fetch user', () => {
448
+ const mockUser = { id: 1, name: 'Alice' };
449
+
450
+ service.getUser(1).subscribe(user => {
451
+ expect(user).toEqual(mockUser);
452
+ });
453
+
454
+ const req = httpMock.expectOne('/api/users/1');
455
+ expect(req.request.method).toBe('GET');
456
+ req.flush(mockUser);
457
+ });
458
+ });
459
+ ```
460
+
461
+ ### 集成测试策略
462
+
463
+ ```typescript
464
+ // ❌ 过度 Mock——测试的是 Mock 而非真实行为
465
+ provideHttpClient: () => ({
466
+ get: jasmine.createSpy().and.returnValue(of(mockData)),
467
+ }),
468
+
469
+ // ✅ 使用 HttpTestingController 验证真实 HTTP 交互
470
+ TestBed.configureTestingModule({
471
+ providers: [
472
+ provideHttpClient(),
473
+ provideHttpClientTesting(),
474
+ ],
475
+ });
476
+
477
+ // ✅ 浅渲染:只测试组件本身,Mock 子组件
478
+ describe('UserProfile', () => {
479
+ it('should pass user to child', () => {
480
+ const fixture = TestBed.createComponent(UserProfile);
481
+ fixture.componentRef.setInput('user', testUser);
482
+ fixture.detectChanges();
483
+
484
+ const child = fixture.debugElement.query(By.directive(UserAvatar));
485
+ expect(child.componentInstance.user()).toEqual(testUser);
486
+ });
487
+ });
488
+ ```
489
+
490
+ ---
491
+
492
+ ## 路由守卫
493
+
494
+ ### AuthGuard / CanActivate
495
+
496
+ ```typescript
497
+ // ✅ 函数式路由守卫(Angular 15+ 推荐)
498
+ export const authGuard: CanActivateFn = (route, state) => {
499
+ const auth = inject(AuthService);
500
+ const router = inject(Router);
501
+
502
+ if (auth.isAuthenticated()) return true;
503
+
504
+ return router.createUrlTree(['/login'], {
505
+ queryParams: { returnUrl: state.url },
506
+ });
507
+ };
508
+
509
+ // 使用
510
+ export const routes: Routes = [
511
+ {
512
+ path: 'dashboard',
513
+ component: DashboardComponent,
514
+ canActivate: [authGuard],
515
+ },
516
+ ];
517
+ ```
518
+
519
+ ### 延迟加载路由守卫
520
+
521
+ ```typescript
522
+ // ✅ 守卫在路由加载时才被解析
523
+ // auth.guard.ts
524
+ export const authGuard: CanActivateFn = () => {
525
+ const auth = inject(AuthService);
526
+ return auth.isAuthenticated();
527
+ };
528
+
529
+ // routes.ts
530
+ export const routes: Routes = [
531
+ {
532
+ path: 'admin',
533
+ loadChildren: () => import('./admin/routes').then(m => m.routes),
534
+ canActivate: [authGuard],
535
+ },
536
+ ];
537
+ ```
538
+
539
+ ### 参数化路由守卫
540
+
541
+ ```typescript
542
+ // ✅ 带角色参数的守卫
543
+ export function roleGuard(allowedRoles: string[]): CanActivateFn {
544
+ return (route, state) => {
545
+ const auth = inject(AuthService);
546
+ const user = auth.currentUser();
547
+ return user ? allowedRoles.includes(user.role) : false;
548
+ };
549
+ }
550
+
551
+ // 使用
552
+ {
553
+ path: 'admin',
554
+ component: AdminComponent,
555
+ canActivate: [roleGuard(['admin', 'superadmin'])],
556
+ }
557
+ ```
558
+
559
+ ### CanDeactivate 守卫
560
+
561
+ ```typescript
562
+ // ✅ 防止未保存修改的导航离开
563
+ export const unsavedChangesGuard: CanDeactivateFn<EditFormComponent> = (
564
+ component
565
+ ) => {
566
+ if (component.hasUnsavedChanges()) {
567
+ return confirm('You have unsaved changes. Leave anyway?');
568
+ }
569
+ return true;
570
+ };
571
+ ```
572
+
573
+ ---
574
+
575
+ ## 依赖注入模式
576
+
577
+ ### InjectionToken 使用
578
+
579
+ ```typescript
580
+ // ❌ 使用字符串 token——易冲突且无类型安全
581
+ providers: [{ provide: 'API_URL', useValue: 'https://api.example.com' }]
582
+
583
+ // ✅ InjectionToken 提供类型安全
584
+ export const API_URL = new InjectionToken<string>('API_URL');
585
+
586
+ providers: [{ provide: API_URL, useValue: 'https://api.example.com' }]
587
+
588
+ // 使用
589
+ private apiUrl = inject(API_URL);
590
+ ```
591
+
592
+ ### 多级提供者
593
+
594
+ ```typescript
595
+ // ✅ 不同注入层级
596
+ // 根级——全局单例
597
+ @Injectable({ providedIn: 'root' })
598
+ export class GlobalService {}
599
+
600
+ // 组件级——每个组件实例独立
601
+ @Component({
602
+ providers: [LocalService],
603
+ })
604
+ export class MyComponent {
605
+ private local = inject(LocalService);
606
+ }
607
+
608
+ // 路由级——路由及其子路由共享
609
+ {
610
+ path: 'checkout',
611
+ providers: [CheckoutService],
612
+ children: [/* ... */],
613
+ }
614
+ ```
615
+
616
+ ### 工厂提供者
617
+
618
+ ```typescript
619
+ // ✅ 根据条件动态提供不同实现
620
+ export const themeProvider: FactoryProvider = {
621
+ provide: ThemeService,
622
+ useFactory: () => {
623
+ const platform = inject(PLATFORM_ID);
624
+ if (isPlatformServer(platform)) {
625
+ return new ServerThemeService();
626
+ }
627
+ return new BrowserThemeService();
628
+ },
629
+ };
630
+
631
+ // ✅ 使用环境变量配置
632
+ export const apiConfigProvider: FactoryProvider = {
633
+ provide: ApiConfig,
634
+ useFactory: () => {
635
+ const env = inject(ENVIRONMENT);
636
+ return env.production
637
+ ? new ProductionApiConfig()
638
+ : new DevelopmentApiConfig();
639
+ },
640
+ };
641
+ ```
642
+
643
+ ---
644
+
645
+ ## HttpInterceptor
646
+
647
+ ### 认证 Token 拦截器
648
+
649
+ ```typescript
650
+ // ✅ 函数式拦截器——自动附加 Auth Token
651
+ export function authInterceptor(
652
+ req: HttpRequest<unknown>,
653
+ next: HttpHandlerFn
654
+ ): Observable<HttpEvent<unknown>> {
655
+ const token = inject(AuthService).token();
656
+ if (!token) return next(req);
657
+
658
+ return next(req.clone({
659
+ setHeaders: { Authorization: `Bearer ${token}` },
660
+ }));
661
+ }
662
+
663
+ // 注册
664
+ provideHttpClient(withInterceptors([authInterceptor]))
665
+ ```
666
+
667
+ ### 错误处理拦截器
668
+
669
+ ```typescript
670
+ // ✅ 函数式拦截器——统一错误处理
671
+ export function errorInterceptor(
672
+ req: HttpRequest<unknown>,
673
+ next: HttpHandlerFn
674
+ ): Observable<HttpEvent<unknown>> {
675
+ const router = inject(Router);
676
+
677
+ return next(req).pipe(
678
+ catchError((error: HttpErrorResponse) => {
679
+ if (error.status === 401) {
680
+ router.navigate(['/login']);
681
+ }
682
+ if (error.status === 500) {
683
+ console.error('Server error:', error);
684
+ }
685
+ return throwError(() => error);
686
+ })
687
+ );
688
+ }
689
+ ```
690
+
691
+ ### 请求/响应转换
692
+
693
+ ```typescript
694
+ // ✅ 函数式拦截器——自动 camelCase ↔ snake_case
695
+ export function transformInterceptor(
696
+ req: HttpRequest<unknown>,
697
+ next: HttpHandlerFn
698
+ ): Observable<HttpEvent<unknown>> {
699
+ const transformedBody = req.body ? toSnakeCase(req.body) : null;
700
+ const transformedReq = req.clone({ body: transformedBody });
701
+
702
+ return next(transformedReq).pipe(
703
+ map(event => {
704
+ if (event instanceof HttpResponse) {
705
+ return event.clone({ body: toCamelCase(event.body) });
706
+ }
707
+ return event;
708
+ })
709
+ );
710
+ }
711
+ ```
712
+
713
+ ### 拦截器顺序
714
+
715
+ ```typescript
716
+ // ✅ 拦截器按注册顺序执行
717
+ // 请求:A → B → C → 后端
718
+ // 响应:后端 → C → B → A
719
+ provideHttpClient(
720
+ withInterceptors([
721
+ authInterceptor,
722
+ loggingInterceptor,
723
+ errorInterceptor,
724
+ ])
725
+ )
726
+ ```
727
+
728
+ ## Review Checklist
729
+
730
+ ### Signals 与变更检测
731
+
732
+ - [ ] Signal + OnPush 用于模板状态(非可变对象)
733
+ - [ ] `@Input()` 对象通过新引用更新(非变异)
734
+ - [ ] 派生状态用 `computed()`,不用 `effect()`
735
+ - [ ] `effect()` 中 Signal 读取在 `await` 之前
736
+ - [ ] `effect()` 只用于 DOM 操作、日志、外部源订阅
737
+
738
+ ### Standalone 组件
739
+
740
+ - [ ] 无 `standalone: false`(Angular 19+)
741
+ - [ ] 组件通过 `imports` 数组导入依赖
742
+ - [ ] 无不必要的 `@NgModule`
743
+
744
+ ### RxJS
745
+
746
+ - [ ] `.subscribe()` 配 `takeUntilDestroyed` 或 `async` pipe
747
+ - [ ] 优先 `toSignal` 而非 `AsyncPipe`
748
+ - [ ] 无重复 `toSignal` 调用
749
+
750
+ ### Zoneless
751
+
752
+ - [ ] 模板状态通过 Signal 管理(非普通属性)
753
+ - [ ] 无 `NgZone.onStable` / `NgZone.onMicrotaskEmpty`
754
+ - [ ] Reactive Forms 变异后有 `markForCheck()`
755
+
756
+ ### 模板
757
+
758
+ - [ ] 复杂逻辑提取为 `computed` Signal
759
+ - [ ] 使用原生 `[class]`/`[style]` 而非 `NgClass`/`NgStyle`
760
+ - [ ] 模板专用成员标记 `protected`
761
+ - [ ] `input`/`output`/`model` 属性标记 `readonly`
762
+ - [ ] 事件处理器以操作命名(`saveData` 而非 `handleClick`)
763
+
764
+ ### 性能
765
+
766
+ - [ ] `effect()` 不用于状态同步
767
+ - [ ] `afterRenderEffect` 分离读写阶段
768
+ - [ ] `inject()` 用于依赖注入