跳到主要内容

3 篇博文 含有标签「Node.js」

查看所有标签

· 阅读需 4 分钟

首先,请死记硬背Node事件循环的 6个阶段

  1. timers 执行setTimeout和setInterval的callback
  2. pending callback 某些系统操作的callback,可以理解为除了其它回调以外的回调
  3. idle/prepare 内部用,不管
  4. poll 执行IO回调和轮询队列中的事件
  5. check 执行setImmediate的callback
  6. close callback 执行close事件的callback

Node事件循环中需要注意在执行上下文中当前处于哪个阶段

setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});

比如这段代码如果从主模块中调用两者,那么时间将受到进程性能的限制。其结果也不一致。这是由于setTimeout本身在超时后才进入队列的机制决定的,当事件循环启动时,定时任务可能还没进入timers队列,从而错过了该轮循环,导致落后于setImmediate

const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});

如果在IO回调即poll阶段调用,那么始终是immediate在前面timeout在后面

nexttick & 微任务 & 宏任务

nexttick即node的process.nextTick,网上有的教程把nexttick笼统的归入微任务,是不合理的,因为nexttick是优先于微任务执行的,如果强行归入微任务的话,那也是微任务中优先级最高的,那这样就需要把微任务队列理解为一个优先级队列,且这个优先级只是单纯为nexttick服务的,增加了理解的复杂度,所以最好还是把nexttick与微任务区分开来,单独理解为一个队列

微任务 在node中只有promise

宏任务 指的就是node中的event loop在node中严格来说是是没宏任务这个概念的,浏览器中倒是有宏任务这个概念,不过为了对标浏览器,减小理解差异性,所以把node中的event loop讲成了宏任务

每个宏任务开始前都会把nexttick队列和微任务队列清空,顺序是nexttick队列优先于微任务队列,所以理论上是可以做到在nexttick/微任务执行过程中不断插入新的nexttick/微任务从而导致无法进入事件循环的

虽说node的执行是由事件循环驱动的,但是不要天真的以为node只有事件循环,不然同步代码放哪里执行?

所以综上所述,node执行代码的顺序是同步代码 → nexttick队列 → 微任务队列 → 事件循环(7个阶段之一)→ nexttick队列 → 微任务队列 → 事件循环(7个阶段之一) → ...

· 阅读需 20 分钟

引子

在编写代码时,我们应该有一些方法将程序像连接水管一样连接起来 -- 当我们需要获取一些数据时,可以去通过"拧"其他的部分来达到目的。这也应该是 IO 应有的方式。 -- Doug McIlroy. October 11, 1964

早先的 unix开始,stream 便开始进入了人们的视野,在过去的几十年的时间里,它被证明是一种可依赖的编程方式,它可以将一个大型的系统拆成一些很小的部分,并且让这些部分之间完美地进行合作。在 unix 中,我们可以使用|符号来实现流。在 node 中,node 内置的stream 模块已经被多个核心模块使用,同时也可以被用户自定义的模块使用。和 unix 类似,node 中的流模块的基本操作符叫做.pipe(),同时你也可以使用一个后压机制来应对那些对数据消耗较慢的对象。

在 node 中,流可以帮助我们将事情的重点分为几份,因为使用流可以帮助我们将实现接口的部分分割成一些连续的接口,这些接口都是可重用的。接着,你可以将一个流的输出口接到另一个流的输入口,然后使用使用一些库来对流实现高级别的控制。

对于小型程序设计(small-program design)以及 unix 哲学来说,流都是一个重要的组成部分,但是除此之外还有一些重要的事情值得我们思考。永远要记得:双鸟在林不如一鸟在手。

为什么应该使用流

在 node 中,I/O 都是异步的,所以在和硬盘以及网络的交互过程中会涉及到传递回调函数的过程。你之前可能会写出这样的代码:

var http = require("http");
var fs = require("fs");

var server = http.createServer(function(req, res) {
fs.readFile(__dirname + "/data.txt", function(err, data) {
res.end(data);
});
});
server.listen(8000);

上面的这段代码并没有什么问题,但是在每次请求时,我们都会把整个data.txt文件读入到内存中,然后再把结果返回给客户端。想想看,如果data.txt文件非常大,在响应大量用户的并发请求时,程序可能会消耗大量的内存,这样很可能会造成用户连接缓慢的问题。

其次,上面的代码可能会造成很不好的用户体验,因为用户在接收到任何的内容之前首先需要等待程序将文件内容完全读入到内存中。

所幸的是,(req,res)参数都是流对象,这意味着我们可以使用一种更好的方法来实现上面的需求:

var http = require("http");
var fs = require("fs");

var server = http.createServer(function(req, res) {
var stream = fs.createReadStream(__dirname + "/data.txt");
stream.pipe(res);
});
server.listen(8000);

在这里,.pipe()方法会自动帮助我们监听dataend事件。上面的这段代码不仅简洁,而且data.txt文件中每一小段数据都将源源不断的发送到客户端。

除此之外,使用.pipe()方法还有别的好处,比如说它可以自动控制后端压力,以便在客户端连接缓慢的时候 node 可以将尽可能少的缓存放到内存中。

想要将数据进行压缩?我们可以使用相应的流模块完成这项工作!

var http = require("http");
var fs = require("fs");
var oppressor = require("oppressor");

var server = http.createServer(function(req, res) {
var stream = fs.createReadStream(__dirname + "/data.txt");
stream.pipe(oppressor(req)).pipe(res);
});
server.listen(8000);

通过上面的代码,我们成功的将发送到浏览器端的数据进行了 gzip 压缩。我们只是使用了一个 oppressor 模块来处理这件事情。

一旦你学会使用流 api,你可以将这些流模块像搭乐高积木或者像连接水管一样拼凑起来,从此以后你可能再也不会去使用那些没有流 API 的模块获取和推送数据了。

流模块基础

在 node 中,一共有五种类型的流:readable,writable,transform,duplex 以及"classic"

pipe

无论哪一种流,都会使用.pipe()方法来实现输入和输出。

.pipe()函数很简单,它仅仅是接受一个源头src并将数据输出到一个可写的流dst中:

src.pipe(dst);

.pipe(dst)将会返回dst因此你可以链式调用多个流:

a.pipe(b)
.pipe(c)
.pipe(d);

上面的代码也可以等价为:

a.pipe(b);
b.pipe(c);
c.pipe(d);

这和你在 unix 中编写流代码很类似:

a | b | c | d

只不过此时你是在 node 中编写而不是在 shell 中!

readable 流

Readable 流可以产出数据,你可以将这些数据传送到一个 writable,transform 或者 duplex 流中,只需要调用pipe()方法:

readableStream.pipe(dst);

创建一个 readable 流

现在我们就来创建一个 readable 流!

var Readable = require("stream").Readable;

var rs = new Readable();
rs.push("beep ");
rs.push("boop\n");
rs.push(null);

rs.pipe(process.stdout);

下面运行代码:

$ node read0.js
beep boop

在上面的代码中rs.push(null)的作用是告诉rs输出数据应该结束了。

需要注意的一点是我们在将数据输出到process.stdout之前已经将内容推送进 readable 流rs中,但是所有的数据依然是可写的。

这是因为在你使用.push()将数据推进一个 readable 流中时,一直要到另一个东西来消耗数据之前,数据都会存在一个缓存中。

然而,在更多的情况下,我们想要的是当需要数据时数据才会产生,以此来避免大量的缓存数据。

我们可以通过定义一个._read函数来实现按需推送数据:

var Readable = require("stream").Readable;
var rs = Readable();

var c = 97;
rs._read = function() {
rs.push(String.fromCharCode(c++));
if (c > "z".charCodeAt(0)) rs.push(null);
};

rs.pipe(process.stdout);

代码的运行结果如下所示:

$ node read1.js
abcdefghijklmnopqrstuvwxyz

在这里我们将字母az推进了 rs 中,但是只有当数据消耗者出现时,数据才会真正实现推送。

_read函数也可以获取一个size参数来指明消耗者想要读取多少比特的数据,但是这个参数是可选的。

需要注意到的是你可以使用util.inherit()来继承一个 Readable 流。

为了说明只有在数据消耗者出现时,_read函数才会被调用,我们可以将上面的代码简单的修改一下:

var Readable = require("stream").Readable;
var rs = Readable();

var c = 97 - 1;

rs._read = function() {
if (c >= "z".charCodeAt(0)) return rs.push(null);

setTimeout(function() {
rs.push(String.fromCharCode(++c));
}, 100);
};

rs.pipe(process.stdout);

process.on("exit", function() {
console.error("\n_read() called " + (c - 97) + " times");
});
process.stdout.on("error", process.exit);

运行上面的代码我们可以发现如果我们只请求 5 比特的数据,那么_read只会运行 5 次:

$ node read2.js | head -c5
abcde
_read() called 5 times

在上面的代码中,setTimeout很重要,因为操作系统需要花费一些时间来发送程序结束信号。

另外,process.stdout.on('error',fn)处理器也很重要,因为当head不再关心我们的程序输出时,操作系统将会向我们的进程发送一个SIGPIPE信号,此时process.stdout将会捕获到一个EPIPE错误。

上面这些复杂的部分在和操作系统相关的交互中是必要的,但是如果你直接和 node 中的流交互的话,则可有可无。

如果你创建了一个 readable 流,并且想要将任何的值推送到其中的话,确保你在创建流的时候指定了 objectMode 参数,Readable({ objectMode: true })

消耗一个 readable 流

大部分时候,将一个 readable 流直接 pipe 到另一种类型的流或者使用 through 或者 concat-stream 创建的流中,是一件很容易的事情。但是有时我们也会需要直接来消耗一个 readable 流。

process.stdin.on("readable", function() {
var buf = process.stdin.read();
console.dir(buf);
});

代码运行结果如下所示:

$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume0.js
<Buffer 61 62 63 0a>
<Buffer 64 65 66 0a>
<Buffer 67 68 69 0a>
null

当数据可用时,readable事件将会被触发,此时你可以调用.read()方法来从缓存中获取这些数据。

当流结束时,.read()将返回null,因为此时已经没有更多的字节可以供我们获取了。

你也可以告诉.read()方法来返回n个字节的数据。虽然所有核心对象中的流都支持这种方式,但是对于对象流来说这种方法并不可用。

下面是一个例子,在这里我们制定每次读取 3 个字节的数据:

process.stdin.on("readable", function() {
var buf = process.stdin.read(3);
console.dir(buf);
});

运行上面的例子,我们将获取到不完整的数据:

$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume1.js
<Buffer 61 62 63>
<Buffer 0a 64 65>
<Buffer 66 0a 67>

这是因为多余的数据都留在了内部的缓存中,因此这个时候我们需要告诉 node 我们还对剩下的数据感兴趣,我们可以使用.read(0)来完成这件事:

process.stdin.on("readable", function() {
var buf = process.stdin.read(3);
console.dir(buf);
process.stdin.read(0);
});

到现在为止我们的代码和我们所期望的一样了!

$ (echo abc; sleep 1; echo def; sleep 1; echo ghi) | node consume2.js
<Buffer 61 62 63>
<Buffer 0a 64 65>
<Buffer 66 0a 67>
<Buffer 68 69 0a>

我们也可以使用.unshift()方法来放置多余的数据。

使用unshift()方法能够防止我们进行不必要的缓存拷贝。在下面的代码中我们将创建一个分割新行的可读解析器:

var offset = 0;

process.stdin.on("readable", function() {
var buf = process.stdin.read();
if (!buf) return;
for (; offset < buf.length; offset++) {
if (buf[offset] === 0x0a) {
console.dir(buf.slice(0, offset).toString());
buf = buf.slice(offset + 1);
offset = 0;
process.stdin.unshift(buf);
return;
}
}
process.stdin.unshift(buf);
});

代码的运行结果如下所示:

$ tail -n +50000 /usr/share/dict/american-english | head -n10 | node lines.js
'hearties'
'heartiest'
'heartily'
'heartiness'
'heartiness\'s'
'heartland'
'heartland\'s'
'heartlands'
'heartless'
'heartlessly'

当然,已经有很多这样的模块比如 split 来帮助你完成这件事情,你完全不需要自己写一个。

writable 流

一个 writable 流指的是只能流进不能流出的流:

src.pipe(writableStream);

创建一个 writable 流

只需要定义一个._write(chunk,enc,next)函数,你就可以将一个 readable 流的数据释放到其中:

var Writable = require("stream").Writable;
var ws = Writable();
ws._write = function(chunk, enc, next) {
console.dir(chunk);
next();
};

process.stdin.pipe(ws);

代码运行结果如下所示:

$ (echo beep; sleep 1; echo boop) | node write0.js
<Buffer 62 65 65 70 0a>
<Buffer 62 6f 6f 70 0a>

第一个参数,chunk代表写进来的数据。

第二个参数enc代表编码的字符串,但是只有在opts.decodeStringfalse的时候你才可以写一个字符串。

第三个参数,next(err)是一个回调函数,使用这个回调函数你可以告诉数据消耗者可以写更多的数据。你可以有选择性的传递一个错误对象error,这时会在流实体上触发一个emit事件。

在从一个 readable 流向一个 writable 流传数据的过程中,数据会自动被转换为Buffer对象,除非你在创建 writable 流的时候制定了decodeStrings参数为false,Writable({decodeStrings: false})

如果你需要传递对象,需要指定objectMode参数为trueWritable({ objectMode: true })

向一个 writable 流中写东西

如果你需要向一个 writable 流中写东西,只需要调用.write(data)即可。

process.stdout.write('beep boop\n');

为了告诉一个 writable 流你已经写完毕了,只需要调用.end()方法。你也可以使用.end(data)在结束前再写一些数据。

var fs = require("fs");
var ws = fs.createWriteStream("message.txt");

ws.write("beep ");

setTimeout(function() {
ws.end("boop\n");
}, 1000);

运行结果如下所示:

$ node writing1.js
$ cat message.txt
beep boop

如果你需要调整内部缓冲区大小,那么需要在创建可写流对象时设置highWaterMark。在调用.write()方法返回 false 时,说明写入的数据大小超过了该值。

为了避免读写速率不匹配而造成内存上涨,可以监听drain事件,等待可写流内部缓存被清空再继续写入。

transform 流  

你可以将 transform 流想象成一个流的中间部分,它可以读也可写,但是并不保存数据,它只负责处理流经它的数据。

duplex 流

Duplex 流是一个可读也可写的流,就好像一个电话,可以接收也可以发送语音。一个 rpc 交换是一个 duplex 流的最好的例子。如果你看到过下面这样的代码:

a.pipe(b).pipe(a);

那么你需要处理的就是一个 duplex 流对象。

classic 流

Classic 流是一个古老的接口,最早出现在 node 0.4 中。虽然现在不怎么用,但是我们最好还是来了解一下它的工作原理。

无论何时,只要一个流对象注册了一个data监听器,它就会自动的切换到classic模式,并且根据旧 API 的方式运行。

classic readable 流

Classic readable 流只是一个事件发射器,当有数据消耗者出现时发射emit事件,当输出数据完毕时发射end事件。

我们可以同构检查stream.readable来检查一个 classic 流对象是否可读。

下面是一个简单的 readable 流对象的例子,程序的运行结果将会输出AJ

var Stream = require("stream");
var stream = new Stream();
stream.readable = true;

var c = 64;
var iv = setInterval(function() {
if (++c >= 75) {
clearInterval(iv);
stream.emit("end");
} else stream.emit("data", String.fromCharCode(c));
}, 100);

stream.pipe(process.stdout);

运行结果如下所示:

$ node classic0.js
ABCDEFGHIJ

为了从一个 classic readable 流中读取数据,你可以注册dataend监听器。下面是一个使用旧 readable 流方式从process.stdin中读取数据的例子:

process.stdin.on("data", function(buf) {
console.log(buf);
});
process.stdin.on("end", function() {
console.log("**END**");
});

运行结果如下所示:

$ (echo beep; sleep 1; echo boop) | node classic1.js
<Buffer 62 65 65 70 0a>
<Buffer 62 6f 6f 70 0a>
**END**

需要注意的一点是当你在一个流对象上注册了一个data监听器,你就将这个流放在了兼容模式下,此时你不能使用两个 stream2 的 api。

如果你自己创建流对象,永远不要绑定dataend监听器。如果你需要和旧版本的流兼容,最好使用第三方库来实现.pipe()方法。

例如,你可以使用 through 模块来避免显式的使用dataend监听器:

var through = require("through");
process.stdin.pipe(through(write, end));

function write(buf) {
console.log(buf);
}
function end() {
console.log("**END**");
}

程序运行结果如下所示:

$ (echo beep; sleep 1; echo boop) | node through.js
<Buffer 62 65 65 70 0a>
<Buffer 62 6f 6f 70 0a>
**END**

你也可以使用 concat-stream 模块来将整个流的内容缓存起来:

var concat = require("concat-stream");
process.stdin.pipe(
concat(function(body) {
console.log(JSON.parse(body));
})
);

程序运行结果如下所示:

$ echo '{"beep":"boop"}' | node concat.js
{ beep: 'boop' }

Classic readable 流拥有.pause().resume()逻辑来暂停一个流,但是这都是可选的。如果你想要使用.pause().resume()方法,你应该使用 through 模块来帮助你处理缓存。

classic writable 流

Classic writable 流非常简单。其中只定义了.write(buf).end(buf),以及.desctory()方法。其中.end(buf)的参数 buf 是可选参数,但是一般来说 node 程序员还是喜欢使用.end(buf)这种写法。

接下来读什么

  • node 核心 stream 模块文档
  • 你可以使用readable-stream模块来确保你的 stream2 代码兼容 node 0.8 及其之前的代码。在你npm install readable-stream之后直接require('readable-stream')而不要require('stream')

本文参考自 stream-handbook,原文地址https://github.com/substack/stream-handbook

· 阅读需 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 请求 - 少数应用程序将崩溃。在这一点上, 我们可以只是重复发送相同的请求, 就可以轻松地搞垮应用程序。