Node.js開発者が犯す最も一般的な間違いトップ10
公開: 2022-03-11Node.jsが世界に公開されて以来、賞賛と批判の両方がかなりの割合で見られます。 議論はまだ続いており、すぐには終わらないかもしれません。 これらの議論で私たちがしばしば見落としているのは、すべてのプログラミング言語とプラットフォームは、プラットフォームの使用方法によって作成される特定の問題に基づいて批判されているということです。 Node.jsが安全なコードの記述をいかに困難にし、高度な並行コードの記述をいかに容易にするかにかかわらず、プラットフォームはかなり前から存在しており、膨大な数の堅牢で洗練されたWebサービスを構築するために使用されてきました。 これらのWebサービスは拡張性が高く、インターネットでの時間の耐久性を通じて安定性が証明されています。
ただし、他のプラットフォームと同様に、Node.jsは開発者の問題や問題に対して脆弱です。 これらの間違いのいくつかはパフォーマンスを低下させますが、他の間違いはNode.jsをあなたが達成しようとしているものにはまったく使用できないように見せます。 この記事では、Node.jsを初めて使用する開発者がよく犯す10のよくある間違いと、それらを回避してNode.jsプロになる方法を見ていきます。
間違い#1:イベントループのブロック
Node.jsのJavaScript(ブラウザーの場合と同様)は、シングルスレッド環境を提供します。 これは、アプリケーションの2つの部分が並行して実行されないことを意味します。 代わりに、同時実行性は、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 }) }
この「sortUsersByAge」関数を呼び出すことは、小さな「users」配列で実行する場合は問題ないかもしれませんが、大きな配列では、全体的なパフォーマンスにひどい影響を及ぼします。 これが絶対に行わなければならないことであり、イベントループで他に何も待機していないことが確実な場合(たとえば、これがNode.jsで構築しているコマンドラインツールの一部であった場合、すべてが同期して実行されたかどうかは関係ありません)、これは問題ではない可能性があります。 ただし、一度に数千人のユーザーにサービスを提供しようとするNode.jsサーバーインスタンスでは、このようなパターンは致命的となる可能性があります。
このユーザーの配列がデータベースから取得されている場合、理想的な解決策は、すでにソートされているユーザーをデータベースから直接フェッチすることです。 金融取引データの長い履歴の合計を計算するために記述されたループによってイベントループがブロックされていた場合、イベントループの占有を回避するために、外部のワーカー/キューの設定に延期される可能性があります。
ご覧のとおり、この種のNode.jsの問題に対する特効薬の解決策はありません。むしろ、それぞれのケースに個別に対処する必要があります。 基本的な考え方は、前面に面したNode.jsインスタンス(クライアントが同時に接続するインスタンス)内でCPUを集中的に使用しないようにすることです。
間違い#2:複数回のコールバックの呼び出し
JavaScriptは、永遠にコールバックに依存してきました。 Webブラウザーでは、イベントは、コールバックのように機能する(多くの場合無名の)関数への参照を渡すことによって処理されます。 Node.jsでは、promiseが導入されるまで、コードの非同期要素が相互に通信する唯一の方法はコールバックでした。 コールバックはまだ使用されており、パッケージ開発者はコールバックを中心にAPIを設計しています。 コールバックの使用に関連する一般的なNode.jsの問題の1つは、コールバックを複数回呼び出すことです。 通常、非同期で何かを実行するためにパッケージによって提供される関数は、非同期タスクが完了したときに呼び出される最後の引数として関数を期待するように設計されています。
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」がコメント化されている場合でも、この関数に文字列以外のパスワードを渡すと、「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') }) }) }) }
タスクが複雑になるほど、これは悪化する可能性があります。 このようにコールバックをネストすることで、エラーが発生しやすく、読みにくく、コードの保守が困難になります。 回避策の1つは、これらのタスクを小さな関数として宣言してから、それらをリンクすることです。 ただし、これに対する(おそらく)最もクリーンなソリューションの1つは、Async.jsなどの非同期JavaScriptパターンを処理するユーティリティNode.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に固有のものではないかもしれませんが、その人気の原因となっています。 他のプログラミング言語では、ステートメント間をジャンプする特定の命令がない限り、2つのステートメントが次々に実行される予測可能な実行順序に慣れています。 それでも、これらは多くの場合、条件ステートメント、ループステートメント、および関数呼び出しに限定されます。
ただし、JavaScriptでは、コールバックを使用すると、待機しているタスクが終了するまで特定の関数が適切に実行されない場合があります。 現在の関数の実行は、停止することなく最後まで実行されます。
function testTimeout() { console.log(“Begin”) setTimeout(function() { console.log(“Done!”) }, duration * 1000) console.log(“Waiting..”) }
お気づきのように、「testTimeout」関数を呼び出すと、最初に「Begin」が出力され、次に「Waiting ..」が出力され、その後に「Done!」というメッセージが表示されます。 約1秒後。
コールバックが発生した後に発生する必要があるものはすべて、コールバック内から呼び出す必要があります。
間違い#5:「module.exports」ではなく「exports」に割り当てる
Node.jsは、各ファイルを小さな分離されたモジュールとして扱います。 パッケージに「a.js」と「b.js」の2つのファイルがある場合、「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には例外の概念があります。 JavaScriptは、JavaやC ++などの例外処理をサポートするほとんどすべての従来の言語の構文を模倣して、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は、非同期の状況で期待されるようには動作しません。 たとえば、1つの大きな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の数値は浮動小数点であり、整数データ型はありません。 フロートの限界を強調するのに十分な数が頻繁に発生しないため、これが問題になるとは思わないでしょう。 それはまさにこれに関連する間違いが起こるときです。 浮動小数点数は特定の値までの整数表現しか保持できないため、計算でその値を超えるとすぐに混乱し始めます。 奇妙に思われるかもしれませんが、Node.jsでは次のように評価されます。
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から画像をフェッチし、それを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:デバッグ目的でConsole.logを使用する
Node.jsでは、「console.log」を使用すると、ほとんどすべてのものをコンソールに出力できます。 オブジェクトを渡すと、JavaScriptオブジェクトリテラルとして出力されます。 任意の数の引数を受け入れ、それらをすべてスペースで区切ってきれいに出力します。 開発者がこれを使用してコードをデバッグしたくなる理由はいくつかあります。 ただし、実際のコードでは「console.log」を使用しないことを強くお勧めします。 コード全体に「console.log」を書き込んでデバッグし、不要になったときにコメントアウトすることは避けてください。 代わりに、デバッグなど、このためだけに構築されたすばらしいライブラリの1つを使用してください。
このようなパッケージは、アプリケーションの起動時に特定のデバッグ行を有効または無効にする便利な方法を提供します。 たとえば、デバッグでは、DEBUG環境変数を設定しないことで、デバッグ行が端末に出力されないようにすることができます。 使い方は簡単です。
// app.js var debug = require('debug')('app') debug('Hello, %s!', 'world')
デバッグ行を有効にするには、環境変数DEBUGを「app」または「*」に設定して次のコードを実行するだけです。
DEBUG=app node app.js
間違い#10:スーパーバイザープログラムを使用しない
Node.jsコードが本番環境で実行されているか、ローカル開発環境で実行されているかに関係なく、プログラムを調整できるスーパーバイザープログラムモニターは非常に便利です。 最新のアプリケーションを設計および実装する開発者がよく推奨する1つの方法では、コードがすぐに失敗することを推奨しています。 予期しないエラーが発生した場合は、それを処理しようとしないでください。プログラムをクラッシュさせ、スーパーバイザーに数秒で再起動させてください。 スーパーバイザープログラムの利点は、クラッシュしたプログラムを再起動することだけではありません。 これらのツールを使用すると、クラッシュ時にプログラムを再起動したり、一部のファイルが変更されたときにプログラムを再起動したりできます。 これにより、Node.jsプログラムの開発がはるかに快適になります。
Node.jsで利用できるスーパーバイザープログラムは多数あります。 例えば:
pm2
永遠に
nodemon
スーパーバイザー
これらのツールにはすべて、長所と短所があります。 それらの中には、同じマシン上で複数のアプリケーションを処理するのに適しているものもあれば、ログ管理に優れているものもあります。 ただし、そのようなプログラムを開始したい場合は、これらすべてが公正な選択です。
結論
お分かりのように、これらのNode.jsの問題のいくつかは、プログラムに壊滅的な影響を与える可能性があります。 Node.jsで最も単純なものを実装しようとしているときに、フラストレーションの原因となるものもあります。 Node.jsを使用すると、初心者は非常に簡単に始めることができますが、それでも同じように混乱しやすい領域があります。 他のプログラミング言語の開発者はこれらの問題のいくつかに関係することができるかもしれませんが、これらの間違いは新しいNode.js開発者の間で非常に一般的です。 幸いなことに、それらは簡単に回避できます。 この短いガイドが、初心者がNode.jsでより良いコードを記述し、私たち全員のために安定した効率的なソフトウェアを開発するのに役立つことを願っています。