本文概述
- 错误1:阻止事件循环
- 错误2:多次调用回呼
- 错误3:深层嵌套回调
- 错误4:期望回调同步运行
- 错误5:分配给” 导出” 而不是” module.exports”
- 错误#6:内部回调引发错误
- 错误7:假设数字为整数数据类型
- 错误8:忽略流API的优势
- 错误9:使用Console.log进行调试
- 错误10:不使用主管程序
- 总结
但是, 像其他平台一样, Node.js容易受到开发人员问题的困扰。这些错误中的一些会降低性能, 而另一些错误则会使Node.js似乎对于你要实现的目标都无法使用。在本文中, 我们将介绍Node.js新手开发人员经常犯的十个常见错误, 以及如何避免成为Node.js pro的错误。
文章图片
错误1:阻止事件循环 Node.js中的JavaScript(就像在浏览器中一样)提供了单线程环境。这意味着你的应用程序中没有两个部分可以并行运行。相反, 并发是通过异步处理I / O绑定操作来实现的。例如, 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实例中连接了成千上万个客户端的一段CPU绑定代码是阻止事件循环所需的全部工作, 从而使所有客户端都需要等待。受CPU约束的代码包括尝试对大型数组进行排序, 运行极长的循环等。例如:
function sortUsersByAge(users) {
users.sort(function(a, b) {
return a.age <
b.age ? -1 : 1
})
}
如果在较小的” 用户” 阵列上运行, 则调用此” sortUsersByAge” 功能可能会很好, 但是对于较大的阵列, 它将对整体性能产生可怕的影响。如果这是绝对必须完成的事情, 并且你确定在事件循环上没有其他等待(例如, 如果这是你使用Node.js构建的命令行工具的一部分, 那么它整个过程是否同步都没有关系), 那么这可能不是问题。但是, 在试图一次为数千个用户提供服务的Node.js服务器实例中, 这种模式可能会致命。
如果要从数据库中检索此用户数组, 则理想的解决方案是直接从数据库中获取已排序的用户。如果事件循环被编写来计算金融交易数据历史记录之和的循环所阻塞, 则可以将其推迟到某些外部工作程序/队列设置中, 以避免占用事件循环。
如你所见, 没有针对此类Node.js问题的解决方案, 而是需要单独解决每种情况。基本思想是不要在前端的Node.js实例(客户端同时连接的实例)中进行CPU密集型工作。
错误2:多次调用回呼 JavaScript一直以来都依赖于回调。在Web浏览器中, 事件是通过传递对充当回调函数的(通常是匿名的)函数的引用来处理的。在Node.js中, 回调曾经是代码中异步元素相互通信的唯一方式-直到引入了promises为止。回调仍在使用中, 程序包开发人员仍围绕回调设计其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语句。这是因为调用回调不会自动结束当前函数的执行。如果第一个” return” 已被注释掉, 则向该函数传递非字符串密码仍然会导致调用” computeHash” 。根据” computeHash” 如何处理这种情况, “ 完成” 可能会被多次调用。当多次调用通过的回调时, 从其他地方使用此功能的任何人都可能会完全措手不及。
要避免该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中以不同方式处理错误的方式, 这使得必须对所有回调函数参数遵循(err, …)模式-如果发生一个回调, 则所有回调的第一个参数都将是一个错误。 。
错误7:假设数字为整数数据类型 JavaScript中的数字是浮点数-没有整数数据类型。你不会期望这会成为问题, 因为通常不会遇到足够大的数字来强调浮动限制。恰恰是与此相关的错误发生时。由于浮点数最多只能容纳某个特定值的整数表示, 因此在任何计算中超过该值将立即开始将其弄乱。看起来似乎很奇怪, 但以下内容在Node.js中的评估结果为true:
Math.pow(2, 53)+1 === Math.pow(2, 53)
不幸的是, JavaScript中带有数字的怪癖并没有到此结束。即使Numbers是浮点数, 使用整数数据类型的运算符也可以在这里工作:
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获取图像, 将其读取到Buffer中, 然后响应请求。鉴于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代码是在生产环境中运行还是在本地开发环境中运行, 能够协调你的程序的主管程序监视器都是一件非常有用的事情。开发人员在设计和实现现代应用程序时经常建议的一种做法是, 建议你的代码应快速失败。如果发生意外错误, 请不要尝试处理它, 而要让你的程序崩溃并让管理员在几秒钟内重新启动它。主管程序的好处不仅限于重启崩溃的程序。这些工具使你可以在崩溃时重新启动程序, 以及在某些文件更改时重新启动程序。这使开发Node.js程序变得更加愉快。
有大量可用于Node.js的主管程序。例如:
- 【Node.js开发人员最常犯的10个错误】下午2
- 永远
- Nodemon
- 主管
总结 如你所知, 其中一些Node.js问题可能会对你的程序产生破坏性影响。当你尝试在Node.js中实现最简单的操作时, 可能会导致挫败感。尽管Node.js使得新手入门变得非常容易, 但它仍然具有容易混淆的地方。其他编程语言的开发人员也许可以解决其中一些问题, 但是这些错误在新的Node.js开发人员中非常普遍。幸运的是, 它们很容易避免。我希望这份简短的指南可以帮助初学者在Node.js中编写更好的代码, 并为我们所有人开发稳定高效的软件。
推荐阅读
- Buggy Java代码(Java开发人员最常犯的10个错误)
- 开发人员最常见的10大C++错误
- Rails上的发布-订阅模式(实现教程)
- Linux磁盘管理
- 5 种在循环中使用 async/await 的方法,建议收藏
- vim怪异的高亮块
- JVM运行时数据区了解一下()
- 10 个 Vue3 实战指南,冲就完事了
- CentOS 7 下 Yum 安装 MySQL 5.7