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

mvpt7011 8年前
   <p>项目结构是一个重要话题,因为你创建应用程序的方式可以决定整个项目生命周期的开发体验。</p>    <p>在这个Node.js项目结构教程中,我将回答我们在[RisingStack]( <a href="/misc/goto?guid=4959736795022340553" rel="nofollow,noindex">https://trace.risingstack.com/</a> 收到的关于结构化高级Node应用程序的一些最常见的问题,并帮助您构建一个复杂项目。</p>    <p>以下是我们的目标:</p>    <ul>     <li> <p>编写易于扩展和维护的应用程序。</p> </li>     <li> <p>项目配置与业务逻辑完全分离。</p> </li>     <li> <p>项目可以包含多个进程类型。</p> </li>    </ul>    <p>Node.js at Scale是一组文章集合,关注具有更多需求的Node.js安装和高级Node开发人员的公司。 章节:</p>    <ul>     <li> <p>使用 npm</p> </li>     <li> <p><a href="/misc/goto?guid=4959736795116776067" rel="nofollow,noindex">npm技巧和最佳实践</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959736795202147663" rel="nofollow,noindex">SemVer和模块发布</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959736795280892662" rel="nofollow,noindex">了解模块系统,CommonJS和require</a></p> </li>     <li> <p>深入了解Node.js底层</p> </li>     <li> <p><a href="/misc/goto?guid=4959736795363746447" rel="nofollow,noindex">Node.js事件循环</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959736795449585613" rel="nofollow,noindex">Node.js垃圾回收机制</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959736795538201240" rel="nofollow,noindex">编写本地Node.js模块</a></p> </li>     <li> <p>新建项目</p> </li>     <li> <p>高级Node.js项目结构 <em>(本文)</em></p> </li>     <li> <p><a href="/misc/goto?guid=4959736795610678589" rel="nofollow,noindex">JavaScript 简介代码最佳实践</a></p> </li>     <li> <p>异步处理</p> </li>     <li> <p>事件源</p> </li>     <li> <p>命令查询责任分离</p> </li>     <li> <p>测试</p> </li>     <li> <p>单元测试</p> </li>     <li> <p>端对端测试</p> </li>     <li> <p>生产环境中的Node.js</p> </li>     <li> <p>监控Node.js应用程序</p> </li>     <li> <p>调试Node.js应用程序</p> </li>     <li> <p>分析Node.js应用程序</p> </li>     <li> <p>微服务</p> </li>     <li> <p>请求签名</p> </li>     <li> <p>分布式跟踪</p> </li>     <li> <p>API 网关</p> </li>    </ul>    <h2>Node.js项目结构</h2>    <p>我们的示例应用程序正在侦听推ter推文并跟踪某些关键字。在关键字匹配的情况下,该推文将被发送到RabbitMQ队列,该队列将被处理并保存到Redis。 我们还将有一个REST API输出我们保存的tweets。</p>    <p>你可以访问[GitHub]上的代码( <a href="/misc/goto?guid=4959736795689455127" rel="nofollow,noindex">https://github.com/RisingStack/multi-process-nodejs-example</a> ). 此项目的文件结构如下所示:</p>    <pre>  <code class="language-javascript">.  |-- 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</code></pre>    <p>在这个例子中,我们有3个进程:</p>    <ul>     <li> <p>推ter-stream-worker : 此进程在推ter上侦听关键字,并将推文发送到RabbitMQ队列。</p> </li>     <li> <p>social-preprocessor-worker : 此进程正在侦听RabbitMQ队列并将这些推文保存到Redis并删除旧的。</p> </li>     <li> <p>web : 此进程正在使用单个端点提供REST API: GET /api/v1/tweets?limit&offset .</p> </li>    </ul>    <p>我们将得到一个 web 和一个 worker 进程的区别,但是让我们从配置开始。</p>    <p><img src="https://simg.open-open.com/show/30582dcef9a28198a239a3917b4c2433.png"></p>    <h2>如何处理不同的环境和配置?</h2>    <p>从环境变量加载特定部署的配置,并且不要将它们作为常量添加到代码库中。这些配置可以在部署和运行时环境(如CI,分段或生产)之间变化。基本上,你用相同的代码在任何地方运行。</p>    <p>对于配置是否与应用程序内部正确分离的一个好的测试是代码库可以随时公开。 这意味着你可以防止意外泄露的秘密或损害版本控制的凭据。</p>    <p><a href="/misc/goto?guid=4959736795785979371" rel="nofollow,noindex">如果你的代码库可以随时公开,就说明你的配置与应用底层正确分离。</a></p>    <p>环境变量可以通过 <a href="/misc/goto?guid=4959736795865637573" rel="nofollow,noindex"> process.env </a> 对象来访问。不要忘了,所有的值都是String类型的,所以你可能需要使用类型转换。</p>    <pre>  <code class="language-javascript">// 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</code></pre>    <h3>验证配置</h3>    <p>验证环境变量也是一个非常有用的技术。它可以帮助你在启动时捕获配置错误,然后应用程序才会执行其他操作。你可以阅读更多关于Adrian Colyer在[此博客] ( <a href="/misc/goto?guid=4959736795952793049" rel="nofollow,noindex">https://blog.acolyer.org/2016/11/29/early-detection-of-configuration-errors-to-reduce-failure-damage/</a> ) 中配置的早期错误检测的好处。</p>    <p>这是我们改进的配置文件看起来像模式验证使用了 joi ( <a href="/misc/goto?guid=4959736796041543647" rel="nofollow,noindex">https://github.com/hapijs/joi)验证器:</a></p>    <pre>  <code class="language-javascript">// 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</code></pre>    <h3>配置分离</h3>    <p>通过组件拆分配置可以是放弃单个增长的配置文件的良好解决方案。</p>    <pre>  <code class="language-javascript">// 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</code></pre>    <p>然后在 config.js 文件中,我们只需要组合组件。</p>    <pre>  <code class="language-javascript">// 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)</code></pre>    <p>你不应该将你的配置一起组成所有环境特定的文件,如 config / production.js 用于生产。因为当你的应用程序随着时间的推移扩展到更多的部署时它不能很好地扩展。</p>    <p><a href="/misc/goto?guid=4959736796122498241" rel="nofollow,noindex">不要将你的配置全部分组到特定环境的文件,它不利于扩展#nodejs</a></p>    <h2>如何组织一个多进程的应用程序?</h2>    <p>该进程是现代应用的主要构建块。一个应用程序可以有多个无状态进程,就像我们的例子。HTTP请求可以由Web进程处理,并由工作者长时间运行或调度的后台任务处理。 它们是无状态的,因为需要持久化的任何数据都存储在有状态的数据库中。 因此,添加更多并发进程非常简单,而且这些进程可以基于负载或其他度量独立地缩放。</p>    <p>在上一节中,我们看到了如何将配置分解成组件。 当有不同的进程类型时会非常方便。 每种类型都可以有自己的配置,只需要组合它所需的组件,而不需要使用无意义的环境变量。</p>    <p>在 config/index.js 文件中:</p>    <pre>  <code class="language-javascript">// 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</code></pre>    <p>在根文件 index.js 中,我们开始使用 PROCESS_TYPE 环境变量选择过程:</p>    <pre>  <code class="language-javascript">// 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'!`)  }</code></pre>    <p>有趣的是,我们仍然有一个应用程序,但我们已经设法将它分成多个独立的进程。 每个进程都可以单独启动和缩放而不影响其他部分。 你可以实现这一点而不牺牲你的DRY代码库,因为代码的部分,如模型,可以在不同的进程之间共享。</p>    <h2>如何组织测试文件?</h2>    <p>使用某种命名约定将测试文件紧挨着测试模块,例如 .spec.js 和 .e2e.spec.js 。 你的测试应与测试模块一起保持同步,否则当测试文件与业务逻辑完全分离时,很难找到并维护测试和相应的功能。</p>    <p><a href="/misc/goto?guid=4959736796204883910" rel="nofollow,noindex">使用某种命名约定将测试文件放在测试模块旁边,如module_name.spec.js</a></p>    <p>一个单独的 / test 文件夹可以保存应用程序本身未使用的所有附加测试设置和实用程序。</p>    <h2>在哪里放置你的构建和脚本文件?</h2>    <p>我们倾向于创建一个 / scripts 文件夹,其中我们将bash和node脚本用于数据库同步,前端构建等。 此文件夹将它们与应用程序代码分离,并阻止您将太多的脚本文件放入根目录。 在您的[npm scripts]( <a href="/misc/goto?guid=4959736796290401340" rel="nofollow,noindex">https://docs.npmjs.com/misc/scripts)中列出它们,以方便使用。</a></p>    <h2>总结</h2>    <p>希望你喜欢这篇关于项目结构的文章。 强烈建议查看我们上一篇关于这个主题的文章,我们在这里阐述了 <a href="/misc/goto?guid=4959736796376461908" rel="nofollow,noindex">Node.js项目结构的5个基本原理</a> .</p>    <p>如果您有任何疑问,欢迎在评论中告知。 在Scale系列的Node.js的下一章中,我们将深入探讨[JavaScript简洁编码]( <a href="/misc/goto?guid=4959736796455370290" rel="nofollow,noindex">https://blog.risingstack.com/javascript-clean-coding-best-practices-node-js</a> -at-scale /)。 下周见!</p>    <p> </p>    <p>来自:http://www.zcfy.cc/article/advanced-node-js-project-structure-tutorial-risingstack-2501.html</p>    <p> </p>