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

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

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

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

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

Введение

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

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

Эта инструкция поможет вам:

  1. Преобразовать главный компонент в Standalone.
  2. Перейти на новую систему регистрации приложений Q.Palette.

Термины

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

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

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

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

Убедитесь, что вы используете бета-версию Q.Palette версии не ниже 7.4.3-sb.4.

1. Предоставьте provideSingleBundle() для главного компонента

Для правильной работы Single Bundle добавьте provideSingleBundle() в провайдеры главного компонента:

// app.component.ts
import { provideSingleBundle } from '@diasoft/qpalette-visual';
 
@Component({
  // ...
  providers: [
    provideSingleBundle() // Добавьте провайдер в компонент
  ]
})
export class AppComponent {}

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

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

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 {}
 
// 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 } 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()
  provideQUrlIdempotency(), // Вместо QUrlIdempotencyModule.forRoot()
  provideQHttpNotifications(), // Вместо QHttpNotificationsModule.forRoot()
  // ...
]);

При новом подходе вызов provideQWebComponent() автоматически добавляет функциональность виджетов с той же конфигурацией (QWidgetModule.forRoot() и QWidgetLoaderModule.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(), // Вместо HttpClientModule
  provideHttpClient(withInterceptorsFromDi()) // или так, если используются интерсепторы
]);

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

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

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

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

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

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

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

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

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

5. Удалите AppModule

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

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

⚠️ Не дублируйте 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, минимальная версия)

⚠️ Такой подход работает, но не рекомендуется.

В данном подходе все зависимости импортируются с помощью importProvidersFrom без разбора.

// app.component.ts
import { HttpClientModule } from '@angular/common/http';
import { Component } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideSingleBundle, QWebComponent } from '@diasoft/qpalette-visual';
import { AppRoutingModule } from './app-routing.module';
// import SomeModuleOne
// import SomeModuleTwo
 
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss'],
    standalone: true,
    providers: [
        provideSingleBundle() // Добавление в провайдеры компонента
    ],
    imports: [
        // Функциональность Angular
        BrowserAnimationsModule,
        HttpClientModule,
        AppRoutingModule,
        // Модули продукта
        SomeModuleOne,
        SomeModuleTwo
    ]
})
export class AppComponent extends QWebComponent {
}
 
import { HttpClientModule } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideQAuth } from '@diasoft/qpalette-auth';
import { provideQCore } from '@diasoft/qpalette-core';
import { createApplication, provideQWebComponent } from '@diasoft/qpalette-visual';
import { AppRoutingModule } from './app/app-routing.module';
import { AppComponent } from './app/app.component';
// import SomeModuleOne
// import SomeModuleTwo
// import SomeModuleThree
// import SomeProviderOne,
// import SomeProviderTwo
 
(async () => {
    const app = createApplication([
        // Модули из `AppModule`
        importProvidersFrom([
            // Функциональность Angular
            BrowserAnimationsModule,
            HttpClientModule,
            AppRoutingModule,
            // Модули продукта
            SomeModuleOne,
            SomeModuleTwo,
            SomeModuleThree.forRoot()
        ]),
        // Функциональность QPalette
        provideQCore(),
        provideQAuth(),
        provideQWebComponent(),
        // Провайдеры продукта из `AppModule`
        SomeProviderOne,
        SomeProviderTwo
    ]);
 
    await app.bootstrap(AppComponent);
})();

Новый подход (Standalone, продвинутая и рекомендуемая версия)

В данном подходе:

  • вместо модулей используются provide*-функции.
  • импортируются только действительно нужные модули.
// 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,
  providers: [
    provideSingleBundle() // Добавление в провайдеры компонента
  ],
  imports: [
    // Модули, используемые в шаблоне
    RouterOutlet,
    // Модули продукта из `AppModule`
    SomeModuleOne,
    SomeModuleTwo
  ]
})
export class AppComponent extends QWebComponent {}
 
// main.ts
import { provideHttpClient } 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(),
        // Функциональность 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 – его тоже нужно удалить.

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

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

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

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

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

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

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

Все перечисленные в инструкции действия могут быть также проделаны и для виджетов, и для библиотечных продуктов.

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

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

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

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