原文:Faking dependencies in Angular applications

应用 Angular 依赖注入零碎的弱小性能,咱们能够伪造特定的用例。 这对于自动化测试很有用,但在本文中,咱们将钻研一种将其用于手动测试的办法。

为了让咱们的生存更轻松,咱们将创立一个浏览器伪造组件,因为自定义构造指令,该组件仅在开发模式下启用。 为了好玩,咱们将增加文本管道以在咱们的组件模板中应用常见的字符串操作。

Simulating a browser environment

Dynamically replacing a dependency using a class-based service

用户代理令牌工厂只对每个模块注入器评估一次,如果它没有被先人组件或指令提供的元素注入器替换,咱们必须应用另一种技术来伪造依赖项。 咱们将应用基于类的服务依赖替换依赖注入令牌依赖。

// internet-explorer-11-banner.component.tsimport { Component } from '@angular/core';import { InternetExplorerService } from './internet-explorer.service';@Component({  selector: 'internet-explorer-11-banner',  templateUrl: './internet-explorer-11-banner.component.html',})export class InternetExplorer11BannerComponent {  private isDismissed = false;  get isBannerVisible() {    return this.internetExplorer.isInternetExplorer11State && !this.isDismissed;  }  constructor(    private internetExplorer: InternetExplorerService,  ) {}  onDismiss() {    this.isDismissed = true;  }}
// internet-explorer-service.tsimport { Inject, Injectable } from '@angular/core';import { userAgentToken } from './user-agent.token';@Injectable({  providedIn: 'root',})export class InternetExplorerService {  get isInternetExplorer11State(): boolean {    return this.isInternetExplorer11(this.userAgent);  }  constructor(    @Inject(userAgentToken) private userAgent: string,  ) {}  isInternetExplorer11(userAgent: string): boolean {    return /Trident\/7\.0.+rv:11\.0/.test(userAgent);  }}

首先,咱们从依赖注入令牌中提取 Internet Explorer 11 检测到咱们新创建的 InternetExplorerService 类。 Internet Explorer 11 检测令牌当初在依据用户代理评估其值时委托给服务。

如前所述,咱们不会应用元素注入器在模板中以申明形式动静替换用户代理令牌。 相同,咱们将强制更改状态。

Creating an observable state

上面展现的方法不应用 userAgent token 的 injection token,而是应用 Observable. 这个 Observable 对象从另一个 Browser service 里取得。

// internet-explorer.service.tsimport { Injectable } from '@angular/core';import { Observable } from 'rxjs';import { map } from 'rxjs/operators';import { BrowserService } from './browser.service';@Injectable({  providedIn: 'root',})export class InternetExplorerService {  isInternetExplorer11$: Observable<boolean> =    this.browser.userAgent$.pipe(      map(userAgent => this.isInternetExplorer11(userAgent)),    );  constructor(    private browser: BrowserService,  ) {}  isInternetExplorer11(userAgent: string): boolean {    return /Trident\/7\.0.+rv:11\.0/.test(userAgent);  }}

browser service 实现里,还是会应用 user agent injection token:

// browser.service.tsimport { Inject, Injectable, OnDestroy } from '@angular/core';import { BehaviorSubject } from 'rxjs';import { distinctUntilChanged } from 'rxjs/operators';import { FakeUserAgent } from './fake-user-agent';import { userAgentToken } from './user-agent.token';@Injectable({  providedIn: 'root',})export class BrowserService implements OnDestroy {// 这体现了 Observable 和 BehaviorSubject 的区别:后者实例化时,须要一个初始值:  private userAgent = new BehaviorSubject(this.realUserAgent);  userAgent$ = this.userAgent.pipe(    distinctUntilChanged(),  );  constructor(    @Inject(userAgentToken) private realUserAgent: string,  ) {}  ngOnDestroy() {    this.userAgent.complete();  }  fakeUserAgent(value: FakeUserAgent) {    this.userAgent.next(FakeUserAgent[value]);  }  stopFakingUserAgent() {    this.userAgent.next(this.realUserAgent);  }}

咱们将以后用户代理状态存储在 BehaviorSubject<string> 中,它裸露在 BrowserService 的可察看 userAgent$ 属性中。 当整个应用程序须要用户代理时,它应该依赖于这个 observable。

最后,behavior subject 的初始值来自用户代理令牌的实在用户代理字符串。 该值也被存储以备后用,因为咱们容许通过两个命令更改浏览器状态。

咱们公开了 fakeUserAgent 办法,该办法将用户代理状态设置为假用户代理字符串。 此外,咱们容许依赖者调用 stopFakingUserAgent 办法,该办法将用户代理状态重置为实在的用户代理字符串。

Internet Explorer Service 当初公开一个名为 isInternetExplorer11$ 的可察看属性,只有浏览器服务的可察看用户代理属性收回值,就会评估该属性。

The Internet Explorer service now exposes an observable property called isInternetExplorer11$ which is evaluated whenever the observable user agent property of the browser service emits a value.

咱们当初须要的只是让弃用横幅组件依赖于可察看的 Internet Explorer 11 检测属性,而不是咱们替换的惯例属性。

<!-- internet-explorer-11-banner.component.html --><aside *ngIf="isBannerVisible$ | async">  Sorry, we will not continue to support Internet Explorer 11.<br />  Please upgrade to Microsoft Edge.<br />  <button (click)="onDismiss()">    Dismiss  </button></aside>

当初 banner 是否 visible,是由两个 boolean 值管制了,所以应用 combineLatest.

// internet-explorer-11-banner.component.tsimport { Component } from '@angular/core';import { BehaviorSubject, combineLatest } from 'rxjs';import { map } from 'rxjs/operators';import { InternetExplorerService } from './internet-explorer.service';@Component({  host: { style: 'display: block;' },  selector: 'internet-explorer-11-banner',  templateUrl: './internet-explorer-11-banner.component.html',})export class InternetExplorer11BannerComponent {  private isDismissed = new BehaviorSubject(false);  isBannerVisible$ = combineLatest(    this.internetExplorer.isInternetExplorer11$,    this.isDismissed,  ).pipe(    map(([isInternetExplorer11, isDismissed]) =>      isInternetExplorer11 && !isDismissed),  );  constructor(    private internetExplorer: InternetExplorerService,  ) {}  onDismiss(): void {    this.isDismissed.next(true);  }}

在弃用横幅组件中,咱们将 Boolean isDismissed 属性替换为 BehaviorSubject<boolean> ,该属性最后被革除(设置为 false)。 咱们当初有一个可察看的 isBannerVisible$ 属性,它是来自 isDismissed 和 InternetExplorerService#isInternetExplorer11$ 的可察看状态的组合。 UI 行为逻辑与之前相似,不同之处在于它当初示意为 observable 管道的一部分。

当初,onDismiss 事件处理程序不再为属性调配布尔值,而是通过 isDismissed 行为主体收回布尔值。

此时,应用程序的行为与咱们引入 Internet Explorer 服务和浏览器服务之前的行为完全相同。 咱们有浏览器状态更改命令,但咱们须要某种机制来触发它们。

为此,咱们将开发一个浏览器伪造器组件,使咱们可能为应用程序的其余部分伪造浏览器环境。

<!-- browser-faker.component.html --><label>  Fake a browser  <select [formControl]="selectedBrowser">    <option value="">      My browser    </option>    <option *ngFor="let browser of browsers"      [value]="browser">      {{browser | replace:wordStartPattern:' $&' | trim}}    </option>  </select></label>
// browser-faker.component.tsimport { Component, OnDestroy, OnInit } from '@angular/core';import { FormControl } from '@angular/forms';import { Observable, Subject } from 'rxjs';import { filter, takeUntil } from 'rxjs/operators';import { BrowserService } from './browser.service';import { FakeUserAgent } from './fake-user-agent';@Component({  host: { style: 'display: block;' },  selector: 'browser-faker',  templateUrl: './browser-faker.component.html',})export class BrowserFakerComponent implements OnDestroy, OnInit {  private defaultOptionValue = '';  private destroy = new Subject<void>();  private fakeBrowserSelection$: Observable<FakeUserAgent>;  private realBrowserSelection$: Observable<void>;  browsers = Object.keys(FakeUserAgent);  selectedBrowser = new FormControl(this.defaultOptionValue);  wordStartPattern = /[A-Z]|\d+/g;  constructor(    private browser: BrowserService,  ) {    this.realBrowserSelection$ = this.selectedBrowser.valueChanges.pipe(      filter(value => value === this.defaultOptionValue),      takeUntil(this.destroy),    );    this.fakeBrowserSelection$ = this.selectedBrowser.valueChanges.pipe(      filter(value => value !== this.defaultOptionValue),      takeUntil(this.destroy),    );  }  ngOnInit(): void {    this.bindEvents();  }  ngOnDestroy() {    this.unbindEvents();  }  private bindEvents(): void {// 一旦这个 Observable 有事件产生,阐明用户抉择了 fake browser    this.fakeBrowserSelection$.subscribe(userAgent =>      this.browser.fakeUserAgent(userAgent));    this.realBrowserSelection$.subscribe(() =>      this.browser.stopFakingUserAgent());  }  private unbindEvents(): void {    this.destroy.next();    this.destroy.complete();  }}

browser faker 组件注入浏览器服务。 它有一个绑定到本机 select 控件的表单控件。 抉择浏览器后,咱们开始通过浏览器服务伪造其用户代理。 抉择默认浏览器选项后,咱们会进行伪造用户代理。

当初咱们有一个浏览器伪造组件,但咱们只心愿在开发过程中启用它。 让咱们创立一个仅在开发模式下有条件地出现的构造指令。

创立一个 injection token:

// is-development-mode.token.tsimport { InjectionToken, isDevMode } from '@angular/core';export const isDevelopmentModeToken: InjectionToken<boolean> =  new InjectionToken('Development mode flag', {    factory: (): boolean => isDevMode(),    providedIn: 'root',  });
// development-only.directive.tsimport {  Directive,  Inject,  OnDestroy,  OnInit,  TemplateRef,  ViewContainerRef,} from '@angular/core';import { isDevelopmentModeToken } from './is-development-mode.token';@Directive({  exportAs: 'developmentOnly',  selector: '[developmentOnly]',})export class DevelopmentOnlyDirective implements OnDestroy, OnInit {  private get isEnabled(): boolean {    return this.isDevelopmentMode;  }  constructor(    private container: ViewContainerRef,    private template: TemplateRef<any>,    @Inject(isDevelopmentModeToken) private isDevelopmentMode: boolean,  ) {}  ngOnInit(): void {    if (this.isEnabled) {      this.createAndAttachView();    }  }  ngOnDestroy(): void {    this.destroyView();  }  private createAndAttachView(): void {    this.container.createEmbeddedView(this.template);  }  private destroyView(): void {    this.container.clear();  }}

如果应用程序在开发模式下运行,则此构造指令仅出现它所附加的组件或元素,正如其测试套件所验证的那样。

当初,剩下的就是将弃用横幅和浏览器假装器增加到咱们的应用程序中。

<!-- app.component.html --><browser-faker *developmentOnly></browser-faker><internet-explorer-11-banner></internet-explorer-11-banner>URL: <code><browser-url></browser-url></code>

最初的成果:抉择 IE 11 时,呈现 deprecation 提醒:

抉择其余浏览器时,该提醒隐没:

Summary

为了可能模仿用户环境,咱们创立了一个在开发模式下有条件地出现的浏览器伪造组件。 咱们将浏览器状态封装在一个基于类的服务中,并让应用程序依赖它。 这与浏览器伪造者应用的服务雷同。

浏览器伪造器是在 Angular 应用程序中伪造依赖项的一个简略示例。 咱们探讨了动静配置 Angular 依赖注入机制的其余技术。

本文提到的测试程序地址:https://stackblitz.com/edit/t...

更多Jerry的原创文章,尽在:"汪子熙":