跳转至

Angular 编码风格

本文件使用 Angular 特定内容扩展 common/coding-style.md。

版本检查

在编写代码之前,务必确认项目的 Angular 版本 — 不同版本之间的功能差异很大。请运行 ng version 或查看 package.json。创建新项目时,除非用户指定,否则不要固定版本。

生成或修改 Angular 代码后,在结束前务必运行 ng build 以检测错误。

文件命名

遵循 Angular CLI 约定,每个文件只放置一个产物:

  • user-profile.component.ts + user-profile.component.html + user-profile.component.spec.ts
  • user.service.ts、auth.guard.ts、date-format.pipe.ts
  • 功能文件夹:features/users/、features/auth/
  • 使用 CLI 生成:ng generate component features/users/user-card

组件

优先使用独立组件(v17+ 默认)。在所有新组件中使用 OnPush 变更检测。

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [RouterModule],
  templateUrl: './user-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  user = input.required<User>();
  select = output<string>();
}

依赖注入

优先使用 inject() 而不是构造函数注入。构造函数应保持为空或完全删除。

// 正しい
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private router = inject(Router);
}

// 誤り: コンストラクタ注入は冗長で、ツリーシェイキングが困難
constructor(private http: HttpClient, private router: Router) {}

对于非类依赖项,请使用 InjectionToken:

const API_URL = new InjectionToken<string>('API_URL');

// 提供:
{ provide: API_URL, useValue: 'https://api.example.com' }

// 使用:
private apiUrl = inject(API_URL);

信号

基本原语

count = signal(0);
doubled = computed(() => this.count() * 2);

increment() {
  this.count.update(n => n + 1);
}

linkedSignal — 可写派生状态

对于需要在源变更时重置或适应,但可独立写入的信号,请使用 linkedSignal:

selectedOption = linkedSignal(() => this.options()[0]);
// options が変更されると最初のオプションにリセットされるが、ユーザーはオーバーライド可能

resource — 将异步数据转换为信号

要无需手动订阅即可响应式获取异步数据,请使用 resource():

userResource = resource({
  request: () => ({ id: this.userId() }),
  loader: ({ request }) => fetch(`/api/users/${request.id}`).then(r => r.json()),
});

// アクセス: userResource.value(), userResource.isLoading(), userResource.error()

effect 的用法

仅将 effect() 用于需要响应信号变更的副作用(日志记录、第三方 DOM 操作)。不要使用 effect 同步信号 — 请改用 computed 或 linkedSignal。对于渲染后的 DOM 操作,请使用 afterRenderEffect。

// 正しい: 副作用
effect(() => console.log('User changed:', this.user()));

// 誤り: 代わりに computed を使用
effect(() => { this.fullName.set(`${this.first()} ${this.last()}`); });

模板

请使用 v17+ 的块语法。在 @for 中务必指定 track:

@for (item of items(); track item.id) {
  <app-item [item]="item" />
}

@if (isLoading()) {
  <app-spinner />
} @else if (error()) {
  <app-error [message]="error()" />
} @else {
  <app-content [data]="data()" />
}

将模板中的逻辑限制为简单条件表达式,并移到组件方法或管道中。

表单

选择与项目现有方法相匹配的表单策略:

  • Signal Forms(v21+):推荐用于 v21+ 的新项目。基于信号的表单状态。
  • Reactive Forms:FormBuilder + FormGroup + FormControl。最适合具有动态验证的复杂表单。
  • Template-Driven Forms:ngModel。仅适用于简单表单。
// Reactive Forms — ほとんどのアプリの標準的なアプローチ
export class LoginComponent {
  private fb = inject(FormBuilder);

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

  submit() {
    if (this.form.valid) {
      // this.form.value を使用
    }
  }
}

组件样式

使用 ViewEncapsulation.Emulated(默认)进行组件级样式。除非有意构建会泄漏样式的设计系统,否则请避免使用 ViewEncapsulation.None。

  • 将样式限定在组件内,不要在组件样式表中使用全局类名
  • 使用 :host 为宿主元素设置样式
  • 对于可主题化的值,优先使用 CSS 自定义属性

变更检测

  • 在所有新组件中默认使用 ChangeDetectionStrategy.OnPush
  • 信号和 async 管道会自动处理检测 — 避免使用 markForCheck() 和 detectChanges()
  • 使用 OnPush 时,不要就地修改 @Input() 对象

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