跳转至

Angular 模式

此文件使用 Angular 特定内容扩展 common/patterns.md。

Smart / Dumb 组件分离

Smart(容器)组件拥有数据获取和状态。Dumb(展示)组件只负责接收输入和发出输出,不注入服务。

// Smart — データを所有
@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
export class UserPageComponent {
  private userService = inject(UserService);
  user = toSignal(this.userService.getUser(this.userId));
}
<!-- Dumb — 純粋なプレゼンテーション -->
<app-user-card [user]="user()" (select)="onSelect($event)" />

服务层

服务拥有所有数据访问和业务逻辑。组件只负责委托——请勿在组件内放置 HttpClient。

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users');
  }
}

使用 resource 的异步数据

对于响应式异步获取,请使用 resource()。对于简单数据加载,请优先使用它而不是手动 RxJS 管道:

export class UserDetailComponent {
  userId = input.required<string>();

  userResource = resource({
    request: () => ({ id: this.userId() }),
    loader: ({ request }) =>
      firstValueFrom(inject(UserService).getUser(request.id)),
  });
}

访问状态:userResource.value()、userResource.isLoading()、userResource.error()、userResource.reload()。

信号状态模式

// ローカルの可変状態
count = signal(0);

// 派生(複製しない)
doubled = computed(() => this.count() * 2);

// ソースとともにリセットされる書き込み可能な派生状態
selectedItem = linkedSignal(() => this.items()[0]);

// Observable からシグナルへのブリッジ
users = toSignal(this.userService.getUsers(), { initialValue: [] });

请勿将派生值存储到另一个信号中——请使用 computed。请勿使用 effect 同步信号——请使用 computed 或 linkedSignal。

订阅清理

对于所有手动订阅,请使用 takeUntilDestroyed()。在新代码中,请勿使用手动 ngOnDestroy + Subject + takeUntil。

export class UserComponent {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.userService.updates$
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(update => this.handleUpdate(update));
  }
}

路由

路由定义

// app.routes.ts
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'admin',
    canMatch: [authGuard],           // CanMatch はチャンクの読み込み自体を防止
    loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
  },
  {
    path: 'users/:id',
    resolve: { user: userResolver },
    component: UserDetailComponent,
  },
];
  • 如果希望未认证用户不加载路由模块,请优先使用 canMatch 而不是 canActivate
  • 使用 loadChildren 延迟加载所有功能模块
  • 使用 resolve 预取数据,以避免组件中的加载状态

函数式守卫

export const authGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  return auth.isAuthenticated()
    ? true
    : inject(Router).createUrlTree(['/login']);
};

数据解析器

export const userResolver: ResolveFn<User> = (route) => {
  return inject(UserService).getUser(route.paramMap.get('id')!);
};

视图过渡

使用 View Transitions API 启用平滑的路由过渡:

// app.config.ts
provideRouter(routes, withViewTransitions())

依赖注入模式

作用域提供程序

如果服务不应是单例,请在组件或路由级别提供它:

@Component({
  providers: [UserEditService],   // このコンポーネントサブツリーにスコープ
})
export class UserEditComponent {}

InjectionToken

export const CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');

// プロバイダ内:
{ provide: CONFIG, useValue: appConfig }
{ provide: CONFIG, useFactory: () => loadConfig(), deps: [] }

// 使用:
private config = inject(CONFIG);

viewProviders 和 providers

  • providers:在组件及其所有内容子元素中可用
  • viewProviders:仅在组件自身的视图中可用(投影内容中不可用)

HTTP 拦截器

对于认证、错误处理和重试,请使用函数式拦截器(v15+):

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).token();
  if (!token) return next(req);
  return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};

在 app.config.ts 中注册:

provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]))

RxJS 操作符

  • switchMap — 搜索、导航(取消前一个请求)
  • mergeMap — 独立的并行请求
  • exhaustMap — 表单提交(完成前忽略)
  • 始终使用 catchError 处理错误——不要隐式终止流
search$ = this.query$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(q => this.service.search(q).pipe(catchError(() => of([])))),
);

表单

请遵循项目现有的表单策略。对于 v21+ 的新应用,请优先使用信号表单。

// Reactive Forms — 複雑なフォームの標準
export class UserFormComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    name: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
  });
}

渲染策略

  • CSR(默认):标准 SPA
  • SSR + 水合:ng add @angular/ssr — 改善 FCP 和 SEO
  • SSG(预渲染):为内容丰富的路由在构建时生成静态页面

如果使用 SSR,请勿直接使用 window、document、localStorage——请使用 isPlatformBrowser 或 DOCUMENT Token。

可访问性

对于无头可访问组件(Accordion、Listbox、Combobox、Menu、Tabs、Toolbar、Tree、Grid),请使用 Angular CDK。不要手动管理 ARIA 属性,而是应用样式:

[aria-selected="true"] { background: var(--color-selected); }

技能参考

有关信号、表单、路由、DI、SSR、可访问性模式的详细指南,请参阅技能:angular-developer。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC