Angular项目开发规范.md 29 KB

Angular 项目开发规范

本文档定义了 Angular 项目开发过程中必须遵守的规范,以确保代码质量、一致性和可维护性。

目录

  1. 组件规范
  2. 路由规范
  3. 服务规范
  4. 依赖管理规范
  5. 模块规范
  6. 命名规范
  7. 代码风格规范
  8. 样式规范
  9. 安全规范
  10. 性能规范
  11. Git 提交规范

1. 组件规范

1.1 必须使用 CLI 生成组件

强制要求:所有组件必须使用 ng g c 命令生成,禁止手动创建单文件组件。

# ✅ 正确:使用 Angular CLI 生成完整组件
ng g c features/users/user-list

# ❌ 错误:手动创建单文件组件
# 禁止创建类似以下的单文件组件:
@Component({
  selector: 'app-user-list',
  template: `<div>用户列表</div>`,
  styles: [`div { color: red; }`]
})
export class UserListComponent {}

生成后的组件结构:

src/app/features/user-list/
├── user-list.component.ts       # 组件类
├── user-list.component.html    # 模板
├── user-list.component.scss    # 样式
└── user-list.component.spec.ts # 单元测试

1.2 Standalone 组件

强制要求:默认使用 Standalone 组件,不使用 NgModule。

// ✅ 正确:Standalone 组件
@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  templateUrl: './user-card.component.html',
  styleUrls: ['./user-card.component.scss']
})
export class UserCardComponent {}

// ❌ 错误:使用 NgModule 的组件(禁止新建)
@Component({
  selector: 'app-user-card',
  moduleId: module.id,
  templateUrl: './user-card.component.html'
})
export class UserCardComponent {}

1.3 组件职责单一

一个组件只负责一个功能,避免"万能组件"。

// ✅ 好:职责单一
UserListComponent        // 只负责用户列表显示
UserCardComponent       // 只负责用户卡片显示
UserFormComponent       // 只负责用户表单

// ❌ 差:职责过多
UserManagementComponent  // 同时负责列表、详情、编辑、删除

1.4 组件输入输出清晰

@Component({
  selector: 'app-button',
  template: `
    <button
      [disabled]="disabled"
      [type]="type"
      (click)="handleClick()">
      {{ label }}
    </button>
  `
})
export class ButtonComponent {
  @Input() label = '按钮';
  @Input() disabled = false;
  @Input() type: 'button' | 'submit' = 'button';

  @Output() clicked = new EventEmitter<void>();

  handleClick() {
    this.clicked.emit();
  }
}

1.5 避免直接操作 DOM

使用 Angular 绑定机制,禁止使用 document.querySelector 等直接 DOM 操作。

// ✅ 正确:使用模板绑定
template: `<div [class.active]="isActive">内容</div>`

// ❌ 错误:直接操作 DOM
ngAfterViewInit() {
  document.querySelector('.active').classList.add('highlight');
}

2. 路由规范

2.1 路由必须集中在 app.routes.ts

强制要求:所有路由必须写在 app.routes.ts 中,禁止在 feature 目录下创建独立的 .routes.ts 文件。

// ✅ 正确:所有路由集中在 app.routes.ts
// src/app/app.routes.ts
export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./layouts/main-layout/main-layout.component')
      .then(m => m.MainLayoutComponent),
    children: [
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
      {
        path: 'dashboard',
        loadComponent: () => import('./features/dashboard/dashboard.component')
          .then(m => m.DashboardComponent)
      },
      {
        path: 'users',
        loadComponent: () => import('./features/users/user-list/user-list.component')
          .then(m => m.UserListComponent)
      },
      {
        path: 'users/:id',
        loadComponent: () => import('./features/users/user-detail/user-detail.component')
          .then(m => m.UserDetailComponent)
      }
    ]
  },
  {
    path: 'login',
    loadComponent: () => import('./features/auth/login/login.component')
      .then(m => m.LoginComponent)
  },
  {
    path: '**',
    loadComponent: () => import('./features/not-found/not-found.component')
      .then(m => m.NotFoundComponent)
  }
];

❌ 错误:禁止在 feature 目录下创建独立的路由文件

// ❌ 错误:禁止创建 features/users/users.routes.ts
// features/users/users.routes.ts
export const USER_ROUTES: Routes = [
  { path: '', component: UserListComponent },
  { path: ':id', component: UserDetailComponent }
];

// ❌ 错误:禁止在 app.routes.ts 中引用外部路由文件
export const routes: Routes = [
  {
    path: 'users',
    loadChildren: () => import('./features/users/users.routes')
      .then(m => m.USER_ROUTES)  // ❌ 禁止
  }
];

2.2 必须使用懒加载

强制要求:所有功能组件必须使用懒加载。

// ✅ 正确:懒加载组件
{
  path: 'users',
  loadComponent: () => import('./features/users/user-list/user-list.component')
    .then(m => m.UserListComponent)
}

// ❌ 错误:直接导入(立即加载)
import { UserListComponent } from './features/users/user-list/user-list.component';

export const routes: Routes = [
  { path: 'users', component: UserListComponent }  // ❌
];

2.3 路由守卫保护

敏感页面必须使用路由守卫。

// core/guards/auth.guard.ts
export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated()) {
    return true;
  }

  router.navigate(['/login'], {
    queryParams: { returnUrl: state.url }
  });
  return false;
};

// app.routes.ts 中使用
{
  path: 'admin',
  loadComponent: () => import('./features/admin/admin.component')
    .then(m => m.AdminComponent),
  canActivate: [authGuard, adminGuard]
}

2.4 路由路径命名

使用 kebab-case 命名路由路径。

// ✅ 正确
{ path: 'user-profile', component: UserProfileComponent }
{ path: 'order-management', component: OrderManagementComponent }

// ❌ 错误
{ path: 'userProfile', component: UserProfileComponent }
{ path: 'OrderManagement', component: OrderManagementComponent }

2.5 404 处理

必须配置 404 页面。

export const routes: Routes = [
  // ... 其他路由
  {
    path: '**',
    loadComponent: () => import('./features/not-found/not-found.component')
      .then(m => m.NotFoundComponent)
  }
];

2.6 嵌套路由配置

嵌套路由也必须写在 app.routes.ts 中。

export const routes: Routes = [
  {
    path: 'products',
    loadComponent: () => import('./features/products/product-list/product-list.component')
      .then(m => m.ProductListComponent)
  },
  {
    path: 'products/:id',
    loadComponent: () => import('./features/products/product-detail/product-detail.component')
      .then(m => m.ProductDetailComponent)
  },
  {
    path: 'products/:id/edit',
    loadComponent: () => import('./features/products/product-edit/product-edit.component')
      .then(m => m.ProductEditComponent)
  }
];

3. 服务规范

3.1 使用 inject() 注入

优先使用 inject() 函数,替代构造函数注入。

// ✅ 正确:使用 inject()
@Injectable({
  providedIn: 'root'
})
export class UserService {
  private http = inject(HttpClient);
  private logger = inject(LoggerService);

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

// ❌ 错误:构造函数注入(仅在必须时使用)
@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor(
    private http: HttpClient,
    private logger: LoggerService
  ) {}
}

3.2 服务作用域

根据需要选择正确的作用域。

// 全局服务(大多数情况)
@Injectable({
  providedIn: 'root'
})
export class UserService {}

// 组件级服务(需要多个实例时)
@Component({
  selector: 'app-cart',
  providers: [CartService]  // 每个组件实例都有独立的服务实例
})
export class CartComponent {}

3.3 服务方法返回 Observable

所有 HTTP 请求方法必须返回 Observable。

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

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

  // ❌ 错误:返回 Promise
  async getUsers(): Promise<User[]> {
    return fetch('/api/users').then(res => res.json());
  }
}

3.4 HTTP 拦截器处理通用逻辑

使用拦截器统一处理认证、错误等。

// core/interceptors/auth.interceptor.ts
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const token = inject(TokenService).getToken();
    if (token) {
      const authReq = req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`)
      });
      return next.handle(authReq);
    }
    return next.handle(req);
  }
}

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor, errorInterceptor])
    )
  ]
};

4. 依赖管理规范

4.1 及时清除未使用的依赖

安装新包后或移除功能后,必须清除未使用的依赖。

# ✅ 正确添加依赖
pnpm add package-name

# ✅ 正确移除依赖(同时从 package.json 和 pnpm-lock.yaml 中移除)
pnpm remove package-name

# ✅ 消除重复依赖(可选)
pnpm dedupe

# ❌ 错误:直接删除 node_modules 中的包

注意:使用 pnpm remove 是清除依赖的唯一正确方式,它会同时更新 package.json 和锁文件。

4.2 生产依赖与开发依赖区分

{
  "dependencies": {
    "@angular/core": "^20.0.0",
    "@angular/common/http": "^20.0.0"
  },
  "devDependencies": {
    "@angular/cli": "^20.0.0",
    "prettier": "^3.0.0",
    "typescript": "~5.6.0"
  }
}

4.3 禁止安装的包

以下类型的包禁止使用:

禁止类型 示例 原因
已弃用的包 @types/node@12 安全风险
jQuery 及插件 jquery, bootstrap 与 Angular 理念冲突
原生 JS 库 原生 XMLHttpRequest 应使用 HttpClient

4.4 版本锁定

生产环境必须锁定依赖版本。

// ✅ 正确:锁定主版本
"dependencies": {
  "@angular/core": "^20.0.0"
}

// ❌ 错误:使用浮动版本
"dependencies": {
  "@angular/core": "20"
}

5. 模块规范

5.1 目录结构规范

src/app/
├── core/                          # 核心服务(全局单例)
│   ├── interceptors/               # HTTP 拦截器
│   ├── guards/                     # 路由守卫
│   ├── services/                   # 核心服务
│   └── models/                     # 全局数据模型
│
├── shared/                         # 共享资源(Standalone)
│   ├── components/                 # 共享组件
│   ├── directives/                 # 共享指令
│   └── pipes/                      # 共享管道
│
├── features/                       # 功能模块(懒加载)
│   ├── dashboard/
│   │   ├── dashboard.component.ts
│   │   ├── dashboard.component.html
│   │   ├── dashboard.component.scss
│   │   └── components/             # 功能内部子组件
│   ├── users/
│   └── products/
│
├── layouts/                        # 布局组件
│   ├── main-layout/
│   └── auth-layout/
│
├── environments/                    # 环境配置
│   ├── environment.ts
│   └── environment.prod.ts
│
├── app.component.ts
├── app.config.ts
└── app.routes.ts

5.2 Core 服务(始终加载)

Core 服务使用 providedIn: 'root',在 app.config.ts 中统一配置拦截器。

// core/services/auth.service.ts
@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private http = inject(HttpClient);
  private tokenService = inject(TokenService);

  isAuthenticated(): boolean {
    return !!this.tokenService.getToken();
  }

  login(credentials: any): Observable<any> {
    return this.http.post('/api/auth/login', credentials);
  }
}
// core/interceptors/auth.interceptor.ts
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const token = inject(TokenService).getToken();
    if (token) {
      const authReq = req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`)
      });
      return next.handle(authReq);
    }
    return next.handle(req);
  }
}
// app.config.ts - 统一注册拦截器
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(
      withInterceptors([authInterceptor, errorInterceptor])
    )
  ]
};

注意:在 Standalone 模式下,不再使用 NgModule,统一使用 providers 数组注册服务。

5.3 Shared 共享资源(按需导入)

在 Standalone 模式下,共享组件、指令、管道直接在组件中导入,不再使用 SharedModule。

// 在组件中按需导入
@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [
    CommonModule,
    ReactiveFormsModule,
    DateFormatPipe,      // 只导入需要的
    PermissionDirective  // 只导入需要的
  ]
})
export class UserListComponent {}

目录结构中的 shared 目录:

src/app/
├── shared/
│   ├── components/                 # 共享组件(直接导入)
│   │   ├── button/
│   │   ├── modal/
│   │   └── data-table/
│   ├── directives/                  # 共享指令
│   │   └── permission.directive.ts
│   └── pipes/                       # 共享管道
│       └── date-format.pipe.ts

注意:不再需要 shared.module.ts,所有共享资源都是 Standalone 组件/指令/管道。

5.4 功能模块(必须懒加载)

每个功能模块独立,使用懒加载,路由集中在 app.routes.ts。

// app.routes.ts
export const routes: Routes = [
  {
    path: 'users',
    loadComponent: () => import('./features/users/user-list/user-list.component')
      .then(m => m.UserListComponent)
  },
  {
    path: 'users/:id',
    loadComponent: () => import('./features/users/user-detail/user-detail.component')
      .then(m => m.UserDetailComponent)
  }
];

禁止创建独立的路由文件:

// ❌ 禁止:features/users/users.routes.ts
// ❌ 禁止:features/products/products.routes.ts

6. 命名规范

6.1 文件命名

类型 规范 示例
组件 kebab-case.component.ts user-profile.component.ts
服务 kebab-case.service.ts auth.service.ts
指令 kebab-case.directive.ts permission.directive.ts
管道 kebab-case.pipe.ts date-format.pipe.ts
守卫 kebab-case.guard.ts auth.guard.ts
拦截器 kebab-case.interceptor.ts auth.interceptor.ts
模型 kebab-case.model.ts user.model.ts
组件目录 kebab-case/ user-profile/

6.2 类命名

类型 规范 示例
组件类 PascalCase + Component UserProfileComponent
服务类 PascalCase + Service AuthService
指令类 PascalCase + Directive PermissionDirective
管道类 PascalCase + Pipe DateFormatPipe
守卫类 PascalCase + Guard AuthGuard
模型接口 PascalCase User, UserProfile
拦截器类 PascalCase + Interceptor AuthInterceptor

6.3 变量与函数命名

// 变量:camelCase
const userName = '张三';
const isLoading = false;
const userList: User[] = [];

// 函数:camelCase
function getUserById(id: number): User { }
function handleClick(): void { }

// 常量:UPPER_SNAKE_CASE
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = '/api';

6.4 路由路径命名

使用 kebab-case,多层嵌套用斜杠分隔。

// ✅ 正确
'/user-profile'
'/order-management/list'
'/admin/system-settings'

// ❌ 错误
'/userProfile'
'/orderManagement'
'/admin/systemSettings'

7. 代码风格规范

7.1 TypeScript 规范

1. 禁止使用 any,必须指定类型。

// ✅ 正确
const user: User = { id: 1, name: '张三' };
function getUser(): Observable<User> { }

// ❌ 错误
const user: any = { id: 1, name: '张三' };
function getUser(): any { }

2. 使用 strict 模式。

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

3. 接口和类型别名优先使用接口。

// ✅ 优先使用接口
interface User {
  id: number;
  name: string;
}

// 仅在需要联合类型或元组时使用类型别名
type UserRole = 'admin' | 'user' | 'guest';
type Point = [number, number];

4. 使用可选链和空值合并。

// ✅ 正确
const name = user?.profile?.name ?? '匿名';
const users = data ?? [];

// ❌ 错误
const name = user && user.profile && user.profile.name || '匿名';

7.2 RxJS 规范

1. 组件中及时取消订阅。

import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({...})
export class UserListComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    this.userService.getUsers()
      .pipe(takeUntil(this.destroy$))
      .subscribe(users => this.users = users);
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

注意:必须导入 takeUntil 操作符和 Subject

2. 优先使用 async pipe。

@Component({
  selector: 'app-user-list',
  template: `
    @for (user of users$ | async; track user.id) {
      <div>{{ user.name }}</div>
    }
  `
})
export class UserListComponent {
  users$ = this.userService.getUsers();
}

3. 统一错误处理。

// 使用 catchError 操作符
this.http.get<User[]>('/api/users').pipe(
  catchError(error => {
    console.error('请求失败', error);
    return of([]);
  })
);

7.3 Signals 响应式规范

Angular 20 推荐使用 Signals 替代部分 RxJS 使用场景。

1. 创建和使用 Signals。

import { signal, computed, effect } from '@angular/core';

// 创建可写的 signal
const count = signal(0);
const user = signal<User | null>(null);

// 读取值(调用函数)
console.log(count()); // 0

// 更新值
count.set(10);
count.update(value => value + 1);

// 创建计算属性
const doubled = computed(() => count() * 2);

// 副作用
effect(() => {
  console.log(`Count changed: ${count()}`);
});

2. 在服务中使用 Signals 替代 BehaviorSubject。

// ✅ 推荐:使用 Signals
@Injectable({
  providedIn: 'root'
})
export class CartService {
  private items = signal<CartItem[]>([]);

  totalItems = computed(() => this.items().length);
  totalPrice = computed(() =>
    this.items().reduce((sum, item) => sum + item.price, 0)
  );

  addItem(item: CartItem) {
    this.items.update(current => [...current, item]);
  }

  removeItem(id: number) {
    this.items.update(current => current.filter(i => i.id !== id));
  }
}

// ❌ 不推荐:使用 BehaviorSubject
@Injectable({
  providedIn: 'root'
})
export class CartService {
  private items$ = new BehaviorSubject<CartItem[]>([]);

  totalItems$ = this.items$.pipe(map(items => items.length));
}

3. 在组件中使用 Signals。

@Component({
  selector: 'app-cart',
  template: `
    <p>商品数量:{{ cart.totalItems() }}</p>
    <p>总价:{{ cart.totalPrice() | currency:'¥' }}</p>
  `
})
export class CartComponent {
  cart = inject(CartService);
}

4. Signals vs RxJS 选择指南。

场景 推荐 原因
组件内本地状态 Signals 简单、直接
服务中的共享状态 Signals 性能更好
HTTP 请求 RxJS 强大的操作符支持
复杂异步流程 RxJS 可取消、retry、重试
实时数据流 RxJS WebSocket 等

7.4 注释规范

仅在必要时添加注释,避免无意义注释。

// ✅ 好的注释:解释为什么
// 使用 setTimeout 而非 setInterval,避免内存泄漏
setTimeout(() => this.refresh(), 5000);

// ✅ 好的注释:复杂逻辑说明
/**
 * 计算用户积分
 * 规则:每消费1元积1分,VIP用户双倍积分
 */
calculatePoints(amount: number, isVip: boolean): number {
  const basePoints = amount;
  return isVip ? basePoints * 2 : basePoints;
}

// ❌ 坏的注释:显而易见的说明
// 设置用户名
this.userName = '张三';

8. 样式规范

8.1 使用 SCSS

强制使用 SCSS 预处理器。

ng new my-app --style=scss

8.2 全局样式配置

全局样式使用 src/styles.scss,直接定义变量和混入。

// src/styles.scss

// 全局变量
$primary-color: #1976d2;
$border-radius: 4px;

// 混入
@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

// CSS 重置
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

// 全局样式
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 14px;
  line-height: 1.5;
  color: #333;
}

// 组件样式使用 BEM
.user-card {
  padding: 16px;

  &__header {
    @include flex-center;
  }

  &__avatar {
    width: 48px;
    border-radius: 50%;
  }

  &--featured {
    border: 2px solid gold;
  }
}

angular.json 配置(自动生成,无需修改):

{
  "styles": ["src/styles.scss"],
  "stylePreprocessorOptions": {
    "includePaths": ["src"]
  }
}

注意:在组件 SCSS 中可以直接使用全局变量和混入。

8.3 样式最佳实践

1. 使用 :host 选择器。

:host {
  display: block;
}

:host(.featured) {
  border: 2px solid gold;
}

2. 禁止使用深度选择器。

// ✅ 正确:使用固定类名
.parent { }
.parent__child { }

// ❌ 错误:/deep/ 已废弃
.parent /deep/ .child { }

// ❌ 错误:::ng-deep 已废弃,不应使用
.parent ::ng-deep .child { }

深度选择器会破坏组件封装,应使用固定的类名或通过 @Input 传递样式配置。

3. 响应式样式使用变量。

// 在 styles.scss 或组件中定义断点
$breakpoint-sm: 576px;
$breakpoint-md: 768px;
$breakpoint-lg: 992px;

.container {
  width: 100%;
  @media (min-width: $breakpoint-md) {
    width: 720px;
  }
}

9. 安全规范

9.1 XSS 防护

使用 Angular 绑定机制,禁止使用 innerHTML 绑定未处理的内容。

// ✅ 正确:自动转义
template: `<div>{{ userContent }}</div>`

// ❌ 错误:可能导致 XSS
template: `<div [innerHTML]="userContent"></div>`

// 如果必须使用 innerHTML,必须先净化
import { DomSanitizer } from '@angular/platform-browser';

constructor(private sanitizer: DomSanitizer) {}

getSafeHtml(html: string): SafeHtml {
  return this.sanitizer.bypassSecurityTrustHtml(html);
}

9.2 CSRF 防护

使用 provideHttpClient 的 withXsrfConfiguration 配置 CSRF 防护。

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN',
        headerName: 'X-XSRF-TOKEN'
      })
    )
  ]
};

9.3 敏感信息处理

禁止在代码中硬编码敏感信息。

// ❌ 错误
const API_KEY = 'sk-xxxxx-xxxxx';
const DATABASE_URL = 'mongodb://admin:password@localhost:27017';

// ✅ 正确:使用环境变量
// environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api'
};

// environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.example.com'
};

9.4 路由守卫

敏感路由必须添加守卫。

// 需要认证的路由
{ path: 'profile', canActivate: [authGuard] }

// 需要特定角色的路由
{ path: 'admin', canActivate: [authGuard, adminGuard] }

10. 性能规范

10.1 懒加载

所有功能组件必须使用懒加载,使用 loadComponent。

// ✅ 正确
{
  path: 'users',
  loadComponent: () => import('./features/users/user-list/user-list.component')
    .then(m => m.UserListComponent)
}

// ❌ 错误:禁止使用 loadChildren
{
  path: 'users',
  loadChildren: () => import('./features/users/users.routes')
    .then(m => m.USER_ROUTES)  // ❌ 禁止
}

// ❌ 错误:直接导入
import { UserListComponent } from './features/users/user-list/user-list.component';
{ path: 'users', component: UserListComponent }  // ❌ 禁止

10.2 OnPush 变更检测

高频更新的组件使用 OnPush。

@Component({
  selector: 'app-user-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `...`
})
export class UserListComponent {}

10.3 trackBy 函数

@for 循环必须使用 track。

<!-- ✅ 正确 -->
@for (user of users; track user.id) {
  <div>{{ user.name }}</div>
}

<!-- ❌ 错误:缺少 track -->
@for (user of users) {
  <div>{{ user.name }}</div>
}

10.4 图片优化

使用 loading="lazy" 延迟加载图片。

<img src="avatar.jpg" alt="头像" loading="lazy" />

10.5 避免内存泄漏

1. RxJS 订阅必须取消。

@Component({...})
export class UserListComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

2. 推荐使用 Signals 替代 RxJS 管理组件状态,可以避免订阅管理问题。

// ✅ 推荐:使用 Signals,无需手动取消订阅
@Injectable({
  providedIn: 'root'
})
export class CartService {
  private items = signal<CartItem[]>([]);
  totalItems = computed(() => this.items().length);
}

// 组件中使用
@Component({
  template: `<p>{{ cart.totalItems() }}</p>`
})
export class CartComponent {
  cart = inject(CartService);
}
// Signals 会自动管理依赖,无需担心内存泄漏

10.6 Bundle 分析

定期分析打包大小。

# 生成打包分析
ng build --stats-json
npx webpack-bundle-analyzer dist/*/stats.json

11. Git 提交规范

11.1 提交信息格式

<type>(<scope>): <subject>

<body>

<footer>

11.2 Type 类型

类型 说明
feat 新功能
fix 修复 bug
docs 文档更新
style 代码格式(不影响功能)
refactor 重构
perf 性能优化
test 测试相关
build 构建相关
ci CI 相关
chore 其他更改

11.3 示例

# 功能提交
git commit -m "feat(user): 添加用户列表分页功能"

# 修复提交
git commit -m "fix(auth): 修复登录失败后 token 未清除的问题"

# 重构提交
git commit -m "refactor(service): 重构 UserService 使用 inject()"

# 提交前检查
git commit -m "feat(cart): 添加购物车功能

- 添加购物车服务
- 添加商品加减功能
- 添加结算页面"

Closes #123

11.4 禁止提交

禁止提交以下内容:

# node_modules
node_modules/

# 构建产物
dist/
build/

# IDE 配置
.idea/
.vscode/

# 环境配置文件
.env
*.local

# npm 锁文件(禁止提交 npm 的 lock 文件)
package-lock.json

# 日志文件
*.log

# 操作系统文件
.DS_Store
Thumbs.db

pnpm-lock.yaml 必须提交,它是 pnpm 的锁文件,确保团队成员安装的依赖版本一致。


附录 A:规范检查清单

新建组件

  • 使用 ng g c 生成
  • 文件结构完整(.ts/.html/.scss/.spec.ts)
  • 使用 standalone
  • 职责单一

新建路由

  • 所有路由写在 app.routes.ts 中(禁止创建独立的 .routes.ts 文件)
  • 使用懒加载 loadComponent
  • 添加路由守卫(如需要)
  • 路径使用 kebab-case

新增依赖

  • 确认是否为必需依赖
  • 正确区分 dependencies 和 devDependencies
  • 检查是否有替代方案

代码审查

  • any 类型
  • 无内存泄漏
  • 无直接 DOM 操作
  • 样式无深度选择器

附录 B:Prettier 配置(可选)

如果需要代码格式化,可以配置 Prettier。

// .prettierrc
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "avoid",
  "endOfLine": "auto"
}

安装和使用:

pnpm add -D prettier
pnpm exec prettier --write src/

文档版本:2.0.0 | 对应 Angular 20.x | 更新日期:2025