Выбор зависимостей JavaScript
Всем привет! В предыдущем посте мы подробно поговорили про добавление зависимостей в проект и про способы и стратегии их обновления.
В этом посте, как и обещал, я хочу начать обсуждение таких невероятно важных вопросов, как стабильность и безопасность в управлении зависимостями. Надеюсь, мои советы помогут вам контролировать хаос, снижать риски и всегда оставаться на безопасной стороне!
Экосистема npm — это мощнейший инструмент, который позволяет нам использовать наработки других людей, чтобы быстрее запускать собственные проекты. Но важно понимать, что эта экосистема неоднородна: наряду с прекрасными высококвалифицированными разработчиками в ней также присутствуют как менее опытные любители, которые склонны совершать ошибки и писать низкокачественный код, так и откровенные злоумышленники, которые жаждут проникнуть в ваш проект с целью получения наживы. Поэтому с точки зрения безопасности и стабильности мы должны относиться к экосистеме npm как к источнику хаоса в нашем проекте, и обязаны предпринимать всевозможные меры, чтобы этот хаос контролировать, не давая ему просочиться внутрь и затронуть конечных пользователей нашего продукта.
Первым шагом на пути сокращения рисков в вашем проекте является правильный выбор зависимостей. Действительно, ведь если мы будем использовать только хорошие зависимости и не использовать плохие, то количество хаоса в проекте значительно сократится и нам нечего будет бояться (нет). Но как понять, какие зависимости «хорошие», а какие «плохие»? Давайте попробуем выработать для себя какие-то критерии.
Думаю, довольно очевидной является мысль, что если не использовать зависимости, то и рисков можно будет полностью избежать. Однако не для всех разработчиков это столь очевидно: часто программисты бегут устанавливать новую зависимость по первой же возникшей потребности. Я хочу предостеречь от такого подхода.
Добавление каждой новой зависимости должно быть ответственным, тщательно взвешенным и продуманным решением. Хорошо подумайте, возможно, то, что вы делаете, можно сделать иначе, и добавление зависимости вообще не понадобится.
Используйте нативные решения
Я рекомендую хорошенько изучить ваши основные инструменты — язык программирования (JavaScript) и среду выполнения (Node.js или браузерные API). Как правило, многие задачи можно решить, используя нативные решения, доступные в самом языке или платформе, а не устанавливая дополнительный пакет. Так что, прежде чем искать очередную стороннюю библиотеку, убедитесь на 100%, что эту задачу нельзя решить нативными методами или написанием собственной несложной функции.
Учитывая вышесказанное, я рекомендую использовать в вашем проекте самые свежие версии языка и платформы. Благодаря Babel (или tsc) и полифилам, вы уже сейчас можете писать код на самой свежей версии языка (ESNext) и использовать самые передовые API вашей платформы. Это очень сильно расширит ваши возможности и позволит избежать установки дополнительных нестандартных решений. Кроме того, при использовании нативных технологий ваш код будет гораздо лучше восприниматься коллегами, потому что со стандартами разработчики знакомы лучше, чем с отдельно взятыми пакетами. И нужно добавить, что нативный код, как правило, более качественный, лучше оптимизирован и выполняется быстрее, чем прикладной.
Используйте уже имеющиеся зависимости
Часто бывает так, что какая-то зависимость содержит в себе целый набор различных решений (например, lodash). Прежде, чем ставить новую зависимость, убедитесь, что уже имеющиеся зависимости не умеют делать то, что вам нужно.
Заменяйте зависимости
Учитывая вышесказанное, при установке новой зависимости проверьте, не может ли она заменить одну или несколько зависимостей, которые уже используются в проекте. В этом случае запланируйте рефакторинг, чтобы отказаться от старых зависимостей в пользу новой. Не допускайте ситуации, когда в вашем проекте используется несколько библиотек, функциональность которых сильно пересекается. К примеру, не стоит устанавливать в одном проекте lodash, ramda и underscore одновременно, выберите что-то одно и следуйте этому.
Смените подход или просто откажитесь
Представьте, что вы реализуете некую фичу, и для написания алгоритма вам понадобилась какая-то библиотека. Подумайте, возможно, получится реализовать этот алгоритм без использования сторонней библиотеки (с минимальными потерями), или использовать другой алгоритм. А быть может, вообще отказаться от этой фичи, если она не является важной (помните, что преждевременная оптимизация — корень зла и вам это не понадобится).
Учтите издержки на поддержку зависимости
В любой ситуации стоит хорошо взвесить, насколько тяжело будет жить без той или иной зависимости и как сильно она нужна в проекте. Добавить зависимость в проект легко, а регулярно поддерживать её на протяжении нескольких лет — не так просто.
Помните, что вам придется регулярно обновлять каждую зависимость, а при выходе мажорных обновлений ещё и переписывать ваш собственный код вручную.
Зафиксировать и забыть
А почему бы просто не зафиксировать версию той или иной зависимости? Это позволило бы не тратить время на её обновление.
Дело в том, что с обновлением зависимостей вы получаете улучшения производительности, и самое важное — исправления безопасности. Таким образом, отказ от обновлений — это увеличение рисков для безопасности, а мы, наоборот, ищем способы их снижения.
А почему бы не игнорировать мажорные обновления?
К сожалению, практика такова, что разработчики, как правило, поддерживают только одну ветку разработки — текущую мажорную. Если вы откажетесь от перехода на новую мажорную версию, то никаких обновлений, увы, больше не получите.
Но если всё же мы решили, что зависимость нам нужна, давайте вернемся к критериям, которые помогут нам сделать правильный выбор.
Есть такое устоявшееся выражение, что судить о качестве кода по количеству звезд на GitHub некорректно, и это действительно так. Однако, количество звезд — это важная метрика, которая показывает популярность той или иной библиотеки. А для нас это важно, ведь чем выше популярность, тем больше шансов на то, что у библиотеки есть хорошие мейнтейнеры и большое количество пользователей, которые с высокой вероятностью будут обнаруживать её недостатки (см. закон Линуса).
По этой причине всё же стоит обращать внимание на количество звезд на GitHub, а также на количество скачиваний пакета из registry npm (графа »Weekly Downloads» на странице библиотеки на сайте npmjs.com).
Кроме того, если речь идет про достаточно популярные технологии, то можно посмотреть сравнение трендов в сервисе Google Trends:
Или упоминания библиотеки на StackOverflow:
В отдельных случаях стоит обратить внимание на вендора (производителя), который стоит за той или иной библиотекой. За некоторыми проектами могут стоять крупные и серьезные компании, которые хорошо зарекомендовали себя в сфере IT и OpenSource (Google, Microsoft, Facebook и т. д.). Как правило, таким поставщикам можно доверять с гораздо большей вероятностью (но не слепо!).
Проанализируйте активность авторов, которые пишут код той или иной библиотеки: чем выше их узнаваемость, активность на GitHub, наличие выступлений на конференциях, статей и записей на YouTube, тем больший кредит доверия вы можете им дать (разумеется, если их деятельность достаточно компетентна).
Одним из ключевых критериев должна стать активность проекта на GitHub. Обязательно посмотрите, сколько у проекта активных мейнтейнеров (хорошо, если их несколько), изучите историю коммитов (когда в последний раз был релиз, как часто вносятся изменения), посмотрите на тикеты (issues) (как много их открыто, как быстро мейнтейнеры отвечают на них и насколько компетентны их ответы), проверьте, нет ли у проекта кучи висящих открытых PR, которые просто игнорируются авторами.
Ответы на эти вопросы помогут получить представление о критически важном показателе — пульса проекта. Если библиотека жива и активно поддерживается, то при обнаружении каких-то проблем вы сможете их решить (обратившись к мейнтейнерам или отправив собственный PR). Кроме того, проблемы будут обнаруживаться и решаться другими людьми, а вам будет достаточно периодически обновлять зависимость.
Как правило, наличие в библиотеке хорошей и подробной документации может говорить о высоком качестве библиотеки. Как минимум, вам будет проще её осваивать и использовать (без необходимости ковыряния исходного кода). Это важный критерий, который сэкономит вам время при работе и поддержке. Кроме того, по документации можно легко увидеть API и архитектуру библиотеки, а это уже многое может сказать опытному разработчику о её качестве.
Наличие журнала изменений (changelog) или списка версий (releases на GitHub) с описанием patch-, minor- и major-изменений тоже критически важно, это говорит о том, что автор понимает принципы семантического версионирования (semver) и следует им. Кроме того, при мажорном обновлении вам будет где почитать про нарушения обратной совместимости и необходимые правки, которые нужно будет вносить.
Если у вас достаточно опыта, то вы можете оценить библиотеку по качеству её кода. Для этого стоит сначала обратить внимание на то, как организован код проекта, насколько хорошо он структурирован и легко читается, есть ли в нем комментарии. Затем, если позволяет время, можете углубиться и оценить качество решений, подходов и самих алгоритмов. Обратите внимание на то, какие технологии используются в коде, не устарели ли они?
Очень важным критерием может являться наличие в библиотеке тестов, интеграция системы CI/CD, наличие автоматического линтинга. Если код библиотеки полностью покрыт автотестами, то это очень хороший сигнал.
Если вы ещё не пишете на TypeScript, то срочно начинайте! (я подожду). Это существенно повысит надежность вашего кода.
Обратите внимание, используется ли в исходном коде рассматриваемой вами библиотеки типизация (TypeScript или Flow). Если да, то это очень хороший признак, особенно если код полностью покрыт строгими типами.
Кроме того, исходный код библиотеки может быть написан на ванильном JavaScript (без типов), но при этом содержать декларации типов (typing declarations). Это хорошо, потому что повышается надежность интеграции с вашим проектом. Убедитесь только, что декларации активно обновляются вместе с API библиотеки, а не случайно залетели туда через чей-то старый PR.
Ещё можно поискать декларации в репозитории Definitely Typed с помощью команды:
npm view @types/
где
— это название пакета, для которого вы ищете декларации. Учтите, если пакет в своем названии содержит имя вендора (scope), то в имени пакета нужно заменить символ @
на два подчеркивания.
Примеры:
# для пакета "react"
npm view @types/react
# для пакета "babel@preset-env"
npm view @types/babel__preset-env
Если для пакета существуют свежие декларации Definitely Typed, то это косвенное подтверждение того, что проект пользуется некой популярностью, и это хорошо.
Иногда бывает сложно сразу выбрать правильную библиотеку. В этом случае имеет смысл попробовать использовать её на практике и проанализировать результаты. Если уже на этапе интеграции вы сталкиваетесь с кучей проблем, неверной документацией, устаревшими декларациями и игнором со стороны мейнтейнеров, то бегите, пока можете. Лучше потерять время на этапе внедрения, чем потерять его потом при поиске багов и откатывании продакшена вечером в субботу.
Кроме того, имеет смысл поспрашивать ваших коллег или других разработчиков в интернете: чем пользуются они? Как давно? Почему они сделали свой выбор именно в пользу этой библиотеки? Реальный опыт ничем не заменишь — советы более опытных коллег могут быть очень полезны, особенно, когда речь идет о подводных камнях, которые вылезают далеко не сразу.
Если вы работаете над front-end проектом, то также будет полезно «взвесить» рассматриваемый вами пакет при помощи сервиса Bundle Phobia, он примерно покажет сколько КБ будет занимать библиотека в бандле вашего приложения:
Не стоит делать выводы о пригодности той или иной библиотеки только по одному критерию из списка. Финальное решение всегда должно основываться на сумме всех критериев после тщательного качественного и количественного анализа с вашей стороны. Но даже если вы приняли какое-то решение, всегда будьте готовы к тому, что оно может оказаться неверным, даже если все критерии указывали на обратное: мир программного обеспечения — это сложная штука, и всего не предусмотреть. Иногда хаос врывается туда, где его совсем не ждут. Будьте всегда настороже и не говорите, что вас не предупреждали.
И помните, что в конечном итоге именно вы несете ответственность за финальный продукт, его качество, надежность и безопасность. При возникновении проблем именно вам придется их решать. Свалить вину на авторов сторонних библиотек не получится.
Если вы не хотите делать ставку на какую-то конкретную библиотеку, или имеете основания не доверять ей, то можно применить очень хороший архитектурный прием, который позволяет существенно сократить издержки, если вы решите заменить библиотеку.
Вместо того, чтобы импортировать и вызывать стороннюю библиотеку по всему коду, вынесите её использование полностью в отдельный модуль, а потом вызывайте вашу собственную абстракцию. И если вы захотите заменить библиотеку на любую другую, или даже на собственную реализацию, то вам достаточно будет отредактировать только один файл, а весь остальной код вашего проекта останется нетронутым (см. adapter pattern).
В отдельных случаях (если это будет оправдано) вы можете даже сделать систему из нескольких адаптеров, которые позволят использовать разные библиотеки и легко переключаться между ними практически без изменения кода (например, через dependency injection и полиморфизм).
Пример:
//=======================//
// utils/do-something.js //
//=======================//
// Импортируем библиотечную функцию
import { doSomething as libraryDoSomething } from 'third-party-library';
// Экспортируем нашу собственную оберточную функцию,
// которая будет вызываться в коде проекта
export function doSomething(...args) {
// Вызываем библиотечную функцию
// (здесь мы можем трансформировать любые аргументы и возвращаемые значения)
return libraryDoSomething(...args);
}
//========//
// foo.js //
//========//
import { doSomething } from './utils/do-something';
// вызываем нашу обертку вместо функции из библиотеки
doSomething();
//========//
// bar.js //
//========//
import { doSomething } from './utils/do-something';
// снова вызываем нашу обертку вместо функции из библиотеки
doSomething(100500, true);
Есть ещё один прием, который поможет вам повысить надежность использования сторонней библиотеки, быстрее и точнее обнаруживать деградации, вызванные нарушением обратной совместимости при обновлении, и улучшит ваш ночной сон — интеграционные тесты.
Просто покройте тестами наиболее критичные области API библиотеки, которые вы используете. И если что-то изменится после обновления, то вы узнаете об этом первыми и будете точно знать, в каком месте произошла ошибка.
Покрывать тестами чужой код считается плохой практикой, но только если это рассматривается в контексте тестирования вашего собственного кода. Если у вас есть веские причины не доверять какой-то библиотеке, но она играет критическую роль в вашем проекте, то нет ничего предосудительного в том, чтобы покрыть тестами ваши сценарии использования этой библиотеки.
А теперь приготовьтесь к небольшой магии! (я знаю, разработчики не очень любят магию, но потерпите) Введите в адресной строке браузера node.cool
, и вы получите внушительный список довольно качественных библиотек для Node.js на все случаи жизни. Не благодарите!
В этом посте мы начали обсуждать вопросы стабильности и недопущения хаоса в ваш проект, рассмотрели критерии оценки качества сторонних библиотек и изучили некоторые приемы, которые позволяют нам контролировать хаос.
В следующем посте я планирую продолжить обсуждение этой темы, но уже немного с другого ракурса. Подписывайтесь и следите за обновлениями!