协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
angular-patterns【开发与工程】Use this skill when the user says 'Angular DI', 'Angular service', 'Angular interceptor', 'Angular guard', 'Angular RxJS', 'NgRx', 'Angular store', 'Angular resolver', 'Angular pipe', 'Angular directive', 'Observable vs Signal', or when implementing Angular-specific patterns. This skill enforces: HTTP interceptors for cross-cutting concerns, route gu…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Implement Angular-specific patterns: HTTP interceptors for cross-cutting, guards for navigation control, Signals for sync state, RxJS for async streams, Signal Store over NgRx unless complexity demands it.
Exact user phrases: "Angular DI", "Angular service", "Angular interceptor", "Angular guard", "Angular RxJS", "NgRx", "Angular store", "Angular resolver", "Angular pipe", "Angular directive", "Observable vs Signal".
Before activating, verify:
No file output. Produces code examples as text.
Pattern: {name}
Purpose: {one sentence}
Code: {implementation}
Registration: {where to provide/register}
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
Per pattern: 20 lines.
| Approach | Trade-off | When to Use |
|---|---|---|
| Standalone components (default since v17) | No NgModules, simpler | New projects, new features |
| NgModules-based | Legacy, shared declarations | Existing Angular <17 projects |
| Signal Store (manual or @ngrx/signals) | Lighter than NgRx | Small-medium apps, CRUD |
| NgRx (Store + Effects + Entity) | Full Redux pattern | Large apps, complex side effects |
| Component Store (@ngrx/component-store) | Local state management | Feature-scoped state |
| Presentational + Container Components | Separates logic from UI | Complex feature components |
How complex is the state?
├── Simple (component-local) -> signal() or BehaviorSubject
├── Medium (feature-scoped) -> Signal Store or Component Store
└── Complex (cross-feature, many side effects) -> NgRx
Does async data control the flow?
├── Yes (HTTP, WebSocket, events) -> RxJS Observables
├── No (UI state, toggles, selections) -> Signals
└── Both -> RxJS for streams, Signals for sync state
Is this a reusable service with multiple implementations?
├── Yes -> Provide with useClass, useExisting, or useFactory
├── Yes -> Inject with InjectionToken and @Optional()
└── No -> providedIn: 'root' singleton
// Wrong — manual subscribe, easy to forget unsubscribe
this.route.params.subscribe(params => this.id = params['id'])
// Correct — automatic cleanup
this.route.params.pipe(takeUntilDestroyed()).subscribe(params => this.id = params['id'])
Always use takeUntilDestroyed() (Angular 16+) or the async pipe. Never manually manage subscriptions in components.
Interceptors should handle cross-cutting concerns only:
Guards should only decide navigation access. They should not dispatch analytics events, modify state, or call mutation endpoints.
NgRx adds significant boilerplate. For most CRUD apps, a Signal Store or Component Store is sufficient. Reserve NgRx for complex state interactions, multi-source event merging, or when Redux DevTools time-travel debugging is needed.
// Wrong — converting signal to observable unnecessarily
const count$ = toObservable(this.count)
// Correct — use signal directly in template
// {{ count() }}
Use Signals in templates and computed() for sync derivations. Only use toObservable() when integrating with RxJS operators or libraries that expect observables.
takeUntilDestroyedBefore Angular 16, manual subscriptions caused memory leaks. Use takeUntilDestroyed() as the standard pattern. In Angular 16+, it works in any injection context without DestroyRef.
| Aspect | Angular | React |
|---|---|---|
| Architecture | Full framework (DI, router, HTTP, forms) | Library (ecosystem via external packages) |
| Reactivity | Signals + RxJS | Hooks + state management |
| DI | Built-in, hierarchical | Manual via context or props |
| TypeScript | First-class, required | Optional via types |
| Bundle size | Larger (~65KB base) | Smaller (~45KB with react-dom) |
| Learning curve | Steeper (many concepts) | Moderate (few concepts, many choices) |
| Performance | Default OnPush + signals | Manual memoization |
Both are full frameworks, but Angular uses TypeScript natively and RxJS for async, while Vue uses a simpler reactivity model with ref() and reactive(). Vue is easier to learn but Angular scales better for enterprise teams.
Svelte compiles away the framework, producing tiny bundles. Angular has a larger runtime but offers more built-in features (DI, routing, forms, HTTP client). Svelte suits smaller teams; Angular suits large enterprise applications.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
OnPush + Signals is the most performant combination. OnPush skips change detection unless inputs change, events fire, or signals/observables notify. Combined with signal() for local state, this minimizes change detection cycles.
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes'),
},
]
Lazy loading at feature level keeps initial bundle small. Each lazy-loaded feature is a separate chunk loaded on navigation.
Unnecessary subscriptions waste memory. Use takeUntilDestroyed() (Angular 16+), async pipe, or first() for one-shot operations.
Signals are lightweight compared to RxJS Subjects:
signal() and computed() have near-zero overhead| Package | Purpose |
|---|---|
| @angular/core | Framework core (components, DI, signals) |
| @angular/router | Routing and navigation |
| @angular/forms | Template-driven and reactive forms |
| @angular/common/http | HTTP client with interceptors |
| @angular/ssr | Server-side rendering |
| Package | Purpose |
|---|---|
| @ngrx/store | Redux-style state management |
| @ngrx/effects | Side effect management |
| @ngrx/component-store | Local/feature state management |
| @ngrx/signals | Signal-based state management |
| ngxtension | Utilities for Signals and RxJS interop |
ng generate), builds.@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private readonly auth = inject(AuthService)
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const token = this.auth.token()
if (token) {
req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
}
return next.handle(req)
}
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(withInterceptorsFromDi()),
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
}
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService)
const token = auth.token()
if (token) {
req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })
}
return next(req)
}
// providers: [provideHttpClient(withInterceptors([authInterceptor]))]
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService)
const router = inject(Router)
if (auth.isAuthenticated()) return true
return router.parseUrl('/login')
}
export const roleGuard = (requiredRole: string): CanMatchFn => {
return () => {
const auth = inject(AuthService)
return auth.hasRole(requiredRole)
}
}
@Component({...})
export class UserSearchComponent {
readonly searchControl = new FormControl('')
readonly searchResults$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter((term): term is string => term.length >= 2),
switchMap(term => this.userService.search(term)),
catchError(() => of([])),
takeUntilDestroyed(),
)
readonly selectedUser = signal<User | null>(null)
selectUser(user: User) {
this.selectedUser.set(user)
}
}
// Signal Store (preferred for most cases)
@Injectable({ providedIn: 'root' })
export class UserStore {
readonly users = signal<User[]>([])
readonly isLoading = signal(false)
readonly selectedId = signal<string | null>(null)
readonly selectedUser = computed(() =>
this.users().find(u => u.id === this.selectedId()) ?? null
)
async loadUsers() {
this.isLoading.set(true)
try {
const users = await this.userService.getAll()
this.users.set(users)
} finally {
this.isLoading.set(false)
}
}
}
@Directive({
selector: '[appHasPermission]',
standalone: true,
})
export class HasPermissionDirective {
private readonly templateRef = inject(TemplateRef)
private readonly viewContainer = inject(ViewContainerRef)
private readonly auth = inject(AuthService)
@Input() set appHasPermission(permission: string) {
if (this.auth.hasPermission(permission)) {
this.viewContainer.createEmbeddedView(this.templateRef)
} else {
this.viewContainer.clear()
}
}
}
@Pipe({
name: 'truncate',
standalone: true,
pure: true,
})
export class TruncatePipe implements PipeTransform {
transform(value: string, maxLength: number = 100, suffix: string = '...'): string {
if (!value) return ''
return value.length > maxLength ? value.slice(0, maxLength) + suffix : value
}
}
// InjectionToken for configuration
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG')
// useFactory for conditional providers
providers: [
{
provide: LoggerService,
useFactory: () => environment.production
? new ProductionLoggerService()
: new DebugLoggerService(),
},
]
// With Angular Testing Library
import { render, screen } from '@testing-library/angular'
import { UsersComponent } from './users.component'
test('renders user list', async () => {
await render(UsersComponent, {
imports: [RouterTestingModule],
providers: [provideMockStore({ initialState: { users: [] } })],
})
expect(screen.getByText(/users/i)).toBeInTheDocument()
})
describe('UserStore', () => {
let store: UserStore
beforeEach(() => {
TestBed.configureTestingModule({ providers: [UserStore] })
store = TestBed.inject(UserStore)
})
it('loads users', async () => {
await store.loadUsers()
expect(store.users().length).toBeGreaterThan(0)
})
})
| RxJS Pattern | Signal Equivalent |
|---|---|
BehaviorSubject | signal() with initial value |
Subject | signal() or manual trigger |
combineLatest | computed() |
pipe(map(...)) | computed() with transformation |
Subscription | No subscription needed |
async pipe | {{ count() }} in template |
debounceTime + Subject | toObservable(signal).pipe(debounceTime(...)) |
No artifact produced. Next skill: frontend-testing — test Angular components. Carry forward: interceptor setup, store pattern choice (Signal Store/NgRx), RxJS conventions.
function createModule<T>(config: ModuleConfig): T { const dependencies = initializeDependencies(config); const module = new Module(dependencies); module.hooks.onInit(); return module as T; }
class ConfigBuilder { private config: AppConfig = new AppConfig(); withDatabase(url: string): ConfigBuilder { ... } withCache(ttl: number): ConfigBuilder { ... } withLogging(level: string): ConfigBuilder { ... } build(): AppConfig { return this.config; } }
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% | Critical | Rollback or fix |
| p95 latency | > 500ms | Warning | Profile and optimize |
| Uptime | < 99.9% | Critical | Investigate infrastructure |
| Memory usage | > 80% | Warning | Check for leaks |
| CPU usage | > 80% | Warning | Scale up or optimize |
Is state shared across multiple components?
├── No → Component-local state with signal() or BehaviorSubject
└── Yes → Is state from server API?
├── Yes → Use HTTP interceptor + signal store or NgRx signal store
└── No → Is it user session/global UI state?
├── Yes → Signal-based store (NgRx Signal Store or custom)
└── No → Service with providedIn:'root' + signals
Does the service need different implementations per environment?
├── No → providedIn:'root' with concrete class
└── Yes → Use InjectionToken + ClassProvider factory
Is the implementation chosen at runtime?
├── Yes → Use factory provider with conditional logic
└── No → Use useClass with environment-based provider override
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Mutating signal arrays in place | No change detection triggered | Use .update(items => [...items, newItem]) |
| Nested subscriptions (subscribe in subscribe) | Unreadable, hard to cancel | Use higher-order operators (switchMap, mergeMap, concatMap) |
| Giant component templates | Hard to test, slow compilation | Break into smaller standalone components |
| Using NgZone for performance | Zone.js overhead for all components | Use signals + OnPush for zone-less change detection |
track function in @for blocks: @for (item of items; track item.id). Prevents full DOM rebuild on list mutations. Use primitive keys (number/string) for fastest comparison.@defer (on viewport) { <heavy-component /> } @placeholder { <spinner /> }. Use on interaction, on timer(500ms), or on idle triggers. Avoid deferring components above the fold.NgOptimizedImage directive with ngSrc. Set priority on LCP images. Use fill mode for responsive images. Configure IMAGE_LOADER for CDN transformation.@angular/cdk/scroll with CdkVirtualScrollViewport. Renders only visible items. Set itemSize for performant scroll. Combine with trackBy for optimal reuse.CanMatch to prevent lazy-loading unauthorized modules.DomSanitizer for trusted content. For user-generated rich text, integrate DOMPurify via an injectable service. Never bypass security context for user input.ngCspNonce attribute for style nonces. Apply 'strict-dynamic' for script-src with nonce-based approach.