App.settings - à la manière angulaire?

87

Je veux ajouter une App Settingssection dans mon application où elle contiendra des consts et des valeurs prédéfinies.

J'ai déjà lu cette réponse qui utilise OpaqueTokenMais elle est obsolète dans Angular. Cet article explique les différences, mais il n'a pas fourni un exemple complet et mes tentatives ont échoué.

Voici ce que j'ai essayé (je ne sais pas si c'est la bonne façon):

//ServiceAppSettings.ts

import {InjectionToken, OpaqueToken} from "@angular/core";

const CONFIG = {
  apiUrl: 'http://my.api.com',
  theme: 'suicid-squad',
  title: 'My awesome app'
};
const FEATURE_ENABLED = true;
const API_URL = new InjectionToken<string>('apiUrl');

Et c'est le composant où je veux utiliser ces consts:

//MainPage.ts

import {...} from '@angular/core'
import {ServiceTest} from "./ServiceTest"

@Component({
  selector: 'my-app',
  template: `
   <span>Hi</span>
  ` ,  providers: [
    {
      provide: ServiceTest,
      useFactory: ( apiUrl) => {
        // create data service
      },
      deps: [

        new Inject(API_URL)
      ]
    }
  ]
})
export class MainPage {


}

Mais cela ne fonctionne pas et j'obtiens des erreurs.

Question:

Comment puis-je consommer les valeurs "app.settings" de manière angulaire?

plunker

NB Bien sûr, je peux créer le service Injectable et le mettre dans le fournisseur du NgModule, mais comme je l'ai dit, je veux le faire avec InjectionToken, de manière angulaire.

Royi Namir
la source
Vous pouvez vérifier ma réponse ici sur la base de la documentation officielle actuelle
JavierFuentes
@javier no. Votre lien a un problème si deux fournisseurs fournissent le même nom, donc vous avez maintenant un problème. Entring opaquetoken
Royi Namir
vous savez que [OpaqueToken est obsolète]. ( angular.io/api/core/OpaqueToken ) Cet article explique comment éviter les collisions de noms dans les fournisseurs angulaires
JavierFuentes
Yaeh i iknow mais l'article lié est toujours faux.
Royi Namir
2
peut être ci-dessous le lien peut être utile pour tous ceux qui aiment utiliser la nouvelle architecture du schéma de configuration angulaire devblogs.microsoft.com/premier-developer/…
M_Farahmand

Réponses:

56

J'ai compris comment faire cela avec InjectionTokens (voir l'exemple ci-dessous), et si votre projet a été construit en utilisant le, Angular CLIvous pouvez utiliser les fichiers d'environnement trouvés dans/environments pour statique application wide settingscomme un point de terminaison API, mais en fonction des exigences de votre projet, vous finirez probablement en utilisant les deux car les fichiers d'environnement ne sont que des littéraux d'objet, tandis qu'une configuration injectable utilisant InjectionToken's peut utiliser les variables d'environnement et comme il s'agit d'une classe, une logique peut être appliquée pour la configurer en fonction d'autres facteurs de l'application, tels que les données de demande http initiales, le sous-domaine , etc.

Exemple de jetons d'injection

/app/app-config.module.ts

import { NgModule, InjectionToken } from '@angular/core';
import { environment } from '../environments/environment';

export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');

export class AppConfig {
  apiEndpoint: string;
}

export const APP_DI_CONFIG: AppConfig = {
  apiEndpoint: environment.apiEndpoint
};

@NgModule({
  providers: [{
    provide: APP_CONFIG,
    useValue: APP_DI_CONFIG
  }]
})
export class AppConfigModule { }

/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppConfigModule } from './app-config.module';

@NgModule({
  declarations: [
    // ...
  ],
  imports: [
    // ...
    AppConfigModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Maintenant, vous pouvez simplement le DI dans n'importe quel composant, service, etc.:

/app/core/auth.service.ts

import { Injectable, Inject } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

import { APP_CONFIG, AppConfig } from '../app-config.module';
import { AuthHttp } from 'angular2-jwt';

@Injectable()
export class AuthService {

  constructor(
    private http: Http,
    private router: Router,
    private authHttp: AuthHttp,
    @Inject(APP_CONFIG) private config: AppConfig
  ) { }

  /**
   * Logs a user into the application.
   * @param payload
   */
  public login(payload: { username: string, password: string }) {
    return this.http
      .post(`${this.config.apiEndpoint}/login`, payload)
      .map((response: Response) => {
        const token = response.json().token;
        sessionStorage.setItem('token', token); // TODO: can this be done else where? interceptor
        return this.handleResponse(response); // TODO:  unset token shouldn't return the token to login
      })
      .catch(this.handleError);
  }

  // ...
}

Vous pouvez également taper vérifier la configuration à l'aide de l'AppConfig exporté.

mtpultz
la source
Non, mais vous pouvez littéralement copier et coller la première partie dans un fichier, l'importer dans votre fichier app.module.ts, et la DI n'importe où et la sortir sur la console. Je prendrais plus de temps pour installer cela dans un plunker que pour faire ces étapes.
mtpultz
Oh je pensais que vous aviez déjà un plunker pour ça :-) Merci.
Royi Namir
Pour ceux qui veulent: plnkr.co/edit/2YMZk5mhP1B4jTgA37B8?p=preview
Royi Namir
1
Je ne pense pas que vous ayez besoin d'exporter l'interface / la classe AppConfig. Vous n'avez certainement pas besoin de l'utiliser lors de la DI. Pour que cela fonctionne dans un fichier, il devait s'agir d'une classe au lieu d'une interface, mais cela n'a pas d'importance. En fait, le guide de style suggère d'utiliser des classes au lieu d'interfaces car cela signifie moins de code et vous pouvez toujours taper check en les utilisant. En ce qui concerne son utilisation par InjectionToken via des génériques, c'est quelque chose que vous voudrez inclure.
mtpultz
1
J'essaie d'injecter le point de terminaison de l'API dynamiquement à l'aide des variables d'environnement d'Azure et des fonctionnalités de transformation JSON, mais il semble que cette réponse ne récupère que l'apiEndpoint du fichier d'environnement. Comment allez-vous le récupérer de la configuration et l'exporter?
Archibald
138

Si vous utilisez , il existe encore une autre option:

Angular CLI fournit des fichiers d'environnement dans src/environments( les fichiers par défaut sont environment.ts(dev) et environment.prod.ts(production)).

Notez que vous devez fournir les paramètres de configuration dans tous les environment.*fichiers, par exemple,

environment.ts :

export const environment = {
  production: false,
  apiEndpoint: 'http://localhost:8000/api/v1'
};

environment.prod.ts :

export const environment = {
  production: true,
  apiEndpoint: '__your_production_server__'
};

et utilisez-les dans votre service (le fichier d'environnement correct est choisi automatiquement):

api.service.ts

// ... other imports
import { environment } from '../../environments/environment';

@Injectable()
export class ApiService {     

  public apiRequest(): Observable<MyObject[]> {
    const path = environment.apiEndpoint + `/objects`;
    // ...
  }

// ...
}

En savoir plus sur les environnements applicatifs sur Github (Angular CLI version 6) ou dans le guide officiel Angular (version 7) .

tilo
la source
2
cela fonctionne bien.Mais lors du déplacement de la version, il est également modifié en tant que bundle.Je devrais changer la configuration dans mon service pas dans le code après le passage à la production
kamalav
43
C'est en quelque sorte un anti-pattern dans le développement logiciel normal; l'URL de l'API est juste une configuration. Reconfigurer une application pour un environnement différent ne devrait pas nécessiter une reconstruction. Il doit être construit une fois, déployer plusieurs fois (pré-production, préparation, production, etc.).
Matt Tester le
3
@MattTester C'est en fait ce qui est maintenant une histoire officielle Angular-CLI. Si vous avez une meilleure réponse à cette question, n'hésitez pas à la poster!
tilo
7
est-il configurable après ng build?
NK
1
Oh ok, j'ai mal lu les commentaires. Je conviens que cela prête à un anti-pattern, je pensais qu'il y avait une histoire pour les configurations dynamiques d'exécution.
Jens Bodal
83

Il n'est pas conseillé d'utiliser les environment.*.tsfichiers pour la configuration de l'URL de votre API. Il semble que vous devriez le faire, car cela mentionne le mot «environnement».

L'utilisation de ceci est en fait une configuration au moment de la compilation . Si vous souhaitez modifier l'URL de l'API, vous devrez reconstruire. C'est quelque chose que vous ne voulez pas avoir à faire ... demandez simplement à votre sympathique service QA :)

Ce dont vous avez besoin est la configuration d'exécution , c'est-à-dire que l'application charge sa configuration au démarrage.

Certaines autres réponses abordent ce sujet, mais la différence est que la configuration doit être chargée dès le démarrage de l'application , afin qu'elle puisse être utilisée par un service normal chaque fois qu'il en a besoin.

Pour implémenter la configuration d'exécution:

  1. Ajouter un fichier de configuration JSON au /src/assets/dossier (afin qu'il soit copié lors de la construction)
  2. Créer un AppConfigServicepour charger et distribuer la configuration
  3. Chargez la configuration à l'aide d'un APP_INITIALIZER

1. Ajoutez le fichier de configuration à /src/assets

Vous pouvez l'ajouter à un autre dossier, mais vous devez indiquer à la CLI qu'il s'agit d'un actif dans le angular.json. Commencez par utiliser le dossier des actifs:

{
  "apiBaseUrl": "https://development.local/apiUrl"
}

2. Créez AppConfigService

C'est le service qui sera injecté chaque fois que vous aurez besoin de la valeur de configuration:

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

  private appConfig: any;

  constructor(private http: HttpClient) { }

  loadAppConfig() {
    return this.http.get('/assets/config.json')
      .toPromise()
      .then(data => {
        this.appConfig = data;
      });
  }

  // This is an example property ... you can make it however you want.
  get apiBaseUrl() {

    if (!this.appConfig) {
      throw Error('Config file not loaded!');
    }

    return this.appConfig.apiBaseUrl;
  }
}

3. Chargez la configuration à l'aide d'un APP_INITIALIZER

Pour permettre à l ' AppConfigServiced'être injecté en toute sécurité, avec une configuration entièrement chargée, nous devons charger la configuration au démarrage de l'application. Il est important de noter que la fonction d'usine d'initialisation doit retourner un Promiseafin qu'Angular sache attendre la fin de sa résolution avant de terminer le démarrage:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      multi: true,
      deps: [AppConfigService],
      useFactory: (appConfigService: AppConfigService) => {
        return () => {
          //Make sure to return a promise!
          return appConfigService.loadAppConfig();
        };
      }
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Vous pouvez maintenant l'injecter partout où vous en avez besoin et toute la configuration sera prête à être lue:

@Component({
  selector: 'app-test',
  templateUrl: './test.component.html',
  styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {

  apiBaseUrl: string;

  constructor(private appConfigService: AppConfigService) {}

  ngOnInit(): void {
    this.apiBaseUrl = this.appConfigService.apiBaseUrl;
  }

}

Je ne peux pas le dire assez fortement, configurer vos URL d'API en tant que configuration au moment de la compilation est un anti-modèle . Utilisez la configuration d'exécution.

Testeur de Matt
la source
4
Fichier local ou service différent, la configuration au moment de la compilation ne doit pas être utilisée pour une URL d'API. Imaginez si votre application est vendue en tant que produit (acheteur à installer), vous ne voulez pas qu'il la compile, etc. De toute façon, vous ne voulez pas recompiler quelque chose qui a été construit il y a 2 ans, juste parce que l'URL de l'API a changé. Le risque!!
Matt Tester
1
@Bloodhound Vous pouvez en avoir plus d'un, APP_INITIALIZERmais je ne pense pas que vous puissiez facilement les rendre dépendants les uns des autres. Il semble que vous ayez une bonne question à poser, alors peut-être un lien vers elle ici?
Matt Tester
2
@MattTester - Si Angular implémentait jamais cette fonctionnalité, cela résoudrait notre problème: github.com/angular/angular/issues/23279#issuecomment-528417026
Mike Becatti
2
@CrhistianRamirez C'est du point de vue de l'application: la configuration n'est connue qu'au moment de l'exécution et le fichier statique est en dehors de la construction et peut être défini de plusieurs façons au moment du déploiement. Le fichier statique convient parfaitement aux configurations non sensibles. Une API ou un autre point de terminaison protégé est possible avec la même technique, mais comment s'authentifier pour le rendre protégé est votre prochain défi.
Matt Tester
1
@DaleK En lisant entre les lignes, vous déployez à l'aide de Web Deploy. Si vous utilisez un pipeline de déploiement, comme Azure DevOps, il est alors possible de définir correctement le fichier de configuration à l'étape suivante. Le paramétrage de la configuration relève de la responsabilité du processus / pipeline de déploiement, qui peut remplacer les valeurs du fichier de configuration par défaut. J'espère que cela clarifie.
Matt Tester
8

Voici ma solution, se charge à partir de .json pour permettre les modifications sans reconstruction

import { Injectable, Inject } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Location } from '@angular/common';

@Injectable()
export class ConfigService {

    private config: any;

    constructor(private location: Location, private http: Http) {
    }

    async apiUrl(): Promise<string> {
        let conf = await this.getConfig();
        return Promise.resolve(conf.apiUrl);
    }

    private async getConfig(): Promise<any> {
        if (!this.config) {
            this.config = (await this.http.get(this.location.prepareExternalUrl('/assets/config.json')).toPromise()).json();
        }
        return Promise.resolve(this.config);
    }
}

et config.json

{
    "apiUrl": "http://localhost:3000/api"
}
PJM
la source
1
Le problème avec cette approche est que config.json est ouvert sur le monde. Comment empêcheriez-vous quelqu'un de taper www.mywebsite.com/assetts/config.json?
Alberto L. Bonfiglio
1
@ AlbertoL.Bonfiglio vous configurez le serveur pour ne pas autoriser l'accès de l'extérieur au fichier config.json (ou le placer dans un répertoire qui n'a pas d'accès public)
Alex Pandrea
C'est aussi ma solution préférée, mais toujours préoccupée par les risques de sécurité.
Viqas
7
S'il vous plaît, pouvez-vous m'aider à faire les choses correctement? En quoi est-ce plus risqué que traditionnel pour les environnements angulaires? Le contenu complet de environments.prod.tsafter ng build --prodsera dans un .jsfichier à un moment donné. Même si obscurcies, les données de environments.prod.tsseront en texte clair. Et comme tous les fichiers .js, il sera disponible sur la machine de l'utilisateur final.
igann
5
@ AlbertoL.Bonfiglio Puisqu'une application Angular est par nature une application cliente et que JavaScript sera utilisé pour transmettre les données et la configuration, aucune configuration secrète ne devrait y être utilisée; toutes les définitions de configuration secrètes doivent se trouver derrière une couche API où le navigateur ou les outils de navigateur d'un utilisateur ne peuvent pas y accéder. Les valeurs telles que l'URI de base d'une API sont acceptables pour le public, car l'API doit avoir ses propres informations d'identification et sa sécurité en fonction de la connexion d'un utilisateur (jeton de support sur https).
Tommy Elliott
4

Fichier de configuration du pauvre homme:

Ajoutez à votre index.html comme première ligne dans la balise body:

<script lang="javascript" src="assets/config.js"></script>

Ajoutez des actifs / config.js:

var config = {
    apiBaseUrl: "http://localhost:8080"
}

Ajoutez config.ts:

export const config: AppConfig = window['config']

export interface AppConfig {
    apiBaseUrl: string
}
Matthias
la source
Sérieusement, +1 pour résumer une solution en ses composants les plus élémentaires tout en maintenant la cohérence du type.
Lumineux
4

J'ai trouvé que l'utilisation d'un APP_INITIALIZERfor this ne fonctionne pas dans les situations où d'autres fournisseurs de services exigent que la configuration soit injectée. Ils peuvent être instanciés avant leur APP_INITIALIZERexécution.

J'ai vu d'autres solutions qui fetchpermettent de lire un fichier config.json et de le fournir à l'aide d'un jeton d'injection dans un paramètre platformBrowserDynamic()avant de démarrer le module racine. Mais fetchn'est pas pris en charge dans tous les navigateurs et en particulier les navigateurs WebView pour les appareils mobiles que je cible.

Ce qui suit est une solution qui fonctionne pour moi à la fois pour PWA et les appareils mobiles (WebView). Remarque: je n'ai testé que sous Android jusqu'à présent; travailler à domicile signifie que je n'ai pas accès à un Mac pour construire.

Dans main.ts:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { APP_CONFIG } from './app/lib/angular/injection-tokens';

function configListener() {
  try {
    const configuration = JSON.parse(this.responseText);

    // pass config to bootstrap process using an injection token
    platformBrowserDynamic([
      { provide: APP_CONFIG, useValue: configuration }
    ])
      .bootstrapModule(AppModule)
      .catch(err => console.error(err));

  } catch (error) {
    console.error(error);
  }
}

function configFailed(evt) {
  console.error('Error: retrieving config.json');
}

if (environment.production) {
  enableProdMode();
}

const request = new XMLHttpRequest();
request.addEventListener('load', configListener);
request.addEventListener('error', configFailed);
request.open('GET', './assets/config/config.json');
request.send();

Ce code:

  1. lance une requête asynchrone pour le config.jsonfichier.
  2. Une fois la requête terminée, analyse le JSON en un objet Javascript
  3. fournit la valeur à l'aide du APP_CONFIGjeton d'injection, avant l'amorçage.
  4. Et enfin amorce le module racine.

APP_CONFIGpeut ensuite être injecté dans tout fournisseur supplémentaire dans app-module.tset il sera défini. Par exemple, je peux initialiser le FIREBASE_OPTIONSjeton d'injection à partir @angular/firede ce qui suit:

{
      provide: FIREBASE_OPTIONS,
      useFactory: (config: IConfig) => config.firebaseConfig,
      deps: [APP_CONFIG]
}

Je trouve que tout cela est étonnamment difficile (et hacky) à faire pour une exigence très courante. Espérons que dans un proche avenir, il y aura un meilleur moyen, tel que la prise en charge des usines de fournisseurs asynchrones.

Le reste du code par souci d'exhaustivité ...

Dans app/lib/angular/injection-tokens.ts:

import { InjectionToken } from '@angular/core';
import { IConfig } from '../config/config';

export const APP_CONFIG = new InjectionToken<IConfig>('app-config');

et dans app/lib/config/config.tsje définis l'interface pour mon fichier de configuration JSON:

export interface IConfig {
    name: string;
    version: string;
    instance: string;
    firebaseConfig: {
        apiKey: string;
        // etc
    }
}

La configuration est stockée dans assets/config/config.json:

{
  "name": "my-app",
  "version": "#{Build.BuildNumber}#",
  "instance": "localdev",
  "firebaseConfig": {
    "apiKey": "abcd"
    ...
  }
}

Remarque: j'utilise une tâche Azure DevOps pour insérer Build.BuildNumber et remplacer d'autres paramètres pour différents environnements de déploiement lors de son déploiement.

Glenn
la source
2

Voici mes deux solutions pour cela

1. Stocker dans des fichiers json

Créez simplement un fichier json et entrez dans votre composant par $http.get()méthode. Si j'avais besoin de ce très bas alors c'est bon et rapide.

2. Stocker en utilisant les services de données

Si vous souhaitez stocker et utiliser tous les composants ou avoir une utilisation importante, il est préférable d'utiliser le service de données. Comme ça :

  1. Créez simplement un dossier statique dans le src/appdossier.

  2. Créez un fichier nommé comme fuels.tsdans le dossier statique. Vous pouvez également stocker d'autres fichiers statiques ici. Laissez définir vos données comme ceci. En supposant que vous ayez des données sur les carburants.

__

export const Fuels {

   Fuel: [
    { "id": 1, "type": "A" },
    { "id": 2, "type": "B" },
    { "id": 3, "type": "C" },
    { "id": 4, "type": "D" },
   ];
   }
  1. Créez un nom de fichier static.services.ts

__

import { Injectable } from "@angular/core";
import { Fuels } from "./static/fuels";

@Injectable()
export class StaticService {

  constructor() { }

  getFuelData(): Fuels[] {
    return Fuels;
  }
 }`
  1. Vous pouvez maintenant le rendre disponible pour chaque module

importez simplement dans le fichier app.module.ts comme celui-ci et changez de fournisseur

import { StaticService } from './static.services';

providers: [StaticService]

Maintenant, utilisez-le comme StaticServicedans n'importe quel module.

C'est tout.

amku91
la source
Bonne solution car vous n'avez pas à recompiler. L'environnement est comme le coder en dur dans le code. Méchant. +1
Terrance00
0

Nous avons eu ce problème il y a des années avant mon adhésion et mis en place une solution utilisant le stockage local pour les informations sur les utilisateurs et l'environnement. Angular 1.0 jours pour être exact. Nous étions auparavant en train de créer dynamiquement un fichier js au moment de l'exécution qui placerait ensuite les URL d'API générées dans une variable globale. Nous sommes un peu plus motivés par la POO ces jours-ci et n'utilisons pas le stockage local pour quoi que ce soit.

J'ai créé une meilleure solution pour déterminer l'environnement et la création d'URL d'API.

En quoi cela diffère-t-il?

L'application ne se charge que si le fichier config.json est chargé. Il utilise des fonctions d'usine pour créer un degré plus élevé de SOC. Je pourrais encapsuler cela dans un service, mais je n'ai jamais vu de raison pour laquelle la seule similitude entre les différentes sections du fichier est qu'elles existent ensemble dans le fichier. Avoir une fonction d'usine me permet de passer la fonction directement dans un module s'il est capable d'accepter une fonction. Enfin, j'ai plus de facilité à configurer InjectionTokens lorsque les fonctions d'usine sont disponibles.

Des inconvénients?

Vous n'avez pas de chance d'utiliser cette configuration (et la plupart des autres réponses) si le module que vous souhaitez configurer ne permet pas à une fonction d'usine d'être transmise à forRoot () ou forChild (), et qu'il n'y a pas d'autre moyen de configurer le package en utilisant une fonction d'usine.

Instructions

  1. En utilisant fetch pour récupérer un fichier json, je stocke l'objet dans la fenêtre et déclenche un événement personnalisé. - n'oubliez pas d'installer whatwg-fetch et de l'ajouter à votre polyfills.ts pour la compatibilité IE
  2. Demandez à un écouteur d'événement d'écouter l'événement personnalisé.
  3. L'écouteur d'événement reçoit l'événement, récupère l'objet de window pour le transmettre à un observable et efface ce qui était stocké dans window.
  4. Bootstrap Angulaire

- C'est là que ma solution commence à vraiment différer -

  1. Créez un fichier exportant une interface dont la structure représente votre config.json - cela aide vraiment à la cohérence de type et la section suivante de code nécessite un type, et ne spécifiez pas {}ou anylorsque vous savez que vous pouvez spécifier quelque chose de plus concret
  2. Créez le BehaviorSubject dans lequel vous passerez le fichier json analysé à l'étape 3.
  3. Utilisez les fonctions d'usine pour référencer les différentes sections de votre configuration pour maintenir le SOC
  4. Créez des jetons d'injection pour les fournisseurs ayant besoin du résultat de vos fonctions d'usine

- et / ou -

  1. Passez les fonctions d'usine directement dans les modules capables d'accepter une fonction dans ses méthodes forRoot () ou forChild ().

- main.ts

Je vérifie que la fenêtre ["environnement"] n'est pas remplie avant de créer un écouteur d'événements pour permettre la possibilité d'une solution où la fenêtre ["environnement"] est remplie par d'autres moyens avant que le code de main.ts ne s'exécute.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { configurationSubject } from './app/utils/environment-resolver';

var configurationLoadedEvent = document.createEvent('Event');
configurationLoadedEvent.initEvent('config-set', true, true);
fetch("../../assets/config.json")
.then(result => { return result.json(); })
.then(data => {
  window["environment"] = data;
  document.dispatchEvent(configurationLoadedEvent);
}, error => window.location.reload());

/*
  angular-cli only loads the first thing it finds it needs a dependency under /app in main.ts when under local scope. 
  Make AppModule the first dependency it needs and the rest are done for ya. Event listeners are 
  ran at a higher level of scope bypassing the behavior of not loading AppModule when the 
  configurationSubject is referenced before calling platformBrowserDynamic().bootstrapModule(AppModule)

  example: this will not work because configurationSubject is the first dependency the compiler realizes that lives under 
  app and will ONLY load that dependency, making AppModule an empty object.

  if(window["environment"])
  {
    if (window["environment"].production) {
      enableProdMode();
    }
    configurationSubject.next(window["environment"]);
    platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.log(err));
  }
*/
if(!window["environment"]) {
  document.addEventListener('config-set', function(e){
    if (window["environment"].production) {
      enableProdMode();
    }
    configurationSubject.next(window["environment"]);
    window["environment"] = undefined;
    platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.log(err));
  });
}

--- environment-resolvers.ts

J'attribue une valeur au BehaviorSubject en utilisant window ["environment"] pour la redondance. Vous pouvez concevoir une solution où votre configuration est déjà préchargée et la fenêtre ["environnement"] est déjà remplie au moment où le code de votre application Angular est exécuté, y compris le code dans main.ts

import { BehaviorSubject } from "rxjs";
import { IConfig } from "../config.interface";

const config = <IConfig>Object.assign({}, window["environment"]);
export const configurationSubject = new BehaviorSubject<IConfig>(config);
export function resolveEnvironment() {
  const env = configurationSubject.getValue().environment;
  let resolvedEnvironment = "";
  switch (env) {
 // case statements for determining whether this is dev, test, stage, or prod
  }
  return resolvedEnvironment;
}

export function resolveNgxLoggerConfig() {
  return configurationSubject.getValue().logging;
}

- app.module.ts - Supprimé pour une meilleure compréhension

Fait amusant! Les anciennes versions de NGXLogger nécessitaient que vous passiez un objet dans LoggerModule.forRoot (). En fait, le LoggerModule le fait toujours! NGXLogger expose gentiment LoggerConfig que vous pouvez remplacer vous permettant d'utiliser une fonction d'usine pour la configuration.

import { resolveEnvironment, resolveNgxLoggerConfig, resolveSomethingElse } from './environment-resolvers';
import { LoggerConfig } from 'ngx-logger';
@NgModule({
    modules: [
        SomeModule.forRoot(resolveSomethingElse)
    ],
    providers:[
        {
            provide: ENVIRONMENT,
            useFactory: resolveEnvironment
        },
        { 
            provide: LoggerConfig,
            useFactory: resolveNgxLoggerConfig
        }
    ]
})
export class AppModule

Addenda

Comment ai-je résolu la création de mes URL API?

Je voulais être capable de comprendre ce que faisait chaque URL via un commentaire et je voulais une vérification de type car c'est la plus grande force de TypeScript par rapport au javascript (IMO). Je voulais également créer une expérience pour d'autres développeurs afin d'ajouter de nouveaux points de terminaison et des apis aussi transparents que possible.

J'ai créé une classe qui prend dans l'environnement (dev, test, stage, prod, "", etc.) et passé cette valeur à une série de classes [1-N] dont le travail est de créer l'URL de base pour chaque collection d'API . Chaque ApiCollection est responsable de la création de l'URL de base pour chaque collection d'API. Il peut s'agir de nos propres API, des API d'un fournisseur ou même d'un lien externe. Cette classe transmettra l'URL de base créée à chaque API ultérieure qu'elle contient. Lisez le code ci-dessous pour voir un exemple simple. Une fois la configuration terminée, il est très simple pour un autre développeur d'ajouter un autre point de terminaison à une classe Api sans avoir à toucher à autre chose.

TLDR; principes de base de la POO et getters paresseux pour l'optimisation de la mémoire

@Injectable({
    providedIn: 'root'
})
export class ApiConfig {
    public apis: Apis;

    constructor(@Inject(ENVIRONMENT) private environment: string) {
        this.apis = new Apis(environment);
    }
}

export class Apis {
    readonly microservices: MicroserviceApiCollection;

    constructor(environment: string) {
        this.microservices = new MicroserviceApiCollection(environment);
    }
}

export abstract class ApiCollection {
  protected domain: any;

  constructor(environment: string) {
      const domain = this.resolveDomain(environment);
      Object.defineProperty(ApiCollection.prototype, 'domain', {
          get() {
              Object.defineProperty(this, 'domain', { value: domain });
              return this.domain;
          },
          configurable: true
      });
  }
}

export class MicroserviceApiCollection extends ApiCollection {
  public member: MemberApi;

  constructor(environment) {
      super(environment);
      this.member = new MemberApi(this.domain);
  }

  resolveDomain(environment: string): string {
      return `https://subdomain${environment}.actualdomain.com/`;
  }
}

export class Api {
  readonly base: any;

  constructor(baseUrl: string) {
      Object.defineProperty(this, 'base', {
          get() {
              Object.defineProperty(this, 'base',
              { value: baseUrl, configurable: true});
              return this.base;
          },
          enumerable: false,
          configurable: true
      });
  }

  attachProperty(name: string, value: any, enumerable?: boolean) {
      Object.defineProperty(this, name,
      { value, writable: false, configurable: true, enumerable: enumerable || true });
  }
}

export class MemberApi extends Api {

  /**
  * This comment will show up when referencing this.apiConfig.apis.microservices.member.memberInfo
  */
  get MemberInfo() {
    this.attachProperty("MemberInfo", `${this.base}basic-info`);
    return this.MemberInfo;
  }

  constructor(baseUrl: string) {
    super(baseUrl + "member/api/");
  }
}
Lumineux
la source