cocoPang

高级 Node.js 项目结构教程 | @RisingStack

cocoPang · 2017-02-12翻译 · 4085阅读 原文链接

项目结构是一个重要话题,因为你创建应用程序的方式可以决定整个项目生命周期的开发体验。

在这个Node.js项目结构教程中,我将回答我们在[RisingStack](https://trace.risingstack.com/) 收到的关于结构化高级Node应用程序的一些最常见的问题,并帮助您构建一个复杂项目。

以下是我们的目标:

  • 编写易于扩展和维护的应用程序。

  • 项目配置与业务逻辑完全分离。

  • 项目可以包含多个进程类型。


> Node.js at Scale 是一组文章集合,关注具有更多需求的Node.js安装和高级Node开发人员的公司。 章节:


Node.js项目结构

我们的示例应用程序正在侦听Twitter推文并跟踪某些关键字。在关键字匹配的情况下,该推文将被发送到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
|   |   `-- 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/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在此博客 中配置的早期错误检测的好处。

这是我们改进的配置文件看起来像模式验证使用了joihttps://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 === '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代码库,因为代码的部分,如模型,可以在不同的进程之间共享。

如何组织测试文件?

使用某种命名约定将测试文件紧挨着测试模块,例如.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 /)。 下周见!

译者cocoPang尚未开通打赏功能

相关文章