链接:www.platformatichq.com/node-princi...
作者:James Snell 等
原标题:9 Principles for Doing Node.js Right in Enterprise Environments
Node.js 为超过 630 万个网站和无数的 API 提供支持,是包括沃尔玛和网飞在内的现代应用程序的有效基石。每年有超过 20 亿次的下载量,它是当今最常用的 Web 开发工具之一(OpenJS 基金会)。对于构建高性能应用程序来说,它是一个绝佳的选择,然而,从经验中我们知道,管理、操作和扩展一个应用程序需要大量的专业知识。
多年来,Platformatic 的联合创始人卢卡·马拉斯基(Luca Maraschi)和马泰奥·科利纳(Matteo Collina)与财富 500 强公司的数十个工程团队合作,设计了他们最重要的 Node.js 应用程序。作为 Node.js 技术指导委员会的一员,马泰奥非常荣幸能够近距离接触 Node 社区及其不断变化的需求。如果你以前构建过 Node.js 应用程序,那么你可能运行过他的代码。他维护的模块每月被下载 26 亿次,这使得他独自承担了大约所有 npm 流量的百分之一。
为了分享我们的见解,我们创建了"九大 Node 支柱"------在企业环境中创建强大、可扩展且可维护的 Node 应用程序的九条指导原则。本指南可用作检查清单,以确定当前实践中的差距,并确定需要改进的优先领域。
为了创建尽可能全面的指南,我们希望利用 Node.js 社区的集体智慧。话虽如此,感谢我们出色的贡献者,他们抽出时间与我们一起完成这一作品。他们的见解和经验在完善"九大 Node 支柱"方面非常宝贵。
1. 不要阻塞事件循环
Node.js 的事件驱动架构是其性能和可扩展性的基石。这个架构的核心是事件循环,一种处理异步操作并确保应用程序保持响应的机制。
事件循环是使 Node.js 能够执行非阻塞 I/O 操作的机制------尽管默认情况下只使用单个 JavaScript 线程------通过在可能的情况下将操作卸载到系统内核。
事件循环由以下阶段组成,每个阶段负责处理不同类型的事件:
事件循环在一个连续的循环中遍历这些阶段。如果在特定阶段没有事件要处理,循环将移动到下一个阶段。这个过程会持续进行,直到没有更多的事件要处理。
阻塞事件循环
事件循环以循环的方式运行,不断检查新事件并执行相应的回调。当发生阻塞操作时,事件循环在操作完成之前无法处理其他事件,从而导致待处理任务积压。低效的代码可能会无意中阻塞事件循环,从而导致性能下降和潜在的应用程序不稳定。
阻塞事件循环的后果:
- 性能降低:由于事件循环停滞,应用程序处理并发请求的能力显著下降,从而影响响应时间。
- 增加的延迟:当事件循环被阻塞时,随着请求排队,用户会遇到延迟。
- 无响应:在极端情况下,应用程序可能会出现冻结或变得无响应。
最佳实践:
为防止事件循环阻塞并保持最佳性能:
- 分解复杂任务:将复杂任务分解为较小的异步步骤,以防止长时间运行的操作阻塞事件循环。这可以涉及有效地使用 Promise、async/await 或回调函数。
- 卸载 CPU 密集型任务:对于计算量大的操作,考虑使用工作线程将它们与主事件循环隔离开来。即使你的进程只处理一种类型的请求并在请求到达时按顺序处理它们,这一点也很重要。例如,在 Kubernetes 下运行时,如果事件循环因长时间运行的请求而被阻塞,活性检查可能会失败。
像piscina这样的库可以简化将 CPU 密集型任务卸载到工作线程的过程。
-Matteo Collina
- 实现缓存和去重:通常在每次 I/O 操作(如数据库或 API 调用)之后都有一个受 CPU 限制的操作。通过在处理后缓存结果来减少它或减少 API 调用;尝试对它们进行去重。像 async-cache-dedupe 这样的库可以对此提供帮助。
- 监控事件循环利用率:使用诸如"perf_hooks"之类的工具或"under-pressure"插件来跟踪事件循环性能并识别潜在的瓶颈。
- 理解某些操作发生的位置:了解 Node.js 中某些操作发生的位置对于性能至关重要。例如,JSON 解析在主线程上进行,而异步文件读取则在单独的线程上进行。这种区分有助于你优化代码以避免阻塞事件循环。
2. 监控Node特定指标并对其采取行动
标准的 Node 应用指标通常缺乏有效故障排除所需的上下文。因此,监控 Node.js 性能通常需要结合来自多个工具和自定义仪表板的见解,同时兼顾 CPU 使用率、内存消耗和延迟等指标。
这种碎片化的视图使得难以确定速度减慢和应用程序无响应的根本原因。
想象一个拥有众多微服务的平台;片面的视图可能会显示 CPU 使用率很高,但在没有上下文的情况下确定罪魁祸首仍然是一场猜谜游戏。这阻碍了及时的故障排除,导致持续的性能瓶颈,使公司损失数百万的收入。
那么,你应该监测什么呢?
-
内存使用情况:
- 使用的堆与堆总量:跟踪正在使用的堆内存比例,以识别潜在的内存泄漏或低效的数据结构。
- RSS(Resident Set Size):衡量进程所占用的物理内存总量。
-
CPU 使用率:监控 CPU 利用率,但将其与 ELU 等其他指标关联,以避免过早做出扩展决策。
-
事件循环利用率 (ELU):此指标衡量 Node.js 事件循环主动处理事件的时间占总耗时的比例。它量化了事件循环的繁忙程度。
根据指标采取行动 有两个关键点可以确保你根据指标采取符合应用程序最佳利益的行动:
- 不要在内存使用量有限的情况下扩展或终止实例。Node.js 将使用提供给进程的所有内存。因此,当进程的内存使用量达到 60-80% 时,终止或扩展进程是一种资源浪费。
- 在根据 CPU 指标进行扩展时,请务必考虑 ELU。Node.js 消耗了 100% 的可用 CPU 并不意味着该进程没有响应。
在生产中制定应急计划
即使采用最佳实践,也会出现意外问题。
精心设计的应急计划对于最大限度地减少停机时间并确保在发生事故时顺利恢复至关重要。作为应急计划的基础,你应该拥有特定于节点的监控系统,以便尽早发现问题并设置警报以通知相关团队。
在此之后,你的计划应包括:
- 自动回滚和金丝雀发布:配置你的部署流水线,以便在出现问题时自动回滚到以前的稳定版本。在全面推出之前,逐步向部分用户推出新版本,以识别和解决潜在问题。
- 容器化和编排:将你的应用程序及其依赖项打包到可移植容器中。Kubernetes 等工具可以简化容器的管理和扩展,从而更容易从故障中恢复。
- 事件响应程序:建立响应事件的明确指南,包括角色和职责、沟通协议和升级路径。
3. 在生产中使用 Node LTS(长期支持)版本
Node.js 长期支持版(Long-Term Support)的发布系列会在较长时间内得到专门维护,为生产环境提供稳定性和可预测性。长期支持版系列会在三年内获得关键错误修复和安全补丁,而非长期支持版系列只有七个月。
使用长期支持(LTS)发布线的好处:
- 长期支持版本降低了重大变更的风险:长期支持版本优先考虑与现有软件包和模块的兼容性,最大限度地减少意外中断的可能性。
- 增强安全性:通过使用长期支持版本,你可以受益于及时的安全更新,保护你的应用程序免受潜在威胁。
- 提高稳定性:长期支持(LTS)版本经过严格测试和维护,确保更可预测和可靠的运行时环境。
LTS 版本的工作原理
Node.js 为 LTS 版本制定了严格的发布时间表,每个版本都会在规定的时间内获得主动支持,然后再获得维护支持一段时间。这种结构化方法可确保你有足够的时间将应用程序更新到较新的 LTS 版本,而不会影响稳定性。
使用 LTS 版本的最佳实践
- 监控 LTS 发布时间表:随时了解即将发布的 LTS 版本并相应地规划你的迁移策略。
- 彻底测试:在迁移到新的 LTS 版本之前,进行彻底的测试以确保与你的应用程序和依赖项的兼容性。
- 利用依赖管理工具:使用 npm audit 或 yarn audit 等工具来识别和解决依赖项中的漏洞,并确保它们与 LTS 版本兼容。
使用废弃版本的危险
如图所示,Node.js v14 和 v16 分别已停用 2 年和 1 年,但它们仍然很受欢迎。这凸显了我们看到的问题------公司没有更新他们的 Node.js 运行时。
4. 尽可能实现测试、代码审查和一致性的自动化
编写自动化测试 自动化测试不仅对于构建可靠且可维护的 Node.js 应用程序至关重要,而且对于加速开发也至关重要。通过建立全面的测试策略,组织可以自信地进行更改,降低缺陷风险,并最终加快步伐。
"测试最佳实践。"
- 定义测试用例:一旦有了测试用例,开发人员就可以开始编写测试。这些测试可能需要针对单个测试或大规模环境的特定配置或固定装置,包括数据和网络基础设施。
- 优先进行现场测试:只要可行,就与实时系统进行交互,而不是模拟组件,以发现潜在的集成问题。
- 专注于行为:测试应主要验证代码的外部行为,将组件视为黑盒。
- 消除测试的不稳定性:持续调查并解决测试失败问题以保持测试的可靠性。保持持续集成处于绿色状态。
- 避免全局状态:减少全局状态可降低测试的不稳定性,从而通过隔离提高测试的稳定性。
- 专注于深度防御性测试:虽然代码覆盖率是一个有价值的指标,但专注于深度防御性测试对于确保 Node.js 应用程序的可靠性至关重要。这涉及到测试边界情况、负面测试和安全测试。
- "测试尽可能接近生产环境:为了准确评估应用程序在实际条件下的行为,尽可能接近生产环境进行测试至关重要。为此,设置暂存环境,并进行性能测试和集成测试。"
全面的测试覆盖范围
全面的测试策略涵盖多种测试类型:
- 单元测试用于单独验证各个代码单元的正确行为。
- 集成测试用于验证不同组件如何交互。
- 端到端测试模拟真实的用户场景,以确保应用程序按预期运行。
通常,我们将数据库视为单元的一部分。因此,我们不建议模拟数据库驱动程序,而是直接访问数据库,至少对于"快乐路径"而言是这样。模拟数据库驱动程序对于测试错误情况非常有用。
选择正确的测试框架
正确选择测试框架至关重要。虽然 Jest 越来越受欢迎,但它在全局状态管理和错误处理方面的局限性使其不太适合 Node.js 测试。具体来说,Jest会覆盖 Node.js 的全局变量,从而在处理网络和数据库时导致各种问题。如果你有一个大型 Jest 测试套件,我们建议切换到jest-light-runner,这样可以避免所有全局变量的 monkey-patching。
考虑Vitest、Node-tap、jest-light-runner、import('node:test' )、tape或Mocha等替代方案,它们可以更好地支持 Node.js 特定的测试要求。
我们建议使用 Playwright 进行涉及浏览器的 E2E 测试。
TypeScript 和测试
TypeScript 虽然提供了有价值的静态类型检查,但并不能消除进行全面测试的需要。测试对于确保运行时正确性和捕获 TypeScript 类型系统可能遗漏的潜在问题至关重要。
如果你使用单独的类型编写 JavaScript,请不要忘记对它们进行测试 - 我们建议使用 tsd 来测试类型。
Mocking
模拟涉及用模拟版本替换真实依赖项以隔离组件并简化测试。当你希望在相同条件下始终使测试可重现时,它也很有用。一个很好的例子是从你无法控制的远程服务器下载外部资源的组件。
常见的mock库包括:
一些测试框架,如 Jest 或 node:test,提供集成的模拟功能。
代码质量和安全
确保 Node.js 应用程序的质量和安全性是交付可靠且值得信赖的软件的关键。通过采用强大的测试实践、利用静态分析工具并定期进行漏洞扫描,你可以显著增强代码库的整体健康状况。
关键实践:
- 定期漏洞扫描:使用 Snyk、SonarQube或npm audit等工具主动识别并解决潜在的安全威胁。
- 静态分析和 linting:使用 ESLint等工具在开发早期检测语法错误、代码样式违规和潜在的安全漏洞。
通过参与这些实践,你将受益于:
- 提高代码质量:增强可读性、可维护性和对最佳实践的遵守。
- 降低安全风险:主动识别和减轻漏洞。
- 更快的开发:及早发现缺陷可减少生产过程中的缺陷数量和所需的返工量,有效提高团队的速度。
5. 避免依赖蔓延
依赖关系蔓延会严重影响 Node.js 应用程序的复杂性、可维护性和性能。了解与过度依赖相关的风险并实施有效的管理策略对于构建强大而高效的软件至关重要。
依赖关系树 你引入项目的每个新包都可能在后续引入其他依赖关系。这可能会导致复杂的依赖关系树,从而难以管理和了解与每个包相关的潜在风险。
避免依赖关系蔓延的关键注意事项:
- 意向性:有意引入依赖关系,仔细评估其好处和潜在风险。
- 质量和可靠性:优先考虑经过充分测试、维护良好且拥有良好维护记录的软件包。
- 大小和范围:选择更小、更原子的包,专注于特定功能,减少引入不必要依赖的可能性。
你可以使用以下策略有效地管理依赖关系并避免不必要的膨胀。
默认使用原生 Node API,并了解它们与 Web 标准 API 的区别
要构建高性能且可维护的 Node.js 应用程序,使用底层运行时至关重要。通过优先使用原生 Node.js API,开发人员可以降低不必要的依赖项带来的风险并符合既定标准。
利用原生功能:
- 核心模块:尽可能 利用 Node.js 提供的内置模块(例如fs、 http 、path)。这些模块经过高度优化、久经考验,并为常见操作提供了熟悉的界面。
- API 采用: 在新的 Node.js API 稳定后,采用它们。例如,使用fetch() API 可以替代传统的http.request API ,这是一种更现代的替代方案。通过随时了解最新进展,你可以从性能改进和增强功能中受益。
原生 API 具有多种优势。它们针对 Node.js 进行了精心优化,与第三方库相比,性能更快。此外,核心模块经过 Node.js 核心团队的广泛测试和维护,确保稳定性并降低意外问题的风险。最后,由于原生 API 的熟悉性和一致性,严重依赖原生 API 的代码库往往更容易理解和维护。
了解原生 Node.js API 与 Web 标准 API
虽然许多 Node.js API 都符合 Web 标准,但仍然存在明显差异,尤其是在较新的 Node.js 版本中。以下是一些主要区别:
- Streams:与 Web 标准流相比,Node.js 流提供了对数据流更精细的控制。Node.js 流可以暂停、恢复并直接传输到其他流,为各种用例提供灵活性。
- HTTP: Node.js 的 http 模块提供对 HTTP 请求和响应的低级控制,允许进行细粒度的自定义。像 fetch 这样的 Web 标准 API 为更简单的 HTTP 交互提供了更高级别的抽象。然而,Node.js 的最佳 HTTP 客户端是undici,它是为了解决 http 模块的所有遗留问题而构建的。
- 文件系统: Node.js 的 fs 模块提供了一组丰富的 API 用于处理文件和目录,提供比 Web 标准文件 API 更精细的控制。
通过了解这些差异,你可以做出明智的决定,何时使用原生 Node.js API 以及何时利用 Web 标准 API 来满足你的特定需求。
尽可能重用模块
Node_modules是宇宙中最重的物体,这是有原因的。
Node.js 和 NPM 共同解决了所有以前的包管理器的关键问题:它们允许在同一进程中加载同一库的多个版本。这引发了注册表中模块的激增,使开发人员可以重用代码而不必担心 API 冲突。这种模块重用模式大大减少了工作量,并帮助组织更快地进入市场。它还有助于采用行业标准模式,而不是重新发明维护成本更高的轮子。NPM
NPM 概览
NPM是 Node 包管理器,它支持高效且可扩展的 Node.js 开发。NPM
的核心设计旨在促进大规模代码重用。通过为可重用的代码包提供集中存储库,NPM 使开发人员能够共享和利用常用功能,从而加快开发周期并减少冗余。
利用 NPM 进行代码重用的主要好处包括:
- 加速开发:通过重用现有模块,开发人员可以专注于构建新功能,而不是重新发明轮子。
- 提高代码质量:共享模块经过更严格的测试和改进,从而提高整个组织的代码质量。
- 增强的可维护性:集中式代码管理简化了更新和错误修复。
- 增强协作: NPM 促进团队之间的知识共享和协作。特别是,通过使用私有注册中心并与标准机构、开源社区和多元化的开发人员群体合作,组织可以从共享知识、代码重用和开源生态系统的整体实力中受益。
全面采用 Node.js 的组织也应该采用 NPM 作为促进内部代码共享的工具。通过创建和发布私有 NPM 包,团队可以在公司内部有效地共享可重用模块,从而避免重复工作并确保项目一致性。这种方法培养了协作和知识共享的文化,提高了开发人员的生产力并提高了代码质量。
为了充分利用 NPM,请确保:
- 标准化依赖关系:在你的组织内建立一组通用的依赖关系,以减少冗余并简化维护。
- 在内部共享代码:考虑创建和发布私有 NPM 包,以便在组织内共享可重复使用的模块。这可以促进协作、避免重复工作并确保项目间的一致性。
Monorepos:扩展代码重用 对于具有多个互连组件的大型项目,monorepos 可以改变游戏规则,提供高效的依赖管理、原子提交、简化的代码共享和改进的开发人员体验。
然而,成功采用需要仔细的考虑和规划。
主要考虑因素:
- 治理:实施单一仓库需要明确的治理指南来管理依赖关系、确保代码质量并维护整个项目的一致性。
- 依赖管理:有效的依赖管理策略对于防止冲突和确保模块之间的兼容性至关重要。这可能涉及版本控制策略、依赖锁定和自动化测试。
- 工具:选择合适的工具来简化 monorepo 管理。这些工具可以自动执行跨多个软件包的版本控制、发布和测试等任务。
虽然monorepos的优势是巨大的,但必须权衡它们与潜在的缺点:
-
复杂性增加:管理包含多个包的单个存储库比管理单独的存储库更为复杂。
-
性能开销:构建和测试整个项目可能非常耗时,尤其是对于大型代码库而言。
-
冲突的可能性: 一个包中的更改可能会无意中影响其他包,从而增加调试工作量。
那么,你应该使用哪个注册客户端?
有多种选择:
- pnpm
- npm
- yarn
我个人的建议是,对于简单的模块使用 npm,对于 monorepo 场景使用 pnpm。
-马特奥·科利纳
6. 降低依赖项的风险
消除依赖关系的风险对于安全性、性能和可维护性至关重要。通过定期扫描漏洞、及时更新和选择维护良好的依赖关系,你可以降低与依赖关系管理相关的风险,并确保你的应用程序保持安全、高效和适应性。
扫描依赖项以查找常见漏洞和暴露 (CVE)
优化 Node.js 应用程序的安全性和性能至关重要。 此安全态势的基石是定期进行依赖项扫描,以便在开发生命周期的早期识别和解决漏洞。忽视这一做法可能会导致灾难性的后果。
什么是 CVE?
CVE 是一份已分配 CVE ID号的公开披露的计算机安全漏洞列表。CVE系统于 1999 年推出,由MITRE 公司运营。漏洞必须满足以下条件才能被分配 CVE ID:
- 可独立固定
- 受影响的供应商已确认或已记录此情况
- 它影响一个代码库
扫描最佳实践:
- 作为 CI/CD 管道的一部分执行自动扫描,以自动检查新旧依赖项中的漏洞。
- 实施漏洞管理流程,根据严重程度和潜在影响确定补救措施的优先顺序。
使用npm audit、Socket.dev或Snyk等工具来简化漏洞管理流程。
-马特奥·科利纳
通过主动解决漏洞,组织可以显著降低安全漏洞的风险并保护其应用程序免受恶意攻击。
请记住,选择正确的依赖项对于构建安全且可维护的应用程序至关重要。在将依赖项集成到你的项目之前,请仔细评估依赖项。
为了减轻供应链带来的风险,你需要优先考虑能够有效更新依赖项的依赖项管理工具,并遵循依赖关系图进行正确的排序和加权。这有助于最大限度地降低周期性依赖项的风险以及可能降低应用程序性能或导致崩溃的其他问题。
保持依赖项最新
除了安全考虑之外,优化依赖项的使用也至关重要,以避免性能瓶颈并改善整体应用程序运行状况。
关键做法:
-
依赖最小化:减少依赖项的数量以最大限度地减少潜在冲突并缩短构建时间。
-
依赖项版本控制:仔细管理依赖项版本以确保兼容性并避免重大更改。
-
依赖树优化:分析依赖树以识别潜在的性能影响并优化包安装。
-
依赖项更新:定期审查和更新依赖项,以便从错误修复、性能改进和新功能中受益。
忽视依赖管理的危险 未能维持最新的依赖关系将导致重大挑战。
Node.js 的长期支持 (LTS) 发布模型强调稳定性,但必须认识到这些版本不会自动更新。依赖过时的依赖项会使你的应用程序面临安全漏洞、性能下降和兼容性问题。
此外,当库达到使用寿命时,你可能会遇到严重错误或错过重要功能。这可能会使你的整个自动化管道变得毫无用处,阻碍开发工作,增加维护成本,并可能危及整个应用程序。
主动依赖项管理对于减轻这些风险并确保你的 Node.js 项目的长期健康至关重要。
通过支持关键项目和依赖项来管理依赖风险
开源生态系统是 Node.js 开发的基石。无法支持关键项目和依赖项可能会带来重大的业务风险,包括漏洞暴露、性能下降、功能限制、供应商锁定和社区分裂。为了培育一个可持续发展和蓬勃发展的社区,组织必须积极贡献和支持关键项目和依赖项。
回馈社区
- 开源贡献:通过提交错误报告、代码改进或新功能来参与开源项目。你的贡献有助于提高整个社区使用的工具的质量和可靠性。
- 资金支持:考虑赞助对你的开发工作至关重要的开源项目。这种支持可以帮助维护者分配更多时间和资源来开发和改进项目。
- 依赖管理:积极管理项目的依赖。保持它们与最新版本同步,及时报告问题,并在可能的情况下帮助修复错误或增强功能。
**支持 **OpenJS 基金会 是一个致力于促进 JavaScript 和相关技术发展的非营利组织。
通过赞助 OpenJS 基金会,你可以直接支持关键 Node.js 项目和计划的开发和维护。
你的支持还有助于应用程序所依赖的依赖项的长期健康和质量。此外,积极与项目维护者互动可以更快地解决问题并缩短响应时间。最后,为开源项目做出贡献可以促进一个强大的协作社区,让所有参与者受益。
7. 避免使用全局变量、配置或单例
在企业开发环境中,构建结构良好且可维护的应用程序需要有意识地尽量减少全局变量、配置和单例模式的使用。
使用依赖注入模式来构建代码
在企业环境中,尽量减少使用全局变量至关重要。
全局变量会引入紧密耦合,使得代码更难进行测试、推理和重构。通过采用依赖注入并促进模块化设计,开发人员可以创建更加独立且可复用的代码组件。
依赖注入涉及将依赖项明确地作为函数参数或构造函数参数传递,而不是依赖隐式的全局访问。这种方法增强了代码的可测试性,促进了更好的代码组织,并降低了意外副作用的风险。为了有效地实施这一原则,组织应建立编码标准,阻止全局变量的使用,并为开发人员提供关于依赖注入和模块化设计的培训和资源。
企业环境需要关注高效的模块管理。一个关键原则是最大限度地实现代码复用------这种做法对长期开发和维护具有重大益处。
单例模式的缺陷
一个常见的错误是使用模块单例来保存状态变量。这会在模块之间创建紧密耦合,使得重构和更新具有挑战性。单例还可能在应用程序的不同部分依赖于同一模块的不同实例时导致版本冲突。
例如,考虑这样一种情况,多个模块导入一个包含随机数生成器的共享模块,这个随机数生成器被实现为单例模式。虽然一开始很方便,但如果一个模块使用了过时的版本,那么在共享模块中更新随机数生成逻辑可能会导致意外行为。
依赖注入来拯救
依赖注入通过促进模块之间的松散耦合提供了一种解决方案。不是模块直接创建或需要它们的依赖项,而是在模块实例化或调用时将依赖项作为参数传递。这允许:
- 提高灵活性:模块变得独立,从而更容易进行测试、交换实现和模拟行为。
- 提高可维护性:只要依赖接口保持一致,对一个模块的更改对其他模块的影响就会很小。
示例:使用依赖注入解耦
假设一个模块负责检索访问令牌,另一个模块执行需要令牌的操作。使用依赖注入,可以解耦令牌检索逻辑:
- 访问令牌检索模块公开一个函数(getToken)来检索令牌。
- 操作模块在创建过程中接收getToken函数作为依赖。
这种方法允许独立测试和更新每个模块而不会影响其他模块。
一个模块一个功能原则
主要优点:
- 隔离性和可重用性:模块变得独立,从而更容易进行测试、交换实现和模拟行为。
- 可扩展性:模块可以独立开发和部署,从而更容易扩展和维护。
- 微服务基础:基于模块的设计为必要时过渡到微服务架构奠定了基础。
注重高内聚、松耦合
要创建模块化、可测试和可维护的 Node.js 应用程序,请遵循高内聚和松散耦合的原则。
- 高内聚性:模块应具有单一、明确定义的职责,并专注于相关功能。这使模块更易于理解、测试和修改。
- 松散耦合:模块对其他模块的依赖应该最小,以降低出现意外副作用的风险并提高可维护性。
为了做到这一点:
- 明确职责:每个模块都应该有明确的目的并专注于单一任务。
- 避免模块过大:将大模块分解为更小、更集中的组件。
- 使用描述性命名:为类、变量和方法选择有意义的名称。
- 最小化全局变量:避免过度使用全局变量,因为它们可能会引入紧密耦合。
始终设置 NODE_ENV=production
虽然NODE_ENV=production通常用于在 Node.js 应用程序中启用优化和特定行为,但滥用它可能会导致开发、测试和登台环境中出现不一致和挑战。
为什么 NODE_ENV 是一种有缺陷的方法 环境是一个数字平台或系统,工程师可以在其中构建、测试、部署和管理软件产品。传统上,我们的应用程序运行在四个阶段或类型的环境中:
-
开发:用于构建、测试和调试应用程序的本地或共享环境。
-
测试:专用于在受控环境中测试应用程序的环境,通常采用自动化测试。
-
暂存:类似于生产的环境,用于在部署到生产之前进行最终测试和质量保证。
-
生产:最终用户可以访问应用程序的实时环境。NODE_ENV
的根本问题在于开发人员将优化和软件行为与其软件运行的环境相结合。结果是如下代码:
js
if (process.env.NODE_ENV === 'development') {
// ...
}
if (process.env.NODE_ENV === 'production') {
// ...
}
if (['production', 'staging'].includes(process.env.NODE_ENV)) {
// ...
}
虽然这看起来无害,但它使生产环境和暂存环境不同,从而无法进行可靠的测试。
例如,当 NODE_ENV 设置为 development 时,测试以及产品的功能可能会通过,但当将NODE_ENV 设置为 production时,测试会失败。
因此,将 NODE_ENV 设置为 production 以外的任何值都被视为反模式。
企业 Node.js 配置最佳实践
- 功能标志:根据专用环境变量启用或禁用功能,从而可以对应用程序行为进行精细控制。
- 专用环境变量:为每个部署阶段(开发、测试、登台、生产)创建单独的环境变量,以管理每个阶段特定的配置。
- 机密管理工具: 利用专用的机密管理解决方案来存储并安全访问敏感信息,例如 API 密钥和数据库凭据。这些工具可以与你的部署流程集成,以在运行时注入机密。在这里,深入了解环境变量在 Node.js 应用程序中的工作原理非常重要------这可能会有所帮助。
- 与环境无关的配置:努力使代码根据运行时加载的配置适应不同的环境,尽量减少对特定于环境的逻辑的需要。
- 注意 CI/CD 管道的演变:相应地调整环境配置。请记住,虽然暂存和生产理想情况下应该相同,但开发环境通常可以具有不同的配置,以方便本地开发和测试。
为什么要遵循这个原则呢?
- 提高可靠性:所有环境中的一致行为降低了部署期间出现错误的风险。
- 增强的安全性: 敏感数据通过专用的管理工具保护。
- 减少维护工作量:由于特定于环境的逻辑被最小化,代码变得更加灵活且更易于管理。
8.处理错误并提供有意义的日志
确保 Node.js 应用程序的稳健性和稳定性取决于实施有效的错误处理和日志记录实践。为此,你需要优雅地捕获和解决意外错误,防止应用程序崩溃,并通过详细的日志记录获得有关应用程序行为的宝贵见解。
让我们更详细地了解一下:
采用一致的错误处理模式
错误处理是 Node.js 开发的一个重要方面。它涉及捕获和管理应用程序执行期间可能发生的错误,防止崩溃并向用户提供有意义的反馈。通过实施有效的错误处理机制,你可以增强 Node.js 应用程序的可靠性和可维护性。
处理全局未捕获异常:
为确保你的应用程序不会意外崩溃,处理全局未捕获异常至关重要。这涉及使用process.on('uncaughtException')事件侦听器来捕获和记录这些异常。然后,你可以采取适当的措施,例如正常关闭应用程序或向监控系统发送警报。
"优雅关闭:"
处理未捕获异常时,请考虑实施正常关闭过程,以确保正确清理资源并避免突然终止。你可以使用close-with-grace等库来注册关闭处理程序,并在应用程序退出前执行关闭数据库连接或处理正在进行的请求等任务。
提供有意义的日志
在 Node.js 开发中,日志记录非常重要,因为它可以帮助开发人员监控应用程序事件、跟踪性能,并在应用程序中出现错误或系统故障时收到警报。
日志记录在生产环境中也非常有用,可以快速检测和修复错误源。此外,日志记录通过提供应用程序行为和性能的清晰视图,为开发人员提供了增强的可视性。
记录什么:
在 Node.js 中记录时,必须在提供足够的信息进行故障排除和避免过多的日志记录(可能影响性能)之间取得平衡。
以下是一些关键注意事项:
- 错误日志:以适当的详细程度(例如,调试、信息、警告、错误)记录错误,以有效地跟踪和解决问题。
- 请求和响应信息:记录传入请求和传出响应,包括 HTTP 方法、URL、请求参数和响应状态代码。这有助于识别和排除潜在问题。
- 性能指标:记录响应时间、请求持续时间和资源利用率等性能指标,以监控应用程序性能并识别瓶颈。
- 应用程序状态:记录重要的应用程序状态变化或事件,以跟踪应用程序的行为并了解导致错误或意外结果的事件序列。
- 自定义日志消息:创建自定义日志消息,提供特定于你的应用程序的有意义的上下文和信息。
记录多少:
适当的记录级别取决于你的特定需求和应用程序的复杂性。请考虑以下准则:
- 生产环境:在生产环境中,以较低级别(例如信息、警告、错误)记录日志,以避免过多的日志记录影响性能。重点记录错误、关键事件和性能指标。
- 开发和测试:在开发和测试环境中,你可以在更高级别(例如,调试)进行日志记录,以收集更详细的信息,用于故障排除和调试目的。
日志库
我个人推荐的日志记录工具是Pino,这是一个开销非常低的 Node.js 日志记录器,于 2014 年首次发布。自发布以来,它就因其速度、效率和灵活的配置而广受欢迎。
Pino 对 Express、Fastify、Nest、Restify、Hapi、Koa 和核心 Node 等其他 Web 框架提供一流的支持,并且是 Fastify 的默认记录器。
开发人员为什么选择 Pino?
首先,Pino 是异步运行的;这意味着如果目的地繁忙,它会累积日志,而当目的地可用时,它会立即发送日志。
其次,Pino 的速度比其他大多数替代方案快五倍左右。为了达到这样的速度,Pino 使用最少的资源进行日志记录,因此,日志消息会随着时间的推移而增加,从而对应用程序产生节流效应,例如每秒请求数减少。
此外,Pino 不是一个独立的工具,因为它支持与 Webpack 和 esbuild 等工具捆绑在一起,为开发人员提供更多的集成选项,从而提供创造的机会。
-马特奥·科利纳
9. 使用API规范并自动生成客户端
在多团队环境中,建立清晰一致的 API 规范对于有效协作和集成至关重要。通过定义 API 的结构、数据类型和操作,你可以确保不同的团队能够独立工作,同时保持兼容性并避免误解。此外,自动根据 API 规范生成客户端代码可以显著简化开发并降低出错风险。
API优先与代码优先
在设计和开发 API 时,有两种主要方法:API First 和 Code First。每种方法都有各自的优点和注意事项。
- API First:在编写实现代码之前定义 API 规范。这种方法提倡契约驱动的开发风格,确保客户端和服务器团队都清楚了解 API 的功能和期望。
- 代码优先:首先开发实现代码,然后根据现有代码生成 API 规范。虽然这种方法可以更快地启动,但它可能会引入不一致,并使保持 API 和实现之间的一致性变得更加困难。
OpenAPI 与 GraphQL
OpenAPI和GraphQL都是定义和与 API 交互的强大工具,但它们的用途不同,且具有不同的优势。
开放API:
- 一种广泛使用的 RESTful API 规范语言。
- 定义 API 的结构、数据类型和操作。
- 非常适合具有预定义 API 契约的场景。
GraphQL:
- 一种提供更多灵活性和效率的 API 查询语言。
- 允许客户端准确指定他们需要的数据,减少过度获取和获取不足。
生成客户端和类型
根据 API 规范自动生成客户端和类型可以显著简化开发并降低出错风险。通过利用OpenAPI Generator 或 Swagger Codegen(适用于OpenAPI 规范)和 graphql-codegen(适用于GraphQL)等工具,你可以自动执行以前耗时且容易出错的任务,加快开发周期,降低类型相关错误的风险,并提高代码质量和可维护性。此外,自动类型生成可以帮助确保 API 定义和客户端代码之间的一致性,从而降低出现兼容性问题的可能性。
通过采用 API 规范并自动生成客户端,你可以简化开发,提高代码质量,并在多团队环境中促进协作。
总结
通过遵循这些指导原则,你将能够设置 Node.js 应用程序以获得长期成功。从掌握事件循环到实现强大的依赖项管理,这些支柱旨在帮助你构建可扩展、安全且可维护的解决方案。
请记住,Node.js 社区依靠协作而蓬勃发展------积极贡献并支持关键项目,以确保所有人都能拥有一个繁荣的生态系统。
有了这些知识,我们希望看到你提升 Node.js 开发水平并构建经得起时间考验的出色应用程序。