この資料は、Node.jsの複雑なアプリケーションの構造化に関するよくある質問への回答に基づいています。 これは、自分の開発の構造を改善する必要性を感じているすべての人を対象としています。
ここで取り上げる主なトピックは次のとおりです。
- 保守が容易な非常にスケーラブルなアプリケーションを開発します。
- 構成データとメインアプリケーションコードの高品質な分離。
- Node.jsアプリケーションでのさまざまなタイプのプロセスの使用。
ここでは、さまざまな概念を説明するために、サンプルアプリケーションを使用します。アプリケーションの完全なコードはGitHubにあります 。
デモプロジェクトの概要
このアプリケーションは、特定のキーワードの更新を購読することにより、Twitterからデータを受け取ります。 適切なツイートがRabbitMQキューに送信されます。 キューの内容が処理され、Redisデータベースに保存されます。 さらに、アプリケーションには、保存されたツイートへのアクセスを提供するREST APIがあります。
プロジェクトファイルの構造は次のようになります。
. |-- config | |-- components | | |-- common.js | | |-- logger.js | | |-- rabbitmq.js | | |-- redis.js | | |-- server.js | | `-- twitter.js | |-- index.js | |-- social-preprocessor-worker.js | |-- twitter-stream-worker.js | `-- web.js |-- models | |-- redis | | |-- index.js | | `-- redis.js | |-- tortoise | | |-- index.js | | `-- tortoise.js | `-- twitter | |-- index.js | `-- twitter.js |-- scripts |-- test | `-- setup.js |-- web | |-- middleware | | |-- index.js | | `-- parseQuery.js | |-- router | | |-- api | | | |-- tweets | | | | |-- get.js | | | | |-- get.spec.js | | | | `-- index.js | | | `-- index.js | | `-- index.js | |-- index.js | `-- server.js |-- worker | |-- social-preprocessor | | |-- index.js | | `-- worker.js | `-- twitter-stream | |-- index.js | `-- worker.js |-- index.js `-- package.json
プロジェクトには3つのプロセスがあります。
-
twitter-stream-worker
プロセスは、ストリーミングAPIを使用してTwitterと対話します。 特定のキーワードを含むツイートを受信し、RabbitMQキューに送信します。
-
social-preprocessor-worker
プロセスは、RabbitMQキューで実行されます。 つまり、ツイートをRedisリポジトリに書き込み、古いデータを削除します。
-
web
プロセスは、1つのエンドポイントでREST APIを提供します:GET /api/v1/tweets?limit&offset
。
web
プロセスと
worker
プロセスの違いについては後で説明します。次に、ソリューションの構成データについて説明します。
さまざまなランタイムおよびアプリケーション構成のサポート
特定のアプリケーションインスタンスの構成データは、環境変数からロードする必要があります。 定数としてコードに追加する必要はありません。
アプリケーションの展開のさまざまなバリエーションや、さまざまなランタイム環境で一致しない可能性のあるパラメーターについて説明しています。 たとえば、開発環境、ビルドサーバー、可能な限り動作に近い環境、そして最終的に運用環境での起動です。 このアプローチにより、任意の条件で機能する単一のアプリケーションコードベースを使用できます。
構成データと内部アプリケーションメカニズムが正しく分離されていることを確認する良い方法は次のとおりです。 プロジェクトコードをいつでも作業できるように公開できる場合は、ロジックと設定を分離する必要があります。 これは、バージョン管理システムに侵入する秘密データまたはアカウント設定に対する保護を自動的に意味します。
環境変数は、
process.env
オブジェクトを使用し
process.env
アクセスできます。 オブジェクトには文字列値のみが格納されるため、ここで型変換が必要になる場合があります。
// config/config.js 'use strict' // [ 'NODE_ENV', 'PORT' ].forEach((name) => { if (!process.env[name]) { throw new Error(`Environment variable ${name} is missing`) } }) const config = { env: process.env.NODE_ENV, logger: { level: process.env.LOG_LEVEL || 'info', enabled: process.env.BOOLEAN ? process.env.BOOLEAN.toLowerCase() === 'true' : false }, server: { port: Number(process.env.PORT) } // ... } module.exports = config
構成データの検証
コードに設定を含めないでください-ソリューションは正しいですが、使用する前に環境変数を確認することも非常に便利です。 これは、作業の最初に構成エラーを検出し、アプリケーションが誤った設定や欠落している設定で作業しようとする状況を回避するのに役立ちます。 構成データのエラーを早期に検出する利点については、 こちらをご覧ください 。
joi
バリデーターを使用してデータ検証を追加することにより、
config.js
ファイルを改善した方法を次に示します。
// config/config.js 'use strict' const joi = require('joi') const envVarsSchema = joi.object({ NODE_ENV: joi.string() .allow(['development', 'production', 'test', 'provision']) .required(), PORT: joi.number() .required(), LOGGER_LEVEL: joi.string() .allow(['error', 'warn', 'info', 'verbose', 'debug', 'silly']) .default('info'), LOGGER_ENABLED: joi.boolean() .truthy('TRUE') .truthy('true') .falsy('FALSE') .falsy('false') .default(true) }).unknown() .required() const { error, value: envVars } = joi.validate(process.env, envVarsSchema) if (error) { throw new Error(`Config validation error: ${error.message}`) } const config = { env: envVars.NODE_ENV, isTest: envVars.NODE_ENV === 'test', isDevelopment: envVars.NODE_ENV === 'development', logger: { level: envVars.LOGGER_LEVEL, enabled: envVars.LOGGER_ENABLED }, server: { port: envVars.PORT } // ... } module.exports = config
構成データの分離
すべての構成データを1つのファイルに保存できますが、プロジェクトの成長および開発中に、そのようなファイルはサイズが大きくなり、作業するのに不便になります。 これらの問題を回避するために、たとえばアプリケーションのコンポーネントに基づいて設定を分離することは理にかなっています。 この例では、次のようになります。
// config/components/logger.js 'use strict' const joi = require('joi') const envVarsSchema = joi.object({ LOGGER_LEVEL: joi.string() .allow(['error', 'warn', 'info', 'verbose', 'debug', 'silly']) .default('info'), LOGGER_ENABLED: joi.boolean() .truthy('TRUE') .truthy('true') .falsy('FALSE') .falsy('false') .default(true) }).unknown() .required() const { error, value: envVars } = joi.validate(process.env, envVarsSchema) if (error) { throw new Error(`Config validation error: ${error.message}`) } const config = { logger: { level: envVars.LOGGER_LEVEL, enabled: envVars.LOGGER_ENABLED } } module.exports = config
その後、メインの
config.js
ファイルで、コンポーネントのパラメーターを組み合わせるだけで済みます。
// config/config.js 'use strict' const common = require('./components/common') const logger = require('./components/logger') const redis = require('./components/redis') const server = require('./components/server') module.exports = Object.assign({}, common, logger, redis, server)
作業環境に応じて構成データをグループ化しないでください。つまり、アプリケーションの製品バージョンの設定を
config/production.js
ファイルに保持しないでください。 このアプローチにより、たとえば、長期にわたって同じ生産バージョンを異なる環境にデプロイする必要がある場合など、アプリケーションのスケーラビリティが妨げられます。
マルチプロセスアプリケーションの構成
このプロセスは、最新のアプリケーションの主要な構成要素です。 ソフトウェア製品は、自身の状態を監視しない多くのプロセスで構成される場合があります。 この例では、このようなプロセスが使用されます。 したがって、HTTP要求は
web
プロセスで処理でき、
worker
プロセスはスケジュールに従って何かを実行したり、長時間かかる特定の操作を実行したりできます。 保存される情報はデータベースに記録されます。 このアーキテクチャのおかげで、ソリューションは並列プロセスの起動によるスケーリングに適しています。 プロセスの数を増やす必要性の基準は、アプリケーションの負荷など、さまざまなメトリックになります。
上記では、構成データのコンポーネントへの分離について説明しました。 このアプローチは、プロジェクトにさまざまなタイプのプロセスがある場合に非常に役立ちます。 各タイプのプロセスは独自の設定を取得でき、以前に使用されていない環境変数の存在を期待せず、必要なコンポーネントのみを要求します。
config/index.js
:
// config/index.js 'use strict' const processType = process.env.PROCESS_TYPE let config try { config = require(`./${processType}`) } catch (ex) { if (ex.code === 'MODULE_NOT_FOUND') { throw new Error(`No config for process type: ${processType}`) } throw ex } module.exports = config
ルートの
index.js
ファイルで、環境変数
PROCESS_TYPE
して目的のプロセスを実行します。
// index.js 'use strict' const processType = process.env.PROCESS_TYPE if (processType === 'web') { require('./web') } else if (processType === 'twitter-stream-worker') { require('./worker/twitter-stream') } else if (processType === 'social-preprocessor-worker') { require('./worker/social-preprocessor') } else { throw new Error(`${processType} is an unsupported process type. Use one of: 'web', 'twitter-stream-worker', 'social-preprocessor-worker'!`) }
その結果、1つのアプリケーションがありますが、多くの独立したプロセスに分割されています。 必要に応じて、それぞれを個別に起動して、同じ種類の複数の並列プロセスを発生させることができますが、これはアプリケーションの他の部分には影響しません。 同時に、異なるプロセスがモデルなどのコードの一部を共有できるため、開発中のDRY原則の遵守に貢献します。
テスト付きファイルの構成
<module_name>.spec.js
や
<module_name>.e2e.spec.js
などの命名規則を使用して、テスト済みのファイルをテスト済みモジュールの隣に配置する必要があります。 テストは、テストするモジュールとともに進化する必要があります。 テストファイルがアプリケーションロジックファイルから分離されている場合、それらを見つけて最新の状態に保つことはより困難になります。
別のフォルダ
/test
、アプリケーション自体で使用されない追加のテストとユーティリティをすべて保存する
/test
は理にかなっています。
ビルドファイルとスクリプトファイルの配置
通常、
/scripts
フォルダーを作成します。このフォルダーには、bashスクリプト、Node.jsスクリプトを配置してデータベースを同期したり、フロントエンドを構築したりします。 このアプローチのおかげで、スクリプトはメインアプリケーションコードから分離され、プロジェクトのルートディレクトリはしばらくするとスクリプトファイルでいっぱいになりません。 このすべてを使いやすくするために、
package.json
ファイルのスクリプトセクションにスクリプトを登録できます。
結論
Node.jsの複雑なプロジェクトの構築とスケーリングに関するアイデアがあなたの役に立つことを願っています。 ところで、ここで、このトピックに関するより多くの資料があります。
Node.jsは非常に柔軟な環境であるため、アプリケーションの構造とスケーリングの分野での決定の一部が究極の真実であるとは言えませんが、他の決定は完全に受け入れられません。 おそらく、上記の推奨事項と根本的に異なる独自の開発を行っているか、同じ方向に進んでいる可能性があります。 そのような成果がある場合、それらを共有することは素晴らしいことです。