项目文件夹

文件
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

1 行
21 KiB
JSON

{"content": "---\nname: angular\ndescription: Modern Angular (v20+) expert with deep knowledge of Signals, Standalone Components, Zoneless applications, SSR/Hydration, and reactive patterns.\nrisk: safe\nsource: self\ndate_added: '2026-02-27'\n---\n\n# Angular Expert\n\nMaster modern Angular development with Signals, Standalone Components, Zoneless applications, SSR/Hydration, and the latest reactive patterns.\n\n## When to Use This Skill\n\n- Building new Angular applications (v20+)\n- Implementing Signals-based reactive patterns\n- Creating Standalone Components and migrating from NgModules\n- Configuring Zoneless Angular applications\n- Implementing SSR, prerendering, and hydration\n- Optimizing Angular performance\n- Adopting modern Angular patterns and best practices\n\n## Do Not Use This Skill When\n\n- Migrating from AngularJS (1.x) → use `angular-migration` skill\n- Working with legacy Angular apps that cannot upgrade\n- General TypeScript issues → use `typescript-expert` skill\n\n## Instructions\n\n1. Assess the Angular version and project structure\n2. Apply modern patterns (Signals, Standalone, Zoneless)\n3. Implement with proper typing and reactivity\n4. Validate with build and tests\n\n## Safety\n\n- Always test changes in development before production\n- Gradual migration for existing apps (don't big-bang refactor)\n- Keep backward compatibility during transitions\n\n---\n\n## Angular Version Timeline\n\n| Version | Release | Key Features |\n| -------------- | ------- | ------------------------------------------------------ |\n| **Angular 20** | Q2 2025 | Signals stable, Zoneless stable, Incremental hydration |\n| **Angular 21** | Q4 2025 | Signals-first default, Enhanced SSR |\n| **Angular 22** | Q2 2026 | Signal Forms, Selectorless components |\n\n---\n\n## 1. Signals: The New Reactive Primitive\n\nSignals are Angular's fine-grained reactivity system, replacing zone.js-based change detection.\n\n### Core Concepts\n\n```typescript\nimport { signal, computed, effect } from \"@angular/core\";\n\n// Writable signal\nconst count = signal(0);\n\n// Read value\nconsole.log(count()); // 0\n\n// Update value\ncount.set(5); // Direct set\ncount.update((v) => v + 1); // Functional update\n\n// Computed (derived) signal\nconst doubled = computed(() => count() * 2);\n\n// Effect (side effects)\neffect(() => {\n console.log(`Count changed to: ${count()}`);\n});\n```\n\n### Signal-Based Inputs and Outputs\n\n```typescript\nimport { Component, input, output, model } from \"@angular/core\";\n\n@Component({\n selector: \"app-user-card\",\n standalone: true,\n template: `\n <div class=\"card\">\n <h3>{{ name() }}</h3>\n <span>{{ role() }}</span>\n <button (click)=\"select.emit(id())\">Select</button>\n </div>\n `,\n})\nexport class UserCardComponent {\n // Signal inputs (read-only)\n id = input.required<string>();\n name = input.required<string>();\n role = input<string>(\"User\"); // With default\n\n // Output\n select = output<string>();\n\n // Two-way binding (model)\n isSelected = model(false);\n}\n\n// Usage:\n// <app-user-card [id]=\"'123'\" [name]=\"'John'\" [(isSelected)]=\"selected\" />\n```\n\n### Signal Queries (ViewChild/ContentChild)\n\n```typescript\nimport {\n Component,\n viewChild,\n viewChildren,\n contentChild,\n} from \"@angular/core\";\n\n@Component({\n selector: \"app-container\",\n standalone: true,\n template: `\n <input #searchInput />\n <app-item *ngFor=\"let item of items()\" />\n `,\n})\nexport class ContainerComponent {\n // Signal-based queries\n searchInput = viewChild<ElementRef>(\"searchInput\");\n items = viewChildren(ItemComponent);\n projectedContent = contentChild(HeaderDirective);\n\n focusSearch() {\n this.searchInput()?.nativeElement.focus();\n }\n}\n```\n\n### When to Use Signals vs RxJS\n\n| Use Case | Signals | RxJS |\n| ----------------------- | --------------- | -------------------------------- |\n| Local component state | ✅ Preferred | Overkill |\n| Derived/computed values | ✅ `computed()` | `combineLatest` works |\n| Side effects | ✅ `effect()` | `tap` operator |\n| HTTP requests | ❌ | ✅ HttpClient returns Observable |\n| Event streams | ❌ | ✅ `fromEvent`, operators |\n| Complex async flows | ❌ | ✅ `switchMap`, `mergeMap` |\n\n---\n\n## 2. Standalone Components\n\nStandalone components are self-contained and don't require NgModule declarations.\n\n### Creating Standalone Components\n\n```typescript\nimport { Component } from \"@angular/core\";\nimport { CommonModule } from \"@angular/common\";\nimport { RouterLink } from \"@angular/router\";\n\n@Component({\n selector: \"app-header\",\n standalone: true,\n imports: [CommonModule, RouterLink], // Direct imports\n template: `\n <header>\n <a routerLink=\"/\">Home</a>\n <a routerLink=\"/about\">About</a>\n </header>\n `,\n})\nexport class HeaderComponent {}\n```\n\n### Bootstrapping Without NgModule\n\n```typescript\n// main.ts\nimport { bootstrapApplication } from \"@angular/platform-browser\";\nimport { provideRouter } from \"@angular/router\";\nimport { provideHttpClient } from \"@angular/common/http\";\nimport { AppComponent } from \"./app/app.component\";\nimport { routes } from \"./app/app.routes\";\n\nbootstrapApplication(AppComponent, {\n providers: [provideRouter(routes), provideHttpClient()],\n});\n```\n\n### Lazy Loading Standalone Components\n\n```typescript\n// app.routes.ts\nimport { Routes } from \"@angular/router\";\n\nexport const routes: Routes = [\n {\n path: \"dashboard\",\n loadComponent: () =>\n import(\"./dashboard/dashboard.component\").then(\n (m) => m.DashboardComponent,\n ),\n },\n {\n path: \"admin\",\n loadChildren: () =>\n import(\"./admin/admin.routes\").then((m) => m.ADMIN_ROUTES),\n },\n];\n```\n\n---\n\n## 3. Zoneless Angular\n\nZoneless applications don't use zone.js, improving performance and debugging.\n\n### Enabling Zoneless Mode\n\n```typescript\n// main.ts\nimport { bootstrapApplication } from \"@angular/platform-browser\";\nimport { provideZonelessChangeDetection } from \"@angular/core\";\nimport { AppComponent } from \"./app/app.component\";\n\nbootstrapApplication(AppComponent, {\n providers: [provideZonelessChangeDetection()],\n});\n```\n\n### Zoneless Component Patterns\n\n```typescript\nimport { Component, signal, ChangeDetectionStrategy } from \"@angular/core\";\n\n@Component({\n selector: \"app-counter\",\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div>Count: {{ count() }}</div>\n <button (click)=\"increment()\">+</button>\n `,\n})\nexport class CounterComponent {\n count = signal(0);\n\n increment() {\n this.count.update((v) => v + 1);\n // No zone.js needed - Signal triggers change detection\n }\n}\n```\n\n### Key Zoneless Benefits\n\n- **Performance**: No zone.js patches on async APIs\n- **Debugging**: Clean stack traces without zone wrappers\n- **Bundle size**: Smaller without zone.js (~15KB savings)\n- **Interoperability**: Better with Web Components and micro-frontends\n\n---\n\n## 4. Server-Side Rendering & Hydration\n\n### SSR Setup with Angular CLI\n\n```bash\nng add @angular/ssr\n```\n\n### Hydration Configuration\n\n```typescript\n// app.config.ts\nimport { ApplicationConfig } from \"@angular/core\";\nimport {\n provideClientHydration,\n withEventReplay,\n} from \"@angular/platform-browser\";\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideClientHydration(withEventReplay())],\n};\n```\n\n### Incremental Hydration (v20+)\n\n```typescript\nimport { Component } from \"@angular/core\";\n\n@Component({\n selector: \"app-page\",\n standalone: true,\n template: `\n <app-hero />\n\n @defer (hydrate on viewport) {\n <app-comments />\n }\n\n @defer (hydrate on interaction) {\n <app-chat-widget />\n }\n `,\n})\nexport class PageComponent {}\n```\n\n### Hydration Triggers\n\n| Trigger | When to Use |\n| ---------------- | --------------------------------------- |\n| `on idle` | Low-priority, hydrate when browser idle |\n| `on viewport` | Hydrate when element enters viewport |\n| `on interaction` | Hydrate on first user interaction |\n| `on hover` | Hydrate when user hovers |\n| `on timer(ms)` | Hydrate after specified delay |\n\n---\n\n## 5. Modern Routing Patterns\n\n### Functional Route Guards\n\n```typescript\n// auth.guard.ts\nimport { inject } from \"@angular/core\";\nimport { Router, CanActivateFn } from \"@angular/router\";\nimport { AuthService } from \"./auth.service\";\n\nexport const authGuard: CanActivateFn = (route, state) => {\n const auth = inject(AuthService);\n const router = inject(Router);\n\n if (auth.isAuthenticated()) {\n return true;\n }\n\n return router.createUrlTree([\"/login\"], {\n queryParams: { returnUrl: state.url },\n });\n};\n\n// Usage in routes\nexport const routes: Routes = [\n {\n path: \"dashboard\",\n loadComponent: () => import(\"./dashboard.component\"),\n canActivate: [authGuard],\n },\n];\n```\n\n### Route-Level Data Resolvers\n\n```typescript\nimport { inject } from '@angular/core';\nimport { ResolveFn } from '@angular/router';\nimport { UserService } from './user.service';\nimport { User } from './user.model';\n\nexport const userResolver: ResolveFn<User> = (route) => {\n const userService = inject(UserService);\n return userService.getUser(route.paramMap.get('id')!);\n};\n\n// In routes\n{\n path: 'user/:id',\n loadComponent: () => import('./user.component'),\n resolve: { user: userResolver }\n}\n\n// In component\nexport class UserComponent {\n private route = inject(ActivatedRoute);\n user = toSignal(this.route.data.pipe(map(d => d['user'])));\n}\n```\n\n---\n\n## 6. Dependency Injection Patterns\n\n### Modern inject() Function\n\n```typescript\nimport { Component, inject } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { UserService } from './user.service';\n\n@Component({...})\nexport class UserComponent {\n // Modern inject() - no constructor needed\n private http = inject(HttpClient);\n private userService = inject(UserService);\n\n // Works in any injection context\n users = toSignal(this.userService.getUsers());\n}\n```\n\n### Injection Tokens for Configuration\n\n```typescript\nimport { InjectionToken, inject } from \"@angular/core\";\n\n// Define token\nexport const API_BASE_URL = new InjectionToken<string>(\"API_BASE_URL\");\n\n// Provide in config\nbootstrapApplication(AppComponent, {\n providers: [{ provide: API_BASE_URL, useValue: \"https://api.example.com\" }],\n});\n\n// Inject in service\n@Injectable({ providedIn: \"root\" })\nexport class ApiService {\n private baseUrl = inject(API_BASE_URL);\n\n get(endpoint: string) {\n return this.http.get(`${this.baseUrl}/${endpoint}`);\n }\n}\n```\n\n---\n\n## 7. Component Composition & Reusability\n\n### Content Projection (Slots)\n\n```typescript\n@Component({\n selector: 'app-card',\n template: `\n <div class=\"card\">\n <div class=\"header\">\n <!-- Select by attribute -->\n <ng-content select=\"[card-header]\"></ng-content>\n </div>\n <div class=\"body\">\n <!-- Default slot -->\n <ng-content></ng-content>\n </div>\n </div>\n `\n})\nexport class CardComponent {}\n\n// Usage\n<app-card>\n <h3 card-header>Title</h3>\n <p>Body content</p>\n</app-card>\n```\n\n### Host Directives (Composition)\n\n```typescript\n// Reusable behaviors without inheritance\n@Directive({\n standalone: true,\n selector: '[appTooltip]',\n inputs: ['tooltip'] // Signal input alias\n})\nexport class TooltipDirective { ... }\n\n@Component({\n selector: 'app-button',\n standalone: true,\n hostDirectives: [\n {\n directive: TooltipDirective,\n inputs: ['tooltip: title'] // Map input\n }\n ],\n template: `<ng-content />`\n})\nexport class ButtonComponent {}\n```\n\n---\n\n## 8. State Management Patterns\n\n### Signal-Based State Service\n\n```typescript\nimport { Injectable, signal, computed } from \"@angular/core\";\n\ninterface AppState {\n user: User | null;\n theme: \"light\" | \"dark\";\n notifications: Notification[];\n}\n\n@Injectable({ providedIn: \"root\" })\nexport class StateService {\n // Private writable signals\n private _user = signal<User | null>(null);\n private _theme = signal<\"light\" | \"dark\">(\"light\");\n private _notifications = signal<Notification[]>([]);\n\n // Public read-only computed\n readonly user = computed(() => this._user());\n readonly theme = computed(() => this._theme());\n readonly notifications = computed(() => this._notifications());\n readonly unreadCount = computed(\n () => this._notifications().filter((n) => !n.read).length,\n );\n\n // Actions\n setUser(user: User | null) {\n this._user.set(user);\n }\n\n toggleTheme() {\n this._theme.update((t) => (t === \"light\" ? \"dark\" : \"light\"));\n }\n\n addNotification(notification: Notification) {\n this._notifications.update((n) => [...n, notification]);\n }\n}\n```\n\n### Component Store Pattern with Signals\n\n```typescript\nimport { Injectable, signal, computed, inject } from \"@angular/core\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { toSignal } from \"@angular/core/rxjs-interop\";\n\n@Injectable()\nexport class ProductStore {\n private http = inject(HttpClient);\n\n // State\n private _products = signal<Product[]>([]);\n private _loading = signal(false);\n private _filter = signal(\"\");\n\n // Selectors\n readonly products = computed(() => this._products());\n readonly loading = computed(() => this._loading());\n readonly filteredProducts = computed(() => {\n const filter = this._filter().toLowerCase();\n return this._products().filter((p) =>\n p.name.toLowerCase().includes(filter),\n );\n });\n\n // Actions\n loadProducts() {\n this._loading.set(true);\n this.http.get<Product[]>(\"/api/products\").subscribe({\n next: (products) => {\n this._products.set(products);\n this._loading.set(false);\n },\n error: () => this._loading.set(false),\n });\n }\n\n setFilter(filter: string) {\n this._filter.set(filter);\n }\n}\n```\n\n---\n\n## 9. Forms with Signals (Coming in v22+)\n\n### Current Reactive Forms\n\n```typescript\nimport { Component, inject } from \"@angular/core\";\nimport { FormBuilder, Validators, ReactiveFormsModule } from \"@angular/forms\";\n\n@Component({\n selector: \"app-user-form\",\n standalone: true,\n imports: [ReactiveFormsModule],\n template: `\n <form [formGroup]=\"form\" (ngSubmit)=\"onSubmit()\">\n <input formControlName=\"name\" placeholder=\"Name\" />\n <input formControlName=\"email\" type=\"email\" placeholder=\"Email\" />\n <button [disabled]=\"form.invalid\">Submit</button>\n </form>\n `,\n})\nexport class UserFormComponent {\n private fb = inject(FormBuilder);\n\n form = this.fb.group({\n name: [\"\", Validators.required],\n email: [\"\", [Validators.required, Validators.email]],\n });\n\n onSubmit() {\n if (this.form.valid) {\n console.log(this.form.value);\n }\n }\n}\n```\n\n### Signal-Aware Form Patterns (Preview)\n\n```typescript\n// Future Signal Forms API (experimental)\nimport { Component, signal } from '@angular/core';\n\n@Component({...})\nexport class SignalFormComponent {\n name = signal('');\n email = signal('');\n\n // Computed validation\n isValid = computed(() =>\n this.name().length > 0 &&\n this.email().includes('@')\n );\n\n submit() {\n if (this.isValid()) {\n console.log({ name: this.name(), email: this.email() });\n }\n }\n}\n```\n\n---\n\n## 10. Performance Optimization\n\n### Change Detection Strategies\n\n```typescript\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n // Only checks when:\n // 1. Input signal/reference changes\n // 2. Event handler runs\n // 3. Async pipe emits\n // 4. Signal value changes\n})\n```\n\n### Defer Blocks for Lazy Loading\n\n```typescript\n@Component({\n template: `\n <!-- Immediate loading -->\n <app-header />\n\n <!-- Lazy load when visible -->\n @defer (on viewport) {\n <app-heavy-chart />\n } @placeholder {\n <div class=\"skeleton\" />\n } @loading (minimum 200ms) {\n <app-spinner />\n } @error {\n <p>Failed to load chart</p>\n }\n `\n})\n```\n\n### NgOptimizedImage\n\n```typescript\nimport { NgOptimizedImage } from '@angular/common';\n\n@Component({\n imports: [NgOptimizedImage],\n template: `\n <img\n ngSrc=\"hero.jpg\"\n width=\"800\"\n height=\"600\"\n priority\n />\n\n <img\n ngSrc=\"thumbnail.jpg\"\n width=\"200\"\n height=\"150\"\n loading=\"lazy\"\n placeholder=\"blur\"\n />\n `\n})\n```\n\n---\n\n## 11. Testing Modern Angular\n\n### Testing Signal Components\n\n```typescript\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { CounterComponent } from \"./counter.component\";\n\ndescribe(\"CounterComponent\", () => {\n let component: CounterComponent;\n let fixture: ComponentFixture<CounterComponent>;\n\n beforeEach(async () => {\n await TestBed.configureTestingModule({\n imports: [CounterComponent], // Standalone import\n }).compileComponents();\n\n fixture = TestBed.createComponent(CounterComponent);\n component = fixture.componentInstance;\n fixture.detectChanges();\n });\n\n it(\"should increment count\", () => {\n expect(component.count()).toBe(0);\n\n component.increment();\n\n expect(component.count()).toBe(1);\n });\n\n it(\"should update DOM on signal change\", () => {\n component.count.set(5);\n fixture.detectChanges();\n\n const el = fixture.nativeElement.querySelector(\".count\");\n expect(el.textContent).toContain(\"5\");\n });\n});\n```\n\n### Testing with Signal Inputs\n\n```typescript\nimport { ComponentFixture, TestBed } from \"@angular/core/testing\";\nimport { ComponentRef } from \"@angular/core\";\nimport { UserCardComponent } from \"./user-card.component\";\n\ndescribe(\"UserCardComponent\", () => {\n let fixture: ComponentFixture<UserCardComponent>;\n let componentRef: ComponentRef<UserCardComponent>;\n\n beforeEach(async () => {\n await TestBed.configureTestingModule({\n imports: [UserCardComponent],\n }).compileComponents();\n\n fixture = TestBed.createComponent(UserCardComponent);\n componentRef = fixture.componentRef;\n\n // Set signal inputs via setInput\n componentRef.setInput(\"id\", \"123\");\n componentRef.setInput(\"name\", \"John Doe\");\n\n fixture.detectChanges();\n });\n\n it(\"should display user name\", () => {\n const el = fixture.nativeElement.querySelector(\"h3\");\n expect(el.textContent).toContain(\"John Doe\");\n });\n});\n```\n\n---\n\n## Best Practices Summary\n\n| Pattern | ✅ Do | ❌ Don't |\n| -------------------- | ------------------------------ | ------------------------------- |\n| **State** | Use Signals for local state | Overuse RxJS for simple state |\n| **Components** | Standalone with direct imports | Bloated SharedModules |\n| **Change Detection** | OnPush + Signals | Default CD everywhere |\n| **Lazy Loading** | `@defer` and `loadComponent` | Eager load everything |\n| **DI** | `inject()` function | Constructor injection (verbose) |\n| **Inputs** | `input()` signal function | `@Input()` decorator (legacy) |\n| **Zoneless** | Enable for new projects | Force on legacy without testing |\n\n---\n\n## Resources\n\n- [Angular.dev Documentation](https://angular.dev)\n- [Angular Signals Guide](https://angular.dev/guide/signals)\n- [Angular SSR Guide](https://angular.dev/guide/ssr)\n- [Angular Update Guide](https://angular.dev/update-guide)\n- [Angular Blog](https://blog.angular.dev)\n\n---\n\n## Common Troubleshooting\n\n| Issue | Solution |\n| ------------------------------ | --------------------------------------------------- |\n| Signal not updating UI | Ensure `OnPush` + call signal as function `count()` |\n| Hydration mismatch | Check server/client content consistency |\n| Circular dependency | Use `inject()` with `forwardRef` |\n| Zoneless not detecting changes | Trigger via signal updates, not mutations |\n| SSR fetch fails | Use `TransferState` or `withFetch()` |\n"}