Chcę dodać App Settings
sekcję 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 OpaqueToken
Ale 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?
NB Jasne, że mogę stworzyć usługę Injectable i umieścić ją u dostawcy NgModule, ale jak powiedziałem, chcę to zrobić InjectionToken
w sposób Angular.
javascript
angular
Royi Namir
źródło
źródło
Odpowiedzi:
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/environments
statycznych,application wide settings
takich 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ącaInjectionToken
'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.
źródło
Jeśli używasz angular-cliistnieje jeszcze jedna opcja:
Angular CLI udostępnia pliki środowiskowe w
src/environments
(domyślne toenvironment.ts
(dev) ienvironment.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) .
źródło
Nie zaleca się używania
environment.*.ts
plikó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:
/src/assets/
folderu (tak, aby był kopiowany podczas kompilacji)AppConfigService
aby załadować i rozpowszechnić plik configAPP_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ć
AppConfigService
bezpieczne 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ć znakPromise
, 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.
źródło
APP_INITIALIZER
ale 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?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" }
źródło
environments.prod.ts
afterng build --prod
będzie.js
w pewnym momencie w jakimś pliku. Nawet jeśli zostaną zaciemnione, dane zenvironments.prod.ts
będą w postaci zwykłego tekstu. I jak wszystkie pliki .js, będzie on dostępny na komputerze użytkownika końcowego.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 }
źródło
Zauważyłem, że użycie
APP_INITIALIZER
for 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ą
fetch
do odczytywania pliku config.json i dostarczania go przy użyciu tokenu iniekcji w parametrzeplatformBrowserDynamic()
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:
config.json
pliku.APP_CONFIG
tokenu wstrzyknięcia przed załadowaniem.APP_CONFIG
można następnie wstrzyknąć do dowolnych dodatkowych dostawców wapp-module.ts
i zostanie zdefiniowany. Na przykład mogę zainicjowaćFIREBASE_OPTIONS
token wstrzyknięcia@angular/fire
za 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.ts
definiuję 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.
źródło
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 :
Po prostu utwórz folder statyczny w
src/app
folderze.Utwórz plik o nazwie as
fuels.ts
w 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" }, ]; }
__
import { Injectable } from "@angular/core"; import { Fuels } from "./static/fuels"; @Injectable() export class StaticService { constructor() { } getFuelData(): Fuels[] { return Fuels; } }`
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.
źródło
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.
źródło
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
- Tutaj moje rozwiązanie zaczyna się naprawdę różnić -
{}
lubany
kiedy wiesz, że możesz określić coś bardziej konkretnego- i / lub -
- 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/"); } }
źródło