NodeJS 開發者的 10 個常見錯誤
自 Node.js 公諸於世的那一刻,就伴隨著讚揚和批評的聲音。這個爭論仍在持續,而且並不會很快消失。而我們常常忽略掉這些爭論產生的原因,每種編程語言和平台都是因某些問題而受到批評,而這些問題的產生,是取決於我們如何使用這個平台。不管有多難才能寫出安全的 Node.js 代碼,或有多容易寫出高並發的代碼,該平台已經有相當長一段時間,並已被用來建立一個數量龐大、穩健和成熟的 web 伺服器。這些 web 伺服器伸縮性強,並且它們通過在 Internet 上穩定的運行時間,證明自己的穩定性。
然而,像其它平台一樣,Node.js 容易因開發者問題而受到批評。一些錯誤會降低性能,而其它一些問題會讓 Node.js 直接崩潰。在這篇文章里,我們將會聊一聊關於 Node.js 新手的 10 個常犯錯誤,並讓他們知道如何避免這些錯誤,從而成為一名 Node.js 高手。
錯誤 #1:阻塞事件循環
JavaScript 在 Node.js (就像在瀏覽器一樣) 提供單線程執行環境。這意味著你的程序不能同時執行兩部分代碼,但能通過 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..
// 嘗試從資料庫中獲取一個用戶對象。在這個函數執行的一刻,Node.js 有空去運行代碼其它部分..
db.User.get(userId, function(err, user) {
// .. until the moment the user object has been retrieved here
// .. 直到用戶對象檢索到這裡的那一刻
})
然而,具有計算密集型代碼的 Node.js 實例被數以萬計客戶端同時連接執行時,會導致阻塞事件循環,並使所有客戶端處於等待響應狀態。計算密集型代碼,包括嘗試給一個龐大數組進行排序操作和運行一個格外長的循環等。例如:
function sortUsersByAge(users) {
users.sort(function(a, b) {
return a.age > b.age ? -1 : 1
})
}
基於小 「users」 數組執行 「sortUserByAge」 函數,可能沒什麼問題,當基於龐大數組時,會嚴重影響整體性能。如果在不得不這樣操作的情況下,你必須確保程序除了等待事件循環而別無他事(例如,用 Node.js 建立命令行工具的一部分,整個東西同步運行是沒問題的),然後這可能沒問題。然而,在 Node.js 伺服器實例嘗試同時服務成千上萬個用戶的情況下,這將是一個毀滅性的問題。
如果用戶數組是從資料庫檢索出來的,有個解決辦法是,先在資料庫中排序,然後再直接檢索。如果因需要計算龐大的金融交易歷史數據總和,而造成阻塞事件循環,這可以創建額外的worker / queue 來避免阻塞事件循環。
正如你所看到的,這沒有新技術來解決這類 Node.js 問題,而每種情況都需要單獨處理。而基本解決思路是:不要讓 Node.js 實例的主線程執行 CPU 密集型工作 – 客戶端同時鏈接時。
錯誤 #2:調用回調函數多於一次
JavaScript 一直都是依賴於回調函數。在瀏覽器中,處理事件是通過調用函數(通常是匿名的),這個動作如同回調函數。Node.js 在引進 promises 之前,回調函數是非同步元素用來互相連接對方的唯一方式 。現在回調函數仍被使用,並且包開發者仍然圍繞著回調函數設計 APIs。一個關於使用回調函數的常見 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),而最後一個 「done」 則可省略返回語句。這是因為調用回調函數後,並不會自動結束當前執行函數。如果第一個 「return」 注釋掉,然後給這個函數傳進一個非字元串密碼,導致 「computeHash」 仍然會被調用。這取決於 「computeHash」 如何處理這樣一種情況,「done」 可能會調用多次。任何一個人在別處使用這個函數可能會變得措手不及,因為它們傳進的該回調函數被多次調用。
只要小心就可以避免這個 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:用「exports」,而不是「module.exports」
Node.js 將每個文件視為一個孤立的小模塊。如果你的包(package)含有兩個文件,或許是 「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 開發新手們產生挫敗感。
錯誤 #6:在回調函數內拋出錯誤
JavaScript 有個「異常」概念。異常處理與大多數傳統語言的語法類似,例如 Java 和 C++,JavaScript 能在 try-catch 塊內 「拋出(throw)」 和 捕捉(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 中都是浮點型,JS 沒有整型。你可能不能預料到這將是一個問題,因為數大到超出浮點型範圍的情況並不常見。
Math.pow(2, 53)+1 === Math.pow(2, 53)
不幸的是,在 JavaScript 中,這種關於數字的怪異情況遠不止於此。儘管數字都是浮點型,對於下面的表達式,操作符對於整型也能正常運行:
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:忽略了 Streaming(流) API 的優勢
大家都說想建立一個小型代理伺服器,它能響應從其它伺服器獲取內容的請求。作為一個案例,我們將建立一個供應 Gravatar 圖像的小型 Web 伺服器:
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 獲取圖像,將它讀進緩存區,然後響應請求。這不是一個多麼糟糕的問題,因為 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 對象字面量的方式列印出來。它接受任意多個參數,並以空格作為分隔符列印它們。有許多個理由讓開發者很想用這個來調試(debug)自己的代碼;然而,我強烈建議你避免在真正程序里使用 「console.log」 。你應該避免在全部代碼里使用 「console.log」 進行調試(debug),當不需要它們的時候,應注釋掉它們。相反,使用專門為調試建立的庫,如:debug。
當你開始編寫應用程序時,這些庫能方便地啟動和禁用某行調試(debug)功能。例如,通過不設置 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 可用的管理程序。例如:
pm2
forever
nodemon
supervisor
所有這些工具各有優劣。一些有利於在同一個機器里處理多個應用程序,而其它擅長於日誌管理。然而,如果你想開始使用這些程序,它們都是很好的選擇。
總結
正如你所知道的那樣,一些 Node.js 問題能對你的程序造成毀滅性打擊。而一些則會在你嘗試完成最簡單的東西時,讓你產生挫敗感。儘管 Node.js 的開發門檻較低,但它仍然有很容易搞混的地方。從其它編程語言轉過來學習 Node.js 開發者可能會遇到這些問題,但這些錯誤在 Node.js 新手中也是十分常見的。幸運的是,它們很容易避免。我希望這個簡短指導能幫助 Node.js 新手寫出更優秀的代碼,並為我們開發出穩定高效的軟體。
推薦閱讀:
※(44)新「男德」守則:身為男人,你還在犯這25個錯誤嗎?
※妻子這三種錯誤,再好也別原諒她!
※男人常犯的性錯誤
※小心!敷面膜的10個致命錯誤
※有緣相遇,無緣牽手.在錯誤的時間,遇上了對的人→《愛過了今生就沒遺憾