Привет! Представляю вашему вниманию адаптированный перевод первой главы "Node.js Best Practices" автора Yoni Goldberg. Подборка рекомендаций по Node.js размещена на github, имеет почти 30 т. звезд, но до сих пор никак не упоминалась на Хабре. Предполагаю, что эта информация будет полезна, как минимум, для новичков.
1. Советы по структуре проектов
1.1 Структурируйте ваш проект по компонентам
Худшая ошибка больших приложений — это архитектура монолита в виде огромной кодовой базы с большим количеством зависимостей (спагетти-кодом), такая структура сильно замедляет разработку особенно внедрение новых функций. Совет — разделяйте ваш код на отдельные компоненты, для каждого компонента выделяйте собственную папку для модулей компонента. Важно чтобы каждый модуль остался маленьким и простым. В разделе «Подробнее» можно посмотреть примеры правильной структуры проектов.
В противном случае: разработчикам будет сложно развивать продукт — добавление нового функционала и внесение изменений в код будут производиться медленно и иметь высокий шанс поломки других зависимые компонент. Считается, что если бизнес-юниты не разделены, то могут возникнуть проблемы с масштабирование приложения.
Для приложений среднего размера и выше монолиты действительно плохи — одна большая программа с множеством зависимостей просто сложна для понимания, да еще и часто приводит к спагетти-коду. Даже опытные программисты, которые умеют правильно «готовить модули», тратят много усилий на проектирование архитектуры и стараются тщательно оценить последствия каждого изменения в связях между объектами. Наилучшим вариантом является архитектура, базирующаяся на наборе небольших программ-компонент: разделите программу на отдельные компоненты, которые не делятся ни с кем своими файлами, каждая компонента должна состоять из небольшого количества модулей (например, модулей: API, сервиса, доступа к БД, тестирования и т.п.), так чтобы структура и состав компоненты были очевидны. Некоторые могут назвать эту архитектуру «микросервисной», но, важно понимать, что микросервисы — это не спецификация, которой вы должны следовать, а скорее набор некоторых принципов. По вашему желанию, вы можете принять на вооружение как отдельные из этих принципов, так и все принципы архитектуры микросервисов. Оба способа хороши если вы сохраняете сложность кода на низком уровне.
Самое меньшее, что вы должны сделать, это определить границы между компонентами: назначить папку в корне вашего проекта для каждого из них и сделать их автономным. Доступ к функционалу компоненты должен быть реализован только через публичный интерфейс или API. Это основа для того, чтобы сохранить простоту ваших компонентов, избежать «ада зависимостей» и дать вашему приложению дорасти до к полноценных микросервисов.
Цитата блога: «Масштабирование требует масштабирования всего приложения»
Из блога MartinFowler.com
Монолитные приложения могут быть успешными, но люди все чаще испытывают разочарование в связи с ними, особенно когда задумываются о развертывании в облаке. Любые, даже небольшие, изменения в приложении требуют сборки и перевыкладки всего монолита. Часто трудно постоянно сохранять хорошую модульную структуру, при которой изменения в одном модуле не затрагивают другие. Масштабирование требует масштабирования всего приложения, а не только отдельных его частей, конечно, для такого подхода требуется больше усилий.
Цитата блога: «О чем говорит архитектура вашего приложения?»
Из блога uncle-bob
… если вы бывали в библиотеке, то вы представляете ее архитектуру: парадный вход, стойки регистрации, читальные залы, конференц-залы и множество залов с книжными полками. Сама архитектура будет говорить: это здание — библиотека.
Так о чем же говорит архитектура вашего приложения? Когда вы смотрите на структуру каталогов верхнего уровня и файлы-модули в них они говорят: я — интернет-магазин, я — бухгалтерия, я — система управления производством? Или они кричат: я — Rails, я — Spring/Hibernate, я — ASP?
(Примечание переводчика, Rails, Spring/Hibernate, ASP — это фреймворки и веб-технологии).
Правильная структура проекта с автономными компонентами
Неправильная структура проекта с группировкой файлов по их назначению
1.2 Разделяйте слои ваших компонентов и не смешивайте их со структурой данных Express
Каждый ваш компонент должен иметь «слои», к примеру, для работы с вебом, бизнес-логикой, доступом к БД, эти слои должны иметь свой собственный формат данных не смешанный с форматом данных сторонних библиотек. Это не только четко разделяет проблемы, но и значительно облегчает проверку и тестирование системы. Часто разработчики API смешивают слои, передавая объекты веб-слоя Express (к примеру, req, res) в бизнес-логику и в слой данных — это делает ваше приложение зависимым и сильно связанным с Express.
В противном случае: для приложения, в котором объекты слоев перемешаны, сложнее обеспечить тестирования кода, организацию CRON-тасков и других «неExpress» вызовов.
Обратная сторона смешение слоев в одной gif-анимации
1.3 Оберните ваши базовые утилиты в пакеты npm
В большом приложении, состоящем из различных сервисов со своими репозиториями, такие универсальные утилиты, как логгер, шифрование и т.п., должны быть обернуты вашим собственным кодом и представлены как приватные npm-пакеты. Это позволяет делиться ими между несколькими кодовыми базами и проектами.
В противном случае: вам придется изобрести собственный велосипед для расшаривания этого кода между отдельными кодовыми базами.
Как только проект начнет расти и у вас на разных серверах будут разные компоненты, использующие одни и те же утилиты, вы должны начать управлять зависимостями. Как можно без дублирования кода вашей утилиты между репозиториями позволить нескольким компонентам использовать ее? Для этого есть специальный инструмент, и называется он — npm…. Начните с обертывания сторонних пакетов утилит вашим собственным кодом, чтобы в будущем его можно было легко заменить, и опубликуйте этот код как частный пакет npm. Теперь вся ваша кодовая база может импортировать код утилит и использовать все возможности управления зависимостями npm. Помните, что есть следующие способы публиковать пакеты npm для личного использования, не открывая их для публичного доступа: частные модули, частный реестр или локальные пакеты npm.
Совместное использование собственных общих утилит в разном окружении
1.4 Разделяйте Express на «приложение» и «сервер»
Избегайте неприятной привычки определять все приложение Express в одном огромном файле, разделите ваш 'Express'-код по крайней мере на два файла: объявление API (app.js) и код www-сервера. Для еще лучшей структуры размещайте объявление API в модулях компонент.
В противном случае: ваш API будет доступен для тестирования только через HTTP-вызовы (что медленнее и намного сложнее для создания отчетов о покрытии). Еще, предполагаю, не слишком большое удовольствие работать с сотнями строк кода в одном файле.
Рекомендуем использовать генератор приложений Express и его подход к формированию базы приложения: объявление API отделено от конфигурации сервера (данных о порте, протоколе и т.п.). Это позволяет тестировать API без выполнения сетевых вызовов, что ускоряет выполнение тестирования и упрощает получение метрик покрытия кода. Это также позволяет гибко развертывать один и тот же API для разных сетевых настроек сервера. Бонусом вы так же получаете лучшее разделение ответственности и более чистый код.
Пример кода: объявление API, должно находиться в app.js
var app = express();
app.use(bodyParser.json());
app.use("/api/events", events.API);
app.use("/api/forms", forms);
Пример кода: сетевых параметров сервера, должно находиться в /bin/www
var app = require('../app');
var http = require('http');
/**
* Получаем порт из переменных окружения и используем его в Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Создать HTTP-сервер.
*/
var server = http.createServer(app);
Пример: протестируем свой API используя supertest (популярный пакет тестирования)
const app = express();
app.get('/user', function(req, res) {
res.status(200).json({ name: 'tobi' });
});
request(app)
.get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '15')
.expect(200)
.end(function(err, res) {
if (err) throw err;
});
1.5 Используйте безопасную иерархическую конфигурацию с учетом переменных окружения
Идеальная настройка конфигурации должна обеспечивать:
(1) считывание ключей как из конфигурационного файла, так и из переменных среды,
(2) хранение секретов вне кода репозитория,
(3) иерархическую (а не плоскую) структуру данных конфигурационного файла для облегчения работы с настройками.
Есть несколько пакетов, которые могут помочь в реализации этих пунктов, такие как: rc, nconf и config.
В противном случае: несоблюдение этих требований к конфигурации приведет к срыву работы как отдельного разработчика, так и всей команды.
Когда вы имеете дело с настройками конфигурации многие вещи могут раздражать и тормозить работу:
1. Задание всех параметров с использованием переменных окружения становится очень утомительным если требуется ввести 100+ ключей (вместо того, чтобы просто зафиксировать их в файле конфигурации), однако если конфигурация будет задаваться только в файлах настроек, то это может быть неудобно для DevOps. Надежное конфигурационное решение должно объединять оба способа: и файлы конфигураций, и переопределения параметров из переменных окружения.
2. Если конфигурационный файл является «плоском» JSON (т.е. все ключи записаны в виде единого списка), то при увеличении количества настроек с ним будет сложно работать. Решить эту проблему можно с помощью формирования вложенных структур содержащих группы ключей по разделам настроек, т.е. организовать иерархического JSON-структуру данных (см. пример ниже). Есть библиотеки, которые позволяют хранить такую конфигурацию в нескольких файлах и объединять данные из них во время выполнения.
3. Не рекомендуется хранить в конфигурационных файлах конфиденциальную информацию (такую как пароль БД), но однозначного удобного решения где и как хранить такую информацию — нет. Некоторые библиотеки конфигураций позволяют шифровать конфигурационные файлы, другие шифруют эти записи во время git-коммитов, а можно вообще не сохранять секретные параметры в файлах и задавать их значения во время развертывания через переменные среды.
4. Некоторые расширенные сценарии конфигураций требуют ввода ключей через командную строку (vargs) или синхронизируют конфигурационные данные через централизованный кэш, такой как Redis, чтобы несколько серверов использовали одни и те же данные.
Есть npm-библиотеки, которые помогут вам с реализацией большинства этих рекомендаций, советуем взглянуть на следующие библиотеки: rc, nconf и config.
Пример кода: иерархическая структура помогает находить записи и работать с объемными файлами конфигураций
{
// Customer module configs
"Customer": {
"dbConfig": {
"host": "localhost",
"port": 5984,
"dbName": "customers"
},
"credit": {
"initialLimit": 100,
// Set low for development
"initialDays": 1
}
}
}
(Примечание переводчика, в классическом JSON-файле нельзя использовать комментарии. Вышеприведенный пример взят из документации библиотеки config, в которой добавлен функционал предварительной очистки JSON-файлов от комментариев. Поэтому пример вполне рабочий, однако линтеры, такие как ESLint, с настройками по умолчанию могут «ругаться» на подобный формат).
Послесловие от переводчика:
- В описании проекта написано, что перевод на русский язык уже запущен, но этого перевода я там не нашла, поэтому и взялась за статью.
- Если перевод кажется вам очень кратким, то попробуйте развернуть подробную информацию в каждом разделе.
- Простите, что иллюстрации оставлены без перевода.
Автор: Анастасия