Nx - это инструмент для работы с фронтендом, который позволяет упростить совместную работу над проектом нескольким командам.
Nx позволяет генерировать код, автоматизировать процессы сборки, тестирования, а также позволяет управлять зависимостями эффективно. Ко всему прочему, nx имеет множество плагинов, которые позволяют использовать в проекте популярные фреймворки из коробки: Angular, React, Vue.js.
За счет модульности кода, которую предлагает nx, улучшается переиспользуемость кода, а также позволяет разным микрофронтам переиспользовать повторяющийся код.
Типы проектов nx:
Package-Based (основанный на пакетах): каждый проект в рабочей области является отдельным независимым пакетом. Каждый пакет имеет свой собственный набор зависимостей, скрипты. Публикуются они как отдельных пакеты.
Integrated (интегрированный): несколько проектов объединяются в одну рабочую область и взаимодействуют друг с другом. Проекты в рабочей области могут зависеть друг от друга, обмениваться кодом, ресурсами и настройками, их зависимости управляются централизованно, в package.json файле всего репозитория.
Standalone (изолированный): это проект, который может функционировать и использоваться независимо от других проектов или компонентов в системе. Такие проекты чаще всего создаются для того чтобы попробовать инструмент nx, а микрофронтендов нет. У нас только есть одно приложение. Следую рекомендациям nx при разработки приложений, у нас есть возможность в бущущем перейти к integrated проекту.
При создании новых библиотек, nx создает алиасы путей paths в файл tsconfig.base.json, это позволяет использовать абсолютные импорты. Также переопределять baseUrl в проектах не получится, т.к. алиасы будут не корректные. Потому что они относительно корня репозитория создаются. Следую подходу создания алиасов в tsconfig.base.json, у нас будет корректно отрабатывать в webstorm автоматический импорт. А также eslint @nx/enforce-module-boundaries. Если использовать npm workspaces, то возникнут проблемы.
Файл nx.json
Одни проекты могут зависеть от других проектов, где изменения в одном проекте влиет на другие проекты. Например, Стандартный Tasks Runner nx, позволяет кэшировать определенные операции в проектах.
Указывается он в файле nx.json в корне репозитория:
{
...
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test", "storybook"],
"parallel": 5
}
}
}
...
}
Благодаря графу зависимостей у нас проекты (А), которые зависят от других(Б), являются affected проектами. Например если в проектах Б обновился кэш, то в проектах А будут выполнены операции повторно.
Настраивается это поведение глобально через файл nx.json:
{
...
"namedInputs": {
"default": ["{projectRoot}/**/*"],
"production": [
"default",
"!{projectRoot}/.eslintrc.json",
"!{projectRoot}/.stylelintrc.json",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/jest.config.[jt]s",
"!{projectRoot}/storybook/**/*",
"!{projectRoot}/**/*.md",
"!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)"
]
},
"targetDefaults": {
"build": {
"inputs": ["default", "^production"]
},
"stylelint": {
"inputs": [
"{projectRoot}/**/*.styles.ts",
"{workspaceRoot}/.stylelintrc.json",
"{workspaceRoot}/.stylelintignore",
"{projectRoot}/.stylelintrc.json"
]
},
"test": {
"inputs": [
"default",
"^production",
"{workspaceRoot}/jest.preset.js",
"{workspaceRoot}/jest.config.js",
"{projectRoot}/jest.config.ts"
]
}
}
...
}
projectRoot - директории проекта, из которого выполняется задача.
workspaceRoot - директория всего репозитория.
namedInputs
namedInputs - это именованные входные данные, на которые ссылается в targetDefaults.
где default это базовые входные данные.
production - данные для "target", если необходимо обновлять кэш в "урезанном" виде. В приведенном выше примере у нас исключаются файлы конфигов, тесты и storybook файлы. Исключение из выборки указывается через восклицательный знак "!".
Названия default и production могут быть любыми, просто здесь они отражают бизнес логику больше.
targetDefaults
targetDefaults используют эти именованные "входные данные". Например, build любого проекта будет учитывать только собственные кэш и кэш зависимых проектов с входными данными production. Значок "^" как раз означает, зависимые проекты. Зависимые проекты могут быть вычислены при прямом импорте в коде, так и при указании в файле project.json проекта, свойство - implicitDependencies.
Файл project.json
project.json является файлом проекта, который подхватывает nx и учитывает в графе зависимостей. В свойство targets указываются задачи, которые можно выполнить через команды nx.
{
"name": "my-main",
...
"targets": {
"build": {
"executor": "my-plugin:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"outputPath": "dist/apps/my-main"
},
"configurations": {
"development": {},
"production": {}
}
},
"static": {
"executor": "my-plugin:static",
"defaultConfiguration": "production",
"options": {
"buildTarget": "my-main:build"
},
"configurations": {
"development": {},
"production": {}
}
}
}
...
}
configurations должны быть указаны, даже если у нас отсутствуют параметры для них, иначе будет браться defaultConfiguration, даже если явно укажем с какой конфигурацией запускаем задачу: nx build my-main -c=development
name в файле указывает на имя проекта, к которому и ссылаемся в команде выше.
"my" - является npm scope, а он предназначен, для того чтобы свои пакеты имели уникальное название, т.к. в npm registry могут быть пакеты со сходными именами.
outputs - это файлы, которые будут попадать в кэш.
executor - это исполнитель, который берется в данном примере из плагина my-plugin. После двоеточия идет название executor.
Рабочее пространство nx
Ключевое понятие в монорепозитории - рабочее пространство (workspace), он объединяет множество проектов. Это пространство обеспечивает единое управление зависимостями, общие настройки и инструменты. Что в конечном итоге позволяет разным командам микрофронтов иметь общее окружение. В принципе, все плюсы и минусы, которые относятся к монорепозиторию, относятся и к workspace, просто это термин которым называют "пространство", в котором находится наше приложение.
nx console - расширения по интерактивному запуску различных команд для VSCode, IntelliJ, VIM
nx cloud - в основном распределенный кэш, а также распределенный запуск задач по разным "машинам". Позволяет ускорить выполнение задач на устройстве разработчика, а также ускорить процессы CI/CD за счет использования кэша.
-
plugins - различные плагины, которые позволяют упростить работу с монорепозиторием, добавить дополнительную логику в окружение приложения. Плагины могут содержать в свою очередь:
generators - позволяют создавать что-то или менять в проекте. Например, новое приложение, библиотеку в проекте. Позволяет упростить создание нового микрофронта используя заготовленный шаблон.
executors - "исполняемые" скрипты, которые используются в библиотеках, приложениях, для запуска определенной задачи в приложении. Например, сборка, тестирование, проверка качество кода, запуск микрофронта в режиме разработки.
code migrations - скрипты для автоматизации миграции с одной версии библиотеки на новую. Также миграции могут изменять исходных код приложения, чтобы при обновлении библиотеки, приложение не поломалось.
devkit (@nx/devkit) - пакет "разработчика" для работы с nx, полезен для написания собственных плагинов.
-
nx - сама библиотека nx, которая предоставляет следующие возможности:
Task Running - выполнение команд (задач). Приложение или библиотека могут запускать разные команды, как напрямую, так и используя executors.
Distribution - связан с nx cloud, позволяет выполнять удаленные команды.
-
Workspace analysis - в проекте nx формирует граф завизимостей, для более быстрого запуска команд и для анализа сброса кэша. Позволяет визуализировать зависимости проекта через команду
nx graph
Ведь если микрофронтенды используют собственную библиотеку в монорепе, мы хотим иметь актуальную версию микрофронтов, а не доставать из кэша. Т.к. кэш уже не актуальный, из-за того, что зависимость, в лице библиотеки, поменялась.
Caching - локальное кэширование executors, кэшируется результат вывода в консоль и выходные файлы.
-
Code generation - включает в себя как plugins generators, так и update generator (который вызывается после обновления nx).
Через code generation мы запускает как собственные generators, которые для своих потребностей "напишем", так и сторонних разработчиков.
Automatic migration - позволяет обновить "экосистему" nx. Запускается обновление пакетов, вызываются update generators, если потребуется.
nx позволяет запускать команды как в самом корне проекта, тогда нам необходимо указывать название приложения (библиотеки), либо в самой директории проекта без указания названия: nx build app
или в директории app: nx build
При разработки собственных плагинов или для уточнения некоторых особенностей использования инструментов nx, не бойтесь ковыряться в исходниках.
Исходники полезны чтобы понять как именно можно создавать свой плагин с executors и/или generators.
CI/CD
При написании скриптов развертывания надо использовать nx affected команды, чтобы при изменении библиотек, приложения которые от них зависят: проверялись на качество кода, проходили тесты, разворачивались и т.д. Примеры ci файлов можете найти здесь.
Микрофронтенды в nx
В файле webpack.config.js мы можем вызывать задействовать ModuleFederationPlugin самого webpack через вызов ф-ции withModuleFederation, разработчики определенные настройки для этого плагина делают за нас. Но данная ф-ция только есть для react и angular. Если не устраивает их ф-ция, то можно напрямую взаимодействовать сModuleFederationPlugin
в файле конфигурации.
Также разработчики nx предусмотрели обновление графа зависимостей от файла module-federation.config.js самого проекта. Полный пример можно посмотреть тут.
module.exports = {
name: 'shell',
remotes: ['shop', 'cart', 'about'],
};
Например эта команды запускает в режиме разработки shell, а также cart и shop. Если не указывать devRemotes, то остальные микрофронты будут отдаваться статикой, т.е. на изменения их кода запущенные микрофронты не будут реагировать, за исключением shell.
Shell в данном случае выступает как host приложения для shop, cart, about.
nx serve shell --devRemotes=cart,shop
Так работает у нас статичный запуск module federation.
Есть и динамический запуск, тогда remotes у host приложения должны быть пустыми. Делается это по разным причинам, одна из них это динамический адрес микрофронта, тогда придется в webpack.config файле описывать логику загрузки модуля. Другая причина - подгружать микрофронтенды по требованию (т.е. на запускать сразу все приложения при старте приложения и загружать все модули сразу). В данном случае можно отказаться от использования ф-ции withModuleFederation, а обращаться напрямую к ModuleFederationPlugin.
Но с другой стороны, сами разработчики nx утверждают, что лучше remotes оставлять "пустым", т.к. это убыстряет развертывание приложений.
Переменные окружения (env)
Работа с переменными окружения такая же как и в любом webpack приложении.
Для загрузки переменных окружения используются два подхода:
Использовать файлы в папке environments
Для каждой среды создаем свой файл кофигурации, например environment.prod.ts. environment.ts. Соответственно файл для продуктового и тестового стендов. Замена файла при необходимости будет осуществляться в момент сборки / разработки.
Логика замены описывается в файле package.json проекта:
"fileReplacements": [
{
"replace": "apps/products/src/environments/environment.ts",
"with": "apps/products/src/environments/environment.prod.ts"
}
]
Для данного способа использования в коде приложения переменных окружения будет простым - простой импорт файла environment.ts
Использование файлов .env, либо загрузка из переменных окружения ОС.
Для загрузки и использования в приложении необходимо использовать плагин webpack.DefinePlugin.
Рекомендации по разработке приложений
Микрофронтенды лучше всего делить по бизнес ценностями, по независимым частям, которые можно отдать разным командам для независимой разработки. Иначе мы скатимся к сотнями и тысячам микрофронтам, если мы будем помещать каждый компонент как отдельное приложение.
Если у нас есть общая логика микрофронтендов, мы помещаем их в библиотеку. Чтобы переиспользовать безболезненно между микрофронтендами.
Создание микрофронтенд приложений должно исходить от бизнеса, но при содействии разработчиков.
Согласно правилу команды nx: 20% кода находится в приложениях, остальные 80% в библиотеках. Это может вызвать определенную трудность к перестройству к подобному подходу, но это позволяет нам переиспользовать код и уменьшать связанной внутри того приложения, где используется этот код, плюс упрощает покрытие тестами. А так как у нас множество маленьких библиотек, у нас эффективность кэширования повышается и соответственно скорость сборки за счет того, что у нас собираются маленькие проекты, а не одно большое.
Чтобы уменьшить размер приложения желательно использовать только одну библиотеку по работе с фронтендом: angular или react или что-то еще. Хотя микрофронтенды это позволяют, но это считается плохой практикой в монорепе.
В следующей статье разберу как создавать свой плагин, executor и generator.
Если есть дополнения или исправления к данной статье, готов обсуждать.