跳到主要内容

Angular常用组件开发指南

Angular组件是构建Angular应用的基础构建块。通过组件化开发,我们可以创建可复用、可测试和可维护的界面元素。本文将介绍Angular常用组件的开发实践。

核心价值

Angular组件化开发优势

  • 🧩 代码复用:封装UI和逻辑为可重用组件
  • 🛠️ 关注点分离:每个组件负责特定功能
  • 📦 声明式模板:直观的HTML模板语法
  • 🔄 单向数据流:更易于理解的数据流动
  • 🔍 变更检测:高效的组件更新机制

1. 组件基础设计模式

1.1 展示型组件 vs 容器型组件

Angular应用中常用的组件设计模式是将组件分为展示型组件和容器型组件:

  1. 展示型组件(Presentational Components)

    • 专注于UI渲染
    • 通过Input接收数据,通过Output发送事件
    • 不依赖外部服务和状态管理
    • 容易测试和重用
  2. 容器型组件(Container Components)

    • 处理数据获取和状态管理
    • 包含业务逻辑
    • 注入服务和与后端交互
    • 将数据传递给展示型组件
展示型组件示例
typescript
1import { Component, Input, Output, EventEmitter } from '@angular/core';
2
3@Component({
4 selector: 'app-user-profile-card',
5 template: `
6 <div class="user-card">
7 <img [src]="user.avatar" alt="{{ user.name }}">
8 <h3>{{ user.name }}</h3>
9 <p>{{ user.email }}</p>
10 <button (click)="onContact()">联系用户</button>
11 </div>
12 `,
13 styleUrls: ['./user-profile-card.component.scss']
14})
15export class UserProfileCardComponent {
16 @Input() user: any;
17 @Output() contact = new EventEmitter<any>();
18
19 onContact() {
20 this.contact.emit(this.user);
21 }
22}
容器型组件示例
typescript
1import { Component, OnInit } from '@angular/core';
2import { UserService } from '../../services/user.service';
3
4@Component({
5 selector: 'app-user-dashboard',
6 template: `
7 <div *ngIf="loading">加载中...</div>
8 <div *ngIf="error">{{ error }}</div>
9
10 <div class="user-grid">
11 <app-user-profile-card
12 *ngFor="let user of users"
13 [user]="user"
14 (contact)="contactUser($event)">
15 </app-user-profile-card>
16 </div>
17 `,
18 styleUrls: ['./user-dashboard.component.scss']
19})
20export class UserDashboardComponent implements OnInit {
21 users = [];
22 loading = false;
23 error = '';
24
25 constructor(private userService: UserService) {}
26
27 ngOnInit() {
28 this.loading = true;
29 this.userService.getUsers()
30 .subscribe({
31 next: (data) => {
32 this.users = data;
33 this.loading = false;
34 },
35 error: (err) => {
36 this.error = '无法加载用户数据';
37 this.loading = false;
38 console.error(err);
39 }
40 });
41 }
42
43 contactUser(user) {
44 this.userService.initiateContact(user);
45 }
46}

1.2 组件通信策略

组件之间的通信是构建Angular应用的关键部分:

  1. 父组件 -> 子组件: 使用@Input()装饰器
  2. 子组件 -> 父组件: 使用@Output()EventEmitter
  3. 无直接关系组件: 使用服务和RxJS Subject/BehaviorSubject
  4. 跨多层级组件: 依赖注入和提供者层级
使用服务进行跨组件通信
typescript
1// 消息服务
2import { Injectable } from '@angular/core';
3import { BehaviorSubject, Observable } from 'rxjs';
4
5@Injectable({
6 providedIn: 'root'
7})
8export class MessageService {
9 private messageSource = new BehaviorSubject<string>('默认消息');
10 currentMessage = this.messageSource.asObservable();
11
12 constructor() { }
13
14 changeMessage(message: string) {
15 this.messageSource.next(message);
16 }
17}
18
19// 发送组件
20@Component({
21 selector: 'app-sender',
22 template: `
23 <input [(ngModel)]="message" placeholder="输入消息">
24 <button (click)="sendMessage()">发送</button>
25 `
26})
27export class SenderComponent {
28 message: string;
29
30 constructor(private messageService: MessageService) {}
31
32 sendMessage() {
33 this.messageService.changeMessage(this.message);
34 }
35}
36
37// 接收组件
38@Component({
39 selector: 'app-receiver',
40 template: `<p>当前消息: {{ message }}</p>`
41})
42export class ReceiverComponent implements OnInit {
43 message: string;
44
45 constructor(private messageService: MessageService) {}
46
47 ngOnInit() {
48 this.messageService.currentMessage.subscribe(message => {
49 this.message = message;
50 });
51 }
52
53 ngOnDestroy() {
54 // 重要:取消订阅避免内存泄漏
55 this.subscription.unsubscribe();
56 }
57}

2. 表单组件

表单组件是任何Web应用的核心部分。Angular提供了强大的表单控制功能,下面介绍几个常用的表单组件实现。

2.1 高级输入框组件

一个带有验证、标签和错误提示的增强输入框组件:

advanced-input.component.ts
typescript
1import { Component, forwardRef, Input } from '@angular/core';
2import { ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl, Validator } from '@angular/forms';
3
4@Component({
5 selector: 'app-advanced-input',
6 template: `
7 <div class="form-field" [class.has-error]="showError">
8 <label *ngIf="label" [for]="id" class="form-label">
9 {{ label }}
10 <span class="required" *ngIf="required">*</span>
11 </label>
12
13 <div class="input-container">
14 <input
15 [type]="type"
16 [id]="id"
17 class="form-input"
18 [placeholder]="placeholder"
19 [value]="value"
20 [disabled]="disabled"
21 (input)="onInputChange($event)"
22 (blur)="onTouched()"
23 />
24
25 <button
26 *ngIf="value && clearable"
27 type="button"
28 class="clear-button"
29 (click)="clearValue()"
30 aria-label="Clear input">
31
32 </button>
33 </div>
34
35 <div *ngIf="showError" class="error-message">
36 {{ errorMessage }}
37 </div>
38 </div>
39 `,
40 styles: [`
41 .form-field {
42 margin-bottom: 1rem;
43 }
44
45 .form-label {
46 display: block;
47 margin-bottom: 0.5rem;
48 font-weight: 500;
49 }
50
51 .input-container {
52 position: relative;
53 display: flex;
54 }
55
56 .form-input {
57 width: 100%;
58 padding: 0.75rem 1rem;
59 border: 1px solid #ccc;
60 border-radius: 4px;
61 font-size: 1rem;
62 transition: border-color 0.2s, box-shadow 0.2s;
63 }
64
65 .form-input:focus {
66 outline: none;
67 border-color: #4299e1;
68 box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
69 }
70
71 .has-error .form-input {
72 border-color: #e53e3e;
73 }
74
75 .has-error .form-input:focus {
76 box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.2);
77 }
78
79 .clear-button {
80 position: absolute;
81 right: 0.75rem;
82 top: 50%;
83 transform: translateY(-50%);
84 background: none;
85 border: none;
86 color: #718096;
87 cursor: pointer;
88 padding: 0.25rem;
89 line-height: 1;
90 }
91
92 .required {
93 color: #e53e3e;
94 margin-left: 0.25rem;
95 }
96
97 .error-message {
98 color: #e53e3e;
99 font-size: 0.875rem;
100 margin-top: 0.5rem;
101 }
102 `],
103 providers: [
104 {
105 provide: NG_VALUE_ACCESSOR,
106 useExisting: forwardRef(() => AdvancedInputComponent),
107 multi: true
108 },
109 {
110 provide: NG_VALIDATORS,
111 useExisting: forwardRef(() => AdvancedInputComponent),
112 multi: true
113 }
114 ]
115})
116export class AdvancedInputComponent implements ControlValueAccessor, Validator {
117 @Input() label: string;
118 @Input() placeholder: string = '';
119 @Input() type: string = 'text';
120 @Input() id: string = `input-${Math.random().toString(36).substring(2, 11)}`;
121 @Input() required: boolean = false;
122 @Input() clearable: boolean = true;
123 @Input() minLength: number = 0;
124 @Input() maxLength: number;
125 @Input() pattern: string;
126 @Input() customValidator: (value: any) => { valid: boolean, message: string };
127
128 value: any = '';
129 disabled: boolean = false;
130 touched: boolean = false;
131 errorMessage: string = '';
132
133 onChange: any = () => {};
134 onTouched: any = () => {};
135
136 constructor() {}
137
138 // ControlValueAccessor 接口
139 writeValue(value: any): void {
140 this.value = value;
141 }
142
143 registerOnChange(fn: any): void {
144 this.onChange = fn;
145 }
146
147 registerOnTouched(fn: any): void {
148 this.onTouched = fn;
149 }
150
151 setDisabledState(isDisabled: boolean): void {
152 this.disabled = isDisabled;
153 }
154
155 // 当输入改变时
156 onInputChange(event: Event): void {
157 const input = event.target as HTMLInputElement;
158 this.value = input.value;
159 this.onChange(this.value);
160 this.validate(null);
161 }
162
163 // 清除输入值
164 clearValue(): void {
165 this.value = '';
166 this.onChange(this.value);
167 this.onTouched();
168 }
169
170 // Validator 接口
171 validate(control: FormControl): {[key: string]: any} | null {
172 // 如果组件控制自己的值,使用this.value代替control.value
173 const value = control ? control.value : this.value;
174
175 // 必填验证
176 if (this.required && !value) {
177 this.errorMessage = '此字段为必填项';
178 return { 'required': true };
179 }
180
181 // 最小长度验证
182 if (this.minLength && value && value.length < this.minLength) {
183 this.errorMessage = `最少需要 ${this.minLength} 个字符`;
184 return { 'minlength': { requiredLength: this.minLength, actualLength: value.length } };
185 }
186
187 // 最大长度验证
188 if (this.maxLength && value && value.length > this.maxLength) {
189 this.errorMessage = `最多允许 ${this.maxLength} 个字符`;
190 return { 'maxlength': { requiredLength: this.maxLength, actualLength: value.length } };
191 }
192
193 // 模式验证
194 if (this.pattern && value) {
195 const regex = new RegExp(this.pattern);
196 if (!regex.test(value)) {
197 this.errorMessage = '输入格式不正确';
198 return { 'pattern': { requiredPattern: this.pattern, actualValue: value } };
199 }
200 }
201
202 // 自定义验证
203 if (this.customValidator && value) {
204 const result = this.customValidator(value);
205 if (!result.valid) {
206 this.errorMessage = result.message;
207 return { 'custom': true };
208 }
209 }
210
211 // 验证通过
212 this.errorMessage = '';
213 return null;
214 }
215
216 // 显示错误的条件
217 get showError(): boolean {
218 return this.errorMessage !== '' && this.touched;
219 }
220}

使用方法示例:

html
1<form [formGroup]="userForm">
2 <app-advanced-input
3 label="用户名"
4 formControlName="username"
5 required
6 [minLength]="3"
7 [maxLength]="20"
8 ></app-advanced-input>
9
10 <app-advanced-input
11 label="电子邮箱"
12 type="email"
13 formControlName="email"
14 required
15 [pattern]="emailPattern"
16 ></app-advanced-input>
17
18 <button type="submit" [disabled]="userForm.invalid">提交</button>
19</form>

2.2 下拉选择组件

带有搜索功能的下拉选择组件:

searchable-select.component.ts
typescript
1import { Component, forwardRef, Input, ElementRef, ViewChild, HostListener } from '@angular/core';
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
3
4interface Option {
5 value: any;
6 label: string;
7}
8
9@Component({
10 selector: 'app-searchable-select',
11 template: `
12 <div class="select-container">
13 <label *ngIf="label" [for]="id" class="select-label">
14 {{ label }}
15 <span class="required" *ngIf="required">*</span>
16 </label>
17
18 <div
19 class="select-input"
20 [class.open]="isOpen"
21 [class.disabled]="disabled"
22 (click)="toggleDropdown()">
23 <span *ngIf="selectedOption; else placeholderTpl">{{ selectedOption.label }}</span>
24 <ng-template #placeholderTpl>
25 <span class="placeholder">{{ placeholder }}</span>
26 </ng-template>
27
28 <div class="select-arrow">▼</div>
29 </div>
30
31 <div class="dropdown" *ngIf="isOpen">
32 <div class="search-container">
33 <input
34 #searchInput
35 type="text"
36 class="search-input"
37 placeholder="搜索..."
38 [value]="searchText"
39 (input)="onSearch($event)"
40 (click)="$event.stopPropagation()">
41 </div>
42
43 <div class="options-list" *ngIf="filteredOptions.length > 0">
44 <div
45 *ngFor="let option of filteredOptions"
46 class="option-item"
47 [class.selected]="value === option.value"
48 (click)="selectOption(option); $event.stopPropagation()">
49 {{ option.label }}
50 </div>
51 </div>
52
53 <div class="no-results" *ngIf="filteredOptions.length === 0">
54 无匹配结果
55 </div>
56 </div>
57 </div>
58 `,
59 styles: [`
60 .select-container {
61 position: relative;
62 margin-bottom: 1rem;
63 }
64
65 .select-label {
66 display: block;
67 margin-bottom: 0.5rem;
68 font-weight: 500;
69 }
70
71 .select-input {
72 display: flex;
73 justify-content: space-between;
74 align-items: center;
75 width: 100%;
76 padding: 0.75rem 1rem;
77 border: 1px solid #ccc;
78 border-radius: 4px;
79 background-color: #fff;
80 cursor: pointer;
81 user-select: none;
82 transition: border-color 0.2s, box-shadow 0.2s;
83 }
84
85 .select-input:hover {
86 border-color: #a0aec0;
87 }
88
89 .select-input.open {
90 border-color: #4299e1;
91 box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
92 }
93
94 .select-input.disabled {
95 background-color: #f7fafc;
96 cursor: not-allowed;
97 color: #a0aec0;
98 }
99
100 .placeholder {
101 color: #a0aec0;
102 }
103
104 .select-arrow {
105 font-size: 0.75rem;
106 color: #718096;
107 transition: transform 0.2s;
108 }
109
110 .open .select-arrow {
111 transform: rotate(180deg);
112 }
113
114 .dropdown {
115 position: absolute;
116 top: 100%;
117 left: 0;
118 width: 100%;
119 background: #fff;
120 border: 1px solid #e2e8f0;
121 border-radius: 4px;
122 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
123 z-index: 10;
124 margin-top: 4px;
125 max-height: 300px;
126 display: flex;
127 flex-direction: column;
128 }
129
130 .search-container {
131 padding: 0.5rem;
132 border-bottom: 1px solid #e2e8f0;
133 }
134
135 .search-input {
136 width: 100%;
137 padding: 0.5rem;
138 border: 1px solid #e2e8f0;
139 border-radius: 4px;
140 font-size: 0.875rem;
141 }
142
143 .options-list {
144 overflow-y: auto;
145 max-height: 240px;
146 }
147
148 .option-item {
149 padding: 0.75rem 1rem;
150 cursor: pointer;
151 transition: background-color 0.15s;
152 }
153
154 .option-item:hover {
155 background-color: #f7fafc;
156 }
157
158 .option-item.selected {
159 background-color: #ebf8ff;
160 color: #3182ce;
161 font-weight: 500;
162 }
163
164 .no-results {
165 padding: 0.75rem 1rem;
166 text-align: center;
167 color: #a0aec0;
168 font-style: italic;
169 }
170
171 .required {
172 color: #e53e3e;
173 margin-left: 0.25rem;
174 }
175 `],
176 providers: [
177 {
178 provide: NG_VALUE_ACCESSOR,
179 useExisting: forwardRef(() => SearchableSelectComponent),
180 multi: true
181 }
182 ]
183})
184export class SearchableSelectComponent implements ControlValueAccessor {
185 @Input() options: Option[] = [];
186 @Input() label: string;
187 @Input() placeholder: string = '请选择';
188 @Input() id: string = `select-${Math.random().toString(36).substring(2, 11)}`;
189 @Input() required: boolean = false;
190
191 @ViewChild('searchInput') searchInput: ElementRef;
192
193 value: any = null;
194 disabled: boolean = false;
195 isOpen: boolean = false;
196 searchText: string = '';
197
198 onChange: any = () => {};
199 onTouched: any = () => {};
200
201 constructor(private elementRef: ElementRef) {}
202
203 // 关闭下拉框的点击外部元素事件
204 @HostListener('document:click', ['$event'])
205 onClickOutside(event: Event) {
206 if (!this.elementRef.nativeElement.contains(event.target)) {
207 this.isOpen = false;
208 }
209 }
210
211 // ControlValueAccessor 接口
212 writeValue(value: any): void {
213 this.value = value;
214 }
215
216 registerOnChange(fn: any): void {
217 this.onChange = fn;
218 }
219
220 registerOnTouched(fn: any): void {
221 this.onTouched = fn;
222 }
223
224 setDisabledState(isDisabled: boolean): void {
225 this.disabled = isDisabled;
226 }
227
228 // 切换下拉框状态
229 toggleDropdown(): void {
230 if (this.disabled) return;
231
232 this.isOpen = !this.isOpen;
233 this.searchText = '';
234
235 if (this.isOpen) {
236 setTimeout(() => {
237 this.searchInput?.nativeElement.focus();
238 });
239 }
240 }
241
242 // 选择选项
243 selectOption(option: Option): void {
244 this.value = option.value;
245 this.onChange(this.value);
246 this.onTouched();
247 this.isOpen = false;
248 }
249
250 // 搜索
251 onSearch(event: Event): void {
252 this.searchText = (event.target as HTMLInputElement).value;
253 }
254
255 // 过滤选项
256 get filteredOptions(): Option[] {
257 if (!this.searchText) return this.options;
258
259 return this.options.filter(option =>
260 option.label.toLowerCase().includes(this.searchText.toLowerCase())
261 );
262 }
263
264 // 获取选中项
265 get selectedOption(): Option | undefined {
266 return this.options.find(option => option.value === this.value);
267 }
268}

使用方法示例:

html
1<form [formGroup]="productForm">
2 <app-searchable-select
3 label="分类"
4 [options]="categoryOptions"
5 formControlName="category"
6 required
7 ></app-searchable-select>
8</form>
9
10<script>
11 // 组件代码
12 categoryOptions = [
13 { value: 'electronics', label: '电子产品' },
14 { value: 'clothing', label: '服装' },
15 { value: 'books', label: '图书' },
16 { value: 'food', label: '食品' },
17 // ...更多选项
18 ];
19</script>

3. 布局组件

布局组件帮助构建页面的整体结构和组织内容。下面介绍几个实用的布局组件。

3.1 网格系统组件

一个灵活的响应式网格系统组件:

grid-row.component.ts
typescript
1import { Component, Input, HostBinding } from '@angular/core';
2
3@Component({
4 selector: 'app-grid-row',
5 template: `<ng-content></ng-content>`,
6 styles: [`
7 :host {
8 display: flex;
9 flex-wrap: wrap;
10 box-sizing: border-box;
11 }
12 `]
13})
14export class GridRowComponent {
15 @Input() gutter: number = 0;
16 @Input() justify: 'start' | 'end' | 'center' | 'space-between' | 'space-around' = 'start';
17 @Input() align: 'top' | 'middle' | 'bottom' = 'top';
18
19 // 映射到flex属性
20 private justifyMap = {
21 'start': 'flex-start',
22 'end': 'flex-end',
23 'center': 'center',
24 'space-between': 'space-between',
25 'space-around': 'space-around'
26 };
27
28 private alignMap = {
29 'top': 'flex-start',
30 'middle': 'center',
31 'bottom': 'flex-end'
32 };
33
34 @HostBinding('style.margin-left.px')
35 @HostBinding('style.margin-right.px')
36 get marginOffset() {
37 return this.gutter ? -(this.gutter / 2) : 0;
38 }
39
40 @HostBinding('style.justify-content')
41 get justifyContent() {
42 return this.justifyMap[this.justify];
43 }
44
45 @HostBinding('style.align-items')
46 get alignItems() {
47 return this.alignMap[this.align];
48 }
49}
grid-column.component.ts
typescript
1import { Component, Input, HostBinding, OnInit } from '@angular/core';
2
3@Component({
4 selector: 'app-grid-col',
5 template: `<ng-content></ng-content>`,
6 styles: [`
7 :host {
8 box-sizing: border-box;
9 }
10
11 /* 响应式断点 - 可以根据需求调整 */
12 @media (max-width: 575.98px) {
13 :host.responsive {
14 flex: 0 0 100%;
15 max-width: 100%;
16 }
17 }
18 `]
19})
20export class GridColumnComponent implements OnInit {
21 @Input() span: number = 24;
22 @Input() offset: number = 0;
23 @Input() xs: number | object;
24 @Input() sm: number | object;
25 @Input() md: number | object;
26 @Input() lg: number | object;
27 @Input() xl: number | object;
28 @Input() gutter: number = 0;
29
30 @HostBinding('style.padding-left.px')
31 @HostBinding('style.padding-right.px')
32 get padding() {
33 return this.gutter ? this.gutter / 2 : 0;
34 }
35
36 @HostBinding('style.flex')
37 @HostBinding('style.max-width')
38 get flexStyle() {
39 return `0 0 ${(this.span / 24 * 100).toFixed(8)}%`;
40 }
41
42 @HostBinding('style.margin-left')
43 get offsetStyle() {
44 return this.offset ? `${(this.offset / 24 * 100).toFixed(8)}%` : null;
45 }
46
47 @HostBinding('class.responsive')
48 get isResponsive() {
49 return !!(this.xs || this.sm || this.md || this.lg || this.xl);
50 }
51
52 constructor() {}
53
54 ngOnInit() {
55 this.setResponsiveStyles();
56 }
57
58 private setResponsiveStyles() {
59 // 这里可以添加更复杂的响应式样式处理逻辑
60 // 例如创建媒体查询样式并添加到document.head
61 }
62}

使用方法示例:

html
1<app-grid-row [gutter]="16" justify="space-between" align="middle">
2 <app-grid-col [span]="8">
3 <div class="column-content">列 1</div>
4 </app-grid-col>
5 <app-grid-col [span]="8">
6 <div class="column-content">列 2</div>
7 </app-grid-col>
8 <app-grid-col [span]="8">
9 <div class="column-content">列 3</div>
10 </app-grid-col>
11</app-grid-row>
12
13<app-grid-row [gutter]="24">
14 <app-grid-col [span]="6" [xs]="24" [sm]="12" [md]="8" [lg]="6">
15 <div class="column-content">响应式列</div>
16 </app-grid-col>
17 <app-grid-col [span]="6" [xs]="24" [sm]="12" [md]="8" [lg]="6">
18 <div class="column-content">响应式列</div>
19 </app-grid-col>
20 <app-grid-col [span]="6" [xs]="24" [sm]="12" [md]="8" [lg]="6">
21 <div class="column-content">响应式列</div>
22 </app-grid-col>
23 <app-grid-col [span]="6" [xs]="24" [sm]="12" [md]="8" [lg]="6">
24 <div class="column-content">响应式列</div>
25 </app-grid-col>
26</app-grid-row>

3.2 卡片组件

一个多功能的卡片组件,用于展示内容:

card.component.ts
typescript
1import { Component, Input } from '@angular/core';
2
3@Component({
4 selector: 'app-card',
5 template: `
6 <div class="card" [ngClass]="cardClasses">
7 <!-- 卡片头部 -->
8 <div class="card-header" *ngIf="title || subtitle || headerTemplate">
9 <ng-container *ngIf="!headerTemplate">
10 <h3 class="card-title" *ngIf="title">{{ title }}</h3>
11 <div class="card-subtitle" *ngIf="subtitle">{{ subtitle }}</div>
12 </ng-container>
13 <ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
14 </div>
15
16 <!-- 卡片封面 -->
17 <div class="card-cover" *ngIf="coverImage">
18 <img [src]="coverImage" [alt]="title || 'Card image'">
19 </div>
20
21 <!-- 卡片内容 -->
22 <div class="card-body">
23 <ng-content></ng-content>
24 </div>
25
26 <!-- 卡片操作区 -->
27 <div class="card-actions" *ngIf="actionsTemplate">
28 <ng-container *ngTemplateOutlet="actionsTemplate"></ng-container>
29 </div>
30 </div>
31 `,
32 styles: [`
33 .card {
34 background-color: #fff;
35 border-radius: 8px;
36 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
37 overflow: hidden;
38 transition: box-shadow 0.3s, transform 0.3s;
39 margin-bottom: 1.5rem;
40 display: flex;
41 flex-direction: column;
42 }
43
44 .card.hoverable:hover {
45 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
46 transform: translateY(-4px);
47 }
48
49 .card.bordered {
50 box-shadow: none;
51 border: 1px solid #eee;
52 }
53
54 .card-header {
55 padding: 1rem 1.5rem;
56 border-bottom: 1px solid #f0f0f0;
57 display: flex;
58 flex-direction: column;
59 }
60
61 .card-title {
62 margin: 0;
63 font-size: 1.25rem;
64 font-weight: 500;
65 color: rgba(0, 0, 0, 0.85);
66 line-height: 1.4;
67 }
68
69 .card-subtitle {
70 margin-top: 0.25rem;
71 color: rgba(0, 0, 0, 0.45);
72 font-size: 0.875rem;
73 }
74
75 .card-cover img {
76 width: 100%;
77 display: block;
78 }
79
80 .card-body {
81 padding: 1.5rem;
82 flex: 1;
83 }
84
85 .card-actions {
86 padding: 0.75rem 1.5rem;
87 border-top: 1px solid #f0f0f0;
88 background: #fafafa;
89 }
90
91 /* 尺寸变体 */
92 .card.small {
93 font-size: 0.875rem;
94 }
95
96 .card.small .card-header {
97 padding: 0.75rem 1rem;
98 }
99
100 .card.small .card-body {
101 padding: 1rem;
102 }
103
104 .card.small .card-title {
105 font-size: 1rem;
106 }
107
108 .card.large .card-header {
109 padding: 1.5rem 2rem;
110 }
111
112 .card.large .card-body {
113 padding: 2rem;
114 }
115
116 .card.large .card-title {
117 font-size: 1.5rem;
118 }
119 `]
120})
121export class CardComponent {
122 @Input() title: string;
123 @Input() subtitle: string;
124 @Input() coverImage: string;
125 @Input() bordered: boolean = false;
126 @Input() hoverable: boolean = false;
127 @Input() size: 'small' | 'default' | 'large' = 'default';
128 @Input() headerTemplate: any; // ng-template引用
129 @Input() actionsTemplate: any; // ng-template引用
130
131 get cardClasses(): {[key: string]: boolean} {
132 return {
133 'bordered': this.bordered,
134 'hoverable': this.hoverable,
135 'small': this.size === 'small',
136 'large': this.size === 'large'
137 };
138 }
139}

使用方法示例:

html
1<!-- 基本用法 -->
2<app-card
3 title="卡片标题"
4 subtitle="卡片副标题"
5 [hoverable]="true"
6 coverImage="assets/images/card-image.jpg">
7 <p>这是卡片内容区域,可以放置任何内容。</p>
8</app-card>
9
10<!-- 使用模板自定义头部和操作区 -->
11<app-card [headerTemplate]="customHeader" [actionsTemplate]="customActions">
12 <p>卡片内容区域</p>
13</app-card>
14
15<ng-template #customHeader>
16 <div class="custom-header">
17 <img src="assets/icons/user.svg" alt="User icon">
18 <div>
19 <h3>自定义标题</h3>
20 <span>创建于:{{ createdDate | date }}</span>
21 </div>
22 </div>
23</ng-template>
24
25<ng-template #customActions>
26 <div class="action-buttons">
27 <button (click)="onEdit()">编辑</button>
28 <button (click)="onDelete()">删除</button>
29 </div>
30</ng-template>

3.3 分隔面板组件

一个可折叠的面板组件,适合展示分组内容:

collapse.component.ts
typescript
1import { Component, Input } from '@angular/core';
2
3@Component({
4 selector: 'app-collapse',
5 template: `
6 <div class="collapse-container">
7 <ng-content></ng-content>
8 </div>
9 `,
10 styles: [`
11 .collapse-container {
12 border: 1px solid #eee;
13 border-radius: 4px;
14 }
15 `]
16})
17export class CollapseComponent {
18 @Input() accordion: boolean = false;
19
20 activeKeys: string[] = [];
21
22 togglePanel(key: string): void {
23 if (this.accordion) {
24 this.activeKeys = this.activeKeys.includes(key) ? [] : [key];
25 } else {
26 const index = this.activeKeys.indexOf(key);
27 if (index > -1) {
28 this.activeKeys.splice(index, 1);
29 } else {
30 this.activeKeys.push(key);
31 }
32 }
33 }
34}
collapse-panel.component.ts
typescript
1import { Component, Input, Host, Optional } from '@angular/core';
2import { CollapseComponent } from './collapse.component';
3
4@Component({
5 selector: 'app-collapse-panel',
6 template: `
7 <div class="collapse-panel" [class.active]="isActive">
8 <div class="panel-header" (click)="toggle()">
9 <div class="header-content">{{ header }}</div>
10 <div class="expand-icon">
11 <i class="arrow" [class.down]="!isActive" [class.up]="isActive"></i>
12 </div>
13 </div>
14
15 <div class="panel-content" [style.height]="isActive ? contentHeight : '0'">
16 <div class="content-inner" #contentWrapper>
17 <ng-content></ng-content>
18 </div>
19 </div>
20 </div>
21 `,
22 styles: [`
23 .collapse-panel {
24 border-bottom: 1px solid #eee;
25 }
26
27 .collapse-panel:last-child {
28 border-bottom: none;
29 }
30
31 .panel-header {
32 padding: 1rem;
33 display: flex;
34 justify-content: space-between;
35 align-items: center;
36 cursor: pointer;
37 user-select: none;
38 color: rgba(0, 0, 0, 0.85);
39 font-weight: 500;
40 }
41
42 .panel-header:hover {
43 background-color: #fafafa;
44 }
45
46 .collapse-panel.active .panel-header {
47 border-bottom: 1px solid #eee;
48 }
49
50 .arrow {
51 border: solid #999;
52 border-width: 0 2px 2px 0;
53 display: inline-block;
54 padding: 3px;
55 transition: transform 0.3s;
56 }
57
58 .arrow.down {
59 transform: rotate(45deg);
60 }
61
62 .arrow.up {
63 transform: rotate(-135deg);
64 }
65
66 .panel-content {
67 overflow: hidden;
68 transition: height 0.3s ease-out;
69 }
70
71 .content-inner {
72 padding: 1rem;
73 color: rgba(0, 0, 0, 0.65);
74 }
75 `]
76})
77export class CollapsePanelComponent {
78 @Input() header: string;
79 @Input() key: string = Math.random().toString(36).substring(2, 9);
80 @Input() disabled: boolean = false;
81
82 private _contentHeight = 'auto';
83
84 get isActive(): boolean {
85 return this.collapseComponent?.activeKeys.includes(this.key) ?? false;
86 }
87
88 get contentHeight(): string {
89 return this.isActive ? this._contentHeight : '0';
90 }
91
92 constructor(@Optional() @Host() private collapseComponent: CollapseComponent) {}
93
94 toggle(): void {
95 if (this.disabled) return;
96
97 this.collapseComponent?.togglePanel(this.key);
98 }
99}

使用方法示例:

html
1<!-- 基本折叠面板 -->
2<app-collapse>
3 <app-collapse-panel header="折叠面板 1" key="panel1">
4 <p>这里是面板1的内容区域。</p>
5 </app-collapse-panel>
6
7 <app-collapse-panel header="折叠面板 2" key="panel2">
8 <p>这里是面板2的内容区域。</p>
9 <p>可以包含任意内容,比如文本、表单或其他组件。</p>
10 </app-collapse-panel>
11
12 <app-collapse-panel header="折叠面板 3" key="panel3" [disabled]="true">
13 <p>这个面板被禁用了,无法点击展开。</p>
14 </app-collapse-panel>
15</app-collapse>
16
17<!-- 手风琴模式 -->
18<app-collapse [accordion]="true">
19 <app-collapse-panel header="部分 1" key="section1">
20 <p>手风琴模式下一次只能展开一个面板。</p>
21 </app-collapse-panel>
22
23 <app-collapse-panel header="部分 2" key="section2">
24 <p>点击这个面板会自动关闭其他面板。</p>
25 </app-collapse-panel>
26</app-collapse>

4. 功能组件

功能组件提供特定的交互功能,如模态对话框、通知提醒等。

4.1 模态对话框组件

一个可复用的模态对话框组件:

modal.component.ts
typescript
1import { Component, Input, Output, EventEmitter, TemplateRef } from '@angular/core';
2import { trigger, state, style, transition, animate } from '@angular/animations';
3
4@Component({
5 selector: 'app-modal',
6 template: `
7 <div class="modal-backdrop" *ngIf="visible" (click)="onBackdropClick()" [@backdropAnimation]="'show'">
8 <div class="modal-dialog" [class]="modalSizeClass" (click)="$event.stopPropagation()" [@dialogAnimation]="'show'">
9 <!-- 模态框头部 -->
10 <div class="modal-header">
11 <ng-container *ngIf="!customHeader">
12 <h4 class="modal-title">{{ title }}</h4>
13 </ng-container>
14 <ng-container *ngTemplateOutlet="customHeader"></ng-container>
15
16 <button *ngIf="showClose" type="button" class="close-button" aria-label="Close" (click)="close()">
17 &times;
18 </button>
19 </div>
20
21 <!-- 模态框内容 -->
22 <div class="modal-body">
23 <ng-content></ng-content>
24 <ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
25 </div>
26
27 <!-- 模态框底部 -->
28 <div class="modal-footer" *ngIf="showFooter">
29 <ng-container *ngIf="!customFooter">
30 <button type="button" class="btn btn-secondary" (click)="close()">{{ cancelText }}</button>
31 <button type="button" class="btn btn-primary" (click)="confirm()">{{ confirmText }}</button>
32 </ng-container>
33 <ng-container *ngTemplateOutlet="customFooter"></ng-container>
34 </div>
35 </div>
36 </div>
37 `,
38 styles: [`
39 .modal-backdrop {
40 position: fixed;
41 top: 0;
42 left: 0;
43 width: 100%;
44 height: 100%;
45 background-color: rgba(0, 0, 0, 0.5);
46 display: flex;
47 align-items: center;
48 justify-content: center;
49 z-index: 1050;
50 }
51
52 .modal-dialog {
53 background-color: #fff;
54 border-radius: 4px;
55 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
56 max-width: 90%;
57 max-height: 90vh;
58 display: flex;
59 flex-direction: column;
60 overflow: hidden;
61 }
62
63 .modal-size-small {
64 width: 400px;
65 }
66
67 .modal-size-medium {
68 width: 600px;
69 }
70
71 .modal-size-large {
72 width: 800px;
73 }
74
75 .modal-header {
76 display: flex;
77 justify-content: space-between;
78 align-items: center;
79 padding: 1rem 1.5rem;
80 border-bottom: 1px solid #eee;
81 }
82
83 .modal-title {
84 margin: 0;
85 font-weight: 500;
86 color: rgba(0, 0, 0, 0.85);
87 }
88
89 .close-button {
90 background: transparent;
91 border: none;
92 font-size: 1.5rem;
93 font-weight: 700;
94 line-height: 1;
95 color: #999;
96 cursor: pointer;
97 padding: 0;
98 }
99
100 .close-button:hover {
101 color: #333;
102 }
103
104 .modal-body {
105 padding: 1.5rem;
106 overflow-y: auto;
107 flex: 1;
108 }
109
110 .modal-footer {
111 padding: 1rem 1.5rem;
112 border-top: 1px solid #eee;
113 display: flex;
114 justify-content: flex-end;
115 gap: 0.5rem;
116 }
117
118 .btn {
119 padding: 0.5rem 1rem;
120 border-radius: 4px;
121 font-weight: 400;
122 border: 1px solid transparent;
123 cursor: pointer;
124 transition: all 0.2s;
125 }
126
127 .btn-primary {
128 background-color: #1890ff;
129 border-color: #1890ff;
130 color: white;
131 }
132
133 .btn-primary:hover {
134 background-color: #40a9ff;
135 border-color: #40a9ff;
136 }
137
138 .btn-secondary {
139 background-color: white;
140 border-color: #d9d9d9;
141 color: rgba(0, 0, 0, 0.65);
142 }
143
144 .btn-secondary:hover {
145 background-color: #f5f5f5;
146 border-color: #d9d9d9;
147 }
148 `],
149 animations: [
150 trigger('backdropAnimation', [
151 state('void', style({
152 opacity: 0
153 })),
154 state('show', style({
155 opacity: 1
156 })),
157 transition('void => show', animate('200ms')),
158 transition('show => void', animate('200ms'))
159 ]),
160 trigger('dialogAnimation', [
161 state('void', style({
162 transform: 'scale(0.7)',
163 opacity: 0
164 })),
165 state('show', style({
166 transform: 'scale(1)',
167 opacity: 1
168 })),
169 transition('void => show', animate('200ms cubic-bezier(0.35, 0, 0.25, 1)')),
170 transition('show => void', animate('200ms cubic-bezier(0.35, 0, 0.25, 1)'))
171 ])
172 ]
173})
174export class ModalComponent {
175 @Input() visible: boolean = false;
176 @Input() title: string = '对话框';
177 @Input() size: 'small' | 'medium' | 'large' = 'medium';
178 @Input() showClose: boolean = true;
179 @Input() showFooter: boolean = true;
180 @Input() closeOnBackdrop: boolean = true;
181 @Input() customHeader: TemplateRef<any>;
182 @Input() customFooter: TemplateRef<any>;
183 @Input() contentTemplate: TemplateRef<any>;
184 @Input() confirmText: string = '确定';
185 @Input() cancelText: string = '取消';
186
187 @Output() visibleChange = new EventEmitter<boolean>();
188 @Output() confirmed = new EventEmitter<void>();
189 @Output() cancelled = new EventEmitter<void>();
190
191 get modalSizeClass(): string {
192 return `modal-size-${this.size}`;
193 }
194
195 close(): void {
196 this.visibleChange.emit(false);
197 this.cancelled.emit();
198 }
199
200 confirm(): void {
201 this.confirmed.emit();
202 this.visibleChange.emit(false);
203 }
204
205 onBackdropClick(): void {
206 if (this.closeOnBackdrop) {
207 this.close();
208 }
209 }
210}

使用方法示例:

html
1<!-- 基本用法 -->
2<button (click)="openModal()">打开模态框</button>
3
4<app-modal
5 [(visible)]="isModalVisible"
6 title="用户信息"
7 (confirmed)="handleConfirm()"
8 (cancelled)="handleCancel()">
9 <p>这是模态框的内容区域。</p>
10 <form>
11 <div class="form-group">
12 <label for="name">姓名</label>
13 <input id="name" type="text">
14 </div>
15 <div class="form-group">
16 <label for="email">电子邮箱</label>
17 <input id="email" type="email">
18 </div>
19 </form>
20</app-modal>
21
22<!-- 使用自定义模板 -->
23<app-modal
24 [(visible)]="isCustomModalVisible"
25 [customHeader]="customModalHeader"
26 [customFooter]="customModalFooter">
27 <p>带有自定义头部和底部的模态框。</p>
28</app-modal>
29
30<ng-template #customModalHeader>
31 <div class="custom-header">
32 <i class="icon-warning"></i>
33 <span>警告提示</span>
34 </div>
35</ng-template>
36
37<ng-template #customModalFooter>
38 <button (click)="cancelAction()">不,我再想想</button>
39 <button class="danger-button" (click)="confirmAction()">是的,我确定</button>
40</ng-template>

4.2 通知提示组件

一个用于显示全局通知的组件,包含服务和组件:

notification.service.ts
typescript
1import { Injectable } from '@angular/core';
2import { Subject } from 'rxjs';
3
4export type NotificationType = 'success' | 'info' | 'warning' | 'error';
5
6export interface NotificationConfig {
7 id?: string;
8 title?: string;
9 message: string;
10 type?: NotificationType;
11 duration?: number; // 显示时长,单位毫秒,0表示不自动关闭
12}
13
14export interface NotificationInstance extends NotificationConfig {
15 id: string;
16 timestamp: number;
17}
18
19@Injectable({
20 providedIn: 'root'
21})
22export class NotificationService {
23 private notifications$ = new Subject<NotificationInstance | { id: string, remove: true }>();
24
25 // 可观察的通知流
26 notifications = this.notifications$.asObservable();
27
28 constructor() {}
29
30 // 显示通知
31 show(config: NotificationConfig): string {
32 const id = config.id || `notification-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
33
34 const notification: NotificationInstance = {
35 ...config,
36 id,
37 type: config.type || 'info',
38 duration: config.duration === undefined ? 4500 : config.duration,
39 timestamp: Date.now()
40 };
41
42 this.notifications$.next(notification);
43
44 return id;
45 }
46
47 // 关闭通知
48 close(id: string): void {
49 this.notifications$.next({ id, remove: true });
50 }
51
52 // 快捷方法
53 success(message: string, title?: string, duration?: number): string {
54 return this.show({ message, title, type: 'success', duration });
55 }
56
57 info(message: string, title?: string, duration?: number): string {
58 return this.show({ message, title, type: 'info', duration });
59 }
60
61 warning(message: string, title?: string, duration?: number): string {
62 return this.show({ message, title, type: 'warning', duration });
63 }
64
65 error(message: string, title?: string, duration?: number): string {
66 return this.show({ message, title, type: 'error', duration });
67 }
68}
notification.component.ts
typescript
1import { Component, OnInit, OnDestroy } from '@angular/core';
2import { trigger, state, style, transition, animate } from '@angular/animations';
3import { NotificationService, NotificationInstance } from './notification.service';
4import { Subscription } from 'rxjs';
5
6@Component({
7 selector: 'app-notification-container',
8 template: `
9 <div class="notification-container">
10 <div
11 *ngFor="let notification of notifications"
12 class="notification"
13 [ngClass]="'notification-' + notification.type"
14 [@notificationAnimation]="'visible'"
15 (@notificationAnimation.done)="onAnimationDone($event, notification)"
16 >
17 <div class="notification-icon">
18 <i class="icon" [ngClass]="getIconClass(notification.type)"></i>
19 </div>
20 <div class="notification-content">
21 <div class="notification-title" *ngIf="notification.title">{{ notification.title }}</div>
22 <div class="notification-message">{{ notification.message }}</div>
23 </div>
24 <button class="notification-close" (click)="close(notification.id)">&times;</button>
25 </div>
26 </div>
27 `,
28 styles: [`
29 .notification-container {
30 position: fixed;
31 top: 20px;
32 right: 20px;
33 z-index: 1060;
34 max-width: 350px;
35 }
36
37 .notification {
38 background: white;
39 border-radius: 4px;
40 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
41 margin-bottom: 16px;
42 padding: 16px;
43 position: relative;
44 display: flex;
45 align-items: flex-start;
46 overflow: hidden;
47 }
48
49 .notification-icon {
50 margin-right: 12px;
51 font-size: 20px;
52 line-height: 24px;
53 }
54
55 .notification-content {
56 flex: 1;
57 }
58
59 .notification-title {
60 font-weight: 500;
61 font-size: 16px;
62 margin-bottom: 4px;
63 color: rgba(0, 0, 0, 0.85);
64 }
65
66 .notification-message {
67 font-size: 14px;
68 color: rgba(0, 0, 0, 0.65);
69 }
70
71 .notification-close {
72 position: absolute;
73 right: 16px;
74 top: 12px;
75 background: transparent;
76 border: none;
77 cursor: pointer;
78 font-size: 16px;
79 color: rgba(0, 0, 0, 0.45);
80 }
81
82 .notification-success {
83 border-left: 4px solid #52c41a;
84 }
85
86 .notification-success .icon {
87 color: #52c41a;
88 }
89
90 .notification-info {
91 border-left: 4px solid #1890ff;
92 }
93
94 .notification-info .icon {
95 color: #1890ff;
96 }
97
98 .notification-warning {
99 border-left: 4px solid #faad14;
100 }
101
102 .notification-warning .icon {
103 color: #faad14;
104 }
105
106 .notification-error {
107 border-left: 4px solid #f5222d;
108 }
109
110 .notification-error .icon {
111 color: #f5222d;
112 }
113 `],
114 animations: [
115 trigger('notificationAnimation', [
116 state('void', style({
117 opacity: 0,
118 transform: 'translateX(100%)'
119 })),
120 state('visible', style({
121 opacity: 1,
122 transform: 'translateX(0)'
123 })),
124 transition('void => visible', animate('200ms cubic-bezier(0, 0, 0.2, 1)')),
125 transition('visible => void', animate('200ms cubic-bezier(0.4, 0, 1, 1)'))
126 ])
127 ]
128})
129export class NotificationContainerComponent implements OnInit, OnDestroy {
130 notifications: NotificationInstance[] = [];
131 private subscription: Subscription;
132 private timers = new Map<string, any>();
133
134 constructor(private notificationService: NotificationService) {}
135
136 ngOnInit(): void {
137 this.subscription = this.notificationService.notifications.subscribe(notification => {
138 if ('remove' in notification) {
139 this.removeNotification(notification.id);
140 } else {
141 this.addNotification(notification);
142 }
143 });
144 }
145
146 ngOnDestroy(): void {
147 this.subscription.unsubscribe();
148 // 清除所有计时器
149 this.timers.forEach(timerId => clearTimeout(timerId));
150 this.timers.clear();
151 }
152
153 private addNotification(notification: NotificationInstance): void {
154 this.notifications = [...this.notifications, notification];
155
156 // 设置自动关闭计时器
157 if (notification.duration > 0) {
158 const timerId = setTimeout(() => {
159 this.close(notification.id);
160 }, notification.duration);
161
162 this.timers.set(notification.id, timerId);
163 }
164 }
165
166 private removeNotification(id: string): void {
167 const index = this.notifications.findIndex(n => n.id === id);
168 if (index > -1) {
169 const notification = this.notifications[index];
170 notification['removing'] = true;
171 this.notifications = [...this.notifications];
172
173 // 清除该通知的计时器
174 if (this.timers.has(id)) {
175 clearTimeout(this.timers.get(id));
176 this.timers.delete(id);
177 }
178 }
179 }
180
181 close(id: string): void {
182 this.notificationService.close(id);
183 }
184
185 onAnimationDone(event: any, notification: NotificationInstance): void {
186 if (event.toState === 'void') {
187 this.notifications = this.notifications.filter(n => n.id !== notification.id);
188 }
189 }
190
191 getIconClass(type: string): string {
192 switch (type) {
193 case 'success': return 'icon-check-circle';
194 case 'info': return 'icon-info-circle';
195 case 'warning': return 'icon-exclamation-circle';
196 case 'error': return 'icon-close-circle';
197 default: return 'icon-info-circle';
198 }
199 }
200}

使用方法示例:

typescript
1import { Component } from '@angular/core';
2import { NotificationService } from './notification.service';
3
4@Component({
5 selector: 'app-demo',
6 template: `
7 <button (click)="showSuccessNotification()">显示成功通知</button>
8 <button (click)="showInfoNotification()">显示信息通知</button>
9 <button (click)="showWarningNotification()">显示警告通知</button>
10 <button (click)="showErrorNotification()">显示错误通知</button>
11 `
12})
13export class DemoComponent {
14 constructor(private notificationService: NotificationService) {}
15
16 showSuccessNotification(): void {
17 this.notificationService.success('操作成功完成', '成功', 3000);
18 }
19
20 showInfoNotification(): void {
21 this.notificationService.info('这是一条信息通知', '提示');
22 }
23
24 showWarningNotification(): void {
25 this.notificationService.warning('请注意可能的问题', '警告');
26 }
27
28 showErrorNotification(): void {
29 this.notificationService.error('操作失败,请重试', '错误', 0); // 0表示不自动关闭
30 }
31}

5. Angular组件最佳实践

5.1 组件设计原则

开发Angular组件时应遵循以下设计原则:

  1. 单一职责原则:每个组件应专注于做一件事,并做好它。
  2. 可复用性:组件应设计为可重用,避免特定业务逻辑硬编码。
  3. 可测试性:组件应便于单元测试,依赖注入而非直接依赖实现。
  4. 可组合性:组件应设计为可组合,支持内容投影和模板定制。
  5. 封装性:组件应封装其内部实现细节,只暴露必要的API。

5.2 组件设计模式

  1. 展示型组件与容器型组件分离

    • 展示型组件:处理UI展示,通过输入接收数据,通过输出发送事件
    • 容器型组件:处理数据和状态管理,注入服务,协调子组件
  2. 组件组合模式

    • 使用内容投影(ng-content)组合组件
    • 使用模板引用定制组件外观
    • 使用组件API进行配置
  3. 智能默认值与渐进式接口

    • 提供合理默认值,减少必要配置
    • 允许通过可选输入进行定制
    • 保持简单接口,同时支持高级功能

5.3 组件性能优化

  1. 使用OnPush变更检测策略
typescript
1@Component({
2 selector: 'app-performance',
3 template: `...`,
4 changeDetection: ChangeDetectionStrategy.OnPush
5})
6export class PerformanceComponent {}
  1. 避免过度使用ngDoCheck

    • 该钩子运行频率高,实现应保持轻量
    • 考虑使用防抖或节流减少执行次数
  2. 跟踪ngFor

html
1<div *ngFor="let item of items; trackBy: trackByFn">{{ item.name }}</div>
typescript
1trackByFn(index: number, item: any): number {
2 return item.id;
3}
  1. 延迟加载

    • 使用*ngIf条件渲染复杂组件
    • 考虑使用虚拟滚动处理大列表
    • 使用defer块(Angular 17+)延迟加载组件
  2. 纯管道替代方法

    • 使用纯管道处理模板中的数据转换,而非组件方法

5.4 组件库开发建议

如果要开发自己的组件库,请遵循以下建议:

  1. 一致的API设计

    • 保持输入/输出命名一致性
    • 使用类似的交互模式
    • 保持相似的错误处理策略
  2. 可访问性

    • 支持键盘导航
    • 添加适当的ARIA属性
    • 确保足够的颜色对比度
    • 支持屏幕阅读器
  3. 主题化支持

    • 使用CSS变量实现主题切换
    • 避免硬编码样式值
    • 提供暗色模式支持
  4. 文档与示例

    • 为每个组件提供清晰文档
    • 包含基本和高级使用示例
    • 记录公共API和事件
  5. 版本控制与迁移

    • 遵循语义版本控制
    • 提供清晰的变更日志
    • 包含版本间迁移指南

评论