Как структурировать структуру проекта «большого проекта Node.js»?

Node.js задняя часть Twitter JavaScript RabbitMQ

Структура проекта — важная тема, потому что то, как вы управляете своим приложением, может определить весь опыт разработки на протяжении всего жизненного цикла проекта.

В этом руководстве по структуре проекта Node.js я отвечу на некоторые из наиболее часто задаваемых вопросов RisingStack о создании расширенных приложений Node и помогу вам создать сложный проект.

Это наши цели:

  • Пишите приложения, которые легко расширять и поддерживать
  • Полное разделение конфигурации и бизнес-логики
  • Одно приложение содержит несколько сервисов

Структура проекта Node.js

Наш пример приложения — «Прослушивание твитов в Твиттере и отслеживание определенных ключевых слов». В случае совпадения ключевого слова твит будет отправлен в очередь 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.

мы обсудимWebиWorkerОтличие, начнем с конфигурации.

Как обращаться с различными средами и конфигурациями?

Загружайте конфигурацию, специфичную для вашего развертывания, из переменных среды и никогда не добавляйте их в базу кода в качестве констант. Эти конфигурации могут различаться в зависимости от среды развертывания и среды выполнения, такой как CI, промежуточная или производственная. По сути, вы можете запускать один и тот же код где угодно.

Хороший способ убедиться, что конфигурация должным образом отделена от приложения, заключается в том, что кодовая база может быть раскрыта. Это означает, что случайная утечка ключей исключена.

Если кодовая база может быть раскрыта, ваша конфигурация должным образом отделена от приложения.

переменные окружения могут быть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-запросы могут обрабатываться веб-процессом, а длительные или запланированные фоновые задачи — рабочими. Они не имеют состояния, поскольку любые данные, которые необходимо сохранить, хранятся в базе данных с отслеживанием состояния. По этой причине добавить больше параллельных процессов очень просто. Эти процессы можно индивидуально масштабировать в зависимости от нагрузки или других показателей.

В предыдущем разделе мы увидели, как разбить конфигурацию на компоненты. Это может быть очень удобно при работе с несколькими службами. Каждая служба может иметь свою собственную конфигурацию, обрабатывая только необходимую конфигурацию компонентов.

существует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 (не повторяйтесь), поскольку части кода (например, модели) могут использоваться совместно различными процессами.

Как организовать ваши тестовые файлы?

Поместите тестовые файлы рядом с тестовыми модулями, используя некоторые соглашения об именах, например<module_name>.spec.jsи<module_name>.e2e.spec.js. Ваши тесты должны синхронизироваться с тестовым модулем. Когда тестовые файлы полностью отделены от бизнес-логики, сложно найти и поддерживать тесты и соответствующие функции.

В отдельной тестовой папке могут храниться все дополнительные тестовые настройки и утилиты, не используемые самим приложением.

Куда поместить файлы сборки и скрипта?

Мы склонны создавать/scriptsпапку, куда мы кладем для синхронизации базы данных, скрипты сборки фронтенда, скрипты bash и node. Эта папка отделяет их от кода приложения и предотвращает размещение слишком большого количества файлов сценариев в корневом каталоге. Перечислите их в сценариях npm для простоты использования.

исходный адрес

Это серия статей, которая будет постоянно обновляться в будущем.

Приглашаем всех обратить внимание на наш официальный публичный аккаунт