Ciao a tutti,
nell’articolo di oggi voglio raccontarvi di come ho sostituito il file di configurazione di Angular (l’environment.ts) con una risorsa esterna (scaricata via http).
Il contesto
In questi mesi sto sviluppando il frontend di un’applicazione enterprise molto complessa, che dovrà essere esposta all’interno di un container docker, in un cluster Kubernetes. L’applicazione la sto scrivendo in Angular (versione 21), ma questo articolo può essere seguito per qualsiasi versione (con qualche adattamento se la vostra versione di angular è minore della versione 19)
L’applicazione in questione dovrà essere deployata su 300 istanze differenti: utilizzare l’environment.ts per gestire le configurazioni di queste 300 istanze è sbagliato perché ci obbligherebbe ad avere 300 immagini differenti nel registry docker.
Mi serviva quindi una strategia per separare la configurazione dall’applicazione stessa.
Funzione provideAppInitializer()
Dalla versione di Angular 19 è stato introdotta la funzione provideAppInitializer() (che sostituisce APP_INITIALIZER, ora deprecato). La funzione è il modo ufficiale per caricare risorse esterne durante durante l’avvio dell’applicazione.
All’interno di questa funzione possiamo inserire una logica che ritorni una Promise o un Observable, in modo tale da non concludere il bootstrap dell’applicazione fino a quando questi non sono stati risolti.
L’esempio potrebbe essere questo:
//...
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideAppInitializer(loadConfiguration),
// ...
provideAppInitializer(() => {
const _configService= inject(ConfigService);
return _configService.loadConfigFromHttp();
}),
}Nell’esempio che ho inserito la funzione provideAppInitializer() inietta un servizio tramite la dependency injection e richiama la funzione loadConfigFromHttp(), che a sua volta ritorna un oggetto di tipo Observable.
Con questa configurazione la funzione provideAppInizializer() non si completerà fino a quando l’Observable della funzione non si concluderà.
Questo approccio però, non va bene per il nostro caso d’uso.

Vediamo ora perchè questa soluzione non può andare sempre bene.
Per effettuare il login nella mia applicazione dovevo utilizzare un provider di autenticazione già configurato nell’infrastruttura (keycloak) e per integrarlo nel progetto c’è un comodo pacchetto da installare.
Una volta installato va inizializzato nell’app.config.ts, esattamente dove abbiamo provideAppInitializer(): il problema nasce proprio qui, perché per inizializzare la libreria ho bisogno di alcuni parametri di configurazione, che vengono valutati subito, prima ancora che la funzione provideAppInitializer() sia conclusa. Se scaricassi le configurazioni da un endpoint http non avrei ancora a disposizione questi parametri durante questa fase.
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideAppInitializer(loadConfiguration),
// ...
//Initialize auth provider
provideKeycloakAsAuthProvider(
environment.pluginSettings.keycloak.url,
environment.pluginSettings.keycloak.realm_id,
environment.pluginSettings.keycloak.client_id,
environment.apiBaseUrl,
environment.pluginSettings.keycloak.authCheck as KeycloakOnLoad,
),
// ...
}In un’applicazione più “semplice” le variabili environment.pluginSettings.keycloak.xxxx appartengono all’environment.ts e sono disponibili fin da subito poichè appartenenti ad una classe typescript.
Se invece la configurazione la carichiamo via http, nel momento in cui inizializzo il provider di keycloak potrei non aver ancora ottenuto la configurazione e quindi environment sarebbe ancora vuoto!
La mia soluzione
La configurazione dobbiamo averla caricata prima che l’applicazione venga avviata e il punto dove innestare la gestione del file di configurazione è il main.ts, ossia il primo file che viene eseguito quando l’applicazione si avvia.
Il processo che ho implementato è il seguente:
- L’utente apre l’applicazione e si avvia l’index.html
- Come prima cosa viene eseguito il contenuto di main.ts, nello specifico la funzione
bootstrap()(vedi il codice più in basso) - Funzione
bootstrap():- L’applicazione scarica la configurazione da un endpoint http (se non c’è in cache)
- Se offline –> Redirect ad una pagina statica offline.html
- Se il download della configurazione è OK allora verifico la struttura della configurazione
- Se ok procedo con il bootstrap dell’applicazione Angular
- Se errata eseguo un redirect ad una pagina statica error.html
Come già detto, lato codice è gestito tutto nel main.ts
// ... import
/**
* Custom bootstrap workflow:
* 1. Load environment configuration from http
* 2. Inizialize configurations and providers
* 3. Startup webapp
*/
async function bootstrap(): Promise<void> {
try {
// 1. Get config from http endpoint (or from cache)
const loadResult = await loadEnvironmentConfig();
//Log
console.debug(
`[Bootstrap] Environment loaded from: ${loadResult.loadedFrom}`,
loadResult.customerKey ? `(customer: ${loadResult.customerKey})` : '(local development)'
);
if (loadResult.loadError) {
//FAILED_TO_FETCH = 'Failed to fetch' and means that my backend is unreachable
if(loadResult.loadError == responseErrors.FAILED_TO_FETCH) {
goToOfflinePage();
return;
}
console.warn('[Bootstrap] Load error:', loadResult.loadError);
goToErrorPage(loadResult.loadError);
}
// 2. Create angular config and initialize angular providers
const appConfig = createAppConfig(loadResult);
// 3. Bootstrap application
await bootstrapApplication(AppComponent, appConfig);
} catch (error) {
console.error('[Bootstrap] Critical error:', error);
goToErrorPage(error);
}
}
function goToErrorPage(error: unknown): void {
// .. Log error to my logger provider
globalThis.location.href = 'error.html';
}
function goToOfflinePage(): void {
// .. Log error to my logger provider
globalThis.location.href = 'offline.html';
}
bootstrap();
Alcuni dettagli rispetto al codice:
await loadEnvironmentConfig(); è all’interno di una classe dedicata e si occupa scaricare la configurazione da un endpoint http. Una volta scaricato la inserisce nelsessionStorage, in modo tale da essere più efficiente e veloce la seconda volta.goToOfflinePage(); porta l’utente alla pagina offline.htmlshowCriticalErrorPage(error);porta l’utente ad una pagina generica di errore, dopo aver tracciato l’errore- La configurazione scaricata (in json) è l’implementazione di un’interfaccia che ho creato nell’applicazione (
IEnvironmentConfig)
export interface IEnvironmentConfig {
appMode: 'development' | 'test' | 'production';
appTitle: string;
logs: {
level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
logToConsole: boolean;
};
pluginSettings: {
keycloak: {
url: string;
client_id: string;
realm_id: string;
authCheck: 'check-sso' | 'login-required';
};
};
}{
"appMode": "development",
"appTitle": "ThinkAsADev webapp",
"logs": {
"level": "warn",
"logToConsole": false
},
"pluginSettings": {
"keycloak": {
"url": "https://xxxxxx:24443/auth",
"client_id": "my-client-id",
"realm_id": "my-realm",
"authCheck": "login-required"
}
}
}InjectionToken per la configurazione
Quando parliamo di InjectionToken ci riferiamo ad una funzionalità di Angular con la quale possiamo associare una chiave ad un oggetto, per poi renderlo disponibile in tutta l’applicazione tramite la dependency injection. Ora vediamo quindi come ho associato la configurazione caricata ad una chiave (ENVIRONMENT).
Per farlo dobbiamo aggiungere un parametro alla funzione createAppConfig() (che risiede nell’app.config.ts)
// ...
const appConfig = createAppConfig(loadResult);
// ...// ...
export function createAppConfig(config: IEnvironmentConfig): ApplicationConfig {
return {
providers: [
provideZonelessChangeDetection(),
{
provide: ENVIRONMENT,
useValue: config,
},
//...
],
};
}In questo modo, in una qualsiasi parte dell’applicazione potrò accedere alla mia configurazione tramite dependency injection in questo modo:
@Injectable({
providedIn: 'root',
})
export class MyService {
private readonly _environment = inject(ENVIRONMENT);
//...
}Gestione del tempo di attesa
La soluzione adottata porta inevitabilmente ad un allungarsi dei tempi di avvio dell’applicazione, quantomento la prima volta (dalla successiva in poi la configurazione viene inserita nel sessionStorage()).
Per gestire questo tempo di attesa possiamo andare ad aggiungere un loader all’interno dei tag <app-root> del file index.html: qualsiasi cosa che inseriamo qui dentro sarà poi eliminata in automatico dal framework e sostituito con l’applicazione angular vera e propria.
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Your app name</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head>
<body class="dx-viewport">
<app-root>
<div class="loader-container">
<img src="/images/your-logo.svg" alt="logo" class="loader-logo">
<div class="loader-spinner"></div>
</div>
</app-root>
</body>
</html>Conclusioni
In questo articolo abbiamo visto come affrontare il caricamento dimanico di una configurazione in un’applicazione Angular, permettendo di andare ad usare i parametri caricati per inizializzare le varie librerie nel progetto.
Con questa strategia le configurazioni vengono esternalizzate dall’applicazione, una strategia chiave nelle applicazioni enterprise ed in quelle che necessitano di essere incluse in un container per essere servite.
Grazie per aver letto, alla prossima!