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

Angular服务设计与实现

2021-12-023.2k 阅读

什么是Angular服务

在Angular应用程序中,服务是一个广义的概念,它是一个提供特定功能的类,通常用于处理一些与业务逻辑、数据获取、存储等相关的任务。服务有助于实现代码的模块化和可重用性,使得应用程序的架构更加清晰和易于维护。

例如,假设我们有一个电子商务应用程序,可能会有一个 ProductService 用于获取产品列表、单个产品的详细信息等;还可能有一个 CartService 用于管理购物车中的商品,包括添加商品、移除商品、计算总价等功能。

服务的设计原则

  1. 单一职责原则:每个服务应该只负责一项特定的任务。比如,上述提到的 ProductService 只专注于与产品相关的操作,而不应该涉及购物车相关的逻辑。这样做的好处是,如果需要修改产品相关的功能,只需要关注 ProductService 这一个地方,而不会影响到其他不相关的功能。
  2. 高内聚低耦合:服务内部的方法和数据应该紧密相关,实现高度的内聚。同时,服务之间的依赖关系应该尽量简单和松散,降低耦合度。例如,CartService 依赖 ProductService 来获取产品的价格等信息,但这种依赖应该是有限的,并且通过合适的接口来实现,使得 CartService 不依赖于 ProductService 的具体实现细节。
  3. 可测试性:设计服务时要考虑到测试的便利性。一个好的设计应该使得服务可以很容易地进行单元测试,不依赖于复杂的外部环境。例如,可以通过注入模拟的依赖对象来测试服务的功能,而不需要启动整个应用程序。

创建Angular服务

在Angular中,使用 ng generate service 命令可以快速生成一个服务的骨架代码。例如,要创建一个 UserService,可以在终端中运行以下命令:

ng generate service user

这将会在项目的 src/app 目录下生成一个 user.service.ts 文件,内容大致如下:

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

@Injectable({
  providedIn: 'root'
})
export class UserService {

  constructor() { }

}

上述代码中,@Injectable() 装饰器用于标记这个类是一个可注入的服务。providedIn: 'root' 表示这个服务将在应用程序的根模块中提供,意味着整个应用程序都可以使用这个服务。

服务中的依赖注入

依赖注入是Angular中的一个核心概念,它使得我们可以轻松地管理服务之间的依赖关系。

假设我们有一个 LoggerService 用于记录日志,而 UserService 需要使用 LoggerService 来记录用户相关的操作日志。首先创建 LoggerService

ng generate service logger

logger.service.ts 内容如下:

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

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

然后在 UserService 中注入 LoggerService

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

@Injectable({
  providedIn: 'root'
})
export class UserService {

  constructor(private logger: LoggerService) { }

  login(username: string, password: string) {
    // 模拟登录逻辑
    const success = true;
    if (success) {
      this.logger.log(`${username} logged in successfully`);
    } else {
      this.logger.log(`${username} login failed`);
    }
  }
}

在上述代码中,通过在 UserService 的构造函数中声明 private logger: LoggerService,Angular会自动创建 LoggerService 的实例并注入到 UserService 中。这样 UserService 就可以使用 LoggerService 提供的 log 方法了。

服务与组件的交互

服务通常会与组件进行交互,为组件提供数据或功能。

例如,我们有一个 ProductService 和一个显示产品列表的 ProductListComponentProductService 负责从后端获取产品数据,ProductListComponent 负责展示这些数据。

product.service.ts

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

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private apiUrl = 'api/products';

  constructor(private http: HttpClient) { }

  getProducts(): Observable<any[]> {
    return this.http.get<any[]>(this.apiUrl);
  }
}

上述代码中,ProductService 使用 HttpClient 来从指定的API获取产品数据。HttpClient 是Angular提供的用于处理HTTP请求的服务,同样通过依赖注入的方式注入到 ProductService 中。

product-list.component.ts

import { Component, OnInit } from '@angular/core';
import { ProductService } from './product.service';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  products: any[] = [];

  constructor(private productService: ProductService) { }

  ngOnInit(): void {
    this.productService.getProducts().subscribe((data) => {
      this.products = data;
    });
  }
}

ProductListComponent 中,通过在构造函数中注入 ProductService,然后在 ngOnInit 生命周期钩子函数中调用 productService.getProducts() 方法来获取产品数据,并将数据赋值给 products 数组,以便在模板中进行展示。

product-list.component.html

<ul>
  <li *ngFor="let product of products">
    {{ product.name }} - {{ product.price }}
  </li>
</ul>

服务的作用域

  1. 根作用域:当我们在服务的 @Injectable 装饰器中使用 providedIn: 'root' 时,该服务处于根作用域。这意味着整个应用程序只有一个该服务的实例,所有需要该服务的组件、指令等都共享这个实例。这种方式适用于一些全局共享的服务,如前面提到的 LoggerService,它可以在整个应用程序中记录日志,不需要每个组件都有自己单独的日志记录服务实例。
  2. 模块作用域:我们也可以将服务提供在特定的模块中。例如,假设有一个 AdminModule,其中有一些管理相关的服务,这些服务只在 AdminModule 及其子模块中使用。我们可以在 admin.module.ts 中这样提供服务:
import { NgModule } from '@angular/core';
import { AdminService } from './admin.service';

@NgModule({
  providers: [AdminService]
})
export class AdminModule { }

这样,AdminService 就只在 AdminModule 及其子模块的作用域内有效,并且在这个作用域内只有一个实例。与根作用域不同的是,其他模块无法直接访问这个服务,除非通过特定的导入和配置。 3. 组件作用域:在某些情况下,我们可能希望一个服务仅在某个组件及其子组件中可用,并且每个组件实例都有自己独立的服务实例。我们可以在组件的 @Component 装饰器中提供服务:

import { Component } from '@angular/core';
import { LocalService } from './local.service';

@Component({
  selector: 'app-specific-component',
  templateUrl: './specific-component.html',
  providers: [LocalService]
})
export class SpecificComponent { }

在上述代码中,LocalService 仅在 SpecificComponent 及其子组件中可用,并且每个 SpecificComponent 的实例都会有自己独立的 LocalService 实例。这在一些需要为每个组件实例独立管理某些状态或功能的场景中非常有用。

服务中的数据共享

  1. 通过属性共享数据:服务可以通过定义属性来实现数据共享。例如,我们有一个 SharedDataService
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class SharedDataService {
  public sharedValue: string = 'default value';

  updateSharedValue(newValue: string) {
    this.sharedValue = newValue;
  }
}

然后在两个不同的组件 ComponentAComponentB 中使用这个服务来共享数据:

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

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

  changeSharedValue() {
    this.sharedService.updateSharedValue('new value from ComponentA');
  }
}
<!-- component-a.html -->
<button (click)="changeSharedValue()">Change Shared Value</button>
import { Component, OnInit } from '@angular/core';
import { SharedDataService } from './shared-data.service';

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

  constructor(private sharedService: SharedDataService) { }

  ngOnInit() {
    this.sharedValue = this.sharedService.sharedValue;
  }
}
<!-- component-b.html -->
<p>The shared value is: {{ sharedValue }}</p>

在上述示例中,ComponentA 可以通过调用 SharedDataServiceupdateSharedValue 方法来更新共享数据,ComponentB 可以获取并显示这个共享数据。 2. 通过Observable共享数据:使用 Observable 可以实现更灵活的数据共享,尤其是在数据需要实时更新的场景中。我们修改 SharedDataService 如下:

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

@Injectable({
  providedIn: 'root'
})
export class SharedDataService {
  private sharedValueSubject = new BehaviorSubject<string>('default value');
  public sharedValue$: Observable<string> = this.sharedValueSubject.asObservable();

  updateSharedValue(newValue: string) {
    this.sharedValueSubject.next(newValue);
  }
}

ComponentB 修改为:

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

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

  constructor(private sharedService: SharedDataService) { }

  ngOnInit() {
    this.sharedService.sharedValue$.subscribe((value) => {
      this.sharedValue = value;
    });
  }
}

这样,当 ComponentA 调用 updateSharedValue 方法时,ComponentB 会实时接收到数据的变化并更新显示。

服务的错误处理

在服务中进行数据获取或其他操作时,可能会发生错误,需要进行适当的错误处理。

ProductService 为例,当从API获取产品数据失败时,我们可以使用 catchError 操作符来处理错误:

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

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private apiUrl = 'api/products';

  constructor(private http: HttpClient) { }

  getProducts(): Observable<any[]> {
    return this.http.get<any[]>(this.apiUrl).pipe(
      catchError((error) => {
        console.error('Error fetching products:', error);
        // 可以在这里返回一个默认值或者抛出一个自定义错误
        return [];
      })
    );
  }
}

在上述代码中,catchError 捕获到HTTP请求失败的错误后,先在控制台打印错误信息,然后返回一个空数组,这样调用 getProducts 方法的组件就不会因为错误而导致应用程序崩溃。

服务的性能优化

  1. 缓存数据:如果服务中的某些数据不会频繁变化,可以考虑进行缓存。例如,ProductService 中获取的产品列表数据可能在一段时间内不会改变,我们可以这样实现缓存:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private apiUrl = 'api/products';
  private cachedProducts: any[] | null = null;

  constructor(private http: HttpClient) { }

  getProducts(): Observable<any[]> {
    if (this.cachedProducts) {
      return new Observable(observer => {
        observer.next(this.cachedProducts);
        observer.complete();
      });
    }

    return this.http.get<any[]>(this.apiUrl).pipe(
      catchError((error) => {
        console.error('Error fetching products:', error);
        return [];
      }),
      (products) => {
        this.cachedProducts = products;
        return products;
      }
    );
  }
}

在上述代码中,cachedProducts 用于缓存产品列表数据。当第一次调用 getProducts 方法时,会从API获取数据并缓存;后续调用时,如果缓存中有数据,则直接返回缓存的数据,减少了不必要的API请求。 2. 懒加载服务:对于一些不常用的服务,可以采用懒加载的方式,只有在真正需要使用时才进行加载。在Angular中,模块的懒加载也会导致模块内提供的服务懒加载。例如,我们有一个 SpecialFeatureService 用于一个不常用的功能,将其放在一个单独的模块 SpecialFeatureModule 中:

import { NgModule } from '@angular/core';
import { SpecialFeatureService } from './special-feature.service';

@NgModule({
  providers: [SpecialFeatureService]
})
export class SpecialFeatureModule { }

然后在路由配置中对 SpecialFeatureModule 进行懒加载:

const routes: Routes = [
  {
    path:'special-feature',
    loadChildren: () => import('./special-feature.module').then(m => m.SpecialFeatureModule)
  }
];

这样,只有当用户访问 /special-feature 路由时,SpecialFeatureModule 及其包含的 SpecialFeatureService 才会被加载,提高了应用程序的初始加载性能。

服务的安全性考虑

  1. 防止XSS攻击:当服务从后端获取数据并传递给组件进行展示时,要防止跨站脚本(XSS)攻击。在Angular中,默认情况下,插值和属性绑定会对数据进行安全的转义。例如,在 ProductListComponent 展示产品名称时:
<ul>
  <li *ngFor="let product of products">
    {{ product.name }}
  </li>
</ul>

如果 product.name 中包含恶意脚本,Angular会将其转义,不会执行脚本。但如果需要动态渲染HTML内容,比如产品描述中可能包含一些富文本格式,就需要特别小心。可以使用 DomSanitizer 服务来确保内容的安全性:

import { Component, OnInit } from '@angular/core';
import { ProductService } from './product.service';
import { DomSanitizer, SafeHtml } from '@angular/platform - browser';

@Component({
  selector: 'app - product - detail',
  templateUrl: './product - detail.component.html'
})
export class ProductDetailComponent implements OnInit {
  productDescription: SafeHtml;

  constructor(private productService: ProductService, private sanitizer: DomSanitizer) { }

  ngOnInit() {
    this.productService.getProductDescription().subscribe((description) => {
      this.productDescription = this.sanitizer.bypassSecurityTrustHtml(description);
    });
  }
}
<div [innerHTML]="productDescription"></div>

在上述代码中,DomSanitizerbypassSecurityTrustHtml 方法用于标记HTML内容是安全的,然后通过 [innerHTML] 绑定到模板中进行渲染。 2. 保护敏感数据:服务可能会处理一些敏感数据,如用户的登录信息、支付信息等。在设计服务时,要确保这些数据在传输和存储过程中的安全性。例如,在与后端进行通信时,应该使用HTTPS协议来加密数据传输。在服务内部,要对敏感数据进行妥善的存储和访问控制。比如,用户的登录令牌应该存储在安全的地方,并且只在需要与后端进行身份验证时使用,不应该随意暴露在组件的模板或其他不安全的地方。

服务与第三方库的集成

在实际项目中,常常需要将Angular服务与第三方库进行集成,以扩展应用程序的功能。

例如,我们要在应用程序中集成地图功能,可以使用 leaflet 库。首先安装 leaflet 及其类型定义:

npm install leaflet @types/leaflet

然后创建一个 MapService 来管理地图相关的操作:

import { Injectable } from '@angular/core';
import * as L from 'leaflet';

@Injectable({
  providedIn: 'root'
})
export class MapService {
  private map: L.Map | null = null;

  initializeMap(elementId: string) {
    this.map = L.map(elementId).setView([51.505, -0.09], 13);
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '© OpenStreetMap contributors'
    }).addTo(this.map);
  }

  addMarker(lat: number, lng: number, message: string) {
    if (this.map) {
      L.marker([lat, lng]).addTo(this.map).bindPopup(message).openPopup();
    }
  }
}

在组件中使用 MapService

import { Component, OnInit } from '@angular/core';
import { MapService } from './map.service';

@Component({
  selector: 'app - map - component',
  templateUrl: './map - component.html'
})
export class MapComponent implements OnInit {
  constructor(private mapService: MapService) { }

  ngOnInit() {
    this.mapService.initializeMap('map - container');
    this.mapService.addMarker(51.5, -0.09, 'Hello, this is a marker!');
  }
}
<!-- map - component.html -->
<div id="map - container" style="height: 400px;"></div>

在上述示例中,MapService 封装了 leaflet 库的基本操作,使得组件可以方便地使用地图功能,同时保持了代码的模块化和可维护性。

服务在大型项目中的架构设计

在大型Angular项目中,服务的架构设计尤为重要。

  1. 分层架构:可以采用分层架构来组织服务。例如,将服务分为数据访问层、业务逻辑层和应用服务层。
    • 数据访问层:负责与后端数据源进行交互,如数据库、RESTful API等。以 ProductDataService 为例,它专门处理与产品数据的CRUD(创建、读取、更新、删除)操作,只关注数据的获取和存储,不涉及复杂的业务逻辑。
    • 业务逻辑层:基于数据访问层提供的功能,实现具体的业务规则。比如 ProductBusinessService,它可能会调用 ProductDataService 的方法来获取产品列表,但会对列表进行一些处理,如根据某些条件过滤产品、计算产品的统计信息等。
    • 应用服务层:为组件提供直接使用的接口。例如 ProductAppService,它可能会调用 ProductBusinessService 的方法,并将处理后的结果以更适合组件使用的方式返回,同时可能会处理一些与应用程序整体相关的逻辑,如权限控制等。
  2. 微服务架构:对于超大型项目,还可以考虑采用微服务架构。将不同的业务功能拆分为独立的微服务,每个微服务可以有自己独立的Angular服务和后端服务。例如,将用户管理、订单管理、产品管理等功能分别拆分为不同的微服务。这些微服务之间通过RESTful API等方式进行通信。在Angular应用程序中,每个微服务对应的部分可以有自己独立的模块和服务,通过HTTP请求与对应的后端微服务进行交互。这样可以提高项目的可扩展性和维护性,不同的团队可以独立开发和维护不同的微服务。

通过合理的服务设计和架构,Angular应用程序可以更加健壮、可维护和可扩展,满足不同规模项目的需求。无论是小型的单页应用还是大型的企业级应用,服务在其中都扮演着至关重要的角色,正确地设计和实现服务是构建优秀Angular应用的关键之一。