10 самых распространенных ошибок, которые допускают разработчики Node.js

Опубликовано: 2022-03-11

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

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

ошибки разработчика node.js

Ошибка №1: блокировка цикла событий

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

 // Trying to fetch an user object from the database. Node.js is free to run other parts of the code from the moment this function is invoked.. db.User.get(userId, function(err, user) { // .. until the moment the user object has been retrieved here }) 

Однопоточная среда node.js

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

 function sortUsersByAge(users) { users.sort(function(a, b) { return a.age < b.age ? -1 : 1 }) }

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

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

Как видите, универсального решения такой проблемы с Node.js не существует, каждый случай нужно рассматривать индивидуально. Фундаментальная идея состоит в том, чтобы не выполнять интенсивную работу ЦП в инстансах Node.js, обращенных к фронту — тех, к которым клиенты подключаются одновременно.

Ошибка № 2: вызов обратного вызова более одного раза

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

 module.exports.verifyPassword = function(user, password, done) { if(typeof password !== 'string') { done(new Error('password should be a string')) return } computeHash(password, user.passwordHashOpts, function(err, hash) { if(err) { done(err) return } done(null, hash === user.passwordHash) }) }

Обратите внимание, что каждый раз, когда вызывается «done», появляется оператор return, вплоть до самого последнего раза. Это связано с тем, что вызов обратного вызова не завершает автоматически выполнение текущей функции. Если первый «возврат» был закомментирован, передача нестрокового пароля в эту функцию все равно приведет к вызову «computeHash». В зависимости от того, как «computeHash» справляется с таким сценарием, «done» может вызываться несколько раз. Любой, кто использует эту функцию из другого места, может быть застигнут врасплох, если обратный вызов, который они передают, вызывается несколько раз.

Чтобы избежать этой ошибки Node.js, нужно быть осторожным. Некоторые разработчики Node.js имеют привычку добавлять ключевое слово return перед каждым вызовом обратного вызова:

 if(err) { return done(err) }

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

Ошибка № 3: Глубоко вложенные обратные вызовы

Глубоко вложенные обратные вызовы, часто называемые «адом обратных вызовов», сами по себе не являются проблемой Node.js. Однако это может привести к проблемам, из-за которых код быстро выйдет из-под контроля:

 function handleLogin(..., done) { db.User.get(..., function(..., user) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) }) }) } 

ад обратного звонка

Чем сложнее задача, тем хуже может быть. Вкладывая обратные вызовы таким образом, мы легко получаем подверженный ошибкам, трудный для чтения и поддержки код. Один из обходных путей — объявить эти задачи как небольшие функции, а затем связать их. Хотя одним из (возможно) самых чистых решений для этого является использование служебного пакета Node.js, который работает с асинхронными шаблонами JavaScript, такими как Async.js:

 function handleLogin(done) { async.waterfall([ function(done) { db.User.get(..., done) }, function(user, done) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { done(null, user, okay) }) }, function(user, okay, done) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) } ], function() { // ... }) }

Подобно «async.waterfall», существует ряд других функций, которые Async.js предоставляет для работы с различными асинхронными шаблонами. Для краткости мы использовали здесь более простые примеры, но реальность часто бывает хуже.

Ошибка № 4: Ожидание, что обратные вызовы будут выполняться синхронно

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

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

 function testTimeout() { console.log(“Begin”) setTimeout(function() { console.log(“Done!”) }, duration * 1000) console.log(“Waiting..”) }

Как вы заметили, вызов функции «testTimeout» сначала напечатает «Begin», затем напечатает «Waiting..», а затем сообщение «Done!» примерно через секунду.

Все, что должно произойти после срабатывания обратного вызова, должно быть вызвано из него.

Ошибка № 5: Назначение «экспорта» вместо «module.exports»

Node.js рассматривает каждый файл как небольшой изолированный модуль. Если в вашем пакете есть два файла, например «a.js» и «b.js», то для того, чтобы «b.js» мог получить доступ к функциям «a.js», «a.js» должен экспортировать его, добавив свойства в объект экспорта:

 // a.js exports.verifyPassword = function(user, password, done) { ... }

Когда это будет сделано, любому, кому требуется «a.js», будет предоставлен объект с функцией свойства «verifyPassword»:

 // b.js require('a.js') // { verifyPassword: function(user, password, done) { ... } }

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

 // a.js module.exports = function(user, password, done) { ... }

Обратите внимание, как мы рассматриваем «экспорт» как свойство объекта модуля. Различие между «module.exports» и «exports» очень важно и часто вызывает разочарование у новых разработчиков Node.js.

Ошибка № 6: Выдача ошибок из внутренних обратных вызовов

В JavaScript есть понятие исключений. Подражая синтаксису почти всех традиционных языков с поддержкой обработки исключений, таких как Java и C++, JavaScript может «генерировать» и перехватывать исключения в блоках try-catch:

 function slugifyUsername(username) { if(typeof username === 'string') { throw new TypeError('expected a string username, got '+(typeof username)) } // ... } try { var usernameSlug = slugifyUsername(username) } catch(e) { console.log('Oh no!') }

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

 try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... usernameSlug = slugifyUsername(user.username) // ... }) } catch(e) { console.log('Oh no!') }

Если бы обратный вызов «db.User.get» запускался асинхронно, область действия, содержащая блок try-catch, давно бы вышла из контекста, чтобы по-прежнему иметь возможность перехватывать эти ошибки, выдаваемые внутри обратного вызова.

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

Ошибка № 7: Предполагать, что число является целым типом данных

Числа в JavaScript представляют собой числа с плавающей запятой — целочисленного типа данных не существует. Вы не ожидаете, что это будет проблемой, так как числа, достаточно большие, чтобы подчеркнуть пределы числа с плавающей запятой, встречаются не часто. Именно тогда случаются ошибки, связанные с этим. Поскольку числа с плавающей запятой могут содержать целочисленные представления только до определенного значения, превышение этого значения в любом вычислении немедленно начнет его путать. Как ни странно, в Node.js следующее оценивается как true:

 Math.pow(2, 53)+1 === Math.pow(2, 53)

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

 5 % 2 === 1 // true 5 >> 1 === 2 // true

Однако, в отличие от арифметических операторов, побитовые операторы и операторы сдвига работают только с последними 32 битами таких больших «целых» чисел. Например, попытка сдвинуть «Math.pow(2, 53)» на 1 всегда будет оцениваться как 0. Попытка выполнить побитовое или 1 с таким же большим числом даст 1.

 Math.pow(2, 53) / 2 === Math.pow(2, 52) // true Math.pow(2, 53) >> 1 === 0 // true Math.pow(2, 53) | 1 === 1 // true

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

Ошибка № 8: игнорирование преимуществ потоковых API

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

 var http = require('http') var crypto = require('crypto') http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } var buf = new Buffer(1024*1024) http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { var size = 0 resp.on('data', function(chunk) { chunk.copy(buf, size) size += chunk.length }) .on('end', function() { res.write(buf.slice(0, size)) res.end() }) }) }) .listen(8080)

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

 http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { resp.pipe(res) }) }) .listen(8080)

Здесь мы извлекаем изображение и просто передаем ответ клиенту. Нам ни в коем случае не нужно считывать весь контент в буфер перед его обслуживанием.

Ошибка № 9: Использование Console.log для целей отладки

В Node.js «console.log» позволяет выводить на консоль почти все что угодно. Передайте ему объект, и он напечатает его как литерал объекта JavaScript. Он принимает любое произвольное количество аргументов и выводит их все аккуратно через пробел. Есть ряд причин, по которым разработчик может испытать искушение использовать это для отладки своего кода; однако настоятельно рекомендуется избегать «console.log» в реальном коде. Вам не следует писать «console.log» по всему коду для его отладки, а затем комментировать их, когда они больше не нужны. Вместо этого используйте одну из замечательных библиотек, созданных именно для этого, например, debug.

Подобные пакеты предоставляют удобные способы включения и отключения определенных строк отладки при запуске приложения. Например, с помощью отладки можно предотвратить вывод любых строк отладки на терминал, не устанавливая переменную среды DEBUG. Использовать его просто:

 // app.js var debug = require('debug')('app') debug('Hello, %s!', 'world')

Чтобы включить строки отладки, просто запустите этот код с переменной среды DEBUG, установленной в «приложение» или «*»:

 DEBUG=app node app.js

Ошибка № 10: Неиспользование программ Supervisor

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

Для Node.js доступно множество супервизорных программ. Например:

  • pm2

  • навсегда

  • нодмон

  • руководитель

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

Заключение

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