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
- 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
- Faça com que um ouvinte de eventos escute o evento personalizado.
- 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.
- Bootstrap Angular
- É aqui que minha solução começa a realmente diferir -
- 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
- Crie o BehaviorSubject para o qual você transmitirá o arquivo json analisado na etapa 3.
- Use as funções de fábrica para fazer referência às diferentes seções de sua configuração para manter o SOC
- Crie InjectionTokens para os fornecedores que precisam do resultado de suas funções de fábrica
- e / ou -
- 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());
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 {
get MemberInfo() {
this.attachProperty("MemberInfo", `${this.base}basic-info`);
return this.MemberInfo;
}
constructor(baseUrl: string) {
super(baseUrl + "member/api/");
}
}