В последнее время все больше компаний сталкиваются с необходимостью создания нескольких брендов на одном сайте или приложении. Такая задача может быть связана с различными целевыми аудиториями пользователей, регионами или продуктами. Реализация мультибрендинга может быть непростой задачей, особенно если у вас уже есть готовый проект.  В этой статье я расскажу, как на проекте с Angular добавить несколько брендов, как добавить разную для них логику, о преимуществах NX для этой задачи и важности архитектуры.  Эта статья будет полезна для всех, кто работает со сложным приложением или задумывается о его масштабировании. Способ добавления мультибрендинга сильно зависит от текущей конфигурации вашего проекта.  К примеру, если у вас классическое Angular приложение, то вы можете для второго бренда создать новый репозиторий и развивать его там независимо. Это позволит легко добавлять уникальные компоненты и стили, настроить независимый деплой. А общий код можно вынести в отдельный репозиторий и использовать в проектах как библиотеку. Однако этот подход имеет и недостатки: в зависимости от чистоты кода и продуманности архитектуры создание общих библиотек может быть непростой задачей.  При использовании монорепозитория NX переиспользование общего кода становится более простым и существенно упрощает реализацию общего функционала, хотя требует более структурированного подхода к ведению кода и немного усложняет CICD. Для проекта над которым я работаю вместе со своей командой мы построили мультибрендинг на NX. К моменту, когда появилась эта задача мы уже перешли с классического приложения на NX и проделали серьёзную работу по реструктуризации кода на библиотеки с горизонтальным и вертикальным делением по слоям. Если коротко, то горизонтальное и вертикальное деление по слоям - это два подхода к организации кода в приложении. Первое это …. Реализация монорепозитория позволила решить нам следующие задачи: Раздельные релизы  Независимые конфигурации приложений брендов Уникальные стили и брендинг приложений Переиспользование общих компонентов и логики Использование уникальных для брендов компонентов и логики Настройка независимой конфигурации роутинга приложений Дальше я коротко расскажу о настройке монорепозитория и используемые нами подходы, которые помогли сделать масштабируемое решение. Если у вас классическое Angular приложение, то перейти на nx можно по инструкции.  После этого надо создать приложения для брендов и перенести туда ваше приложение. Об этом ниже. Для каждого бренда создаётся отдельное приложение. Это даёт большую гибкость при настройке независимых релизов, уникальные конфигурации приложений и брендинг. NX предлагает удобный CLI для добавления приложения https://nx.dev/packages/angular/generators/application При добавлении приложения в папке apps создаётся папка с проектом и e2e тестами. В конечном итоге файловая структура будет выглядеть так: /apps/brand1 – приложение бренда 1 /apps/brand1-e2e – интеграционные тесты приложения бренда 1 /apps/brand2 – приложение бренда 2 /apps/brand2-e2e – интеграционные тесты приложения бренда 2 /libs/… – общие библиотеки После создания приложений необходимо расположить общий для них код в библиотеках NX.  Перед созданием библиотек я рекомендую заранее продумать горизонтальное и вертикальное деление вашего проекта. Вертикальное деление это деление по доменам, а горизонтальное – по слоям. NX позволяет для библиотек указать принадлежность к слоям и в ESLint настроить правила их импортов. Затем мы можем в хуке прекоммита и в пайпах GitLab настроить проверку импортов и циклических зависимостей. Эта тема довольно интересная и об этом я расскажу в отдельной статье. Создать библиотеку можно также через NX CLI https://nx.dev/packages/angular/generators/library Если в проекте определены правила импорта библиотек, то NX предоставляет инструмент, который будет следить за этим.  Для этого в файле project.json в свойстве tags необходимо описать теги приложения, которые отражают его принадлежность к бренду или слою, к примеру, добавим библиотеку слоя feature и правило, по которому слой page может импортировать feature: Затем в .eslint.json добавьте правила для enforce-module-boundaries: Приложения в NX можно настроить в файле конфигурации, они расположены по адресу apps/(brand1/brand2)/project.json.  При настройке уникальными значениями являются такие параметры как имена проектов, префикс, путь к проекту и сборке, другие значения могут быть переиспользованы. К примеру у проектов может быть общий index.html (хотя на практике удобнее иметь разные). Параметры проектов в основном повторяют друг друга, тк проекты очень похожи, основные настройки с учётом бренда привожу в таблице: Параметр Описание Значение для брендов name уникальное имя проекта brand1/brand2 sourceRoot путь к проекту apps/(brand1/brand2)/src prefix Префикс проекта (мы используем сокращённое имя бренда) brnd1/brnd2 targets/build/options/outputPath путь к сборке проекта dist/apps/(brand1/brand2)/browser targets/build/options/index путь к index.html apps/(brand1/brand2)/src/index.html targets/build/options/main Путь к main.ts apps/(brand1/brand2)/src/main.ts targets/build/options/polyfills Путь к polyfills.ts apps/(brand1/brand2)/src/polyfills.ts targets/build/options/tsConfig Путь к tsconfig.app.json apps/(brand1/brand2)/tsconfig.app.json targets/build/options/assets Статика {    "glob": "**/*",    "input": "libs/assets/src/lib/assets-ng",    "output": "./assets-ng/" }, – общая статика для обоих брендов {    "glob": "**/*",    "input": "apps/(brand1/brand2)/src/assets-( brnd1/brnd2)",    "output": "./assets-( brnd1/brnd2)/" } – уникальная статика бренда targets/build/options/styles Стили проекта apps/(brand1/brand2)/src/styles.scss  targets/build/options/stylePreprocessorOptions/includePaths Глобальные переменные стилей apps/(brand1/brand2)/src/styles/core/color Все скрипты для обоих приложений размещены в package.json, они дублируются с учётом специфики проекта. Вот пример скриптов для разработки:  "brand1:start": "nx serve brand1 -o", "brand2:start": "nx serve brand2 -o", Определение бренда внутри общих библиотек и работа с уникальными для бренда параметрами может быть достигнуто разными способами и зависит от задачи и архитектуры приложения. Мы используем такие подходы, как файлы конфигурации, токены, сервисы, роутинг, if/else (но в меру.) и другие подходы. Дальше я опишу их все. Добавление файла конфигурации позволяет хранить уникальные настройки для каждого из брендов и при этом не вносить логику его получения перед использованием нужных значений. В такую конфигурацию мы вынесли GTM ключ для аналитики, путь к assets, конфиг CDN и другие важные и повторяющиеся для каждого бренда параметры. Для настройки используются 3 файла конфигурации brand.config.ts. Один общий и 2 для брендов - в папках приложений: apps/(brand1/brand2)/src/brand.config.ts. Далее в project.json настроена замену общего файла конфигурации на нужный для бренда в параметре targets/build/configurations/${имя конфига}/fileReplacements:. Логика такая же как и с обычным файлом переменных окружения environment.ts. В мультибрендинге общие компоненты составляют бОльшую часть всех компонентов, они используются во всех брендах и являются ключевыми в приложении, так как позволяют существенно сократить время разработки.  Чтобы обеспечить повторное использование, общие компоненты должны быть максимально абстрагированы от особенностей брендов. Такими компонентами могут быть атомарные UI элементы или наоборот общие шаблоны страниц, они могут встраивать в себя уникальные для бренда компоненты и зависимости и для этого требуется особый подход, и об этом ниже. Наряду с общими компонентами часто требуется создавать уникальные для брендов компоненты. Они могут отличаться внешним видом, функционалом и другими специфичными для бренда параметрами. Важно понимать, что создание уникальных компонентов увеличивает время и сложность проекта.  Для их отображения чаще всего можно использовать общие компоненты или роутинг. Далее мы рассмотрим эти способы. Выбор подхода в получении компонента бренда влияет на читаемость кода, его переиспользование и масштабирование. Можно в каждом общем компоненте реализовать свою логику получения компонента и иногда это может быть оправдано, например, вот так: Но этот способ неудобен, такая реализация будет дублировать код из компонента в компонент, а это значит, что неизбежны ошибки и вообще DRY. Способ лучше это получить компонент через DI. DI позволяет эффективно и надёжно управлять зависимостями в компонентах брендов, изолировать специфичную для них логику, делать общие компоненты более чистыми, избегать дублирования кода и упрощать тестирование. Грамотная настройка DI в мультибрендинге позволяет избежать ошибок, связанных с неправильным управлением зависимостями и упрощает поддержку кода в дальнейшем. Давайте перепишем пример выше и получим компонент бренда через DI. Для этого определяем базовый интерфейс для компонента бренда, где описываем общие методы или свойства, которые должны быть реализованы: Создаём компоненты броендов и имплементируем общий интерфейс:   Создаём сервис BrandService, который будет предоставлять информацию о текущем бренде:   Делаем фабрику BrandComponentFactory и в неё внедряем BrandService, в результате она возвращает нужный компонент: В модуле, где определены компоненты бренда, указываем провайдер с использованием useFactory и внедряем BrandService: И в общем компоненте, через DI получаем компонент бренда: Далее в примерах я буду использовать этот способ получения компонента через DI. Также мы можем передать настройки специфичные для бренда. Для этого мы создадим токен: создадим конфиги для каждого бренда и далее запровайдим в app.module для каждого application: После этого мы сможет через токен получить нужное значение в общих или уникальных для брендов компонентах: Таким образом компоненту не придётся заботиться о проверке бренда, он получит конфиг в зависимости от сборки проекта. В сервисе бренды могут иметь уникальную бизнес логику, которую в свою очередь надо передать в общий компонент. Для этого мы должны создать токен и фабричную функцию, которая будет возвращать нужным нам сервис: Далее регистрируем функцию в провайдере сервиса, указав её в useFactory, а в deps передаём нужные фабрике зависимости: И затем добавляем провайдер в модуль или компонент:    И инжектим в конструктор токен: Этот подход позволяет избежать логики проверки бренда внутри компонентов и делает их чистыми. Каждое приложение имеет свой RoutingModule, это позволяет для одинаковых маршрутов указывать разные компоненты, уникальные для бренда. Также в Angular 15 появился routerGuard – CanMatch, он позволяет для одного маршрута в зависимости от условия загрузить нужный компонент, в нашем примере нужный для бренда: Ещё у роутинга есть возможность передавать компонент в нужный селектор, указанный в outlet:   Затем в шаблоне компонента LayoutCompoenent указываем router-outlet, где в атрибуте name задаём «header»: Так в общем компоненте через роутинг по селектору «header» для каждого бренда мы можем указать специфичный компонент хедера, где в свою очередь через ng-content мы можем передать уникальное для бренда содержимое. Подробнее об этом ниже. ng-content позволяет передавать содержимое внутрь компонента, что делает его более гибким и настраиваемым. В контексте мультибрендинга, мы используем это для настройки отображения содержимого компонентов в зависимости от конкретного бренда. Вот пример как настроить уникальные хедеры для брендов, используя общий компонент: Создаём HeaderComponent, который будет использовать ng-content:  В разметке шаблона родительского компонента HeaderComponent используем <ng-content></ng-content>, чтобы указать место, куда будет вставлено содержимое. Создаём компоненты для каждого бренда, которые будут использовать BrandComponent и предоставлять свое содержимое: Затем мы можем передать компоненты HeaderAComponent и HeaderBComponent через роутинг как было описано выше. В результате мы получаем хедер для бренда А: И для бренда Б: Angular может динамически создавать компоненты, это даёт нам возможность гибко управлять отображением и функциональностью общих компонентов в зависимости от бренда или других условий. Вот пример как динамически загрузить нужный компонент: Чтобы оптимизировать размер проекта можно лениво загружать компоненты, таким образом при сборке компонент попадает в отдельный бандл. Вот как выглядит метод ленивой загрузки: Мы пошли дальше и для удобства сделали директиву для ленивой загрузки компонентов. Компонент бренда или метод ленивой загрузки можно передать через DI, где через фабрику реализовать их получение в зависимости от бренда. Это опять же позволит делать компоненты чистыми. Директива *ngComponentOutlet позволяет динамически создавать компоненты в Angular. В контексте мультибрендинга она позволяет в общем компоненте отобразить уникальный компонент бренда, а также передать в него инжекты и контент, это делает её использование ещё более функциональным. К примеру, можно сделать общий компонент согласования, в нём мы будем показывать компонент нужного бренда, куда передадим 2 темплейта и инжект с необходимыми данными: Структурные директивы позволяют в зависимости от какого-либо условия показать нужный шаблон, вот несколько таких примеров: Стурктурная директива *ngIf на основании результата тернарного выражения позволяет указать один из шаблонов: *ngSwith на основнании сравнения со значением переменной может вернуть один из нескольких вариантов шаблона: *ngTemplateOutlet позволяет отрендерить один из шаблонов: Но надо помнить, что эти подходы усложняют логику компонента, снижают его возможность масштабирования и читабельность. Разделение стилей и статики является важным аспектом при разработке мультибрендинга. Каждый бренд может иметь свою уникальную цветовую палитру, изображения, шрифты и другие элементы визуального дизайна. Поэтому важно иметь возможность разделять стили и статические файлы для каждого бренда.  Далее мы рассмотрим способы их настройки. Бренды используют как общую статику, например, в виде иконок кнопок, так и уникальную, в виде брендовых изображений. Общую статику мы разместили в библиотеке libs/assets. Брендовую статику разместили в каждом приложении бренда, в своей assets по адресу apps/(brand1/brand2)/src/assets-(brnd1/brnd2).  Ссылка на оба assets задаётся в project.json в параметре targets/build/options/assets: Далее доступ к общей статике указываем ссылкой “assets-ng/”, к брендовой через файл конфигурации бренда brand.config.assets, где для каждого бренда мы указываем через enum нужный путь: Настройка переменных scss позволяет указать уникальных для бренда цвета, шрифты, отступы и другие важные для бренда настройки. К примеру, для общего компонента в стилях мы может задать background-image, передав адрес к брендовой картинке: background-image: url($backgroundImageUrl + $assetsUrl + 'icon-arrow-primary.svg'); Здесь: $backgroundImageUrl – задаёт относительную ссылку для среды разработки или ссылку на CDN в проде $assetsUrl – путь к assets бренда В общих деталях настройка $assetsUrl и $backgroundImageUrl выглядит похоже, но они разделены по разным файлам, тк у них разная причина изменения: $assetsUrl – зависит от бренда, а $backgroundImageUrl – от среды. Настройка $backgroundImageUrl: В проекте находятся 3 файла конфигурации: один для среды разработки и два - для прода. Для разработки: «libs/lu/core/shared/src/lib/environments/scss/_env-vars.scss» Для прода:  «apps/(brand1/brand2)/src/environments/scss-prod/_global-vars.scss» Настройка $assetsUrl: В каждом приложении бренда есть файл apps/(brand1/brand2)/src/environments/scss/_brand-vars.scss Внутри файла определены scss переменные: Все файлы с переменными SCSS задаются в project.json в параметре targets/build/options/stylePreprocessorOptions/includePaths: Далее оба файла надо прописать в mixins.scss В свою очередь mixins.scss добавляется в styles.scss бренда. SCSS функция может быть полезна, что бы задать уникальный для бренда стиль. Можно создать функцию для определённого стиля. Мы сделали общую функцию, которая на основании переменной scss переменной задаёт переданное ей свойство:  Используется она так: Для каждого бренда мы создали файл с конфигом своей цветовой палитры. Эти файлы находятся в папках приложений: apps/(brand1/brand2)/src/styles/color/colors.scss В файле определены переменные для цветов бренда. Названия переменных одинаковы для брендов. Также можно указать конфигурации цветов для основных объектов приложения, например, для кнопки: Это позволяет гибко настраивать цветовую схему для каждого из брендов. Конфиги добавлены в project.json в параметре targets/build/options/stylePreprocessorOptions/includePaths: Независимые релизы брендов позволяют обеспечить собственные циклы разработки и релизов, обеспечить масштабируемость за счёт независимости ресурсов. Мы настроили деплой через GitLab CI, в файле .gitlab-ci.yml описали шаги для развёртывания. Бренды имеют общие шаги по запуску unit-test’ов, lint’ов, сборке и независимые релизы на окружения: тестовые и на прод. Это позволяет убедиться, что новый функционал одинаково хорошо работает во всех сборках и при этом мы можем релизить только нужный бренд.  На проде в Kubernets деплой в прод происходит на разные поды, настроен автоскейлинг. Нагрузка на бренды от пользователей может быть разной и это совершенно их не аффектит. Мы используем Angular Universal и для кеширования статики используем Redis и здесь мы пошли по аналогичному пути – у каждого свой отдельный сервер со своими настройками по ресурсам.  Для мониторинга приложений мы используем Sentry и OpenTelemetry. Наличие нескольких приложений позволяет легко передать нужные настройки для подключения и настроить сбор логов для брендов отдельно, это упрощает мониторинг и выявление ошибок у клиентов. На выбор технической реализации мультибрендинга влияет архитектура вашего проекта и потребности компании. На примере нашего проекта я показал, что Angular и Nx являются эффективными инструментами для реализации мультибрендинга. Angular позволяет строить модульную и масштабируемую систему, а NX предоставляет инструменты для работы с несколькими приложениями в монорепозитории.  Помимо этого, есть важные темы для мультибрендинга - это SEO и архитектура приложения.  SEO затрагивает вопросы оптимизации брендов для поисковых систем, как правильно развести бренды в поисковой выдаче, выбор стратегии позиционирования брендов, разделение контента, фасилитацию.   Тема архитектуры описывает вопросы изоляции и переиспользование кода, гибкости и масштабируемости, управление зависимостями, иначе говорят, всё что позволяет успешно развивать приложение сейчас и в будущем. Обе эти темы очень большие и о них я расскажу в следующих статьях. Все приведённые в статье подходы мы успешно используем в нашем проекте. Напишите, что из примеров вы используете у себя на проекте или поделитесь своими интересными решениями.