composer vs npm: многомодульная разработка

    Последние года три-четыре я при программировании на PHP использовал composer для управления зависимостями приложения. Сейчас появилась потребность перейти на nodejs и, как следствие, настроить привычную для себя среду разработки. Благо, что я использую IDE PhpStorm, который позволяет работать и с PHP, и с JS. Особенностью проектов, в которых я участвую, является многомодульность. Функциональность разделяется между модулями не столько для повторного использования, сколько для уменьшения итоговой сложности приложения за счёт декомпозиции на слабосвязанные компоненты. В общем, для этих проектов нормально, когда в рамках решения одной задачи изменения вносятся в несколько модулей и коммитятся в несколько репозиториев.


    image


    При настройке nodejs-проекта я столкнулся с некоторыми особенностями, которые осложняют многомодульную разработку. Данная публикация родилась в процессе попытки разобраться с этими особенностями. Под катом взгляд PHP'шника на развёртывание nodejs-проекта.


    Структура демо-проекта


    Проект состоит из 3 модулей:


    • приложение: головной модуль, подключающий зависимости;
    • функциональный модуль: содержит функции, вызываемые из головного модуля;
    • базовый модуль: содержит функции, вызываемые из функционального модуля;

    image


    Код каждого модуля располагается на github'е:



    Дескрипторы модуля для соответствующих менеджеров зависимостей (composer.json и package.json) находятся в каждом модуле, т.е., каждый модуль можно разворачивать и как php-модуль, и как js-модуль. PHP-код и JS-код в модулях просто размещаются рядом, не пересекаясь друг с другом.


    Запуск приложения на выполнение:


    $ php index.php
    $ nodejs index.js

    Результат работы в обоих случаях:


    This is application.
    This is func module.
    This is base module.

    Цели


    Проект в рабочем окружении должен позволять:


    • отслеживать через IDE изменения в каждом модуле, участвующем в разработке;
    • при помощи IDE коммитить изменения в разные репозитории в рамках одного действия;
    • использовать отладчик для трассировки выполнения кода модулей;

    Развёртывание через composer


    Здесь всё привычно. В дескрипторе развёртывания (composer.json) приложения указываем адреса репозиториев с модулями и прописываем master-ветки модулей в качестве зависимостей с желаемой версией:


    {
      "require": {
        "flancer64/habr-cvsn-mod-base": "dev-master as 0.1.0",
        "flancer64/habr-cvsn-mod-func": "dev-master as 0.1.0"
      },
      "repositories": [
        {
          "type": "vcs",
          "url": "https://github.com/flancer64/habr-cvsn-mod-base"
        },
        {
          "type": "vcs",
          "url": "https://github.com/flancer64/habr-cvsn-mod-func"
        }
      ]
    }

    После выполнения команды:


    $ composer install

    В каталоге ./vendor появляются подкаталоги с модулями, в свою очередь содержащие .git-каталоги:


    • ./vendor/
      • ./flancer64/
        • ./habr-cvsn-mod-base/
          • ./.git/
        • ./habr-cvsn-mod-base/
          • ./.git/

    Т.е., composer сразу разворачивает зависимости в виде, пригодном для разработки (контроля версий). Остаётся только настроить IDE PhpStorm (поставить зависимые модули под контроль версий):


    image


    и можно отслеживать изменения во всех разрабатываемых модулях:


    image


    и одновременно коммитить все изменения в локальные репозитории:


    image


    и push'ить в удалённые:


    image


    С отладкой также нет проблем. Можем ставить точки останова (breakpoints) на любой строке кода базового модуля — после запуска приложения под отладчиком останов происходит там, где нужно:


    image


    В общем, привычное окружение, удобное, как домашние тапочки.


    Обычное развёртывание через npm


    В отличие от composernpm не предполагает, что в проекте какие-то модули из каталога node_modules могут находиться под контролем версий. Мы можем в дескрипторе развёртывания package.json указать, что модуль нужно подгружать из внешнего репозитория (например, с githib'а):


    {
      "dependencies": {
        "habr-cvsn-mod-base": "github:flancer64/habr-cvsn-mod-base",
        "habr-cvsn-mod-func": "github:flancer64/habr-cvsn-mod-func"
      }
    }

    но у нас нет опции, чтобы npm при этом создал локальный репозиторий для загруженного модуля (подкаталог .git).


    После выполнения команды:


    $ npm install

    зависимости загружаются и устанавливаются локально без возможности использования контроля версий:


    image


    (отсутствует подкаталог ./.git/ в ./node_modules/habr-cvsn-mod-base/)


    Зато отладчик останавливается в базовом модуле без проблем:


    image


    Развёртывание через npm с использованием опции link


    Чтобы модули из ./node_modules/ можно было держать под контролем версий, разработчики npm предлагают использовать опцию 'link'. Если коротко, то суть подхода заключается в том, что модули, которые нужно версионировать, клонируются в произвольное место на диске разработчика (допустим, в /home/alex/work/habr/), а затем линкуются в /usr/lib/node_modules/ при помощи команды:


    # npm link

    (мне понадобились права root'а для выполнения)


    После чего уже в проекте можно использовать команды:


    $ npm link habr-cvsn-mod-base
    $ npm link habr-cvsn-mod-func

    npm найдёт соответствующие модули в /usr/lib/node_modules/ и замкнёт на них соответствующие подкаталоги из ./node_modules/ проекта:


    $ ls -lh ./node_modules/
    total 0
    lrwxrwxrwx 1 alex alex 57 jūl  2 16:18 habr-cvsn-mod-base -> ../../../../../../usr/lib/node_modules/habr-cvsn-mod-base
    lrwxrwxrwx 1 alex alex 57 jūl  2 16:18 habr-cvsn-mod-func -> ../../../../../../usr/lib/node_modules/habr-cvsn-mod-func

    Модули в /usr/lib/node_modules/ сами, в свою очередь, являются ссылками на оригинальное местонахождение модулей:


    $ ls -lh /usr/lib/node_modules/
    ...
    lrwxrwxrwx  1 root root   39 jūl  2 16:18 habr-cvsn-mod-base -> /home/alex/work/habr/habr-cvsn-mod-base
    lrwxrwxrwx  1 root root   39 jūl  2 16:18 habr-cvsn-mod-func -> /home/alex/work/habr/habr-cvsn-mod-func
    ...

    И по своему постоянному месту "прописки" содержат локальный репозиторий:


    $ ls -lha /home/alex/work/habr/habr-cvsn-mod-base
    ...
    drwxrwxr-x 8 alex alex 4,0K jūl  2 16:18 .git
    ...

    Таким образом, мы можем настроить IDE на контроль изменений в зависимостях проекта:


    image


    Проблемы начинаются при попытке запустить приложение:


    $ nodejs index.js 
    internal/modules/cjs/loader.js:670
        throw err;
        ^
    
    Error: Cannot find module 'habr-cvsn-mod-base'
        at Function.Module._resolveFilename (internal/modules/cjs/loader.js:668:15)
        at Function.Module._load (internal/modules/cjs/loader.js:591:27)
        at Module.require (internal/modules/cjs/loader.js:723:19)
        at require (internal/modules/cjs/helpers.js:14:16)
        at Object.<anonymous> (/home/alex/work/habr/habr-cvsn-mod-func/src/index.js:3:14)
    ...

    Приложение видит залинкованный функциональный модуль, но сам функциональный модуль не видит залинкованный базовый. Для выхода из положения предназначен ключ '--preserve-symlinks' для nodejs:


    $ nodejs --preserve-symlinks index.js

    Добавляем ключ в команду запуска проекта в IDE:


    image


    Теперь запуск проходит, но есть проблемы с отладкой — не отрабатывают точки останова в зависимостях. Можно остановиться в головном модуле и спуститься по шагам в исходники зависимостей, но самой точки останова IDE PhpStorm в runtime не видит, хотя и отображает:


    image


    Разработчики IDE говорят, что должно работать, но не работает (кэш сбрасывал, IDE перезапускал).


    В общем-то, целью этой публикации был опрос js-коллег, как они выкручиваются в подобной ситуации, но в процессе написания статьи вырисовалась ещё одна комбинация для развёртывания проекта:


    Размещение исходников во внутренних каталогах npm-проекта


    Оказалось, что если для линковки использовать клоны модулей с github'а, созданные composer'ом в подкаталоге ./vendor/, а не завязываться на внешние по отношению к проекту каталоги, то js-скрипты запускаются без ключа --preserve-symlinks и, что для меня более важно, IDE PhpStorm видит точки останова внутри модулей. Т.к. нет смысла использовать composer лишь для клонирования модулей проекта, я использовал обычный git и склонировал исходники модулей в подкаталог ./own_modules. После чего повторил манипуляции из предыдущего пункта:


    • связал модули в подкаталоге ./own_modules/... с системной библиотекой /usr/lib/node_modules/;
    • связал модули в системной библиотеке с проектом;
    • настроил IDE PhpStorm на работу с локальными репозиториями в подкаталоге ./own_modules/;

    image


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


    Резюме


    Сравнив два подхода к сборке многомодульных приложений (PHP с composer'ом и JS с npm), могу сделать вывод, что composer более дружелюбен к разработчикам, чем npm. Возможно, что разработчики composer (первый релиз в 2012) учли опыт разработчиков npm (первый релиз в 2010). Тем не менее, при некоторых дополнительных усилиях, npm также предоставляет возможность разрабатывать многомодульные приложения в довольно комфортных условиях.


    Командные скрипты для развёртывания проекта в различных режимах:


    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 3

      0

      А не исследовали как yarn себя ведёт на таких задачах? Обычно он более дружелюбен к разработчикам чем npm.

        +1

        Нет, насколько я понял, после того, как npm ускорился после выхода yarn'а, интерес к yarn'у значимо упал. А меня интересовал наиболее типовой подход для сборки nodejs-приложения.

        +1

        Не буду оспаривать фичу одновременного коммита в несколько репозиториев, которая имхо довольно рисковая даже если есть хорошие тесты, но вообще-то у composer есть рекомендуемая схема для вложенных пакетов


        Вместо


          "repositories": [
            {
              "type": "vcs",
              "url": "https://github.com/flancer64/habr-cvsn-mod-base"
            }
          ]

        Использовать


          "repositories": [
            {
              "type": "path",
              "url": "/local/path/to/flancer64/habr-cvsn-mod-base"
            },

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


        Кроме того для git-хабов (github, gitlab, bitbucket и т.п.) есть другая рекомендуемая схема подключения кастомных репозиториев


              "type": "git",
              "url": "github.com:flancer64/habr-cvsn-mod-base.git"

        в этом случае работа идет через ssh и настройки подключения определяются на уровне пользователя а не конкретного проекта. Например, на винде при подлючении пакета через https будет проверяться отдельная схема авторизации в git-хабе


        ну и совсем мелочь, для composer есть парочка интересных плагинов, которые, кажется, не очень хорошо работают… однако предназначены для чего-то подобного, если под многомодульностью понимать аналог многомодульности из gradle а не, что в случае композера называется зависимость


        1. wikimedia/composer-merge-plugin
        2. beberlei/composer-monorepo-plugin

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

        Самое читаемое