Node.js 開發人員最常犯的 10 個錯誤

已發表: 2022-03-11

自從 Node.js 面世以來,它就受到了相當多的讚揚和批評。 辯論仍在繼續,可能不會很快結束。 在這些辯論中我們經常忽略的是,每種編程語言和平台都會基於某些問題受到批評,這些問題是由我們使用平台的方式造成的。 不管 Node.js 讓編寫安全代碼多麼困難,以及編寫高並發代碼多麼容易,該平台已經存在了很長一段時間,並已被用於構建大量強大而復雜的 Web 服務。 這些 Web 服務擴展性很好,並且通過在 Internet 上的持久性證明了它們的穩定性。

但是,與任何其他平台一樣,Node.js 容易受到開發人員問題的影響。 其中一些錯誤會降低性能,而另一些錯誤會使 Node.js 看起來無法用於您想要實現的任何目標。 在本文中,我們將看看 Node.js 新手經常犯的十個常見錯誤,以及如何避免這些錯誤成為 Node.js 專業人士。

node.js 開發人員錯誤

錯誤 #1:阻塞事件循環

Node.js 中的 JavaScript(就像在瀏覽器中一樣)提供了一個單線程環境。 這意味著您的應用程序沒有兩個部分是並行運行的。 相反,並發是通過異步處理 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.. db.User.get(userId, function(err, user) { // .. until the moment the user object has been retrieved here }) 

node.js 單線程環境

然而,在連接了數千個客戶端的 Node.js 實例中,一段受 CPU 限制的代碼就足以阻止事件循環,讓所有客戶端等待。 受 CPU 限制的代碼包括嘗試對大型數組進行排序、運行極長的循環等等。 例如:

 function sortUsersByAge(users) { users.sort(function(a, b) { return a.age < b.age ? -1 : 1 }) }

如果在一個小的“users”數組上運行,調用這個“sortUsersByAge”函數可能沒問題,但是對於一個大數組,它會對整體性能產生可怕的影響。 如果這是絕對必須完成的事情,並且您確定在事件循環上不會有任何其他等待(例如,如果這是您使用 Node.js 構建的命令行工具的一部分,並且它如果整個事情同步運行都沒有關係),那麼這可能不是問題。 但是,在嘗試一次為數千名用戶提供服務的 Node.js 服務器實例中,這種模式可能是致命的。

如果從數據庫中檢索此用戶數組,理想的解決方案是直接從數據庫中獲取已排序的用戶。 如果事件循環被編寫用於計算長期金融交易數據歷史總和的循環阻塞,則可以將其推遲到某些外部工作者/隊列設置,以避免佔用事件循環。

如您所見,對於此類 Node.js 問題沒有靈丹妙藥的解決方案,而是需要單獨解決每種情況。 基本思想是不在前端 Node.js 實例中進行 CPU 密集型工作——客戶端同時連接的那些。

錯誤 #2:多次調用回調

JavaScript 一直依賴回調。 在 Web 瀏覽器中,事件是通過傳遞對(通常是匿名的)函數的引用來處理的,這些函數的作用類似於回調。 在 Node.js 中,回調曾經是代碼中異步元素相互通信的唯一方式——直到引入了 Promise。 回調仍在使用中,包開發人員仍然圍繞回調設計他們的 API。 與使用回調相關的一個常見 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”被註釋掉,向這個函數傳遞一個非字符串密碼仍然會導致“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') }) }) }) } 

回調地獄

任務越複雜,情況就越糟糕。 通過以這種方式嵌套回調,我們很容易以容易出錯、難以閱讀和難以維護的代碼告終。 一種解決方法是將這些任務聲明為小函數,然後將它們鏈接起來。 雖然,(可以說)最乾淨的解決方案之一是使用處理異步 JavaScript 模式的實用程序 Node.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.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 將每個文件視為一個小的獨立模塊。 如果你的包有兩個文件,可能是“a.js”和“b.js”,那麼為了讓“b.js”訪問“a.js”的功能,“a.js”必須通過添加屬性來導出它出口對象:

 // a.js exports.verifyPassword = function(user, password, done) { ... }

完成後,任何需要“a.js”的人都將獲得一個具有屬性函數“verifyPassword”的對象:

 // b.js require('a.js') // { verifyPassword: function(user, password, done) { ... } }

但是,如果我們想直接導出這個函數,而不是作為某個對象的屬性呢? 我們可以覆蓋導出來執行此操作,但我們不能將其視為全局變量:

 // a.js module.exports = function(user, password, done) { ... }

請注意我們如何將“導出”視為模塊對象的屬性。 這裡“module.exports”和“exports”之間的區別非常重要,並且經常是新 Node.js 開發人員感到沮喪的原因。

錯誤 #6:從內部回調中拋出錯誤

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!') }

但是,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:假設 Number 是整數數據類型

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 將計算為 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:忽略流式 API 的優勢

假設我們想要構建一個類似代理的小型 Web 服務器,通過從另一個 Web 服務器獲取內容來響應請求。 例如,我們將構建一個服務 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 對象字面量。 它接受任意數量的參數,並以空格分隔的方式打印它們。 開發人員可能會想用它來調試他的代碼有很多原因; 但是,強烈建議您在實際代碼中避免使用“console.log”。 您應該避免在整個代碼中編寫“console.log”來調試它,然後在不再需要它們時將它們註釋掉。 相反,請使用專門為此構建的令人驚嘆的庫之一,例如調試。

此類軟件包提供了在啟動應用程序時啟用和禁用某些調試行的便捷方法。 例如,使用 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

  • 永遠

  • 節點監視器

  • 導師

所有這些工具都各有利弊。 其中一些適合在同一台機器上處理多個應用程序,而另一些則更擅長日誌管理。 但是,如果您想開始使用這樣的程序,所有這些都是公平的選擇。

結論

如您所知,其中一些 Node.js 問題可能會對您的程序產生破壞性影響。 當您嘗試在 Node.js 中實現最簡單的事情時,有些可能是令人沮喪的原因。 儘管 Node.js 讓新手上手非常容易,但它仍然存在同樣容易搞砸的地方。 來自其他編程語言的開發人員可能能夠解決其中一些問題,但這些錯誤在 Node.js 新開發人員中很常見。 幸運的是,它們很容易避免。 我希望這個簡短的指南能夠幫助初學者在 Node.js 中編寫更好的代碼,並為我們所有人開發穩定高效的軟件。