在推特上关注我们!@nodepractices
用不同的语言阅读:中文、法语、巴西、俄罗斯、波兰语、日本语、欧洲语(西班牙、瑞士、韩国和土耳其语正在进行中!
🛰 现代化到 2023 年:大量文本编辑、新的推荐库和一些新的最佳实践
#new
#updated
🔖 想看看例子吗?我们有一个入门:访问 Practica.js,我们的应用程序示例和样板(测试版),了解一些实际操作
1. 你正在阅读数十篇最好的 Node.js 文章 - 此存储库是对 Node 上排名靠前的内容的总结和策划.js最佳实践,以及协作者在此处编写的内容
2. 它是最大的汇编,并且每周都在增长 - 目前,提供了 80 多个最佳实践、风格指南和架构技巧。每天都会创建新的问题和拉取请求,以保持此实时书籍的更新。我们很乐意看到你在这里做出贡献,无论是修复代码错误、帮助翻译还是提出绝妙的新想法。在此处查看我们的写作指南
3. 最佳实践有额外的信息 - 大多数项目符号都包含一个🔗“阅读更多”链接,该链接通过代码示例、所选博客的引用和更多信息来扩展实践
1.1 按组件构建解决方案 #strategic
#updated 1.2 对组件进行分层,将 Web 层保持在边界内
1.3 将常用实用程序包装为软件包,考虑发布 1.4 使用环境感知、安全和分层配置 #strategic #updated
#updated 1.5
选择主框架时考虑所有后果 #new
1.6 谨慎使用打字稿,周到地使用 #new
2.1 使用 Async-Await 或 promise 进行异步错误处理 2.2 扩展内置错误对象 #strategic #updated 2.3 区分操作错误和程序员错误 #strategic
2.4 集中处理错误,而不是在中间件内处理错误 #updated
#strategic
2.5 使用 OpenAPI 或 GraphQL 记录 API
当陌生人来到镇上时优雅地退出流程 错误 2.6
#strategic
2.7 使用成熟的记录器来提高错误的可见性 #updated
2.8 使用你喜欢的测试框架测试错误流 #updated
2.9 使用 APM 产品发现错误和停机时间 2.10 捕获未处理的承诺拒绝 #updated
2.11 快速失败,使用专用库验证参数 2.12 在返回之前始终等待承诺以避免部分堆栈跟踪 #new
2.13 订阅事件发射器“错误”事件 #new
3.1 使用 ESLint #strategic
3.2 使用 Node.js eslint 扩展插件 #updated
3.3 在同一行上启动代码块的大括号 3.4 正确分隔语句 3.5 命名函数 3.6 对变量、常量、函数和类使用命名约定 3.7 更喜欢常量而不是让。抛弃变量 3.8 首先需要模块,而不是函数内部 3.9 设置模块/文件夹的显式入口点 #updated
3.10 使用 === 运算符 3.11 使用异步等待,避免回调 #strategic
3.12 使用箭头函数表达式 (=>) 3.13 避免函数 #new 之外的效果
4.1 至少编写API(组件)
测试 #strategic 4.2 每个测试名称包含 3 个部分 #new 4.3
AAA模式的结构测试 #strategic
4.4 确保节点版本统一 #new
4.5 避免全局测试夹具和种子,为每个测试添加数据 #strategic 4.6
标记测试 #advanced
4.7 检查测试覆盖率,这有助于识别错误的测试模式 4.8 使用类似生产的环境进行e2e测试 4.9 定期使用静态分析工具重构 4.10 外部HTTP服务的模拟响应 #advanced #new
4.11 隔离测试中间件 4.12 在生产中指定端口,在测试中随机化 #advanced
#new 4.13
测试五种可能的结果 #strategic#new
5.1. 监控 #strategic
5.2.使用智能日志记录 #strategic 5.3 提高可观测性
。将任何可能的内容(例如 gzip、SSL)委托给反向代理 #strategic
5.4。锁定依赖关系 5.5.使用正确的工具保护过程正常运行时间 5.6.利用所有 CPU 内核 5.7。创建“维护终结点” 5.8.使用 APM 产品发现未知因素 #advanced
5.9。使你的代码可用于生产 5.10。测量并保护内存使用情况 #updated
#advanced
5.11。从节点 5.12 中获取前端资产。努力成为无国籍 #strategic
5.13。使用自动检测漏洞的工具 5.14.在 5.15 #advanced 为每个日志语句分配事务 ID
。设置 NODE_ENV=生产 5.16。#advanced 5.17 设计自动化、原子和零停机部署
。使用 Node.js 5.18 的 LTS 版本。登录到标准输出,避免在 5.19 #updated 应用程序中指定日志目标
。使用 npm ci #new 安装软件包
6.1. 采用棉绒安全规则 6.2.使用中间件限制并发请求 6.3 从配置文件中提取机密或使用包对其进行加密 #strategic
6.4.使用 ORM/ODM 库 #strategic 6.5 防止查询注入漏洞
。通用安全最佳实践的集合 6.6.调整 HTTP 响应标头以增强安全性 6.7.持续自动检查易受攻击的依赖项 #strategic
6.8。使用 bcrypt 或 scrypt #strategic 6.9 保护用户的密码/机密
。转义 HTML、JS 和 CSS 输出 6.10。验证传入的 JSON 架构 #strategic
6.11。支持将 JWT 6.12 列入黑名单。防止针对授权 #advanced
6.13 的暴力攻击。以非 root 用户 6.14 身份运行 Node.js。使用反向代理或中间件限制有效负载大小 6.15。避免使用 JavaScript 评估语句 6.16。防止邪恶的正则表达式使你的单线程执行过载 6.17。避免使用变量 6.18 加载模块。在沙盒 6.19 中运行不安全的代码。使用 6.20 #advanced 子进程时要格外小心
。对客户端隐藏错误详细信息 6.21.为 npm 或纱线 #strategic
6.22 配置 2FA。修改会话中间件设置 6.23.通过显式设置进程崩溃的时间 #advanced 6.24 来避免 DOS 攻击
。防止不安全的重定向 6.25.避免将机密发布到 npm 注册表 6.26. 6.26 检查过时的软件包 6.27. 使用“node:”协议导入内置模块 #new
8.1 使用多阶段构建实现更精简、更安全的 Docker 镜像 #strategic
8.2.使用 node 命令引导,避免 npm start 8.3。让 Docker 运行时处理复制和正常运行时间 #strategic
8.4。使用 .dockerignore 防止泄露机密 8.5.生产前清理依赖关系 8.6.智能而优雅地关闭 #advanced
8.7。使用 Docker 和 v8 #advanced #strategic
8.8 设置内存限制。规划高效缓存 8.9.使用显式图像引用,避免使用最新标签 8.10。首选较小的 Docker 基础映像 8.11。清理构建时机密,避免在 8.12 #strategic #new 参数中使用机密
。扫描图像以查找多层漏洞 #advanced
8.13 清理NODE_MODULE缓存 8.14。通用 Docker 实践 8.15。清理你的 Dockerfile #new
1. Project Architecture Practices
📝 #updated
博士:系统的根目录应包含表示合理大小的业务模块的文件夹或存储库。每个组件代表一个产品域(即边界上下文),如“用户组件”、“订单组件”等。每个组件都有自己的 API、逻辑和逻辑数据库。有什么重大优点?使用自治组件,每个更改都是在粒度和更小的范围内执行的 - 精神过载、开发摩擦和部署恐惧要小得多,也好得多。因此,开发人员可以更快地行动。这不一定需要物理分离,可以使用 Monorepo 或多存储库来实现
my-system
├─ apps (components)
│ ├─ orders
│ ├─ users
│ ├─ payments
├─ libraries (generic cross-component functionality)
│ ├─ logger
│ ├─ authenticator
否则:当来自不同模块/主题的工件混合在一起时,很有可能出现紧密耦合的“意大利面条”系统。例如,在“module-a controller”可能调用“module-b service”的架构中,没有明确的模块化边界 - 每个代码更改都可能影响其他任何内容。使用这种方法,编写新功能的开发人员很难意识到其更改的范围和影响。因此,他们担心破坏其他模块,并且每次部署都变得更慢,风险更大
📝 #updated
博士:每个组件都应包含“层” - 一个用于常见问题的专用文件夹:控制器所在的“入口点”,逻辑所在的“域”和“数据访问”。最流行的架构的主要原则是将技术问题(例如,HTTP,DB等)与应用程序的纯逻辑分开,以便开发人员可以编写更多功能,而不必担心基础设施问题。将每个关注点放在专用文件夹(也称为 3 层模式)中是实现此目标的最简单方法
my-system
├─ apps (components)
│ ├─ component-a
│ ├─ entry-points
│ │ ├─ api # controller comes here
│ │ ├─ message-queue # message consumer comes here
│ ├─ domain # features and flows: DTO, services, logic
│ ├─ data-access # DB calls w/o ORM
否则:经常看到开发人员将请求/响应等 Web 对象传递给域/逻辑层中的函数 - 这违反了分离原则,并且使以后更难被其他客户端访问逻辑代码,如测试代码、计划作业、消息队列等
博士:将所有可重用模块放在专用文件夹中,例如“库”,并在每个模块下方的自己的文件夹中,例如“/libraries/logger”。使模块成为具有自己的 package.json 文件的独立包,以增加模块封装,并允许将来发布到存储库。在 Monorepo 设置中,模块可以通过“npm 链接”到它们的物理路径、使用 ts-path 或通过从包管理器存储库(如 npm 注册表)发布和安装来使用。
my-system
├─ apps (components)
│ ├─ component-a
├─ libraries (generic cross-component functionality)
│ ├─ logger
│ │ ├─ package.json
│ │ ├─ src
│ │ │ ├─ index.js
否则:模块的客户端可能会导入并耦合到模块的内部功能。在根目录中使用 package.json,可以设置 package.json.main 或 package.json.exports 来明确告知哪些文件和函数是公共接口的一部分。
📝 #updated
博士:完美的配置设置应确保 (a) 可以从文件和从环境变量中读取密钥 (b) 机密保存在提交的代码之外 (c) 配置是分层的,以便于查找 (d) 类型支持 (e) 快速失败验证 (f) 为每个密钥指定默认值。有一些软件包可以帮助勾选大多数框,如罪犯,env-var,zod等
否则:请考虑未提供的必需环境变量。应用程序成功启动并处理请求,某些信息已保存到数据库。然后,它意识到如果没有此强制键,请求将无法完成,从而使应用程序处于脏状态
🌟 #new
博士:构建应用和 API 时,必须使用框架。很容易忽略替代框架或重要考虑因素,然后最终落在次优选项上。截至 2023/2024 年,我们认为这四个框架值得考虑:Nest.js、Fastify、Express 和 Koa。单击下面的阅读更多内容,了解每个框架的详细优缺点。简单地说,我们相信 Nest.js 是希望使用 OOP 和/或构建无法划分为较小自治组件的大型应用程序的团队的最佳选择。Fastify 是我们建议使用具有合理大小的组件(例如微服务)的应用,这些组件是围绕简单的 Node.js 机制构建的。在此处阅读我们的完整注意事项指南
否则:由于考虑因素众多,很容易根据部分信息做出决定并将苹果与橙子进行比较。例如,人们认为Fastify是一个最小的Web服务器,应该仅与express进行比较。实际上,它是一个丰富的框架,其中包含许多官方插件,涵盖了许多问题。
🌟 #new
博士:没有类型安全的编码不再是一种选择,TypeScript 是这项任务最受欢迎的选项。使用它来定义变量和函数返回类型。因此,它也是一把双刃剑,可以通过其额外的~50个关键字和复杂的功能极大地鼓励复杂性。考虑谨慎使用它,主要是简单的类型,并且仅在真正需要时才使用高级功能
否则:研究表明,使用TypeScript可以帮助更早地检测~20%的错误。没有它,开发人员在 IDE 中的体验也是无法忍受的。另一方面,80% 的其他错误不是使用类型发现的。因此,类型化语法很有价值,但有限。只有有效的测试才能发现所有错误,包括与类型相关的错误。它也可能违背其目的:复杂的代码功能可能会增加代码复杂性,这本身会增加错误的数量和平均错误修复时间。
2. Error Handling Practices
博士:以回调风格处理异步错误可能是通往地狱的最快方法(又名末日金字塔)。你可以给你的代码最好的礼物是将 Promise 与 async-await 一起使用,它支持更紧凑和熟悉的代码语法,如 try-catch
否则:Node.js回调样式,函数(err,response)是一种有前途的无法维护的代码,因为错误处理与随意代码,过多的嵌套和笨拙的编码模式混合在一起
📝 #updated
博士:一些库以字符串或某些自定义类型的形式抛出错误 - 这使错误处理逻辑和模块之间的互操作性复杂化。相反,请创建扩展内置 Error 对象的应用错误对象/类,并在拒绝、引发或发出错误时使用它。应用错误应添加有用的命令性属性,例如错误名称/代码和灾难性属性。通过这样做,所有错误都具有统一的结构并支持更好的错误处理。有 ESLint 规则严格检查这一点(尽管它有一些限制,在使用 TypeScript 和设置规则时可以解决)
no-throw-literal
@typescript-eslint/no-throw-literal
否则:当调用某些组件时,不确定返回哪种类型的错误 - 这使得正确的错误处理变得更加困难。更糟糕的是,使用自定义类型描述错误可能会导致堆栈跟踪等关键错误信息的丢失!
📝 #updated
博士:操作错误(例如,API 收到无效输入)是指完全理解错误影响并且可以深思熟虑地处理的已知情况。另一方面,灾难性错误(也称为程序员错误)是指指示正常重新启动应用程序的异常代码故障
否则:当出现错误时,你可能总是重新启动应用程序,但为什么要因为轻微的、可预测的操作错误而让 ~5000 名在线用户关闭呢?反之亦然也不理想 - 当发生未知的灾难性问题(程序员错误)时保持应用程序正常运行可能会导致不可预测的行为。区分两者可以巧妙地采取行动,并根据给定的背景应用平衡的方法
博士:错误处理逻辑(如日志记录、决定是否崩溃和监控指标)应封装在一个专用的集中式对象中,当出现错误时,所有入口点(例如 API、cron 作业、计划作业)都会调用该对象
否则:不在一个位置处理错误将导致代码重复,并可能导致错误处理不当
博士:让你的 API 调用方知道哪些错误可能会返回,以便他们可以深思熟虑地处理这些错误而不会崩溃。对于RESTful API,这通常是通过OpenAPI等文档框架完成的。如果你使用的是 GraphQL,你也可以利用你的模式和注释。
否则:API 客户端可能决定崩溃并重新启动,只是因为它收到无法理解的错误。注意:API 的调用方可能是你(在微服务环境中非常典型)
🔗 阅读更多:在 Swagger 或 GraphQL 中记录 API 错误
博士:发生未知错误(灾难性错误,请参阅最佳实践 2.3)时 - 应用程序运行状况存在不确定性。在这种情况下,无法避免使错误 Observable 到,关闭连接并退出进程。任何信誉良好的运行时框架(如 Docker 化服务或云无服务器解决方案)都将注意重新启动
否则:当发生不熟悉的异常时,某些对象可能处于错误状态(例如,全局使用的事件发射器由于某些内部故障而不再触发事件),并且所有未来的请求都可能失败或行为疯狂
📝 #updated
博士:像Pino或Winston这样的强大日志记录工具使用日志级别,漂亮的打印着色等功能来提高错误的可见性。控制台.log缺少这些命令性功能,应避免使用。一流的记录器允许将自定义有用的属性附加到日志条目,并将序列化性能损失降至最低。开发人员应将日志写入相应的日志聚合器,并让基础结构将流通过管道传输到相应的日志聚合器
stdout
否则:浏览 console.logs 或手动浏览凌乱的文本文件,而无需查询工具或体面的日志查看器,可能会让你忙于工作直到深夜
📝 #updated
博士:无论是专业的自动化 QA 还是简单的手动开发人员测试 – 确保你的代码不仅满足积极的场景,而且还处理和返回正确的错误。最重要的是,模拟更深层次的错误流,如未捕获的异常,并确保错误处理程序正确处理这些错误流(请参阅“阅读更多”部分中的代码示例)
否则:如果没有测试,无论是自动还是手动,你都不能依赖代码来返回正确的错误。没有有意义的错误 – 没有错误处理
博士:监控和性能产品(又名 APM)主动衡量你的代码库或 API,以便它们可以自动突出显示你缺少的错误、崩溃和缓慢部分
否则:你可能会花费大量精力来衡量 API 性能和停机时间,可能你永远不会意识到在实际场景中哪些代码部分最慢,以及这些代码部分如何影响用户体验
📝 #updated
博士:承诺中抛出的任何异常都将被吞噬并丢弃,除非开发人员没有忘记显式处理它。即使你的代码已订阅!通过注册活动来克服这个问题
process.uncaughtException
process.unhandledRejection
否则:你的错误会被吞噬,不留痕迹。没什么可担心的
博士:断言 API 输入以避免以后更难跟踪的讨厌错误。验证代码通常很繁琐,除非你使用的是现代验证库,如 ajv、zod 或 typebox
否则:考虑一下 – 你的函数需要一个数字参数“Discount”,调用者忘记传递该参数,稍后,你的代码会检查 Discount!=0(允许的折扣金额大于零),然后它将允许用户享受折扣。天哪,真是个讨厌的错误。你能看到吗?
🌟 #new
博士:在返回承诺时始终这样做,以使完整的错误堆栈跟踪受益。如果函数返回一个承诺,则必须将该函数声明为函数,并在返回之前显式声明为承诺
return await
async
await
否则:返回承诺而不等待的函数不会出现在堆栈跟踪中。这种缺失帧可能会使理解导致错误的流程复杂化,特别是如果异常行为的原因在缺失函数内部
🌟 #new
博士:与典型函数不同,try-catch 子句不会获得源自事件发射器和从它继承的任何内容(例如流)的错误。订阅事件发射器的“error”事件,而不是 try-catch,以便代码可以在上下文中处理错误。在处理 EventTargets(事件发射器的 Web 标准版本)时,没有“错误”事件,所有错误都以 process.on('error) 全局事件结束 - 在这种情况下,至少确保进程崩溃或不基于所需的上下文。另外,请注意,除非使用 {captureRejections: true} 初始化事件发射器,否则不会捕获源自异步事件处理程序的错误
否则:事件发射器通常用于全局和关键应用程序功能,例如数据库或消息队列连接。当这种关键对象抛出错误时,充其量进程会由于未处理的异常而崩溃。更糟糕的是,当关键功能关闭时,它将像僵尸一样存活
3. Code Patterns And Style Practices
TL;DR:ESLint 是检查可能的代码错误和修复代码样式的事实标准,不仅可以识别细节间距问题,还可以检测严重的代码反模式,例如开发人员在没有分类的情况下抛出错误。虽然 ESLint 可以自动修复代码样式,但其他工具(如 prettier)在格式化修复并与 ESLint 结合使用方面更强大。
否则:开发人员将专注于繁琐的间距和线宽问题,并且可能会浪费时间过度思考项目的代码风格
📝 #updated
博士:在涵盖原版 JavaScript 的 ESLint 标准规则之上,添加 Node.js特定插件,如 eslint-plugin-node、eslint-plugin-mocha 和 eslint-plugin-node-security、eslint-plugin-require、/eslint-plugin-jest 和其他有用的规则
否则:许多错误的 Node.js 代码模式可能会在雷达下逃脱。例如,开发人员可能需要(变量AsPath)文件,其中变量作为路径,允许攻击者执行任何JS脚本。节点.js短绒可以检测到这种模式并及早投诉
博士:代码块的左大括号应与开始语句位于同一行
// Do
function someFunction() {
// code block
}
// Avoid
function someFunction()
{
// code block
}
否则:推迟此最佳实践可能会导致意外结果,如下面的 StackOverflow 线程所示:
无论你是否使用分号来分隔语句,了解不正确换行或自动插入分号的常见陷阱,都将帮助你消除常规语法错误。
博士:使用 ESLint 了解分离问题。Prettier或Standardjs可以自动解决这些问题。
否则:如上一节所示,如果没有分号,JavaScript 的解释器会自动在语句末尾添加一个分号,或者认为语句没有在应该结束的地方结束,这可能会导致一些不希望的结果。你可以使用赋值并避免使用立即调用的函数表达式来防止大多数意外错误。
// Do
function doThing() {
// ...
}
doThing()
// Do
const items = [1, 2, 3]
items.forEach(console.log)
// Avoid — throws exception
const m = new Map()
const a = [1,2,3]
[...m.values()].forEach(console.log)
> [...m.values()].forEach(console.log)
> ^^^
> SyntaxError: Unexpected token ...
// Avoid — throws exception
const count = 2 // it tries to run 2(), but 2 is not a function
(function doSomething() {
// do something amazing
}())
// put a semicolon before the immediate invoked function, after the const definition, save the return value of the anonymous function to a variable or avoid IIFEs altogether
博士:命名所有函数,包括闭包和回调。避免匿名函数。这在分析节点应用程序时特别有用。命名所有函数将使你能够在检查内存快照时轻松了解你正在查看的内容
否则:使用核心转储(内存快照)调试生产问题可能会变得具有挑战性,因为你注意到匿名函数会消耗大量内存
博士:命名常量、变量和函数时使用 lowerCamelCase,命名类时使用 UpperCamelCase(首字母大写),命名全局变量或静态变量时使用 UPPER_SNAKE_CASE。这将帮助你轻松区分普通变量、函数、需要实例化的类和在全局模块范围内声明的变量。使用描述性名称,但尽量保持简短
否则:JavaScript是世界上唯一允许直接调用构造函数(“类”)而无需先实例化它的语言。因此,类和函数构造函数通过从 UpperCamelCase 开始进行区分
// for global variables names we use the const/let keyword and UPPER_SNAKE_CASE
let MUTABLE_GLOBAL = "mutable value";
const GLOBAL_CONSTANT = "immutable value";
const CONFIG = {
key: "value",
};
// examples of UPPER_SNAKE_CASE convention in nodejs/javascript ecosystem
// in javascript Math.PI module
const PI = 3.141592653589793;
// https://github.com/nodejs/node/blob/b9f36062d7b5c5039498e98d2f2c180dca2a7065/lib/internal/http2/core.js#L303
// in nodejs http2 module
const HTTP_STATUS_OK = 200;
const HTTP_STATUS_CREATED = 201;
// for class name we use UpperCamelCase
class SomeClassExample {
// for static class properties we use UPPER_SNAKE_CASE
static STATIC_PROPERTY = "value";
}
// for functions names we use lowerCamelCase
function doSomething() {
// for scoped variable names we use the const/let keyword and lowerCamelCase
const someConstExample = "immutable value";
let someMutableExample = "mutable value";
}
博士:使用 意味着一旦分配了变量,就无法重新分配它。首选将帮助你不想将同一变量用于不同的用途,并使代码更清晰。如果需要重新分配变量,例如在 for 循环中,请使用来声明它。另一个重要方面是,使用它声明的变量仅在定义它的块范围内可用。 是函数作用域的,而不是块作用域的,现在你已经可以使用了,不应该在 ES6 中使用
const
const
let
let
var
const
let
否则:当遵循经常更改的变量时,调试变得更加麻烦
博士:要求在每个文件的开头、任何函数之前和外部使用模块。这个简单的最佳实践不仅可以帮助你轻松快速地在顶部分辨文件的依赖关系,还可以避免一些潜在的问题
否则:需求由 Node.js 同步运行。如果从函数内部调用它们,则可能会阻止在更关键的时间处理其他请求。此外,如果所需的模块或其任何依赖项引发错误并使服务器崩溃,则最好尽快找到它,如果函数中需要该模块,则情况可能并非如此。
📝 #updated
博士:开发模块/库时,设置一个显式根文件,用于导出公共和感兴趣的代码。不鼓励客户端代码导入深层文件并熟悉内部结构。使用commonjs(require),这可以通过文件夹根目录或package.json.main字段中的index.js文件来完成。使用ESM(导入),如果根目录上存在package.json,则“export”字段允许指定模块的根文件。如果不存在 package.json,你可以在根目录上放置一个 index.js 文件,该文件会重新导出所有公共功能
否则:拥有一个显式的根文件就像一个公共“接口”,它封装了内部文件,将调用者定向到公共代码,并在不破坏契约的情况下促进未来的更改
// Avoid: client has deep familiarity with the internals
// Client code
const SMSWithMedia = require("./SMSProvider/providers/media/media-provider.js");
// Better: explicitly export the public functions
//index.js, module code
module.exports.SMSWithMedia = require("./SMSProvider/providers/media/media-provider.js");
// Client code
const { SMSWithMedia } = require("./SMSProvider");
===
博士:更喜欢严格相等运算符而不是较弱的抽象相等运算符。 将在将两个变量转换为通用类型后比较它们。中没有类型转换,两个变量的类型必须相同才能相等
===
==
==
===
否则:与运算符相比,不相等的变量可能会返回 true
==
"" == "0"; // false
0 == ""; // true
0 == "0"; // true
false == "false"; // false
false == "0"; // true
false == undefined; // false
false == null; // false
null == undefined; // true
" \t\r\n " == 0; // true
如果与 一起使用,上述所有语句都将返回 false
===
博士:Async-await 是表达异步流的最简单方法,因为它使异步代码看起来是同步的。Async-await 还将产生更紧凑的代码和对 try-catch 的支持。在大多数情况下,此技术现在取代了回调和承诺。在你的代码中使用它可能是送给代码阅读器最好的礼物
否则:在回调样式中处理异步错误可能是最快的方法——这种风格强制检查所有错误,处理尴尬的代码嵌套,并且很难推理代码流
博士:尽管建议在处理接受承诺或回调的旧 API 时使用 async-await 并避免使用函数参数 - 箭头函数使代码结构更加紧凑并保持根函数的词法上下文(即
this)
否则:较长的代码(在 ES5 函数中)更容易出现错误且阅读繁琐
🌟 #new
博士:避免将具有网络或数据库调用等效果的代码放在函数之外。当另一个文件需要该文件时,将立即执行此类代码。当底层系统尚未准备就绪时,可能会执行此“浮动”代码。即使此模块的功能最终不会在运行时使用,它也会带来性能损失。最后,在函数之外,模拟这些用于测试的数据库/网络调用更难。相反,将此代码放在应显式调用的函数中。如果某些数据库/网络代码必须在模块加载时正确执行,请考虑使用工厂或显示模块模式
否则:典型的 Web 框架设置错误处理程序、环境变量和监控。在初始化 Web 框架之前进行数据库/网络调用时,由于缺少配置数据,它们不会被监视或失败
4. Testing And Overall Quality Practices
_We有专门的测试指南,见下文。此处的最佳实践列表是这些指南的简要摘要
a. JavaScript 测试最佳实践 b. 节点.js测试 - 超越基础 _
博士:由于时间表短,大多数项目没有任何自动化测试,或者“测试项目”经常失控并被放弃。出于这个原因,优先考虑并从 API 测试开始,这是最简单的编写方法,并且比单元测试提供更多的覆盖范围(你甚至可以使用 Postman 等工具在没有代码的情况下制作 API 测试)。之后,如果你有更多的资源和时间,请继续使用高级测试类型,如单元测试、数据库测试、性能测试等
否则:你可能会花费很长时间编写单元测试,以发现你只获得了 20% 的系统覆盖率
🌟 #new
博士:使测试在需求级别进行,以便对于不熟悉代码内部的 QA 工程师和开发人员来说,这也是不言自明的。在测试名称中说明正在测试的内容(被测试单元)、在什么情况下以及预期结果是什么
否则:部署刚刚失败,名为“添加产品”的测试失败。这是否告诉你到底是什么故障?
🌟 #new
博士:使用3个分隔良好的部分来构建你的测试:安排,行动和断言(AAA)。第一部分包括测试设置,然后是被测单元的执行,最后是断言阶段。遵循此结构可确保读者无需花费脑力 CPU 来理解测试计划
否则:你不仅每天花很长时间来理解主代码,而且现在应该只是一天中简单的部分(测试)会让你的大脑得到伸展
🌟 #new
博士:使用鼓励或强制跨不同环境和开发人员使用相同 Node.js 版本的工具。nvm和Volta等工具允许在文件中指定项目的版本,以便每个团队成员都可以运行单个命令以符合项目的版本。或者,可以将此定义复制到 CI 和生产运行时(例如,将指定的值复制到 .Dockerfile 构建和 CI 声明文件)
否则:开发人员可能会遇到或错过错误,因为她使用的 Node.js 版本与她的队友不同。更糟糕的是 - 生产运行时可能与执行测试的环境不同
博士:为了防止测试耦合并轻松推理测试流,每个测试都应添加并操作其自己的一组数据库行。每当测试需要提取或假设某些数据库数据存在时 - 它必须显式添加该数据并避免更改任何其他记录
否则:考虑一个场景,部署由于测试失败而中止,团队现在将花费宝贵的调查时间,最终得出一个可悲的结论:系统运行良好,但是测试相互干扰并破坏构建
博士:不同的测试必须在不同的场景上运行:快速冒烟、无 IO、测试应在开发人员保存或提交文件时运行、完整的端到端测试通常在提交新的拉取请求时运行等。这可以通过使用关键字(如 #cold #api #sanity)标记测试来实现,以便你可以使用测试工具进行 grep 并调用所需的子集。例如,这就是你仅使用 Mocha 调用健全性测试组的方式:mocha --grep 'sanity'
否则:运行所有测试,包括执行数十个数据库查询的测试,每当开发人员进行小的更改时,都会非常慢,并使开发人员远离运行测试
博士:像伊斯坦布尔/纽约这样的代码覆盖率工具之所以很棒,有三个原因:它是免费的(不需要努力使本报告受益),它有助于识别测试覆盖率的下降,最后但并非最不重要的是它突出了测试不匹配:通过查看彩色代码覆盖率报告,你可能会注意到,例如,从未像 catch 子句那样测试的代码区域(这意味着测试只调用快乐路径,而不是应用程序的方式对错误的行为)。将其设置为在覆盖范围低于特定阈值时失败构建
否则:不会有任何自动指标告诉你何时测试未涵盖大部分代码
博士:包括实时数据的端到端 (e2e) 测试曾经是 CI 流程中最薄弱的环节,因为它依赖于多个繁重的服务,如数据库。使用尽可能接近真实生产环境的环境,例如 a-continue(此处错过了 - 继续,需要内容。从 Else 条款来看,这应该提到 docker-compose)
否则:如果没有 docker-compe,团队必须为每个测试环境(包括开发人员的计算机)维护一个测试数据库,使所有这些数据库保持同步,以便测试结果不会因环境而异
博士:使用静态分析工具有助于提供客观的方法来提高代码质量并保持代码的可维护性。可以将静态分析工具添加到 CI 生成,以便在发现代码异味时失败。它与普通 linting 相比的主要卖点是能够在多个文件的上下文中检查质量(例如检测重复)、执行高级分析(例如代码复杂性)以及跟踪代码问题的历史记录和进度。你可以使用的两个工具示例是Sonarqube(2,600+星)和Code Climate(1,500+星)。
否则:由于代码质量差,错误和性能将始终是一个问题,没有 shiny 的新库或最先进的功能可以解决
🌟 #new
博士:使用网络模拟工具来模拟通过网络(例如,REST,Graph)处理的外部协作者服务的响应。这不仅是为了隔离被测组件,而且主要是为了模拟不满意的路径流。nock(进程内)或模拟服务器等工具允许在一行代码中定义外部服务的特定响应。请记住还要模拟错误、延迟、超时以及生产中可能发生的任何其他事件
否则:允许组件访问实际的外部服务实例可能会导致主要涵盖快乐路径的幼稚测试。测试也可能是片状和缓慢的
博士:当一个中间件拥有一些跨越许多请求的巨大逻辑时,值得在不唤醒整个 Web 框架的情况下单独测试它。这可以通过对 {req, res, next} 对象进行存根和监视来轻松实现
否则:Express 中间件中的错误 === 所有或大多数请求中的错误
🌟 #new
博士:针对 API 进行测试时,在测试中初始化 Web 服务器是很常见且可取的。让服务器在测试中随机化 Web 服务器端口以防止冲突。如果你使用的是 Node.js http 服务器(大多数框架都使用),这样做只需要传递端口号零 - 这将随机化可用端口
否则:指定固定端口将阻止两个测试进程同时运行。默认情况下,大多数现代测试运行程序使用多个进程运行
🌟 #new
博士:测试流时,请确保涵盖五个潜在类别。每当触发某些操作(例如,API 调用)时,都会发生 React ,产生有意义的结果并要求进行测试。每个流都有五种可能的结果类型:响应、可见状态更改(例如 DB)、传出 API 调用、队列中的新消息和可观测性调用(例如,日志记录、指标)。请参阅此处的清单。每种类型的结果都有独特的挑战和技术来缓解这些挑战 - 我们有关于这个主题的专门指南:节点.js测试 - 超越基础知识
否则:在测试向系统添加新产品时,请考虑一个案例。通常看到仅断言有效响应的测试。如果无论 React 如何,产品都未能持续存在怎么办?如果在添加新产品时需要调用某些外部服务,或将消息放入队列中,测试是否也应该断言这些结果?很容易忽略各种路径,这就是清单派上用场的地方
5. Going To Production Practices
博士:监控是一场在客户之前发现问题的游戏——显然,这应该被赋予前所未有的重要性。市场上充斥着优惠,因此请考虑从定义你必须遵循的基本指标开始(我在里面的建议),然后查看其他花哨的功能并选择符合所有条件的解决方案。在任何情况下,都必须涵盖 4 层可观测性:正常运行时间、专注于面向用户的症状和 Node 的指标.js事件循环滞后等技术指标、使用开放式遥测和日志记录进行分布式流测量。单击下面的“阅读更多”,了解解决方案的概述
否则:失败===失望的客户。简单
博士:日志可以是调试语句的哑仓库,也可以是讲述应用故事的漂亮 dashboard 的启用者。从第一天开始规划你的日志平台:如何收集、存储和分析日志,以确保可以真正提取所需的信息(例如错误率、通过服务和服务器跟踪整个事务等)
否则:你最终得到一个难以推理的黑匣子,然后你开始重写所有日志记录语句以添加其他信息
博士:Node在执行CPU密集型任务(如gzipping,SSL终止等)方面非常糟糕。你应该使用专门的基础架构,例如nginx,HAproxy或云供应商服务。
否则:糟糕的单线程将忙于执行基础结构任务,而不是处理应用程序核心,性能将相应下降
博士:你的代码在所有环境中必须相同,但如果没有特殊的锁文件 npm,依赖项会在环境之间漂移。确保提交你的 package-lock.json,以便所有环境都相同
否则:QA 将彻底测试代码并批准在生产中行为不同的版本。更糟糕的是,同一生产集群中的不同服务器可能运行不同的代码
博士:该过程必须继续,并在失败时重新启动。现代运行时平台,如 Docker 化平台(例如 Kubernetes)和无服务器会自动处理这个问题。当应用程序托管在裸机服务器上时,必须注意像systemd这样的流程管理工具。避免在监控应用程序实例(例如 Kubernetes)的现代平台中包含自定义流程管理工具 - 这样做会隐藏基础架构中的故障。当底层基础结构不知道错误时,它无法执行有用的缓解步骤,例如将实例重新放置在其他位置
否则:在没有明确策略和太多工具(集群管理、docker、PM2)的情况下运行数十个实例可能会导致 DevOps 混乱
博士:在其基本形式中,Node 应用程序在单个 CPU 内核上运行,而所有其他内核都处于空闲状态。你有责任复制节点进程并利用所有 CPU。大多数现代运行时平台(例如 Kubernetes)允许复制应用程序的实例,但它们不会验证所有内核是否都已使用——这是你的职责。如果应用程序托管在裸服务器上,你也有责任使用某些进程复制解决方案(例如 systemd)
否则:你的应用程序可能只利用其可用资源的 25% (!) 甚至更少。请注意,典型的服务器有 4 个或更多 CPU 内核,Node.js 的天真部署仅利用 1 个(即使使用 AWS beanstalk 等 PaaS 服务!
博士:在安全的 API 中公开一组与系统相关的信息,例如内存使用情况和 REPL 等。尽管强烈建议依赖标准和经过实战测试的工具,但使用代码可以更轻松地完成一些有价值的信息和操作
否则:你会发现你正在执行许多“诊断部署” - 将代码运送到生产环境只是为了提取一些信息以进行诊断
📝 #updated
博士:考虑向生产堆栈添加另一个安全层 - APM。虽然大多数症状和原因都可以使用传统的监测技术检测到,但在分布式系统中,不仅仅是眼睛看到的。应用程序监控和性能产品(又名 APM)可以自动超越传统监控,并提供额外的发现和开发人员体验层。例如,某些 APM 产品可以突出显示最终用户端加载速度太慢的事务,同时建议根本原因。APM 还为尝试通过显示发生错误时服务器忙于处理什么来排查日志错误的开发人员提供更多上下文。举几个例子
否则:你可能会花费大量精力来衡量 API 性能和停机时间,可能你永远不会意识到在实际场景中哪个代码部分最慢,以及这些代码部分如何影响用户体验
博士:代码牢记最终目标,从第一天开始计划生产。这听起来有点模糊,所以我整理了一些与生产维护密切相关的开发技巧(点击“阅读更多”)
否则:一个世界冠军IT/DevOps的人不会拯救一个写得不好的系统
博士:Node.js 与内存有争议的关系:v8 引擎对内存使用有软限制(1.4GB),并且在 Node 的代码中存在泄漏内存的已知路径——因此监视 Node 的进程内存是必须的。在小型应用中,你可以使用 shell 命令定期测量内存,但在中型应用中,请考虑将内存手表烘焙到强大的监视系统中
否则:你的进程内存可能每天泄漏一百兆字节,就像在沃尔玛发生的那样
博士:使用专门的基础架构(nginx,S3,CDN)提供前端内容,因为由于其单线程模型,Node在处理许多静态文件时会受到损害。此准则的一个例外是执行服务器端渲染时
否则:你的单个 Node 线程将忙于流式传输数百个 html/images/angular/react 文件,而不是为其诞生的任务分配所有资源 - 提供动态内容
博士:在外部数据存储中存储任何类型的数据(例如用户会话、缓存、上传的文件)。当应用在进程中保存数据时,这会增加额外的维护复杂性,例如将用户路由到同一实例以及重新启动进程的更高成本。为了强制和鼓励无状态方法,大多数现代运行时平台都允许定期“重新应用”实例
否则:给定服务器的故障将导致应用程序停机,而不仅仅是杀死有故障的计算机。此外,由于依赖于特定服务器,横向扩展弹性将变得更具挑战性
博士:即使是最有信誉的依赖项(如 Express)也存在已知漏洞(不时),这些漏洞可能会使系统面临风险。这可以使用社区和商业工具轻松驯服,这些工具不断检查漏洞并发出警告(在本地或在GitHub上),有些甚至可以立即修补它们。
否则:在没有专用工具的情况下保持代码免受漏洞的影响,需要你不断关注有关新威胁的在线出版物。相当乏味
博士:为单个请求中的每个日志条目分配相同的标识符 transaction-id: uuid()(也称为相关性 id/tracing-id/request-context)。然后,在检查日志中的错误时,轻松得出之前和之后发生的事情。Node 有一个内置的机制 AsyncLocalStorage,用于在异步调用之间保持相同的上下文。查看内部代码示例
否则:在没有上下文的情况下查看生产错误日志(之前发生的事情)会使推理问题变得更加困难和缓慢
🔗 阅读更多:为每个日志语句分配“TransactionId”
NODE_ENV=production
博士:将环境变量设置为“生产”或“开发”以标记是否应激活生产优化 - 某些 npm 包确定当前环境并优化其代码以进行生产
NODE_ENV
否则:在处理某些特定库(如 Express 服务器端呈现)时,省略此简单属性可能会大大降低性能
博士:研究表明,执行许多部署的团队可以降低出现严重生产问题的可能性。不需要有风险的手动步骤和服务停机时间的快速自动化部署可显著改善部署过程。你可能应该使用 Docker 与 CI 工具结合使用来实现这一点,因为它们已成为简化部署的行业标准。
否则:长时间部署 - >生产停机时间和人为错误 - >团队对进行部署没有信心 - >更少的部署和功能
博士:确保你使用的是 LTS 版本的 Node.js以接收关键错误修复、安全更新和性能改进
否则:新发现的错误或漏洞可用于利用在生产环境中运行的应用程序,并且你的应用程序可能不受各种模块的支持并且更难维护
📝 #updated
博士:日志目标不应由开发人员在应用程序代码中进行硬编码,而应由应用程序运行的执行环境定义。开发人员应该使用记录器实用程序写入日志,然后让执行环境(容器、服务器等)将流管道传输到适当的目标(即 Splunk、Graylog、ElasticSearch 等)。
stdout
stdout
否则:如果开发人员设置日志路由,则留给希望自定义它的运维专业人员的灵活性会降低。除此之外,如果应用尝试直接登录到远程位置(例如弹性搜索),以防出现恐慌或崩溃 - 可能无法到达可能解释问题的更多日志
npm ci
博士:运行以严格执行与 package.json 和 package-lock.json 匹配的依赖项的全新安装。显然,生产代码必须使用用于测试的包的确切版本。虽然 package-lock.json 文件为依赖项设置了严格的版本,但如果与文件 package.json 不匹配,命令“npm install”会将 package.json 视为事实来源。另一方面,如果这些文件不匹配,命令“npm ci”将退出并出错
npm ci
否则:QA 将彻底测试代码并批准在生产中行为不同的版本。更糟糕的是,同一生产群集中的不同服务器可能运行不同的代码。
6. Security Best Practices
博士:利用与安全相关的 linter 插件(如 eslint-plugin-security)尽早捕获安全漏洞和问题,最好是在编码时。这有助于捕获安全漏洞,例如使用 eval、调用子进程或导入带有字符串文字(例如用户输入)的模块。单击下面的“阅读更多”以查看将被安全棉绒捕获的代码示例
否则:在开发过程中可能是一个直接的安全漏洞,在生产中变成了一个主要问题。此外,项目可能不遵循一致的代码安全实践,导致引入漏洞或将敏感机密提交到远程存储库
博士:DOS攻击非常流行,相对容易进行。使用外部服务实现速率限制,例如云负载均衡器、云防火墙、nginx、速率限制器灵活包,或(对于较小和不太重要的应用程序)速率限制中间件(例如快速速率限制))
否则:应用程序可能会受到攻击,从而导致拒绝服务,其中真实用户收到降级或不可用的服务。
博士:切勿在配置文件或源代码中存储纯文本机密。相反,利用秘密管理系统,如Vault产品,Kubernetes / Docker Secrets或使用环境变量。作为最后的手段,必须对存储在源代码管理中的机密进行加密和管理(滚动密钥、过期、审核等)。利用预提交/推送钩子来防止意外提交机密
否则:源代码管理,即使是私有仓库,也可能被错误地公开,此时所有机密都会暴露。外部方对源代码管理的访问将无意中提供对相关系统(数据库、API、服务等)的访问。
博士:为了防止 SQL/NoSQL 注入和其他恶意攻击,请始终使用 ORM/ODM 或数据库库来转义数据或支持命名或索引参数化查询,并负责验证预期类型的用户输入。切勿仅使用 JavaScript 模板字符串或字符串串联将值注入查询,因为这会使你的应用程序面临各种漏洞。所有信誉良好的 Node.js 数据访问库(例如 Sequelize、Knex、mongoose)都有针对注入攻击的内置保护。
否则:在使用MongoDB for NoSQL时,未经验证或未经净化的用户输入可能会导致操作员注入,并且不使用适当的清理系统或ORM将很容易允许SQL注入攻击,从而产生巨大的漏洞。
博士:这是一个与 Node 没有直接关系的安全建议集合.js - Node 实现与任何其他语言没有太大区别。单击“阅读更多”以浏览。
博士:应用程序应使用安全标头来防止攻击者使用常见攻击,如跨站点脚本 (XSS)、点击劫持和其他恶意攻击。这些可以使用头盔等模块轻松配置。
否则:攻击者可能会对应用程序的用户进行直接攻击,从而导致巨大的安全漏洞
博士:在 npm 生态系统中,项目通常有许多依赖项。在发现新漏洞时,应始终检查依赖项。使用 npm Audit 或 snyk 等工具来跟踪、监控和修补易受攻击的依赖项。将这些工具与 CI 设置集成,以便在将易受攻击的依赖项投入生产之前将其捕获。
否则:攻击者可以检测到你的 Web 框架并攻击其所有已知漏洞。
博士:密码或机密(例如 API 密钥)应使用安全哈希 + salt 函数(如 、)或最坏情况进行存储。
bcrypt
scrypt
pbkdf2
否则:不使用安全功能存储的密码和机密容易受到暴力破解和字典攻击,最终会导致其泄露。
博士:发送到浏览器的不受信任的数据可能会被执行,而不仅仅是显示,这通常称为跨站点脚本 (XSS) 攻击。通过使用专用库将数据显式标记为永远不应执行的纯内容(即编码、转义)来缓解此问题
否则:攻击者可能会在你的数据库中存储恶意 JavaScript 代码,然后将其按原样发送到较差的客户端。
博士:验证传入请求的正文有效负载并确保其符合预期,否则会快速失败。为了避免在每个路由中进行繁琐的验证编码,你可以使用基于 JSON 的轻量级验证架构,例如 jsonschema 或 joi
否则:你的慷慨和宽容的方法大大增加了攻击面,并鼓励攻击者尝试许多输入,直到他们找到某种组合来使应用程序崩溃
博士:使用 JSON Web 令牌(例如,使用 Passport.js)时,默认情况下没有撤销已颁发令牌的访问权限的机制。一旦发现一些恶意用户活动,只要他们持有有效的令牌,就无法阻止他们访问系统。通过实施针对每个请求验证的不受信任令牌的阻止列表来缓解此问题。
否则:第三方可能会恶意使用过期或放错位置的令牌来访问应用程序并模拟令牌的所有者。
博士:一种简单而强大的技术是使用两个指标限制授权尝试:
否则:攻击者可以发出无限制的自动密码尝试,以获取对应用程序特权帐户的访问权限
博士:在一种常见情况下,Node.js 以具有无限权限的根用户身份运行。例如,这是 Docker 容器中的默认行为。建议创建一个非 root 用户并将其烘焙到 Docker 映像中(下面给出的示例),或者通过调用带有标志“-u 用户名”的容器来代表该用户运行进程
否则:设法在服务器上运行脚本的攻击者在本地计算机上获得无限的权力(例如,更改iptable并将流量重新路由到其服务器)
博士:主体有效负载越大,单个线程处理它就越困难。这是攻击者在没有大量请求(DOS/DDOS攻击)的情况下使服务器瘫痪的机会。缓解这种限制边缘(例如防火墙、ELB)上传入请求的正文大小,或者通过将快速正文解析器配置为仅接受小型有效负载来缓解这种情况
否则:你的应用程序将不得不处理大型请求,无法处理它必须完成的其他重要工作,从而导致性能影响和易受 DOS 攻击。
TL;DR:是邪恶的,因为它允许在运行时执行自定义JavaScript代码。这不仅是一个性能问题,也是一个重要的安全问题,因为恶意 JavaScript 代码可能来自用户输入。另一个应该避免的语言特性是构造函数。 并且永远不应该传递动态 JavaScript 代码。
eval
new Function
setTimeout
setInterval
否则:恶意 JavaScript 代码找到一种方式进入文本传递到或其他实时评估 JavaScript 语言函数,并将获得对页面上 JavaScript 权限的完全访问权限。此漏洞通常表现为 XSS 攻击。
eval
博士:正则表达式虽然很方便,但对整个JavaScript应用程序构成了真正的威胁,尤其是Node.js平台。要匹配的文本的用户输入可能需要大量的 CPU 周期来处理。正则表达式处理可能效率低下,以至于验证 10 个单词的单个请求可能会阻塞整个事件循环 6 秒,并将 CPU 设置为 🔥 .出于这个原因,更喜欢第三方验证包,如验证器.js而不是编写自己的正则表达式模式,或者利用安全正则表达式来检测易受攻击的正则表达式模式
否则:编写不佳的正则表达式可能容易受到正则表达式 DoS 攻击,这些攻击将完全阻塞事件循环。例如,流行的软件包在 2017 年 11 月被发现容易受到恶意 RegEx 使用的攻击
moment
博士:避免要求/导入另一个具有作为参数给出的路径的文件,因为担心它可能源自用户输入。此规则可以扩展为访问一般文件(即)或使用源自用户输入的动态变量的其他敏感资源访问。Eslint-plugin-security linter可以捕获此类模式并及早发出警告
fs.readFile()
否则:恶意用户输入可能会找到用于要求篡改文件的参数,例如,文件系统上以前上传的文件,或访问已存在的系统文件。
博士:当任务是运行运行时给出的外部代码(例如.plugin)时,请使用任何类型的“沙盒”执行环境来隔离和保护主代码免受插件的影响。这可以使用专用进程(例如)、无服务器环境或充当沙箱的专用 npm 包来实现
cluster.fork()
否则:插件可以通过无穷无尽的各种选项进行攻击,例如无限循环、内存过载和访问敏感的进程环境变量
博士:尽可能避免使用子进程,并验证和清理输入以缓解 shell 注入攻击(如果仍有必要)。首选使用 根据定义,它只会执行具有一组属性的单个命令,并且不允许 shell 参数扩展。
child_process.execFile
否则:由于恶意用户输入传递到未经净化的系统命令,幼稚地使用子进程可能会导致远程命令执行或 shell 注入攻击。
博士:默认情况下,集成的快速错误处理程序隐藏错误详细信息。但是,使用自定义 Error 对象实现自己的错误处理逻辑的可能性很大(许多人认为这是最佳实践)。如果这样做,请确保不要将整个 Error 对象返回到客户端,其中可能包含一些敏感的应用程序详细信息
否则:敏感的应用程序详细信息(例如服务器文件路径、正在使用的第三方模块以及可能被攻击者利用的应用程序的其他内部工作流)可能会从堆栈跟踪中找到的信息中泄露
博士:开发链中的任何步骤都应该受到MFA(多因素身份验证)的保护,npm / Yarn对于攻击者来说是一个甜蜜的机会,他们可以获取某些开发人员的密码。使用开发人员凭据,攻击者可以将恶意代码注入跨项目和服务广泛安装的库中。如果公开发布,甚至可能在整个网络上发布。在 npm 中启用 2 因素身份验证,攻击者更改包代码的机会几乎为零。
博士:每个Web框架和技术都有其已知的弱点 - 告诉攻击者我们使用哪个Web框架对他们有很大帮助。使用会话中间件的默认设置可能会以与标头类似的方式将应用暴露给特定于模块和框架的劫持攻击。尝试隐藏任何识别和显示你的技术堆栈的内容(例如 Node.js、express)
X-Powered-By
否则:Cookie 可能通过不安全的连接发送,攻击者可能使用会话标识来识别 Web 应用程序的底层框架以及特定于模块的漏洞
博士:未处理错误时,节点进程将崩溃。许多最佳实践甚至建议退出,即使捕获并处理了错误。例如,Express 会在出现任何异步错误时崩溃 - 除非你使用 catch 子句包装路由。这为攻击者打开了一个非常甜蜜的攻击点,攻击者认识到哪些输入使进程崩溃并重复发送相同的请求。对此没有即时补救措施,但一些技术可以减轻痛苦:每当进程因未处理的错误而崩溃时,以严重性发出警报,验证输入并避免由于无效的用户输入而导致进程崩溃,用 catch 包装所有路由,并考虑在请求中产生错误时不要崩溃(而不是全局发生的情况)
否则:这只是一个有根据的猜测:给定许多 Node.js 应用程序,如果我们尝试将一个空的 JSON 正文传递给所有 POST 请求 - 少数应用程序将崩溃。此时,我们可以重复发送相同的请求以轻松关闭应用程序
博士:不验证用户输入的重定向可使攻击者启动网络钓鱼诈骗、窃取用户凭据和执行其他恶意操作。
否则:如果攻击者发现你没有验证用户提供的外部输入,他们可能会通过在论坛、社交媒体和其他公共场所发布特制链接来利用此漏洞,以吸引用户单击它。
博士:应采取预防措施,以避免意外将机密发布到公共 npm 注册表的风险。文件可用于忽略特定文件或文件夹,或者 中的数组可以充当允许列表。
.npmignore
files
package.json
否则:项目的 API 密钥、密码或其他机密很容易被遇到它们的任何人滥用,这可能会导致经济损失、冒充和其他风险。
博士:使用你喜欢的工具(例如 或 npm-check-updates) 来检测已安装的过时包,将此检查注入 CI 管道,甚至在严重的情况下使构建失败。例如,当已安装的软件包落后 5 个补丁提交(例如,本地版本为 1.3.1,存储库版本为 1.3.8)或被其作者标记为已弃用时,严重的情况可能是 - 终止构建并阻止部署此版本
npm outdated
否则:你的作品将运行被其作者明确标记为有风险的包
🌟 #new
博士:使用“节点协议”语法导入或要求内置 Node.js 模块:
import { functionName } from "node:module"; // note that 'node:' prefix
例如:
import { createServer } from "node:http";
这种风格确保了全局 npm 包没有歧义,并使读者清楚地知道代码引用了一个受信任的官方模块。这种风格可以通过 eslint 规则“首选节点协议”强制执行
否则:使用不带“node:”前缀的导入语法为域名仿冒攻击打开了大门,在这种攻击中,人们可能会错误地输入模块名称(例如,“event”而不是“events”),并获得一个恶意包,该软件包只是为了诱骗用户安装它们而构建的
7. Draft: Performance Best Practices
博士:避免 CPU 密集型任务,因为它们会阻塞大部分单线程事件循环,并将其卸载到专用线程、进程甚至基于上下文的不同技术。
否则:由于事件循环被阻塞,Node.js将无法处理其他请求,从而导致并发用户延迟。3000 个用户正在等待响应,内容已准备好提供,但一个请求阻止服务器将结果调度回来
博士:使用像本机方法这样的实用程序库通常更受惩罚,因为它会导致不必要的依赖项和性能降低。请记住,随着新的 V8 引擎和新的 ES 标准的引入,本机方法得到了改进,现在它的性能比实用程序库高出约 50%。
lodash
underscore
否则:你必须维护性能较低的项目,在这些项目中,你可以简单地使用已经可用的项目,或者处理更多的行以换取更多的文件。
8. Docker Best Practices
博士:使用多阶段生成仅复制必要的生产项目。运行应用程序不需要大量构建时依赖项和文件。对于多阶段构建,可以在构建期间使用这些资源,而运行时环境仅包含必要的资源。多阶段构建是摆脱超重和安全威胁的简单方法。
否则:较大的映像将需要更长的时间来构建和交付,仅生成工具可能包含漏洞,并且仅用于构建阶段的机密可能会泄露。
FROM node:14.4.0 AS build
COPY . .
RUN npm ci && npm run build
FROM node:slim-14.4.0
USER node
EXPOSE 8080
COPY --from=build /home/node/app/dist /home/node/app/package.json /home/node/app/package-lock.json ./
RUN npm ci --production
CMD [ "node", "dist/app.js" ]
node
npm start
博士:用于启动应用,避免使用不会将操作系统信号传递给代码的 npm 脚本。这可以防止子进程、信号处理、正常关闭和僵尸进程出现问题
CMD ['node','server.js']
更新:从 npm 7 开始,npm 声称可以传递信号。我们关注并将相应地更新
否则:当未传递任何信号时,你的代码将永远不会收到有关关闭的通知。否则,它将失去正确关闭的机会,可能会丢失当前请求和/或数据
阅读更多:使用 node 命令引导容器,避免 npm start
博士:当使用 Docker 运行时编排器(例如 Kubernetes)时,直接调用 Node.js 进程,而无需中间进程管理器或复制进程的自定义代码(例如 PM2、集群模块)。运行时平台具有最高的数据和可见性,可用于做出放置决策 - 它最了解需要多少个进程,如何传播它们以及在发生崩溃时该怎么做
否则:由于缺乏资源,容器不断崩溃,进程管理器将无限期地重新启动。如果 Kubernetes 意识到这一点,它可以将其重新定位到另一个宽敞的实例。
TL;DR:包括一个筛选出常见机密文件和开发工件的文件。通过这样做,可以防止机密泄漏到映像中。作为奖励,构建时间将显着减少。另外,确保不要递归复制所有文件,而是明确选择应该复制到 Docker 的文件
.dockerignore
否则:常见的个人机密文件,如 ,将与有权访问映像的任何人共享(例如 Docker 存储库)
.env
.aws
.npmrc
博士:尽管在生成和测试生命周期中有时需要开发依赖项,但最终交付到生产的映像应该是最小的,并且与开发依赖项无关。这样做可以保证只提供必要的代码,并将潜在攻击(即攻击面)的数量降至最低。使用多阶段构建(请参阅专用项目符号)时,这可以通过先安装所有依赖项,最后运行来实现
npm ci --production
否则:许多臭名昭著的 npm 安全漏洞都是在开发包中发现的(例如 eslint-scope)
博士:处理进程 SIGTERM 事件并清理所有现有连接和资源。这应该在响应正在进行的请求时完成。在 Docker 化运行时中,关闭容器并不罕见,而是作为日常工作的一部分经常发生的事件。实现这一点需要一些深思熟虑的代码来编排几个移动部分:负载均衡器、保持活动连接、HTTP 服务器和其他资源
否则:立即死亡意味着无法回应成千上万的失望用户
博士:始终使用 Docker 和 JavaScript 运行时标志配置内存限制。需要 Docker 限制来做出深思熟虑的容器放置决策,需要 --v8 的标志 max-old-space 来按时启动 GC 并防止内存利用率不足。实际上,将 v8 的旧空间内存设置为略小于容器限制
否则:需要 docker 定义来执行深思熟虑的扩展决策并防止饿死其他公民。如果不定义 v8 的限制,它将利用容器资源 - 如果没有明确的说明,它在利用 ~50-60% 的主机资源时崩溃
博士:如果操作正确,从缓存重建整个 docker 映像几乎是即时的。更新较少的说明应该在 Dockerfile 的顶部,而不断变化的指令(如应用程序代码)应该在底部。
否则:Docker 构建将非常长,即使进行微小的更改也会消耗大量资源
latest
博士:指定显式映像摘要或版本控制标签,切勿参阅。开发人员经常被引导相信指定标记将为他们提供存储库中的最新映像,但事实并非如此。使用摘要可确保服务的每个实例都运行完全相同的代码。
latest
latest
此外,引用映像标记意味着基础映像可能会更改,因为确定性安装不能依赖映像标记。相反,如果需要确定性安装,则可以使用 SHA256 摘要来引用精确映像。
否则:新版本的基础映像可能会通过重大更改部署到生产中,从而导致意外的应用程序行为。
博士:大图像会导致更容易暴露在漏洞中并增加资源消耗。使用更精简的 Docker 映像(如 Slim 和 Alpine Linux 变体)可以缓解此问题。
否则:构建、推送和拉取映像需要更长的时间,恶意参与者可以使用未知的攻击媒介,并消耗更多资源。
🌟 #new
博士:避免机密从 Docker 构建环境中泄露。Docker 映像通常在多个环境中共享,例如 CI 和注册表,这些环境不像生产环境那样经过清理。一个典型的例子是 npm 令牌,它通常作为参数传递给 dockerfile。此令牌在需要后很长时间仍保留在映像中,并允许攻击者无限期访问专用 npm 注册表。这可以通过复制一个秘密文件,然后使用多阶段构建将其删除(当心,构建历史记录也应该被删除)或使用 Docker 构建工具包秘密功能来避免,该功能不会留下任何痕迹
.npmrc
否则:每个有权访问 CI 和 docker 注册表的人也将获得一些宝贵的组织机密作为奖励
博士:除了检查代码依赖项漏洞外,还会扫描交付到生产的最终映像。Docker 映像扫描程序检查代码依赖项,还会检查操作系统二进制文件。此 E2E 安全扫描覆盖了更多内容,并验证在构建过程中没有坏人注入坏东西。因此,建议将其作为部署前的最后一步运行。有一些免费的商业扫描仪也提供CI / CD插件
否则:你的代码可能完全没有漏洞。但是,由于应用程序通常使用的操作系统级二进制文件(例如OpenSSL,TarBall)的易受攻击版本,它仍然可能被黑客入侵。
博士:在容器中安装依赖项后,删除本地缓存。复制依赖项以加快将来的安装速度没有任何意义,因为不会有任何进一步的安装 - Docker 映像是不可变的。使用一行代码将数十MB(通常为图像大小的10-50%)剃掉
否则:由于文件永远不会被使用,将交付到生产中的图像的重量将增加 30%
博士:这是与 Node 没有直接关系的 Docker 建议的集合.js - Node 实现与任何其他语言没有太大区别。单击“阅读更多”以浏览。
🌟 #new
博士:检查 Dockerfile 是识别 Dockerfile 中与最佳实践不同的问题的重要步骤。通过使用专门的 Docker 棉绒检查潜在的缺陷,可以轻松识别性能和安全性改进,从而节省生产代码中浪费的无数时间或安全问题。
否则:Dockerfile 创建者错误地将 Root 作为生产用户,并且还使用了来自未知源存储库的映像。只需一个简单的棉绒就可以避免这种情况。
为了维护本指南并使其保持最新,我们在社区的帮助下不断更新和改进指南和最佳实践。如果你想为这个项目做出贡献,你可以关注我们的里程碑并加入工作组
所有翻译均由社区提供。我们很乐意在已完成、正在进行的或新的翻译方面获得任何帮助!
认识指导委员会成员 - 共同努力为项目提供指导和未来方向的人。此外,委员会的每位成员都领导一个在我们的 GitHub 项目下跟踪的项目。
独立节点.js与美国、欧洲和以色列的客户合作构建大规模 Node.js 应用程序的顾问。上述许多最佳实践最初是在 goldbergyoni.com 上发布的。在@goldbergyoni或 me@goldbergyoni.com 到达尤尼
全栈软件工程师/开发人员,专门从事安全性,DevOps / DevSecOps和ERP集成。
知道如何退出 Vim 并热爱架构、虚拟化和安全性的全栈开发人员。
如果你曾经想为开源做出贡献,那么现在就是你的机会!有关详细信息,请参阅贡献文档。
感谢这些为这个存储库做出贡献的优秀人士!
全栈开发人员和站点可靠性工程师,常驻新西兰,对Web应用程序安全感兴趣,并构建和构建Node.js应用程序以在全球范围内执行。
独立的全栈开发人员,喜欢运维和自动化。
JavaScript及其生态系统的深度专家 - React,Node.js,TypeScript,GraphQL,MongoDB,几乎所有涉及系统任何层的JS/JSON的东西 - 使用Web平台为世界上最知名的品牌构建产品。节点.js基金会的个人成员。