Configurare la tua app prima che parta.

L'idioma inglese "to pull oneself up by one's bootstraps" (in italiano: tirarsi su per le stringhe degli stivali) descrive un compito impossibile: sollevarsi da terra tirando i propri lacci. Nell'ingegneria del software, tuttavia, il bootstrapping è molto reale. È quel momento critico in cui un'applicazione carica la sua configurazione, imposta il suo ambiente e prepara per l'utente—tutto prima che il primo componente venga renderizzato.

L'Approccio Standard: APP_INITIALIZER

La letteratura standard suggerisce spesso di utilizzare il token APP_INITIALIZER. Questo ti consente di agganciarti al processo di bootstrap di Angular ed eseguire una funzione (di solito restituendo una Promise o un Observable) prima che l'app finisca di inizializzarsi.

// Il Metodo "Standard"
{
  provide: APP_INITIALIZER,
  useFactory: (configService: ConfigService) => () => configService.loadConfig(),
  deps: [ConfigService],
  multi: true
}

Sebbene funzionale, questo avviene all'interno del ciclo di vita di Angular. Ciò significa che i servizi e componenti vengono già istanziati dal contenitore DI (Dependency Injection). Se la tua configurazione è lenta o fallisce, potresti incontrare stati di "flickering" sulla UI o condizioni in cui un servizio tenta di accedere a un valore di configurazione che non è ancora inizializzato.

Un Approccio Più Intelligente: Il Metodo "Hot Config"

Nel mio lavoro con il RAD Framework, ho perfezionato un metodo che rende l'applicazione "intelligente" dal millisecondo zero spostando la logica al di fuori del ciclo di vita di Angular.

1. Hot Config (Basato su JSON)

Invece di codificare valori nei file di ambiente (che richiedono una ricompilazione per ogni modifica), utilizziamo un app-config.json autonomo. Questo consente una "Hot Configuration"—modificando gli endpoint API o i flag delle funzionalità al volo. Basta un reload della pagina.

{
  "appName": "RAG System",
  "apiUrl": "http://localhost:3000/api/v1",
  "logMode": "console",
  "siteMode": "development",
  "version": "1.0.0",
  "langs": [
    {
      "code": "en",
      "label": "ENG",
      "description": "English",
      "langEn": "English"
    },
    {
      "code": "it",
      "label": "ITA",
      "description": "Italiano",
      "langEn": "Italian"
    }
  ],
  "google": {
    "clientId": "YOUR_GOOGLE_CLIENT_ID",
    "redirectUri": "http://localhost:4200",
    "button": {
      "theme": "outline",
      "size": "large",
      "text": "continue_with",
      "logo_alignment": "left"
    },
    "analyticsId": "YOUR_GOOGLE_ANALYTICS_ID"
  },
  "uploadConfig": {
    "maxFileSize": 50,
    "maxFileCount": 5,
    "acceptedFileTypes": "*/*",
    "allowMultiple": false
  },
  "menus": {
    "guest": [
      {
        "label": "Benvenuto",
        "icon": "home",
        "route": "/welcome"
      },
      {
        "label": "Accedi",
        "icon": "login",
        "route": "/login"
      }
    ],
    "user": [
      {
        "label": "Home",
        "icon": "home",
        "route": "/home"
      }
    ],
    "admin": [
      {
        "label": "_DIVIDER_"
      },
      {
        "label": "Configurazione",
        "icon": "settings",
        "route": "/admin-config"
      }
    ]
  }
}

2. StoreService

Per evitare il problema "chicken and egg" della Dependency Injection, utilizzo un service specializzato (StoreService). Utilizzando proprietà e motodi statici, la configurazione diventa una "Fonte di Verità" globale disponibile anche prima che il contenitore DI di Angular sia completamente pronto.

import { Injectable } from "@angular/core";
import { BehaviorSubject } from "rxjs";
import {
  I18nLang,
  IAppConfig,
  IMenuItem,
  SiteModeType,
} from "../../Models/config.model";

/**
 * La configurazione dell'app viene caricata quando Angular si avvia in main.ts
 * Questo servizio fornisce accesso alla configurazione
 * e gestisce lo stato dell'applicazione in localStorage/sessionStorage
 */
@Injectable({
  providedIn: "root",
})
export abstract class StoreService {
  private static PREFIX: string = "MYAPP";
  private static CONFIG = {} as IAppConfig;

  // Observable per il cambio di token
  private static tokenSubject = new BehaviorSubject("");
  public static tokenChange$ = StoreService.tokenSubject.asObservable();

  constructor() {}

  static set(key: string, value: any, persistent: boolean = false) {
    key = this.PREFIX + key;
    if (persistent) localStorage.setItem(key, value);
    else sessionStorage.setItem(key, value);
  }

  static get(key: string): any {
    key = this.PREFIX + key;
    return localStorage.getItem(key)
      ? localStorage.getItem(key)
      : sessionStorage.getItem(key);
  }

  static remove(key: string) {
    key = this.PREFIX + key;
    localStorage.removeItem(key);
    sessionStorage.removeItem(key);
  }
  static clear() {
    localStorage.clear();
    sessionStorage.clear();
  }

  static getAll() {
    const allItems = { ...localStorage, ...sessionStorage };
    const filteredItems: { [key: string]: string } = {};
    for (const key in allItems) {
      if (key.startsWith(this.PREFIX)) {
        filteredItems[key.replace(this.PREFIX, "")] = allItems[key];
      }
    }
    return filteredItems;
  }

  // SEZIONE TOKEN
  static setJwtToken(token: string) {
    if (!token) {
      this.remove("auth_token");
      this.tokenSubject.next(""); // Emetti token vuoto (logout)
      return;
    }
    this.set("auth_token", token);
    this.tokenSubject.next(token); // Emetti nuovo token (login/refresh)
  }

  static getJwtToken(): string {
    return this.get("auth_token") || "";
  }
  static setRefreshToken(token: string | null) {
    this.set("refresh_token", token);
  }

  static getRefreshToken(): string | null {
    return this.get("refresh_token") || null;
  }

  // SEZIONE CONFIGURAZIONE
  static setConfig(config: IAppConfig) {
    this.CONFIG = config;
  }

  private static getConfig(): IAppConfig {
    return this.CONFIG;
  }

  static getApiUrl(): string {
    return this.getConfig().apiUrl;
  }
  // aggiungi funzioni statiche per ogni opzione necessaria
}

3. Il "Pre-Flight" Boot in main.ts

Questa è la parte "intelligente". Invece di lasciare che Angular inizi e poi chieda la configurazione, recuperiamo la configurazione in main.ts e poi avviamo il processo di bootstrap.

import { provideZoneChangeDetection } from "@angular/core";
/// <reference types="@angular/localize" />
import { bootstrapApplication } from "@angular/platform-browser";
import { appConfig } from "./app/app.config";
import { AppComponent } from "./app/app.component";
import { StoreService } from "./app/Core/services/store.service";

// Carica la configurazione prima di avviare l'applicazione evitando la cache del browser
// In questa sezione si può pre-caricare tutto cio che si vuole
const loadConfig = async () => {
  try {
    const dt = new Date().getMilliseconds();
    const response = await fetch(`./assets/config/app-config.json?ver=${dt}`);

    if (!response.ok) {
      throw new Error(
        `Impossibile caricare la configurazione: ${response.status} ${response.statusText}`,
      );
    }

    const config = await response.json();
    StoreService.setConfig(config);

    // Avvia l'applicazione dopo che la configurazione è stata caricata (o non è riuscita a caricarsi)
    bootstrapApplication(AppComponent, {
      ...appConfig,
      providers: [provideZoneChangeDetection(), ...appConfig.providers],
    }).catch((err) => console.error("Avvio dell'applicazione non riuscito:", err));
  } catch (error) {
    console.error(
      "Errore fatale: l'applicazione non può avviarsi senza configurazione:",
      error,
    );
  }
};

// Esegui il caricamento della configurazione
loadConfig();

Perché è meglio?

  • Zero Flickering: Nessun "loading" spinner mentre si attendono le impostazioni.
  • Nessun Errore Undefined: Non è necessario controllare if(config) in ogni costruttore.
  • Indipendente dall'Ambiente: Lo stesso build funziona in Dev, Staging e Prod semplicemente scambiando il file JSON.

Conclusione

Spostando la logica di configurazione al punto di ingresso della tua applicazione, crei un sistema più robusto, flessibile e prevedibile.

Riferimenti