如何構建「大型 Node.js 項目」的項目結構?

項目結構是一個重要的主題,由於您引導應用程序的方式能夠決定項目整個生命週期的整個開發體驗。前端

在這個 Node.js 項目結構教程中,我將回答 RisingStack 關於構造高級 Node 應用程序的一些最多見的問題,並幫助您構建一個複雜的項目。node

這些是咱們的目標:git

  • 編寫易於擴展和維護的應用程序
  • 配置與業務邏輯徹底分離
  • 單應用下包含多服務

Node.js 項目結構

咱們的示例應用程序是「監聽 Twitter 推文並跟蹤某些關鍵字」。在關鍵字匹配的狀況下,推文將被髮送到 RabbitMQ 隊列,該隊列將被處理並保存到 Redis。咱們還提供一個 REST API 用於訪問持久化的推文。github

你能夠看看 GitHub 上的代碼。該項目的文件結構以下所示web

|-- 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個進程:redis

  • twitter-stream-worker:該進程正在 Twitter 上偵聽關鍵字並將推文發送到RabbitMQ 隊列。
  • social-preprocessor-worker:該進程正在偵聽 RabbitMQ 隊列,並將推文保存到 Redis 並刪除舊的。
  • web:該流程使用單個端點提供 REST API: GET /api/v1/tweetslimit&offset

咱們將討論 WebWorker 的區別,接下來讓咱們從配置開始.數據庫

如何處理不一樣的環境和配置?

從環境變量加載特定於您的部署的配置,而且永遠不要將它們做爲常量添加到代碼庫中。這些配置能夠在部署和運行時環境之間有所不一樣,如CI,staging 或 production。基本上,你能夠在任何地方運行相同的代碼。npm

對於配置是否與應用正確分離的一個很好的驗證方式是,代碼庫是否能夠公開。這意味着能夠防止意外泄漏祕鑰。json

若是代碼庫能夠公開,那麼您的配置與應用程序正確分離。api

環境變量能夠經過 process.env 對象訪問。請記住,全部值都是字符串類型,所以您可能須要使用類型轉換。

// config/config.js
'use strict'

// required environment variables
[
 '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/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
複製代碼

配置拆分

經過組件拆分配置,是避免單個配置文件不斷變大的一個很好的解決方案。

// 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 進程處理,並由工做人員處理長時間運行或預約的後臺任務。 它們是無狀態的,由於須要持久化的任何數據都存儲在有狀態的數據庫中。 出於這個緣由,添加更多併發進程很是簡單。 這些過程能夠根據負載或其餘度量單獨進行縮放。

在上一節中,咱們看到了如何將配置根據組件來拆解。處理多個服務時,這將很是方便。 每種服務均可以有本身的配置,只處理它須要的組件配置。

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'!`)
}
複製代碼

關於這一點的好處是咱們仍然只有一個應用程序,但咱們已經支持將它分紅多個獨立的進程。 它們中的每個均可以單獨啓動和擴展,而不會影響其餘部分。 您能夠在不犧牲 DRY(Dont repeat yourself) 代碼庫的狀況下實現此目標,由於部分代碼(如模型)能夠在不一樣進程之間共享.

如何組織你的測試文件?

使用某種命名約定將測試文件放在測試模塊旁邊,如 <module_name>.spec.js<module_name>.e2e.spec.js。 您的測試應該與測試模塊一塊兒生活,保持同步。 當測試文件與業務邏輯徹底分離時,很難找到並維護測試和相應的功能。

單獨的測試文件夾能夠容納應用程序自己未使用的全部附加測試設置和實用程序。

何處放置構建和腳本文件?

咱們傾向於建立一個 /scripts 文件夾,在這裏咱們放置用於數據庫同步,前端構建腳本、bash 和 node 腳本。 此文件夾將它們與應用程序代碼分開,並防止將太多腳本文件放入根目錄。 將它們列在 npm 腳本中以便於使用。

原文地址

此爲系列文章, 後續會持續更新

歡迎你們關注咱們的官方公衆號

相關文章
相關標籤/搜索