|
|
@@ -0,0 +1,1421 @@
|
|
|
+# Angular 项目开发规范
|
|
|
+
|
|
|
+> 本文档定义了 Angular 项目开发过程中必须遵守的规范,以确保代码质量、一致性和可维护性。
|
|
|
+
|
|
|
+## 适用范围与基线
|
|
|
+
|
|
|
+- 适用 Angular:20.x(本仓库 frontend 当前为 `@angular/* ^20.3.x`)
|
|
|
+- 包管理器:pnpm(本仓库 `frontend/angular.json` 已配置 `cli.packageManager = pnpm`)
|
|
|
+- 推荐运行环境:Node.js LTS(以 Angular 20 官方要求为准),团队统一版本并写入 `.nvmrc` 或 `volta`/`asdf` 配置(如项目已使用)
|
|
|
+- 目标:Standalone-first、功能按路由懒加载、移动端/PC 端目录强隔离(见 [前端目录规范](file:///e:/拉迷大纲/lami-base-v1/doc/前端目录规范.md))
|
|
|
+
|
|
|
+## 目录
|
|
|
+
|
|
|
+0. [工程与 pnpm 规范](#0-工程与-pnpm-规范)
|
|
|
+1. [组件规范](#1-组件规范)
|
|
|
+2. [路由规范](#2-路由规范)
|
|
|
+3. [服务规范](#3-服务规范)
|
|
|
+4. [依赖管理规范](#4-依赖管理规范)
|
|
|
+5. [模块/目录规范](#5-模块目录规范)
|
|
|
+6. [命名规范](#6-命名规范)
|
|
|
+7. [代码风格规范](#7-代码风格规范)
|
|
|
+ - [7.1 TypeScript 规范](#71-typescript-规范)
|
|
|
+ - [7.2 RxJS 规范](#72-rxjs-规范)
|
|
|
+ - [7.3 Signals 响应式规范](#73-signals-响应式规范)
|
|
|
+ - [7.4 注释规范](#74-注释规范)
|
|
|
+8. [样式规范](#8-样式规范)
|
|
|
+9. [安全规范](#9-安全规范)
|
|
|
+10. [性能规范](#10-性能规范)
|
|
|
+11. [Git 提交规范](#11-git-提交规范)
|
|
|
+12. [测试与 CI(建议)](#12-测试与-ci建议)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 0. 工程与 pnpm 规范
|
|
|
+
|
|
|
+### 0.1 必须使用 pnpm(并提交锁文件)
|
|
|
+
|
|
|
+- 只允许使用 pnpm 安装依赖与运行脚本
|
|
|
+- 必须提交 `pnpm-lock.yaml`
|
|
|
+- 禁止提交 `package-lock.json`、`yarn.lock`
|
|
|
+
|
|
|
+### 0.2 推荐使用 corepack 锁定包管理器版本
|
|
|
+
|
|
|
+```bash
|
|
|
+corepack enable
|
|
|
+pnpm -v
|
|
|
+```
|
|
|
+
|
|
|
+建议在 `frontend/package.json` 增加 `packageManager` 字段(如团队需要强约束):
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "packageManager": "pnpm@9.0.0"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 0.3 常用命令(以 frontend 为例)
|
|
|
+
|
|
|
+```bash
|
|
|
+pnpm install
|
|
|
+pnpm start
|
|
|
+pnpm build
|
|
|
+pnpm test
|
|
|
+
|
|
|
+# 生成代码(推荐用 pnpm 调用 ng)
|
|
|
+pnpm ng g c mobile/features/home/components/foo
|
|
|
+pnpm ng g s mobile/core/services/foo
|
|
|
+```
|
|
|
+
|
|
|
+CI 中建议使用冻结锁文件安装(防止隐式更新依赖):
|
|
|
+
|
|
|
+```bash
|
|
|
+pnpm install --frozen-lockfile
|
|
|
+```
|
|
|
+
|
|
|
+## 1. 组件规范
|
|
|
+
|
|
|
+### 1.1 必须使用 CLI 生成组件
|
|
|
+
|
|
|
+**强制要求:所有组件必须使用 `ng g c`(或 `pnpm ng g c`)生成,避免手工创建导致的风格漂移。**
|
|
|
+
|
|
|
+```bash
|
|
|
+# ✅ 正确:使用 Angular CLI 生成完整组件
|
|
|
+pnpm ng g c mobile/features/home/components/user-list
|
|
|
+
|
|
|
+# ❌ 不推荐:手工创建且内联模板/样式(除非非常小且经过评审)
|
|
|
+```
|
|
|
+
|
|
|
+**生成后的组件结构:**
|
|
|
+
|
|
|
+```
|
|
|
+src/app/mobile/features/home/components/user-list/
|
|
|
+├── user-list.ts # 组件类(Angular 20 CLI 默认不再强制 .component.ts 命名)
|
|
|
+├── user-list.html # 模板
|
|
|
+└── user-list.scss # 样式
|
|
|
+```
|
|
|
+
|
|
|
+> 说明:本仓库 `angular.json` 已配置 `skipTests: true`,因此默认不会生成 `*.spec.ts`。如业务关键逻辑需要单测,请手工补齐测试文件并纳入 CI。
|
|
|
+
|
|
|
+### 1.2 Standalone 组件
|
|
|
+
|
|
|
+**强制要求:Standalone-first。Angular 20 默认以 Standalone 为主,禁止为新功能创建 NgModule(除非为了兼容历史库)。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确:Standalone 组件
|
|
|
+@Component({
|
|
|
+ selector: 'app-user-card',
|
|
|
+ imports: [CommonModule, ReactiveFormsModule],
|
|
|
+ templateUrl: './user-card.html',
|
|
|
+ styleUrl: './user-card.scss'
|
|
|
+})
|
|
|
+export class UserCard {}
|
|
|
+
|
|
|
+// ❌ 错误:使用 NgModule 的组件(禁止新建)
|
|
|
+@Component({
|
|
|
+ selector: 'app-user-card',
|
|
|
+ templateUrl: './user-card.html'
|
|
|
+})
|
|
|
+export class UserCard {}
|
|
|
+```
|
|
|
+
|
|
|
+> 说明:`standalone: true` 在 Angular 20 的开发体验中可能是“可省略项”(取决于 CLI/编译器配置)。为了降低理解成本,团队可以选择“统一显式写出”或“统一省略”,但禁止混用两套风格。
|
|
|
+
|
|
|
+### 1.3 组件职责单一
|
|
|
+
|
|
|
+**一个组件只负责一个功能,避免"万能组件"。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 好:职责单一
|
|
|
+UserListComponent // 只负责用户列表显示
|
|
|
+UserCardComponent // 只负责用户卡片显示
|
|
|
+UserFormComponent // 只负责用户表单
|
|
|
+
|
|
|
+// ❌ 差:职责过多
|
|
|
+UserManagementComponent // 同时负责列表、详情、编辑、删除
|
|
|
+```
|
|
|
+
|
|
|
+### 1.4 组件输入输出清晰
|
|
|
+
|
|
|
+```typescript
|
|
|
+@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 操作。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确:使用模板绑定
|
|
|
+template: `<div [class.active]="isActive">内容</div>`
|
|
|
+
|
|
|
+// ❌ 错误:直接操作 DOM
|
|
|
+ngAfterViewInit() {
|
|
|
+ document.querySelector('.active').classList.add('highlight');
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 路由规范
|
|
|
+
|
|
|
+### 2.1 路由组织方式(适配双端隔离)
|
|
|
+
|
|
|
+本仓库为“双端隔离”结构:
|
|
|
+
|
|
|
+- `src/app/app.routes.ts`:仅负责 `/mobile` 与 `/pc` 分发(全局路由入口)
|
|
|
+- `src/app/mobile/app-mobile.routes.ts`:移动端主路由树
|
|
|
+- `src/app/pc/app-pc.routes.ts`:PC 端主路由树
|
|
|
+
|
|
|
+**强制要求:**
|
|
|
+
|
|
|
+- 业务路由必须归属到对应平台(mobile 或 pc)的路由树内,禁止跨区引用
|
|
|
+- 允许在 feature 内声明 `routes` 常量并通过 `loadChildren` 懒加载(适合较大功能的路由树),但必须遵守目录边界与命名规范
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确:全局路由分发(示意)
|
|
|
+export const routes: Routes = [
|
|
|
+ {
|
|
|
+ path: 'mobile',
|
|
|
+ loadChildren: () => import('./mobile/app-mobile.routes').then(m => m.mobileRoutes)
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: 'pc',
|
|
|
+ loadChildren: () => import('./pc/app-pc.routes').then(m => m.pcRoutes)
|
|
|
+ },
|
|
|
+ {
|
|
|
+ path: '**',
|
|
|
+ redirectTo: 'mobile'
|
|
|
+ }
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 必须使用懒加载
|
|
|
+
|
|
|
+**强制要求:业务页面/功能必须懒加载。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确:懒加载单页面组件
|
|
|
+{
|
|
|
+ path: 'users',
|
|
|
+ loadComponent: () => import('./features/users/user-list/user-list.component')
|
|
|
+ .then(m => m.UserListComponent)
|
|
|
+}
|
|
|
+
|
|
|
+// ✅ 正确:懒加载一组路由(适合 feature 路由树)
|
|
|
+{
|
|
|
+ path: 'learning',
|
|
|
+ loadChildren: () => import('./features/learning/learning.routes').then(m => m.routes)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 路由守卫保护
|
|
|
+
|
|
|
+**敏感页面必须使用路由守卫。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 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 命名路由路径。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确
|
|
|
+{ path: 'user-profile', component: UserProfileComponent }
|
|
|
+{ path: 'order-management', component: OrderManagementComponent }
|
|
|
+
|
|
|
+// ❌ 错误
|
|
|
+{ path: 'userProfile', component: UserProfileComponent }
|
|
|
+{ path: 'OrderManagement', component: OrderManagementComponent }
|
|
|
+```
|
|
|
+
|
|
|
+### 2.5 404 处理
|
|
|
+
|
|
|
+**必须配置 404 页面。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+export const routes: Routes = [
|
|
|
+ // ... 其他路由
|
|
|
+ {
|
|
|
+ path: '**',
|
|
|
+ loadComponent: () => import('./features/not-found/not-found.component')
|
|
|
+ .then(m => m.NotFoundComponent)
|
|
|
+ }
|
|
|
+];
|
|
|
+```
|
|
|
+
|
|
|
+### 2.6 嵌套路由配置
|
|
|
+
|
|
|
+**嵌套路由也必须写在 app.routes.ts 中。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+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()` 函数,替代构造函数注入。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确:使用 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 服务作用域
|
|
|
+
|
|
|
+**根据需要选择正确的作用域。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 全局服务(大多数情况)
|
|
|
+@Injectable({
|
|
|
+ providedIn: 'root'
|
|
|
+})
|
|
|
+export class UserService {}
|
|
|
+
|
|
|
+// 组件级服务(需要多个实例时)
|
|
|
+@Component({
|
|
|
+ selector: 'app-cart',
|
|
|
+ providers: [CartService] // 每个组件实例都有独立的服务实例
|
|
|
+})
|
|
|
+export class CartComponent {}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 服务方法返回 Observable
|
|
|
+
|
|
|
+**所有 HTTP 请求方法必须返回 Observable。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+@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 拦截器处理通用逻辑
|
|
|
+
|
|
|
+**使用拦截器统一处理认证、错误等。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 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 及时清除未使用的依赖
|
|
|
+
|
|
|
+**安装新包后或移除功能后,必须清除未使用的依赖。**
|
|
|
+
|
|
|
+```bash
|
|
|
+# ✅ 正确添加依赖
|
|
|
+pnpm add package-name
|
|
|
+
|
|
|
+# ✅ 正确移除依赖(同时从 package.json 和 pnpm-lock.yaml 中移除)
|
|
|
+pnpm remove package-name
|
|
|
+
|
|
|
+# ✅ 消除重复依赖(可选)
|
|
|
+pnpm dedupe
|
|
|
+
|
|
|
+# ❌ 错误:直接删除 node_modules 中的包
|
|
|
+```
|
|
|
+
|
|
|
+**注意:使用 `pnpm remove` 是清除依赖的唯一正确方式,它会同时更新 `package.json` 和锁文件。**
|
|
|
+
|
|
|
+### 4.2 生产依赖与开发依赖区分
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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 版本锁定
|
|
|
+
|
|
|
+**版本策略:**
|
|
|
+
|
|
|
+- `package.json` 中允许使用 `^`/`~`(团队统一即可),真正的“锁定”依赖以 `pnpm-lock.yaml` 为准
|
|
|
+- CI 必须使用 `--frozen-lockfile`,避免隐式更新
|
|
|
+- 升级 Angular 主版本(如 20.x -> 21.x)必须走专项升级流程(升级、构建、回归、灰度)
|
|
|
+
|
|
|
+```json
|
|
|
+// ✅ 推荐:允许 minor/patch,结合 lockfile 保证一致性
|
|
|
+"dependencies": {
|
|
|
+ "@angular/core": "^20.0.0"
|
|
|
+}
|
|
|
+
|
|
|
+// ❌ 不推荐:过度宽泛(可读性差)
|
|
|
+"dependencies": {
|
|
|
+ "@angular/core": "20"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 模块/目录规范
|
|
|
+
|
|
|
+> Angular 20 Standalone-first 的项目中,“模块”更多体现为目录边界与路由边界。为避免误导,本节强调目录结构与分层职责。
|
|
|
+
|
|
|
+### 5.1 目录结构规范
|
|
|
+
|
|
|
+```text
|
|
|
+src/app/
|
|
|
+├── mobile/ # [📱 移动端绝对隔离区]
|
|
|
+│ ├── app-mobile.routes.ts # 移动端主路由
|
|
|
+│ ├── core/ # 移动端专属基建(services/models/guards/...)
|
|
|
+│ ├── shared/ # 移动端专属共享 UI 与工具
|
|
|
+│ ├── layouts/ # 移动端布局
|
|
|
+│ ├── features/ # 移动端业务功能(建议懒加载)
|
|
|
+│ └── pages/ # 移动端独立页面
|
|
|
+│
|
|
|
+├── pc/ # [💻 PC 端绝对隔离区]
|
|
|
+│ ├── app-pc.routes.ts # PC 端主路由
|
|
|
+│ ├── core/
|
|
|
+│ ├── shared/
|
|
|
+│ ├── layouts/
|
|
|
+│ ├── features/
|
|
|
+│ └── pages/
|
|
|
+│
|
|
|
+├── app.routes.ts # 全局路由分发(/mobile、/pc)
|
|
|
+├── app.config.ts # 全局 providers
|
|
|
+└── app.ts # 根组件(仅承载 router-outlet 等)
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 Core 服务(始终加载)
|
|
|
+
|
|
|
+**Core 服务使用 `providedIn: 'root'`,在 app.config.ts 中统一配置拦截器。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 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);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 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);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```typescript
|
|
|
+// app.config.ts - 统一注册拦截器
|
|
|
+export const appConfig: ApplicationConfig = {
|
|
|
+ providers: [
|
|
|
+ provideRouter(routes),
|
|
|
+ provideHttpClient(
|
|
|
+ withInterceptors([authInterceptor, errorInterceptor])
|
|
|
+ )
|
|
|
+ ]
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+**注意:在 Standalone 模式下,不再使用 NgModule,统一使用 providers 数组注册服务。**
|
|
|
+
|
|
|
+### 5.3 Shared 共享资源(按需导入)
|
|
|
+
|
|
|
+**在 Standalone 模式下,共享组件、指令、管道直接在组件中导入,不再使用 SharedModule。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 在组件中按需导入
|
|
|
+@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 功能模块(必须懒加载)
|
|
|
+
|
|
|
+**每个功能独立,使用懒加载。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 单页组件:loadComponent
|
|
|
+{
|
|
|
+ path: 'home',
|
|
|
+ loadComponent: () => import('./features/home/home/home').then(m => m.Home)
|
|
|
+}
|
|
|
+
|
|
|
+// ✅ 一组路由:loadChildren
|
|
|
+{
|
|
|
+ path: 'practice',
|
|
|
+ loadChildren: () => import('./features/practice/practice.routes').then(m => m.routes)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> 说明:是否允许 feature 内存在 `*.routes.ts`,取决于团队选择的路由组织方式。本仓库双端隔离的前提下,推荐“平台路由树内聚 + feature 可选拆分”。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 命名规范
|
|
|
+
|
|
|
+### 6.1 文件命名
|
|
|
+
|
|
|
+| 类型 | 规范 | 示例 |
|
|
|
+|-----|------|------|
|
|
|
+| 组件(TS) | `kebab-case.ts` | `user-profile.ts` |
|
|
|
+| 组件(模板) | `kebab-case.html` | `user-profile.html` |
|
|
|
+| 组件(样式) | `kebab-case.scss` | `user-profile.scss` |
|
|
|
+| 服务 | `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` 后缀) | `UserProfile` / `UserProfileComponent` |
|
|
|
+| 服务类 | PascalCase + Service | `AuthService` |
|
|
|
+| 指令类 | PascalCase + Directive | `PermissionDirective` |
|
|
|
+| 管道类 | PascalCase + Pipe | `DateFormatPipe` |
|
|
|
+| 守卫类 | PascalCase + Guard | `AuthGuard` |
|
|
|
+| 模型接口 | PascalCase | `User`, `UserProfile` |
|
|
|
+| 拦截器类 | PascalCase + Interceptor | `AuthInterceptor` |
|
|
|
+
|
|
|
+### 6.3 变量与函数命名
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 变量: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,多层嵌套用斜杠分隔。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确
|
|
|
+'/user-profile'
|
|
|
+'/order-management/list'
|
|
|
+'/admin/system-settings'
|
|
|
+
|
|
|
+// ❌ 错误
|
|
|
+'/userProfile'
|
|
|
+'/orderManagement'
|
|
|
+'/admin/systemSettings'
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 代码风格规范
|
|
|
+
|
|
|
+### 7.1 TypeScript 规范
|
|
|
+
|
|
|
+**1. 禁止使用 `any`,必须指定类型。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确
|
|
|
+const user: User = { id: 1, name: '张三' };
|
|
|
+function getUser(): Observable<User> { }
|
|
|
+
|
|
|
+// ❌ 错误
|
|
|
+const user: any = { id: 1, name: '张三' };
|
|
|
+function getUser(): any { }
|
|
|
+```
|
|
|
+
|
|
|
+**2. 使用 strict 模式。**
|
|
|
+
|
|
|
+```json
|
|
|
+// tsconfig.json
|
|
|
+{
|
|
|
+ "compilerOptions": {
|
|
|
+ "strict": true,
|
|
|
+ "noImplicitAny": true,
|
|
|
+ "strictNullChecks": true
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**3. 接口和类型别名优先使用接口。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 优先使用接口
|
|
|
+interface User {
|
|
|
+ id: number;
|
|
|
+ name: string;
|
|
|
+}
|
|
|
+
|
|
|
+// 仅在需要联合类型或元组时使用类型别名
|
|
|
+type UserRole = 'admin' | 'user' | 'guest';
|
|
|
+type Point = [number, number];
|
|
|
+```
|
|
|
+
|
|
|
+**4. 使用可选链和空值合并。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确
|
|
|
+const name = user?.profile?.name ?? '匿名';
|
|
|
+const users = data ?? [];
|
|
|
+
|
|
|
+// ❌ 错误
|
|
|
+const name = user && user.profile && user.profile.name || '匿名';
|
|
|
+```
|
|
|
+
|
|
|
+### 7.2 RxJS 规范
|
|
|
+
|
|
|
+**1. 组件中及时取消订阅。**
|
|
|
+
|
|
|
+优先使用 Angular 提供的销毁工具,减少手工 Subject 管理:
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { Component, DestroyRef, inject } from '@angular/core';
|
|
|
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
|
+
|
|
|
+@Component({...})
|
|
|
+export class UserListComponent {
|
|
|
+ private destroyRef = inject(DestroyRef);
|
|
|
+
|
|
|
+ ngOnInit() {
|
|
|
+ this.userService.getUsers()
|
|
|
+ .pipe(takeUntilDestroyed(this.destroyRef))
|
|
|
+ .subscribe(users => this.users = users);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+如必须兼容旧代码,也允许使用 `Subject + takeUntil`:
|
|
|
+
|
|
|
+```typescript
|
|
|
+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。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+@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. 统一错误处理。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 使用 catchError 操作符
|
|
|
+this.http.get<User[]>('/api/users').pipe(
|
|
|
+ catchError(error => {
|
|
|
+ console.error('请求失败', error);
|
|
|
+ return of([]);
|
|
|
+ })
|
|
|
+);
|
|
|
+```
|
|
|
+
|
|
|
+### 7.3 Signals 响应式规范
|
|
|
+
|
|
|
+**Angular 20 推荐使用 Signals 替代部分 RxJS 使用场景。**
|
|
|
+
|
|
|
+**1. 创建和使用 Signals。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+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。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 推荐:使用 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。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+@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 等 |
|
|
|
+
|
|
|
+**5. Signals 与 RxJS 互操作(推荐使用官方 interop)。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { toSignal, toObservable } from '@angular/core/rxjs-interop';
|
|
|
+import { signal } from '@angular/core';
|
|
|
+
|
|
|
+const count = signal(0);
|
|
|
+const count$ = toObservable(count);
|
|
|
+
|
|
|
+const user = toSignal(this.userService.user$);
|
|
|
+```
|
|
|
+
|
|
|
+### 7.4 注释规范
|
|
|
+
|
|
|
+**仅在必要时添加注释,避免无意义注释。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 好的注释:解释为什么
|
|
|
+// 使用 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 预处理器。**
|
|
|
+
|
|
|
+```bash
|
|
|
+ng new my-app --style=scss
|
|
|
+```
|
|
|
+
|
|
|
+### 8.2 全局样式配置
|
|
|
+
|
|
|
+**全局样式使用 `src/styles.scss`,直接定义变量和混入。**
|
|
|
+
|
|
|
+```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 配置(自动生成,无需修改):**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "styles": ["src/styles.scss"],
|
|
|
+ "stylePreprocessorOptions": {
|
|
|
+ "includePaths": ["src"]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**注意:在组件 SCSS 中可以直接使用全局变量和混入。**
|
|
|
+
|
|
|
+### 8.3 样式最佳实践
|
|
|
+
|
|
|
+**1. 使用 :host 选择器。**
|
|
|
+
|
|
|
+```scss
|
|
|
+:host {
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+:host(.featured) {
|
|
|
+ border: 2px solid gold;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**2. 禁止使用深度选择器。**
|
|
|
+
|
|
|
+```scss
|
|
|
+// ✅ 正确:使用固定类名
|
|
|
+.parent { }
|
|
|
+.parent__child { }
|
|
|
+
|
|
|
+// ❌ 错误:/deep/ 已废弃
|
|
|
+.parent /deep/ .child { }
|
|
|
+
|
|
|
+// ❌ 错误:::ng-deep 已废弃,不应使用
|
|
|
+.parent ::ng-deep .child { }
|
|
|
+```
|
|
|
+
|
|
|
+**深度选择器会破坏组件封装,应使用固定的类名或通过 @Input 传递样式配置。**
|
|
|
+
|
|
|
+**3. 响应式样式使用变量。**
|
|
|
+
|
|
|
+```scss
|
|
|
+// 在 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` 绑定未处理的内容。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确:自动转义
|
|
|
+template: `<div>{{ userContent }}</div>`
|
|
|
+
|
|
|
+// ❌ 错误:可能导致 XSS
|
|
|
+template: `<div [innerHTML]="userContent"></div>`
|
|
|
+
|
|
|
+// 如必须渲染富文本:
|
|
|
+// 1) 优先后端完成白名单净化(推荐)
|
|
|
+// 2) 前端仅展示“已净化内容”
|
|
|
+// 3) 禁止对不可信用户输入直接调用 bypassSecurityTrustHtml
|
|
|
+```
|
|
|
+
|
|
|
+### 9.2 CSRF 防护
|
|
|
+
|
|
|
+**使用 provideHttpClient 的 withXsrfConfiguration 配置 CSRF 防护。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// app.config.ts
|
|
|
+export const appConfig: ApplicationConfig = {
|
|
|
+ providers: [
|
|
|
+ provideHttpClient(
|
|
|
+ withXsrfConfiguration({
|
|
|
+ cookieName: 'XSRF-TOKEN',
|
|
|
+ headerName: 'X-XSRF-TOKEN'
|
|
|
+ })
|
|
|
+ )
|
|
|
+ ]
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+### 9.3 敏感信息处理
|
|
|
+
|
|
|
+**禁止在代码中硬编码敏感信息。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ❌ 错误
|
|
|
+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 路由守卫
|
|
|
+
|
|
|
+**敏感路由必须添加守卫。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 需要认证的路由
|
|
|
+{ path: 'profile', canActivate: [authGuard] }
|
|
|
+
|
|
|
+// 需要特定角色的路由
|
|
|
+{ path: 'admin', canActivate: [authGuard, adminGuard] }
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. 性能规范
|
|
|
+
|
|
|
+### 10.1 懒加载
|
|
|
+
|
|
|
+**所有功能必须懒加载:**
|
|
|
+
|
|
|
+- 单页:`loadComponent`
|
|
|
+- 一组路由(feature 路由树):`loadChildren`
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 正确
|
|
|
+{
|
|
|
+ path: 'users',
|
|
|
+ loadComponent: () => import('./features/users/user-list/user-list.component')
|
|
|
+ .then(m => m.UserListComponent)
|
|
|
+}
|
|
|
+
|
|
|
+// ✅ 正确:懒加载 feature 路由树
|
|
|
+{
|
|
|
+ path: 'learning',
|
|
|
+ loadChildren: () => import('./features/learning/learning.routes').then(m => m.routes)
|
|
|
+}
|
|
|
+
|
|
|
+// ❌ 错误:直接导入
|
|
|
+import { UserListComponent } from './features/users/user-list/user-list.component';
|
|
|
+{ path: 'users', component: UserListComponent } // ❌ 禁止
|
|
|
+```
|
|
|
+
|
|
|
+### 10.2 OnPush 变更检测
|
|
|
+
|
|
|
+**高频更新的组件使用 OnPush。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+@Component({
|
|
|
+ selector: 'app-user-list',
|
|
|
+ changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
+ template: `...`
|
|
|
+})
|
|
|
+export class UserListComponent {}
|
|
|
+```
|
|
|
+
|
|
|
+### 10.3 trackBy 函数
|
|
|
+
|
|
|
+**@for 循环必须使用 track。**
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- ✅ 正确 -->
|
|
|
+@for (user of users; track user.id) {
|
|
|
+ <div>{{ user.name }}</div>
|
|
|
+}
|
|
|
+
|
|
|
+<!-- ❌ 错误:缺少 track -->
|
|
|
+@for (user of users) {
|
|
|
+ <div>{{ user.name }}</div>
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 10.4 图片优化
|
|
|
+
|
|
|
+**使用 `loading="lazy"` 延迟加载图片。**
|
|
|
+
|
|
|
+```html
|
|
|
+<img src="avatar.jpg" alt="头像" loading="lazy" />
|
|
|
+```
|
|
|
+
|
|
|
+### 10.5 避免内存泄漏
|
|
|
+
|
|
|
+**1. RxJS 订阅必须取消。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+@Component({...})
|
|
|
+export class UserListComponent implements OnDestroy {
|
|
|
+ private destroy$ = new Subject<void>();
|
|
|
+
|
|
|
+ ngOnDestroy() {
|
|
|
+ this.destroy$.next();
|
|
|
+ this.destroy$.complete();
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**2. 推荐使用 Signals 替代 RxJS 管理组件状态,可以避免订阅管理问题。**
|
|
|
+
|
|
|
+```typescript
|
|
|
+// ✅ 推荐:使用 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 分析
|
|
|
+
|
|
|
+**定期分析打包大小。**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 生成打包分析
|
|
|
+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 示例
|
|
|
+
|
|
|
+```bash
|
|
|
+# 功能提交
|
|
|
+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 禁止提交
|
|
|
+
|
|
|
+**禁止提交以下内容:**
|
|
|
+
|
|
|
+```gitignore
|
|
|
+# 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 的锁文件,确保团队成员安装的依赖版本一致。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 12. 测试与 CI(建议)
|
|
|
+
|
|
|
+> 本仓库当前默认跳过组件测试生成(`skipTests: true`)。但随着业务增长,建议将关键路径纳入测试与 CI。
|
|
|
+
|
|
|
+### 12.1 建议分层测试策略
|
|
|
+
|
|
|
+- 纯函数/工具:优先单测(fast)
|
|
|
+- 服务层:HttpClient Mock + 单测(中)
|
|
|
+- 关键页面流:E2E(慢,谨慎)
|
|
|
+
|
|
|
+### 12.2 CI 最低建议项
|
|
|
+
|
|
|
+- `pnpm install --frozen-lockfile`
|
|
|
+- `pnpm build`
|
|
|
+- `pnpm test`(如果启用)
|
|
|
+- 代码格式化/静态检查(如果启用,例如 Prettier/ESLint)
|
|
|
+
|
|
|
+## 附录 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。**
|
|
|
+
|
|
|
+```json
|
|
|
+// .prettierrc
|
|
|
+{
|
|
|
+ "semi": true,
|
|
|
+ "singleQuote": true,
|
|
|
+ "tabWidth": 2,
|
|
|
+ "trailingComma": "es5",
|
|
|
+ "printWidth": 100,
|
|
|
+ "bracketSpacing": true,
|
|
|
+ "arrowParens": "avoid",
|
|
|
+ "endOfLine": "auto"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**安装和使用:**
|
|
|
+
|
|
|
+```bash
|
|
|
+pnpm add -D prettier
|
|
|
+pnpm exec prettier --write src/
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+*文档版本:2.0.0 | 对应 Angular 20.x | 更新日期:2025*
|