作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
马哈茂德·里德万的头像

马哈茂德Ridwan

Mahmud是一名软件开发人员,拥有多年的经验和效率诀窍, 可伸缩性, 稳定的解.

工作经验

13

分享

从时刻开始.当它向世界亮相时,它已经看到了两者的公平份额 表扬与批评. 争论仍在继续,而且可能不会很快结束. 在这些争论中,我们经常忽略的是,每种编程语言和平台都是基于某些问题而受到批评的, 是由我们如何使用这个平台产生的吗. 不管节点有多难.Js使得编写安全的代码, 以及它使编写高度并发代码变得多么容易, 这个平台已经存在很长一段时间了,已经被用来构建大量健壮和复杂的web服务. 这些web服务可扩展性很好, 并通过他们在互联网上的耐力证明了他们的稳定性.

然而,像任何其他平台一样,节点.Js很容易受到开发人员问题的影响. 其中一些错误会降低性能,而另一些则会导致节点.不管你想要达到什么效果,j都显得毫无用处. 在本文中,我们将介绍节点新手开发人员常犯的10个错误.j经常犯的错误,以及如何避免它们成为一个 节点.js职业.

节点.Js开发者错误

错误1:阻塞事件循环

JavaScript in 节点.Js(就像在浏览器中一样)提供了一个单线程环境. This means that no two parts of your application run in parallel; instead, 并发是通过异步处理I/O绑定操作来实现的. 例如,来自节点的请求.js到数据库引擎获取一些文档是允许节点.让Js专注于应用程序的其他部分:

//试图从数据库中获取一个用户对象. 节点.从调用这个函数的那一刻起,Js就可以自由地运行代码的其他部分..
db.用户.get (userId函数(err, 用户){
	// .. 直到在这里检索到用户对象为止
})

节点.Js单线程环境

但是,节点中的一段cpu绑定代码.一个有数千个客户端连接的Js实例就是阻塞事件循环所需要的, 让所有客户等待. cpu密集型代码包括尝试对大型数组进行排序、运行超长循环等等. 例如:

sort用户sByAge(用户){
	用户.Sort(函数(a, b) {
		返回一个.年龄 < b.年龄 ? -1 : 1
	})
}

如果在一个小的“用户”数组上运行,调用这个“sort用户sByAge”函数可能没问题, 但是有一个大阵列, 这将对整体性能产生可怕的影响. 如果这是绝对必须要做的事情, 并且可以确定在事件循环中没有其他东西在等待(例如, 如果这是使用节点构建的命令行工具的一部分.Js(如果整个程序同步运行也没关系),那么这可能不是问题. 然而,在一个节点中.如果一个服务器实例试图同时为数千个用户提供服务,这种模式可能是致命的.

是否正在从数据库检索此用户数组, 理想的解决方案是直接从数据库中获取已经排序好的数据. 如果事件循环被一个用于计算长历史金融交易数据总和的循环阻塞, 它可以延迟到一些外部工作者/队列设置,以避免占用事件循环.

正如您所看到的,没有针对这种节点的万能解决方案.而是每个案例都需要单独解决. 其基本思想是不要在面向前端的节点中执行CPU密集型工作.Js实例——客户端并发连接的实例.

错误#2:多次调用回调

JavaScript一直以来都依赖于回调. 在网页浏览器中, 事件是通过传递引用(通常是匿名的)函数来处理的,这些函数的作用类似于回调. 在节点.js, 回调曾经是代码中异步元素相互通信的唯一方式——直到承诺被引入. 回调仍然在使用,包开发人员仍然围绕回调设计他们的api. 一个普通节点.与使用回调相关的Js问题是多次调用它们. 通常, 包提供的用于异步执行某些操作的函数被设计为期望函数作为其最后一个参数, 当异步任务完成时调用:

模块.出口.verifyPassword = function(user, password,完成){
	如果(typeof密码 !== ' string ') {
		done(new Error(' password应为字符串'))
		返回
	}

	哈希密码,用户.passwordhashhopts函数(err, hash) {
		如果(err) {
			(错)
			返回
		}
		
		完成(null, hash === user).passwordHash)
	})
}

注意,每次调用“done”时都有一个返回语句,直到最后一次调用为止. 这是因为调用回调不会自动结束当前函数的执行. 如果第一个“返回”被注释掉了, 向该函数传递非字符串密码仍将导致调用“computeHash”. 根据“computeHash”处理这种场景的方式,“done”可能被调用多次. 当他们传递的回调被多次调用时,任何从其他地方使用这个函数的人都可能完全措手不及.

只要小心就能避开这个节点.js错误. 一些节点.Js开发者习惯于在每次回调调用之前添加返回关键字:

如果(err) {
	返回完成(错)
}

在许多异步函数中, 返回值几乎没有意义, 因此,这种方法通常可以很容易地避免这样的问题.

错误#3:深度嵌套回调

深嵌套回调,通常被称为“回调地狱”,不是节点.问题本身. 然而,这可能会导致代码迅速失去控制的问题:

函数handleLogin (...,完成){
	db.用户.get (...函数(..., 用户){
		if(!用户){
			返回done(null, '登录失败')
		}
		跑龙套.verifyPassword (...函数(...,好的){
			如果(好吧){
				返回done(null, '登录失败')
			}
			会话.登录(..., function() {
				完成(null, '已登录')
			})
		})
	})
}

回调地狱

任务越复杂,情况就越糟糕. 通过以这种方式嵌套回调, 我们很容易以容易出错告终, 难以阅读, 而且很难维护代码. 一种解决方法是将这些任务声明为小函数,然后将它们链接起来. 尽管如此,(有争议的)最干净的解决方案之一是使用实用程序节点.的Js包 异步JavaScript 模式,例如 异步.js:

函数handleLogin(done) {
	异步.瀑布([
		函数(做){
			db.用户.get (...,完成)
		},
		函数(user,完成){
			if(!用户){
			返回done(null, '登录失败')
			}
			跑龙套.verifyPassword (...函数(...,好的){
				完成(null, user, okay)
			})
		},
		函数(user, okay,完成){
			如果(好吧){
				返回done(null, '登录失败')
			}
			会话.登录(..., function() {
				完成(null, '已登录')
			})
		}
	, function() {
		// ...
	})
}

类似于“异步”.“瀑布”,还有许多其他的异步函数.Js提供了处理不同异步模式的方法. 为简洁起见,我们在这里使用了更简单的示例,但实际情况往往更糟.

错误4:期望回调函数同步运行

带回调的异步编程可能不是JavaScript和节点独有的东西.j,但他们对它的流行负有责任. 使用其他编程语言, 我们习惯了两个语句一个接一个执行的可预测的执行顺序, 除非有特定的指令在语句之间跳转. 即使在那时, 这些通常仅限于条件语句, 循环语句, 以及函数调用.

然而, 在JavaScript中, 对于回调函数,一个特定的函数可能在它等待的任务完成之前无法正常运行. 当前函数的执行将一直运行到结束,没有任何停止:

testTimeout() {
	控制台.日志(“开始”)
	setTimeout(函数(){
		控制台.日志(“完成!”)
	},持续时间* 1000)
	控制台.日志(“等待..”)
}

你会注意到, 调用" testTimeout "函数将首先打印" Begin ", 然后打印“等待”..,然后是“完成”!大约一秒钟后.

在回调触发后需要发生的任何事情都需要从回调内部调用.

错误5:赋值给“出口”,而不是“模块.出口”

节点.Js将每个文件视为一个独立的小模块. 如果你的包有两个文件,也许“a.和b.Js”,然后是“b”.a .“访问”.Js的功能,a.Js "必须通过在出口对象中添加属性来导出它:

// a.js
出口.verifyPassword = function(user, password,完成){ ... }

完成后,任何需要“a”的人.将会得到一个带有属性函数" verifyPassword "的对象:

// b.js
要求(“.// {verifyPassword: function(user, password,完成){ ... } } 

但是,如果我们想直接导出这个函数,而不是作为某个对象的属性,该怎么办呢? 我们可以重写出口来实现这一点,但我们不能把它当作一个全局变量:

// a.js
模块.Exports = function(user, password,完成){ ... }

注意我们是如何将“出口”作为模块对象的属性来处理的. 这里的区别是“模块”.“出口”和“出口”是非常重要的,并且经常是导致新节点受挫的原因.js开发人员.

错误#6:从内部回调抛出错误

JavaScript有异常的概念. 模仿几乎所有具有异常处理支持的传统语言的语法, 例如Java和c++, JavaScript可以在try-catch块中“抛出”和捕获异常:

函数slugify用户name(用户名){
	If (typeof username === ' string ') {
		抛出新的TypeError(' expected a string username, got '+(typeof 用户名))
	}
	// ...
}

尝试{
	var usernameSlug = slugify用户name(用户名)
} catch(e) {
	控制台.日志(“哦,不!’)
}

但是,try-catch在异步情况下的行为与您所期望的不同. 例如, 如果您想用一个大的try-catch块来保护包含大量异步活动的大块代码, 这并不一定有效:

尝试{
	db.用户.get (userId函数(err, 用户){
		如果(err) {
			把犯错
		}
		// ...
		usernameSlug = slugify用户name.用户名)
		// ...
	})
} catch(e) {
	控制台.日志(“哦,不!’)
}

如果回调到“db.用户.获得“异步触发”, 包含try-catch块的作用域早就脱离了上下文,因此它仍然能够捕捉回调内部抛出的那些错误.

这就是节点中以不同的方式处理错误的方式.js, 这就使得遵循(错误, …)模式-所有回调函数的第一个参数如果发生错误,预计将是一个错误.

错误#7:假设Number是整数数据类型

JavaScript中的数字是浮点数——没有整数数据类型. 你不会认为这是个问题, 由于数字大到足以强调浮点数的限制,所以不经常遇到. 这正是与此相关的错误发生的时候. 因为浮点数只能保存一定值以内的整数表示, 在任何计算中超过这个值都会立即开始混乱. 虽然看起来很奇怪,但在节点中,下面的计算结果为true.js:

数学.pow(2,53)+1 ===数学.现年53岁的战俘(2)

不幸的是,JavaScript中数字的怪癖并没有就此结束. 尽管数字是浮点数, 在整数数据类型上工作的操作符也可以在这里工作:

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

然而, 不像算术运算符, 按位运算符和移位运算符只对这样大的“整数”数的后32位有效. 例如,试图改变“数学”.Pow(2,53)”× 1总是等于0. 如果用同样大的数对1进行位运算,结果会是1.

数学.pow(2,53) / 2 ===数学.Pow (2,52) // true
数学.现年53岁的战俘(2) >> 1 === 0 // true
数学.Pow (2,53) | 1 === 1 // true

您可能很少需要处理大的数字, 但如果你这么做了, 有许多大型整数库可以实现对大精度数的重要数学运算, 如 节点-bigint.

错误8:忽视流媒体api的优势

假设我们想要构建一个小型的代理式web服务器,通过从另一个web服务器获取内容来响应请求. 作为一个例子,我们将构建一个小型的web服务器来提供Gravatar图像:

Var HTTP = require(' HTTP ')
Var crypto = require('crypto')

http.createServer ()
.On ('request', function(req, res) {
	Var email = req.url.substr(要求.url.lastIndexOf(“/”)+ 1)
	if(!电子邮件){
		res.writeHead (404)
		返回res.结束()
	}

	var buf = new Buffer(1024*1024)
	http.get (" http://www.功能.com/avatar/ ' +加密.createHash (md5).更新(电子邮件).摘要('hex'),函数(分别地) {
		Var大小= 0
		分别地.On ('data', function(块) {
			块.复制(buf、大小)
			Size += 块.长度
		})
		.On ('end', function() {
			res.写(buf.片(0,大小)
			res.结束()
		})
	})
})
.听(8080)

在这个特定的节点示例中.js的问题, 我们正在从Gravatar获取图像, 读入缓冲区, 然后对请求做出响应. 这并不是一件坏事,因为Gravatar图像并不太大. 但是,想象一下,如果我们代理的内容的大小是数千兆字节. 一个更好的方法应该是:

http.createServer ()
.On ('request', function(req, res) {
	Var email = req.url.substr(要求.url.lastIndexOf(“/”)+ 1)
	if(!电子邮件){
		res.writeHead (404)
		返回res.结束()
	}

	http.get (" http://www.功能.com/avatar/ ' +加密.createHash (md5).更新(电子邮件).摘要('hex'),函数(分别地) {
		分别地.管(res)
	})
})
.听(8080)

在这里,我们获取图像并简单地将响应传输到客户机. 在任何时候,我们都不需要在提供内容之前将整个内容读入缓冲区.

错误9:使用主机.调试日志

在节点.js:“控制台.“Log”允许您将几乎所有内容打印到控制台. 将一个对象传递给它,它将把它作为JavaScript对象文字打印出来. 它接受任意数量的参数,并将它们整齐地以空格分隔打印出来. There are a number of reasons why a developer may feel tempted to use this to 调试 his code; however, 强烈建议您避免使用控制台.Log”的真实代码. 你应该避免写“控制台”.记录所有的代码以进行调试,然后在不再需要它们时将它们注释掉. 相反,您可以使用专门为此构建的令人惊叹的库之一,例如 调试.

这样的包提供了在启动应用程序时启用和禁用某些调试行的方便方法. 例如, 使用调试,可以通过不设置调试环境变量来防止任何调试行被打印到终端. 使用它很简单:

/ /应用程序.js
Var 调试 = require('调试')('app')
调试(“你好,% s!”、“世界”)

要启用调试行,只需运行以下代码,并将环境变量调试设置为“app”或“*”:

DEBUG=app节点的app.js

错误10:不使用主管程序

无论您的节点是否.Js代码在生产环境或本地开发环境中运行, 可以编排程序的管理程序监视器是非常有用的东西. 设计和实现现代应用程序的开发人员经常推荐的一种做法是,代码应该快速失败. 如果发生意外错误, 不要试着去处理它, 让你的程序崩溃,让管理员在几秒钟内重新启动它. 管理程序的好处不仅限于重新启动崩溃的程序. 这些工具允许您在崩溃时重新启动程序, 以及在某些文件更改时重新启动它们. 这使得开发节点.Js程序的体验要愉快得多.

有大量的管理程序可用于节点.js. 例如:

所有这些工具都有其优点和缺点. 其中一些很适合处理同一台机器上的多个应用程序, 而其他人则更擅长日志管理. 然而,如果你想开始这样一个程序,所有这些都是公平的选择.

结论

正如你所看到的,这些节点中的一些.Js的问题可能会对您的程序造成毁灭性的影响. 当您试图在节点中实现最简单的东西时,其中一些可能会使您感到沮丧.js. 尽管节点.Js使得新手入门非常容易, 它仍然有一些地方很容易搞砸. 使用其他编程语言的开发人员可能会遇到其中的一些问题, 但是这些错误在新人中是很常见的 节点.js开发人员. 幸运的是,它们很容易避免. 我希望这篇简短的指南能帮助初学者 用节点编写更好的代码.js,为大家开发出稳定高效的软件.

就这一主题咨询作者或专家.
预约电话
马哈茂德·里德万的头像
马哈茂德Ridwan

位于 达卡,达卡区,孟加拉国

成员自 2014年1月16日

作者简介

Mahmud是一名软件开发人员,拥有多年的经验和效率诀窍, 可伸缩性, 稳定的解.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

工作经验

13

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.