
Всем привет!
Меня зовут Илья Чубко, я — технический архитектор в направлении, которое занимается внедрением CRM-системы на low-code платформе BPMSoft для автоматизации и управления бизнес-процессами крупных и средних компаний в единой цифровой среде вендора «БПМСофт».
В статье «Как с помощью Angular доработать CRM-систему: наш опыт с BPMSoft» мы уже описывали подход к разработке визуальных компонентов и их интеграции в CRM-систему.
А что, если таких компонентов становится всё больше, они зависят друг от друга и используются как строительные блоки в архитектуре платформы и создаваемых на ней решений?
В этой статье я поделюсь опытом, как мы в К2Тех построили единую экосистему Angular-компонентов — с общей инфраструктурой, управлением зависимостями и автоматической сборкой на базе монорепозитория Nx.
Почему монорепозиторий?
При создании большого количества Angular-компонентов обычно начинают с отдельных репозиториев. Это удобно на первых этапах разработки, но по мере роста системы возникают типичные проблемы: несогласованность версий библиотек, дублирование кода и конфигураций, усложнение сборки.
Монорепозиторий решает эти задачи, объединяя всё в единую архитектуру и обеспечивая общие правила для разработки и сборки приложений.
Ниже — краткое сравнение классического подхода и монорепозитория:
|
Критерий |
Несколько отдельных репозиториев |
Монорепозиторий (Nx) |
|---|---|---|
Структура проекта |
Каждый проект в отдельном Git-репозитории |
Все приложения и библиотеки хранятся в едином workspace |
Управление зависимостями |
Разные версии библиотек, дублирование пакетов |
Единый |
Повторное использование кода |
Ограничено, общий код приходится копировать |
Любая библиотека доступна другим проектам внутри workspace |
Сборка и тестирование |
Отдельные пайплайны, дублирование конфигураций |
Общие правила сборки и тестов для всех пакетов |
CI/CD |
Каждая сборка запускается отдельно |
|
Кэширование и производительность |
Нет кэширования между проектами |
Встроенное кэширование сборок и тестов |
Контроль архитектуры |
Нет связей между проектами |
Визуализация зависимостей, контроль импортов |
Правила оформления и анализа кода (eslint, prettier) |
Настройки копируются вручную |
Общие конфигурации линтера и форматтера |
Обновление зависимостей |
Нужно обновлять в каждом проекте отдельно |
Обновление выполняется один раз для всех |
Масштабирование |
Трудно поддерживать и развивать |
Оптимально для десятков и сотен библиотек |
Порог входа |
Низкий, легко начать |
Требует единовременной настройки, затем проще сопровождать |
Установка и подготовка Nx
Если Nx ещё не установлен, добавим CLI глобально через npm
npm install -g nx
Создаем workspace командой npm или nx:
npx create-nx-workspace@latest my-workspace --preset=angular-standalone
nx create nx-template --preset=angular-standalone
Выбираем стиль, движок тестирования и даже ai agent. Можно создать workspace без интерактива следующей командой:
npx create-nx-workspace@latest nx-template --preset=angular-standalone --bundler=esbuild --style=scss --unitTestRunner=vitest --e2eTestRunner=none --ssr=false --nxCloud=skip --aiAgents=gemini
Структура директорий
Для начала необходимо определиться с префиксом компонентов, модулей, приложений. Для примера буду использовать одинаковый префикс tmp.
Для организации workspace рекомендую использовать следующую структуру в корне приложения
apps/
├─ tmp-app1/
├─ tmp-app2/
libs/
├─ core/
└─ shared/
├─ tmp-lib1/
├─ tmp-lib2/
└─ tmp-lib3/
Создание приложения
Приложения (apps) — это готовые проекты, которые собираются и разворачиваются как самостоятельные продукты.
Приложения могут обращаться к общим библиотекам, но не должны дублировать их функциональность.
Для создания приложения используем встроенный генератор Nx:
npx nx g @nx/angular:app /apps/tmp-app --prefix=tmp --bundler=esbuild --ssr=false
Пример минимальной структуры приложения внутри директории apps:
apps/
└─ tmp-app/
├─ config/
├─ public/
└─ src/
├─ components/
│ └─ app.ts
├─ index.html
└─ styles.scss
└─ project.json
Чтобы запустить приложение, необходимо выполнить команду:
npx nx run <application>:<target>:<environment>
npx nx run tmp-app1:serve:development
После вызова serve запустится первоначальный Hello World на http://localhost:4200
Создание библиотеки
Библиотеки (libs) — это наборы модулей, компонентов и сервисов, предназначенные для совместного использования между приложениями.
Каждая библиотека изолирована, имеет собственный project.json и может использоваться в других проектах внутри workspace. Можно создать отдельные группировки, например, core и shared, по своему усмотрению. Библиотеки лежат в основе масштабируемой архитектуры.
Создадим первую UI-библиотеку с префиксом tmp и поместим её в libs/shared
npx nx g @nx/angular:lib libs/shared/tmp-lib1 --buildable --prefix=tmp --inline-style --skip-tests --unit-test-runner=none
Пример рекомендуемой структуры библиотеки:
tmp-lib1/
├─ src/
│ ├─ fonts/
│ └─ lib/
│ ├─ components/
│ │ ├─ index.ts
│ │ ├─ tmp-lib1-handler.ts
│ │ └─ tmp-lib1-wrapper.ts
│ ├─ directives/
│ │ ├─ index.ts
│ │ └─ example.directive.ts
│ ├─ models/
│ │ ├─ index.ts
│ │ └─ example.model.ts
│ ├─ ngrx/
│ │ ├─ index.ts
│ │ └─ example.store.ts
│ ├─ pipes/
│ │ ├─ index.ts
│ │ └─ example.pipe.ts
│ └─ tokens/
│ ├─ index.ts
│ └─ example.token.ts
│ └─ index.ts
├─ ng-package.json
├─ package.json
├─ project.json
└─ tsconfig.json
В корне index.ts содержит все блоки, которые мы будем экспортировать
export * from "./lib/directives";
export * from "./lib/components";
export * from "./lib/pipes";
export * from "./lib/models";
export * from "./lib/tokens";
export * from "./lib/ngrx";
А внутри каждого блока, например, components содержится список схем в index.ts:
export * from './tmp-lib1-handler';
export * from './tmp-lib1-wrapper';
А сами компоненты можно создавать в виде одного ts файла для компактности.
Например, файл tmp-lib1-wrapper.ts выглядит следующим образом:
import { Component } from '@angular/core';
@Component({
selector: 'tmp-lib1-wrapper',
imports: [],
template: `<p>Hello I'm tmp-lib1</p>`,
styles: ``,
})
export class TmpLib1WrapperComponent {}
Использование библиотек в приложениях
После создания библиотек их можно подключать в любые приложения внутри workspace.
Nx автоматически подхватывает структуру проектов и позволяет использовать библиотеки без дополнительных настроек.
Подключение библиотеки выполняется аналогично добавлению любого компонента
import { Component } from '@angular/core';
import { TmpLib1WrapperComponent } from '@nx-template/tmp-lib1';
@Component({
imports: [TmpLib1WrapperComponent],
selector: 'tmp-root',
template: `
<tmp-lib1-wrapper/>
`,
styles: ``,
})
export class App {}
Обратите внимание, что импорт делается не через абсолютную или относительную ссылку, а через контекст workspace. В нашем случае это @nx-template
import { TmpLib1WrapperComponent } from '@nx-template/tmp-lib1';
Чтобы отобразить зависимости компонентов внутри приложения и связи между проектами, необходимо выполнить следующую команду:
nx graph
Пример графа изображён на следующем рисунке:

Интеграция и применение в сторонних системах
Предположим, что ваше приложение должно по-разному обрабатывать аутентификацию, пути к сервисам или получать данные — в зависимости от того, где оно запущено: как самостоятельное приложение или как Custom Element в CRM-системе. Вместо того, чтобы загромождать код условными операторами, мы можем использовать DI для «внедрения» нужной реализации сервиса для каждого конкретного случая.
Внедрение зависимостей (DI) — это основной шаблон проектирования в Angular, который позволяет компонентам, сервисам и другим частям вашего приложения получать свои зависимости извне, вместо того, чтобы создавать их самостоятельно.
Подготовка
Для разделения логики скорректируем структуру проекта и определим разные конфигурации:
tmp-app/
└─ config/
├─ app/
│ ├─ app.config.ts
│ ├─ main.ts
│ └─ tsconfig.app.json
└─ ce/
├─ app.config.ce.ts
├─ main.ce.ts
└─ tsconfig.ce.json
В main.ts описываем точку входа нашего приложения, например, для Сustom Element файл main.ce.ts будет выглядеть следующим образом:
import { createApplication } from "@angular/platform-browser";
import { createCustomElement } from "@angular/elements";
import { AppComponent } from "../../src/components/app";
import { appConfig } from "./app.config.ce";
createApplication(appConfig)
.then((appRef) => {
const injector = appRef.injector;
const customElement = createCustomElement(AppComponent, { injector });
customElements.define("tmp-ce", customElement);
})
.catch((err) => console.error(err));
Дальше нужно определить структуру, которая будет меняться. Для этого создадим модель данных в файле app-config.model.ts, где будем хранить имя приложения и путь к директории public:
export interface AppConfig {
appName: string;
publicPath: string;
}
Для того, чтобы использовать всю мощь DI, создадим так называемый InjectionToken.
В данном коде мы определили структуру и сразу же указали значение по умолчанию через factory:
import { InjectionToken } from "@angular/core";
import { AppConfig } from "../model/app-config.model";
export const APP_CONFIG = new InjectionToken<AppConfig>("APP_CONFIG", {
providedIn: "root",
factory: (): AppConfig => ({
appName: "Angular",
publicPath: "../public",
}),
});
InjectionToken — это специальный объект в Angular, который используется как уникальный ключ для регистрации и получения зависимостей через DI.
После этого остаётся последний шаг: определить конфиг для Custom Element в файле app.config.ce.ts:
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from "@angular/core";
import { APP_CONFIG } from "../../src/tokens/app-config.token";
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
{
provide: APP_CONFIG,
useValue: {
appName: "BPMSoft Angular",
publicPath: "../public",
},
},
],
};
Использование
Для применения конфига в шаблоне достаточно внедрить токен через inject и добавить в html, например, с помощью интерполяции:
@Component({
imports: [TmpLib1WrapperComponent, TmpLib2Component],
selector: "tmp-root",
template: `
<h1>{{ appConfig.appName }}</h1>
`
})
export class AppComponent {
appConfig = inject<AppConfig>(APP_CONFIG);
}
Результат
В итоге приложение показывает заголовок Angular, а при использовании внутри CRM — BPMSoft Angular


Результаты
В статье я привёл упрощённые примеры, сфокусированные на ключевых принципах построения экосистемы: разделении ответственности, использовании библиотек, конфигурировании сборок и внедрении зависимостей. Этого достаточно, чтобы понять общие подходы и начать использовать монорепозиторий на практике.
В реальном проекте такие решения дополняются более широким набором инструментов и технологий:
CI/CD — автоматизация сборок и проверок через
nx affected, что позволяет пересобирать только изменённые части workspace;NgRx Signal Store — хранение состояния, работа через сигналы и вычисляемые значения;
Tailwind CSS — единая система стилей и токенов, ускоряющая работу с UI-компонентами;
Vite — быстрый dev-сервер и удобная сборка;
Vitest — современный тестовый раннер с высокой скоростью выполнения.
На практике экосистема получается значительно шире, чем базовые примеры.
Но фундамент остаётся тем же: единый monorepo-workspace, чёткая структура, переиспользуемые библиотеки и продуманная конфигурация.
Отдельное приложение можно создать, например, для описания и отображения элементов системы, по типу UI kit. Дополнительно в него можно включить:
выбор темы и цветовой схемы;
выбор размера шрифта;
переключение между светлой и тёмной темой.


Заключение
Монорепозиторий, в частности Nx, подходит не для всех проектов.
Если компонентов немного и зависимости между ними минимальны, такой подход будет избыточным: он требует общей инфраструктуры, единых правил и поддержки структуры workspace. В небольших проектах удобнее работать с отдельными репозиториями.
Но когда количество компонентов растёт, а их взаимосвязи становятся сложнее, преимущества монорепозиторной архитектуры становятся заметными. Она помогает централизовать зависимости, снизить дублирование кода и упростить обновление библиотек. Сборка, тестирование и интеграция становятся более управляемыми и предсказуемыми.
Если поддержка множества отдельных компонентов начинает занимать всё больше времени, монорепозиторий может значительно упростить разработку и сопровождение системы.