序文
Node.jsのの発売以来、それは賞賛と批判の多くを取得します。この議論は終了しません短い時間まで継続します。これらの議論では、私たちはしばしば、すべての言語やプラットフォームを無視批判するコアの問題のいくつかに基づいており、それは我々がこれらのプラットフォームを使用しない方法、です。そして、どのように単純な問題で並行コードが書かれているいかに難しいか、高いのNode.jsを使用して信頼性の高いコードを書くために、プラットフォームは結局そこの期間があって、堅牢で複雑なWebサービスの多数を作成するために使用されます。これらのWebサービスは、優れた拡張性を持っている、と期間を介してインターネット上での堅牢性を証明されていないだけ。
しかし、他のプラットフォームとして、のように、開発者は簡単にミスをすることができますNode.jsの。これらのエラーのいくつかは、プログラムのパフォーマンスが低下しますNode.jsのは、いくつか使用不可能につながります。この記事では、エラーのNode.jsの初心者の10種類が表示され、どのようにそれらを避けるために。
エラー1:イベントループを遮断します
JavaScriptで(ブラウザなど)のNode.jsはシングルスレッド環境を提供します。これは、あなたのプログラムではなく、非同期処理のため、同時に2つのことを実行する必要がないことを意味するI / O集中型の操作は、同時実行もたらされます。データベースは、いくつかのデータを取得するための要求を開始するためにNode.jsのとき例えば、Node.jsのは、プログラムの他の部分に集中することができます。
//データベースからユーザーオブジェクトを取得しようとしています。Node.jsのは、この関数が呼び出された時点からのコードの他の部分を実行するために自由である.. db.User.get(userIdを、関数(ERR、ユーザ){ // ..瞬間までユーザオブジェクトは、ここで検索されました }) 复制代码
ただし、クライアント接続のNode.jsの数千の例では、短いCPU計算集約コードを待たなければならなかったすべてのクライアントにつながる、ライブイベントループを詰まらせます。CPUの計算集約型のコードはように、巨大な配列をソート非常に長い時間のかかる機能を実行し、しようとする試みが含まれています。例えば:
関数sortUsersByAge(ユーザ){ users.sort(関数(B){ 戻りa.age <b.age -1:1 }) } 复制代码
配列の小さな「ユーザー」の「sortUsersByAge」メソッドを呼び出すことは問題ありませんが、それは大きな配列上にある場合、それは全体のパフォーマンスに大きな影響を持つことになります。この事はしなければならなかった、とあなたは必ずイベントループには他のイベントが待っていないことを確認できた場合(例えばNode.jsのように、これは単なるコマンドラインツールであるが、すべてのものが動作するように同期されている問題ではない)、これは問題ありませんことを、そして、。しかし、同時に数千のユーザーにサービスを提供しようとしているのNode.jsサーバの場合には、それが問題の原因となります。
ユーザー配列がデータベースからフェッチされる場合には、理想的な解決策は、データベースからデータを整理していることです。イベントはブロックされた歴史的な金融取引データの合計値を計算するための循環ループである場合は、このサイクルは、イベントループを取るしないように、イベントループの実行キューの外に押されるように計算されます。
あなたが見ることができるように、このタイプのエラーを解決する特効薬はありません、それが唯一それぞれのケースで個別に解決することができます。基本的な考え方は、計算集約型の仕事をするためにCPUの同時接続のNode.jsのインスタンス内のクライアントに対処するためではありません。
エラー2:複数のコールバック関数を呼び出します
JavaScriptは常にコールバック関数に依存してきました。ブラウザで、イベントは、によって処理するコールバック関数(通常は匿名関数)を参照イベントオブジェクトによって渡されます。Node.jsのでは、他のコードとの唯一の方法は、非同期通信に一度コールバック関数は約束が表示されるまで。コールバック関数は、まだ現在使用中であり、多くの開発者はAPIを設定し、その周りにはまだです。コールバック関数の使用に伴う一般的なエラーは、彼らに何度も呼び出すことです。典型的には、非同期処理の数を包装する方法は、それが非同期を処理した後に呼ばれる伝達関数として設計された最後のパラメータです。
module.exports.verifyPassword =機能(ユーザー、パスワード、行わ){ (typeof演算パスワード!== '文字列')であれば{ なさ(新しいエラー( 'パスワードは文字列でなければなりません')) リターン } computeHash(パスワード、user.passwordHashOpts 、関数(ERR、ハッシュ){ IF(ERR){ 行わ(ERR) リターン } 行う(ヌル、ハッシュ=== user.passwordHash) }) } 复制代码
ここでは、メソッドが呼び出された後、最後の時間を除いて、「完了」するたびに、return文があることに注意してください。コールバック関数が自動的に現在のメソッドの実行を終了していないためです。我々は最初のreturn文をコメントアウトした場合は、この関数にパスワードの文字列以外の型を渡す、我々はまだコールcomputeHash方法で終了します。この場合computeHashアプローチよると、「完了」を複数回呼び出されます。コールバック関数の最後のパスが複数回呼び出されると、誰でも不意打ちされます。
注意する必要があるだけで、この問題を回避します。いくつかのNode.jsの開発者は、したがって、すべてのコールバック関数ステートメントの前に習慣、プラスのリターンキーワードを開発します:
(ERR){IF リターンDONE(ERR)は } コードをコピー
ただ単純にのみ、このエラーを回避するために、このような動き非常に多くの非同期機能では、戻り値を返すのこの種のは、無意味です。
エラー3:深くネストされたコールバック関数
深くネストされたコールバック関数は、通常は問題ではありません、それ自体で「コールバック地獄」として知られているが、それはすぐに制御不能になったコードにつながることができます:
関数handleLogin(...、行って){ db.User.get(...、機能(...、ユーザー){ 場合(!ユーザー){ リターンが行われ(ヌル'のログインに失敗しました。') } utilsの。 verifyPassword(...、関数(...、大丈夫){ IF(大丈夫){ リターンが行わ(NULL、 'ログインに失敗しました') } session.login(...、関数(){ '、ヌル(行わログイン) }) }) }) } 复制代码
タスク、大きな害より複雑。ネストされたコールバックと同様に、私たちのプログラムはエラーを起こしやすい、そして困難なコードが読み込まれ、維持します。これらのタスクは、その場しのぎの対策は小さな機能の一つとして宣言することであり、その後、それらを一緒にリンクします。しかし、(存在し得る)最も簡単な解決策の一つは、Async.jsような非同期JS、に対処するためのNode.jsパブリックアセンブリを使用することです。
関数handleLogin(行なわ){ async.waterfall([ 機能(実行される){ db.User.get(...、行って) }、 機能(ユーザー、行わ){ 場合(!ユーザー){ リターン行われ(nullは、「失敗しました)ログインする } utils.verifyPassword(...、関数(...、大丈夫){ (ヌル、ユーザ、大丈夫)行わ }) }、 関数(ユーザ、大丈夫、行わ){ IF(大丈夫){ リターン(NULL、 'ログインに失敗しました')完了 } (...、関数(){session.login 行う(NULL、 'ログイン') }) } ]、関数(){ // ... }) } 复制代码
Async.jsも異なる非同期のシナリオに対処するための多くの同様の「async.waterfall」アプローチを提供しています。簡単にするため、ここでは簡単な例を示し、現実には、多くの場合、より複雑です。
(:Dポスト広告隣の「ES6ジェネレータはじめに」ジェネレーターはああ地獄コールバックを解決することができ、より自然な組み合わせ約束の使用は、家主の右隣に次の記事、お楽しみに言及しました)
エラー4:コールバック関数は、同期を実行することを期待
非同期コールバック関数は、単にJavaScriptとNode.jsのを持っていないが、彼らは、この誘導プログラムが普及するようになったしましょう。他のプログラミング言語では、私たちが次々と実行の二つの文を使用し、二つの文の間でない限り、特別なジャンプ命令を持っています。それでも、これらは条件文、ループ文と関数呼び出しによって制限されていました。
しかし、JavaScriptで、タスクが完了するまで、コールバック機能を備えたコールバックメソッドが完了しなくてもよいです。現在の関数は、最後に実行されています。
機能testTimeout(){ はconsole.log(「開始」) のsetTimeout(関数(){ はconsole.log(「やった!」) }、期間* 1000) はconsole.log(「待機中..」) } 复制代码
あなたは、その後、「開始」を出力する「testTimeout」関数を呼び出し、出力「待機中..」に気づくことがあり、出力数秒後に続いて「完了します!」。
コールバック関数内で実行される任意のコードが完了する前に、我々は、コールバック関数で呼び出す必要があります。
エラー5:「輸出」の割り当てではなく、「module.exportsは」へ
Node.jsの各ファイルかかわらず独立したモジュールです。あなたのバッグは、2つのファイルを持っている場合、仮定は「a.js」と「b.js」で、そして「a.js」機能、「a.jsを」使用する「b.jsは、」輸出ターゲットに渡す必要がありますこれらの機能を公開するプロパティを追加します:
// 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 语句块中捕获异常:
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 对象。
错误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 做位运算也总是得到 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。
错误8:忽略了流式 API 的优势
现在我们想创建一个简单的类代理 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) 复制代码
这里我们只是拉取图片然后简单地以管道方式响应给客户端,而不需要在响应它之前读取完整的数据存入缓存。
错误9:出于 Debug 的目的使用 Console.log
在 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 复制代码
错误10:不使用监控程序
不管你的 Node.js 代码是跑在生产环境或是你的本地开发环境,一个能协调你程序的监控程序是十分值得拥有的。一条经常被开发者提及的,针对现代程序设计和开发的建议是你的代码应该有 fail-fast 机制。如果发生了一个意料之外的错误,不要尝试去处理它,而应该让你的程序崩溃然后让监控程序在几秒之内重启它。监控程序的好处不只是重启崩溃的程序,这些工具还能让你在程序文件发生改变的时候重启它,就像崩溃重启那样。这让开发 Node.js 程序变成了一个更加轻松愉快的体验。
Node.js 有太多的监控程序可以使用了,例如:
pm2
forever
nodemon
supervisor
所有这些工具都有它的优缺点。一些擅长于在一台机器上处理多个应用程序,而另一些擅长于日志管理。不管怎样,如果你想开始写一个程序,这些都是不错的选择。
总结
你可以看到,这其中的一些错误能给你的程序造成破坏性的影响,在你尝试使用 Node.js 实现一些很简单的功能时一些错误也可能会导致你受挫。即使 Node.js 已经使得新手上手十分简单,但它依然有些地方容易让人混乱。从其他语言过来的开发者可能已知道了这其中某些错误,但在 Node.js 新手里这些错误都是很常见的。幸运的是,它们都可以很容易地避免。我希望这个简短指南能帮助新手更好地编写 Node.js 代码,而且能够给我们大家开发出健壮高效的软件。
加入我们一起学习吧!