Статьи

Управление асинхронной природой Node.js

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


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

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


Самый простой подход — назвать каждый обратный вызов (который поможет вам отладить код) и разбить весь ваш код на модули. Приведенный выше пример входа в систему можно превратить в модуль за несколько простых шагов.

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

Класс построен с двумя параметрами: username и password . Рассматривая пример кода, нам нужны три функции: одна для проверки правильности имени пользователя ( _checkUsername ), другая для проверки пароля ( _checkPassword ) и еще одна для возврата данных, связанных с пользователем ( _getData ), и уведомление приложения о том, что вход был успешным. Существует также помощник _checkForErrors , который будет обрабатывать все ошибки. Наконец, есть функция perform , которая запускает процедуру входа в систему (и является единственной открытой функцией в классе). Наконец, мы наследуем от EventEmitter чтобы упростить использование этого класса.

Функция _checkForErrors проверит, произошла ли какая-либо ошибка или запрос SQL не возвращает строк, и выдаст соответствующую ошибку (с _checkForErrors предоставленной причины):

Он также возвращает true или false , в зависимости от того, произошла ошибка или нет.

Функция perform должна будет выполнить только одну операцию: выполнить первый запрос SQL (чтобы проверить, существует ли имя пользователя) и назначить соответствующий обратный вызов:

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

Следующим шагом является проверка правильности имени пользователя и, если это так, запуск второго запроса — проверка пароля:

Примерно такой же код, как в грязном образце, за исключением обработки ошибок.

Эта функция почти такая же, как и предыдущая, единственное отличие заключается в запросе:

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

Последнее, что нужно сделать, это экспортировать класс. Добавьте эту строку после всего кода:

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

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

Это был первый подход, давайте перейдем ко второму.


Использование обещаний — еще один способ решения этой проблемы. Обещание (как вы можете прочитать в приведенной ссылке) «представляет конечное значение, возвращаемое при однократном завершении операции». На практике это означает, что вы можете объединить вызовы, чтобы сгладить пирамиду и сделать код более легким для чтения.

Мы будем использовать модуль Q , доступный в репозитории NPM.

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

Это очень похоже на Object.prototype.call . Вы также можете использовать Q.nfapply который похож на Object.prototype.apply :

Кроме того, когда мы создаем обещание, мы добавляем каждый шаг с помощью метода then(stepCallback) , отлавливаем ошибки с помощью catch(errorCallback) и заканчиваем с помощью done() .

В этом случае, поскольку объект sql является экземпляром, а не статическим классом, мы должны использовать Q.ninvoke или Q.npost , которые аналогичны приведенным выше. Разница в том, что мы передаем имя метода в виде строки в первом аргументе, а экземпляр класса, с которым мы хотим работать, — во втором, чтобы избежать отвлечения метода от экземпляра.

Первое, что нужно сделать, это выполнить первый шаг, используя Q.nfcall или Q.nfapply (используйте тот, который вам нравится больше, разницы нет):

Обратите внимание на отсутствие точки с запятой в конце строки — вызовы функций будут соединены в цепочку, поэтому их там не будет. Мы просто вызываем sql.query как в грязном примере, но мы опускаем параметр обратного вызова — он обрабатывается обещанием.

Теперь мы можем создать обратный вызов для SQL-запроса, он будет практически идентичен тому, что был в примере с «пирамидой гибели». Добавьте это после вызова Q.ninvoke :

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

Как и в примере с модуляризацией, проверка пароля практически идентична проверке имени пользователя. Это должно идти сразу после последнего, then позвоните:

Последним шагом будет тот, где мы помещаем данные пользователей в сеанс. Еще раз, обратный вызов не сильно отличается от грязного примера:

При использовании обещаний и библиотеки Q все ошибки обрабатываются с помощью обратного вызова, установленного с помощью метода catch . Здесь мы отправляем только HTTP 500 независимо от ошибки, как в приведенных выше примерах:

После этого мы должны вызвать метод done чтобы «убедиться, что, если ошибка не будет обработана до конца, она будет переброшена и сообщена» (из библиотеки README). Теперь наш прекрасно сплющенный код должен выглядеть так (и вести себя так же, как грязный):

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


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

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


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

Плюсы:

  • Нет внешних библиотек
  • Помогает сделать код более пригодным для повторного использования

Минусы:

  • Больше кода
  • Много переписывать, если вы конвертируете существующий проект

Плюсы:

  • Меньше кода
  • Только немного переписать, если применить к существующему проекту

Минусы:

  • Вы должны использовать внешнюю библиотеку
  • Требуется немного обучения

Плюсы:

  • Простота в использовании, обучение не требуется
  • Довольно много копируй и вставляй, если конвертируешь существующий проект

Минусы:

  • Нет общего обработчика ошибок
  • Немного сложнее сделать правильный step

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