高級 Node.js 項目結構教程 | @RisingStack

mvpt7011 7年前發布 | 15K 次閱讀 Node.js Node.js 開發

項目結構是一個重要話題,因為你創建應用程序的方式可以決定整個項目生命周期的開發體驗。

在這個Node.js項目結構教程中,我將回答我們在[RisingStack]( https://trace.risingstack.com/ 收到的關于結構化高級Node應用程序的一些最常見的問題,并幫助您構建一個復雜項目。

以下是我們的目標:

  • 編寫易于擴展和維護的應用程序。

  • 項目配置與業務邏輯完全分離。

  • 項目可以包含多個進程類型。

Node.js at Scale是一組文章集合,關注具有更多需求的Node.js安裝和高級Node開發人員的公司。 章節:

Node.js項目結構

我們的示例應用程序正在偵聽推ter推文并跟蹤某些關鍵字。在關鍵字匹配的情況下,該推文將被發送到RabbitMQ隊列,該隊列將被處理并保存到Redis。 我們還將有一個REST API輸出我們保存的tweets。

你可以訪問[GitHub]上的代碼( https://github.com/RisingStack/multi-process-nodejs-example ). 此項目的文件結構如下所示:

.
|-- config
|   |-- components
|   |   |-- common.js
|   |   |-- logger.js
|   |   |-- rabbitmq.js
|   |   |-- redis.js
|   |   |-- server.js
|   |   `-- 推ter.js
|   |-- index.js
|   |-- social-preprocessor-worker.js
|   |-- 推ter-stream-worker.js
|   `-- web.js
|-- models
|   |-- redis
|   |   |-- index.js
|   |   `-- redis.js
|   |-- tortoise
|   |   |-- index.js
|   |   `-- tortoise.js
|   `-- 推ter
|       |-- index.js
|       `-- 推ter.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
|   `-- 推ter-stream
|       |-- index.js
|       `-- worker.js
|-- index.js
`-- package.json

在這個例子中,我們有3個進程:

  • 推ter-stream-worker : 此進程在推ter上偵聽關鍵字,并將推文發送到RabbitMQ隊列。

  • social-preprocessor-worker : 此進程正在偵聽RabbitMQ隊列并將這些推文保存到Redis并刪除舊的。

  • web : 此進程正在使用單個端點提供REST API: GET /api/v1/tweets?limit&offset .

我們將得到一個 web 和一個 worker 進程的區別,但是讓我們從配置開始。

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

從環境變量加載特定部署的配置,并且不要將它們作為常量添加到代碼庫中。這些配置可以在部署和運行時環境(如CI,分段或生產)之間變化。基本上,你用相同的代碼在任何地方運行。

對于配置是否與應用程序內部正確分離的一個好的測試是代碼庫可以隨時公開。 這意味著你可以防止意外泄露的秘密或損害版本控制的憑據。

如果你的代碼庫可以隨時公開,就說明你的配置與應用底層正確分離。

環境變量可以通過 process.env 對象來訪問。不要忘了,所有的值都是String類型的,所以你可能需要使用類型轉換。

// 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

驗證配置

驗證環境變量也是一個非常有用的技術。它可以幫助你在啟動時捕獲配置錯誤,然后應用程序才會執行其他操作。你可以閱讀更多關于Adrian Colyer在[此博客] ( https://blog.acolyer.org/2016/11/29/early-detection-of-configuration-errors-to-reduce-failure-damage/ ) 中配置的早期錯誤檢測的好處。

這是我們改進的配置文件看起來像模式驗證使用了 joi ( https://github.com/hapijs/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 用于生產。因為當你的應用程序隨著時間的推移擴展到更多的部署時它不能很好地擴展。

不要將你的配置全部分組到特定環境的文件,它不利于擴展#nodejs

如何組織一個多進程的應用程序?

該進程是現代應用的主要構建塊。一個應用程序可以有多個無狀態進程,就像我們的例子。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 === '推ter-stream-worker') {
  require('./worker/推ter-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', '推ter-stream-worker', 'social-preprocessor-worker'!`)
}

有趣的是,我們仍然有一個應用程序,但我們已經設法將它分成多個獨立的進程。 每個進程都可以單獨啟動和縮放而不影響其他部分。 你可以實現這一點而不犧牲你的DRY代碼庫,因為代碼的部分,如模型,可以在不同的進程之間共享。

如何組織測試文件?

使用某種命名約定將測試文件緊挨著測試模塊,例如 .spec.js 和 .e2e.spec.js 。 你的測試應與測試模塊一起保持同步,否則當測試文件與業務邏輯完全分離時,很難找到并維護測試和相應的功能。

使用某種命名約定將測試文件放在測試模塊旁邊,如module_name.spec.js

一個單獨的 / test 文件夾可以保存應用程序本身未使用的所有附加測試設置和實用程序。

在哪里放置你的構建和腳本文件?

我們傾向于創建一個 / scripts 文件夾,其中我們將bash和node腳本用于數據庫同步,前端構建等。 此文件夾將它們與應用程序代碼分離,并阻止您將太多的腳本文件放入根目錄。 在您的[npm scripts]( https://docs.npmjs.com/misc/scripts)中列出它們,以方便使用。

總結

希望你喜歡這篇關于項目結構的文章。 強烈建議查看我們上一篇關于這個主題的文章,我們在這里闡述了 Node.js項目結構的5個基本原理 .

如果您有任何疑問,歡迎在評論中告知。 在Scale系列的Node.js的下一章中,我們將深入探討[JavaScript簡潔編碼]( https://blog.risingstack.com/javascript-clean-coding-best-practices-node-js -at-scale /)。 下周見!

 

來自:http://www.zcfy.cc/article/advanced-node-js-project-structure-tutorial-risingstack-2501.html

 

 本文由用戶 mvpt7011 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!