歡迎光臨
每天分享高質量文章

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 新手寫出更優秀的程式碼,併為我們開發出穩定高效的軟體。

原文出處:www.toptal.com

譯文出處:伯樂線上 – 劉健超-J.c

連結:http://web.jobbole.com/82504/


贊(0)

分享創造快樂