如何構建「大型 Node.js 專案」的專案結構?

TeambitionNG發表於2018-05-15

專案結構是一個重要的主題,因為您引導應用程式的方式可以決定專案整個生命週期的整個開發體驗。

在這個 Node.js 專案結構教程中,我將回答 RisingStack 關於構造高階 Node 應用程式的一些最常見的問題,並幫助您構建一個複雜的專案。

這些是我們的目標:

  • 編寫易於擴充套件和維護的應用程式
  • 配置與業務邏輯完全分離
  • 單應用下包含多服務

Node.js 專案結構

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

你可以看看 GitHub 上的程式碼。該專案的檔案結構如下所示

|-- 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:該程式正在 Twitter 上偵聽關鍵字並將推文傳送到RabbitMQ 佇列。
  • social-preprocessor-worker:該程式正在偵聽 RabbitMQ 佇列,並將推文儲存到 Redis 並刪除舊的。
  • web:該流程使用單個端點提供 REST API: GET /api/v1/tweetslimit&offset

我們將討論 WebWorker 的區別,接下來讓我們從配置開始.

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

從環境變數載入特定於您的部署的配置,並且永遠不要將它們作為常量新增到程式碼庫中。這些配置可以在部署和執行時環境之間有所不同,如CI,staging 或 production。基本上,你可以在任何地方執行相同的程式碼。

對於配置是否與應用正確分離的一個很好的驗證方式是,程式碼庫是否可以公開。這意味著可以防止意外洩漏祕鑰。

如果程式碼庫可以公開,那麼您的配置與應用程式正確分離。

環境變數可以通過 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 指令碼中以便於使用。

原文地址

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

歡迎大家關注我們的官方公眾號

如何構建「大型 Node.js 專案」的專案結構?

相關文章