10 распространенных ошибок разработчиков Node.js

JavaScript
10 распространенных ошибок разработчиков 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 с тысячами клиентских подключений небольшой фрагмент кода, интенсивно использующего ЦП, может блокировать цикл обработки событий, заставляя всех клиентов ждать. Код с интенсивным использованием процессора включает в себя попытку сортировки огромного массива, запуск функции, которая занимает много времени, и т. д. Например:

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

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

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

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

Ошибка 2: Многократный вызов callback-функции

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

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. Это связано с тем, что вызов функции обратного вызова не завершает автоматически выполнение текущего метода. Если бы мы закомментировали первый оператор return и передали функции нестроковый пароль, мы бы все равно вызвали метод calculateHash. В зависимости от того, как calculateHash обрабатывает эту ситуацию, функция "done" будет вызываться несколько раз. Любой может быть застигнут врасплох, когда переданный обратный вызов вызывается несколько раз.

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

if(err) {
	return done(err)
}

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

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

Глубоко вложенные функции обратного вызова часто называют «адом обратных вызовов», что по своей сути не является проблемой, но может привести к тому, что код быстро выйдет из-под контроля:

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 для обработки этого асинхронного js, например 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.js также предоставляет множество методов, таких как «async.waterfall», для обработки различных асинхронных сценариев. Для краткости здесь мы показываем простой пример, реальность часто намного сложнее.

(Для рекламы генератор, упомянутый в «ES6 Generator Introduction» по соседству, также может решить ад обратных вызовов, и более естественно использовать его с Promise, пожалуйста, ждите следующей статьи от соседнего домовладельца: D)

Ошибка 4: Ожидание синхронного выполнения callback-функций

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

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

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

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

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

Ошибка 5: Присвоение значения «exports» вместо «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!’)
}

Однако в асинхронной среде tary-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 по-разному, и поэтому все аргументы функции обратного вызова должны иметь форму (err,...), где первый аргумент — это объект ошибки, когда произошла ошибка.

Ошибка 7: Мыслительное число — это целочисленный формат данных

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

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 всегда дает 0:

Math.pow(2, 53) / 2 === Math.pow(2, 52) // true
Math.pow(2, 53) >> 1 === 0 // true
Math.pow(2, 53) | 1 === 0 // 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)

В этом примере мы извлекаем изображение из 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. Использовать его очень просто:

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

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

DEBUG=app node app.js

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

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

Node.js имеет слишком много доступных программ мониторинга, таких как:

pm2

forever

nodemon

supervisor

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

Суммировать

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

Присоединяйтесь к нам, чтобы учиться вместе!группа обмена обучением узлов

Если группа обмена состоит из 100 человек, она не может автоматически присоединиться к группе.Пожалуйста, добавьте учетную запись WeChat помощника группы: узел [coder_qi] Note, и он автоматически втянет вас в группу.