跳到主要内容

Node.js 最佳实践

· 阅读需 64 分钟

欢迎! 首先您应该知道的三件事情:

1. 当您读到这里,实际上您读了很多关于 Node.js 的优秀文章 - 这是对 Node.js 最佳实践中排名最高的内容的总结和分享

2. 这里是最大的汇集,且每周都在增长 - 当前,超过 50 个最佳实现,样式指南,架构建议已经呈现。每天都有新的 issue 和 PR 被创建,以使这本在线书籍不断更新。我们很乐于见到您能在这里做出贡献,不管是修复一些代码的错误,或是提出绝妙的新想法。请查看我们的milestones

3. 大部分的条目包含额外的信息 - 大部分的最佳实践条目的旁边,您将发现 🔗Read More 链接,它将呈现给您示例代码,博客引用和更多信息




目录

  1. 项目结构实践 (5)
  2. 异常处理实践 (11)
  3. 编码规范实践 (12)
  4. 测试和总体质量实践 (8)
  5. 进入生产实践 (16)
  6. ⭐ 新: 安全实践(23)
  7. Performance Practices (coming soon)



1. 项目结构实践

1.1 组件式构建你的解决方案

TL;DR: 大型项目的最坏的隐患就是维护一个庞大的,含有几百个依赖的代码库 - 当开发人员准备整合新的需求的时候,这样一个庞然大物势必减缓了开发效率。反之,把您的代码拆分成组件,每一个组件有它自己的文件夹和代码库,并且确保每一个组件小而简单。查看正确的项目结构的例子请访问下面的 ‘更多’ 链接。

否则: 当编写新需求的开发人员逐步意识到他所做改变的影响,并担心会破坏其他的依赖模块 - 部署会变得更慢,风险更大。当所有业务逻辑没有被分开,这也会被认为很难扩展

🔗 更多: 组件结构



1.2 分层设计组件,保持 Express 在特定的区域

TL;DR: 每一个组件都应该包含'层级' - 一个专注的用于接入网络,逻辑,数据的概念。这样不仅获得一个清晰的分离考量,而且使仿真和测试系统变得异常容易。尽管这是一个普通的模式,但接口开发者易于混淆层级关系,比如把网络层的对象(Express req, res)传给业务逻辑和数据层 - 这会令您的应用彼此依赖,并且只能通过 Express 使用。

否则: 对于混淆了网络层和其它层的应用,将不易于测试,执行 CRON 的任务,其它非-Express 的调用者无法使用

🔗 更多: 应用分层



1.3 封装公共模块成为 NPM 的包

TL;DR: 由大量代码构成的一个大型应用中,贯彻全局的,比如日志,加密和其它类似的公共组件,应该进行封装,并暴露成一个私有的 NPM 包。这将使其在更多的代码库和项目中被使用变成了可能。

否则: 您将不得不重造部署和依赖的轮子

🔗 更多: 通过需求构建



1.4 分离 Express 'app' and 'server'

TL;DR: 避免定义整个Express应用在一个单独的大文件里, 这是一个不好的习惯 - 分离您的 'Express' 定义至少在两个文件中: API 声明(app.js) 和 网络相关(WWW)。对于更好的结构,是把你的 API 声明放在组件中。

否则: 您的 API 将只能通过 HTTP 的调用进行测试(慢,并且很难产生测试覆盖报告)。维护一个有着上百行代码的文件也不是一个令人开心的事情。

🔗 更多: 分离 Express 'app' and 'server'



1.5 使用易于设置环境变量,安全和分级的配置

TL;DR: 一个完美无瑕的配置安装应该确保 (a) 元素可以从文件中,也可以从环境变量中读取 (b) 密码排除在提交的代码之外 (c) 为了易于检索,配置是分级的。仅有几个包可以满足这样的条件,比如rc, nconfconfig

否则: 不能满足任意的配置要求将会使开发,运维团队,或者两者,易于陷入泥潭。

🔗 更多: 配置最佳实践




2. 错误处理最佳实践

2.1 使用 Async-Await 和 promises 用于异步错误处理

TL;DR: 使用回调的方式处理异步错误可能是导致灾难的最快的方式(a.k.a the pyramid of doom)。对您的代码来说,最好的礼物就是使用规范的 promise 库或 async-await 来替代,这会使其像 try-catch 一样更加简洁,具有熟悉的代码结构。

否则: Node.js 回调特性, function(err, response), 是导致不可维护代码的一个必然的方式。究其原因,是由于混合了随意的错误处理代码,臃肿的内嵌,蹩脚的代码模式。

🔗 更多: 避免回调



2.2 仅使用内建的错误对象

TL;DR: 很多人抛出异常使用字符串类型或一些自定义类型 - 这会导致错误处理逻辑和模块间的调用复杂化。是否您 reject 一个 promise,抛出异常或发出(emit)错误 - 使用内建的错误对象将会增加设计一致性,并防止信息的丢失。

否则: 调用某些模块,将不确定哪种错误类型会返回 - 这将会使恰当的错误处理更加困难。更坏的情况是,使用特定的类型描述错误,会导致重要的错误信息缺失,比如 stack trace!

🔗 更多: 使用内建错误对象



2.3 区分运行错误和程序设计错误

TL;DR: 运行错误(例如, API 接受到一个无效的输入)指的是一些已知场景下的错误,这类错误的影响已经完全被理解,并能被考虑周全的处理掉。同时,程序设计错误(例如,尝试读取未定义的变量)指的是未知的编码问题,影响到应用得当的重启。

否则: 当一个错误产生的时候,您总是得重启应用,但为什么要让 ~5000 个在线用户不能访问,仅仅是因为一个细微的,可以预测的,运行时错误?相反的方案,也不完美 – 当未知的问题(程序问题)产生的时候,使应用依旧可以访问,可能导致不可预测行为。区分两者会使处理更有技巧,并在给定的上下文下给出一个平衡的对策。

🔗 更多: 运行错误和程序设计错误



2.4 集中处理错误,不要在 Express 中间件中处理错误

TL;DR: 错误处理逻辑,比如给管理员发送邮件,日志应该封装在一个特定的,集中的对象当中,这样当错误产生的时候,所有的终端(例如 Express 中间件,cron 任务,单元测试)都可以调用。

否则: 错误处理的逻辑不放在一起将会导致代码重复和非常可能不恰当的错误处理。

🔗 更多: 集中处理错误



2.5 对 API 错误使用 Swagger 文档化

TL;DR: 让你的 API 调用者知道哪种错误会返回,这样他们就能完全的处理这些错误,而不至于系统崩溃。Swagger,REST API 的文档框架,通常处理这类问题。

否则: 任何 API 的客户端可能决定崩溃并重启,仅仅因为它收到一个不能处理的错误。注意:API 的调用者可能是你(在微服务环境中非常典型)。

🔗 更多: 使用 Swagger 记录错误



2.6 当一个特殊的情况产生,停掉服务是得体的

TL;DR: 当一个不确定错误产生(一个开发错误,最佳实践条款#3) - 这就意味着对应用运转健全的不确定。一个普通的实践将是建议仔细地重启进程,并使用一些‘启动器’工具,比如 Forever 和 PM2。

否则: 当一个未知的异常被抛出,意味着某些对象包含错误的状态(例如某个全局事件发生器由于某些内在的错误,不在产生事件),未来的请求可能失败或者行为异常。

🔗 更多: 停掉服务



2.7 使用一个成熟的日志工具提高错误的可见性

TL;DR: 一系列成熟的日志工具,比如 Winston,Bunyan 和 Log4J,会加速错误的发现和理解。忘记 console.log 吧。

否则: 浏览 console 的 log,和不通过查询工具或者一个好的日志查看器,手动浏览繁琐的文本文件,会使你忙于工作到很晚。

🔗 更多: 使用好用的日志工具



2.8 使用你最喜欢的测试框架测试错误流

TL;DR: 无论专业的自动化测试或者简单的手动开发测试 - 确保您的代码不仅满足正常的场景,而且处理并且返回正确的错误。测试框架,比如 Mocha & Chai 可以非常容易的处理这些问题(在"Gist popup"中查看代码实例) 。

否则: 没有测试,不管自动还是手动,您不可能依赖代码去返回正确的错误。而没有可以理解的错误,那将毫无错误处理可言。

🔗 更多: 测试错误流向



2.9 使用 APM 产品发现错误和宕机时间

TL;DR: 监控和性能产品 (别名 APM) 先前一步的检测您的代码库和 API,这样他们能自动的,像使用魔法一样的强调错误,宕机和您忽略的性能慢的部分。

否则: 您花了很多的力气在测量 API 的性能和错误,但可能您从来没有意识到真实场景下您最慢的代码块和他们对 UX 的影响。

🔗 更多: 使用 APM 产品



2.10 捕获未处理的 promise rejections

TL;DR: 任何在 promise 中被抛出的异常将被收回和遗弃,除非开发者没有忘记去明确的处理。即使您的代码调用的是 process.uncaughtException!解决这个问题可以注册到事件 process.unhandledRejection。

否则: 您的错误将被回收,无踪迹可循。没有什么可以需要考虑。

🔗 更多: 捕获未处理的 promise rejection



2.11 快速查错,验证参数使用一个专门的库

TL;DR: 这应该是您的 Express 最佳实践中的一部分 – assert API 输入避免难以理解的漏洞,这类漏洞以后会非常难以追踪。而验证代码通常是一件乏味的事情,除非使用一些非常炫酷的帮助库比如 Joi。

否则: 考虑这种情况 – 您的功能期望一个数字参数 “Discount” ,然而调用者忘记传值,之后在您的代码中检查是否 Discount!=0 (允许的折扣值大于零),这样它将允许用户使用一个折扣。OMG,多么不爽的一个漏洞。你能明白吗?

🔗 更多: 快速查错




3. 编码风格实践

3.1 使用 ESLint

TL;DR: ESLint是检查可能的代码错误和修复代码样式的事实上的标准,不仅可以识别实际的间距问题, 而且还可以检测严重的反模式代码, 如开发人员在不分类的情况下抛出错误。尽管 ESlint 可以自动修复代码样式,但其他的工具比如prettierbeautify在格式化修复上功能强大,可以和 Eslint 结合起来使用。

否则: 开发人员将必须关注单调乏味的间距和线宽问题, 并且时间可能会浪费在过多考虑项目的代码样式。



3.2 Node.js 特定的插件

TL;DR: 除了仅仅涉及 vanilla JS 的 ESLint 标准规则,添加 Node 相关的插件,比如eslint-plugin-node, eslint-plugin-mocha and eslint-plugin-node-security

否则: 许多错误的 Node.js 代码模式可能在检测下逃生。例如,开发人员可能需要某些文件,把一个变量作为路径名 (variableAsPath) ,这会导致攻击者可以执行任何 JS 脚本。Node.JS linters 可以检测这类模式,并及早预警。



3.3 在同一行开始一个代码块的大括号

TL;DR: 代码块的第一个大括号应该和声明的起始保持在同一行中。

代码示例

// 建议
function someFunction() {
// 代码块
}

// 避免
function someFunction() {
// 代码块
}

否则: 不遵守这项最佳实践可能导致意外的结果,在 Stackoverflow 的帖子中可以查看到,如下:

🔗 更多: "Why does a results vary based on curly brace placement?" (Stackoverflow)



3.4 不要忘记分号

TL;DR: 即使没有获得一致的认同,但在每一个表达式后面放置分号还是值得推荐的。这将使您的代码, 对于其他阅读代码的开发者来说,可读性,明确性更强。

否则: 在前面的章节里面已经提到,如果表达式的末尾没有添加分号,JavaScript 的解释器会在自动添加一个,这可能会导致一些意想不到的结果。



3.5 命名您的方法

TL;DR: 命名所有的方法,包含闭包和回调, 避免匿名方法。当剖析一个 node 应用的时候,这是特别有用的。命名所有的方法将会使您非常容易的理解内存快照中您正在查看的内容。

否则: 使用一个核心 dump(内存快照)调试线上问题,会是一项非常挑战的事项,因为你注意到的严重内存泄漏问题极有可能产生于匿名的方法。



3.6 变量、常量、函数和类的命名约定

TL;DR: 当命名变量和方法的时候,使用 lowerCamelCase ,当命名类的时候,使用 UpperCamelCase (首字母大写),对于常量,则 UPPERCASE 。这将帮助您轻松地区分普通变量/函数和需要实例化的类。使用描述性名称,但使它们尽量简短。

否则: JavaScript 是世界上唯一一门不需要实例化,就可以直接调用构造函数("Class")的编码语言。因此,类和函数的构造函数由采用 UpperCamelCase 开始区分。

代码示例

  // 使用UpperCamelCase命名类名
class SomeClassExample () {

// 常量使用const关键字,并使用lowerCamelCase命名
const config = {
key: 'value'
};

// 变量和方法使用lowerCamelCase命名
let someVariableExample = 'value';
function doSomething() {

}

}


3.7 使用 const 优于 let,废弃 var

TL;DR: 使用const意味着一旦一个变量被分配,它不能被重新分配。使用 const 将帮助您免于使用相同的变量用于不同的用途,并使你的代码更清晰。如果一个变量需要被重新分配,以在一个循环为例,使用let声明它。let 的另一个重要方面是,使用 let 声明的变量只在定义它的块作用域中可用。 var是函数作用域,不是块级作用域,既然您有 const 和 let 让您随意使用,那么不应该在 ES6 中使用 var

否则: 当经常更改变量时,调试变得更麻烦了。

🔗 更多: JavaScript ES6+: var, let, or const?



3.8 先 require, 而不是在方法内部

TL;DR: 在每个文件的起始位置,在任何函数的前面和外部 require 模块。这种简单的最佳实践,不仅能帮助您轻松快速地在文件顶部辨别出依赖关系,而且避免了一些潜在的问题。

否则: 在 Node.js 中,require 是同步运行的。如果从函数中调用它们,它可能会阻塞其他请求,在更关键的时间得到处理。另外,如果所 require 的模块或它自己的任何依赖项抛出错误并使服务器崩溃,最好尽快查明它,如果该模块在函数中 require 的,则可能不是这样的情况。



3.9 require 文件夹,而不是文件

TL;DR: 当在一个文件夹中开发库/模块,放置一个文件 index.js 暴露模块的 内部,这样每个消费者都会通过它。这将作为您模块的一个接口,并使未来的变化简单而不违反规则。

否则: 更改文件内部结构或签名可能会破坏与客户端的接口。

代码示例

// 建议
module.exports.SMSProvider = require("./SMSProvider");
module.exports.SMSNumberResolver = require("./SMSNumberResolver");

// 避免
module.exports.SMSProvider = require("./SMSProvider/SMSProvider.js");
module.exports.SMSNumberResolver = require("./SMSNumberResolver/SMSNumberResolver.js");


3.10 使用 === 操作符

TL;DR: 对比弱等于 ==,优先使用严格的全等于 =====将在它们转换为普通类型后比较两个变量。在 === 中没有类型转换,并且两个变量必须是相同的类型。

否则:== 操作符比较,不相等的变量可能会返回 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。



3.11 使用 Async Await, 避免回调

TL;DR: Node 8 LTS 现已全面支持异步等待。这是一种新的方式处理异步请求,取代回调和 promise。Async-await 是非阻塞的,它使异步代码看起来像是同步的。您可以给你的代码的最好的礼物是用 async-await 提供了一个更紧凑的,熟悉的,类似 try catch 的代码语法。

否则: 使用回调的方式处理异步错误可能是陷入困境最快的方式 - 这种方式必须面对不停地检测错误,处理别扭的代码内嵌,难以推理编码流。

🔗更多: async await 1.0 引导



3.12 使用 (=>) 箭头函数

TL;DR: 尽管使用 async-await 和避免方法作为参数是被推荐的, 但当处理那些接受 promise 和回调的老的 API 的时候 - 箭头函数使代码结构更加紧凑,并保持了根方法上的语义上下文 (例如 'this')。

否则: 更长的代码(在 ES5 方法中)更易于产生缺陷,并读起来很是笨重。

🔗 更多: 这是拥抱箭头函数的时刻




4. 测试和总体的质量实践

4.1 至少,编写 API(组件)测试

TL;DR: 大多数项目只是因为时间表太短而没有进行任何自动化测试,或者测试项目失控而正被遗弃。因此,优先从 API 测试开始,这是最简单的编写和提供比单元测试更多覆盖率的事情(你甚至可能不需要编码而进行 API 测试,像Postman。之后,如果您有更多的资源和时间,继续使用高级测试类型,如单元测试、DB 测试、性能测试等。

否则: 您可能需要花很长时间编写单元测试,才发现只有 20%的系统覆盖率。



4.2 使用一个 linter 检测代码问题

TL;DR: 使用代码 linter 检查基本质量并及早检测反模式。在任何测试之前运行它, 并将其添加为预提交的 git 钩子, 以最小化审查和更正任何问题所需的时间。也可在Section 3中查阅编码样式实践

否则: 您可能让一些反模式和易受攻击的代码传递到您的生产环境中。



4.3 仔细挑选您的持续集成(CI)平台

TL;DR: 您的持续集成平台(cicd)将集成各种质量工具(如测试、lint),所以它应该是一个充满活力的生态系统,包含各种插件。jenkins曾经是许多项目的默认选项,因为它有最大的社区,同时也是一个非常强大的平台,这样的代价是要求一个陡峭的学习曲线。如今,使用 SaaS 工具,比如CircleCI及其他,安装一套 CI 解决方案,相对是一件容易的事情。这些工具允许构建灵活的 CI 管道,而无需管理整个基础设施。最终,这是一个鲁棒性和速度之间的权衡 - 仔细选择您支持的方案。

否则: 一旦您需要一些高级定制,选择一些细分市场供应商可能会让您停滞不前。另一方面,伴随着 jenkins,可能会在基础设施设置上浪费宝贵的时间。

🔗 更多: 挑选 CI 平台



4.4 经常检查易受攻击的依赖

TL;DR: 即使是那些最有名的依赖模块,比如 Express,也有已知的漏洞。使用社区和商业工具,比如 🔗 npm audit ,集成在您的 CI 平台上,在每一次构建的时候都会被调用,这样可以很容易地解决漏洞问题。

否则: 在没有专用工具的情况下,使代码清除漏洞,需要不断地跟踪有关新威胁的在线出版物,相当繁琐。



4.5 测试标签化

TL;DR: 不同的测试必须运行在不同的情景:quick smoke,IO-less,当开发者保存或提交一个文件,测试应该启动;完整的端到端的测试通常运行在一个新的 pull request 被提交之后,等等。这可以通过对测试用例设置标签,比如关键字像#cold #api #sanity,来完成。这样您可以对您的测试集进行 grep,调用需要的子集。例如,这就是您通过Mocha仅仅调用 sanity 测试集所需要做的:mocha --grep 'sanity'。

否则: 运行所有的测试,包括执行数据库查询的几十个测试,任何时候开发者进行小的改动都可能很慢,这使得开发者不愿意运行测试。



4.6 检查测试覆盖率,它有助于识别错误的测试模式

TL;DR: 代码覆盖工具比如 Istanbul/NYC,很好用有 3 个原因:它是免费的(获得这份报告不需要任何开销),它有助于确定测试覆盖率降低的部分,以及最后但非最不重要的是它指出了测试中的不匹配:通过查看颜色标记的代码覆盖报告您可以注意到,例如,从来不会被测到的代码片段像 catch 语句(即测试只是调用正确的路径,而不调用应用程序发生错误时的行为)。如果覆盖率低于某个阈值,则将其设置为失败的构建。

否则: 当你的大部分代码没有被测试覆盖时,就不会有任何自动化的度量指标告诉你了。



4.7 检查过期的依赖包

TL;DR: 使用您的首选工具 (例如 “npm outdated” or npm-check-updates 来检测已安装的过期依赖包, 将此检查注入您的 CI 管道, 甚至在严重的情况下使构建失败。例如, 当一个已安装的依赖包滞后 5 个补丁时 (例如:本地版本是 1.3.1 的, 存储库版本是 1.3.8 的), 或者它被其作者标记为已弃用, 可能会出现严重的情况 - 停掉这次构建并防止部署此版本。

否则: 您的生产环境将运行已被其作者明确标记为有风险的依赖包



4.8 对于 e2e testing,使用 docker-compose

TL;DR: 端对端(e2e)测试包含现场数据,由于它依赖于很多重型服务如数据库,习惯被认为是 CI 过程中最薄弱的环节。Docker-compose 通过制定类似生产的环境,并使用一个简单的文本文件和简单的命令,轻松化解了这个问题。它为了 e2e 测试,允许制作所有相关服务,数据库和隔离网络。最后但并非最不重要的一点是,它可以保持一个无状态环境,该环境在每个测试套件之前被调用,然后立即消失。

否则: 没有 docker-compose,团队必须维护一个测试数据库在每一个测试环境上,包含开发机器,保持所有数据同步,这样测试结果不会因环境不同而不同。




5. 上线实践

5.1. 监控!

TL;DR: 监控是一种在顾客之前发现问题的游戏 – 显然这应该被赋予前所未有的重要性。考虑从定义你必须遵循的基本度量标准开始(我的建议在里面),到检查附加的花哨特性并选择解决所有问题的解决方案。市场已经淹没其中。点击下面的 ‘The Gist’ ,了解解决方案的概述。

否则: 错误 === 失望的客户. 非常简单.

🔗 更多: 监控!



5.2. 使用智能日志增加透明度

TL;DR: 日志可以是调试语句的一个不能说话的仓库,或者表述应用运行过程的一个漂亮仪表板的驱动。从第 1 天计划您的日志平台:如何收集、存储和分析日志,以确保所需信息(例如,错误率、通过服务和服务器等完成整个事务)都能被提取出来。

否则: 您最终像是面对一个黑盒,不知道发生了什么事情,然后你开始重新写日志语句添加额外的信息。

🔗 更多: 使用智能日志增加透明度



5.3. 委托可能的一切(例如:gzip,SSL)给反向代理

TL;DR: Node 处理 CPU 密集型任务,如 gzipping,SSL termination 等,表现糟糕。相反,使用一个 ‘真正’ 的中间件服务像 Nginx,HAProxy 或者云供应商的服务。

否则: 可怜的单线程 Node 将不幸地忙于处理网络任务,而不是处理应用程序核心,性能会相应降低。

🔗 更多: 委托可能的一切(例如:gzip,SSL)给反向代理



5.4. 锁住依赖

TL;DR: 您的代码必须在所有的环境中是相同的,但是令人惊讶的是,NPM 默认情况下会让依赖在不同环境下发生偏移 – 当在不同的环境中安装包的时候,它试图拿包的最新版本。克服这种问题可以利用 NPM 配置文件, .npmrc,告诉每个环境保存准确的(不是最新的)包的版本。另外,对于更精细的控制,使用 NPM “shrinkwrap”。*更新:作为 NPM5,依赖默认锁定。新的包管理工具,Yarn,也默认锁定。

否则: QA 测试通过的代码和批准的版本,在生产中表现不一致。更糟糕的是,同一生产集群中的不同服务器可能运行不同的代码。

🔗 更多: 锁住依赖



5.5. 使用正确的工具保护进程正常运行

TL;DR: 进程必须继续运行,并在失败时重新启动。对于简单的情况下,“重启”工具如 PM2 可能足够,但在今天的“Dockerized”世界 – 集群管理工具也值得考虑

否则: 运行几十个实例没有明确的战略和太多的工具(集群管理,docker,PM2)可能导致一个 DevOps 混乱

🔗 更多: 使用正确的工具保护进程正常运行



5.6. 利用 CPU 多核

TL;DR: 在基本形式上,node 应用程序运行在单个 CPU 核心上,而其他都处于空闲状态。复制 node 进程和利用多核,这是您的职责 – 对于中小应用,您可以使用 Node Cluster 和 PM2. 对于一个大的应用,可以考虑使用一些 Docker cluster(例如 k8s,ECS)复制进程或基于 Linux init system(例如 systemd)的部署脚本

否则: 您的应用可能只是使用了其可用资源中的 25% (!),甚至更少。注意,一台典型的服务器有 4 个或更多的 CPU,默认的 Node.js 部署仅仅用了一个 CPU(甚至使用 PaaS 服务,比如 AWS beanstalk,也一样)。

🔗 更多: 利用所有的 CPU



5.7. 创建一个“维护端点”

TL;DR: 在一个安全的 API 中暴露一组系统相关的信息,比如内存使用情况和 REPL 等等。尽管这里强烈建议依赖标准和作战测试工具,但一些有价值的信息和操作更容易使用代码完成。

否则: 您会发现,您正在执行许多“诊断部署” – 将代码发送到生产中,仅仅只为了诊断目的提取一些信息。

🔗 更多: 创建一个 '维护端点'



5.8. 使用 APM 产品发现错误和宕机时间

TL;DR: 监控和性能的产品(即 APM)先前一步地评估代码库和 API,自动的超过传统的监测,并测量在服务和层级上的整体用户体验。例如,一些 APM 产品可以突显导致最终用户负载过慢的事务,同时指出根本原因。

否则: 你可能会花大力气测量 API 性能和停机时间,也许你永远不会知道,真实场景下哪个是你最慢的代码部分,这些怎么影响用户体验。

🔗 更多: 使用 APM 产品发现错误和宕机时间



5.9. 使您的代码保持生产环境就绪

TL;DR: 在意识中抱着最终上线的想法进行编码,从第 1 天开始计划上线。这听起来有点模糊,所以我编写了一些与生产维护密切相关的开发技巧(点击下面的要点)

否则: 一个世界冠军级别的 IT/运维人员也不能拯救一个编码低劣的系统。

🔗 更多: 使您的代码保持生产环境就绪



5.10. 测量和保护内存使用

TL;DR: Node.js 和内存有引起争论的联系:V8 引擎对内存的使用有稍微的限制(1.4GB),在 node 的代码里面有内存泄漏的很多途径 – 因此监视 node 的进程内存是必须的。在小应用程序中,你可以使用 shell 命令周期性地测量内存,但在中等规模的应用程序中,考虑把内存监控建成一个健壮的监控系统。

否则: 您的内存可能一天泄漏一百兆,就像曾发生在沃尔玛的一样。

🔗 更多: 测量和保护内存使用



5.11. Node 外管理您的前端资源

TL;DR: 使用专门的中间件(nginx,S3,CDN)服务前端内容,这是因为在处理大量静态文件的时候,由于 node 的单线程模型,它的性能很受影响。

否则: 您的单个 node 线程将忙于传输成百上千的 html/图片/angular/react 文件,而不是分配其所有的资源为了其擅长的任务 – 服务动态内容

🔗 更多: Node 外管理您的前端资源



5.12. 保持无状态,几乎每天都要停下服务器

TL;DR: 在外部数据存储上,存储任意类型数据(例如用户会话,缓存,上传文件)。考虑间隔地停掉您的服务器或者使用 ‘serverless’ 平台(例如 AWS Lambda),这是一个明确的强化无状态的行为。

否则: 某个服务器上的故障将导致应用程序宕机,而不仅仅是停用故障机器。此外,由于依赖特定服务器,伸缩弹性会变得更具挑战性。

🔗 更多: 保持无状态,几乎每天都要停下服务器



5.13. 使用自动检测漏洞的工具

TL;DR: 即使是最有信誉的依赖项,比如 Express,会有使系统处于危险境地的已知漏洞(随着时间推移)。通过使用社区的或者商业工具,不时的检查漏洞和警告(本地或者 Github 上),这类问题很容易被抑制,有些问题甚至可以立即修补。

否则: 否则: 在没有专用工具的情况下,使代码清除漏洞,需要不断地跟踪有关新威胁的在线出版物。相当繁琐。

🔗 更多: 使用自动检测漏洞的工具



5.14. 在每一个 log 语句中指明 ‘TransactionId’

TL;DR: 在每一个请求的每一条 log 入口,指明同一个标识符,transaction-id: {某些值}。然后在检查日志中的错误时,很容易总结出前后发生的事情。不幸的是,由于 Node 异步的天性自然,这是不容易办到的,看下代码里面的例子

否则: 在没有上下文的情况下查看生产错误日志,这会使问题变得更加困难和缓慢去解决。

🔗 更多: 在每一个 log 语句中指明 ‘TransactionId’



5.15. 设置 NODE_ENV=production

TL;DR: 设置环境变量 NODE_ENV 为‘production’ 或者 ‘development’,这是一个是否激活上线优化的标志 - 很多 NPM 的包通过它来判断当前的环境,据此优化生产环境代码。

否则: 遗漏这个简单的属性可能大幅减弱性能。例如,在使用 Express 作为服务端渲染页面的时候,如果未设置 NODE_ENV,性能将会减慢大概三分之一!

🔗 更多: 设置 NODE_ENV=production



5.16. 设计自动化、原子化和零停机时间部署

TL;DR: 研究表明,执行许多部署的团队降低了严重上线问题的可能性。不需要危险的手动步骤和服务停机时间的快速和自动化部署大大改善了部署过程。你应该达到使用 Docker 结合 CI 工具,使他们成为简化部署的行业标准。

否则: 长时间部署 -> 线上宕机 & 和人相关的错误 -> 团队部署时不自信 -> 更少的部署和需求




6. 安全最佳实践

53 items

6.1. 拥护 linter 安全准则

TL;DR: 使用安全相关的 linter 插件,比如eslint-plugin-security,尽早捕获安全隐患或者问题,最好在编码阶段。这能帮助察觉安全的问题,比如使用 eval,调用子进程,或者根据字面含义(比如,用户输入)引入模块等等。点击下面‘更多’获得一个安全 linter 可以检测到的代码示例。

Otherwise: 在开发过程中, 可能一个直白的安全隐患, 成为生产环境中一个严重问题。此外, 项目可能没有遵循一致的安全规范, 而导致引入漏洞, 或敏感信息被提交到远程仓库中。

🔗 更多: Lint 规范



6.2. 使用中间件限制并发请求

TL;DR: DOS 攻击非常流行而且相对容易处理。使用外部服务,比如 cloud 负载均衡, cloud 防火墙, nginx, 或者(对于小的,不是那么重要的 app)一个速率限制中间件(比如express-rate-limit),来实现速率限制。

否则: 应用程序可能受到攻击, 导致拒绝服务, 在这种情况下, 真实用户会遭受服务降级或不可用。

🔗 更多: 实施速率限制



6.3 把机密信息从配置文件中抽离出来,或者使用包对其加密

TL;DR: 不要在配置文件或源代码中存储纯文本机密信息。相反, 使用诸如 Vault 产品、Kubernetes/Docker Secrets 或使用环境变量之类的安全管理系统。最后一个结果是, 存储在源代码管理中的机密信息必须进行加密和管理 (滚动密钥(rolling keys)、过期时间、审核等)。使用 pre-commit/push 钩子防止意外提交机密信息。

否则: 源代码管理, 即使对于私有仓库, 也可能会被错误地公开, 此时所有的秘密信息都会被公开。外部组织的源代码管理的访问权限将无意中提供对相关系统 (数据库、api、服务等) 的访问。

🔗 更多: 安全管理



6.4. 使用 ORM/ODM 库防止查询注入漏洞

TL;DR: 要防止 SQL/NoSQL 注入和其他恶意攻击, 请始终使用 ORM/ODM 或 database 库来转义数据或支持命名的或索引的参数化查询, 并注意验证用户输入的预期类型。不要只使用 JavaScript 模板字符串或字符串串联将值插入到查询语句中, 因为这会将应用程序置于广泛的漏洞中。所有知名的 Node.js 数据访问库(例如Sequelize, Knex, mongoose)包含对注入漏洞的内置包含措施。

否则: 未经验证或未脱敏处理的用户输入,可能会导致操作员在使用 MongoDB 进行 NoSQL 操作时进行注入, 而不使用适当的过滤系统或 ORM 很容易导致 SQL 注入攻击, 从而造成巨大的漏洞。

🔗 更多: 使用 ORM/ODM 库防止查询注入



6.5. 通用安全最佳实际集合

TL;DR: 这些是与 Node.js 不直接相关的安全建议的集合-Node 的实现与任何其他语言没有太大的不同。单击 "阅读更多" 浏览。

🔗 更多: 通用安全最佳实际



6.6. 调整 HTTP 响应头以加强安全性

TL;DR: 应用程序应该使用安全的 header 来防止攻击者使用常见的攻击方式,诸如跨站点脚本(XSS)、点击劫持和其他恶意攻击。可以使用模块,比如 helmet轻松进行配置。

否则: 攻击者可以对应用程序的用户进行直接攻击, 导致巨大的安全漏洞

🔗 更多: 在应用程序中使用安全的 header



6.7. 经常自动检查易受攻击的依赖库

TL;DR: 在 npm 的生态系统中, 一个项目有许多依赖是很常见的。在找到新的漏洞时, 应始终将依赖项保留在检查中。使用工具,类似于npm audit 或者 snyk跟踪、监视和修补易受攻击的依赖项。将这些工具与 CI 设置集成, 以便在将其上线之前捕捉到易受攻击的依赖库。

否则: 攻击者可以检测到您的 web 框架并攻击其所有已知的漏洞。

🔗 更多: 安全依赖



6.8. 避免使用 Node.js 的 crypto 库处理密码,使用 Bcrypt

TL;DR: 密码或机密信息(API 密钥)应该使用安全的哈希+salt 函数(如 "bcrypt")来存储, 因为性能和安全原因, 这应该是其 JavaScript 实现的首选。

否则: 在不使用安全功能的情况下,保存的密码或秘密信息容易受到暴力破解和字典攻击, 最终会导致他们的泄露。

🔗 更多: 使用 Bcrypt



6.9. 转义 HTML、JS 和 CSS 输出

TL;DR: 发送给浏览器的不受信任数据可能会被执行, 而不是显示, 这通常被称为跨站点脚本(XSS)攻击。使用专用库将数据显式标记为不应执行的纯文本内容(例如:编码、转义),可以减轻这种问题。

否则: 攻击者可能会将恶意的 JavaScript 代码存储在您的 DB 中, 然后将其发送给可怜的客户端。

🔗 更多: 转义输出



6.10. 验证传入的 JSON schemas

TL;DR: 验证传入请求的 body payload,并确保其符合预期要求, 如果没有, 则快速报错。为了避免每个路由中繁琐的验证编码, 您可以使用基于 JSON 的轻量级验证架构,比如jsonschema or joi

否则: 您疏忽和宽松的方法大大增加了攻击面, 并鼓励攻击者尝试许多输入, 直到他们找到一些组合, 使应用程序崩溃。

🔗 更多: 验证传入的 JSON schemas



6.11. 支持黑名单的 JWT

TL;DR: 当使用 JSON Web Tokens(例如, 通过Passport.js), 默认情况下, 没有任何机制可以从发出的令牌中撤消访问权限。一旦发现了一些恶意用户活动, 只要它们持有有效的标记, 就无法阻止他们访问系统。通过实现一个不受信任令牌的黑名单,并在每个请求上验证,来减轻此问题。

否则: 过期或错误的令牌可能被第三方恶意使用,以访问应用程序,并模拟令牌的所有者。

🔗 更多: 为 JSON Web Token 添加黑名单



6.12. 限制每个用户允许的登录请求

TL;DR: 一类保护暴力破解的中间件,比如express-brute,应该被用在 express 的应用中,来防止暴力/字典攻击;这类攻击主要应用于一些敏感路由,比如/admin 或者 /login,基于某些请求属性, 如用户名, 或其他标识符, 如正文参数等。

否则: 攻击者可以发出无限制的密码匹配尝试, 以获取对应用程序中特权帐户的访问权限。

🔗 更多: 限制登录频率



6.13. 使用非 root 用户运行 Node.js

TL;DR: Node.js 作为一个具有无限权限的 root 用户运行,这是一种普遍的情景。例如,在 Docker 容器中,这是默认行为。建议创建一个非 root 用户,并保存到 Docker 镜像中(下面给出了示例),或者通过调用带有"-u username" 的容器来代表此用户运行该进程。

否则: 在服务器上运行脚本的攻击者在本地计算机上获得无限制的权利 (例如,改变 iptable,引流到他的服务器上)

🔗 更多: 使用非 root 用户运行 Node.js



6.14. 使用反向代理或中间件限制负载大小

TL;DR: 请求 body 有效载荷越大, Node.js 的单线程就越难处理它。这是攻击者在没有大量请求(DOS/DDOS 攻击)的情况下,就可以让服务器跪下的机会。在边缘上(例如,防火墙,ELB)限制传入请求的 body 大小,或者通过配置express body parser仅接收小的载荷,可以减轻这种问题。

否则: 您的应用程序将不得不处理大的请求, 无法处理它必须完成的其他重要工作, 从而导致对 DOS 攻击的性能影响和脆弱性。

🔗 更多: 限制负载大小



6.15. 避免 JavaScript 的 eval 声明

TL;DR: eval 是邪恶的, 因为它允许在运行时执行自定义的 JavaScript 代码。这不仅是一个性能方面的问题, 而且也是一个重要的安全问题, 因为恶意的 JavaScript 代码可能来源于用户输入。应该避免的另一种语言功能是 new Function 构造函数。setTimeoutsetInterval 也不应该传入动态 JavaScript 代码。

否则: 恶意 JavaScript 代码查找传入 eval 或其他实时判断的 JavaScript 函数的文本的方法, 并将获得在该页面上 javascript 权限的完全访问权。此漏洞通常表现为 XSS 攻击。

🔗 更多: 避免 JavaScript 的 eval 声明



6.16. 防止恶意 RegEx 让 Node.js 的单线程过载执行

TL;DR: 正则表达式,在方便的同时,对 JavaScript 应用构成了真正的威胁,特别是 Node.js 平台。匹配文本的用户输入需要大量的 CPU 周期来处理。在某种程度上,正则处理是效率低下的,比如验证 10 个单词的单个请求可能阻止整个 event loop 长达 6 秒,并让 CPU 引火烧身。由于这个原因,偏向第三方的验证包,比如validator.js,而不是采用正则,或者使用safe-regex来检测有问题的正则表达式。

否则: 写得不好的正则表达式可能容易受到正则表达式 DoS 攻击的影响, 这将完全阻止 event loop。例如,流行的moment包在 2017 年的 11 月,被发现使用了错误的 RegEx 用法而易受攻击。

🔗 更多: 防止恶意正则



6.17. 使用变量避免模块加载

TL;DR: 避免通过作为参数的路径 requiring/importing 另一个文件, 原因是它可能源自用户输入。此规则可扩展为访问一般文件(即:fs.readFile())或使用来自用户输入的动态变量访问其他敏感资源。Eslint-plugin-security linter 可以捕捉这样的模式, 并尽早提前警告。

否则: 恶意用户输入可以找到用于获得篡改文件的参数, 例如, 文件系统上以前上载的文件, 或访问已有的系统文件。

🔗 更多: 安全地加载模块



6.18. 在沙箱中运行不安全代码

TL;DR: 当任务执行在运行时给出的外部代码时(例如, 插件), 使用任何类型的沙盒执行环境保护主代码,并隔离开主代码和插件。这可以通过一个专用的过程来实现 (例如:cluster.fork()), 无服务器环境或充当沙盒的专用 npm 包。

否则: 插件可以通过无限循环、内存超载和对敏感进程环境变量的访问等多种选项进行攻击

🔗 更多: 在沙箱中运行不安全代码



6.19. 使用子进程时要格外小心

TL;DR: 尽可能地避免使用子进程,如果您仍然必须这样做,验证和清理输入以减轻 shell 注入攻击。更喜欢使用 "child_process"。execFile 的定义将只执行具有一组属性的单个命令, 并且不允许 shell 参数扩展。倾向于使用child_process.execFile,从定义上来说,它将仅仅执行具有一组属性的单个命令,并且不允许 shell 参数扩展。

否则: 由于将恶意用户输入传递给未脱敏处理的系统命令, 直接地使用子进程可能导致远程命令执行或 shell 注入攻击。

🔗 更多: 处理子进程时要格外小心



6.20. 隐藏客户端的错误详细信息

TL;DR: 默认情况下, 集成的 express 错误处理程序隐藏错误详细信息。但是, 极有可能, 您实现自己的错误处理逻辑与自定义错误对象(被许多人认为是最佳做法)。如果这样做, 请确保不将整个 Error 对象返回到客户端, 这可能包含一些敏感的应用程序详细信息。

否则: 敏感应用程序详细信息(如服务器文件路径、使用中的第三方模块和可能被攻击者利用的应用程序的其他内部工作流)可能会从 stack trace 发现的信息中泄露。

🔗 更多: 隐藏客户端的错误详细信息



6.21. 对 npm 或 Yarn,配置 2FA

TL;DR: 开发链中的任何步骤都应使用 MFA(多重身份验证)进行保护, npm/Yarn 对于那些能够掌握某些开发人员密码的攻击者来说是一个很好的机会。使用开发人员凭据, 攻击者可以向跨项目和服务广泛安装的库中注入恶意代码。甚至可能在网络上公开发布。在 npm 中启用 2 因素身份验证(2-factor-authentication), 攻击者几乎没有机会改变您的软件包代码。

否则: Have you heard about the eslint developer who's password was hijacked?



6.22. 修改 session 中间件设置

TL;DR: 每个 web 框架和技术都有其已知的弱点-告诉攻击者我们使用的 web 框架对他们来说是很大的帮助。使用 session 中间件的默认设置, 可以以类似于X-Powered-Byheader 的方式向模块和框架特定的劫持攻击公开您的应用。尝试隐藏识别和揭露技术栈的任何内容(例如:Nonde.js, express)。

否则: 可以通过不安全的连接发送 cookie, 攻击者可能会使用会话标识来标识 web 应用程序的基础框架以及特定于模块的漏洞。

🔗 更多: cookie 和 session 安全



6.23. 通过显式设置进程应崩溃的情况,以避免 DOS 攻击

TL;DR: 当错误未被处理时, Node 进程将崩溃。即使错误被捕获并得到处理,许多最佳实践甚至建议退出。例如, Express 会在任何异步错误上崩溃 - 除非使用 catch 子句包装路由。这将打开一个非常惬意的攻击点, 攻击者识别哪些输入会导致进程崩溃并重复发送相同的请求。没有即时补救办法, 但一些技术可以减轻苦楚: 每当进程因未处理的错误而崩溃,都会发出警报,验证输入并避免由于用户输入无效而导致进程崩溃,并使用 catch 将所有路由处理包装起来,并在请求中出现错误时, 考虑不要崩溃(与全局发生的情况相反)。

否则: 这只是一个起到教育意义的假设: 给定许多 Node.js 应用程序, 如果我们尝试传递一个空的 JSON 正文到所有 POST 请求 - 少数应用程序将崩溃。在这一点上, 我们可以只是重复发送相同的请求, 就可以轻松地搞垮应用程序。