自 Node.js 面世以來,它得到了大量的讚美和批判。這種爭論會一直持續,短期內都不會結束。而在這些爭論中,咱們經常會忽略掉全部語言和平臺都是基於一些核心問題來批判的,就是咱們怎麼去使用這些平臺。不管使用 Node.js 編寫可靠的代碼有多難,而編寫高併發代碼又是多麼的簡單,這個平臺終究是有那麼一段時間了,並且被用來建立了大量的健壯而又複雜的 web 服務。這些 web 服務不只擁有良好的擴展性,並且經過在互聯網上持續的時間證實了它們的健壯性。javascript
然而就像其它平臺同樣,Node.js 很容易令開發者犯錯。這些錯誤有些會下降程序性能,有些則會致使 Node.js 不可用。在本文中,咱們會看到 Node.js 新手常犯的十種錯誤
,以及如何去避免它們。java
Node.js(正如瀏覽器)裏的 JavaScript 提供了一種單線程環境。這意味着你的程序不會有兩塊東西同時在運行,取而代之的是異步處理 I/O 密集操做所帶來的併發。好比說 Node.js 給數據庫發起一個請求去獲取一些數據時,Node.js 能夠集中精力在程序的其餘地方:node
// 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 計算密集型代碼包括了嘗試排序一個巨大的數組、跑一個耗時很長的函數等等。例如:web
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 計算密集型工做。數組
一直以來 JavaScript 都依賴於回調函數。在瀏覽器裏,事件都是經過傳遞事件對象的引用給一個回調函數(一般都是匿名函數)來處理。在 Node.js 裏,回調函數曾經是與其餘代碼異步通訊的惟一方式,直到 promise 出現。回調函數如今仍在使用,並且不少開發者依然圍繞着它來設置他們的 API。一個跟使用回調函數相關的常見錯誤是屢次調用它們。一般,一個封裝了一些異步處理的方法,它的最後一個參數會被設計爲傳遞一個函數,這個函數會在異步處理完後被調用:promise
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 關鍵詞:緩存
if(err) {
return done(err)
}
複製代碼
在不少異步函數裏,這種 return 的返回值都是沒有意義的,因此這種舉動只是爲了簡單地避免這個錯誤而已。
深層嵌套的回調函數一般被譽爲「 回調地獄」,它自己並非什麼問題,可是它會致使代碼很快變得失控:
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 介紹》說起的 Generator 也是能夠解決回調地獄的哦,並且結合 Promise 使用更加天然,請期待隔壁樓主的下篇文章吧:D)
使用回調函數的異步程序不僅是 JavaScript 和 Node.js 有,只是它們讓這種異步程序變得流行起來。在其餘編程語言裏,咱們習慣了兩個語句一個接一個執行,除非兩個語句之間有特殊的跳轉指令。即便那樣,這些還受限於條件語句、循環語句以及函數調用。
然而在 JavaScript 裏,一個帶有回調函數的方法直到回調完成以前可能都沒法完成任務。當前函數會一直執行到底:
function testTimeout() {
console.log(「Begin」)
setTimeout(function() {
console.log(「Done!」)
}, duration * 1000)
console.log(「Waiting..」)
}
複製代碼
你可能會注意到,調用「testTimeout」 函數會先輸出「Begin」,而後輸出「Waiting..」,緊接着幾秒後輸出「Done!」。
任何要在回調函數執行完後才執行的代碼,都須要在回調函數裏調用。
Node.js 認爲每一個文件都是一個獨立的模塊。若是你的包有兩個文件,假設是「a.js」 和「b.js」,而後「b.js」 要使用「a.js」 的功能,「a.js」 必需要經過給 exports 對象增長屬性來暴露這些功能:
// a.js
exports.verifyPassword = function(user, password, done) { ... }
複製代碼
完成這步後,全部須要「a.js」 的都會得到一個帶有「verifyPassword」 函數屬性的對象:
// b.js
require(‘a.js’) // { verifyPassword: function(user, password, done) { ... } }
複製代碼
然而,若是咱們想直接暴露這個函數,而不是讓它做爲某些對象的屬性呢?咱們能夠覆寫 exports 來達到目的,可是咱們絕對不能把它當作一個全局變量:
// a.js
module.exports = function(user, password, done) { ... }
複製代碼
注意到咱們是把「exports」 當作 module 對象的一個屬性。「module.exports」 和「exports」 這之間區別是很重要的,並且常常會使 Node.js 新手踩坑。
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, ...) 這種形式,其中第一個參數是錯誤發生時的 error 對象。
在 JavaScript 裏數字都是浮點型,沒有整型的數據格式。你可能認爲這不是什麼問題,由於數字大到溢出浮點型限制的狀況不多出現。可實際上,當這種狀況發生時就會出錯。由於浮點數在表達一個整型數時只能表示到一個最大上限值,在計算中超過這個最大值時就會出問題。也許看起來有些奇怪,但在 Node.js 中下面代碼的值是 true:
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。
如今咱們想建立一個簡單的類代理 web 服務器,它能經過拉取其餘 web 服務器的內容來響應和發起請求。做爲例子,咱們建立一個小型 web 服務器爲 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 拉取圖片,將它存進一個 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)
複製代碼
這裏咱們只是拉取圖片而後簡單地以管道方式響應給客戶端,而不須要在響應它以前讀取完整的數據存入緩存。
在 Node.js 裏,「console.log」 容許你打印任何東西到控制檯上。好比傳一個對象給它,它會以 JavaScript 對象的字符形式打印出來。它能接收任意多個的參數並將它們以空格做爲分隔符打印出來。有不少的理由能夠解釋爲何開發者喜歡使用它來 debug 他的代碼,然而我強烈建議你不要在實時代碼裏使用「console.log」。你應該要避免在全部代碼裏使用「console.log」 去 debug,並且應該在不須要它們的時候把它們註釋掉。你可使用一種專門作這種事的庫代替,好比 debug。
這些庫提供了便利的方式讓你在啓動程序的時候開啓或關閉具體的 debug 模式,例如,使用 debug 的話,你可以阻止任何 debug 方法輸出信息到終端上,只要不設置 DEBUG 環境變量便可。使用它十分簡單:
// app.js
var debug = require(‘debug’)(‘app’)
debug(’Hello, %s!’, ‘world’)
複製代碼
開啓 debug 模式只需簡單地運行下面的代碼把環境變量 DEBUG 設置到「app」 或「*」 上:
DEBUG=app node app.js
複製代碼
無論你的 Node.js 代碼是跑在生產環境或是你的本地開發環境,一個能協調你程序的監控程序是十分值得擁有的。一條常常被開發者說起的,針對現代程序設計和開發的建議是你的代碼應該有 fail-fast
機制。若是發生了一個意料以外的錯誤,不要嘗試去處理它,而應該讓你的程序崩潰而後讓監控程序在幾秒以內重啓它。監控程序的好處不僅是重啓崩潰的程序,這些工具還能讓你在程序文件發生改變的時候重啓它,就像崩潰重啓那樣。這讓開發 Node.js 程序變成了一個更加輕鬆愉快的體驗。
Node.js 有太多的監控程序可使用了,例如:
pm2
forever
nodemon
supervisor
全部這些工具都有它的優缺點。一些擅長於在一臺機器上處理多個應用程序,而另外一些擅長於日誌管理。無論怎樣,若是你想開始寫一個程序,這些都是不錯的選擇。
你能夠看到,這其中的一些錯誤能給你的程序形成破壞性的影響,在你嘗試使用 Node.js 實現一些很簡單的功能時一些錯誤也可能會致使你受挫。即便 Node.js 已經使得新手上手十分簡單,但它依然有些地方容易讓人混亂。從其餘語言過來的開發者可能已知道了這其中某些錯誤,但在 Node.js 新手裏這些錯誤都是很常見的。幸運的是,它們均可以很容易地避免。我但願這個簡短指南能幫助新手更好地編寫 Node.js 代碼,並且可以給咱們你們開發出健壯高效的軟件。
加入咱們一塊兒學習吧!
node學習交流羣交流羣滿100人不能自動進羣, 請添加羣助手微信號:【coder_qi】備註node,自動拉你入羣。