Angular 中的设计模式(第一部分)

2025-06-08

Angular 中的设计模式(第一部分)

原始封面照片由Caspar Camille Rubin在 Unsplash 上拍摄。

每位经验丰富的开发人员都至少听说过一些设计模式。但普遍的刻板印象是,在前端开发中,很少有人使用过这些模式。今天,让我们深入探讨一下 Angular 开发中已经使用过的设计模式,或者更确切地说,可以用来克服常见的挑战。

单例模式

单例模式是一种设计模式,指某个类只能拥有一个实例。当你需要某个类只有一个实例,但又不想每次都需要创建新实例时,单例模式非常有用;当我们想要共享资源或数据时,单例模式也非常有用。

如果你正在使用 Angular 的依赖注入,那么你已经在使用单例模式,尤其是在你使用 提供服务的情况下providedIn: root。如果我们在 某个 中提供服务,NgModule那么它只会在该 的范围内才是“单例” NgModule

工厂

工厂是一种设计模式,它可以创建具有相同接口(或从同一类扩展)但根据上下文具有不同实现的对象。您可能在 Angular 的依赖注入 (DI) 中提供服务时熟悉该选项useFactory。这本质上就是在利用该设计模式。在我的文章“ Angular 依赖注入技巧”中,我提供了一个示例,说明如何使用该useFactory选项提供记录器服务的不同实现。如果您不想阅读整篇文章,以下是工厂函数:

export function loggerFactory(
  environment: Environment, 
  http: HttpClient,
): LoggerService {
  switch (environment.name) {
    case 'develop': {
      return new DevelopLoggerService();
    }
    case 'qa': {
      return new QALoggerService(http, environment);
    }
    case 'prod': {
      return new ProdLoggerService(http, environment);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

我们用变量来确定要使用environment哪个实现。然后使用这个工厂函数来提供它:LoggerService

@NgModule({
   providers: [
     {
       provide: LoggerService,
       useFactory: loggerFactory,
       deps: [HttpClient, Environment],
       // we tell Angular to provide this dependencies
       // to the factory arguments
    },
     {provide: Environment, useValue: environment}
   ],
   // other metadata
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

您可以在文章中阅读有关其工作原理的更详细解释。

使用设计模式解决特定问题

现在,让我们讨论其他设计模式,并讨论如何使用它们来应对某些挑战。我们将讨论以下内容:

  • 适配器模式
  • 外观模式
  • 战略

适配器

适配器是一种模式,它允许我们将其他类(通常来自第三方)包装在
具有可预测接口的容器类中,并且可以轻松地被我们的代码使用。

假设我们正在使用一个处理特定 API 的第三方库。它可以是
Google Cloud、地图、AWS 服务或其他任何服务。我们希望能够在处理同一资源时,移除该类并插入另一个类。

举个例子,我们有一个服务以 XML 格式提供数据(例如 SOAP API),但我们所有的代码都使用 JSON 格式,而未来 XML API 可能会被 JSON 格式取代。让我们创建一个可以用来使用 XML API 的 Angular 服务:

@Injectable()
export class APIService {

  constructor(
    private readonly xmlAPIService: XmlApiService,
  ) { }

  getData<Result>(): Result {
    return this.xmlAPIService.getXMLData<Result>();
  }

  sendData<DataDTO>(data: DataDTO): void {
    this.xmlAPIService.sendXMLData(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,代码中有几个重要方面我们需要注意:

  1. 我们编写的服务没有提及 XML、JSON 或我们正在使用的 API 的任何实现细节
  2. 方法名称也仅反映我们处理某些数据的事实。我们处理的是哪种 API 并不重要
  3. 使用的数据类型也不重要,并且与实现并不紧密相关 - 方法是通用的
  4. 我们用此服务包装第三方 XML API,以便将来可以轻松替换

正如上一点所述,我们仅使用我们的服务来调用 API,而不是第三方库类。
这意味着,如果 XML API 被 JSON API 替换,我们只需修改服务本身,而无需修改使用它的代码。以下是从 XML 切换到 JSON 所需的代码更改:

@Injectable()
export class APIService {

  constructor(
    private readonly jsonAPIService: JsonApiService,
  ) { }

  getData<Result>(): Result {
    return this.jsonAPIService.getJSONData<Result>();
  }

  sendData<DataDTO>(data: DataDTO): void {
    this.jsonAPIService.sendJSONData(data);
  }
}
Enter fullscreen mode Exit fullscreen mode

如您所见,服务的接口保持完全相同,这意味着注入此服务的其他服务和组件
不必改变。

正面

外观模式是一种设计模式,它允许我们将复杂的子系统隐藏在应用程序的其他部分之外。当我们拥有一大组相互交互的类,并希望这些类易于被其他服务/组件使用时,外观模式非常有用。

随着 Angular 应用中使用 NgRx,Facades 变得越来越流行,因为组件现在需要处理调度操作、选择状态以及订阅特定操作。以下是一个使用 NgRx Store 而不使用 Facade 的 Angular 组件的示例:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  users$ = this.store.select(selectUsers);
  selectedUser$ = this.store.select(selectSelectedUser);
  query$ = this.store.select(selectQuery);

  constructor(
    private readonly store: Store,
    private readonly actions$: Actions,
    private readonly dialog: DialogService,
  ) { }

  ngOnInit() {
    this.store.dispatch(loadData());

    this.actions$.pipe(
      ofType(deleteUser),
      tap(() => this.dialog.open(
        'Are you sure you want to delete this user?',
      )),
    ).subscribe(() => this.store.dispatch(loadData()));
  }

  tryDeleteUser(user: User) {
    this.store.dispatch(deleteUser({ user }));
  }

  selectUser(user: User) {
    this.store.dispatch(selectUser({ user }));
  }

}
Enter fullscreen mode Exit fullscreen mode

现在,这个组件要处理很多事情,并且多次调用store.dispatch和,这会让代码稍微复杂一些。例如,store.select我们希望有一个专门的系统来处理“用户”部分。让我们为此实现一个 Facade:Store

@Injectable()
export class UsersFacade {

  users$ = this.store.select(selectUsers);
  selectedUser$ = this.store.select(selectSelectedUser);
  query$ = this.store.select(selectQuery);
  tryDeleteUser$ = this.actions$.pipe(
    ofType(deleteUser),
  );

  constructor(
    private readonly store: Store,
    private readonly actions$: Actions,
  ) { }

  tryDeleteUser(user: User) {
    this.store.dispatch(deleteUser({ user }));
  }

  selectUser(user: User) {
    this.store.dispatch(selectUser({ user }));
  }

}
Enter fullscreen mode Exit fullscreen mode

现在,让我们重构我们的组件来使用这个外观:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  users$ = this.usersFacade.users$;
  selectedUser$ = this.usersFacade.selectedUser$;
  query$ = this.usersFacade.query$;

  constructor(
    private readonly usersFacade: UsersFacade,
    private readonly dialog: DialogService,
  ) { }

  ngOnInit() {
    this.usersFacade.tryDeleteUser$.subscribe(
      () => this.dialog.open(
        'Are you sure you want to delete this user?',
      ),
    ); // do not forget to unsubscribe
  }

  tryDeleteUser(user: User) {
    this.usersFacade.tryDeleteUser(user);
  }

  selectUser(user: User) {
    this.usersFacade.selectUser(user);
  }

}
Enter fullscreen mode Exit fullscreen mode

战略

策略是一种设计模式,它允许我们在设计系统时充分考虑可定制性。
例如,我们可以创建一个以特定逻辑运行的库,但最终用户(另一个开发人员)可以
决定使用哪个 API 来实现该逻辑。

从某种意义上说,它可以被认为是适配器模式的逆:
在适配器中,最终用户将第三方服务包装在可定制的类中,而在这里使用策略
模式,我们正在设计“第三方”,同时允许最终用户选择使用哪种策略。

假设我们想要创建一个包装的库HttpClient,并且我们希望允许最终用户选择
调用哪些 API、如何进行身份验证等。我们可以创建一个 Angular 模块和一个包装类,然后
提供功能,同时还允许导入一个Strategy类,这将帮助我们决定如何使用这个包装服务,当用户未通过身份验证时该做什么,等等。

首先,我们需要创建一个Strategy最终用户必须实现的接口:

export interface HttpStrategy {
  authenticate(): void;
  isAuthenticated(): boolean;
  getToken(): string;
  onUnAuthorized(): void;
}
Enter fullscreen mode Exit fullscreen mode

然后,我们需要实现我们的包装器:

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

  constructor(
    private readonly http: HttpClient,
    @Inject(STRATEGY) private readonly strategy: HttpStrategy,
  ) { }

  get<Result>(url: string): Observable<Result> {
    return this.http.get<Result>(this.http, url);
  }

  // other methods...
}
Enter fullscreen mode Exit fullscreen mode

现在,我们必须实现拦截器来处理身份验证错误并向客户端发送标头:

@Injectable({
  providedIn: 'root',
})
export class AuthenticationInterceptor implements HttpInterceptor {

  constructor(
    @Inject(STRATEGY) private readonly strategy: HttpStrategy,
  ) { }

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler,
  ): Observable<HttpEvent<any>> {
    if (this.strategy.isAuthenticated()) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${this.strategy.getToken()}`,
        },
      });
    }
    return next.handle(request);
  }
}
Enter fullscreen mode Exit fullscreen mode

如您所见,我们将一个Strategy类注入到另一个AuthenticationInterceptor类中,以便最终用户可以决定如何进行身份验证。他们可以使用cookieslocalStorage或者其他存储来获取令牌。

现在我们还需要实现当我们得到授权错误时的拦截器:

@Injectable({
  providedIn: 'root',
})
export class UnAuthorizedErrorInterceptor implements HttpInterceptor {

  constructor(
    @Inject(STRATEGY) private readonly strategy: HttpStrategy,
  ) { }

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler,
  ): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          this.strategy.onUnAuthorized();
        }
        return throwError(error);
      }
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

这里我们再次将Strategy类注入到UnAuthorizedErrorInterceptor类中,以便最终用户可以决定如何处理错误。他们可以使用 Angularrouter.navigate或其他dialog.open方式将用户重定向到登录页面,或者显示弹窗,或者任何其他场景。从“第三方”的角度来看,最后要做的
就是创建一个NgModule封装上述所有内容的类:

const STRATEGY = new InjectionToken('STRATEGY');

@NgModule({
  imports: [
    HttpClientModule,
  ],
})
export class HttpWrapperModule {

  forRoot(strategy: any): ModuleWithProviders {
    return {
      ngModule: AppModule,
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: AuthenticationInterceptor,
          multi: true,
        },
        {
          provide: HTTP_INTERCEPTORS,
          useClass: UnAuthorizedErrorInterceptor,
          multi: true,
        },
        { provide: STRATEGY, useClass: strategy },
        // we use the `InjectionToken`
        // to provide the `Strategy` class dynamically
      ],
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,此类的用户只需HttpStrategy在导入模块时实现接口并提供该服务:

@Injectable({
  providedIn: 'root',
})
export class MyStrategy implements HttpStrategy {
  authenticate(): void {
    // do something
  }
  isAuthenticated(): boolean {
    return validateJWT(this.getToken());
  }
  getToken(): string {
    return localStorage.getItem('token');
  }
  onUnAuthorized(): void {
    this.router.navigate(['/login']);
  }

  constructor(
    private readonly router: Router,
  ) { }
}
Enter fullscreen mode Exit fullscreen mode

在模块中:

import { MyStrategy } from './my-strategy';

@NgModule({
  imports: [
    HttpWrapperModule.forRoot(MyStrategy),
  ],
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

现在我们还可以在另一个应用程序中以不同的策略使用这个包装模块。

综上所述

如果使用得当,设计模式可以成为 Angular 应用程序不可或缺的一部分,因此,在下一篇文章中,我们将探讨一些其他模式及其用例

鏂囩珷鏉ユ簮锛�https://dev.to/armandotrue/design-patterns-in-angular-part-i-3ld7
PREV
如何说服客户关注 Web 性能:案例研究构建概念验证 (PoC) 结论
NEXT
撰写技术博客 101