MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular组件的定义与使用

2022-07-064.6k 阅读

Angular 组件基础概念

在 Angular 开发中,组件是构建用户界面的基本单元。它封装了特定的功能和对应的视图,使得代码具有更好的模块化和可维护性。每个 Angular 应用都由一个或多个组件组成,从最顶层的根组件开始,层层嵌套形成一个组件树结构。

组件主要由三部分构成:组件类、模板和样式。组件类是 TypeScript 类,用于定义组件的行为逻辑,包括属性和方法。模板则是 HTML 代码,用于描述组件的外观和结构。样式可以是 CSS、SCSS 等样式语言,用来定义组件的视觉风格。

创建 Angular 组件

使用 Angular CLI(命令行界面)可以快速创建组件。在项目根目录下执行以下命令:

ng generate component component - name

其中 component - name 是你自定义的组件名称,例如 headerproduct - card。Angular CLI 会自动在 src/app 目录下创建一个新的组件文件夹,里面包含组件所需的文件:.ts(组件类文件)、.html(模板文件)、.css.scss(样式文件)以及 .spec.ts(测试文件)。

组件类详解

以一个简单的计数器组件为例,其组件类代码如下:

import { Component } from '@angular/core';

@Component({
  selector: 'app - counter',
  templateUrl: './counter.component.html',
  styleUrls: ['./counter.component.css']
})
export class CounterComponent {
  count: number = 0;

  increment() {
    this.count++;
  }

  decrement() {
    if (this.count > 0) {
      this.count--;
    }
  }
}

在上述代码中,通过 @Component 装饰器来标识这是一个 Angular 组件。selector 属性定义了组件在模板中使用的标签名,这里是 <app - counter>templateUrl 指向组件的模板文件,styleUrls 指向组件的样式文件。

组件类中定义了一个 count 属性用于存储计数器的值,以及 incrementdecrement 两个方法分别用于增加和减少计数器的值。

组件模板

组件模板是组件呈现给用户的界面部分。继续以上述计数器组件为例,其模板文件 counter.component.html 内容如下:

<div>
  <p>Count: {{ count }}</p>
  <button (click)="increment()">Increment</button>
  <button (click)="decrement()">Decrement</button>
</div>

在模板中,使用了 Angular 的插值语法 {{ count }} 来显示计数器的值。同时,通过事件绑定语法 (click)="increment()"(click)="decrement()" 分别为两个按钮绑定了点击事件,当按钮被点击时,会调用组件类中对应的方法。

组件样式

组件样式可以通过多种方式定义。一种常见的方式是在组件的样式文件(如 counter.component.css)中编写样式。例如:

div {
  padding: 10px;
  border: 1px solid #ccc;
  border - radius: 5px;
}

button {
  margin: 5px;
}

这样定义的样式只作用于该组件内部,不会影响到其他组件,实现了样式的封装。另外,也可以使用 :host 选择器来设置组件宿主元素(即 <app - counter> 标签)的样式:

:host {
  display: block;
}

组件的输入与输出

输入属性(@Input)

当组件需要接收外部传递的数据时,就可以使用 @Input 装饰器。例如,创建一个显示用户信息的组件 user - info.component.ts

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app - user - info',
  templateUrl: './user - info.component.html',
  styleUrls: ['./user - info.component.css']
})
export class UserInfoComponent {
  @Input() name: string;
  @Input() age: number;
}

在模板文件 user - info.component.html 中可以这样使用:

<div>
  <p>Name: {{ name }}</p>
  <p>Age: {{ age }}</p>
</div>

在父组件的模板中,可以通过以下方式传递数据给 user - info 组件:

<app - user - info [name]="userName" [age]="userAge"></app - user - info>

在父组件类中需要定义 userNameuserAge 属性:

import { Component } from '@angular/core';

@Component({
  selector: 'app - parent - component',
  templateUrl: './parent - component.html',
  styleUrls: ['./parent - component.css']
})
export class ParentComponent {
  userName: string = 'John Doe';
  userAge: number = 30;
}

输出属性(@Output)与事件绑定

组件有时需要向父组件传递数据或通知父组件某些事件发生,这时就用到了 @Output 装饰器和 EventEmitter。例如,创建一个按钮组件 custom - button.component.ts,当按钮点击时向父组件传递一个消息:

import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app - custom - button',
  templateUrl: './custom - button.component.html',
  styleUrls: ['./custom - button.component.css']
})
export class CustomButtonComponent {
  @Output() buttonClicked = new EventEmitter<string>();

  clickHandler() {
    this.buttonClicked.emit('Button was clicked!');
  }
}

在模板文件 custom - button.component.html 中:

<button (click)="clickHandler()">Click Me</button>

在父组件的模板中监听这个事件:

<app - custom - button (buttonClicked)="handleButtonClick($event)"></app - custom - button>

在父组件类中定义 handleButtonClick 方法:

import { Component } from '@angular/core';

@Component({
  selector: 'app - parent - component',
  templateUrl: './parent - component.html',
  styleUrls: ['./parent - component.css']
})
export class ParentComponent {
  handleButtonClick(message: string) {
    console.log(message);
  }
}

组件间通信

除了通过输入输出属性进行父子组件通信外,Angular 还提供了其他方式来实现组件间通信。

服务用于组件通信

可以创建一个服务来作为数据共享的中介。例如,创建一个 DataService

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private data: string;

  setData(value: string) {
    this.data = value;
  }

  getData() {
    return this.data;
  }
}

在一个组件(如 ComponentA)中设置数据:

import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app - component - a',
  templateUrl: './component - a.html',
  styleUrls: ['./component - a.css']
})
export class ComponentA {
  constructor(private dataService: DataService) {}

  setSharedData() {
    this.dataService.setData('Data from ComponentA');
  }
}

在另一个组件(如 ComponentB)中获取数据:

import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app - component - b',
  templateUrl: './component - b.html',
  styleUrls: ['./component - b.css']
})
export class ComponentB {
  sharedData: string;

  constructor(private dataService: DataService) {
    this.sharedData = this.dataService.getData();
  }
}

祖先与后代组件通信

通过 @Input()@Output() 结合组件树结构可以实现祖先与后代组件通信。祖先组件通过输入属性将数据传递给直接子组件,子组件再依次向下传递。对于后代组件向祖先组件传递数据,可以通过事件层层向上冒泡来实现。另外,也可以使用 ViewChildViewChildren 来获取子组件实例,从而进行直接通信。例如,在父组件中获取子组件实例:

import { Component, ViewChild } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
  selector: 'app - parent - component',
  templateUrl: './parent - component.html',
  styleUrls: ['./parent - component.css']
})
export class ParentComponent {
  @ViewChild(ChildComponent) child: ChildComponent;

  ngAfterViewInit() {
    console.log(this.child.someMethod());
  }
}

在子组件 child.component.ts 中:

import { Component } from '@angular/core';

@Component({
  selector: 'app - child - component',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  someMethod() {
    return 'This is a method from ChildComponent';
  }
}

组件生命周期

Angular 组件有自己的生命周期,从创建到销毁经历多个阶段。通过实现特定的生命周期钩子函数,可以在组件生命周期的不同阶段执行自定义逻辑。

ngOnInit

ngOnInit 钩子函数在组件初始化完成后调用,通常用于进行数据的初始化、订阅服务等操作。例如:

import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent implements OnInit {
  data: string;

  constructor(private dataService: DataService) {}

  ngOnInit() {
    this.data = this.dataService.getData();
  }
}

ngOnChanges

ngOnChanges 钩子函数在组件的输入属性发生变化时调用。它接收一个 SimpleChanges 对象,包含了变化前后的属性值。例如:

import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent implements OnChanges {
  @Input() value: number;

  ngOnChanges(changes: SimpleChanges) {
    if (changes.value) {
      console.log('Value changed from', changes.value.previousValue, 'to', changes.value.currentValue);
    }
  }
}

ngDoCheck

ngDoCheck 钩子函数用于自定义变更检测。Angular 默认的变更检测机制可能无法满足某些复杂场景,这时可以使用 ngDoCheck 手动检测变化。例如:

import { Component, DoCheck } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent implements DoCheck {
  private previousValue: number;

  constructor() {
    this.previousValue = 0;
  }

  ngDoCheck() {
    // 假设这里有一个复杂的逻辑来检测某个内部状态的变化
    if (this.someInternalValue!== this.previousValue) {
      console.log('Internal value has changed');
      this.previousValue = this.someInternalValue;
    }
  }
}

ngAfterContentInit 和 ngAfterContentChecked

ngAfterContentInit 在组件的内容(即通过 <ng - content> 投影进来的内容)初始化后调用,ngAfterContentChecked 在每次组件内容检查后调用。例如,在一个包含投影内容的组件中:

import { Component, AfterContentInit, AfterContentChecked } from '@angular/core';

@Component({
  selector: 'app - container - component',
  templateUrl: './container - component.html',
  styleUrls: ['./container - component.css']
})
export class ContainerComponent implements AfterContentInit, AfterContentChecked {
  ngAfterContentInit() {
    console.log('Content has been initialized');
  }

  ngAfterContentChecked() {
    console.log('Content has been checked');
  }
}

ngAfterViewInit 和 ngAfterViewChecked

ngAfterViewInit 在组件的视图(包括子视图)初始化后调用,ngAfterViewChecked 在每次组件视图检查后调用。例如:

import { Component, AfterViewInit, AfterViewChecked } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent implements AfterViewInit, AfterViewChecked {
  ngAfterViewInit() {
    console.log('View has been initialized');
  }

  ngAfterViewChecked() {
    console.log('View has been checked');
  }
}

ngOnDestroy

ngOnDestroy 钩子函数在组件销毁前调用,通常用于清理资源,如取消订阅、清除定时器等。例如:

import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DataService } from './data.service';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent implements OnDestroy {
  private subscription: Subscription;

  constructor(private dataService: DataService) {
    this.subscription = this.dataService.dataChange.subscribe(data => {
      console.log('Data changed:', data);
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

组件的复用与定制

组件复用

通过合理设计组件的输入输出属性和逻辑,可以实现组件的高度复用。例如,前面提到的 user - info 组件可以用于显示不同用户的信息,只要在使用时传递不同的 nameage 属性值即可。另外,组件库(如 Angular Material)中的组件也是高度复用的示例,它们提供了通用的 UI 功能,如按钮、输入框等,可以在不同项目中使用。

组件定制

在复用组件的基础上,有时需要根据具体需求对组件进行定制。这可以通过修改组件的输入属性、样式或继承组件并重写部分方法来实现。例如,如果想要改变 user - info 组件的显示样式,可以在父组件的样式文件中通过深度选择器来覆盖组件内部的样式:

app - user - info /deep/ p {
  color: red;
}

或者通过继承 user - info 组件,在子类中添加新的功能或修改现有功能:

import { Component } from '@angular/core';
import { UserInfoComponent } from './user - info.component';

@Component({
  selector: 'app - custom - user - info',
  templateUrl: './custom - user - info.html',
  styleUrls: ['./custom - user - info.css']
})
export class CustomUserInfoComponent extends UserInfoComponent {
  additionalInfo: string = 'This is additional info';
}

custom - user - info.html 模板中可以使用父组件的属性和方法,同时也可以使用新增的 additionalInfo 属性:

<div>
  <p>Name: {{ name }}</p>
  <p>Age: {{ age }}</p>
  <p>{{ additionalInfo }}</p>
</div>

组件性能优化

变更检测策略

Angular 提供了两种变更检测策略:默认的 Default 策略和 OnPush 策略。Default 策略在每次事件循环时检查组件树中所有组件的变化,而 OnPush 策略只有在以下情况时触发变更检测:

  1. 组件的输入属性引用发生变化。
  2. 组件接收到事件(如点击、输入等)。
  3. 手动调用 ChangeDetectorRef.markForCheck()

使用 OnPush 策略可以显著提高性能,特别是对于那些数据变化不频繁的组件。例如,在组件类中设置变更检测策略为 OnPush

import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
  // 组件逻辑
}

避免不必要的渲染

在组件模板中,尽量减少不必要的 DOM 操作和计算。例如,避免在插值表达式中进行复杂的计算,可以将计算结果提前存储在组件类的属性中。另外,使用 *ngIf*ngFor 指令时要注意其性能影响。*ngIf 会根据条件动态添加或移除 DOM 元素,而 *ngFor 会根据数组长度变化重新渲染列表。如果列表数据量较大,可以考虑使用 trackBy 函数来优化 *ngFor 的性能。例如:

<ul>
  <li *ngFor="let item of items; trackBy: trackByFn">{{ item.name }}</li>
</ul>

在组件类中定义 trackByFn 函数:

import { Component } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css']
})
export class MyComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];

  trackByFn(index: number, item: any) {
    return item.id;
  }
}

这样,当 items 数组中的元素顺序发生变化但 id 不变时,Angular 不会重新创建和销毁 DOM 元素,而是复用已有的元素,从而提高性能。

高级组件技巧

动态组件加载

在某些情况下,需要根据运行时的条件动态加载组件。Angular 提供了 ComponentFactoryResolverViewContainerRef 来实现动态组件加载。例如,假设有两个组件 ComponentAComponentB,根据用户的选择动态加载其中一个: 首先,在模块中声明这两个组件:

import { NgModule } from '@angular/core';
import { ComponentA } from './component - a.component';
import { ComponentB } from './component - b.component';

@NgModule({
  declarations: [ComponentA, ComponentB],
  exports: [ComponentA, ComponentB]
})
export class DynamicComponentsModule {}

在需要动态加载组件的组件类中:

import { Component, ComponentFactoryResolver, ViewContainerRef, OnInit } from '@angular/core';
import { DynamicComponentsModule } from './dynamic - components.module';

@Component({
  selector: 'app - dynamic - component - loader',
  templateUrl: './dynamic - component - loader.html',
  styleUrls: ['./dynamic - component - loader.css']
})
export class DynamicComponentLoaderComponent implements OnInit {
  selectedComponent: string = 'A';

  constructor(private resolver: ComponentFactoryResolver, private container: ViewContainerRef) {}

  ngOnInit() {
    this.loadComponent();
  }

  loadComponent() {
    let componentFactory;
    if (this.selectedComponent === 'A') {
      componentFactory = this.resolver.resolveComponentFactory(ComponentA);
    } else {
      componentFactory = this.resolver.resolveComponentFactory(ComponentB);
    }
    this.container.clear();
    this.container.createComponent(componentFactory);
  }
}

在模板文件 dynamic - component - loader.html 中:

<select [(ngModel)]="selectedComponent" (ngModelChange)="loadComponent()">
  <option value="A">Component A</option>
  <option value="B">Component B</option>
</select>
<div #dynamicComponentContainer></div>

这里通过 ViewContainerRef 获取到一个用于动态添加组件的容器,然后根据条件使用 ComponentFactoryResolver 解析出对应的组件工厂并创建组件实例添加到容器中。

组件装饰器与元数据

Angular 组件的 @Component 装饰器除了常见的 selectortemplateUrlstyleUrls 等属性外,还有其他一些有用的元数据属性。例如,providers 属性可以用于为组件提供依赖注入的服务。如果一个组件需要自己独立的服务实例,可以在 providers 数组中声明该服务:

import { Component, Injectable } from '@angular/core';

@Injectable()
export class MyService {
  data: string = 'Initial data';
}

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css'],
  providers: [MyService]
})
export class MyComponent {
  constructor(private myService: MyService) {}
}

这样,该组件及其子组件将使用这个独立的 MyService 实例,而不会与其他组件共享同一个服务实例。另外,encapsulation 属性可以用于设置组件样式的封装模式,有 Emulated(默认,模拟样式封装)、Native(使用浏览器原生的 Shadow DOM 进行样式封装)和 None(不进行样式封装,组件样式会影响全局)三种模式。例如:

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.html',
  styleUrls: ['./my - component.css'],
  encapsulation: ViewEncapsulation.Native
})
export class MyComponent {}

组件的测试

为了保证组件的质量和稳定性,需要对组件进行单元测试。Angular 提供了 @angular/core/testing 模块来辅助编写组件测试。以计数器组件为例,其测试文件 counter.component.spec.ts 内容如下:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  let component: CounterComponent;
  let fixture: ComponentFixture<CounterComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [CounterComponent]
    })
     .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should increment count', () => {
    const initialCount = component.count;
    component.increment();
    expect(component.count).toBe(initialCount + 1);
  });

  it('should decrement count', () => {
    component.count = 5;
    const initialCount = component.count;
    component.decrement();
    expect(component.count).toBe(initialCount - 1);
  });
});

在上述测试代码中,通过 TestBed 来配置测试环境,创建组件实例。describeit 函数用于定义测试套件和具体的测试用例。使用 expect 来断言组件的行为是否符合预期。

在实际开发中,组件的测试还可以包括对模板渲染、事件绑定、输入输出属性等方面的测试,以确保组件的功能完整性和正确性。

通过深入理解和掌握 Angular 组件的定义与使用,包括组件的基础构成、通信方式、生命周期、性能优化以及高级技巧等方面,开发者能够构建出高效、可维护且具有良好用户体验的前端应用程序。在不断实践和应用中,还可以根据具体项目需求进一步挖掘和探索 Angular 组件的强大功能。