Ustawienia aplikacji - sposób kątowy?

90

Chcę dodać App Settingssekcję do mojej aplikacji, w której będzie zawierała niektóre stałe i wstępnie zdefiniowane wartości.

Przeczytałem już tę odpowiedź, która używa OpaqueTokenAle jest przestarzała w Angular. Ten artykuł wyjaśnia różnice, ale nie zawiera pełnego przykładu, a moje próby zakończyły się niepowodzeniem.

Oto, czego próbowałem (nie wiem, czy to właściwy sposób):

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

A to jest komponent, w którym chcę użyć tych stałych:

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


}

Ale to nie działa i otrzymuję błędy.

Pytanie:

Jak mogę używać wartości „app.settings” w sposób Angular?

plunker

NB Jasne, że mogę stworzyć usługę Injectable i umieścić ją u dostawcy NgModule, ale jak powiedziałem, chcę to zrobić InjectionTokenw sposób Angular.

Royi Namir
źródło
Możesz sprawdzić moją odpowiedź tutaj na podstawie aktualnej oficjalnej dokumentacji
JavierFuentes
@javier no. Twój link ma problem, jeśli dwóch dostawców podało tę samą nazwę, więc masz teraz problem. Entring opaquetoken
Royi Namir
wiesz, że [OpaqueToken jest przestarzały]. ( angular.io/api/core/OpaqueToken ) Ten artykuł mówi o tym, jak zapobiegać kolizjom nazw w dostawcach Angular
JavierFuentes
Tak, wiem, ale link do artykułu jest błędny.
Royi Namir
2
poniżej może być link może być pomocny dla każdego, kto lubi używać nowej architektury schematu konfiguracji kątowej devblogs.microsoft.com/premier-developer/ ...
M_Farahmand

Odpowiedzi:

58

Dowiedziałem się, jak to zrobić za pomocą InjectionTokens (patrz przykład poniżej), a jeśli twój projekt został zbudowany przy użyciu Angular CLI, możesz użyć plików środowiska znalezionych w /environmentsstatycznych, application wide settingstakich jak punkt końcowy API, ale w zależności od wymagań twojego projektu najprawdopodobniej skończysz używając obu, ponieważ pliki środowiskowe są po prostu literałami obiektowymi, podczas gdy konfiguracja do wstrzykiwania używająca InjectionToken's może używać zmiennych środowiskowych, a ponieważ jest to klasa, może mieć logikę do konfiguracji w oparciu o inne czynniki w aplikacji, takie jak początkowe dane żądania http, subdomena itp.

Przykład tokenów wtrysku

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

Teraz możesz po prostu DI to w dowolnym komponencie, usłudze itp .:

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

  // ...
}

Następnie możesz również wpisać sprawdź konfigurację za pomocą wyeksportowanego pliku AppConfig.

mtpultz
źródło
Nie, ale możesz dosłownie skopiować i wkleić pierwszą część do pliku, zaimportować ją do pliku app.module.ts, a DI to w dowolnym miejscu i wyprowadzić na konsolę. Skonfigurowanie tego w plunkerze zajęłoby mi więcej czasu, niż wykonanie tych kroków.
mtpultz
Och, myślałem, że masz już do tego plunkra :-) Dziękuję.
Royi Namir
Dla tych, którzy chcą: plnkr.co/edit/2YMZk5mhP1B4jTgA37B8?p=preview
Royi Namir
1
Nie sądzę, że musisz eksportować interfejs / klasę AppConfig. Zdecydowanie nie musisz go używać podczas wykonywania DI. Aby to zadziałało w jednym pliku, musiała to być klasa zamiast interfejsu, ale to nie ma znaczenia. W rzeczywistości przewodnik po stylach sugeruje używanie klas zamiast interfejsów, ponieważ oznacza to mniej kodu i nadal możesz używać ich do sprawdzania typu. Jeśli chodzi o jego użycie przez InjectionToken za pośrednictwem generycznych, jest to coś, co chcesz uwzględnić.
mtpultz
1
Próbuję dynamicznie wstrzyknąć punkt końcowy interfejsu API przy użyciu zmiennych środowiskowych platformy Azure i funkcji transformacji JSON, ale wygląda na to, że ta odpowiedź po prostu pobiera apiEndpoint z pliku środowiska. Jak można go pobrać z konfiguracji i wyeksportować?
Archibald
138

Jeśli używasz istnieje jeszcze jedna opcja:

Angular CLI udostępnia pliki środowiskowe w src/environments(domyślne to environment.ts(dev) i environment.prod.ts(production)).

Pamiętaj, że musisz podać parametry konfiguracyjne we wszystkich environment.*plikach, np.

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

i używaj ich w swojej usłudze (właściwy plik środowiska wybierany jest automatycznie):

api.service.ts

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

@Injectable()
export class ApiService {     

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

// ...
}

Przeczytaj więcej o środowiskach aplikacji na Github (wersja 6 Angular CLI) lub w oficjalnym przewodniku po Angular (wersja 7) .

tilo
źródło
3
działa dobrze, ale podczas przenoszenia kompilacji jest również zmieniany jako pakiet. Powinienem zmienić konfigurację w moim serwisie, a nie w kodzie po przejściu do produkcji
kamalav
44
Jest to w pewnym stopniu anty-wzorzec w normalnym tworzeniu oprogramowania; URL interfejsu API to tylko konfiguracja. Ponowna kompilacja nie powinna wymagać ponownej konfiguracji aplikacji dla innego środowiska. Powinien być tworzony raz, wdrażany wiele razy (pre-prod, staging, prod itp.).
Matt Tester,
3
@MattTester To jest obecnie oficjalna historia Angular-CLI. Jeśli masz lepszą odpowiedź na to pytanie: napisz ją!
tilo
7
czy można go konfigurować po kompilacji ng?
NK
1
OK, źle odczytałem komentarze. Zgadzam się, że to nadaje się do anty-wzorca, myślałem, że istnieje historia dla konfiguracji dynamicznych w czasie wykonywania.
Jens Bodal
89

Nie zaleca się używania environment.*.tsplików do konfiguracji adresu URL interfejsu API. Wydaje się, że powinieneś, ponieważ w tym słowie pojawia się słowo „środowisko”.

Używanie tego jest w rzeczywistości konfiguracją w czasie kompilacji . Jeśli chcesz zmienić adres URL interfejsu API, musisz ponownie zbudować. To jest coś, czego nie chcesz robić ... po prostu zapytaj swój przyjazny dział kontroli jakości :)

Potrzebujesz konfiguracji środowiska wykonawczego , czyli aplikacja ładuje swoją konfigurację podczas uruchamiania.

Niektóre inne odpowiedzi dotykają tego, ale różnica polega na tym, że konfiguracja musi zostać załadowana zaraz po uruchomieniu aplikacji , aby mogła być używana przez normalną usługę, kiedy tylko tego potrzebuje.

Aby zaimplementować konfigurację środowiska wykonawczego:

  1. Dodaj plik konfiguracyjny JSON do /src/assets/folderu (tak, aby był kopiowany podczas kompilacji)
  2. Utwórz plik, AppConfigServiceaby załadować i rozpowszechnić plik config
  3. Załaduj konfigurację za pomocą pliku APP_INITIALIZER

1. Dodaj plik konfiguracyjny do /src/assets

Możesz dodać go do innego folderu, ale musisz powiedzieć CLI, że jest to zasób w angular.json. Zacznij od folderu zasobów:

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

2. Utwórz AppConfigService

To jest usługa, która zostanie wstrzyknięta, gdy będziesz potrzebować wartości konfiguracyjnej:

@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. Załaduj konfigurację za pomocą pliku APP_INITIALIZER

Aby umożliwić AppConfigServicebezpieczne wstrzyknięcie, przy w pełni załadowanej konfiguracji, musimy załadować konfigurację podczas uruchamiania aplikacji. Co ważne, funkcja fabryki inicjalizacji musi zwrócić znak Promise, aby Angular wiedział, że musi poczekać, aż zakończy proces rozwiązywania przed zakończeniem uruchamiania:

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

Teraz możesz wstrzyknąć go gdziekolwiek chcesz, a cała konfiguracja będzie gotowa do przeczytania:

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

}

Nie mogę powiedzieć tego wystarczająco mocno, konfigurowanie adresów URL API jako konfiguracji w czasie kompilacji jest anty-wzorcem . Użyj konfiguracji środowiska wykonawczego.

Matt Tester
źródło
4
Plik lokalny lub inna usługa, konfiguracja w czasie kompilacji nie powinna być używana dla adresu URL interfejsu API. Wyobraź sobie, że Twoja aplikacja jest sprzedawana jako produkt (nabywca do zainstalowania), nie chcesz, aby ją kompilowali itp. Tak czy inaczej, nie chcesz ponownie kompilować czegoś, co zostało utworzone 2 lata temu tylko dlatego, że URL interfejsu API został zmieniony. Ryzyko!!
Matt Tester
1
@Bloodhound Możesz mieć więcej niż jednego, APP_INITIALIZERale nie sądzę, aby łatwo było ich uzależnić od siebie. Wygląda na to, że masz dobre pytanie do zadania, więc może łącze do niego tutaj?
Matt Tester
2
@MattTester - Gdyby Angular kiedykolwiek wdrożył tę funkcję, rozwiązałoby to nasz problem: github.com/angular/angular/issues/23279#issuecomment-528417026
Mike Becatti
2
@CrhistianRamirez Z punktu widzenia aplikacji: konfiguracja nie jest znana do czasu uruchomienia, a plik statyczny znajduje się poza kompilacją i można go ustawić na wiele sposobów podczas wdrażania. Plik statyczny jest odpowiedni dla konfiguracji niewrażliwych. API lub inny chroniony punkt końcowy jest możliwy przy użyciu tej samej techniki, ale następnym wyzwaniem jest sposób uwierzytelnienia, aby był chroniony.
Matt Tester
1
@DaleK Czytając między wierszami, wdrażasz za pomocą Web Deploy. Jeśli korzystasz z potoku wdrażania, takiego jak Azure DevOps, możesz poprawnie ustawić plik konfiguracyjny w następnym kroku. Za ustawienie konfiguracji odpowiada proces / potok wdrażania, który może przesłonić wartości w domyślnym pliku konfiguracyjnym. Mam nadzieję, że to wyjaśnia.
Matt Tester
8

Oto moje rozwiązanie, ładuje z .json, aby umożliwić zmiany bez przebudowywania

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

i config.json

{
    "apiUrl": "http://localhost:3000/api"
}
PJM
źródło
1
Problem z tym podejściem polega na tym, że config.json jest otwarty na świat. Jak uniemożliwić komuś wpisywanie adresu www.mywebsite.com/assetts/config.json?
Alberto L. Bonfiglio
1
@ AlbertoL.Bonfiglio konfigurujesz serwer tak, aby nie zezwalał na dostęp z zewnątrz do pliku config.json (lub umieszczasz go w katalogu, do którego nie ma dostępu publicznego)
Alex Pandrea
To także moje ulubione rozwiązanie, ale nadal martwię się o zagrożenia bezpieczeństwa.
ViqMontana
7
Proszę, czy możesz mi pomóc, żeby to naprawić? Dlaczego jest to bardziej ryzykowne niż tradycyjne w przypadku środowisk kątowych? Pełna zawartość environments.prod.tsafter ng build --prodbędzie .jsw pewnym momencie w jakimś pliku. Nawet jeśli zostaną zaciemnione, dane z environments.prod.tsbędą w postaci zwykłego tekstu. I jak wszystkie pliki .js, będzie on dostępny na komputerze użytkownika końcowego.
igann
5
@ AlbertoL.Bonfiglio Ponieważ aplikacja Angular jest z natury aplikacją kliencką, a do przekazywania danych i konfiguracji używany jest JavaScript, nie powinno się w niej stosować żadnej tajnej konfiguracji; wszystkie tajne definicje konfiguracji powinny znajdować się za warstwą API, do której przeglądarka użytkownika lub narzędzia przeglądarki nie mają do niej dostępu. Wartości, takie jak podstawowy identyfikator URI interfejsu API, są w porządku dla publiczności, ponieważ interfejs API powinien mieć własne poświadczenia i zabezpieczenia oparte na logowaniu użytkownika (token okaziciela przez https).
Tommy Elliott
5

Plik konfiguracyjny biedaka:

Dodaj do swojego index.html jako pierwszą linię w tagu body:

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

Dodaj zasoby / config.js:

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

Dodaj config.ts:

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

export interface AppConfig {
    apiBaseUrl: string
}
Matthias
źródło
Poważnie, +1 za sprowadzenie roztworu do najbardziej podstawowych składników i zachowanie spójności typu.
Luminous
5

Zauważyłem, że użycie APP_INITIALIZERfor this nie działa w sytuacjach, gdy inni dostawcy usług wymagają wstrzyknięcia konfiguracji. Można je utworzyć wcześniejAPP_INITIALIZER uruchomieniem.

Widziałem inne rozwiązania, które używają fetchdo odczytywania pliku config.json i dostarczania go przy użyciu tokenu iniekcji w parametrze platformBrowserDynamic()przed załadowaniem modułu głównego. Alefetch nie jest obsługiwany we wszystkich przeglądarkach, aw szczególności w przeglądarkach WebView na docelowych urządzeniach mobilnych.

Poniżej przedstawiam rozwiązanie, które działa u mnie zarówno na PWA, jak i na urządzeniach mobilnych (WebView). Uwaga: do tej pory testowałem tylko w systemie Android; praca w domu oznacza, że ​​nie mam dostępu do komputera Mac do tworzenia.

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

Ten kod:

  1. uruchamia żądanie asynchroniczne dla config.jsonpliku.
  2. Po zakończeniu żądania analizuje JSON w obiekt JavaScript
  3. dostarcza wartość przy użyciu APP_CONFIGtokenu wstrzyknięcia przed załadowaniem.
  4. I w końcu ładuje moduł główny.

APP_CONFIGmożna następnie wstrzyknąć do dowolnych dodatkowych dostawców w app-module.tsi zostanie zdefiniowany. Na przykład mogę zainicjować FIREBASE_OPTIONStoken wstrzyknięcia @angular/fireza pomocą:

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

Uważam, że cała ta sprawa jest zaskakująco trudna (i zepsuta) dla bardzo powszechnego wymagania. Miejmy nadzieję, że w niedalekiej przyszłości pojawi się lepszy sposób, na przykład obsługa fabryk dostawców asynchronicznych.

Reszta kodu dla kompletności ...

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

aw części app/lib/config/config.tsdefiniuję interfejs dla mojego pliku konfiguracyjnego JSON:

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

Konfiguracja jest przechowywana w assets/config/config.json:

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

Uwaga: używam zadania Azure DevOps, aby wstawić Build.BuildNumber i zastąpić inne ustawienia dla różnych środowisk wdrażania podczas wdrażania.

Glenn
źródło
2

Oto moje dwa rozwiązania tego problemu

1. Przechowuj w plikach json

Po prostu utwórz plik json i wejdź do komponentu przez $http.get() metodą. Gdybym potrzebował tego bardzo nisko, to dobrze i szybko.

2. Przechowywać za pomocą usług danych

Jeśli chcesz przechowywać i używać we wszystkich komponentach lub mieć duże użycie, lepiej użyć usługi danych. Lubię to :

  1. Po prostu utwórz folder statyczny w src/appfolderze.

  2. Utwórz plik o nazwie as fuels.tsw folderze statycznym. Możesz tutaj również przechowywać inne pliki statyczne. Zdefiniujmy w ten sposób swoje dane. Zakładając, że masz dane dotyczące paliw.

__

export const Fuels {

   Fuel: [
    { "id": 1, "type": "A" },
    { "id": 2, "type": "B" },
    { "id": 3, "type": "C" },
    { "id": 4, "type": "D" },
   ];
   }
  1. Utwórz nazwę pliku static.services.ts

__

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

@Injectable()
export class StaticService {

  constructor() { }

  getFuelData(): Fuels[] {
    return Fuels;
  }
 }`
  1. Teraz możesz udostępnić to dla każdego modułu

po prostu zaimportuj do pliku app.module.ts w ten sposób i zmień dostawców

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

providers: [StaticService]

Teraz użyj tego jako StaticService w każdym module.

To wszystko.

amku91
źródło
Dobre rozwiązanie, ponieważ nie musisz ponownie kompilować. Środowisko przypomina zakodowanie go na stałe w kodzie. Paskudny. +1
Terrance00,
0

Uważam, że to Angular How-to: Editable Config Files z blogów Microsoft Dev jest najlepszym rozwiązaniem. Możesz skonfigurować ustawienia kompilacji deweloperskiej lub ustawienia kompilacji prod.

Melf
źródło
0

Mieliśmy ten problem wiele lat temu, zanim dołączyłem do programu i opracowałem rozwiązanie wykorzystujące lokalną pamięć masową do przechowywania informacji o użytkowniku i środowisku. Angular 1.0 dnia, aby być dokładnym. Wcześniej dynamicznie tworzyliśmy plik js w czasie wykonywania, który następnie umieszczałby wygenerowane adresy URL interfejsu API w zmiennej globalnej. W dzisiejszych czasach jesteśmy nieco bardziej nastawieni na OOP i do niczego nie używamy lokalnej pamięci.

Stworzyłem lepsze rozwiązanie zarówno do określania środowiska, jak i tworzenia adresu URL interfejsu API.

Czym się to różni?

Aplikacja nie zostanie załadowana, dopóki nie zostanie załadowany plik config.json. Wykorzystuje funkcje fabryczne do tworzenia wyższego stopnia SOC. Mógłbym zawrzeć to w usłudze, ale nigdy nie widziałem żadnego powodu, dla którego jedynym podobieństwem między różnymi sekcjami pliku jest to, że istnieją one razem w pliku. Posiadanie funkcji fabrycznej pozwala mi przekazać tę funkcję bezpośrednio do modułu, jeśli jest w stanie zaakceptować funkcję. Wreszcie, mam łatwiejsze konfigurowanie wtrysków, gdy dostępne są funkcje fabryczne.

Wady?

Nie masz szczęścia używając tej konfiguracji (i większości innych odpowiedzi), jeśli moduł, który chcesz skonfigurować, nie pozwala na przekazanie funkcji fabrycznej do forRoot () lub forChild () i nie ma innego sposobu, aby skonfigurować pakiet przy użyciu funkcji fabrycznej.

Instrukcje

  1. Używając funkcji fetch do pobrania pliku json, przechowuję obiekt w oknie i wywołuję zdarzenie niestandardowe. - pamiętaj, aby zainstalować whatwg-fetch i dodać go do swoich polyfills.ts w celu zapewnienia zgodności z IE
  2. Niech detektor zdarzeń nasłuchuje zdarzenia niestandardowego.
  3. Detektor zdarzeń odbiera zdarzenie, pobiera obiekt z okna, aby przekazać go do obserwowalnego, i czyści to, co zostało zapisane w oknie.
  4. Bootstrap Angular

- Tutaj moje rozwiązanie zaczyna się naprawdę różnić -

  1. Utwórz plik eksportując interfejs, którego struktura reprezentuje twój config.json - to naprawdę pomaga w spójności typu, a następna sekcja kodu wymaga typu i nie określaj {}lub anykiedy wiesz, że możesz określić coś bardziej konkretnego
  2. Utwórz BehaviorSubject, do którego przekażesz przeanalizowany plik JSON w kroku 3.
  3. Użyj funkcji fabrycznych, aby odwołać się do różnych sekcji konfiguracji, aby zachować SOC
  4. Utwórz InjectionTokens dla dostawców potrzebujących wyników funkcji fabrycznych

- i / lub -

  1. Przekaż funkcje fabryczne bezpośrednio do modułów, które mogą akceptować funkcję w jej metodach forRoot () lub forChild ().

- main.ts

Sprawdzam, czy okno [„środowisko”] nie jest zapełnione przed utworzeniem nasłuchiwania zdarzeń, aby umożliwić rozwiązanie, w którym okno [„środowisko”] jest wypełniane w inny sposób, zanim kod w main.ts zostanie kiedykolwiek wykonany.

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

--- środowisko-resolvers.ts

Przypisuję wartość do BehaviorSubject przy użyciu window [„environment”] do redundancji. Możesz wymyślić rozwiązanie, w którym Twoja konfiguracja jest już wstępnie załadowana, a okno [„środowisko”] jest już wypełnione do czasu uruchomienia dowolnego kodu aplikacji Angulara, w tym kodu z 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 - uproszczony dla łatwiejszego zrozumienia

Śmieszny fakt! Starsze wersje NGXLoggera wymagały przekazania obiektu do LoggerModule.forRoot (). W rzeczywistości LoggerModule nadal to robi! NGXLogger uprzejmie udostępnia LoggerConfig, który możesz zmienić, umożliwiając użycie funkcji fabrycznej do konfiguracji.

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

Uzupełnienie

Jak rozwiązałem problem tworzenia moich adresów URL API?

Chciałem być w stanie zrozumieć, co robił każdy adres URL za pomocą komentarza i chciałem sprawdzać typy, ponieważ jest to największa siła TypeScript w porównaniu z javascript (IMO). Chciałem również stworzyć środowisko dla innych programistów, aby dodać nowe punkty końcowe i interfejs API, który był tak płynny, jak to tylko możliwe.

Utworzyłem klasę, która przyjmuje środowisko (programowanie, testowanie, etap, produkcja, „” itd.) I przekazałem tę wartość do serii klas [1-N], których zadaniem jest utworzenie podstawowego adresu URL dla każdej kolekcji API . Każda kolekcja ApiCollection jest odpowiedzialna za tworzenie podstawowego adresu URL dla każdego zbioru interfejsów API. Mogą to być nasze własne interfejsy API, interfejsy API dostawcy, a nawet łącze zewnętrzne. Ta klasa przekaże utworzony podstawowy adres URL do każdego kolejnego zawartego w niej interfejsu API. Przeczytaj poniższy kod, aby zobaczyć przykład gołej kości. Po skonfigurowaniu inny programista może bardzo łatwo dodać kolejny punkt końcowy do klasy API bez konieczności dotykania czegokolwiek innego.

TLDR; podstawowe zasady OOP i leniwe metody pobierające do optymalizacji pamięci

@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/");
  }
}
Świetlny
źródło