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

Angular组件开发指南:从基础到高级

2022-02-072.3k 阅读

Angular组件基础概念

Angular 是一款流行的前端 JavaScript 框架,组件是 Angular 应用程序的基本构建块。每个 Angular 应用都由一个或多个组件组成,组件控制着页面的一部分,称为视图。

组件的定义与结构

一个典型的 Angular 组件由三部分构成:TypeScript 类、HTML 模板和 CSS 样式。以一个简单的 HelloWorld 组件为例:

// hello - world.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-hello - world',
  templateUrl: './hello - world.component.html',
  styleUrls: ['./hello - world.component.css']
})
export class HelloWorldComponent {
  message = 'Hello, World!';
}

在上述代码中,@Component 装饰器用于定义组件。selector 是组件在 HTML 中使用的标签名,templateUrl 指向组件的 HTML 模板文件,styleUrls 指向组件的 CSS 样式文件。

<!-- hello - world.component.html -->
<div>
  <p>{{message}}</p>
</div>

这里通过 {{message}} 语法将组件类中的 message 属性绑定到模板中显示。

/* hello - world.component.css */
div {
  background - color: lightblue;
  padding: 10px;
}

此 CSS 样式为组件的 div 元素设置了浅蓝色背景和内边距。

组件的生命周期

Angular 组件有自己的生命周期,从创建到销毁,会经历一系列的阶段。常用的生命周期钩子函数有:

  1. ngOnInit:在组件初始化完成后调用,通常用于进行数据获取、初始化操作等。
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.component.html'
})
export class MyComponentComponent implements OnInit {
  data: any;

  constructor() { }

  ngOnInit() {
    // 模拟从服务获取数据
    this.data = { name: 'John' };
  }
}
  1. ngOnDestroy:在组件销毁前调用,可用于清理订阅、释放资源等操作。
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { MyService } from './my - service';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.component.html'
})
export class MyComponentComponent implements OnInit, OnDestroy {
  data: any;
  private subscription: Subscription;

  constructor(private myService: MyService) { }

  ngOnInit() {
    this.subscription = this.myService.getData().subscribe(data => {
      this.data = data;
    });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}
  1. ngOnChanges:当组件的输入属性发生变化时调用。假设组件有一个 inputValue 输入属性:
import { Component, OnInit, OnChanges, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.component.html'
})
export class MyComponentComponent implements OnInit, OnChanges {
  @Input() inputValue: string;

  constructor() { }

  ngOnInit() { }

  ngOnChanges(changes: SimpleChanges) {
    if (changes.inputValue) {
      console.log('inputValue has changed to:', changes.inputValue.currentValue);
    }
  }
}

组件间通信

在一个复杂的 Angular 应用中,组件之间需要进行通信来共享数据和交互。

父子组件通信

  1. 父传子:通过 @Input() 装饰器将父组件的数据传递给子组件。 首先创建一个子组件 child - component
// child - component.component.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app - child - component',
  templateUrl: './child - component.component.html'
})
export class ChildComponentComponent {
  @Input() parentData: string;
}
<!-- child - component.component.html -->
<p>Received from parent: {{parentData}}</p>

在父组件模板中使用子组件并传递数据:

<!-- parent - component.component.html -->
<app - child - component [parentData]="messageFromParent"></app - child - component>
// parent - component.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app - parent - component',
  templateUrl: './parent - component.component.html'
})
export class ParentComponentComponent {
  messageFromParent = 'Hello from parent';
}
  1. 子传父:通过 @Output() 装饰器和 EventEmitter 来实现。子组件触发一个事件,父组件监听该事件并获取数据。 子组件 child - component 代码如下:
import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app - child - component',
  templateUrl: './child - component.component.html'
})
export class ChildComponentComponent {
  @Output() childEvent = new EventEmitter<string>();

  sendDataToParent() {
    this.childEvent.emit('Data from child');
  }
}
<button (click)="sendDataToParent()">Send Data to Parent</button>

父组件模板监听子组件事件:

<app - child - component (childEvent)="handleChildEvent($event)"></app - child - component>
import { Component } from '@angular/core';

@Component({
  selector: 'app - parent - component',
  templateUrl: './parent - component.component.html'
})
export class ParentComponentComponent {
  handleChildEvent(data: string) {
    console.log('Received from child:', data);
  }
}

非父子组件通信

  1. 使用服务:创建一个服务来共享数据,任何组件都可以注入该服务并获取或修改数据。 首先创建一个 SharedService
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SharedService {
  private dataSource = new BehaviorSubject<string>('default value');
  currentData = this.dataSource.asObservable();

  changeData(data: string) {
    this.dataSource.next(data);
  }
}

组件 A 注入服务并修改数据:

import { Component } from '@angular/core';
import { SharedService } from './shared.service';

@Component({
  selector: 'app - component - a',
  templateUrl: './component - a.component.html'
})
export class ComponentAComponent {
  constructor(private sharedService: SharedService) { }

  updateData() {
    this.sharedService.changeData('New value from Component A');
  }
}

组件 B 注入服务并订阅数据:

import { Component, OnInit } from '@angular/core';
import { SharedService } from './shared.service';

@Component({
  selector: 'app - component - b',
  templateUrl: './component - b.component.html'
})
export class ComponentBComponent implements OnInit {
  data: string;

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
    this.sharedService.currentData.subscribe(data => {
      this.data = data;
    });
  }
}
  1. 使用 SubjectSubscription:类似服务的方式,但更加灵活,直接在组件间通过 Subject 传递数据。
import { Component } from '@angular/core';
import { Subject, Subscription } from 'rxjs';

@Component({
  selector: 'app - component - c',
  templateUrl: './component - c.component.html'
})
export class ComponentCComponent {
  dataSubject = new Subject<string>();
  subscription: Subscription;

  constructor() { }

  sendData() {
    this.dataSubject.next('Data from Component C');
  }

  ngOnInit() {
    this.subscription = this.dataSubject.subscribe(data => {
      console.log('Received in Component C:', data);
    });
  }

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

另一个组件可以注入 ComponentCComponent 并获取 dataSubject 数据。

高级组件开发技巧

组件的样式封装与作用域

Angular 提供了多种方式来管理组件的样式作用域。

  1. Emulated:这是默认模式,Angular 通过添加唯一的属性选择器到组件的 HTML 和 CSS 规则来模拟样式的封装。例如:
/* my - component.component.css */
.my - class {
  color: red;
}
<!-- my - component.component.html -->
<div class="my - class">This text will be red</div>

生成的 HTML 可能类似:

<div class="my - class _ngcontent - app - 123">This text will be red</div>
.my - class._ngcontent - app - 123 {
  color: red;
}
  1. Shadow DOM:使用 ViewEncapsulation.ShadowDom 开启 Shadow DOM 封装。在这种模式下,组件的样式和 DOM 与外部完全隔离。
import { Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.component.html',
  styleUrls: ['./my - component.component.css'],
  encapsulation: ViewEncapsulation.ShadowDom
})
export class MyComponentComponent { }
  1. None:使用 ViewEncapsulation.None 关闭样式封装,组件的样式会影响全局。

动态组件加载

在某些场景下,需要根据运行时的条件动态加载组件。Angular 提供了 ComponentFactoryResolverViewContainerRef 来实现动态组件加载。 假设我们有一个 DynamicComponent 和一个加载它的 AppComponent

// dynamic - component.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app - dynamic - component',
  templateUrl: './dynamic - component.component.html'
})
export class DynamicComponentComponent {
  message = 'This is a dynamic component';
}
<!-- dynamic - component.component.html -->
<p>{{message}}</p>

AppComponent 中动态加载 DynamicComponent

import { Component, ComponentFactoryResolver, ViewChild, ViewContainerRef } from '@angular/core';
import { DynamicComponentComponent } from './dynamic - component.component';

@Component({
  selector: 'app - root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  @ViewChild('dynamicComponentContainer', { read: ViewContainerRef }) dynamicComponentContainer: ViewContainerRef;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

  loadDynamicComponent() {
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(DynamicComponentComponent);
    this.dynamicComponentContainer.clear();
    const componentRef = this.dynamicComponentContainer.createComponent(componentFactory);
  }
}
<!-- app.component.html -->
<button (click)="loadDynamicComponent()">Load Dynamic Component</button>
<ng - container #dynamicComponentContainer></ng - container>

当点击按钮时,DynamicComponent 会被动态加载到 ng - container 中。

组件的性能优化

  1. OnPush 变更检测策略:对于一些数据不经常变化的组件,可以使用 ChangeDetectionStrategy.OnPush 来优化性能。
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponentComponent {
  data = { value: 'initial' };

  updateData() {
    // 这里需要创建新的对象,因为 OnPush 策略下对象引用不变不会触发变更检测
    this.data = { value: 'updated' };
  }
}

在这种策略下,只有当组件的输入属性引用发生变化、组件接收到事件(如点击)或 Observable 数据发生变化时,才会触发变更检测,减少不必要的检测开销。 2. TrackBy 函数:在使用 *ngFor 指令渲染大量列表时,trackBy 函数可以提高性能。假设我们有一个列表组件:

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

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

  trackByFn(index: number, item: any) {
    return item.id;
  }
}
<ul>
  <li *ngFor="let item of items; trackBy: trackByFn">{{item.name}}</li>
</ul>

通过 trackByFn 函数,Angular 可以通过 item.id 来跟踪列表项,而不是每次重新渲染整个列表,从而提高性能。

组件与路由

路由基础与组件关联

Angular 路由允许我们在单页应用中实现页面导航和组件切换。首先,配置路由模块:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

在上述代码中,Routes 数组定义了不同路径与组件的映射关系。path 为空字符串时映射到 HomeComponentpathabout 时映射到 AboutComponent。 然后在主组件模板中使用 router - outlet 来显示路由对应的组件:

<!-- app.component.html -->
<nav>
  <a routerLink="/">Home</a>
  <a routerLink="/about">About</a>
</nav>
<router - outlet></router - outlet>

当点击导航链接时,router - outlet 会加载对应的组件。

路由参数与组件交互

  1. 传递路由参数:可以通过路由传递参数给组件。例如,我们有一个 UserComponent 用于显示用户详情,通过用户 ID 作为参数。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { UserComponent } from './user.component';

const routes: Routes = [
  { path: 'user/:id', component: UserComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class UserRoutingModule { }

UserComponent 中获取参数:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app - user',
  templateUrl: './user.component.html'
})
export class UserComponent implements OnInit {
  userId: number;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.route.params.subscribe(params => {
      this.userId = +params['id'];
    });
  }
}
  1. 查询参数:除了路径参数,还可以使用查询参数。例如:
<a routerLink="/search" [queryParams]="{ keyword: 'angular' }">Search Angular</a>

在组件中获取查询参数:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app - search',
  templateUrl: './search.component.html'
})
export class SearchComponent implements OnInit {
  keyword: string;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.route.queryParams.subscribe(params => {
      this.keyword = params['keyword'];
    });
  }
}

组件测试

单元测试组件

使用 Jest 和 Angular Testing Library 可以对 Angular 组件进行单元测试。以 HelloWorldComponent 为例:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HelloWorldComponent } from './hello - world.component';

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

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

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

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

  it('should display the message', () => {
    const compiled = fixture.nativeElement;
    expect(compiled.textContent).toContain('Hello, World!');
  });
});

在上述测试中,TestBed 用于配置测试环境,ComponentFixture 用于创建和操作组件实例。beforeEach 钩子函数用于在每个测试用例执行前进行初始化。测试用例使用 it 函数定义,通过 expect 断言来验证组件的行为。

集成测试组件与服务

当组件依赖服务时,需要进行集成测试。假设 MyComponent 依赖 MyService

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyComponent } from './my - component.component';
import { MyService } from './my - service';
import { of } from 'rxjs';

describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let myService: MyService;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [MyService]
    })
    .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    myService = TestBed.inject(MyService);
    fixture.detectChanges();
  });

  it('should fetch data from service', () => {
    const mockData = { name: 'Mocked' };
    spyOn(myService, 'getData').and.returnValue(of(mockData));
    component.ngOnInit();
    fixture.detectChanges();
    expect(component.data).toEqual(mockData);
  });
});

这里通过 spyOn 函数来模拟 MyServicegetData 方法,返回模拟数据,然后验证组件在 ngOnInit 时是否正确获取了数据。

通过以上从基础到高级的内容,我们对 Angular 组件开发有了全面深入的了解,能够开发出高质量、可维护且性能优良的 Angular 应用程序。无论是简单的页面构建块还是复杂的动态交互组件,都可以通过这些知识和技巧来实现。在实际开发中,不断实践和总结经验,将有助于进一步提升 Angular 组件开发的能力。