Начало любого JavaScript проекта сопровождается амбициозным намерением - использовать как можно меньше npm пакетов в процессе разработки. Но сколько бы усилий мы не предпринимали, рано или поздно пакеты начинают накапливаться. Со временем строк в package.json становится всё больше, а благодаря package-lock.json пул реквесты приобретают все более устрашающий вид со всеми своими дополнениями или удалениями в процессе добавления зависимостей.
"И нормально", - скажет лидер команды разработчиков, а все остальные только кивнут в ответ. А что еще остается делать в такой ситуации? Ведь у нас есть счастливая возможность наблюдать, как оживает и процветает экосистема JavaScript. Нам не нужно каждый раз изобретать колесо и ломать голову над вопросами, которые уже давно решены сообществом открытого ПО.
Предположим, вы захотели создать блог и выбрали для этого Gatsby.js. Попробуйте установить и сохранить его в число ваших зависимостей. Поздравляю! Вместе с этим фреймворком вы только что получили 19000 дополнительных зависимостей. Как вам такой подарок? До какой же степени может разрастаться дерево зависимостей JavaScript? Как же мы оказываемся в аду зависимостей? Давайте копнем поглубже и выясним.
Что же такое пакет JavaScript?
npm, менеджер пакетов, входящий в состав Node.js, содержит самый полный реестр пакетов JavaScript в мире! Он больше, чем RubyGems, PyPi и Maven вместе взятые! Данные приведены согласно исследованиям веб-сайта Module Counts, который отслеживает количество пакетов самых популярных реестров.
"Ничего себе сколько кода", - подумали вы. Так и есть. Чтобы фрагмент вашего кода стал npm пакетом, в проекте нужно использовать package.json. Именно так код становится пакетом, который вы можете отправить в npm реестр.
Что такое package.json?
Согласно определению package.json:
Для полной картины просто представьте README на стероидах. Вы можете определять зависимости пакета, писать сборку и тестировать скрипты, а также версионировать пакет по усмотрению и описывать его функционал. Для нас же наибольший интерес представляет возможность определять зависимости внутри package.json.
Пока звучит немного хаотично. Представьте себе бесконечную череду пакетов, зависящих друг от друга. Вот почему при установке одного пакета Gatsby вы получили 19 тысяч дополнительных зависимостей.
Типы зависимостей в package.json
Чтобы прояснить вопрос накопления зависимостей с течением времени, рассмотрим разные типы зависимостей проекта. В пакете package.json встречаются несколько из них:
Назначение package-lock.json
Всем известен тот самый файл, который получает много дополнений и удалений в пул реквестах, и это принимается как должное. package-lock.json автоматически создается каждый раз при изменении файла package.json или директории node_modules. Он сохраняет в неизменном виде дерево зависимостей, созданное при установке, чтобы все последующие зависимости могли создавать идентичное дерево. Это решает проблему, при которой у меня одна зависимость, а у вас другая.
Рассмотрим проект, имеющий среди своих зависимостей React. Если вы перейдете в package-lock.json, то увидите:
"react": { "version": "16.13.0", "resolved": "https://registry.npmjs.org/react/-/react-16.13.0.tgz", "integrity": "sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2" } }
package-lock.json является длинным списком зависимостей в проекте. Он указывает их версию, положение модуля (URI), хэш, отображающий взаимодействие модулей и необходимых для них пакетов. Продолжив чтение списка, вы найдете каждую запись для каждого пакета, необходимого для React и т.д. Вот тут-то и находится настоящий ад зависимостей. Он определяет все, что нужно проекту.
Разбираемся с зависимостями Gatsby.js
Итак, как же нам выйти из ситуации, в которой при установке одной зависимости мы получили в нагрузку 19 000? Ответ - зависимости зависимостей. Вот что происходит при установке Gatsby.js:
$ npm install --save gatsby ... + gatsby@2.19.28 added 1 package from 1 contributor, removed 9 packages, updated 10 packages and audited 19001 packages in 40.382s
В package.json можно увидеть только одну зависимость. Но присмотревшись к package-lock.json, нельзя не заметить новорожденного монстра, раскинувшего свои 14 тысяч строк. Более детальную информацию можно получить в package.json, расположенном в GitHub репозитории Gatbsy.js. По подсчетам npm число прямых зависимостей составляет 136. А теперь представьте, что каждая из этих зависимостей имеет еще одну зависимость, и в итоге вы получаете 272 зависимости. И это я еще преуменьшил! В действительности у каждой зависимости может быть больше одной зависимости, так что их список продолжит пополняться.
Например, посмотрим, сколько библиотек требует lodash.
$ npm ls lodash example-js-package@1.0.0 └─┬ gatsby@2.19.28 ├─┬ @babel/core@7.8.6 │ ├─┬ @babel/generator@7.8.6 │ │ └── lodash@4.17.15 deduped │ ├─┬ @babel/types@7.8.6 │ │ └── lodash@4.17.15 deduped │ └── lodash@4.17.15 deduped ├─┬ @babel/traverse@7.8.6 │ └── lodash@4.17.15 deduped ├─┬ @typescript-eslint/parser@2.22.0 │ └─┬ @typescript-eslint/typescript-estree@2.22.0 │ └── lodash@4.17.15 deduped ├─┬ babel-preset-gatsby@0.2.29 │ └─┬ @babel/preset-env@7.8.6 │ ├─┬ @babel/plugin-transform-block-scoping@7.8.3 │ │ └── lodash@4.17.15 deduped │ ├─┬ @babel/plugin-transform-classes@7.8.6 │ │ └─┬ @babel/helper-define-map@7.8.3 │ │ └── lodash@4.17.15 deduped │ ├─┬ @babel/plugin-transform-modules-amd@7.8.3 │ │ └─┬ @babel/helper-module-transforms@7.8.6 │ │ └── lodash@4.17.15 deduped │ └─┬ @babel/plugin-transform-sticky-regex@7.8.3 │ └─┬ @babel/helper-regex@7.8.3 │ └── lodash@4.17.15 deduped ...
К счастью, большинство из них используют одну и ту же версию lodash, которая требует установки лишь одной библиотеки lodash в node_modules. Но в реальных проектах продакшена ситуация не такая оптимистичная: иногда разные пакеты требуют разных версий других пакетов. В связи с этим как только не шутили по поводу тяжелого веса директории node_modules! В нашем же случае не все так плохо:
$ du -sh node_modules 200M node_modules
200 мегабайт, как я уже говорил, совсем неплохой результат. Бывали случаи, когда размер директории мог легко перевалить за 700 мегабайт. Если вам интересно, какие модули занимают большую часть памяти, можете выполнить следующую команду:
$ du -sh ./node_modules/* | sort -nr | grep '\dM.*' 17M ./node_modules/rxjs 8.4M ./node_modules/@types 7.4M ./node_modules/core-js 6.8M ./node_modules/@babel 5.4M ./node_modules/gatsby 5.2M ./node_modules/eslint 4.8M ./node_modules/lodash 3.6M ./node_modules/graphql-compose 3.6M ./node_modules/@typescript-eslint 3.5M ./node_modules/webpack 3.4M ./node_modules/moment 3.3M ./node_modules/webpack-dev-server 3.2M ./node_modules/caniuse-lite 3.1M ./node_modules/graphql ...
Ага, rxjs, ну и хитрая же ты штучка. Есть одна простая команда, которая поможет вам с размером node_modules и уменьшением дублирования зависимостей - npm dedup:
$ npm dedup moved 1 package and audited 18701 packages in 4.622s 51 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
Действие дедупликации призвано упростить структуру дерева зависимостей путем поиска общих пакетов между ними и их перемещением для последующего переиспользования. Как раз то, что происходит в нашем примере с lodash. Большинство пакетов останавливаются на lodash@4.17.15, поэтому нет других версий lodash для установки. Мы добились этого результата в самом начале, так как только что установили наши зависимости, но если вы в течение какого-то времени добавляли зависимости в package.json, то лучше выполнить npm dedup.
Чем больше сила, тем больше ответственность
Подводя итоги, без преувеличения скажу, что JavaScript и npm просто супер, а возможность гибкого подхода при выборе из океана зависимостей - еще лучше. Сущий пустяк - выполнить npm install для сохранения пары строк кода, но иногда мы почему-то забываем, что скрывается за этим действием.