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

Angular指令与服务的交互:解耦与复用

2023-01-272.3k 阅读

理解 Angular 指令与服务

在 Angular 开发中,指令和服务是两个关键概念,它们共同构成了 Angular 应用的核心架构。指令主要用于操作 DOM,扩展 HTML 语法,实现动态行为和组件化。而服务则侧重于提供可复用的业务逻辑、数据存储和数据交互等功能。

指令基础

Angular 中有三种类型的指令:组件(Component)、结构型指令(Structural Directive)和属性型指令(Attribute Directive)。

组件:是一种特殊的指令,它有自己的模板、样式和逻辑。例如,我们可以创建一个简单的 HelloWorldComponent

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!';
}

hello - world.component.html 中可以简单展示:

<p>{{message}}</p>

结构型指令:用于改变 DOM 的结构,比如 *ngIf*ngFor*ngIf 根据表达式的值来决定是否渲染一个元素:

<div *ngIf="isLoggedIn">Welcome, user!</div>

*ngFor 用于迭代一个集合来创建多个 DOM 元素:

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

属性型指令:用于改变元素的外观或行为,例如 NgStyleNgClassNgStyle 可以动态设置元素的样式:

<div [ngStyle]="{'color': isActive? 'green' : 'red'}">Text color changes</div>

NgClass 可以动态添加或移除 CSS 类:

<div [ngClass]="{ 'active': isActive }">Toggle active class</div>

服务基础

服务是一个广义的概念,它可以是任何可注入的类。服务通常用于封装与组件无关的逻辑,比如数据获取、日志记录等。

例如,创建一个简单的 LoggerService

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

@Injectable({
  providedIn: 'root'
})
export class LoggerService {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

然后在组件中使用这个服务:

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app - logging - component',
  templateUrl: './logging - component.html'
})
export class LoggingComponent {
  constructor(private logger: LoggerService) {
    this.logger.log('Component initialized');
  }
}

指令与服务交互的需求

在实际开发中,指令和服务往往需要相互协作。例如,一个属性型指令可能需要从服务中获取一些配置信息来决定如何改变元素的行为,或者一个结构型指令可能需要根据服务中的数据来决定是否渲染某些元素。

解耦的重要性

解耦指令和服务之间的关系可以提高代码的可维护性和可测试性。如果指令和服务紧密耦合,当服务的实现发生变化时,可能需要对多个指令进行修改,这增加了代码的维护成本。

例如,假设我们有一个 HighlightDirective,它根据用户的偏好来高亮元素。如果这个偏好直接硬编码在指令中,那么当用户偏好改变时,我们需要修改指令代码。但如果偏好存储在一个 UserPreferenceService 中,指令只需要从服务中获取数据,这样就实现了解耦。

复用的需求

复用性是软件设计的一个重要目标。通过让指令和服务良好交互,可以将一些通用的逻辑封装在服务中,供多个指令复用。

比如,一个用于验证输入格式的服务可以被多个表单相关的指令复用,这样可以避免在每个指令中重复编写验证逻辑。

指令中注入服务

在 Angular 中,指令可以像组件一样注入服务。这使得指令能够使用服务提供的功能。

简单示例:注入 LoggerService 到指令

我们继续使用之前定义的 LoggerService,现在创建一个 HighlightDirective 并注入 LoggerService

import { Directive, ElementRef, HostListener } from '@angular/core';
import { LoggerService } from './logger.service';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef, private logger: LoggerService) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
    this.logger.log('Mouse entered the element');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
    this.logger.log('Mouse left the element');
  }

  private highlight(color: string | null) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

在上述代码中,HighlightDirective 在鼠标进入和离开元素时,调用 LoggerServicelog 方法记录日志,同时改变元素的背景颜色。

依赖注入的原理

当 Angular 创建一个指令实例时,它会检查指令的构造函数参数。如果参数是一个服务类型,Angular 会尝试从注入器(Injector)中获取该服务的实例。注入器是一个维护服务实例的容器,它遵循一定的查找规则。

如果服务是在根模块(@NgModule)中通过 providedIn: 'root' 提供的,那么 Angular 会在应用的根注入器中查找该服务。如果指令所在的组件或模块有自己的注入器,Angular 会先在组件或模块的注入器中查找,如果找不到再到父级注入器中查找,直到根注入器。

通过服务共享数据

服务不仅可以提供功能,还可以用于在不同指令或组件之间共享数据。

数据共享示例

创建一个 SharedDataService 用于在指令之间共享数据:

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

@Injectable({
  providedIn: 'root'
})
export class SharedDataService {
  private _sharedValue = 0;

  get sharedValue() {
    return this._sharedValue;
  }

  set sharedValue(value: number) {
    this._sharedValue = value;
  }
}

然后创建两个指令 IncrementDirectiveDisplayDirective

import { Directive, HostListener } from '@angular/core';
import { SharedDataService } from './shared - data.service';

@Directive({
  selector: '[appIncrement]'
})
export class IncrementDirective {
  constructor(private sharedData: SharedDataService) {}

  @HostListener('click') onClick() {
    this.sharedData.sharedValue++;
  }
}
import { Directive, ElementRef, OnInit } from '@angular/core';
import { SharedDataService } from './shared - data.service';

@Directive({
  selector: '[appDisplay]'
})
export class DisplayDirective implements OnInit {
  constructor(private el: ElementRef, private sharedData: SharedDataService) {}

  ngOnInit() {
    this.el.nativeElement.textContent = `Shared value: ${this.sharedData.sharedValue}`;
  }
}

在 HTML 中使用这两个指令:

<button appIncrement>Increment</button>
<span appDisplay></span>

当点击按钮时,IncrementDirective 会增加 SharedDataService 中的 sharedValueDisplayDirective 会在 ngOnInit 生命周期钩子中显示这个值。

数据共享的注意事项

在使用服务共享数据时,需要注意数据的变化通知。在上述示例中,如果 DisplayDirective 需要实时更新显示的值,我们可以使用 Observable

修改 SharedDataService 如下:

import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SharedDataService {
  private _sharedValue = 0;
  private valueChange = new Subject<number>();

  get sharedValue() {
    return this._sharedValue;
  }

  set sharedValue(value: number) {
    this._sharedValue = value;
    this.valueChange.next(value);
  }

  getValueChange(): Observable<number> {
    return this.valueChange.asObservable();
  }
}

修改 DisplayDirective

import { Directive, ElementRef, OnInit } from '@angular/core';
import { SharedDataService } from './shared - data.service';
import { Subscription } from 'rxjs';

@Directive({
  selector: '[appDisplay]'
})
export class DisplayDirective implements OnInit {
  private subscription: Subscription;

  constructor(private el: ElementRef, private sharedData: SharedDataService) {}

  ngOnInit() {
    this.updateDisplay();
    this.subscription = this.sharedData.getValueChange().subscribe(() => {
      this.updateDisplay();
    });
  }

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

  private updateDisplay() {
    this.el.nativeElement.textContent = `Shared value: ${this.sharedData.sharedValue}`;
  }
}

这样,当 sharedValue 发生变化时,DisplayDirective 会实时更新显示。

指令与服务交互中的生命周期管理

指令和服务都有自己的生命周期,在它们交互时,需要注意生命周期的协调。

指令的生命周期钩子

指令有多个生命周期钩子,如 ngOnInitngOnDestroy 等。在与服务交互时,这些钩子可以用于初始化服务相关的操作和清理资源。

例如,在 HighlightDirective 中,如果 LoggerService 需要一些初始化操作,我们可以在 ngOnInit 中进行:

import { Directive, ElementRef, HostListener, OnInit } from '@angular/core';
import { LoggerService } from './logger.service';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {
  constructor(private el: ElementRef, private logger: LoggerService) {}

  ngOnInit() {
    this.logger.init(); // 假设 LoggerService 有 init 方法
  }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
    this.logger.log('Mouse entered the element');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
    this.logger.log('Mouse left the element');
  }

  private highlight(color: string | null) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

服务的生命周期

服务的生命周期与应用的生命周期相关。如果服务是在根模块中提供的(providedIn: 'root'),它会在应用启动时创建,在应用销毁时销毁。

但是,如果服务是在组件或模块的注入器中提供的,它的生命周期会与组件或模块的生命周期相关。例如,如果一个服务是在某个组件的注入器中提供的,当组件销毁时,该服务的实例也会被销毁。

在指令与服务交互时,要确保指令不会在服务已经销毁的情况下尝试访问服务的方法或属性。

指令与服务交互的最佳实践

单一职责原则

指令和服务都应该遵循单一职责原则。指令应该专注于 DOM 操作和用户交互,而服务应该专注于业务逻辑和数据处理。

例如,一个 FormValidationDirective 应该只负责验证表单输入的格式是否正确,并在 DOM 上显示相应的提示信息。而验证的具体逻辑,如邮箱格式验证、密码强度验证等,应该封装在一个 FormValidationService 中。

依赖注入的层次

合理选择服务的注入层次。如果一个服务是全局共享的,应该在根模块中提供(providedIn: 'root')。如果一个服务只在某个特定的组件或模块内使用,应该在相应的组件或模块的注入器中提供。

例如,一个用于管理用户登录状态的 AuthService 通常应该在根模块中提供,因为整个应用都可能需要知道用户的登录状态。而一个用于某个特定组件内部的数据缓存服务,可以在该组件的注入器中提供。

测试友好性

设计指令与服务的交互时,要考虑测试的便利性。通过解耦和依赖注入,我们可以很容易地使用模拟服务来测试指令。

例如,在测试 HighlightDirective 时,我们可以创建一个模拟的 LoggerService,这样可以隔离 LoggerService 的实际实现,专注于测试 HighlightDirective 的功能:

import { TestBed } from '@angular/core/testing';
import { HighlightDirective } from './highlight.directive';
import { LoggerService } from './logger.service';

describe('HighlightDirective', () => {
  let loggerService: LoggerService;
  let directive: HighlightDirective;
  let mockLoggerService: any;

  beforeEach(() => {
    mockLoggerService = {
      log: jasmine.createSpy('log')
    };

    TestBed.configureTestingModule({
      providers: [
        { provide: LoggerService, useValue: mockLoggerService }
      ]
    });

    loggerService = TestBed.inject(LoggerService);
    const el = document.createElement('div');
    directive = new HighlightDirective(el, loggerService);
  });

  it('should log when mouse enters', () => {
    directive.onMouseEnter();
    expect(mockLoggerService.log).toHaveBeenCalledWith('Mouse entered the element');
  });

  it('should log when mouse leaves', () => {
    directive.onMouseLeave();
    expect(mockLoggerService.log).toHaveBeenCalledWith('Mouse left the element');
  });
});

指令与服务交互中的常见问题及解决方法

服务注入失败

可能原因是服务没有正确提供,或者注入器没有找到服务。

解决方法

  1. 确保服务在正确的模块或组件注入器中提供。如果是全局服务,检查 providedIn: 'root' 是否正确设置。
  2. 检查指令所在的模块是否导入了提供服务的模块。

数据不一致问题

当多个指令依赖同一个服务的数据时,可能会出现数据不一致的情况,尤其是在异步操作的情况下。

解决方法

  1. 使用 Observable 来处理服务中的数据变化,以便指令能够订阅数据变化并及时更新。
  2. 在服务中提供方法来统一更新数据,避免不同指令直接修改数据导致不一致。

内存泄漏

如果指令在销毁时没有正确清理与服务的关联,可能会导致内存泄漏。

解决方法

  1. 在指令的 ngOnDestroy 生命周期钩子中取消订阅服务的 Observable,如前面 DisplayDirective 示例中所示。
  2. 如果指令在服务中注册了一些回调函数,在 ngOnDestroy 中取消这些注册。

复杂场景下的指令与服务交互

指令链与服务协同

在一些复杂的 UI 场景中,可能会有多个指令同时作用于一个元素,形成指令链。这些指令可能需要与同一个服务协同工作。

例如,我们有一个 TooltipDirectiveHighlightDirective 同时作用于一个按钮。TooltipDirective 用于显示工具提示,HighlightDirective 用于高亮按钮。它们都可能依赖一个 ThemeService 来获取当前主题相关的颜色和样式。

import { Directive, ElementRef, HostListener } from '@angular/core';
import { ThemeService } from './theme.service';

@Directive({
  selector: '[appTooltip]'
})
export class TooltipDirective {
  constructor(private el: ElementRef, private themeService: ThemeService) {}

  @HostListener('mouseenter') onMouseEnter() {
    const tooltipColor = this.themeService.getTooltipColor();
    // 显示工具提示并设置颜色
  }

  @HostListener('mouseleave') onMouseLeave() {
    // 隐藏工具提示
  }
}
import { Directive, ElementRef, HostListener } from '@angular/core';
import { ThemeService } from './theme.service';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef, private themeService: ThemeService) {}

  @HostListener('mouseenter') onMouseEnter() {
    const highlightColor = this.themeService.getHighlightColor();
    this.el.nativeElement.style.backgroundColor = highlightColor;
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.el.nativeElement.style.backgroundColor = null;
  }
}

在这种情况下,ThemeService 确保了两个指令在颜色和样式方面的一致性。

动态指令与服务交互

动态指令是在运行时创建和销毁的指令。例如,使用 ComponentFactoryResolver 创建动态组件(也是一种特殊的指令)。

假设我们有一个 DynamicComponentService 用于创建动态组件,并且这些动态组件需要与一个 DataService 交互获取数据。

import { Injectable } from '@angular/core';
import { ComponentFactoryResolver, ComponentRef, Injector, Type } from '@angular/core';
import { DynamicComponent } from './dynamic.component';

@Injectable({
  providedIn: 'root'
})
export class DynamicComponentService {
  constructor(private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector) {}

  createComponent(componentType: Type<DynamicComponent>): ComponentRef<DynamicComponent> {
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentType);
    const componentRef = componentFactory.create(this.injector);
    return componentRef;
  }
}
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';

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

  constructor(private dataService: DataService) {}

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

在组件中使用 DynamicComponentService 创建动态组件:

import { Component } from '@angular/core';
import { DynamicComponentService } from './dynamic - component.service';
import { DynamicComponent } from './dynamic.component';

@Component({
  selector: 'app - main',
  templateUrl: './main.component.html'
})
export class MainComponent {
  constructor(private dynamicComponentService: DynamicComponentService) {}

  createDynamicComponent() {
    const componentRef = this.dynamicComponentService.createComponent(DynamicComponent);
    // 将组件插入到 DOM 中
  }
}

这样,动态创建的组件能够与 DataService 交互获取数据。

性能优化在指令与服务交互中的应用

减少不必要的服务调用

在指令中,如果频繁调用服务的方法获取相同的数据,会影响性能。可以通过缓存数据来减少不必要的服务调用。

例如,在一个 StockPriceDirective 中,它需要从 StockService 获取股票价格并显示。如果股票价格变化不频繁,我们可以在指令中缓存价格:

import { Directive, ElementRef, OnInit } from '@angular/core';
import { StockService } from './stock.service';

@Directive({
  selector: '[appStockPrice]'
})
export class StockPriceDirective implements OnInit {
  private cachedPrice: number;

  constructor(private el: ElementRef, private stockService: StockService) {}

  ngOnInit() {
    this.updatePrice();
    setInterval(() => {
      const newPrice = this.stockService.getStockPrice();
      if (newPrice!== this.cachedPrice) {
        this.cachedPrice = newPrice;
        this.updatePrice();
      }
    }, 5000);
  }

  private updatePrice() {
    this.el.nativeElement.textContent = `Stock price: ${this.cachedPrice || this.stockService.getStockPrice()}`;
  }
}

优化服务的性能

服务本身的性能也会影响指令与服务的交互。对于一些复杂的计算或数据获取操作,服务可以使用异步操作、缓存等技术来提高性能。

例如,DataService 在获取远程数据时,可以使用 HttpClient 的缓存机制:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, shareReplay } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private dataCache: Observable<any>;

  constructor(private http: HttpClient) {}

  getData(): Observable<any> {
    if (!this.dataCache) {
      this.dataCache = this.http.get('/api/data').pipe(
        shareReplay(1)
      );
    }
    return this.dataCache;
  }
}

这样,当多个指令或组件调用 getData 方法时,只会发起一次 HTTP 请求。

避免过度的 DOM 操作

指令主要用于 DOM 操作,但过度的 DOM 操作会影响性能。在与服务交互时,要尽量减少不必要的 DOM 操作。

例如,HighlightDirective 在更新高亮颜色时,只有当颜色真正发生变化时才更新 DOM:

import { Directive, ElementRef, HostListener } from '@angular/core';
import { ThemeService } from './theme.service';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  private currentColor: string | null;

  constructor(private el: ElementRef, private themeService: ThemeService) {}

  @HostListener('mouseenter') onMouseEnter() {
    const newColor = this.themeService.getHighlightColor();
    if (newColor!== this.currentColor) {
      this.currentColor = newColor;
      this.el.nativeElement.style.backgroundColor = newColor;
    }
  }

  @HostListener('mouseleave') onMouseLeave() {
    const newColor = null;
    if (newColor!== this.currentColor) {
      this.currentColor = newColor;
      this.el.nativeElement.style.backgroundColor = newColor;
    }
  }
}