Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Node.js 十大常见的开发者错误

Node.js 十大常见的开发者错误

作者头像
coder_koala
发布于 2019-08-28 05:41:57
发布于 2019-08-28 05:41:57
1.3K00
代码可运行
举报
运行总次数:0
代码可运行

前言

自 Node.js 面世以来,它获得了大量的赞美和批判。这种争论会一直持续,短时间内都不会结束。而在这些争论中,我们常常会忽略掉所有语言和平台都是基于一些核心问题来批判的,就是我们怎么去使用这些平台。无论使用 Node.js 编写可靠的代码有多难,而编写高并发代码又是多么的简单,这个平台终究是有那么一段时间了,而且被用来创建了大量的健壮而又复杂的 web 服务。这些 web 服务不仅拥有良好的扩展性,而且通过在互联网上持续的时间证明了它们的健壮性。

然而就像其它平台一样,Node.js 很容易令开发者犯错。这些错误有些会降低程序性能,有些则会导致 Node.js 不可用。在本文中,我们会看到 Node.js 新手常犯的 十种错误,以及如何去避免它们。

错误1:阻塞事件循环

Node.js(正如浏览器)里的 JavaScript 提供了一种单线程环境。这意味着你的程序不会有两块东西同时在运行,取而代之的是异步处理 I/O 密集操作所带来的并发。比如说 Node.js 给数据库发起一个请求去获取一些数据时,Node.js 可以集中精力在程序的其他地方:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 计算密集型代码包括了尝试排序一个巨大的数组、跑一个耗时很长的函数等等。例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function sortUsersByAge(users) {    users.sort(function(a, b) {        return a.age < b.age ? -1 : 1    })}

在一个小的“users” 数组上调用“sortUsersByAge” 方法是没有任何问题的,但如果是在一个大数组上,它会对整体性能造成巨大的影响。如果这种事情不得不做,而且你能确保事件循环上没有其他事件在等待(比如这只是一个 Node.js 命令行工具,而且它不在乎所有事情都是同步工作的)的话,那这没有问题。但是,在一个 Node.js 服务器试图给上千用户同时提供服务的情况下,它就会引发问题。

如果这个 users 数组是从数据库获取的,那么理想的解决方案是从数据库里拿出已排好序的数据。如果事件循环被一个计算金融交易数据历史总和的循环所阻塞,这个计算循环应该被推到事件循环外的队列中执行以免占用事件循环。

正如你所见,解决这类错误没有银弹,只有针对每种情况单独解决。基本理念是不要在处理客户端并发连接的 Node.js 实例上做 CPU 计算密集型工作。

错误2:多次调用一个回调函数

一直以来 JavaScript 都依赖于回调函数。在浏览器里,事件都是通过传递事件对象的引用给一个回调函数(通常都是匿名函数)来处理。在 Node.js 里,回调函数曾经是与其他代码异步通信的唯一方式,直到 promise 出现。回调函数现在仍在使用,而且很多开发者依然围绕着它来设置他们的 API。一个跟使用回调函数相关的常见错误是多次调用它们。通常,一个封装了一些异步处理的方法,它的最后一个参数会被设计为传递一个函数,这个函数会在异步处理完后被调用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 语句,然后传一个非字符串类型的 password 给这个函数,我们依然会以调用 computeHash 方法结束。根据 computeHash 在这种情况下的处理方式,“done” 函数会被调用多次。当传过去的回调函数被多次调用时,任何人都会被弄得措手不及。

避免这个问题只需要小心点即可。一些 Node.js 开发者因此养成了一个习惯,在所有调用回调函数的语句前加一个 return 关键词:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if(err) {    return done(err)}

在很多异步函数里,这种 return 的返回值都是没有意义的,所以这种举动只是为了简单地避免这个错误而已。

错误3:深层嵌套的回调函数

深层嵌套的回调函数通常被誉为“ 回调地狱”,它本身并不是什么问题,但是它会导致代码很快变得失控:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 介绍》提及的 Generator 也是可以解决回调地狱的哦,而且结合 Promise 使用更加自然,请期待隔壁楼主的下篇文章吧:D)

错误4:期待回调函数同步执行

使用回调函数的异步程序不只是 JavaScript 和 Node.js 有,只是它们让这种异步程序变得流行起来。在其他编程语言里,我们习惯了两个语句一个接一个执行,除非两个语句之间有特殊的跳转指令。即使那样,这些还受限于条件语句、循环语句以及函数调用。

然而在 JavaScript 里,一个带有回调函数的方法直到回调完成之前可能都无法完成任务。当前函数会一直执行到底:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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” 必须要通过给 exports 对象增加属性来暴露这些功能:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// a.jsexports.verifyPassword = function(user, password, done) { ... }

完成这步后,所有需要“a.js” 的都会获得一个带有“verifyPassword” 函数属性的对象:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// b.jsrequire(‘a.js’) // { verifyPassword: function(user, password, done) { ... } } 

然而,如果我们想直接暴露这个函数,而不是让它作为某些对象的属性呢?我们可以覆写 exports 来达到目的,但是我们绝对不能把它当做一个全局变量:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// a.jsmodule.exports = function(user, password, done) { ... }

注意到我们是把“exports” 当做 module 对象的一个属性。“module.exports” 和“exports” 这之间区别是很重要的,而且经常会使 Node.js 新手踩坑。

错误6:从回调里抛出错误

JavaScript 有异常的概念。在语法上,学绝大多数传统语言(如 Java、C++)对异常的处理那样,JavaScript 可以抛出异常以及在 try-catch 语句块中捕获异常:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 去保护一大段含有许多异步处理的代码,它可能不会正常的工作:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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, ...) 这种形式,其中第一个参数是错误发生时的 error 对象。

错误7:认为 Number 是一种整型数据格式

在 JavaScript 里数字都是浮点型,没有整型的数据格式。你可能认为这不是什么问题,因为数字大到溢出浮点型限制的情况很少出现。可实际上,当这种情况发生时就会出错。因为浮点数在表达一个整型数时只能表示到一个最大上限值,在计算中超过这个最大值时就会出问题。也许看起来有些奇怪,但在 Node.js 中下面代码的值是 true:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Math.pow(2, 53)+1 === Math.pow(2, 53)

很不幸的是,JavaScript 里有关数字的怪癖可还不止这些。尽管数字是浮点型的,但如下这种整数运算能正常工作:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
5 % 2 === 1 // true5 >> 1 === 2 // true

然而和算术运算不同的是,位运算和移位运算只在小于 32 位最大值的数字上正常工作。例如,让“Math.pow(2, 53)” 位移 1 位总是得到 0,让其与 1 做位运算也总是得到 0:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Math.pow(2, 53) / 2 === Math.pow(2, 52) // trueMath.pow(2, 53) >> 1 === 0 // trueMath.pow(2, 53) | 1 === 0 // true

你可能极少会去处理如此大的数字,但如果你需要的话,有很多实现了大型精密数字运算的大整数库可以帮到你,比如 node-bigint。

错误8:忽略了流式 API 的优势

现在我们想创建一个简单的类代理 web 服务器,它能通过拉取其他 web 服务器的内容来响应和发起请求。作为例子,我们创建一个小型 web 服务器为 Gravatar 的图像服务。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 拉取图片,将它存进一个 Buffer 里,然后响应请求。如果 Gravatar 的图片都不是很大的话,这样做没问题。但想象下如果我们代理的内容大小有上千兆的话,更好的处理方式是下面这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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:出于 Debug 的目的使用 Console.log

在 Node.js 里,“console.log” 允许你打印任何东西到控制台上。比如传一个对象给它,它会以 JavaScript 对象的字符形式打印出来。它能接收任意多个的参数并将它们以空格作为分隔符打印出来。有很多的理由可以解释为什么开发者喜欢使用它来 debug 他的代码,然而我强烈建议你不要在实时代码里使用“console.log”。你应该要避免在所有代码里使用“console.log” 去 debug,而且应该在不需要它们的时候把它们注释掉。你可以使用一种专门做这种事的库代替,比如 debug。

这些库提供了便利的方式让你在启动程序的时候开启或关闭具体的 debug 模式,例如,使用 debug 的话,你能够阻止任何 debug 方法输出信息到终端上,只要不设置 DEBUG 环境变量即可。使用它十分简单:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// app.jsvar debug = require(‘debug’)(‘app’)debug(’Hello, %s!’, ‘world’)

开启 debug 模式只需简单地运行下面的代码把环境变量 DEBUG 设置到“app” 或“*” 上:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 代码,而且能够给我们大家开发出健壮高效的软件。

  • 原文地址:http://www.toptal.com/nodejs/top-10-common-nodejs-developer-mistakes
  • 原文作者:MAHMUD RIDWAN
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员成长指北 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
node中常见的10个错误
非原创,格式不太整齐。 错误 #1:阻塞事件循环 JavaScript在 Node.js (就像在浏览器一样) 提供单线程执行环境。这意味着你的程序不能同时执行两部分代码,但能通过 I/O 绑定异步回
IMWeb前端团队
2018/01/08
1.9K0
深入浅析Node.js中的异步
以点菜吃饭为例子:去饭堂点菜吃饭需要排队等待,在这个过程中,阿姨每次只能接待一个人,“点菜-阿姨抖勺装菜-把饭菜给到你”这个过程中阿姨并不能接收其他人的点菜,这个就是阻塞 I/O;而去餐馆点菜吃饭,去到餐馆就可以跟服务员你要吃番茄炒蛋,服务员记下来之后交给后厨,这时候来了另一桌人就把服务员招呼过去说想吃小龙虾,也就是说,在把菜给你上上来之前服务员接收了其他人的点菜,那这个就是非阻塞型 I/O。
conanma
2022/01/05
1.3K0
Node.js基础教程
Node.js 的推出,不仅从工程化的角度自动化掉更多琐碎费时的工作,更打破了前端后端的语言边界,让 JavaScript 流畅的运行在服务器端,本系列课程旨在引导前端开发工程师,以及 Node.js 初学者走进这个活泼而有富有生命力的新世界。
达达前端
2022/04/29
5550
Node.js基础教程
Node.js内存泄漏分析
在极客教育出版了一个视频是关于《Node.js 内存泄漏分析》,本文章主要是从内容上介绍如何来处理Node.js内存异常问题。如果希望学习可前往极客学院:http://www.jikexueyuan.com/course/2561.html 本文章的关键词 - 内存泄漏 - 内存泄漏检测 - GC分析 - memwatch ---- 文章概要 由于内存泄漏在Node.js中非常的常见,可能在浏览器中应用javascript时,对于其内存泄漏不是特别敏感,但作为服
蛋未明
2018/06/07
2.5K0
Node.js 使用详情
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境。它使得开发者能够使用 JavaScript 编写服务器端应用程序。在本文中,我们将详细讨论 Node.js 的特点、使用方法以及一些实际应用的代码示例。
世间万物皆对象
2024/03/20
2030
深入理解JS异步编程(一)
版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://blog.csdn.net/wkyseo/article/details/51516121
空空云
2018/09/27
1.2K0
15 个常见的 Node.js 面试问题及答案
临阵磨枪,不快也光。对于成功的编程面试来说,准备和知识面一样重要。准备使你有信心参加面试,而不用担心莫名的紧张情绪。如果第一次参加编程面试,这一点尤其重要。
coder_koala
2021/03/09
1.9K0
不错的node.js入门
关于 本书致力于教会你如何用Node.js来开发应用,过程中会传授你所有所需的“高级”JavaScript知识。本书绝不是一本“Hello World”的教程。 状态 你正在阅读的已经是本书的最终版。因此,只有当进行错误更正以及针对新版本Node.js的改动进行对应的修正时,才会进行更新。 本书中的代码案例都在Node.js 0.6.11版本中测试过,可以正确工作。 读者对象 本书最适合与我有相似技术背景的读者: 至少对一门诸如Ruby、Python、PHP或者Java这样面向对象的语言有一定的经验;对Ja
脑洞的蜂蜜
2018/02/02
4K0
【读书笔记】《深入浅出 Node.js》
[《深入浅出 Node.js》] 读书笔记。 # 缘起 最初的目标是写一个基于事件驱动、非阻塞 I/O 的 Web 服务器。考虑高性能、符合事件驱动、没有历史包袱选择了 JavaScript。 # 特点 异步 I/O 事件与回调函数 事件优势:轻量级、松耦合、只关注事务点 单线程 优点 无需像多线程在意状态同步问题,没有死锁的存在 无线程上下文交换带来的性能上的开销 弱势 无法利用多核 CPU 错误会引起整个应用退出,应用的健壮性值得考验 大量计算占用 CPU 导致无法继续调用异步 I/O Node
Cellinlab
2023/05/17
8530
【读书笔记】《深入浅出 Node.js》
Node.js异步编程进化论
我们知道,Node.js中有两种事件处理方式,分别是callback(回调)和EventEmitter(事件发射器)。本文首先介绍的是callback。
童欧巴
2020/03/28
8920
走进Node.js 之 HTTP实现分析
作者:正龙(沪江Web前端开发工程师) 本文为原创文章,转载请注明作者及出处 上文“走进Node.js启动过程”中我们算是成功入门了。既然Node.js的强项是处理网络请求,那我们就来分析一个HT
iKcamp
2018/01/04
2.1K0
走进Node.js 之 HTTP实现分析
入门 node.js 你必须知道的那些事
exports 是 module.exports 的一个引用,意思就是指向同一块内存地址,node 中真正生效的是 module.exports, 修改 exports 本质上也是修改 module.exports 的值,
IT派
2018/07/30
1.1K0
Node.js中常用的设计模式有哪些?
设计模式是由经验丰富的程序员在日积月累中抽象出的用以解决通用问题的可复用解决方案,它提供了标准化的代码设计方案提升开发体验。Node.js 作为一款用来构建可扩展高性能应用的流行平台,自然也遵循设计模式解决通用问题。本文中,我们将讨论 Node.js 中设计模式的重要性并提供一些代码示例。
葡萄城控件
2023/10/23
3430
Node.js中常用的设计模式有哪些?
Node.js学习笔记(二)——Node.js模块化、文件读写、环境变量
(1)、在浏览器端使用var或不使用关键字定义的变量属于全局作用域,也就是可以使用window对象访问。
张果
2022/09/28
6.4K0
Node.js学习笔记(二)——Node.js模块化、文件读写、环境变量
Node.js基础常用知识点全总结
Node.js是一个javascript运行环境。它让javascript可以开发后端程序,实现几乎其他后端语言实现的所有功能,可以与PHP、Java、Python、.NET、Ruby等后端语言平起平坐。
害恶细君
2022/11/22
3.5K0
Node.js基础常用知识点全总结
node.js基础入门
node.js是一个基于Google V8引擎的、跨平台的JavaScript运行环境,不是一个语言
黄啊码
2022/06/20
7870
这里有一份Node.js入门指南和实践,请注意查收
JS 是脚本语言,脚本语言都需要一个解析器才能运行。对于写在 HTML 页面里的 JS,浏览器充当了解析器的角色。而对于需要独立运行的 JS,NodeJS 就是一个解析器。
null仔
2020/02/28
3.7K0
这里有一份Node.js入门指南和实践,请注意查收
【灵魂拷问】你为什么要来学习Node.js呢?
Node是一种新技术,诞生于2009年,经过几年的变化,Node.js生态圈以及逐渐走向了稳定,不少开发者入门时,总是要经历一种思维的转变,但是学会Node.js可以进行Web后端的开发,同时掌握事件驱动的异步式编程风格。
达达前端
2019/12/24
1.2K0
【灵魂拷问】你为什么要来学习Node.js呢?
深入解析Node.js:V8引擎、事件驱动和非阻塞式I/O
🎉欢迎来到架构设计专栏~探索Java中的静态变量与实例变量深入解析Node.js:V8引擎、事件驱动和非阻塞式I/O
IT_陈寒
2023/12/14
3030
深入解析Node.js:V8引擎、事件驱动和非阻塞式I/O
Node.js笔记
用户把 JSON 数据 POST 给服务器,服务器再把数据中的 msg 取出来,返回给用户
赤蓝紫
2023/01/02
4.8K0
Node.js笔记
相关推荐
node中常见的10个错误
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验