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 新手常犯的 <span class="pun">十種錯誤</span>
#,以及如何去避免它們。
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 &lt; 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 代码是跑在生产环境或是你的本地开发环境,一个能协调你程序的监控程序是十分值得拥有的。一条经常被开发者提及的,针对现代程序设计和开发的建议是你的代码应该有 <span class="pln">fail</span><span class="pun">-</span><span class="pln">fast</span>
机制。如果发生了一个意料之外的错误,不要尝试去处理它,而应该让你的程序崩溃然后让监控程序在几秒之内重启它。监控程序的好处不只是重启崩溃的程序,这些工具还能让你在程序文件发生改变的时候重启它,就像崩溃重启那样。这让开发 Node.js 程序变成了一个更加轻松愉快的体验。
Node.js 有太多的监控程序可以使用了,例如:
<span class="pln">pm2</span>
<span class="pln">forever</span>
<span class="pln">nodemon</span>
<span class="pln">supervisor</span>
所有这些工具都有它的优缺点。一些擅长于在一台机器上处理多个应用程序,而另一些擅长于日志管理。不管怎样,如果你想开始写一个程序,这些都是不错的选择。
总结
你可以看到,这其中的一些错误能给你的程序造成破坏性的影响,在你尝试使用 Node.js 实现一些很简单的功能时一些错误也可能会导致你受挫。即使 Node.js 已经使得新手上手十分简单,但它依然有些地方容易让人混乱。从其他语言过来的开发者可能已知道了这其中某些错误,但在 Node.js 新手里这些错误都是很常见的。幸运的是,它们都可以很容易地避免。我希望这个简短指南能帮助新手更好地编写 Node.js 代码,而且能够给我们大家开发出健壮高效的软件。
更多node相关知识,请访问:nodejs 教程!!
以上是nodejs有哪十大常見錯誤的詳細內容。更多資訊請關注PHP中文網其他相關文章!