Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP5 min czytania1112 słów

Ź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.
  • Lepsza percepcja wydajności. Struktury szkieletowe informują użytkowników, że aplikacja jest aktywna, podczas gdy flagi i uprawnienia ładowane są w tle.
  • Odporność na błędy. Awaria punktu końcowego konfiguracji nie powoduje już awarii bootstrapa. Aplikacja może wyświetlić stan błędu, zaproponować ponowną próbę i utrzymać użyteczność niespowiązanych części panelu sterowania.
  • 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.
    • rxResource przekształ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

  • Faster Angular przez robienie mniej: dziesięć technik poza OnPush i trackBy — Ograniczanie marnotrawstwa zasobów sieciowych, pracy reaktywnej, DOM i procesora w nowoczesnym Angular dzięki @defer, stopniowej hydratacji, funkcji resource(), sygnałom computed, fazom renderowania oraz Web Workers.
  • Runes w Svelte 5 i sygnały SolidJS: aktualizacje interfejsu bez ponownego renderowania — Dowiedz się, jak Svelte 5 kompiluje runes oraz SolidJS przekazuje sygnały w celu bezpośredniej aktualizacji DOM, jak różni się to od React i Angular oraz kiedy warto na to przesiąść się.