Инструкции
Миграция на Single Bundle

Миграция на Single Bundle и Standalone

⚠️ Технологии пока находятся в стадии пилотирования.

⚠️ Если вы уже переходили на Single Bundle ранее, необходимо заново воспользоваться данной инструкцией, так как подход сильно изменен с первой версии Single Bundle.

Технология Single Bundle позволяет использовать уже загруженный бандл при повторной загрузке проекта. Это позволяет сэкономить потребляемую память.

Для перехода на этот режим необходимо преобразовать главный компонент в Standalone и перейти на новую систему регистрации приложений.

Standalone-компоненты – это технология Angular (opens in a new tab), которая позволяет загружать компоненты без использования модулей.

Преимущества нового подхода

Новая система регистрации приложений в Q.Palette:

  • Уменьшает потребляемую память (режим Single Bundle).
  • Использует современный подход Angular с Standalone-компонентами.
  • Внедряет более современный подход с provide*-функциями для регистрации функциональности.
  • Решает проблему дублирования импортов корневых модулей (как ранее было с forRoot-методами).
  • Избавляет от необходимости задавать конфигурацию модулей, если она не отличается от глобальных настроек.
  • Упрощает обратно-совместимые изменения в Q.Palette без изменения кода продуктов.

Термины

Далее в инструкции предполагается, что главный модуль называется AppModule, а главный компонент – AppComponent. Модуль и компонент могут называться по-другому (как в случае с виджетам). Главное, что нужно понимать:

  • Главный модуль – тот, который грузится в main.ts приложения.
  • Главный компонент – тот, который грузится внутри главного модуля с помощью функции QWebComponentModule.doBootstrap().

Шаги миграции

0. Подготовка

Убедитесь, что используется версия Q.Palette не ниже 7.5.1.

1. Преобразуйте главный компонент в Standalone

@Component({
  // ...
  standalone: true, // Добавьте эту строку
})
export class AppComponent extends QWebComponent {}

2. Удалите providers главного компонента

Если режим Single Bundle уже был включен ранее, то наверняка у главного компонента есть секция providers – она больше не нужна.

@Component({
  // ...
  providers: [ // Удалите секцию
      //...
      SomeService
  ],
})
export class AppComponent extends QWebComponent {}

Для сервисов, которые ранее были добавлены в секцию providers, достаточно прописать в их декораторах providedIn: 'root', чтобы сервисы имели один экземпляр на все приложение:

@Injectable({
  providedIn: 'root' // У сервисов, добавленных ранее в `providers` главного компонента
})
export class SomeService {}

3. Обновите main.ts

Вместо platformBrowserDynamic().bootstrapModule используйте createApplication и await app.bootstrap внутри асинхронной самовызываемой функции:

// Старый подход
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
 
platformBrowserDynamic().bootstrapModule(AppModule);
 
// Новый подход
import { createApplication } from '@diasoft/qpalette-visual';
import { AppComponent } from './app/app.component';
 
(async () => {
  const app = createApplication([
    // ...провайдеры уровня Environment (см. следующую главу инструкции)
  ]);
 
  await app.bootstrap(AppComponent);
})();

4. Портируйте зависимости главного модуля

Перенесите модули

Перенесите модули из секции imports главного модуля

  • в секцию imports главного компонента (исключая те, которые с вызовом forRoot()),
  • в createApplication с помощью importProvidersFrom() (включая те, которые с вызовом forRoot()):
// Старый подход
@NgModule({
  declarations: [AppComponent],
  imports: [
    SomeModuleOne,
    SomeModuleTwo,
    // ...другие модули
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
 
// Новый подход
@Component({
  imports: [
    SomeModuleOne, // Модули без `forRoot()` перемещаются на уровень `imports` компонента `AppComponent`
    SomeModuleTwo, // Модули без `forRoot()` перемещаются на уровень `imports` компонента `AppComponent`
    // ...другие модули
  ]
})
export class AppComponent extends QWebComponent {}
 
// main.ts
const app = createApplication([
  // ...
  importProvidersFrom(
    SomeModuleOne, // Помимо главного компонента, модули импортируются тут с помощью `importProvidersFrom`
    SomeModuleTwo, // Помимо главного компонента, модули импортируются тут с помощью `importProvidersFrom`
    SomeModuleThree.forRoot() // Модули с `forRoot` перемещаются в `importProvidersFrom`
  )
]);

Измените способ импорта модулей Q.Palette

Импортируйте модули Q.Palette с помощью provide*-функций (importProvidersFrom в данном случае не нужен):

import { provideQCore, provideQHttpNotifications } from '@diasoft/qpalette-core';
import { provideQAuth } from '@diasoft/qpalette-auth';
import { provideQPermissions } from '@diasoft/qpalette-permissions';
import { provideQUrls } from '@diasoft/qpalette-urls';
import { provideQFileViewer } from '@diasoft/qpalette-file-viewer';
import { provideQFormFlow } from '@diasoft/qpalette-form-flow';
import { provideQWebComponent, provideQWidget, provideQWidgetLoader } from '@diasoft/qpalette-visual';
import { provideQUrlIdempotency } from '@diasoft/qpalette-urls-addons';
 
const app = createApplication([
  // ...
  provideQCore(),      // Вместо QCoreModule.forRoot()
  provideQAuth(),      // Вместо QAuthModule.forRoot()
  provideQPermissions(), // Вместо QPermissionsModule.forRoot()
  provideQUrls(),      // Вместо QUrlModule.forRoot()
  provideQFileViewer(), // Вместо QFileViewerModule.forRoot()
  provideQFormFlow(),  // Вместо QPaletteFormFlowModule.forRoot()
  provideQWebComponent(), // Вместо QWebComponentModule.forRoot()
  provideQWidget(), // Вместо QWidgetModule.forRoot()
  provideQWidgetLoader(), // Вместо QWidgetLoaderModule.forRoot()
  provideQUrlIdempotency(), // Вместо QUrlIdempotencyModule.forRoot()
  provideQHttpNotifications(), // Вместо QHttpNotificationsModule.forRoot()
  // ...
]);

⚠️ Можно по-прежнему импортировать модули Q.Palette с помощью .forRoot() внутри importProvidersFrom, однако .forRoot()-методы устарели и будут удалены в ближайшем будущем.

Удалите ненужные конфигурации

  • По умолчанию теперь используется глобальная конфигурация (объект QCoreService.config). Если вам не нужна специфичная конфигурация, просто вызывайте provide*-функции без аргументов: provideQCore().
  • Если требуется своя конфигурация, передайте её в функцию: provideQCore({ apiBaseUrl: 'https://custom-api.example.com' }).
  • При задании пользовательской конфигурации она будет автоматически объединена с глобальной, при этом заданные вами значения будут иметь приоритет.

Измените способ импорта модулей Angular

Современный подход – использовать provide*-функции для включения функциональности Angular.

Импортируйте (если используются) модули анимации, роутера и HTTP платформы Angular в файл main.ts с помощью provide*-функций:

import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { ROUTES } from './app/routes.const';
 
const app = createApplication([
  // ...
  provideAnimations(), // Вместо BrowserAnimationsModule
  provideRouter(ROUTES), // Вместо RouterModule.forRoot(ROUTES)
  provideHttpClient(withInterceptorsFromDi()), // Вместо HttpClientModule
]);

Если предоставляется provideAnimations(), убедитесь, что BrowserAnimationsModule не импортирован нигде в приложении.

Если предоставляется provideHttpClient(), убедитесь, что HttpClientModule не импортирован нигде в приложении. Другие provide-замены HTTP-модулям Angular см. в документации (opens in a new tab).

Если предоставляется provideRouter(), убедитесь, что RouterModule.forRoot() не импортирован нигде в приложении (если используется что-то вроде AppRoutingModule, его тоже можно удалить после миграции на provideRouter()).

Некоторые другие Angular-зависимости также могут иметь свои provide*-функции. Подробности – в документации Angular.

Перенесите провайдеры

Перенесите провайдеры из главного модуля:

// Старый подход
@NgModule({
  providers: [
    // ...
    SomeProvderOne,
    SomeProviderTwo
  ]
})
export class AppModule {}
 
// Новый подход
const app = createApplication([
  // ...
  SomeProvderOne,
  SomeProviderTwo
]);

Проверьте секцию declarations главного модуля

Если в главном модуле присутствуют какие-либо компоненты, помимо AppComponent, нужно также убедиться, что они продолжат работу. Есть два варианта: либо сделать их Standalone согласно инструкциям Angular (предпочтительно), либо объявить их в других модулях.

Перенесите код из главного модуля (если есть) в главный компонент

Если в AppModule есть пользовательская логика – вынесите ее в главный компонент.

⚠️ Это не касается метода ngDoBootstrap – он больше не нужен.

5. Удалите AppModule

После перехода на Standalone можно безопасно удалить AppModule. Убедитесь, что все зависимости внутри него перенесены согласно инструкциям выше.

Локализация (экспериментально)

В новом режиме добавлена возможность включения локализации одной строчкой:

// main.ts
import { provideQLocalization } from '@diasoft/qpalette-visual';
 
const app = createApplication([
    provideQLocalization()
]);

При таком подходе:

  • Вызывать метод PrimeNGConfig.setTranslation() из пакета primeng и функцию registerLocaleData из пакета @angular/common больше не нужно.
  • Предоставлять провайдер LOCALE_ID из пакета @angular/core больше не требуется.
  • Локализация PrimeNG и Angular работает прямо "из коробки".

Если в продукте используется пакет @ngneat/transloco, то необходимо также использовать функцию withTransloco:

// main.ts
import { provideQLocalization, withTransloco } from '@diasoft/qpalette-visual';
 
const app = createApplication([
    provideQLocalization(
      withTransloco({
        translationsUrl: `/assets/<your-pbc-name>/i18n`
      })
    )
]);

Строку <your-pbc-name> нужно заменить на имя продукта, как это требовалось при старом подходе (opens in a new tab).

В этом случае файл transloco-root.module.ts (см. описание старого подхода (opens in a new tab)) больше не нужен, его можно удалить.

⚠️ Если локализация ранее не использовалась, то рекомендуем ознакомиться с инструкцией (opens in a new tab) с учетом обозначенных здесь изменений.

⚠️ Если используется любой другой пакет для переводов (например, @ngx-translate и подобные), рекомендуется произвести миграцию на @ngneat/transloco.

Важные предупреждения

⚠️ Не дублируйте provide*-функции! Каждая provide*-функция должна быть вызвана только один раз на корневом уровне приложения. Повторный вызов приведет к ошибке.

⚠️ Не используйте provide*-функции внутри своих модулей. Предоставляйте их только на корневом уровне приложения.

⚠️ Методы forRoot перестают работать после использования provide*-функций. После вызова provide*-функции соответствующий метод forRoot больше не будет иметь эффекта и может быть безопасно удален из кода.

Пример полной миграции

Старый подход (NgModule)

// app.component.ts
import { Component } from '@angular/core';
import { QWebComponent } from '@diasoft/qpalette-visual';
 
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent extends QWebComponent {
}
 
// app.module.ts
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { QAuthModule } from '@diasoft/qpalette-auth';
import { QCoreModule, QCoreService } from '@diasoft/qpalette-core';
import { QWebComponentModule } from '@diasoft/qpalette-visual';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
// import SomeModuleOne
// import SomeModuleTwo
// import SomeModuleThree
// import SomeProviderOne
// import SomeProviderTwo
 
@NgModule({
    declarations: [AppComponent],
    imports: [
        // Функциональность Angular
        BrowserAnimationsModule,
        HttpClientModule,
        AppRoutingModule,
        // Функциональность QPalette
        QCoreModule.forRoot(QCoreService.config.common),
        QAuthModule.forRoot(QCoreService.config.auth),
        QWebComponentModule.forRoot(QCoreService.config.bundleLoader, true),
        // Модули продукта
        SomeModuleOne,
        SomeModuleTwo,
        SomeModuleThree.forRoot()
    ],
    providers: [
        // Провайдеры продукта
        SomeProviderOne,
        SomeProviderTwo
    ],
    bootstrap: [AppComponent]
})
export class AppModule {
}
 
// main.ts
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module';
 
platformBrowserDynamic().bootstrapModule(AppModule);

Новый подход (Standalone)

// app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { provideSingleBundle, QWebComponent } from '@diasoft/qpalette-visual';
// import SomeModuleOne
// import SomeModuleTwo
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  standalone: true,
  imports: [
    // Модули, используемые в шаблоне
    RouterOutlet,
    // Модули продукта из `AppModule`
    SomeModuleOne,
    SomeModuleTwo
  ]
})
export class AppComponent extends QWebComponent {}
 
// main.ts
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter } from '@angular/router';
import { provideQAuth } from '@diasoft/qpalette-auth';
import { provideQCore } from '@diasoft/qpalette-core';
import { createApplication, provideQWebComponent } from '@diasoft/qpalette-visual';
import { AppComponent } from './app/app.component';
import { ROUTES } from './app/routes.const';
// import SomeModuleOne
// import SomeModuleTwo
// import SomeModuleThree
// import SomeProviderOne,
// import SomeProviderTwo
 
(async () => {
    const app = createApplication([
        // Функциональность Angular
        provideAnimations(),
        provideRouter(ROUTES),
        provideHttpClient(withInterceptorsFromDi()),
        // Функциональность QPalette
        provideQCore(),
        provideQAuth(),
        provideQWebComponent(),
        // Зависимости других модулей импортируются с помощью `importProvidersFrom`
        importProvidersFrom(
            // Модули продукта из `AppModule`
            SomeModuleOne,
            SomeModuleTwo,
            SomeModuleThree.forRoot()
        ),
        // Провайдеры продукта из `AppModule`
        SomeProviderOne,
        SomeProviderTwo
    ]);
 
    await app.bootstrap(AppComponent);
})();

Решение проблем

Проблемы с BrowserModule

После миграции можно получить ошибку в консоли:

RuntimeError: NG05100: Providers from the `BrowserModule` have already been loaded. If you need access to common directives such as NgIf and NgFor, import the `CommonModule` instead.

Ошибка говорит о том, что где-то в проекте уже импортирован BrowserModule или BrowserAnimationsModule – такие импорты нужно найти и удалить. Функция provideAnimations() является заменой BrowserAnimationsModule – достаточно одного такого вызова на уровне main.ts.

Проблемы с Router

После миграции можно получить ошибку в консоли:

RuntimeError: NG04007: The Router was provided more than once. This can happen if 'forRoot' is used outside of the root injector. Lazy loaded modules should use RouterModule.forChild() instead.

Это значит, что в проекте уже импортирован RouterModule.forRoot() – нужно его удалить. Функция provideRouter() является заменой RouterModule.forRoot() – достаточно одного такого вызова на уровне main.ts.

В случае использования модуля вроде AppRoutingModule – его тоже нужно удалить.

Пустой экран при загрузке бандла

См. раздел Если есть проблемы с загрузкой бандлов (opens in a new tab) инструкции по миграции на Q.Palette 7.

Не работают lazy-компоненты Angular

Standalone-компоненты Angular могут загружаться аналогично lazy-модулям (opens in a new tab), однако эта функциональность пока не работает – вопрос исследуется.

При этом lazy-модули полностью поддерживаются.

Ответы на частые вопросы

Мне нужно перевести весь продукт на Standalone или только главный компонент?

Только главный компонент. Другие компоненты могут быть переведены постепенно на Standalone или все разом с помощью миграции Angular (opens in a new tab).

Почему бы не использовать автоматизированную миграцию от Angular вместо данных инструкций?

По нескольким причинам:

  • Миграция Angular переводит на Standalone весь продукт. В данной инструкции – только главный компонент.
  • Миграция не учитывает нового способа с provide*-функциями.
  • У Angular свой способ загрузки приложений в main.ts, для Q.Palette этого недостаточно.
  • Автоматическая миграция все равно потребует вмешательства разработчика, чтобы довести до ума результат миграции.

Инструкция подходит для виджетов?

Нет, виджеты пока не поддерживаются, переводить их на Standalone не нужно. Однако они продолжат загружаться в проекты, переведенные на Standalone.

Это касается как обычных виджетов (которые наследуют класс QWidgetModuleAbstract), так и виджетов лукап-директив (которые наследуют класс QWebComponent).

Есть ли ограничения у новой технологии?

Каких-либо новых ограничений технология не накладывает. Однако есть ограничения функциональности, доступной только в режиме Standalone (см. Не работают lazy-компоненты Angular (opens in a new tab)).

Нужно ли дорабатывать или обновлять продукты, которые будут загружать мой продукт?

Сторонние продукты, которые будут загружать ваш продукт, не нуждаются в доработке.

Однако если наблюдаются проблемы с загрузкой вашего продукта в других продуктах – убедитесь, что они используют версию Q.Palette не ниже 7.4, так как в некоторых версиях младше наблюдались проблемы с загрузкой бандлов в целом.

Общей рекомендаций по-прежнему будет использовать последнюю версию Q.Palette (и в клиентском, и в вашем продукте).

Нужно ли обновлять рутовое?

Рутовое приложение не требует обновления.

Однако если наблюдаются проблемы с загрузкой вашего продукта – убедитесь, что версия рутового приложения не ниже 7.4, так как в некоторых версиях младше наблюдались проблемы с загрузкой бандлов в целом.

Общей рекомендаций по-прежнему будет использовать последнюю версию Q.Palette в рутовом приложении.