Безблокуюче запускання Angular: завантаження конфігурації за допомогою rxResource замість іншого методу
Дізнайтеся, чому використання BLOCKING APP_INITIALIZER залишає користувачів Angular на порожній сторінці, та як rxResource, а також інструменти app shell та route resolvers завантажують конфігурацію без нього.
Джерела: Stop Blocking Your App Load: Architecting APP_INITIALIZER with Signals
Корпоративні додатки на Angular зазвичай потребують багато чого ще до того, як з’явиться перший корисний екран: флаги функцій, права доступу користувача за моделлю RBAC, тема для окремого клієнта, клієнт для збору телеметричних даних. Традиційний спосіб їх отримання — це APP_INITIALIZER, який повертає обіцянку, і Angular нічого не відображає, поки ця обіцянка не буде виконана. Нижче ви побачите, у що це обходиться, та як обробляти конфігурацію як реактивний стан за допомогою rxResource, щоб сторінка з’являлась миттєво, а лише ті частини, які справді залежать від конфігурації, чекали.
Чому блокуючий ініціалайзер шкодить
Класична налаштування реєструють фабрику під мультипровайдером APP_INITIALIZER. Фабрика повертає функцію, яка перетворює запит на конфігурацію на обіцянку, і фреймворк не буде запускати кореневий компонент, поки вона не буде виконана.
// 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
}
]
Ефект полягає у простих арифметичних обчисленнях: якщо кінцева точка конфігурації обробляє запит протягом 600 мс, користувачі бачать порожню сторінку принаймні протягом цього часу під час кожного завантаження. Ця затримка безпосередньо впливає на показ першого змістовного елемента, а повільна чи несправна кінцева точка перетворюється на порожнє чи нефункціональне додатко, яке не має можливості відобразити корисне повідомлення.
У цьому уривку є два аспекти, які вже застаріли. Функція toPromise() у RxJS вважається застарілою, і її замінила firstValueFrom(), а останні версії Angular вважають застарілим токен APP_INITIALIZER, замінивши його на функцію provideAppInitializer(). Перевірте документацію до версії Angular, яку ви використовуєте. Однак жодна з цих змін не усуває проблему блокування; вони лише змінюють синтаксис.
Обробка конфігурації як реактивної залежності
Альтернативою є припинення розгляду конфігурації як бар’єру та початок розгляду її як даних, які інтерфейс користувача очікує отримати. Сімейство resource() у Angular саме це й передбачає: асинхронне значення, чий стан завантаження та помилок відображається у вигляді сигналів.
Сервіс конфігурації, побудований на rxResource
Замість того, щоб передати Angular обіцянку, сервіс оголошує ресурс. rxResource бере загрузчик, який повертає Observable — у цьому випадку виклик HttpClient — запускає його під час створення сервісу та робить value, isLoading та error доступними у вигляді сигналів. Під час ініціалізації ніщо не чекає на його результати.
// 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; }
}
Зверніть увагу, що геттери повертають самі сигнали, тому користувачі викликають їх, як у configService.isLoading(). API ресурсів постійно еволюціонують з кожною новою версією (у новіших версіях опція rxResource називається stream, а не loader), тому слід підбирати назви опцій відповідно до вашої версії Angular.
Керування конфігурацією, без якої додаток не може працювати
Деякі налаштування є справді обов’язковими, наприклад значення, необхідні для аутентифікації. Проте немає потреби блокувати весь додаток; існують два кращі способи очікування.
Варіант 1: спочатку відобразити шелл
Це зазвичай найкращий вибір. Шелл (заголовок, навігація, макет) відображається одразу, а блок керування @if показує скелет до завершення завантаження ресурсу, після чого бере на себе керування роутер.
<!-- Non-blocking, instant render -->
@if (configService.isLoading()) {
<app-skeleton-loader />
} @else {
<router-outlet />
}
Користувачі отримують негайну візуальну відповідь, а шаблон є природним місцем для обробки помилок. Додайте гілку для configResource.error() із кнопкою повторної спроби, яка викликає reload() ресурсу; інакше невдала заявка призведе до відображення результату без конфігурації.
Варіант 2: резолвер маршрутів
Якщо певному маршруту потрібна конфігурація перед його активацією, резолвер обмежує очікування лише цим переходом, а не всім процесом запуску.
export const configResolver: ResolveFn<AppConfig> = () => {
return inject(ConfigService).configResource.value!;
};
// In app.routes.ts
{ path: 'dashboard', component: DashboardComponent, resolve: { config: configResolver } }
Будьте обережні з цією саме реалізацією. configResource.value є сигналом, і твердження про те, що він не дорівнює null, лише приглушує попередження компілятора: резолвер негайно повертає сигнал без очікування запиту. Щоб справді почекати, резолвер має повернути щось, що буде завершено після отримання даних, наприклад Observable, отриманий від ресурсу та який фільтрує стан завантаження, або прямий виклик HttpClient, обгорнутий у firstValueFrom().
Яку перевагу дає неблокуючий підхід
- Швидше відображення контенту. Браузер може негайно відобразити HTML-структуру та базовий CSS замість порожнього кореневого елемента, що покращує показники First Contentful Paint та інші метрики, які відстежують пошукові системи та панелі керування.
Існує компроміс, який потрібно прийняти: кожен компонент, який читає конфігурацію, тепер мусить обробляти стан „ще не завантажено“, або знаходитися за межами (за допомогою @if шелу чи резолвера), який це гарантує. Для чіткого моделювання таких станів добре підходить дискримінований союз, як показано у заміні прапорців завантаження на союз AsyncState.
Підсумок
- Ініціалайзер, який повертає обіцянку, утримує всю програму в заручниках через найповільніший запит на старт.
rxResourceперетворює конфігурацію на сигнали значення, завантаження та помилки, на які можуть реагувати шаблони.- За замовчуванням використовується оболонка додатку зі скелетом
@if; резолвер маршрутів підходить для маршрутів, які не повинні активуватися без даних, за умови що він справді чекає. - Залишайте ініціалайзери для завдань, які справді мають бути завершені перед відображенням будь-чого, наприклад для реєстрації глобального обробника помилок.
Незалежно від того, чи оберете ви глобальну оболонку чи резолвери для кожного маршруту, принцип залишається тим самим: зробіть процес очікування видимим станом локального інтерфейсу замість порожньої сторінки.