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
Из чего состоит nx
  • 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 оставлять "пустым", т.к. это убыстряет развертывание приложений.

Keeping the applications independent allows them to be deployed on different cadences, which is the whole point of MFEs.

Переменные окружения (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.

Если есть дополнения или исправления к данной статье, готов обсуждать.

Комментарии (0)