溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Node.js 十大常見(jiàn)的開(kāi)發(fā)者錯(cuò)誤

發(fā)布時(shí)間:2020-07-19 10:06:31 來(lái)源:網(wǎng)絡(luò) 閱讀:405 作者:可樂(lè)程序員 欄目:web開(kāi)發(fā)

前言

自 Node.js 面世以來(lái),它獲得了大量的贊美和批判。這種爭(zhēng)論會(huì)一直持續(xù),短時(shí)間內(nèi)都不會(huì)結(jié)束。而在這些爭(zhēng)論中,我們常常會(huì)忽略掉所有語(yǔ)言和平臺(tái)都是基于一些核心問(wèn)題來(lái)批判的,就是我們?cè)趺慈ナ褂眠@些平臺(tái)。無(wú)論使用 Node.js 編寫(xiě)可靠的代碼有多難,而編寫(xiě)高并發(fā)代碼又是多么的簡(jiǎn)單,這個(gè)平臺(tái)終究是有那么一段時(shí)間了,而且被用來(lái)創(chuàng)建了大量的健壯而又復(fù)雜的 web 服務(wù)。這些 web 服務(wù)不僅擁有良好的擴(kuò)展性,而且通過(guò)在互聯(lián)網(wǎng)上持續(xù)的時(shí)間證明了它們的健壯性。

然而就像其它平臺(tái)一樣,Node.js 很容易令開(kāi)發(fā)者犯錯(cuò)。這些錯(cuò)誤有些會(huì)降低程序性能,有些則會(huì)導(dǎo)致 Node.js 不可用。在本文中,我們會(huì)看到 Node.js 新手常犯的十種錯(cuò)誤,以及如何去避免它們。

錯(cuò)誤1:阻塞事件循環(huán)

Node.js(正如瀏覽器)里的 JavaScript 提供了一種單線程環(huán)境。這意味著你的程序不會(huì)有兩塊東西同時(shí)在運(yùn)行,取而代之的是異步處理 I/O 密集操作所帶來(lái)的并發(fā)。比如說(shuō) Node.js 給數(shù)據(jù)庫(kù)發(fā)起一個(gè)請(qǐng)求去獲取一些數(shù)據(jù)時(shí),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
})
復(fù)制代碼

然而,在一個(gè)有上千個(gè)客戶端連接的 Node.js 實(shí)例里,一小段 CPU 計(jì)算密集的代碼會(huì)阻塞住事件循環(huán),導(dǎo)致所有客戶端都得等待。CPU 計(jì)算密集型代碼包括了嘗試排序一個(gè)巨大的數(shù)組、跑一個(gè)耗時(shí)很長(zhǎng)的函數(shù)等等。例如:

function?sortUsersByAge(users)?{
	users.sort(function(a,?b)?{
		return?a.age?<?b.age???-1?:?1
	})
}
復(fù)制代碼

在一個(gè)小的“users” 數(shù)組上調(diào)用“sortUsersByAge” 方法是沒(méi)有任何問(wèn)題的,但如果是在一個(gè)大數(shù)組上,它會(huì)對(duì)整體性能造成巨大的影響。如果這種事情不得不做,而且你能確保事件循環(huán)上沒(méi)有其他事件在等待(比如這只是一個(gè) Node.js 命令行工具,而且它不在乎所有事情都是同步工作的)的話,那這沒(méi)有問(wèn)題。但是,在一個(gè) Node.js 服務(wù)器試圖給上千用戶同時(shí)提供服務(wù)的情況下,它就會(huì)引發(fā)問(wèn)題。

如果這個(gè) users 數(shù)組是從數(shù)據(jù)庫(kù)獲取的,那么理想的解決方案是從數(shù)據(jù)庫(kù)里拿出已排好序的數(shù)據(jù)。如果事件循環(huán)被一個(gè)計(jì)算金融交易數(shù)據(jù)歷史總和的循環(huán)所阻塞,這個(gè)計(jì)算循環(huán)應(yīng)該被推到事件循環(huán)外的隊(duì)列中執(zhí)行以免占用事件循環(huán)。

正如你所見(jiàn),解決這類錯(cuò)誤沒(méi)有銀彈,只有針對(duì)每種情況單獨(dú)解決?;纠砟钍遣灰谔幚砜蛻舳瞬l(fā)連接的 Node.js 實(shí)例上做 CPU 計(jì)算密集型工作。

錯(cuò)誤2:多次調(diào)用一個(gè)回調(diào)函數(shù)

一直以來(lái) JavaScript 都依賴于回調(diào)函數(shù)。在瀏覽器里,事件都是通過(guò)傳遞事件對(duì)象的引用給一個(gè)回調(diào)函數(shù)(通常都是匿名函數(shù))來(lái)處理。在 Node.js 里,回調(diào)函數(shù)曾經(jīng)是與其他代碼異步通信的唯一方式,直到 promise 出現(xiàn)?;卣{(diào)函數(shù)現(xiàn)在仍在使用,而且很多開(kāi)發(fā)者依然圍繞著它來(lái)設(shè)置他們的 API。一個(gè)跟使用回調(diào)函數(shù)相關(guān)的常見(jiàn)錯(cuò)誤是多次調(diào)用它們。通常,一個(gè)封裝了一些異步處理的方法,它的最后一個(gè)參數(shù)會(huì)被設(shè)計(jì)為傳遞一個(gè)函數(shù),這個(gè)函數(shù)會(huì)在異步處理完后被調(diào)用:

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)
	})
}
復(fù)制代碼

這里注意到除了最后一次,每次“done” 方法被調(diào)用之后都會(huì)有一個(gè) return 語(yǔ)句。這是因?yàn)檎{(diào)用回調(diào)函數(shù)不會(huì)自動(dòng)結(jié)束當(dāng)前方法的執(zhí)行。如果我們注釋掉第一個(gè) return 語(yǔ)句,然后傳一個(gè)非字符串類型的 password 給這個(gè)函數(shù),我們依然會(huì)以調(diào)用 computeHash 方法結(jié)束。根據(jù) computeHash 在這種情況下的處理方式,“done” 函數(shù)會(huì)被調(diào)用多次。當(dāng)傳過(guò)去的回調(diào)函數(shù)被多次調(diào)用時(shí),任何人都會(huì)被弄得措手不及。

避免這個(gè)問(wèn)題只需要小心點(diǎn)即可。一些 Node.js 開(kāi)發(fā)者因此養(yǎng)成了一個(gè)習(xí)慣,在所有調(diào)用回調(diào)函數(shù)的語(yǔ)句前加一個(gè) return 關(guān)鍵詞:

if(err)?{
	return?done(err)
}
復(fù)制代碼

在很多異步函數(shù)里,這種 return 的返回值都是沒(méi)有意義的,所以這種舉動(dòng)只是為了簡(jiǎn)單地避免這個(gè)錯(cuò)誤而已。

錯(cuò)誤3:深層嵌套的回調(diào)函數(shù)

深層嵌套的回調(diào)函數(shù)通常被譽(yù)為“ 回調(diào)地獄”,它本身并不是什么問(wèn)題,但是它會(huì)導(dǎo)致代碼很快變得失控:

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’)
			})
		})
	})
}
復(fù)制代碼

越復(fù)雜的任務(wù),這個(gè)的壞處就越大。像這樣嵌套回調(diào)函數(shù),我們的程序很容易出錯(cuò),而且代碼難以閱讀和維護(hù)。一個(gè)權(quán)宜之計(jì)是把這些任務(wù)聲明為一個(gè)個(gè)的小函數(shù),然后再將它們聯(lián)系起來(lái)。不過(guò),(有可能是)最簡(jiǎn)便的解決方法之一是使用一個(gè) Node.js 公共組件來(lái)處理這種異步 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()?{
		//?...
	})
}
復(fù)制代碼

Async.js 還提供了很多類似“async.waterfall” 的方法去處理不同的異步場(chǎng)景。為了簡(jiǎn)便起見(jiàn),這里我們演示了一個(gè)簡(jiǎn)單的示例,實(shí)際情況往往復(fù)雜得多。

(打個(gè)廣告,隔壁的《ES6 Generator 介紹》提及的 Generator 也是可以解決回調(diào)地獄的哦,而且結(jié)合 Promise 使用更加自然,請(qǐng)期待隔壁樓主的下篇文章吧:D)

錯(cuò)誤4:期待回調(diào)函數(shù)同步執(zhí)行

使用回調(diào)函數(shù)的異步程序不只是 JavaScript 和 Node.js 有,只是它們讓這種異步程序變得流行起來(lái)。在其他編程語(yǔ)言里,我們習(xí)慣了兩個(gè)語(yǔ)句一個(gè)接一個(gè)執(zhí)行,除非兩個(gè)語(yǔ)句之間有特殊的跳轉(zhuǎn)指令。即使那樣,這些還受限于條件語(yǔ)句、循環(huán)語(yǔ)句以及函數(shù)調(diào)用。

然而在 JavaScript 里,一個(gè)帶有回調(diào)函數(shù)的方法直到回調(diào)完成之前可能都無(wú)法完成任務(wù)。當(dāng)前函數(shù)會(huì)一直執(zhí)行到底:

function?testTimeout()?{
	console.log(“Begin”)
	setTimeout(function()?{
		console.log(“Done!”)
	},?duration?*?1000)
	console.log(“Waiting..”)
}
復(fù)制代碼

你可能會(huì)注意到,調(diào)用“testTimeout” 函數(shù)會(huì)先輸出“Begin”,然后輸出“Waiting..”,緊接著幾秒后輸出“Done!”。

任何要在回調(diào)函數(shù)執(zhí)行完后才執(zhí)行的代碼,都需要在回調(diào)函數(shù)里調(diào)用。

錯(cuò)誤5:給“exports” 賦值,而不是“module.exports”

Node.js 認(rèn)為每個(gè)文件都是一個(gè)獨(dú)立的模塊。如果你的包有兩個(gè)文件,假設(shè)是“a.js” 和“b.js”,然后“b.js” 要使用“a.js” 的功能,“a.js” 必須要通過(guò)給 exports 對(duì)象增加屬性來(lái)暴露這些功能:

//?a.js
exports.verifyPassword?=?function(user,?password,?done)?{?...?}
復(fù)制代碼

完成這步后,所有需要“a.js” 的都會(huì)獲得一個(gè)帶有“verifyPassword” 函數(shù)屬性的對(duì)象:

//?b.js
require(‘a(chǎn).js’)?//?{?verifyPassword:?function(user,?password,?done)?{?...?}?}?
復(fù)制代碼

然而,如果我們想直接暴露這個(gè)函數(shù),而不是讓它作為某些對(duì)象的屬性呢?我們可以覆寫(xiě) exports 來(lái)達(dá)到目的,但是我們絕對(duì)不能把它當(dāng)做一個(gè)全局變量:

//?a.js
module.exports?=?function(user,?password,?done)?{?...?}
復(fù)制代碼

注意到我們是把“exports” 當(dāng)做 module 對(duì)象的一個(gè)屬性?!癿odule.exports” 和“exports” 這之間區(qū)別是很重要的,而且經(jīng)常會(huì)使 Node.js 新手踩坑。

錯(cuò)誤6:從回調(diào)里拋出錯(cuò)誤

JavaScript 有異常的概念。在語(yǔ)法上,學(xué)絕大多數(shù)傳統(tǒng)語(yǔ)言(如 Java、C++)對(duì)異常的處理那樣,JavaScript 可以拋出異常以及在 try-catch 語(yǔ)句塊中捕獲異常:

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!’)
}
復(fù)制代碼

然而,在異步環(huán)境下,tary-catch 可能不會(huì)像你所想的那樣。比如說(shuō),如果你想用一個(gè)大的 try-catch 去保護(hù)一大段含有許多異步處理的代碼,它可能不會(huì)正常的工作:

try?{
	db.User.get(userId,?function(err,?user)?{
		if(err)?{
			throw?err
		}
		//?...
		usernameSlug?=?slugifyUsername(user.username)
		//?...
	})
}?catch(e)?{
	console.log(‘Oh?no!’)
}
復(fù)制代碼

如果“db.User.get” 的回調(diào)函數(shù)異步執(zhí)行了,那么 try-catch 原來(lái)所在的作用域就很難捕獲到回調(diào)函數(shù)里拋出的異常了。

這就是為什么在 Node.js 里通常使用不同的方式處理錯(cuò)誤,而且這使得所有回調(diào)函數(shù)的參數(shù)都需要遵循 (err, ...) 這種形式,其中第一個(gè)參數(shù)是錯(cuò)誤發(fā)生時(shí)的 error 對(duì)象。

錯(cuò)誤7:認(rèn)為 Number 是一種整型數(shù)據(jù)格式

在 JavaScript 里數(shù)字都是浮點(diǎn)型,沒(méi)有整型的數(shù)據(jù)格式。你可能認(rèn)為這不是什么問(wèn)題,因?yàn)閿?shù)字大到溢出浮點(diǎn)型限制的情況很少出現(xiàn)。可實(shí)際上,當(dāng)這種情況發(fā)生時(shí)就會(huì)出錯(cuò)。因?yàn)楦↑c(diǎn)數(shù)在表達(dá)一個(gè)整型數(shù)時(shí)只能表示到一個(gè)最大上限值,在計(jì)算中超過(guò)這個(gè)最大值時(shí)就會(huì)出問(wèn)題。也許看起來(lái)有些奇怪,但在 Node.js 中下面代碼的值是 true:

Math.pow(2,?53)+1?===?Math.pow(2,?53)
復(fù)制代碼

很不幸的是,JavaScript 里有關(guān)數(shù)字的怪癖可還不止這些。盡管數(shù)字是浮點(diǎn)型的,但如下這種整數(shù)運(yùn)算能正常工作:

5?%?2?===?1?//?true
5?>>?1?===?2?//?true
復(fù)制代碼

然而和算術(shù)運(yùn)算不同的是,位運(yùn)算和移位運(yùn)算只在小于 32 位最大值的數(shù)字上正常工作。例如,讓“Math.pow(2, 53)” 位移 1 位總是得到 0,讓其與 1 做位運(yùn)算也總是得到 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
復(fù)制代碼

你可能極少會(huì)去處理如此大的數(shù)字,但如果你需要的話,有很多實(shí)現(xiàn)了大型精密數(shù)字運(yùn)算的大整數(shù)庫(kù)可以幫到你,比如 node-bigint。

錯(cuò)誤8:忽略了流式 API 的優(yōu)勢(shì)

現(xiàn)在我們想創(chuàng)建一個(gè)簡(jiǎn)單的類代理 web 服務(wù)器,它能通過(guò)拉取其他 web 服務(wù)器的內(nèi)容來(lái)響應(yīng)和發(fā)起請(qǐng)求。作為例子,我們創(chuàng)建一個(gè)小型 web 服務(wù)器為 Gravatar 的圖像服務(wù)。

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)
復(fù)制代碼

在這個(gè)例子里,我們從 Gravatar 拉取圖片,將它存進(jìn)一個(gè) Buffer 里,然后響應(yīng)請(qǐng)求。如果 Gravatar 的圖片都不是很大的話,這樣做沒(méi)問(wèn)題。但想象下如果我們代理的內(nèi)容大小有上千兆的話,更好的處理方式是下面這樣:

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)
復(fù)制代碼

這里我們只是拉取圖片然后簡(jiǎn)單地以管道方式響應(yīng)給客戶端,而不需要在響應(yīng)它之前讀取完整的數(shù)據(jù)存入緩存。

錯(cuò)誤9:出于 Debug 的目的使用 Console.log

在 Node.js 里,“console.log” 允許你打印任何東西到控制臺(tái)上。比如傳一個(gè)對(duì)象給它,它會(huì)以 JavaScript 對(duì)象的字符形式打印出來(lái)。它能接收任意多個(gè)的參數(shù)并將它們以空格作為分隔符打印出來(lái)。有很多的理由可以解釋為什么開(kāi)發(fā)者喜歡使用它來(lái) debug 他的代碼,然而我強(qiáng)烈建議你不要在實(shí)時(shí)代碼里使用“console.log”。你應(yīng)該要避免在所有代碼里使用“console.log” 去 debug,而且應(yīng)該在不需要它們的時(shí)候把它們注釋掉。你可以使用一種專門(mén)做這種事的庫(kù)代替,比如 debug。

這些庫(kù)提供了便利的方式讓你在啟動(dòng)程序的時(shí)候開(kāi)啟或關(guān)閉具體的 debug 模式,例如,使用 debug 的話,你能夠阻止任何 debug 方法輸出信息到終端上,只要不設(shè)置 DEBUG 環(huán)境變量即可。使用它十分簡(jiǎn)單:

//?app.js
var?debug?=?require(‘debug’)(‘a(chǎn)pp’)
debug(’Hello,?%s!’,?‘world’)
復(fù)制代碼

開(kāi)啟 debug 模式只需簡(jiǎn)單地運(yùn)行下面的代碼把環(huán)境變量 DEBUG 設(shè)置到“app” 或“*” 上:

DEBUG=app?node?app.js
復(fù)制代碼

錯(cuò)誤10:不使用監(jiān)控程序

不管你的 Node.js 代碼是跑在生產(chǎn)環(huán)境或是你的本地開(kāi)發(fā)環(huán)境,一個(gè)能協(xié)調(diào)你程序的監(jiān)控程序是十分值得擁有的。一條經(jīng)常被開(kāi)發(fā)者提及的,針對(duì)現(xiàn)代程序設(shè)計(jì)和開(kāi)發(fā)的建議是你的代碼應(yīng)該有 fail-fast 機(jī)制。如果發(fā)生了一個(gè)意料之外的錯(cuò)誤,不要嘗試去處理它,而應(yīng)該讓你的程序崩潰然后讓監(jiān)控程序在幾秒之內(nèi)重啟它。監(jiān)控程序的好處不只是重啟崩潰的程序,這些工具還能讓你在程序文件發(fā)生改變的時(shí)候重啟它,就像崩潰重啟那樣。這讓開(kāi)發(fā) Node.js 程序變成了一個(gè)更加輕松愉快的體驗(yàn)。

Node.js 有太多的監(jiān)控程序可以使用了,例如:

pm2

forever

nodemon

supervisor

所有這些工具都有它的優(yōu)缺點(diǎn)。一些擅長(zhǎng)于在一臺(tái)機(jī)器上處理多個(gè)應(yīng)用程序,而另一些擅長(zhǎng)于日志管理。不管怎樣,如果你想開(kāi)始寫(xiě)一個(gè)程序,這些都是不錯(cuò)的選擇。

總結(jié)

你可以看到,這其中的一些錯(cuò)誤能給你的程序造成破壞性的影響,在你嘗試使用 Node.js 實(shí)現(xiàn)一些很簡(jiǎn)單的功能時(shí)一些錯(cuò)誤也可能會(huì)導(dǎo)致你受挫。即使 Node.js 已經(jīng)使得新手上手十分簡(jiǎn)單,但它依然有些地方容易讓人混亂。從其他語(yǔ)言過(guò)來(lái)的開(kāi)發(fā)者可能已知道了這其中某些錯(cuò)誤,但在 Node.js 新手里這些錯(cuò)誤都是很常見(jiàn)的。幸運(yùn)的是,它們都可以很容易地避免。我希望這個(gè)簡(jiǎn)短指南能幫助新手更好地編寫(xiě) Node.js 代碼,而且能夠給我們大家開(kāi)發(fā)出健壯高效的軟件。

加入我們一起學(xué)習(xí)吧!


向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI