node的十大常見錯誤:1、阻塞事件迴圈;2、多次呼叫一個回撥函數;3、深層巢狀的回撥函數;4、期待回撥函數同步執行;5、給「exports」賦值;6、從回撥裡丟擲錯誤;7、認為Number是一種整型資料格式;8、忽略流式API的優勢等等。
本教學操作環境:windows7系統、nodejs 12.19.0版,DELL G3電腦。
自 Node.js 面世以來,它獲得了大量的讚美和批判。這種爭論會一直持續,短時間內都不會結束。而在這些爭論中,我們常常會忽略掉所有語言和平臺都是基於一些核心問題來批判的,就是我們怎麼去使用這些平臺。無論使用 Node.js 編寫可靠的程式碼有多難,而編寫高並行程式碼又是多麼的簡單,這個平臺終究是有那麼一段時間了,而且被用來建立了大量的健壯而又複雜的 web 服務。這些 web 服務不僅擁有良好的擴充套件性,而且通過在網際網路上持續的時間證明了它們的健壯性。
然而就像其它平臺一樣,Node.js 很容易令開發者犯錯。這些錯誤有些會降低程式效能,有些則會導致 Node.js 不可用。在本文中,我們會看到 Node.js 新手常犯的 十種錯誤
,以及如何去避免它們。
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 範例裡,一小段 CPU 計算密集的程式碼會阻塞住事件迴圈,導致所有使用者端都得等待。CPU 計算密集型程式碼包括了嘗試排序一個巨大的陣列、跑一個耗時很長的函數等等。例如:
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。一個跟使用回撥函數相關的常見錯誤是多次呼叫它們。通常,一個封裝了一些非同步處理的方法,它的最後一個引數會被設計為傳遞一個函數,這個函數會在非同步處理完後被呼叫:
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相關知識,請存取:!!
以上就是nodejs有哪十大常見錯誤的詳細內容,更多請關注TW511.COM其它相關文章!