Node.js: история создания, архитектура и лучшие практики разработки для создания эффективных и устойчивых приложений
14246

Node.js: история создания, архитектура и лучшие практики разработки для создания эффективных и устойчивых приложений


Несмотря на то, что Node.js существует всего 12 лет, он стал одним из самых популярных фреймворков для веб-разработки за последнее десятилетие. Я большой фанат Javascript, и благодаря Node.js я могу писать код на Javascript вне браузера для создания серверных веб-приложений, неблокирующих, легких, быстрых, надежных и масштабируемых.

В этой статье я хочу рассказать о двух аспектах программирования на Node.js - о внутренней механике фреймворка и о лучших практиках разработки для создания эффективных и устойчивых приложений Node.js.

Сознательно прилагая усилия для понимания внутренней работы фреймворка, мы открываем для себя понимание путей и способов работы не только самого фреймворка, но и распространенных парадигм программирования и их проектных решений. Со временем эти знания нижнего уровня отражаются в том, как мы пишем код, и определяют наше понимание того, как мы можем оптимизировать наши приложения для большей скорости и лучшей производительности. Неотъемлемым аспектом того, как Node.js работает под капотом, является его однопоточный, основанный на циклах событий аппарат для достижения асинхронного поведения. Мы рассмотрим это более подробно в первой половине этой статьи.

Вторая половина этой статьи будет посвящена другому концу спектра - выделению 12 лучших практик, которые следует иметь в виду, начиная новый проект на Node.js. Они представляют собой различные аспекты создания надежного приложения с точки зрения общей архитектуры, структуры папок, модульности, написания чистого кода, управления зависимостями и многого другого. В некотором роде это будет экстраполяцией нашего понимания строительных блоков Node.js для установления определенных базовых правил и рекомендаций по созданию прочного фундамента для наших проектов.

Задача этой статьи ответить на следующие вопросы: 

1. Как работает Node.js под капотом?
2. Как достигается параллелизм?
3. Как он соотносится с другими многопоточными веб-фреймворками?
4. Как выглядит правильная установка проекта Node.js?
5. Какие наиболее важные правила следует учитывать при создании приложения Node.js?

Краткая история Node.js


Чтобы дать вам представление о хронологии событий, Всемирная паутина появилась около 30 лет назад. Javascript родился около 26 лет назад, и примерно столько же - PHP (27 лет). Node.js, с другой стороны, всего 12 лет. Несмотря на относительно небольшой срок существования, Node.js успел сотворить чудеса для разработчиков по всему миру.

С тех пор как появился Javascript, были попытки использовать Javascript для бэк-энда. Например, Netscape пытался сделать что-то подобное с Netscape Livewire. Однако эти попытки оказались безуспешными. Примерно с 2004 года, когда накатили первые волны Web 2.0, Javascript начал получать большую популярность благодаря тенденциям современного веб-опыта. Поскольку Javascript был (и остается) самым распространенным языком программирования на стороне клиента, браузеры соревновались в стремлении создать наиболее оптимизированные движки Javascript для лучшей производительности. Одним из таких движков был Chrome V8, на основе которого позже был создан Node.js. В результате этого импульса Javascript расцвел, а вместе с ним и движок V8. 

В 2009 году в нужном месте, в нужное время родился Node.js. С тех пор развитие Node.js стремительно пошло в гору. Несмотря на конкуренцию со стороны таких пионеров, как PHP и Advance Java, Node.js стал более предпочтительным выбором серверной части для многих приложений сегодня, благодаря асинхронному вводу/выводу, событийно-ориентированной архитектуре, легковесности, скорости, масштабируемости и тому факту, что он использует самый популярный язык программирования, т.е. Javascript. Сегодня серверы Node.js используются в продакшене для приложений и компаний, которые обслуживают сотни миллионов пользователей по всему миру - Netflix, Linkedin, Microsoft, GoDaddy, Paypal и многие другие. Чтобы дать вам оценку его популярности, менеджер пакетов Node, NPM, регистрирует миллиарды загрузок каждую неделю.

Node.js очень активно поддерживается благодаря огромному сообществу пользователей и разработчиков. Это означает, что в Интернете есть богатая поддержка, если вы где-то застряли и вам нужна помощь с вашим кодом или любой совет по веб-разработке в целом.

Теперь давайте посмотрим, что дает Node.js его преимущество - как он работает под капотом.

Архитектура и принципы работы Node.js


Node.js наиболее популярен благодаря асинхронной, событийно-ориентированной, неблокирующей обработке ввода-вывода. Большую часть этого параллелизма и асинхронности он получает от модели однопоточного цикла событий Javascript (event loop model).

Большинство других альтернатив веб-разработки, таких как ASP.NET, JSP, Spring, используют многопоточную архитектуру обработки для удовлетворения одновременных запросов клиентов. Давайте подробнее рассмотрим эти многопоточные модели, прежде чем противопоставить их тому, что предлагает Node.js.

Традиционная модель многопоточной обработки в веб-фреймворках


При многопоточной обработке каждый сервер имеет в своем распоряжении ограниченный пул потоков. Каждый раз, когда сервер получает запрос от клиента, он выбирает поток из пула и назначает его на запрос клиента. Этот поток будет выполнять всю обработку, связанную с этим запросом. Внутри этих потоков обработка происходит последовательно и синхронно, т.е. за один раз выполняется одна операция. Независимо от этого, когда на сервер поступает новый параллельный запрос, он может взять любой свободный поток из пула и запустить его в работу.

Так может продолжаться до тех пор, пока все потоки не будут исчерпаны. Когда это происходит, ваш сервер вынужден ждать, пока освободится хотя бы один из занятых потоков, чтобы удовлетворить новый запрос (запросы). Если не отнестись к этому ответственно, то это может оказаться медленным и неэффективным для вашего приложения. Кроме того, синхронная природа обработки внутри каждого потока означает, что даже если мы можем запустить несколько потоков для одновременных запросов, каждый поток в отдельности будет замедляться при столкновении с блокирующим кодом. Такая многопоточная поддержка также влечет за собой трудности, связанные с синхронизацией и управлением несколькими потоками. Существует также риск возникновения dead-locking, когда несколько потоков блокируются навсегда в процессе ожидания друг друга, чтобы освободить ресурсы.

Теперь давайте рассмотрим, как Node.js обрабатывает параллелизм.

Архитектура однопоточного цикла событий в Node.js


Существует много путаницы по поводу того, действительно ли Node.js выполняет все только с одним потоком. Как такое возможно? Как он может конкурировать с другими многопоточными фреймворками, используя всего один поток?

Как мы знаем, Node.js - это, по сути, среда выполнения Javascript, построенная поверх движка V8 Javascript в Chrome. Это означает, что она основана на однопоточной архитектуре Javascript. Поэтому каждый раз, когда поступает клиентский запрос, он обрабатывается одним главным потоком. Цикл событий - это основной компонент, который позволяет Node.js выполнять особым образом блокирующие операции ввода-вывода неблокирующим способом. Он постоянно отслеживает состояние ваших асинхронных задач (например, кода в ваших callback функциях) и перемещает их обратно в очередь выполнения, когда они завершены. Он работает в том же главном потоке, о котором мы уже говорили.

Интересно отметить, что, хотя на первый взгляд основной поток всего один, в ядре системы существует множество вспомогательных потоков, которые Node.js может использовать для выполнения обширных асинхронных операций на диске и в сети. Эта группа потоков составляет так называемый рабочий пул.

Цикл событий может сам позаботиться об основной обработке, но для асинхронных операций ввода-вывода, включающих такие модули, как fs (I/O-heavy) и crypto (CPU-heavy), он может перегрузить обработку на рабочий пул в ядре системы. Рабочий пул реализован в libuv и может порождать и управлять несколькими потоками в соответствии с требованиями. Эти потоки могут индивидуально выполнять свои соответствующие задачи синхронно и возвращать свой ответ в цикл событий, когда он готов. Пока эти потоки работают над своими задачами, цикл событий может продолжать работать в обычном режиме, параллельно удовлетворяя другие запросы. Когда потоки выполнят свои задачи, они могут вернуть свой результат в цикл событий, который затем может поместить его обратно в очередь на выполнение или вернуть обратно клиенту.

Мысль о принятии такой архитектуры можно объяснить тем, что при типичных веб-нагрузках один основной поток может работать и масштабироваться гораздо лучше по сравнению с традиционными архитектурами "один поток на запрос". В результате Node.js является наиболее предпочтительным вариантом для многих из-за его преимуществ в плане скорости и масштабируемости. Однако здесь есть оговорка: производительность может пострадать при выполнении сложных операций, требующих большого объема памяти, таких как матричные умножения для обработки изображений, data science и machine learning приложения. Они могут заблокировать единственный основной поток, что сделает сервер недоступным. Однако для таких случаев в Node.js были введены рабочие потоки, которые разработчики могут использовать для создания эффективных многопоточных приложений Node.js.

Почему хорошая структура проекта важна для приложений Node.js


Теперь, когда у нас есть четкое понимание механики работы Node.js под капотом, давайте перейдем к более прикладной стороне вещей и рассмотрим, что представляет собой продуманный проект Node.js.

Хорошая структура проекта - это ключ к любой разработке программного обеспечения и прочный фундамент для эффективного приложения. Когда вы начинаете новый проект Node.js, хорошо определенная структура, заложенная заранее, дает четкое представление о работе вашей системы с высоты птичьего полета. Она также помогает систематизировать бизнес-логику, сервисы, маршруты API, модели данных и т.д. Это обеспечивает согласованность и ясность в отношении роли и места различных компонентов в вашем проекте.

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

1. Целостная, согласованная (coherent), четко определенная структура для ясности

2. Возможность повторного использования, модульность и разделение ответственности (Термин Separation of Concerns был впервые предложен Эдсгером Дейкстрой в 1974 году)

3. Простота для лучшего понимания

4. Простота отладки и поддержки

5. Автоматизированное тестирование, механизмы логирования.

6. Использование лучших принципов программирования и разработки

Чтобы установить набор основных правил и рекомендаций, которые следует учитывать при создании приложений Node.js, давайте перейдем к следующему разделу, где мы обсудим лучшие практики разработки в проектах Node.js.

Лучшие практики для разработки на Node.js


В Интернете полно учебников, документации, блогов и видео, рассказывающих об основах веб-разработки. Но обычно информацию о лучших практиках мы узнаем по ходу разработки, по мере создания большего количества приложений, по мере неудач и успехов на этом пути.

В этом разделе я хочу свести наиболее важные аспекты веб-разработки к набору пунктов, которые необходимо всегда учитывать при создании веб-приложений на Node.js. Эти пункты дают представление о том, как определенные проектные решения могут принести огромные дивиденды в течение жизненного цикла веб-разработки.

Лучшая практика №1. Используйте многоуровневый подход: разделение ответственности


Популярные Node.js-фреймворки, такие как Express.js, позволяют определять обработчики маршрутов как функции обратного вызова, которые выполняются при получении клиентского запроса. Учитывая гибкость, которую предоставляют эти фреймворки, может возникнуть определенный соблазн определить всю бизнес-логику непосредственно в этих функциях. Если вы начнете идти по этому пути, вы заметите, что ситуация может быстро обостриться, и вы не успеете оглянуться, как ваш маленький файл маршрутов сервера превратится в громоздкий и грязный сгусток кода, который не только трудно читать, поддерживать и управлять, но и трудно тестировать.

Таким образом, это самое подходящее место для реализации известного принципа программирования "разделение ответственности". Согласно этому принципу, мы должны иметь различные модули для решения различных проблем, относящихся к нашему приложению. Что касается приложений на стороне сервера, различные модули (или уровни) должны отвечать за различные аспекты обработки ответа на запрос клиента. В целом, в большинстве случаев это будет выглядеть следующим образом 

Запрос клиента -> Некоторая бизнес-логика + некоторые манипуляции с данными (базой) -> Возвращение ответа

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

Контроллер
(маршруты и конечные точки API)
Сервисный слой
 (для бизнес-логики)
Уровень доступа к данным
(для работы с базой данных)

Уровень контроллера

Это модуль вашего кода, в котором определяются маршруты API. Здесь вы определяете только и только лишь ваши API-маршруты. В функциях обработчиков маршрутов вы можете деконструировать объект запроса, выбрать важные части данных и передать их на обработку сервисному слою.

Сервисный уровень

Это место, где живет ваша бизнес-логика, даже секретный соус вашего приложения. Он содержит кучу классов и методов, которые берут на себя единичную ответственность и являются многократно используемыми (а также следуют другим принципам программирования S.O.L.I.D). Этот слой позволяет эффективно отделить логику обработки от того, где определяются маршруты.  

Еще один аспект, который необходимо рассмотреть, - это база данных. Для самостоятельной работы с ней нам нужен еще один слой.

Слой доступа к данным

Слой доступа к данным может взять на себя ответственность за общение с базой данных - получение данных из базы, запись в нее и обновление. Здесь должны быть определены все ваши SQL-запросы, соединения с базой данных, модели, ORM и т.д.

Эта трехслойная структура служит надежными строительными лесами для большинства приложений Node.js, что делает ваши приложения более простыми в написании, поддержке, отладке и тестировании. Теперь давайте рассмотрим, как мы можем реализовать эти слои в нашем проекте.

Лучшая практика №2. Структура папок проекта Node.js: Правильно организуйте файлы кода


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

Это обеспечивает ясность в том, какая функциональность где находится, и позволяет нам организовать наши классы и методы в отдельные контейнеры, которыми легче управлять. Ниже приведена общая (но эффективная) структура папок, которую можно использовать в качестве шаблона при создании нового проекта Node.js.

  src
      ├── app.js         входная точка приложения
      ├── /api           слой контроллеров: маршруты api
      ├── /config        конфигурационные установки, переменные окружения
      ├── /services      слой сервисов: бизнес-логика
      ├── /models        слой данных: модели БД
      ├── /scripts       NPM скрипты
      ├── /subscribers   асинхронные обработчики событый
      └── /test          тестовые кейсы

Здесь каталоги /API (слой контроллеров), /services и /models (слои доступа к данным) представляют три слоя, которые мы обсуждали в предыдущем разделе. Каталог /scripts может использоваться для хранения сценариев автоматизации рабочего процесса для построения (например, развертывания) конвейеров (pipelnes), а каталог /test используется для хранения тестовых кейсов. Назначение директорий /config и /subscriber мы рассмотрим позже в этой статье, когда будем говорить о конфигурационных файлах, переменных окружения и pub/sub-моделях.

Как разработчику, мне ничто не приносит большего удовольствия, чем чтение (и написание) чисто структурированного и организованного кода. Это может привести нас к следующей важной практике разработки, о которой следует помнить - чистый код и легкая читаемость.

Лучшая практика №3. Модель издатель-подписчик


Модель "издатель/подписчик" - это популярная модель обмена данными, в которой есть два взаимодействующих субъекта - издатели и подписчики. Издатели (отправители сообщений) посылают сообщения по определенным каналам без какого-либо знания о принимающих сущностях. Подписчики (получатели сообщений), с другой стороны, проявляют интерес к одному или нескольким из этих каналов без каких-либо знаний об издающих сущностях.

Хорошая идея - включить такую модель в свой проект, чтобы управлять несколькими дочерними операциями, соответствующими одному действию. Например, ваше приложение, создавая нового пользователя при регистрации, будет выполнять множество действий - создавать запись пользователя в базе данных, генерировать ключ авторизации, отправлять подтверждение по электронной почте и многое другое. Если все это будет обрабатываться одной сервисной функцией в вашем приложении, то оно не только вырастет длиннее обычного, но и нарушит принцип Single Responsibility. Вот пример такого кода:

export default class UserService() {
      async function signup(user) {
        // 1. Create user record
        // 2. Generate auth key
        // 3. Send confirmation email
        // ...  
  }
}

Давайте посмотрим, как мы можем упростить и эффективно модулировать это с помощью модели pub/sub.

Модель pub/sub может быть настроена в Node.js с помощью API Events. В приведенном выше примере вы могли бы запрограммировать свой код так, чтобы при получении запроса сначала генерировалось событие 'signup'. В этом случае вашему сервисному модулю нужно сделать только один вызов для генерации соответствующего события, в отличие от нескольких вызовов функций в модели не pub/subsetting.

 var events = require('events');
      var eventEmitter = new events.EventEmitter();
      export default class UserService() {
        async function signup(user) {
          // emit 'signup' event
          eventEmitter.emit('signup', user.data)
        }
      }

Для обработки таких событий вы можете иметь несколько подписчиков, которые, по сути, являются слушателями событий, ожидающими определенных событий. Эти подписчики могут быть собраны в отдельные файлы в зависимости от их назначения и храниться в каталоге /subscribers, который мы рассматривали в разделе "Структура папок" выше. Теперь давайте создадим примеры файлов подписчиков для нашего примера выше:

// email.js
    // ...
    eventEmitter.on('signup', async ({ data }) => {  // event listener 
      // send email 
    })
    // ...
// auth.js
    // ...
    eventEmitter.on('signup', async ({ data }) => {	// event listener
      // generate auth key
    })
    // ...

Как видите, этот подход чище, гибче и, следовательно, лучше для обслуживания и масштабируемости.

Лучшая практика №4. Чистый код и легкая читаемость кода


Используйте линтеры кода (статические анализаторы кода) и руководства по стилю; добавляйте комментарии.

Основная цель линтинга и форматирования - улучшить качество кода и сделать его легко читаемым. Большинство рабочих процессов по настройке кода всегда содержат линтер и форматер кода. Линтер ищет и предупреждает о синтаксически (и даже семантически) ошибочном коде, в то время как форматер кода (как следует из названия) работает над больше над стилистическими аспектами вашего кода, чтобы обеспечить набор рекомендаций по форматированию и стилизации, согласованных во всем проекте. Одними из самых популярных линтеров для Javascript являются ESLint, JSLint и JSHint. Для форматирования кода вы можете использовать Prettier. Хорошо то, что большинство IDE/редакторов кода, таких как Visual Studio Code (VSCode), Atom и т.д., понимают важность написания качественного кода и предоставляют плагины для линтинга и форматирования, которые очень интуитивно понятны и просты в настройке.

При написании кода еще одним важным моментом является добавление полезных комментариев, которыми могут воспользоваться другие разработчики в вашей команде. Достаточно всего лишь пяти-шести слов, чтобы подтолкнуть товарища по команде в правильном направлении к пониманию цели даже самого сложного фрагмента кода. Это сэкономит всем много времени и поможет избежать путаницы и, следовательно, всегда является беспроигрышной ситуацией.

Здесь следует отметить, что мы также должны быть разумны в своих комментариях, т.е. не комментировать слишком много и не комментировать слишком мало. Беспокоитесь о том, как найти баланс? Говоря словами Майкла Гэри Скотта, "Ты научишься, детка. Ты научишься!".

Комментарии также служат способом документирования API вашего проекта (для обзора на верхнем уровне, уведомления об авторских правах, информации об авторе и т.д.), его классов (описание, параметры), методов и именованных функций (описание, параметры, тип возврата и т.д.). Это также можно сделать с помощью инструментов генератора документации API, таких как JSDoc.

Лучшая практика №5. Пишите асинхронный код


Используйте промисы, синтаксис async/await. Javascript широко известен своими функциями обратного вызова (функции, которые могут быть переданы в качестве аргумента другим функциям). Они также позволяют определять асинхронное поведение в Javascript. Проблема с обратными вызовами заключается в том, что по мере увеличения количества цепочек операций ваш код становится все более громоздким, что приводит к тому, что печально известно как "ад обратных вызовов". Чтобы решить эту проблему, в ES 6 (ECMASCRIPT 2015) появился API Promises, который значительно упростил написание асинхронного кода в Javascript. В дополнение к этому в ES 8 (2017) был введен синтаксис async/await, чтобы еще больше упростить ситуацию и сделать API еще более интуитивным и естественным.

Поэтому рекомендуется отказаться от громоздких функций обратного вызова в пользу синтаксиса async/await и promises в вашем приложении Node.js. Это позволяет сделать код чище, читабельнее, легче обрабатывать ошибки и тестировать; все это при сохранении четкого потока управления и более последовательного функционального программирования.

Чтобы дать вам представление о том, насколько проще может быть ваша жизнь с async/await, вот сравнение обоих способов написания асинхронного кода.

Пример кода с callback функциями

<script>
    function get_data() {
        $.get('https://url.com/one', () => {
            $.get('https://url.com/two', () => {
                $.get('https://url.com/three', (res) => {
                    console.log(res)
                })
            })
        })
    }
</script>

Пример кода с async/await

<script>
    async function get_data() { // async function
        await $.get('https://url.com/one')
        await $.get('https://url.com/two')
        let res = await $.get('https://url.com/three')
        console.log(res)
    }
</script>

Лучшая практика №6. Конфигурационные файлы и переменные окружения


По мере расширения вашего приложения вы заметите, что некоторые глобальные параметры конфигурации и настройки должны быть доступны для всех модулей. Хорошей практикой всегда является хранение этих параметров вместе в отдельном файле в папке config в вашем проекте. Мы рассматривали эту папку ранее в разделе "Структура папок" этой статьи. Эта папка может содержать все различные параметры конфигурации, сгруппированные в файлы в зависимости от их использования.

  /config
        ├── index.js
        ├── module1.js
        └── module2.js

Эти параметры конфигурации могут содержать как обычные, базовые настройки, так и защищенные ключи API, URL подключения к базе данных и т.д. Последние должны храниться в файлах .env как переменные окружения. Вот как файл .env хранит данные в виде пар ключ-значение:

DB_HOST=localhost
DB_USER=root
DB_PASS=my_password_123

Эти .env файлы являются секретными файлами, которые не подлежат отслеживанию и версионированию (Git) и поэтому не фиксируются и не публикуются (за исключением первого раза с пустыми значениями).

Доступ к этим переменным окружения можно получить в коде с помощью пакета dotenv от npm, как показано ниже.

// app.js
require('dotenv').config()

console.log(process.env.DB_HOST) 
console.log(process.env.DB_USER)

Очень распространенной практикой разработки является импорт всех этих переменных (наряду с другими предопределенными опциями и настройками) в ваши конфигурационные файлы и представление их в виде объекта для остальной части вашего приложения. Таким образом, при необходимости вам нужно будет внести изменения в общие настройки только в одном месте, и они будут отражены во всем приложении. Вот фрагмент, показывающий, как это можно сделать:

// config/database.js

      require('dotenv').config()

      export default {
        host: process.env.DB_HOST,		
        user: process.env.DB_USER,
        pass: process.env.DB_PASS,
      }

Лучшая практика №7. Тестирование, логирование и обработка исключений


Ошибки - важная часть процесса, поэтому тестируйте свой код. Люди, начинающие разрабатывать программное обеспечение, часто упускают из виду важность написания тестовых примеров для своего кода. Однако тестирование является неотъемлемой частью любого программного приложения - оно позволяет проверить достоверность, точность и надежность вашего кода, выявляя даже самые незначительные неточности - не только в общей системе, но даже в отдельных ее составляющих. Тестирование позволяет сделать все это и даже больше, причем в удобной автоматизированной форме.

Юнит-тесты составляют основу большинства систем тестирования. Здесь отдельные блоки/компоненты тестируются в изоляции от остального кода для проверки их корректности. Это позволяет проверить ваш код на (логически) более низком уровне, чтобы убедиться, что каждый из внутренних компонентов работает точно, как ожидалось. Ниже приведен пример очень базового модульного теста (unit test)

// example.test.js

const assert = require('assert');

describe('Basic addition test', () => {
 it('should add up to 3', () => {
        assert.equal(2 + 1, 3);
    });
 
it('should equal 8', () => {
        assert.equal(4 * 2, 8);
    });
});

Эти тестовые примеры проверяют, возвращают ли компоненты вашего кода ожидаемые результаты или нет. Для разработки таких наборов тестов разработчикам Node.js предлагается множество фреймворков тестирования. Одними из самых популярных являются Mocha, Jest и Jasmine.

Логирование играет важную роль на всех этапах работы любого программного приложения: от разработки до тестирования и выпуска в производство, хорошо реализованная система логирования позволяет записывать важную информацию и понимать различные аспекты точности и производительности вашего приложения. Это не только позволяет вам лучше понимать и управлять вашим приложением, но и значительно облегчает отладку.

Javascript предоставляет множество функций для вывода и логирования информации. Для записи общей информации в журнал и отладки можно использовать - console.log(), console.info(), сonsole.debug() Для логирования ошибок и предупреждений - console.error(), console.warn() Он также позволяет передавать сообщения журнала либо на консоль, либо в файловый поток (с помощью оператора ' > '). Однако, если вы ищете больше функциональности и удобства для настройки логирования, вы можете рассмотреть возможность использования сторонних библиотек логирования в своем коде. Одними из наиболее распространенных библиотек логирования для Node.js являются Winston, Bunyan и Morgan.

Истина заключается в том, что ошибки - это хорошо, но для разработчиков. Они позволяют им понять неточности и уязвимости в их коде, предупреждая их, когда их код ломается. Они также предоставляют необходимую информацию о том, что пошло не так, где и что нужно сделать, чтобы исправить ситуацию.

Но вместо того, чтобы позволять Node.js выбрасывать исключения, прерывать выполнение кода и даже иногда выходить из строя, мы бы предпочли взять на себя управление потоком управления нашего приложения, обрабатывая эти условия ошибок. Именно этого мы можем добиться с помощью обработки исключений с использованием блоков try/catch. Предоставляя разработчикам возможность программно управлять такими исключениями, мы сохраняем стабильность, облегчаем отладку, а также предотвращаем негативный опыт конечного пользователя. Ниже приведен базовый пример блока try/catch в Node.js.

 try {
          if (xyzHappens) {
            throw "my error message ⚠️"; // 🛫				
          }
        }

        catch (e) {
          console.log(e); // 🛬
        }

        finally {
          console.log("Finally executed! 🏁");
        }

Лучшая практика №8. Сжатие и размер файла


Сжимайте с помощью Gzip! Gzip - это формат файлов без потерь (также программное приложение), используемый для сжатия (и распаковки) файлов для более быстрой передачи данных по сети. Поэтому он может быть чрезвычайно полезен для сжатия веб-страниц, обслуживаемых нашими серверами Node.js.

Такие фреймворки, как Express.js, позволяют невероятно просто настроить Gzip-сжатие с помощью промежуточного ПО сжатия. Использование Gzip-сжатия также является советом номер один, который документация Express.js рекомендует для повышения производительности приложений. Взгляните на то, как выглядит код для этого:

  var compression = require('compression')
  var express = require('express')
  var app = express()
  app.use(compression())	

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

В то время как вы работаете над оптимизацией производительности на стороне сервера, важно также следить за своим внешним кодом - быть в курсе размеров обслуживаемых веб-страниц. Поэтому перед обслуживанием необходимо минифицировать HTML, CSS, Javascript с помощью таких инструментов, как HTMLMinifier, CSSNano и UglifyJS. Эти минификаторы удаляют ненужные пробелы и комментарии из ваших файлов, а также выполняют некоторые тривиальные оптимизации компилятора, что в целом приводит к уменьшению размера файла.

Поэтому использование Gzip-сжатия вместе с минифицированным кодом фронтенда - это верный путь!

Лучшая практика № 9. Инъекция зависимостей


Инъекция зависимостей (Dependency Injection) - это паттерн проектирования программного обеспечения, который поддерживает передачу (инъекцию) зависимостей (или сервисов) в качестве параметров нашим модулям вместо того, чтобы требовать или создавать конкретные зависимости внутри них.

Это действительно модный термин для очень базовой концепции, которая позволяет сделать ваши модули более гибкими, независимыми, многократно используемыми, масштабируемыми и легко тестируемыми во всем приложении. Давайте посмотрим на код, чтобы понять, что это значит на самом деле.

Вот модуль в нашем коде, который раскрывает две функции для работы с произвольным классом Emoji. Он также использует модуль yellow-emojis, который, как можно предположить, работает с банком данных желтых эмодзи.

    const Emoji = require('./Emoji');
        const YellowEmojis = require('./yellow-emojis');

        async function getAllEmojis() {
          return YellowEmojis.getAll(); // 🌕 🌟 💛 🎗 🌼
        }

        async function addEmoji(emojiData) {
          const emoji = new Emoji(emojiData);

          return YellowEmojis.addEmoji(emoji);
        }

        module.exports = {
          getAllEmojis,
          addEmoji
        }	

Проблема с этой настройкой заключается в том, что она тесно связана с обслуживанием одного конкретного набора эмодзи (желтые). В результате, если вы захотите изменить свой код, чтобы иметь возможность работать с другим набором эмодзи (или несколькими наборами), вам придется создавать отдельные функции или переделывать текущую настройку.

Поэтому гораздо лучшим подходом было бы создание функций, которые не зависят от одного способа выполнения действий, а являются более общими и гибкими. Давайте посмотрим, как это можно сделать, внедрив зависимость в наш модуль:

 const Emoji = require('./Emoji');

            function EmojisService(emojiColor) {

                async function getAllEmojis() {
                    return emojiColor.getAll();					
                }

                async function addEmoji(emojiData) {
                    const emoji = new Emoji(emojiData);

                    return emojiColor.addEmoji(emoji);
                }

                return {
                    getAllEmojis,
                    addEmoji
                };		

            }

            module.exports = EmojisService

Здесь мы создаем новую функцию (EmojisService) для нашего сервиса, которая принимает нашу зависимость (emojiColor) в качестве параметра вместо того, чтобы зацикливаться на работе только с одним типом цвета (желтым). Это то, что мы подразумеваем под инъекцией зависимостей. Благодаря этому наш сервис представляет собой скорее общий интерфейс, который не только легко использовать повторно, но и легче тестировать. Это происходит потому, что в предыдущем случае нам пришлось бы создать заглушку yellow-emojis для тестирования модуля. Теперь же мы можем напрямую передать нашу зависимость emojiColor в тестовый пример.

Лучшая практика № 10. Решения сторонних производителей


Не изобретайте колесо заново. Но и не жадничайте. Node.js имеет огромное сообщество разработчиков по всему миру. Что касается поддержки сторонних разработчиков, менеджер пакетов Node, NPM, полон многофункциональных, хорошо поддерживаемых, хорошо документированных фреймворков, библиотек и инструментов для любого случая использования, который вы можете себе представить. Поэтому разработчикам очень удобно подключать эти существующие решения к своему коду и максимально использовать их API.

Как разработчику, вам полезно быть в поиске инструментов, которые облегчают вашу жизнь. Вот некоторые популярные библиотеки Node.js, которые могут эффективно улучшить ваши рабочие процессы кодирования:

  • Nodemon (автоматически перезапускает приложение при обновлении файлов кода)
  • Gulp, Grunt (автоматические программы для выполнения задач)
  • Winston (фреймворк для ведения логов)
  • Agenda (планирование заданий)
  • Moment (работа с датой и временем)

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

Лучшая практика №11. Следуйте хорошей общей практике написания кода


"Всегда пишите код так, как будто парень, который будет поддерживать ваш код, будет жестоким психопатом, который знает, где вы живете". - Мартин Голдинг

  • DRY (Don't Repeat Yourself): переиспользуйте код
  • Принцип единственной ответственности (SRP)
  • KISS (keep it simple and straightforward) провозглашает, что простота кода – превыше всего
  • Разделение ответственности (Separation of Concerns)
  • YAGNI (You ain't gonna need it) - не писать лишнего кода
  • Избегайте преждевременной оптимизации
  • Принципы программирования S.O.L.I.D
  • Инъекция зависимостей (Dependency injection)

Лучшая практика №12. Используйте инструменты мониторинга приложений


Для крупных производственных приложений одной из основных целей является лучшее понимание того, как пользователи взаимодействуют с приложением: какие маршруты или функции чаще всего используются, какие операции чаще всего выполняются и т. д. Кроме того, особое внимание уделяется оценке показателей производительности, проблем качества, узких мест, распространенных ошибок и т.д. и использованию этой информации для внесения необходимых изменений и улучшений. Именно здесь на помощь приходят инструменты мониторинга приложений

Заключение


Готовы начать свой проект Node.js? Отлично! Всю необходимую информацию вы найдете в этой статье. Несмотря на то, что существует еще множество аспектов грамотной веб-разработки, здесь представлены наиболее важные темы, которые необходимо учитывать при создании приложений Node.js.

В этом статье мы сначала рассмотрели внутренние аспекты архитектуры Node.js - мы узнали о его однопоточной архитектуре и механизме циклов событий для выполнения асинхронного кода. Затем мы перешли к пониманию различных аспектов того, что требуется для создания надежного, устойчивого и масштабируемого приложения Node.js. Мы рассмотрели 12 лучших практик, которые охватывают все: от логической структуры проекта до обзора логирования, тестирования, форматирования, линтинга, до тонкостей написания асинхронного кода и многого другого.

Теперь, когда вы знаете все о создании надежных, пуленепробиваемых приложений Node.js, идите вперед и внедрите все, что вы узнали сегодня, в свои существующие проекты или создайте их с нуля и поделитесь ими с миром. Удачи!