標籤:

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個致命錯誤
    有緣相遇,無緣牽手.在錯誤的時間,遇上了對的人→《愛過了今生就沒遺憾

    TAG:錯誤 | 開發者 |