App.settings - o jeito Angular?

87

Quero adicionar uma App Settingsseção em meu aplicativo onde conterá alguns constantes e valores predefinidos.

Já li esta resposta que usa OpaqueTokenMas está obsoleto no Angular. Este artigo explica as diferenças, mas não fornece um exemplo completo, e minhas tentativas não tiveram êxito.

Aqui está o que tentei (não sei se é o caminho certo):

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

E este é o componente onde desejo usar essas 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 {


}

Mas não funciona e recebo erros.

Questão:

Como posso consumir valores "app.settings" da maneira Angular?

êmbolo

Obs. Claro que posso criar o serviço Injetável e colocar no provedor do NgModule, mas como disse quero fazer com InjectionToken, do jeito Angular.

Royi Namir
fonte
Você pode verificar minha resposta aqui com base na documentação oficial atual
JavierFuentes
@javier no. Seu link terá um problema se dois provedores fornecerem o mesmo nome, então agora você tem um problema. Entring opaquetoken
Royi Namir
você sabe [OpaqueToken está obsoleto]. ( angular.io/api/core/OpaqueToken ) Este artigo fala sobre como evitar colisões de nomes em provedores angulares
JavierFuentes
Sim, eu sei, mas ainda assim o artigo vinculado está errado.
Royi Namir
2
pode estar abaixo do link pode ser útil para todos que gostam de usar a nova arquitetura do esquema de configuração angular devblogs.microsoft.com/premier-developer/…
M_Farahmand

Respostas:

56

Eu descobri como fazer isso com o InjectionTokens (veja o exemplo abaixo), e se o seu projeto foi construído usando o, Angular CLIvocê pode usar os arquivos de ambiente encontrados em /environmentsestático application wide settingscomo um endpoint de API, mas dependendo dos requisitos do seu projeto, você provavelmente acabará usando ambos, já que os arquivos de ambiente são apenas literais de objeto, enquanto uma configuração injetável usando InjectionToken's pode usar as variáveis ​​de ambiente e, uma vez que é uma classe, pode ter lógica aplicada para configurá-la com base em outros fatores no aplicativo, como dados de solicitação inicial de http, subdomínio etc.

Exemplo de tokens de injeção

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

Agora você pode simplesmente INICIá-lo em qualquer componente, serviço, 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);
  }

  // ...
}

Você também pode digitar verificar a configuração usando o AppConfig exportado.

mtpultz
fonte
Não, mas você pode literalmente copiar e colar a primeira parte em um arquivo, importá-la em seu arquivo app.module.ts e identificá-la em qualquer lugar e enviá-la para o console. Eu levaria mais tempo para configurar isso em um êmbolo do que para fazer essas etapas.
mtpultz,
Oh, eu pensei que você já tem um êmbolo para isso :-) Obrigado.
Royi Namir,
Para quem deseja: plnkr.co/edit/2YMZk5mhP1B4jTgA37B8?p=preview
Royi Namir
1
Não acredito que você precise exportar a interface / classe AppConfig. Você definitivamente não precisa usá-lo ao fazer DI. Para fazer isso funcionar em um arquivo, ela tinha que ser uma classe em vez de uma interface, mas isso não importa. Na verdade, o guia de estilo sugere o uso de classes em vez de interfaces, pois isso significa menos código e você ainda pode digitar check usando-os. Com relação ao seu uso pelo InjectionToken via genéricos, isso é algo que você deseja incluir.
mtpultz
1
Estou tentando injetar o ponto de extremidade da API dinamicamente usando as variáveis ​​de ambiente do Azure e os recursos de transformação JSON, mas parece que esta resposta apenas obtém o apiEndpoint do arquivo de ambiente. Como você o obteria da configuração e o exportaria?
Archibald
138

Se você estiver usando , há ainda outra opção:

Angular CLI fornece arquivos de ambiente em src/environments(os padrões são environment.ts(dev) eenvironment.prod.ts (produção)).

Observe que você precisa fornecer os parâmetros de configuração em todos os environment.*arquivos, por exemplo,

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

e usá-los em seu serviço (o arquivo de ambiente correto é escolhido automaticamente):

api.service.ts

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

@Injectable()
export class ApiService {     

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

// ...
}

Leia mais sobre ambientes de aplicativos no Github (Angular CLI versão 6) ou no guia oficial Angular (versão 7) .

tilo
fonte
2
está funcionando bem. Mas ao mover a compilação, ele também é alterado como pacote. Devo alterar a configuração em meu serviço, não no código, após passar para a produção
kamalav
43
Isso é um pouco como um antipadrão no desenvolvimento de software normal; o URL da API é apenas configuração. Não deve ser necessário reconstruir para reconfigurar um aplicativo para um ambiente diferente. Deve ser compilado uma vez, implantado várias vezes (pré-produção, preparação, produção, etc).
Matt Tester
3
@MattTester Na verdade, esta é a história oficial da Angular-CLI. Se você tiver uma resposta melhor para esta pergunta: fique à vontade para postá-la!
tilo
7
é configurável após a construção do ng?
NK
1
Oh ok, eu interpretei mal os comentários. Eu concordo que isso leva a um antipadrão, pensei que havia uma história para configurações dinâmicas de tempo de execução.
Jens Bodal
83

Não é aconselhável usar os environment.*.tsarquivos para a configuração do URL da API. Parece que você deveria, porque isso menciona a palavra "ambiente".

Usar isso é na verdade uma configuração em tempo de compilação . Se quiser alterar o URL da API, você precisará reconstruí-lo. Isso é algo que você não quer ter que fazer ... pergunte ao seu amigável departamento de controle de qualidade :)

O que você precisa é uma configuração de tempo de execução , ou seja, o aplicativo carrega sua configuração quando é inicializado.

Algumas outras respostas tocam nisso, mas a diferença é que a configuração precisa ser carregada assim que o aplicativo iniciar , para que possa ser usada por um serviço normal sempre que precisar.

Para implementar a configuração do tempo de execução:

  1. Adicione um arquivo de configuração JSON à /src/assets/pasta (para que seja copiado na construção)
  2. Crie um AppConfigServicepara carregar e distribuir a configuração
  3. Carregue a configuração usando um APP_INITIALIZER

1. Adicionar arquivo de configuração a /src/assets

Você pode adicioná-lo a outra pasta, mas precisa informar a CLI que é um ativo no angular.json. Comece usando a pasta de ativos:

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

2. Crie AppConfigService

Este é o serviço que será injetado sempre que você precisar do valor de configuração:

@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. Carregue a configuração usando um APP_INITIALIZER

Para permitir que o AppConfigServiceseja injetado com segurança, com a configuração totalmente carregada, precisamos carregar a configuração no momento da inicialização do aplicativo. É importante ressaltar que a função de fábrica de inicialização precisa retornar um Promisepara que o Angular saiba que deve esperar até que termine de resolver antes de terminar a inicialização:

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

Agora você pode injetá-lo onde precisar e toda a configuração estará pronta para ler:

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

}

Eu não posso dizer com força suficiente, configurar seus urls de API como configuração em tempo de compilação é um antipadrão . Use a configuração de tempo de execução.

Matt Tester
fonte
4
Arquivo local ou serviço diferente, configuração de tempo de compilação não deve ser usado para um URL de API. Imagine se seu aplicativo for vendido como um produto (comprador para instalar), você não quer que eles o compilem etc. De qualquer forma, você não quer recompilar algo que foi construído há 2 anos, só porque o URL da API mudou. O risco!!
Matt Tester
1
@Bloodhound Você pode ter mais de um, APP_INITIALIZERmas não acho que possa facilmente torná-los dependentes uns dos outros. Parece que você tem uma boa pergunta a fazer, então, talvez um link para ela aqui?
Matt Tester
2
@MattTester - Se o Angular algum dia implementar esse recurso, ele resolverá nosso problema: github.com/angular/angular/issues/23279#issuecomment-528417026
Mike Becatti
2
@CrhistianRamirez É do ponto de vista do aplicativo: a configuração não é conhecida até o tempo de execução e o arquivo estático está fora da construção e pode ser definido de várias maneiras no momento da implantação. Arquivo estático é adequado para configuração não sensível. Uma API ou algum outro ponto de extremidade protegido é possível com a mesma técnica, mas como autenticar para torná-lo protegido é seu próximo desafio.
Matt Tester
1
@DaleK Lendo nas entrelinhas, você está implantando usando o Web Deploy. Se você estiver usando um pipeline de implantação, como o Azure DevOps, será possível definir o arquivo de configuração corretamente como a próxima etapa. A configuração do config é de responsabilidade do processo / pipeline de implantação, que pode substituir os valores no arquivo de configuração padrão. Espero que isso esclareça.
Matt Tester
8

Esta é minha solução, carrega de .json para permitir alterações sem reconstruir

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

e config.json

{
    "apiUrl": "http://localhost:3000/api"
}
PJM
fonte
1
O problema com essa abordagem é que config.json está aberto para o mundo. Como você impediria que alguém digite www.mywebsite.com/assetts/config.json?
Alberto L. Bonfiglio
1
@ AlbertoL.Bonfiglio você configura o servidor para não permitir o acesso de fora ao arquivo config.json (ou colocá-lo em um diretório que não tenha acesso público)
Alex Pandrea
Esta é a minha solução favorita também, mas ainda estou preocupada com os riscos de segurança.
Viqas
7
Por favor, você pode me ajudar a acertar? Por que é mais arriscado do que o tradicional para ambientes angulares? O conteúdo completo de environments.prod.tsafter ng build --prodestará em algum .jsarquivo em algum momento. Mesmo se ofuscados, os dados de environments.prod.tsestarão em texto não criptografado. E como todos os arquivos .js, ele estará disponível na máquina do usuário final.
igann
5
@ AlbertoL.Bonfiglio Como um aplicativo Angular é por natureza um aplicativo cliente e JavaScript será usado para transmitir dados e configuração, não deve haver nenhuma configuração secreta usada nele; todas as definições de configuração secretas devem estar atrás de uma camada de API onde o navegador do usuário ou as ferramentas do navegador não podem acessá-lo. Valores como o URI básico de uma API podem ser acessados ​​pelo público porque a API deve ter suas próprias credenciais e segurança com base no login do usuário (token do portador sobre https).
Tommy Elliott
4

Arquivo de configuração do pobre:

Adicione ao seu index.html como a primeira linha na tag do corpo:

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

Adicione ativos / config.js:

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

Adicione config.ts:

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

export interface AppConfig {
    apiBaseUrl: string
}
Matthias
fonte
Sério, +1 para resumir uma solução em seus componentes mais básicos e ainda manter a consistência do tipo.
Luminous
4

Descobri que usar um APP_INITIALIZERpara isso não funciona em situações em que outros provedores de serviço exigem que a configuração seja injetada. Eles podem ser instanciados antes de APP_INITIALIZERserem executados.

Já vi outras soluções que usam fetchpara ler um arquivo config.json e fornecê-lo usando um token de injeção em um parâmetro platformBrowserDynamic()antes de inicializar o módulo raiz. Mas fetchnão é compatível com todos os navegadores e, em particular, os navegadores WebView para os dispositivos móveis que pretendo.

A seguir está uma solução que funciona para mim tanto para PWA quanto para dispositivos móveis (WebView). Nota: Eu só testei no Android até agora; trabalhar em casa significa que não tenho acesso a um Mac para construir.

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

Este código:

  1. inicia uma solicitação assíncrona para o config.jsonarquivo.
  2. Quando a solicitação é concluída, analisa o JSON em um objeto Javascript
  3. fornece o valor usando o APP_CONFIGtoken de injeção, antes da inicialização.
  4. E, finalmente, inicializa o módulo raiz.

APP_CONFIGpode então ser injetado em quaisquer provedores adicionais app-module.tse será definido. Por exemplo, posso inicializar o FIREBASE_OPTIONStoken de injeção @angular/firecom o seguinte:

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

Acho tudo isso uma coisa surpreendentemente difícil (e hackeada) de fazer para um requisito muito comum. Esperançosamente, em um futuro próximo, haverá uma maneira melhor, como suporte para fábricas de provedores assíncronos.

O resto do código para integridade ...

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

e em app/lib/config/config.tseu defino a interface para meu arquivo de configuração JSON:

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

A configuração é armazenada em assets/config/config.json:

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

Observação: eu uso uma tarefa do Azure DevOps para inserir Build.BuildNumber e substituir outras configurações para diferentes ambientes de implantação conforme ele está sendo implantado.

Glenn
fonte
2

Aqui estão minhas duas soluções para isso

1. Armazene em arquivos json

Basta fazer um arquivo json e entrar em seu componente pelo $http.get()método. Se eu precisar disso muito baixo, então é bom e rápido.

2. Armazene usando serviços de dados

Se você deseja armazenar e usar em todos os componentes ou em grande uso, é melhor usar o serviço de dados. Como isso :

  1. Basta criar uma pasta estática dentro da src/apppasta.

  2. Crie um arquivo denominado fuels.tsem uma pasta estática. Você pode armazenar outros arquivos estáticos aqui também. Vamos definir seus dados assim. Supondo que você tenha dados de combustíveis.

__

export const Fuels {

   Fuel: [
    { "id": 1, "type": "A" },
    { "id": 2, "type": "B" },
    { "id": 3, "type": "C" },
    { "id": 4, "type": "D" },
   ];
   }
  1. Crie um arquivo de nome static.services.ts

__

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

@Injectable()
export class StaticService {

  constructor() { }

  getFuelData(): Fuels[] {
    return Fuels;
  }
 }`
  1. Agora você pode disponibilizar isso para todos os módulos

basta importar no arquivo app.module.ts como este e alterar os provedores

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

providers: [StaticService]

Agora use isso como StaticServiceem qualquer módulo.

Isso é tudo.

amku91
fonte
Boa solução, pois você não precisa recompilar. O ambiente é como codificá-lo no código. Desagradável. +1
Terrance00
0

Considero este Angular How-to: Editable Config Files dos blogs de desenvolvimento da Microsoft a melhor solução. Você pode definir as configurações de desenvolvimento de desenvolvimento ou configurações de desenvolvimento de produto.

Melf
fonte
0

Tivemos esse problema anos atrás, antes de eu entrar e implementar uma solução que usava armazenamento local para informações do usuário e do ambiente. Angular 1,0 dias para ser exato. Anteriormente, estávamos criando dinamicamente um arquivo js em tempo de execução que colocaria os api urls gerados em uma variável global. Estamos um pouco mais orientados para OOP atualmente e não usamos armazenamento local para nada.

Criei uma solução melhor para determinar o ambiente e a criação de APIs.

Como isso difere?

O aplicativo não será carregado a menos que o arquivo config.json seja carregado. Ele usa funções de fábrica para criar um grau mais alto de SOC. Eu poderia encapsular isso em um serviço, mas nunca vi nenhum motivo quando a única semelhança entre as diferentes seções do arquivo é que elas existem juntas no arquivo. Ter uma função de fábrica me permite passar a função diretamente para um módulo se ele for capaz de aceitar uma função. Por último, é mais fácil configurar o InjectionTokens quando as funções de fábrica estão disponíveis para uso.

Desvantagens?

Você está sem sorte ao usar esta configuração (e a maioria das outras respostas) se o módulo que você deseja configurar não permite que uma função de fábrica seja passada para forRoot () ou forChild (), e não há outra maneira de configure o pacote usando uma função de fábrica.

Instruções

  1. Usando fetch para recuperar um arquivo json, eu armazeno o objeto na janela e aciono um evento personalizado. - lembre-se de instalar whatwg-fetch e adicioná-lo ao seu polyfills.ts para compatibilidade com o IE
  2. Faça com que um ouvinte de eventos escute o evento personalizado.
  3. O ouvinte de evento recebe o evento, recupera o objeto da janela para passar para um observável e limpa o que foi armazenado na janela.
  4. Bootstrap Angular

- É aqui que minha solução começa a realmente diferir -

  1. Crie um arquivo exportando uma interface cuja estrutura representa seu config.json - realmente ajuda com a consistência do tipo e a próxima seção do código requer um tipo e não especifique {}ou anyquando você sabe que pode especificar algo mais concreto
  2. Crie o BehaviorSubject para o qual você transmitirá o arquivo json analisado na etapa 3.
  3. Use as funções de fábrica para fazer referência às diferentes seções de sua configuração para manter o SOC
  4. Crie InjectionTokens para os fornecedores que precisam do resultado de suas funções de fábrica

- e / ou -

  1. Passe as funções de fábrica diretamente para os módulos capazes de aceitar uma função em seus métodos forRoot () ou forChild ().

- main.ts

Eu verifico que a janela ["ambiente"] não está preenchida antes de criar um ouvinte de evento para permitir a possibilidade de uma solução em que a janela ["ambiente"] seja preenchida por algum outro meio antes que o código em main.ts seja executado.

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

Eu atribuo um valor ao BehaviorSubject usando a janela ["ambiente"] para redundância. Você poderia conceber uma solução em que sua configuração já esteja pré-carregada e a janela ["ambiente"] já esteja preenchida no momento em que qualquer código do aplicativo Angular for executado, incluindo o código em 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 - Simplificado para facilitar a compreensão

Fato engraçado! Versões mais antigas do NGXLogger exigiam que você passasse um objeto para LoggerModule.forRoot (). Na verdade, o LoggerModule ainda o faz! NGXLogger gentilmente expõe LoggerConfig que você pode substituir permitindo que você use uma função de fábrica para configuração.

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

Termo aditivo

Como resolvi a criação de meus URLs de API?

Eu queria ser capaz de entender o que cada url fazia por meio de um comentário e queria verificar o tipo, já que essa é a maior força do TypeScript em comparação com o javascript (IMO). Eu também queria criar uma experiência para que outros desenvolvedores adicionassem novos endpoints e apis que fosse o mais integrada possível.

Eu criei uma classe que leva no ambiente (dev, test, stage, prod, "" e etc) e passei esse valor para uma série de classes [1-N] cujo trabalho é criar o url base para cada coleção de API . Cada ApiCollection é responsável por criar a url base para cada coleção de APIs. Podem ser nossas próprias APIs, APIs de um fornecedor ou até mesmo um link externo. Essa classe passará o url de base criado para cada API subsequente que ela contém. Leia o código abaixo para ver um exemplo básico. Uma vez configurado, é muito simples para outro desenvolvedor adicionar outro endpoint a uma classe Api sem ter que tocar em mais nada.

TLDR; princípios básicos de OOP e lazy getters para otimização de memória

@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/");
  }
}
Luminoso
fonte