Nieblokujące uruchamianie Angulara: Ładowanie konfiguracji za pomocą rxResource
Dowiedz się, dlaczego użycie blokującego APP_INITIALIZER powoduje, że użytkownicy Angular trafiają na pustą stronę, oraz jak rxResource oraz narzędzia do obsługi szkieletu aplikacji i tras ładowają konfigurację bez niego.
Źródła: Stop Blocking Your App Load: Architecting APP_INITIALIZER with Signals
Aplikacje Enterprise Angular zazwyczaj wymagają wielu elementów przed pokazaniem pierwszego użytecznego ekranu: flag funkcjonalnych, uprawnień RBAC użytkownika, tematu najemcy oraz klienta telemetrycznego. Tradycyjnym sposobem na ich dostarczenie jest użycie APP_INITIALIZER, który zwraca obietnicę, a Angular nie renderuje niczego, dopóki ta obietnica się nie spełni. Poniżej zobaczysz, ile to kosztuje, oraz jak traktować konfigurację jako stan reaktywny za pomocą rxResource, dzięki czemu strona pojawia się natychmiast, a tylko te części, które faktycznie zależą od konfiguracji, czekają.
Dlaczego blokujący inicjalizator szkodzi
Klasyczna konfiguracja rejestruje fabrykę w ramach wielo-providera APP_INITIALIZER. Fabryka ta zwraca funkcję, która przekształca żądanie konfiguracji w obietnicę, a framework nie uruchomi komponentu korzeniowego, dopóki ta obietnica się nie rozwiąże.
// The Blocking Legacy Pipeline
export function initializeConfig(configService: ConfigService) {
// Angular halts all execution until this Promise resolves
return () => configService.loadConfiguration().toPromise();
}
// In app.config.ts
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeConfig,
deps: [ConfigService],
multi: true
}
]
Efekt jest prostą operacją arytmetyczną: jeśli koniec punktu konfiguracji obsługuje zapytanie przez 600 ms, użytkownicy widzą pustą stronę przez co najmniej 600 ms przy każdym załadowaniu. Ten opóźnienie bezpośrednio wpływa na czas pierwszego wyświetlenia treści, a wolny lub niepracujący koniec punktu konfiguracji powoduje, że aplikacja wygląda pusto lub nie działa prawidłowo, bez możliwości pokazania przydatnej informacji.
Dwa elementy w tym fragmencie są już przestarzałe. Funkcja toPromise() została zdeprecjonowana w RxJS na rzecz firstValueFrom(), a nowsze wersje Angulara zdeprecjonują token APP_INITIALIZER na rzecz funkcji provideAppInitializer(). Sprawdź dokumentację dotyczącą wersji Angulara, którą używasz. Żadna z tych zmian jednak nie eliminuje problemu blokowania działania aplikacji – jedynie zmieniają składnię.
Traktowanie konfiguracji jako zależności reaktywnej
Opcją alternatywną jest przestanie traktowania konfiguracji jako bramy wejściowej i zaczęcie traktowania jej jako danych, których interfejs użytkownika oczekuje. Rodzina funkcji resource() w Angularze wyraża dokładnie to: wartość asynchroniczną, której stan ładowania i błędu jest udostępniany jako sygnały.
Służba konfiguracyjna oparta na rxResource
Zamiast przekazać Angularowi obietnicę (promise), służba deklaruje zasób. rxResource bierze loader zwracający Observable, w tym przypadku wywołanie HttpClient, uruchamia go podczas tworzenia służby i udostępnia value, isLoading oraz error jako sygnały. Podczas procesu bootstrap nic nie czeka na jego działanie.
// The Non-Blocking Reactive Architecture
@Injectable({ providedIn: 'root' })
export class ConfigService {
private http = inject(HttpClient);
// Define a resource that doesn't block the bootstrap
configResource = rxResource({
loader: () => this.http.get<AppConfig>('/api/config')
});
// Expose as a signal for easy template consumption
get config() { return this.configResource.value; }
get isLoading() { return this.configResource.isLoading; }
}
Należy pamiętać, że gettery zwracają same sygnały, więc konsumenty wywołują je, jak w przykładzie configService.isLoading(). API do zarządzania zasobami ewoluują z każdą nową wersją (w nowszych wersjach opcja rxResource nazywa się stream, a nie loader), dlatego nazwy opcji muszą odpowiadać wersji Angulara, którą używasz.
Zarządzanie konfiguracją niezbędną do uruchomienia aplikacji
Część ustawień jest rzeczywiście wymagana, na przykład wartości potrzebne do autoryzacji. Nie jest konieczne blokowanie całej aplikacji; istnieją dwa lepsze miejsca na oczekiwanie.
Opcja 1: najpierw wyświetlić szkielet
Zazwyczaj jest to najlepszy wybór. Szkielet (nakładka, nawigacja, układ) jest renderowany natychmiast, a blok strumieniowania @if pokazuje szkielet, dopóki zasób nie zostanie w pełni załadowany, po czym przejmuje kontrolę element router outlet.
<!-- Non-blocking, instant render -->
@if (configService.isLoading()) {
<app-skeleton-loader />
} @else {
<router-outlet />
}
Użytkownicy otrzymują natychmiastową wizualną informację zwrotną, a szablon stanowi również odpowiednie miejsce na obsługę błędów. Należy dodać gałąź dla configResource.error() z przyciskiem ponownej próby, który wywołuje metodę reload() zasobu; w przeciwnym razie nieudana prośba skutkuje wyświetleniem elementu bez żadnej konfiguracji.
Opcja 2: rozwiązanie typu route resolver
Jeśli określona trasa wymaga konfiguracji przed uruchomieniem, rozwiązanie to ogranicza czas oczekiwania do tej konkretnej nawigacji, a nie do całego procesu bootstrapowania.
export const configResolver: ResolveFn<AppConfig> = () => {
return inject(ConfigService).configResource.value!;
};
// In app.routes.ts
{ path: 'dashboard', component: DashboardComponent, resolve: { config: configResolver } }
Należy uważać przy takiej dokładnej implementacji. configResource.value jest sygnałem, a stwierdzenie typu „nie może być null” jedynie ucisza kompilator: resolver natychmiast zwraca ten sygnał bez czekania na żądanie. Aby faktycznie poczekać, resolver musi zwrócić coś, co zostanie ukończone po dostępności danych – na przykład Observable pochodzący od zasobu, który filtruje stan ładowania, lub bezpośredni wywołanie HttpClient otoczone funkcją firstValueFrom().
Korzyści z podejścia nieblokującego
- Szybsze pierwsze wyświetlenie. Przeglądarka może natychmiast renderować podstawowy HTML i CSS zamiast pustego elementu korzeniowego, co poprawia wskaźnik First Contentful Paint oraz inne metryki monitorowane przez wyszukiwarki i panele kontrolne.
Należy zaakceptować pewien kompromis: każdy komponent, który odczytuje konfigurację, musi teraz radzić sobie ze stanem „jeszcze nie załadowane”, albo znajdować się za granicą (np. @if w szkieletowym kodzie lub mechanizmem rozwiązywania), która to gwarantuje. Aby czysto modelować te stany, dobrze sprawdza się zjednoczenie dyskryminowane, jak pokazano w zastępowaniu flag ładowania zjednoczeniem AsyncState.
Podsumowanie
- Inicjalizator zwracający obietnicę trzyma całą aplikację jako zakładnika twojego najwolniejszego żądania startowego.
rxResourceprzekształca konfigurację w sygnały dotyczące wartości, ładowania i błędów, na które mogą reagować szablony.- Domyślnie używany jest shell aplikacji z szkieletem
@if; rozwiązanie problemu tras odpowiada za trasy, które nie mogą zostać aktywowane bez danych, pod warunkiem że faktycznie czekają. - Zachowaj inicjalizatory do zadań, które naprawdę muszą zostać ukończone przed wyświetleniem czegokolwiek, takich jak rejestracja globalnego obsługiwanika błędów.
Niezależnie od tego, czy wybierzesz globalny shell, czy rozwiązania dla poszczególnych tras, zasada jest ta sama: spraw, by oczekiwanie było widocznym stanem interfejsu użytkownika, a nie pustą stroną.
Literatura pokrewna
- Zamiana flag ładujących się elementów w Angularu za pomocą Discriminated AsyncState Union — Modelowanie asynchronicznych operacji w Angularu jako jednego zunion typów, wyznaczanie wartości logicznych za pomocą sygnałów obliczeniowych oraz kierowanie każdą zmianą stanu za pośrednictwem pojedynczego operatora RxJS.
- Jak Angular Dependency Injection rozwiązuje problemy z usługami za pomocą inject() — Zrozumienie sposobu, w jaki Angular DI dostarcza usługi za pomocą funkcji inject(), providerów oraz hierarchicznych injectorów, a także jak ograniczyć zakres instancji i uniknąć błędów związanych z brakującymi providerami.