51 votes

Comment injecter un service différent basé sur un certain environnement de construction dans Angular2 ?

J'ai HeroMockService qui renverra des données simulées et HeroService qui appellera le service de back end pour récupérer les héros de la base de données.

En supposant qu'Angular2 a un environnement de construction, je prévois d'injecter HeroMockService a AppComponent si l'environnement de construction actuel est "dev-mock" . Si l'environnement de construction actuel est "dev-rest" , HeroService doit être injecté dans AppComponent à la place.

J'aimerais savoir comment y parvenir ?

1 votes

Je suggérerais de renommer cette question pour inclure l'expression "données fictives" ou "backend fictif" - c'est vraiment une excellente question et une excellente réponse, elle était difficile à trouver !

34voto

void Points 390

Voir ci-dessous ma solution est basée sur celle de @peeskillet.

Créez une interface que les deux simuler y real mettre en œuvre le service, pour votre exemple IHeroService .

export interface IHeroService {
    getHeroes();
}

export class HeroService implements IHeroService {
    getHeroes() {
    //Implementation goes here
    }
}

export class HeroMockService implements IHeroService {
    getHeroes() {
    //Mock implementation goes here
}

En supposant que vous avez créé l'application Angular à l'aide d'Angular-CLI, dans votre fichier environment.ts ajoute la mise en œuvre appropriée, par exemple :

import { HeroService } from '../app/hero.service';

export const environment = {
  production: false,
  heroService: HeroService
};

Pour chaque différent environment.(prod|whatever).ts vous devez définir une heroService qui pointe vers l'implémentation et ajoute l'importation.

Maintenant, disons que vous voulez importer le service dans la classe AppModule (vous pouvez également le faire sur le composant où vous avez besoin du service).

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

La partie importante est le fournisseur :

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

Maintenant, où que vous vouliez utiliser le service, vous devez faire ce qui suit :

import { IHeroService } from './../hero.service';

export class HeroComponent {

constructor(@Inject('IHeroService') private heroService: IHeroService) {}

Fuentes: Est-il possible d'injecter une interface avec angular2 ? https://medium.com/beautiful-angular/angular-2-and-environment-variables-59c57ba643be http://tattoocoder.com/angular-cli-using-the-environment-option/

2 votes

Ça a l'air génial mais je comprends : WARNING in Circular dependency detected: my.service.ts -> logger.service.ts -> src/environments/environment.ts -> my.service.ts

0 votes

@Mick J'ai résolu ce problème en utilisant plutôt un drapeau dans le fichier environment pour indiquer quelle mise en œuvre, et useFactory dans mon providers .

4 votes

IMHO, ce devrait être la réponse choisie ! Elle répond à plusieurs modèles de code propre, comme le modèle de responsabilité unique, le principe d'ouverture et de fermeture et le modèle d'inversion de contrôle, plus que toute autre solution.

26voto

peeskillet Points 32287

IMO, une meilleure option serait d'utiliser l'option angular-in-memory-web-api .

note : ce projet a été tiré dans angulaire/angulaire de son ancien emplacement .

Il simule le backend que Http Ainsi, au lieu d'effectuer un appel XHR, il se contente de saisir les données que vous lui fournissez. Pour l'obtenir, il suffit d'installer

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

Pour créer la base de données, vous mettez en œuvre la méthode createDb dans votre InMemoryDbService

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

Ensuite, configurez-le

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

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

Maintenant, lorsque vous utilisez Http et faire une demande à /api/cats il récupérera tous les chats de la base de données. Si vous allez dans /api/cats/1 il aura le premier chat. Vous pouvez effectuer toutes les opérations CRUD, GET, POST, PUT, DELETE.

Une chose à noter est qu'il s'attend à un chemin de base. Dans l'exemple /api est le chemin de base. Vous pouvez également configurer un chemin racine (différent du chemin de base) dans la configuration.

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

Vous pouvez maintenant utiliser /root/api/cats .


UPDATE

Pour ce qui est de la question de savoir comment passer de la phase de développement à la phase de production, vous pouvez utiliser une usine pour créer les fournisseurs. Il en va de même si vous utilisez votre service fantaisie au lieu de l'API Web en mémoire.

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 ]
]

En ce qui concerne l'API Web en mémoire, je dois revenir vers vous (je dois tester une théorie). Je viens de commencer à l'utiliser et je ne suis pas encore arrivé au point où je dois passer en production. Pour l'instant, je n'ai que la configuration ci-dessus. Mais je suis sûr qu'il y a un moyen de le faire fonctionner sans avoir à changer quoi que ce soit.

MISE À JOUR 2

Ok, donc pour l'im-memory-web-api, ce que nous pouvons faire au lieu d'importer le module, c'est de fournir simplement le fichier XHRBackend que le module fournit. Le site XHRBackend est le service qui Http utilise pour faire des appels XHR. L'api in-memory-wep-api simule ce service. C'est tout ce que fait le module. Ainsi, nous pouvons simplement fournir le service nous-mêmes, en utilisant une fabrique

@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 {
}

Remarquez le BrowserXhr , XSRFStrategy y ResponseOptions dépendances. C'est ainsi que l'original XHRBackend est créé. Maintenant, au lieu d'importer le HttpModule dans votre module d'application, il suffit d'importer le AppHttpModule .

En ce qui concerne le environment c'est quelque chose que tu dois découvrir. Avec angular-cli, il existe déjà un environnement qui est automatiquement basculé en production lorsque nous construisons en mode production.

Voici l'exemple complet que j'ai utilisé pour tester avec

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 {
}

1 votes

En supposant que je sois passé à angular-in-memory-web-api en se référant à ma question, comment passer de simuler en mode real sans changements majeurs (comme la modification de toutes les fonctions de l'entreprise). import ) ? Je vise quelque chose d'aussi simple qu'un changement d'environnement de construction (par exemple du mode dev au mode prod).

0 votes

Voir ma mise à jour pour le service fictif. Je vais devoir revenir vers vous pour l'utilisation de l'API Web en mémoire. C'est quelque chose que j'aurais besoin de tester. Je n'ai pas mes outils en ce moment

1 votes

Où se trouve environment proviennent-ils ?

12voto

Pace Points 10393

Un grand nombre de ces réponses sont correctes mais échoueront au secouage des arbres. Les services fictifs seront inclus dans la demande finale, ce qui n'est généralement pas ce qui est souhaité. De plus, il n'est pas facile d'entrer et de sortir du mode simulé.

J'ai créé un exemple fonctionnel qui résout ces problèmes : https://github.com/westonpace/angular-example-mock-services

  1. Pour chaque service, créez une classe abstraite ou un jeton de valeur et une interface. Créez un service fictif et un service réel qui mettent en œuvre la classe abstraite / l'interface.
  2. Créez un MockModule qui fournit tous vos services fictifs et un RealModule qui fournit tous vos services réels (assurez-vous d'utiliser la balise useClass / provide pour fournir l'interface/la classe abstraite)
  3. Dans la section appropriée environment.*.ts chargent soit le RealModule, soit le MockModule.
  4. Apportez des modifications au angular.json utilisé par angular-cli pour créer une nouvelle cible de construction mock qui se construit avec le fichier d'environnement fantaisie qui injecte le fichier MockModule . Créer un nouveau serve qui sert la construction fictive pour que vous puissiez faire ng serve -c mock . Modifiez la configuration par défaut de Protractor pour qu'il utilise la cible de service simulée de la manière suivante ng e2e s'exécutera contre vos services fictifs.

0 votes

Cette approche nécessite-t-elle de faire quelque chose de spécial avec les tests unitaires (au-delà de ce que nous pourrions faire normalement) ?

1 votes

Si vos modules fantaisie sont dans votre module d'application, les tests unitaires ne devraient pas charger le module d'application dans son intégralité. Si vos modules fantaisie sont dans des projets de bibliothèque, votre test unitaire peut charger LibraryCommonModule y LibraryMockModule directement (à moins que vous ne souhaitiez exécuter le module de production dans votre test unitaire, auquel cas vous pouvez le charger directement).

0 votes

J'ai suivi cette procédure et cela semble très bien fonctionner. Merci

8voto

newbie Points 923

Si vous organisez votre code en modules Angular2, vous pouvez créer un module supplémentaire pour importer les services simulés, par exemple :

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

En supposant que vous déclariez l'importation des services normaux dans votre module principal :

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

Tant que vous importez MockModule après CoreModule alors la valeur qui sera injectée pour l'élément HeroService Le jeton est HeroMockService . En effet, Angular utilisera la dernière valeur s'il existe deux fournisseurs pour le même jeton.

Vous pouvez ensuite personnaliser l'importation pour MockModule en fonction d'une certaine valeur (qui représente l'environnement de construction), par exemple :

// 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 {}

1 votes

C'est la meilleure solution, à mon avis.

0 votes

C'est ce que j'ai fait aussi. Assurez-vous simplement de fournir des services et non de les importer, sinon le problème sera très difficile à diagnostiquer.

0voto

RVandersteen Points 837

Pour ceux qui, comme moi, obtiennent l'erreur suivante :

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

Dans mon cas, il s'agissait d'un ErrorHandler :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X