【问题标题】:How to inject different service based on certain build environment in Angular2?如何在Angular2中基于特定的构建环境注入不同的服务?
【发布时间】:2017-02-17 22:06:50
【问题描述】:

我有 HeroMockService 将返回模拟数据和 HeroService 将调用后端服务以从数据库中检索英雄。

假设 Angular2 有构建环境,如果当前构建环境是 "dev-mock",我计划将 HeroMockService 注入到 AppComponent。如果当前构建环境为"dev-rest",则应将HeroService 注入AppComponent

我想知道我怎样才能做到这一点?

【问题讨论】:

  • 我建议将其重命名为包含“模拟数据”或“模拟后端”这一短语——这是一个非常好的问题和很好的答案,很难找到!

标签: angular


【解决方案1】:

IMO,更好的选择是使用angular-in-memory-web-api

注意:这个项目是从its old location拉到angular/angular的。

它模拟Http 使用的后端,因此它不会进行实际的 XHR 调用,而是获取您提供给它的数据。要获得它,只需安装

npm install --save angular-in-memory-web-api

要创建数据库,请在 InMemoryDbService 中实现 createDb 方法

import { InMemoryDbService } from 'angular-in-memory-web-api'

export class MockData implements InMemoryDbService {
  let cats = [
    { id: 1, name: 'Fluffy' },
    { id: 2, name: 'Snowball' },
    { id: 3, name: 'Heithcliff' },
  ];
  let dogs = [
    { id: 1, name: 'Clifford' },
    { id: 2, name: 'Beethoven' },
    { id: 3, name: 'Scooby' },
  ];
  return { cats, dogs, birds };
}

然后配置一下

import { InMemoryWebApiModule } from 'angular-in-memory-web-api';

@NgModule({
  imports: [
    HttpModule,
    InMemoryWebApiModule.forRoot(MockData, {
      passThruUnknownUrl: true
    }),
  ]
})

现在,当您使用Http 并向/api/cats 发出请求时,它将从数据库中获取所有猫。如果你去/api/cats/1,它会得到第一只猫。您可以执行所有 CRUD 操作,GET、POST、PUT、DELETE。

需要注意的一点是它需要一个基本路径。在示例中,/api 是基本路径。您还可以在配置中配置根(这与基本不同)路径

InMemoryWebApiModule.forRoot(MockData, {
  rootPath: 'root',
  passThruUnknownUrl: true // forwards request not in the db
})

现在您可以使用/root/api/cats


更新

关于如何从开发切换到生产的问题,您可以使用工厂来创建提供程序。如果您要使用模拟服务而不是 in-memory-web-api,情况也是如此

providers: [
  Any,
  Dependencies
  {
    // Just inject `HeroService` everywhere, and depending
    // on the environment, the correct on will be chosen
    provide: HeroService, 
    useFactory: (any: Any, dependencies: Dependencies) => {
      if (environment.production) {
        return new HeroService(any, dependencies);
      } else {
        return new MockHeroService(any, dependencies);
      }
    },
    deps: [ Any, Dependencies ]
]

至于 in-memory-web-api,我需要回复您(我需要测试一个理论)。我刚开始使用它,还没有到需要切换到生产的地步。现在我只有上面的配置。但我确信有一种方法可以让它工作而无需更改任何内容

更新 2

好的,对于 im-memory-web-api,我们可以做的不是导入模块,而是只提供模块提供的XHRBackendXHRBackendHttp 用来进行 XHR 调用的服务。 in-memory-wep-api 模拟该服务。这就是模块所做的一切。所以我们可以自己提供服务,使用工厂

@NgModule({
  imports: [ HttpModule ],
  providers: [
    {
      provide: XHRBackend,
      useFactory: (injector: Injector, browser: BrowserXhr,
                   xsrf: XSRFStrategy, options: ResponseOptions): any => {
        if (environment.production) {
          return new XHRBackend(browser, options, xsrf);
        } else {
          return new InMemoryBackendService(injector, new MockData(), {
            // This is the configuration options
          });
        }
      },
      deps: [ Injector, BrowserXhr, XSRFStrategy, ResponseOptions ]
    }
  ]
})
export class AppHttpModule {
}

注意 BrowserXhrXSRFStrategyResponseOptions 依赖项。这就是创建原始XHRBackend 的方式。现在无需将HttpModule 导入您的应用模块,只需导入AppHttpModule

至于environment,这是您需要弄清楚的事情。有了 angular-cli,当我们在生产模式下构建时,它已经是一个自动切换到生产环境的环境。

这是我用来测试的完整示例

import { NgModule, Injector } from '@angular/core';
import { HttpModule, XHRBackend, BrowserXhr,
         ResponseOptions,  XSRFStrategy } from '@angular/http';

import { InMemoryBackendService, InMemoryDbService } from 'angular-in-memory-web-api';

let environment = {
  production: true
};

export class MockData implements InMemoryDbService {
  createDb() {
    let cats = [
      { id: 1, name: 'Fluffy' }
    ];
    return { cats };
  }
}

@NgModule({
  imports: [ HttpModule ],
  providers: [
    {
      provide: XHRBackend,
      useFactory: (injector: Injector, browser: BrowserXhr,
                   xsrf: XSRFStrategy, options: ResponseOptions): any => {
        if (environment.production) {
          return new XHRBackend(browser, options, xsrf);
        } else {
          return new InMemoryBackendService(injector, new MockData(), {});
        }
      },
      deps: [ Injector, BrowserXhr, XSRFStrategy, ResponseOptions ]
    }
  ]
})
export class AppHttpModule {
}

【讨论】:

  • 假设我切换到angular-in-memory-web-api,参考我的问题,如何在没有重大变化的情况下从 mock 模式切换到 real 请求模式(比如修改所有import 语句)?我的目标是像改变构建环境一样简单(比如从开发模式到生产模式)。
  • 查看我的模拟服务更新。我需要就使用他的 in-memory-web-api 与您联系。我需要测试的东西。现在没有我的工具
  • environment 来自哪里?
  • 环境可以在.angular-cli.json文件中配置,默认cli会生成两个环境:devprod。在最初的问题中,有第三个自定义环境 (dev-mock)。您可以使用 -e 在环境之间切换,例如:"ng serve -e dev-mock"
  • 这是一个很好的方法,但我没有关注 new XHRBackend(browser, options, xsrf);浏览器参数是做什么用的?
【解决方案2】:

这是使用条件环境逻辑执行此操作的最新方法的示例。请注意,EventService 和 LogService 只是您可能拥有的其他服务的示例。

providers: [
    EventService,
    LogService,
    MyService,
    {
      provide: MyService,
      useClass: (environment.custom == null || environment.custom === '' || environment.custom === 'MyCustomerService')
        ? MyCustomService : MyService
    }
  ]
]

【讨论】:

    【解决方案3】:

    这些答案中有很多是正确的,但会导致摇树失败。模拟服务将作为最终应用程序的一部分包含在内,这通常不是我们想要的。此外,模拟模式的切换也不是很容易。

    我创建了一个解决这些问题的工作示例:https://github.com/westonpace/angular-example-mock-services

    1. 为每个服务创建一个抽象类或一个值令牌和接口。创建一个模拟服务和一个实现抽象类/接口的真实服务。
    2. 创建一个提供所有模拟服务的 MockModule 和一个提供所有真实服务的 RealModule(确保使用 useClass/provide 字段来提供接口/抽象类)
    3. 在适当的 environment.*.ts 文件中加载 RealModule 或 MockModule
    4. 更改angular-cli 使用的angular.json 文件以创建一个新的构建目标mock,该目标使用注入MockModule 的模拟环境文件构建。创建一个新的serve 配置来提供模拟构建,以便您可以执行ng serve -c mock。更改默认量角器配置,使其使用模拟服务目标,以便ng e2e 将针对您的模拟服务运行。

    【讨论】:

    • 这种方法是否需要对单元测试做任何特别的事情(超出我们通常会做的事情)?
    • @theMayer 否。如果您的模拟模块在您的应用程序模块中,那么单元测试不应加载整个应用程序模块。如果你的模拟模块在库项目中,那么你的单元测试可以直接加载LibraryCommonModuleLibraryMockModule(除非你真的想在单元测试中运行生产模块,在这种情况下你可以直接加载它)。
    • 我跟着这个,它似乎工作得很好。谢谢
    • 迄今为止最好的方法,最重要的是一个完整的例子。它应该是 IMO 接受的答案
    • @jrahhali void 的方法应该允许摇树。只要您的模拟服务不依赖您最终不会在生产中使用的外部模块,它就应该可以正常工作。我想在我写答案的时候,我有一个独立的测试专用SimulatorModule,我在几个不同的地方使用过。
    【解决方案4】:

    这在 Angular 6 中对我有用。利用 @void's Interface approach(真正的服务和模拟服务都实现了服务接口)。接受的答案忽略了工厂的目的是返回接口的实现。

    heroservice/heroservice.module.ts

    import { NgModule, ModuleWithProviders } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { IHeroService } from './ihero.service';
    import { HeroService } from './hero.service';
    import { MockHeroService } from './mock-hero.service';
    
    
    @NgModule({
      imports: [
        CommonModule
      ],
    })
    export class HeroServiceModule {
      static forRoot(mock: boolean): ModuleWithProviders {
        return {
          ngModule: HeroServiceModule ,
          providers: [
            {
              provide: IHeroService,
              useClass: mock ? MockHeroService : HeroService,
            },
          ],
        };
      }
    }
    

    app.module.ts

    import { NgModule, isDevMode } from '@angular/core';
    import { HeroServiceModule } from './heroservice/heroservice.module';
    
    // ...
    
      imports: [
        BrowserModule,
        HttpClientModule,
        // ...
        HeroServiceModule.forRoot(
          isDevMode() // use mock service in dev mode
        ),
      ],
    
    // ...
    

    some/some.component.ts

    像实际服务一样导入和使用接口(实现将在运行时“提供”)。

    import { IHeroService } from '../heroservice/iheroservice.service';
    
    // ...
    

    【讨论】:

      【解决方案5】:

      Luuk G 在这篇文章中提供了一个伟大而简单的解决方案。

      https://hackernoon.com/conditional-module-imports-in-angular-518294aa4cc

      总结:

      let dev = [
      StoreDevtoolsModule.instrument({
          maxAge: 10,
        }),
      ];
      
      // if production clear dev imports and set to prod mode
      if (process.env.NODE_ENV === 'production') {
        dev = [];
        enableProdMode();
      }
      
      @NgModule({
        bootstrap: [
          Base,
        ],
        declarations: [
          Base,
        ],
        imports: [
          Home,
          RouterModule.forRoot(routes, { useHash: true }),
          StoreModule.forRoot(reducers.reducerToken),
          ...dev,
        ],
        providers: [
        ],
      })
      export class Root { }
      

      【讨论】:

        【解决方案6】:

        @Inject('IHeroService') 不适用于生产。就我而言,我只是这样做了:

        import { Injectable } from '@angular/core';
        import * as Priority from 'priority-web-sdk';
        
        
        @Injectable()
        export class PriorityService
        {
            priority;
            constructor( ){
                this.priority=Priority;
            }
        }
        

        我想导入的Priority 库没有导出模块,所以我需要作弊。 起初我尝试了@Inject('IHeroService') 方式,但是当我编译为 prod 时它不起作用。

        希望它对某人有所帮助!

        【讨论】:

          【解决方案7】:

          见下文,我的解决方案基于 @peeskillet 之一。

          创建一个 mockreal 服务都实现的接口,例如 IHeroService

          export interface IHeroService {
              getHeroes();
          }
          
          export class HeroService implements IHeroService {
              getHeroes() {
              //Implementation goes here
              }
          }
          
          export class HeroMockService implements IHeroService {
              getHeroes() {
              //Mock implementation goes here
          }
          

          假设您已经使用 Angular-CLI 创建了 Angular 应用,请在您的 environment.ts 文件中添加适当的实现,例如:

          import { HeroService } from '../app/hero.service';
          
          export const environment = {
            production: false,
            heroService: HeroService
          };
          

          对于每个不同的environment.(prod|whatever).ts,您必须定义一个指向实现的heroService 并添加导入。

          现在,假设您要在 AppModule 类中导入服务(您也可以在需要该服务的组件上执行此操作)

          @NgModule({
            declarations: [
              AppComponent,
            ],
            imports: [
              FormsModule,
              HttpModule,
              AlertModule.forRoot()
            ],
            providers: [
              {
                provide: 'IHeroService',
                useClass: environment.heroService
              }
            ],
            bootstrap: [AppComponent]
          })
          export class AppModule { }
          

          重要的部分是提供者:

            providers: [
              {
                provide: 'IHeroService',
                useClass: environment.heroService
              }
          

          现在无论您想在哪里使用该服务,您都必须执行以下操作:

          import { IHeroService } from './../hero.service';
          
          export class HeroComponent {
          
          constructor(@Inject('IHeroService') private heroService: IHeroService) {}
          

          来源: Is it possible to inject interface with angular2? https://medium.com/beautiful-angular/angular-2-and-environment-variables-59c57ba643be http://tattoocoder.com/angular-cli-using-the-environment-option/

          【讨论】:

          • 听起来不错,但我明白了:WARNING in Circular dependency detected: my.service.ts -> logger.service.ts -> src/environments/environment.ts -> my.service.ts
          • @Mick 我解决了这个问题,而是在environment 中使用一个标志来指示哪个实现,并在我的providers 中使用useFactory
          • 恕我直言,这应该是选择的答案!它比其他解决方案更符合单一职责模式、开放封闭原则和控制反转模式等几种干净的代码模式。
          • @techphoria414 我喜欢工厂的想法但是这个解决方案似乎更干净,因为我为每项服务提供 20 项服务,我必须使用工厂提供程序,这将是重复的,并且开发人员必须记住使用工厂每个http服务
          【解决方案8】:

          对于像我这样遇到以下错误的人:

          ERROR in Error encountered resolving symbol values statically.
          Function calls are not supported. Consider replacing the function
          or lambda with a reference to an exported function
          

          为了我的目的,它是一个 ErrorHandler:

          {
              provide: ErrorHandler,
              useFactory: getErrorHandler,
              deps: [ Injector ]
          }
          ...
          export function getErrorHandler(injector: Injector): ErrorHandler {
              if (environment.production) {
                  return new MyErrorHandler(injector);
              } else {
                  return new ErrorHandler();
              }
          }
          

          【讨论】:

            【解决方案9】:

            如果您将代码组织成 Angular2 模块,您可以创建一个附加模块来导入模拟服务,例如:

            @NgModule({
               providers: [
                  { provide: HeroService, useClass: HeroMockService }
               ]
            })
            export class MockModule {}
            

            假设您在核心模块中为普通服务声明导入:

            @NgModule({
               providers: [ HeroService ]
            })
            export class CoreModule {}
            

            只要在CoreModule 之后导入MockModule,那么将为HeroService 令牌注入的值就是HeroMockService。这是因为如果同一个令牌有两个提供者,Angular 将使用最新的值。

            然后您可以根据某个值(代表构建环境)为MockModule 自定义导入,例如:

            // Normal imported modules
            var importedModules: Array<any> = [
               CoreModule,
               BrowserModule,
               AppRoutingModule
            ];
            
            if (process.env.ENV === 'mock') {
               console.log('Enabling mocked services.');
               importedModules.push(MockModule);
            }
            
            @NgModule({
                imports: importedModules
            })
            export class AppModule {}
            

            【讨论】:

            • 这是更好的方式恕我直言
            • 我也是这样做的。只需确保提供服务而不是导入它们,否则将很难诊断问题。
            猜你喜欢
            • 2022-01-09
            • 1970-01-01
            • 1970-01-01
            • 2011-06-28
            • 2017-09-05
            • 1970-01-01
            • 1970-01-01
            • 2021-11-16
            • 2020-07-21
            相关资源
            最近更新 更多