App.settings - der Winkelweg?

85

Ich möchte App Settingsmeiner App einen Abschnitt hinzufügen, der einige Konstanten und vordefinierte Werte enthält.

Ich habe diese Antwort bereits gelesen , die verwendet. OpaqueTokenAber sie ist in Angular veraltet. Dieser Artikel erklärt die Unterschiede, liefert jedoch kein vollständiges Beispiel, und meine Versuche waren erfolglos.

Folgendes habe ich versucht (ich weiß nicht, ob es der richtige Weg ist):

//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');

Und dies ist die Komponente, in der ich diese Konstanten verwenden möchte:

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


}

Aber es funktioniert nicht und ich bekomme Fehler.

Frage:

Wie kann ich "app.settings" -Werte auf Winkel-Weise konsumieren?

Plunker

NB Sicher, ich kann einen injizierbaren Dienst erstellen und ihn in den Anbieter des NgModule stellen, aber wie gesagt, ich möchte es auf InjectionTokenAngular-Weise tun .

Royi Namir
quelle
Sie können meine Antwort hier anhand der aktuellen offiziellen Dokumentation
überprüfen
@ Javier Nr. Ihr Link hat ein Problem, wenn zwei Anbieter denselben Namen angeben, sodass Sie jetzt ein Problem haben. Eintreten opaquetoken
Royi Namir
Sie wissen, dass [OpaqueToken veraltet ist]. ( angle.io/api/core/OpaqueToken ) In diesem Artikel wird erläutert, wie Namenskollisionen in Angular Providers
JavierFuentes verhindert werden können
Ja, ich weiß, aber der verlinkte Artikel ist immer noch falsch.
Royi Namir
2
Der folgende Link kann für jeden hilfreich sein, der eine neue Architektur des Winkelkonfigurationsschemas devblogs.microsoft.com/premier-developer/…
M_Farahmand verwenden möchte

Antworten:

54

Ich habe herausgefunden, wie dies mit InjectionTokens gemacht wird (siehe Beispiel unten). Wenn Ihr Projekt mit dem erstellt wurde Angular CLI, können Sie die Umgebungsdateien /environmentsfür static application wide settingswie einen API-Endpunkt verwenden, aber abhängig von den Anforderungen Ihres Projekts werden Sie höchstwahrscheinlich enden Verwenden von beiden, da Umgebungsdateien nur Objektliterale sind, während eine injizierbare Konfiguration mit InjectionToken's die Umgebungsvariablen verwenden kann und da es sich um eine Klasse handelt, kann Logik angewendet werden, um sie basierend auf anderen Faktoren in der Anwendung zu konfigurieren, z. B. anfängliche http-Anforderungsdaten, Subdomain , etc.

Beispiel für Injektionstoken

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

Jetzt können Sie es einfach in eine beliebige Komponente, einen Dienst usw. integrieren:

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

  // ...
}

Sie können dann auch check the config mit der exportierten AppConfig eingeben.

mtpultz
quelle
Nein, aber Sie können den ersten Teil buchstäblich kopieren und in eine Datei einfügen, ihn in Ihre Datei app.module.ts importieren und ihn an einer beliebigen Stelle DI an die Konsole ausgeben. Ich würde länger brauchen, um dies in einem Plunker einzurichten, als diese Schritte.
Mtpultz
Oh ich dachte du hast schon einen Plunker dafür :-) Danke.
Royi Namir
Für diejenigen, die wollen: plnkr.co/edit/2YMZk5mhP1B4jTgA37B8?p=preview
Royi Namir
1
Ich glaube nicht, dass Sie die AppConfig-Schnittstelle / Klasse exportieren müssen. Sie müssen es definitiv nicht verwenden, wenn Sie DI ausführen. Damit dies in einer Datei funktioniert, musste es eine Klasse anstelle einer Schnittstelle sein, aber das spielt keine Rolle. Tatsächlich schlägt der Styleguide vor, Klassen anstelle von Schnittstellen zu verwenden, da dies weniger Code bedeutet und Sie die Prüfung trotzdem mit ihnen eingeben können. In Bezug auf die Verwendung durch das InjectionToken über Generika ist dies etwas, das Sie einbeziehen möchten.
Mtpultz
1
Ich versuche, den API-Endpunkt mithilfe der Umgebungsvariablen von Azure und der JSON-Transformationsfunktionen dynamisch einzufügen, aber es sieht so aus, als würde diese Antwort nur den apiEndpoint aus der Umgebungsdatei abrufen. Wie würden Sie es aus der Konfiguration holen und exportieren?
Archibald
136

Wenn Sie verwenden gibt es noch eine andere Option:

Angular CLI bietet Umgebungsdateien in src/environments(Standarddateien sind environment.ts(dev) und environment.prod.ts(Produktion)).

Beachten Sie, dass Sie die Konfigurationsparameter in allen environment.*Dateien angeben müssen , z.

Umwelt.ts :

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

environment.prod.ts :

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

und verwenden Sie sie in Ihrem Dienst (die richtige Umgebungsdatei wird automatisch ausgewählt):

api.service.ts

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

@Injectable()
export class ApiService {     

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

// ...
}

Weitere Informationen zu Anwendungsumgebungen auf Github (Angular CLI Version 6) oder im offiziellen Angular-Handbuch (Version 7) .

Tilo
quelle
2
Es funktioniert einwandfrei. Aber während des Verschiebens des Builds wird es auch als Bundle geändert. Ich sollte die Konfiguration in meinem Serve ändern, nicht im Code, nachdem ich in die Produktion
gewechselt bin
39
Dies ist in gewisser Weise ein Anti-Muster in der normalen Softwareentwicklung. Die API-URL ist nur eine Konfiguration. Es sollte keine Neuerstellung erforderlich sein, um eine App für eine andere Umgebung neu zu konfigurieren. Es sollte einmal erstellt und mehrmals bereitgestellt werden (Pre-Prod, Staging, Prod usw.).
Matt Tester
3
@MattTester Dies ist eigentlich eine offizielle Angular-CLI-Geschichte. Wenn Sie eine bessere Antwort auf diese Frage haben, können Sie sie gerne posten!
bis
7
ist es nach ng build konfigurierbar?
NK
1
Oh ok, ich habe die Kommentare falsch verstanden. Ich würde zustimmen, dass dies zu einem Anti-Pattern führt. Ich dachte, es gibt eine Geschichte für dynamische Laufzeitkonfigurationen.
Jens Bodal
79

Es ist nicht ratsam, die environment.*.tsDateien für Ihre API-URL-Konfiguration zu verwenden. Es scheint, als ob Sie sollten, weil dies das Wort "Umwelt" erwähnt.

Dies ist eine Konfiguration zur Kompilierungszeit . Wenn Sie die API-URL ändern möchten, müssen Sie sie neu erstellen. Das möchten Sie nicht tun müssen ... fragen Sie einfach Ihre freundliche QS-Abteilung :)

Was Sie benötigen, ist die Laufzeitkonfiguration , dh die App lädt ihre Konfiguration beim Start.

Einige andere Antworten berühren dies, aber der Unterschied besteht darin, dass die Konfiguration geladen werden muss, sobald die App gestartet wird , damit sie von einem normalen Dienst verwendet werden kann, wann immer sie benötigt wird.

So implementieren Sie die Laufzeitkonfiguration:

  1. Fügen Sie dem /src/assets/Ordner eine JSON-Konfigurationsdatei hinzu (damit diese beim Erstellen kopiert wird).
  2. Erstellen Sie eine AppConfigService, um die Konfiguration zu laden und zu verteilen
  3. Laden Sie die Konfiguration mit einem APP_INITIALIZER

1. Fügen Sie die Konfigurationsdatei hinzu /src/assets

Sie können es einem anderen Ordner hinzufügen, müssen der CLI jedoch mitteilen, dass es sich um ein Asset in der handelt angular.json. Beginnen Sie mit dem Assets-Ordner:

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

2. Erstellen AppConfigService

Dies ist der Dienst, der injiziert wird, wenn Sie den Konfigurationswert benötigen:

@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. Laden Sie die Konfiguration mit einem APP_INITIALIZER

Damit die AppConfigServiceKonfiguration bei vollständig geladener Konfiguration sicher injiziert werden kann, muss die Konfiguration beim Start der App geladen werden. Wichtig ist, dass die Factory-Funktion für die Initialisierung a zurückgeben muss, Promisedamit Angular warten kann, bis die Auflösung abgeschlossen ist, bevor der Start abgeschlossen wird:

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

Jetzt können Sie es überall einspritzen, und die gesamte Konfiguration kann gelesen werden:

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

}

Ich kann es nicht stark genug sagen. Die Konfiguration Ihrer API-URLs als Konfiguration zur Kompilierungszeit ist ein Anti-Pattern . Verwenden Sie die Laufzeitkonfiguration.

Matt Tester
quelle
4
Lokale Datei oder ein anderer Dienst, Konfiguration zur Kompilierungszeit sollte nicht für eine API-URL verwendet werden. Stellen Sie sich vor, wenn Ihre App als Produkt verkauft wird (Käufer muss sie installieren), möchten Sie nicht, dass sie sie kompiliert usw. In beiden Fällen möchten Sie etwas, das vor 2 Jahren erstellt wurde, nicht neu kompilieren, nur weil Die API-URL wurde geändert. Das Risiko!!
Matt Tester
1
@Bloodhound Du kannst mehr als einen haben, APP_INITIALIZERaber ich glaube nicht, dass du sie leicht voneinander abhängig machen kannst. Es hört sich so an, als hätten Sie eine gute Frage zu stellen. Vielleicht können Sie hier darauf verlinken?
Matt Tester
2
@ MattTester - Wenn Angular diese Funktion jemals implementiert, würde dies unser Problem lösen: github.com/angular/angular/issues/23279#issuecomment-528417026
Mike Becatti
2
@CrhistianRamirez Aus Sicht der App: Die Konfiguration ist erst zur Laufzeit bekannt, und die statische Datei befindet sich außerhalb des Builds und kann zum Zeitpunkt der Bereitstellung auf verschiedene Arten festgelegt werden. Statische Datei ist in Ordnung für nicht sensible Konfiguration. Eine API oder ein anderer geschützter Endpunkt ist mit derselben Technik möglich, aber wie Sie sich authentifizieren, um sie geschützt zu machen, ist Ihre nächste Herausforderung.
Matt Tester
1
@DaleK Wenn Sie zwischen den Zeilen lesen, stellen Sie Web Deploy bereit. Wenn Sie eine Bereitstellungspipeline wie Azure DevOps verwenden, können Sie die Konfigurationsdatei als nächsten Schritt korrekt festlegen. Die Einstellung der Konfiguration liegt in der Verantwortung des Bereitstellungsprozesses / der Pipeline, die Werte in der Standardkonfigurationsdatei überschreiben kann. Hoffe das klärt sich.
Matt Tester
8

Hier ist meine Lösung, die von .json geladen wird, um Änderungen ohne Neuerstellung zu ermöglichen

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

und config.json

{
    "apiUrl": "http://localhost:3000/api"
}
PJM
quelle
1
Das Problem bei diesem Ansatz ist, dass config.json für die Welt offen ist. Wie würden Sie verhindern, dass jemand www.mywebsite.com/assetts/config.json eingibt?
Alberto L. Bonfiglio
1
@ AlbertoL.Bonfiglio Sie konfigurieren den Server so, dass kein Zugriff von außen auf die Datei config.json möglich ist (oder legen Sie ihn in einem Verzeichnis ab, das keinen öffentlichen Zugriff hat)
Alex Pandrea
Dies ist auch meine Lieblingslösung, aber immer noch besorgt über die Sicherheitsrisiken.
Viqas
5
Kannst du mir bitte helfen, es richtig zu machen? Wie ist es in eckigen Umgebungen riskanter als herkömmlich? Der vollständige Inhalt von environments.prod.tsafter ng build --prodwird sich irgendwann in einer .jsDatei befinden. Selbst wenn sie verschleiert sind, werden die Daten von environments.prod.tsim Klartext angezeigt. Und wie alle .js-Dateien wird es auf dem Endbenutzer-Computer verfügbar sein.
Igann
4
@ AlbertoL.Bonfiglio Da eine Angular-App von Natur aus eine Client-Anwendung ist und JavaScript zum Weitergeben von Daten und Konfiguration verwendet wird, sollte keine geheime Konfiguration verwendet werden. Alle geheimen Konfigurationsdefinitionen sollten sich hinter einer API-Schicht befinden, auf die der Browser oder die Browser-Tools eines Benutzers nicht zugreifen können. Werte wie der Basis-URI einer API sind für die Öffentlichkeit in Ordnung, da die API über eigene Anmeldeinformationen und Sicherheit verfügen sollte, die auf der Anmeldung eines Benutzers basieren (Inhaber-Token über https).
Tommy Elliott
4

Ich habe festgestellt, dass die Verwendung APP_INITIALIZERvon a dafür nicht in Situationen funktioniert, in denen andere Dienstanbieter die Injektion der Konfiguration benötigen. Sie können instanziiert werden, bevor sie APP_INITIALIZERausgeführt werden.

Ich habe andere Lösungen gesehen, die verwendet werden fetch, um eine config.json-Datei zu lesen und sie mithilfe eines Injektionstokens in einem Parameter bereitzustellen, platformBrowserDynamic()bevor das Root-Modul gebootet wird. Aber fetchin allen Browsern und insbesondere WebView - Browser für die mobilen Geräte nicht unterstützt I Ziel.

Das Folgende ist eine Lösung, die für mich sowohl für PWA- als auch für mobile Geräte (WebView) funktioniert. Hinweis: Ich habe bisher nur in Android getestet. Wenn ich von zu Hause aus arbeite, habe ich keinen Zugriff auf einen Mac zum Erstellen.

In 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();

Dieser Code:

  1. Startet eine asynchrone Anforderung für die config.jsonDatei.
  2. Wenn die Anforderung abgeschlossen ist, wird der JSON in ein Javascript-Objekt analysiert
  3. Liefert den Wert mithilfe des APP_CONFIGInjection-Tokens vor dem Bootstrapping.
  4. Und schließlich wird das Root-Modul gebootet.

APP_CONFIGkann dann in weitere Anbieter in injiziert werden app-module.tsund es wird definiert. Zum Beispiel kann ich den FIREBASE_OPTIONSInjektionstoken wie @angular/firefolgt initialisieren :

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

Ich finde das Ganze überraschend schwierig (und hackig) für eine sehr häufige Anforderung. Hoffentlich gibt es in naher Zukunft einen besseren Weg, beispielsweise die Unterstützung für asynchrone Anbieterfabriken.

Der Rest des Codes der Vollständigkeit halber ...

In 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');

und in app/lib/config/config.tsdefiniere ich die Schnittstelle für meine JSON-Konfigurationsdatei:

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

Die Konfiguration ist gespeichert in assets/config/config.json:

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

Hinweis: Ich verwende eine Azure DevOps-Aufgabe, um Build.BuildNumber einzufügen und andere Bereitstellungsumgebungen während der Bereitstellung durch andere Einstellungen zu ersetzen.

Glenn
quelle
3

Konfigurationsdatei des armen Mannes:

Fügen Sie Ihrer index.html als erste Zeile im Body-Tag hinzu:

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

Assets hinzufügen / config.js:

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

Fügen Sie config.ts hinzu:

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

export interface AppConfig {
    apiBaseUrl: string
}
Matthias
quelle
2

Hier sind meine beiden Lösungen dafür

1. In JSON-Dateien speichern

Erstellen Sie einfach eine JSON-Datei und rufen Sie Ihre Komponente nach $http.get()Methode auf. Wenn ich das sehr niedrig brauchte, dann ist es gut und schnell.

2. Speichern Sie mithilfe von Datendiensten

Wenn Sie in allen Komponenten speichern und verwenden möchten oder eine große Nutzung haben möchten, ist es besser, den Datendienst zu verwenden. So was :

  1. Erstellen Sie einfach einen statischen Ordner innerhalb des src/appOrdners.

  2. Erstellen Sie eine Datei mit dem Namen as fuels.tsin einem statischen Ordner. Sie können hier auch andere statische Dateien speichern. Lassen Sie Ihre Daten so definieren. Angenommen, Sie haben Kraftstoffdaten.

__ __

export const Fuels {

   Fuel: [
    { "id": 1, "type": "A" },
    { "id": 2, "type": "B" },
    { "id": 3, "type": "C" },
    { "id": 4, "type": "D" },
   ];
   }
  1. Erstellen Sie einen Dateinamen static.services.ts

__ __

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

@Injectable()
export class StaticService {

  constructor() { }

  getFuelData(): Fuels[] {
    return Fuels;
  }
 }`
  1. Jetzt können Sie dies für jedes Modul zur Verfügung stellen

Importieren Sie einfach die Datei app.module.ts wie folgt und wechseln Sie den Anbieter

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

providers: [StaticService]

Verwenden Sie dies nun wie StaticServicein jedem Modul.

Das ist alles.

amku91
quelle
Gute Lösung, da Sie nicht neu kompilieren müssen. Die Umgebung ist wie eine harte Codierung im Code. Böse. +1
Terrance00