Node.js позволяет быстро и легко создавать приложения. Но из-за его асинхронного характера может быть трудно писать читаемый и управляемый код. В этой статье я покажу вам несколько советов о том, как этого добиться.
Обратный вызов Ад или Пирамида Судьбы
Node.js построен таким образом, что заставляет вас использовать асинхронные функции. Это означает обратные вызовы, обратные вызовы и даже больше обратных вызовов. Вы, наверное, видели или даже писали себе фрагменты кода, подобные этому:
app.get ('/ login', function (req, res) { sql.query ('SELECT 1 FROM пользователей WHERE name =?;', [req.param ('username')], функция (ошибка, строки) { if (error) { res.writeHead (500); return res.end (); } if (rows.length & lt; 1) { res.end ('Неверное имя пользователя!'); } еще { sql.query ('ВЫБЕРИТЕ 1 ИЗ ПОЛЬЗОВАТЕЛЕЙ, ГДЕ имя =? && пароль = MD5 (?);', [req.param ('имя пользователя'), req.param ('пароль')], функция (ошибка, строки) { if (error) { res.writeHead (500); return res.end (); } if (rows.length & lt; 1) { res.end ('Неправильный пароль!'); } еще { sql.query ('SELECT * FROM userdata WHERE name =?;', [req.param ('username')], функция (ошибка, строки) { if (error) { res.writeHead (500); return res.end (); } req.session.username = req.param ('username'); req.session.data = row [0]; res.rediect ( '/ userarea'); }); } }); } }); });
На самом деле это фрагмент прямо из одного из моих первых приложений Node.js. Если вы сделали что-то более сложное в Node.js, вы, вероятно, все понимаете, но проблема здесь в том, что код перемещается вправо каждый раз, когда вы используете какую-то асинхронную функцию. Становится все труднее читать и труднее отлаживать. К счастью, есть несколько решений для этого беспорядка, поэтому вы можете выбрать подходящее для вашего проекта.
Решение 1: именование и модульность обратного вызова
Самый простой подход — назвать каждый обратный вызов (который поможет вам отладить код) и разбить весь ваш код на модули. Приведенный выше пример входа в систему можно превратить в модуль за несколько простых шагов.
Структура
Давайте начнем с простой структуры модуля. Чтобы избежать описанной выше ситуации, когда вы просто разбиваете беспорядок на более мелкие, давайте рассмотрим класс:
var util = require ('util'); функция входа в систему (имя пользователя, пароль) { function _checkForErrors (ошибка, строки, причина) { } function _checkUsername (ошибка, строки) { } function _checkPassword (ошибка, строки) { } function _getData (ошибка, строки) { } function execute () { } this.perform = execute; } util.inherits (Логин, EventEmitter);
Класс построен с двумя параметрами: username
и password
. Рассматривая пример кода, нам нужны три функции: одна для проверки правильности имени пользователя ( _checkUsername
), другая для проверки пароля ( _checkPassword
) и еще одна для возврата данных, связанных с пользователем ( _getData
), и уведомление приложения о том, что вход был успешным. Существует также помощник _checkForErrors
, который будет обрабатывать все ошибки. Наконец, есть функция perform
, которая запускает процедуру входа в систему (и является единственной открытой функцией в классе). Наконец, мы наследуем от EventEmitter
чтобы упростить использование этого класса.
Помощник
Функция _checkForErrors
проверит, произошла ли какая-либо ошибка или запрос SQL не возвращает строк, и выдаст соответствующую ошибку (с _checkForErrors
предоставленной причины):
function _checkForErrors (ошибка, строки, причина) { if (error) { this.emit ('error', error); вернуть истину; } if (rows.length & lt; 1) { this.emit («неудача», причина); вернуть истину; } вернуть ложь; }
Он также возвращает true
или false
, в зависимости от того, произошла ошибка или нет.
Выполнение входа
Функция perform
должна будет выполнить только одну операцию: выполнить первый запрос SQL (чтобы проверить, существует ли имя пользователя) и назначить соответствующий обратный вызов:
function execute () { sql.query ('ВЫБЕРИТЕ 1 ИЗ ПОЛЬЗОВАТЕЛЕЙ, ГДЕ name =?;', [username], _checkUsername); }
Я предполагаю, что ваше SQL-соединение доступно глобально в переменной sql
(просто для упрощения, обсуждение того, является ли это хорошей практикой, выходит за рамки данной статьи). И это все для этой функции.
Проверка имени пользователя
Следующим шагом является проверка правильности имени пользователя и, если это так, запуск второго запроса — проверка пароля:
function _checkUsername (ошибка, строки) { if (_checkForErrors (error, row, 'username')) { вернуть ложь; } еще { sql.query ('ВЫБЕРИТЕ 1 ИЗ ПОЛЬЗОВАТЕЛЕЙ, ГДЕ имя =? && пароль = MD5 (?);', [имя пользователя, пароль], _checkPassword); } }
Примерно такой же код, как в грязном образце, за исключением обработки ошибок.
Проверка пароля
Эта функция почти такая же, как и предыдущая, единственное отличие заключается в запросе:
function _checkPassword (ошибка, строки) { if (_checkForErrors (error, row, 'password')) { вернуть ложь; } еще { sql.query ('SELECT * FROM userdata WHERE name =?;', [username], _getData); } }
Получение пользовательских данных
Последняя функция в этом классе получит данные, относящиеся к пользователю (необязательный шаг), и запустит с ним событие успеха:
function _getData (ошибка, строки) { if (_checkForErrors (error, row)) { вернуть ложь; } еще { this.emit ('success', row [0]); } }
Последние штрихи и использование
Последнее, что нужно сделать, это экспортировать класс. Добавьте эту строку после всего кода:
module.exports = Логин;
Это сделает класс Login
единственным, что будет экспортировать модуль. Позже его можно использовать следующим образом (при условии, что вы назвали файл модуля login.js
и он находится в том же каталоге, что и основной скрипт):
var Login = require ('./ login.js'); ... app.get ('/ login', function (req, res) { var login = new Login (req.param ('имя пользователя'), req.param ('пароль)); login.on ('error', function (error) { res.writeHead (500); Отправить(); }); login.on («ошибка», функция (причина) { if (reason == 'username') { res.end ('Неверное имя пользователя!'); } else if (причина == 'пароль') { res.end ('Неправильный пароль!'); } }); login.on ('success', function (data) { req.session.username = req.param ('username'); req.session.data = data; res.redirect ( '/ userarea'); }); login.perform (); });
Вот еще несколько строк кода, но читаемость кода заметно возросла. Кроме того, это решение не использует никаких внешних библиотек, что делает его идеальным, если кто-то новый приходит в ваш проект.
Это был первый подход, давайте перейдем ко второму.
Решение 2: Обещания
Использование обещаний — еще один способ решения этой проблемы. Обещание (как вы можете прочитать в приведенной ссылке) «представляет конечное значение, возвращаемое при однократном завершении операции». На практике это означает, что вы можете объединить вызовы, чтобы сгладить пирамиду и сделать код более легким для чтения.
Мы будем использовать модуль Q , доступный в репозитории NPM.
Q в двух словах
Прежде чем мы начнем, позвольте мне познакомить вас с Q. Для статических классов (модулей) мы в первую очередь будем использовать функцию Q.nfcall
. Это помогает нам в преобразовании каждой функции, следующей за шаблоном обратного вызова Node.js (где параметрами обратного вызова являются ошибка и результат), в обещание. Используется так:
Q.nfcall (http.get, options);
Это очень похоже на Object.prototype.call
. Вы также можете использовать Q.nfapply
который похож на Object.prototype.apply
:
Q.nfapply (fs.readFile, ['filename.txt', 'utf-8']);
Кроме того, когда мы создаем обещание, мы добавляем каждый шаг с помощью метода then(stepCallback)
, отлавливаем ошибки с помощью catch(errorCallback)
и заканчиваем с помощью done()
.
В этом случае, поскольку объект sql
является экземпляром, а не статическим классом, мы должны использовать Q.ninvoke
или Q.npost
, которые аналогичны приведенным выше. Разница в том, что мы передаем имя метода в виде строки в первом аргументе, а экземпляр класса, с которым мы хотим работать, — во втором, чтобы избежать отвлечения метода от экземпляра.
Готовим обещание
Первое, что нужно сделать, это выполнить первый шаг, используя Q.nfcall
или Q.nfapply
(используйте тот, который вам нравится больше, разницы нет):
var Q = require ('q'); ... app.get ('/ login', function (req, res) { Q.ninvoke ('query', sql, 'SELECT 1 FROM пользователей WHERE name =?;', [Req.param ('username')]) });
Обратите внимание на отсутствие точки с запятой в конце строки — вызовы функций будут соединены в цепочку, поэтому их там не будет. Мы просто вызываем sql.query
как в грязном примере, но мы опускаем параметр обратного вызова — он обрабатывается обещанием.
Проверка имени пользователя
Теперь мы можем создать обратный вызов для SQL-запроса, он будет практически идентичен тому, что был в примере с «пирамидой гибели». Добавьте это после вызова Q.ninvoke
:
.then (функция (строки) { if (rows.length & lt; 1) { res.end ('Неверное имя пользователя!'); } еще { return Q.ninvoke ('query', sql, 'SELECT 1 FROM users WHERE name =? && password = MD5 (?);', [req.param ('username'), req.param ('password')]) ; } })
Как вы можете видеть, мы прикрепляем обратный вызов (следующий шаг) с помощью метода then
. Кроме того, в обратном вызове мы опускаем параметр error
, потому что мы будем перехватывать все ошибки позже. Мы вручную проверяем, возвращал ли запрос что-то, и если да, то возвращаем следующее обещание, которое будет выполнено (опять же, без точки с запятой из-за цепочки).
Проверка пароля
Как и в примере с модуляризацией, проверка пароля практически идентична проверке имени пользователя. Это должно идти сразу после последнего, then
позвоните:
.then (функция (строки) { if (rows.length & lt; 1) { res.end ('Неправильный пароль!'); } еще { return Q.ninvoke ('query', sql, 'SELECT * FROM userdata WHERE name =?;', [req.param ('username')]); } })
Получение пользовательских данных
Последним шагом будет тот, где мы помещаем данные пользователей в сеанс. Еще раз, обратный вызов не сильно отличается от грязного примера:
.then (функция (строки) { req.session.username = req.param ('username'); req.session.data = row [0]; res.rediect ( '/ userarea'); })
Проверка на ошибки
При использовании обещаний и библиотеки Q все ошибки обрабатываются с помощью обратного вызова, установленного с помощью метода catch
. Здесь мы отправляем только HTTP 500 независимо от ошибки, как в приведенных выше примерах:
.catch (функция (ошибка) { res.writeHead (500); Отправить(); }) .сделано();
После этого мы должны вызвать метод done
чтобы «убедиться, что, если ошибка не будет обработана до конца, она будет переброшена и сообщена» (из библиотеки README). Теперь наш прекрасно сплющенный код должен выглядеть так (и вести себя так же, как грязный):
var Q = require ('q'); ... app.get ('/ login', function (req, res) { Q.ninvoke ('query', sql, 'SELECT 1 FROM пользователей WHERE name =?;', [Req.param ('username')]) .then (функция (строки) { if (rows.length & lt; 1) { res.end ('Неверное имя пользователя!'); } еще { return Q.ninvoke ('query', sql, 'SELECT 1 FROM users WHERE name =? && password = MD5 (?);', [req.param ('username'), req.param ('password')]) ; } }) .then (функция (строки) { if (rows.length & lt; 1) { res.end ('Неправильный пароль!'); } еще { return Q.ninvoke ('query', sql, 'SELECT * FROM userdata WHERE name =?;', [req.param ('username')]); } }) .then (функция (строки) { req.session.username = req.param ('username'); req.session.data = row [0]; res.rediect ( '/ userarea'); }) .catch (функция (ошибка) { res.writeHead (500); Отправить(); }) .сделано(); });
Код намного чище и требует меньше переписывания, чем подход модульности.
Решение 3: Библиотека шагов
Это решение похоже на предыдущее, но оно проще. Q немного тяжеловат, потому что он реализует идею всех обещаний. Библиотека Step существует только для того, чтобы сгладить ад обратного вызова. Это также немного проще в использовании, потому что вы просто вызываете единственную функцию, которая экспортируется из модуля, передаете все ваши обратные вызовы в качестве параметров и используете this
вместо каждого обратного вызова. Таким образом, грязный пример можно преобразовать в это, используя модуль Step:
var step = require ('step'); ... app.get ('/ login', function (req, res) { шаг ( function start () { sql.query ('ВЫБЕРИТЕ 1 ИЗ ПОЛЬЗОВАТЕЛЕЙ, ГДЕ name =?;', [req.param ('username')], this); }, function checkUsername (ошибка, строки) { if (error) { res.writeHead (500); return res.end (); } if (rows.length & lt; 1) { res.end ('Неверное имя пользователя!'); } еще { sql.query ('ВЫБЕРИТЕ 1 ИЗ ПОЛЬЗОВАТЕЛЕЙ, ГДЕ name =? && password = MD5 (?);', [req.param ('username'), req.param ('password')], this); } }, function checkPassword (ошибка, строки) { if (error) { res.writeHead (500); return res.end (); } if (rows.length & lt; 1) { res.end ('Неправильный пароль!'); } еще { sql.query ('SELECT * FROM userdata WHERE name =?;', [req.param ('username')], this); } }, функция (ошибка, строки) { if (error) { res.writeHead (500); return res.end (); } req.session.username = req.param ('username'); req.session.data = row [0]; res.rediect ( '/ userarea'); } ); });
Недостатком здесь является то, что нет общего обработчика ошибок. Хотя любые исключения, сгенерированные в одном обратном вызове, передаются следующему как первый параметр (поэтому сценарий не отключится из-за необработанного исключения), в большинстве случаев удобно иметь один обработчик для всех ошибок.
Какой выбрать?
Это в значительной степени личный выбор, но чтобы помочь вам выбрать правильный, вот список плюсов и минусов каждого подхода:
Модульность:
Плюсы:
- Нет внешних библиотек
- Помогает сделать код более пригодным для повторного использования
Минусы:
- Больше кода
- Много переписывать, если вы конвертируете существующий проект
Обещания (Q):
Плюсы:
- Меньше кода
- Только немного переписать, если применить к существующему проекту
Минусы:
- Вы должны использовать внешнюю библиотеку
- Требуется немного обучения
Библиотека шагов:
Плюсы:
- Простота в использовании, обучение не требуется
- Довольно много копируй и вставляй, если конвертируешь существующий проект
Минусы:
- Нет общего обработчика ошибок
- Немного сложнее сделать правильный
step
Вывод
Как видите, асинхронной природой Node.js можно управлять и избежать ада обратного вызова. Я лично использую подход модульности, потому что мне нравится, чтобы мой код был хорошо структурирован. Я надеюсь, что эти советы помогут вам писать код более читабельно и легче отлаживать сценарии.