域
History
在 VM 上下文中创建的任何 Promise 不再拥有 .domain 属性。然而,它们的处理程序仍然在正确的域中执行,并且在主上下文中创建的 Promise 仍然拥有 .domain 属性。
Promise 的处理程序现在在链中第一个 promise 被创建时的域中调用。
稳定性:0 - 已弃用
此模块待弃用。 一旦替代 API 最终确定,此模块将被完全弃用。大多数开发人员不应该有理由使用此模块。绝对需要域提供的功能的用户可以暂时依赖它,但应该期望将来必须迁移到不同的解决方案。
域提供了一种将多个不同的 IO 操作作为单个组来处理的方法。如果注册到域的任何事件发射器或回调发出 'error' 事件或抛出错误,则域对象将被通知,而不是在 process.on('uncaughtException') 处理程序中丢失错误上下文,或导致程序立即以错误代码退出。
域错误处理程序不能替代在发生错误时关闭进程。
根据 JavaScript 中 throw 的工作原理,几乎从来没有办法安全地“从断点处继续”,而不泄露引用或创建某种其他未定义的脆弱状态。
响应抛出错误的最安全方法是关闭进程。当然,在正常的 Web 服务器中,可能有许多打开的连接,因为某人触发了错误而突然关闭这些连接是不合理的。
更好的方法是向触发错误的请求发送错误响应,同时让其他请求在正常时间内完成,并停止在该工作进程中监听新请求。
这样,domain 的使用与 cluster 模块携手并进,因为主进程可以在工作进程遇到错误时 fork 一个新的工作进程。对于扩展到多台机器的 Node.js 程序,终止代理或服务注册表可以注意到故障,并相应地做出反应。
例如,这不是一个好主意:
// XXX 警告!坏主意! const d = require('node:domain').create(); d.on('error', (er) => { // 错误不会使进程崩溃,但它做的更糟! // 虽然我们防止了进程突然重启,但如果这种情况发生,我们会泄露 // 大量资源。 // 这并不比 process.on('uncaughtException') 好! console.log(`error, but oh well ${er.message}`); }); d.run(() => { require('node:http').createServer((req, res) => { handleRequest(req, res); }).listen(PORT); });
通过使用域的上下文,以及将我们的程序分离到多个工作进程的弹性,我们可以更适当地反应,并以更大的安全性处理错误。
// 好多了! const cluster = require('node:cluster'); const PORT = +process.env.PORT || 1337; if (cluster.isPrimary) { // 更现实的场景会有超过 2 个工作进程, // 并且可能不会把主进程和工作进程放在同一个文件中。 // // 也可以把日志记录做得更复杂一点,并且 // 实现任何需要的自定义逻辑来防止 DoS // 攻击和其他不良行为。 // // 参见 cluster 文档中的选项。 // // 重要的是主进程做得很少, // 增加我们对意外错误的弹性。 cluster.fork(); cluster.fork(); cluster.on('disconnect', (worker) => { console.error('disconnect!'); cluster.fork(); }); } else { // 工作进程 // // 这里是我们要放 bug 的地方! const domain = require('node:domain'); // 参见 cluster 文档以获取更多关于使用 // 工作进程来服务请求的详情。它是如何工作的,注意事项等。 const server = require('node:http').createServer((req, res) => { const d = domain.create(); d.on('error', (er) => { console.error(`error ${er.stack}`); // 我们处于危险境地! // 根据定义,发生了一些意外的事情, // 这可能是我们不想要的。 // 现在什么都可能发生!非常小心! try { // 确保我们在 30 秒内关闭 const killtimer = setTimeout(() => { process.exit(1); }, 30000); // 但不要仅仅为此保持进程打开! killtimer.unref(); // 停止接受新请求。 server.close(); // 让主进程知道我们挂了。这将触发 // cluster 主进程中的 'disconnect',然后它将 fork // 一个新的工作进程。 cluster.worker.disconnect(); // 尝试向触发问题的请求发送错误 res.statusCode = 500; res.setHeader('content-type', 'text/plain'); res.end('Oops, there was a problem!\n'); } catch (er2) { // 好吧,此时我们做不了太多。 console.error(`Error sending 500! ${er2.stack}`); } }); // 因为 req 和 res 是在这个域存在之前创建的, // 我们需要显式地添加它们。 // 参见下方隐式与显式绑定的解释。 d.add(req); d.add(res); // 现在在域中运行处理函数。 d.run(() => { handleRequest(req, res); }); }); server.listen(PORT); } // 这部分不重要。只是一个路由示例。 // 在这里放复杂的应用逻辑。 function handleRequest(req, res) { switch (req.url) { case '/error': // 我们做一些异步事情,然后... setTimeout(() => { // 哎呀! flerb.bark(); }, timeout); break; default: res.end('ok'); } }
任何时候 Error 对象通过域路由,都会向其添加一些额外字段。
error.domain第一个处理错误的域。error.domainEmitter发出带有错误对象的'error'事件的事件发射器。error.domainBound绑定到域并将错误作为第一个参数传递的回调函数。error.domainThrown布尔值,指示错误是抛出的、发出的还是传递给绑定回调函数的。
如果正在使用域,则所有新的 EventEmitter 对象(包括 Stream 对象、请求、响应等)将在创建时隐式绑定到活动域。
此外,传递给低级事件循环请求(例如 fs.open() 或其他接受回调的方法)的回调将自动绑定到活动域。如果它们抛出,域将捕获错误。
为了防止过度内存使用,Domain 对象本身不会隐式添加为活动域的子项。如果是这样,那么防止请求和响应对象被正确垃圾回收就太容易了。
要将 Domain 对象作为父 Domain 的子项嵌套,必须显式添加它们。
隐式绑定将抛出的错误和 'error' 事件路由到 Domain 的 'error' 事件,但不在 Domain 上注册 EventEmitter。
隐式绑定仅处理抛出的错误和 'error' 事件。
有时,使用的域不应该是用于特定事件发射器的域。或者,事件发射器可能是在一个域的上下文中创建的,但应该绑定到另一个域。
例如,可能有一个域用于 HTTP 服务器,但也许我们想为每个请求使用一个单独的域。
这是可以通过显式绑定实现的。
// 为服务器创建一个顶层域 const domain = require('node:domain'); const http = require('node:http'); const serverDomain = domain.create(); serverDomain.run(() => { // 服务器在 serverDomain 的作用域中创建 http.createServer((req, res) => { // Req 和 res 也在 serverDomain 的作用域中创建 // 然而,我们更喜欢为每个请求拥有一个独立的域。 // 首先创建它,并将 req 和 res 添加进去。 const reqd = domain.create(); reqd.add(req); reqd.add(res); reqd.on('error', (er) => { console.error('Error', er, req.url); try { res.writeHead(500); res.end('Error occurred, sorry.'); } catch (er2) { console.error('Error sending 500', er2, req.url); } }); }).listen(1337); });
domain.create(): void
- 返回:
Domain
- 继承:
EventEmitter
Domain 类封装了将错误和未捕获异常路由到活动 Domain 对象的功能。
要处理它捕获的错误,请监听其 'error' 事件。
- 类型:
Array
一个已显式添加到域的事件发射器数组。
domain.add(emitter): void
EventEmitter显式地将一个发射器添加到域。如果发射器调用的任何事件处理程序抛出错误,或者发射器发出 'error' 事件,它将被路由到域的 'error' 事件,就像隐式绑定一样。
如果 EventEmitter 已经绑定到一个域,它将从该域移除,并绑定到这个域。
domain.bind(callback): void
Function返回的函数将是提供的回调函数的包装器。当调用返回的函数时,任何抛出的错误将被路由到域的 'error' 事件。
const d = domain.create(); function readSomeFile(filename, cb) { fs.readFile(filename, 'utf8', d.bind((er, data) => { // 如果这抛出,它也会被传递给域。 return cb(er, data ? JSON.parse(data) : null); })); } d.on('error', (er) => { // 某处发生了错误。如果我们现在抛出它,它将使程序崩溃 // 并显示正常的行号和栈消息。 });
domain.enter(): void
enter() 方法是由 run()、bind() 和 intercept() 方法使用的管道,用于设置活动域。它将 domain.active 和 process.domain 设置为该域,并隐式地将该域推入由域模块管理的域栈(详见 domain.exit() 了解域栈的详情)。调用 enter() 标志着绑定到域的异步调用和 I/O 操作链的开始。
调用 enter() 仅更改活动域,不更改域本身。enter() 和 exit() 可以在单个域上调用任意次数。
domain.exit(): void
exit() 方法退出当前域,将其从域栈中弹出。任何时候执行将要切换到不同异步调用链的上下文时,确保退出当前域很重要。调用 exit() 标志着绑定到域的异步调用和 I/O 操作链的结束或中断。
如果有多个嵌套域绑定到当前执行上下文,exit() 将退出该域内嵌套的任何域。
调用 exit() 仅更改活动域,不更改域本身。enter() 和 exit() 可以在单个域上调用任意次数。
domain.intercept(callback): void
Function此方法几乎与 domain.bind(callback) 相同。但是,除了捕获抛出的错误外,它还将拦截作为第一个参数发送给函数的 Error 对象。
这样,常见的 if (err) return callback(err); 模式可以被单个地方的单个错误处理程序取代。
const d = domain.create(); function readSomeFile(filename, cb) { fs.readFile(filename, 'utf8', d.intercept((data) => { // 注意,第一个参数永远不会传递给 // 回调,因为它被假定为 'Error' 参数 // 因此被域拦截。 // 如果这抛出,它也会被传递给域 // 这样错误处理逻辑可以移动到 'error' // 域上的事件,而不是在整个程序中重复 // 程序。 return cb(null, JSON.parse(data)); })); } d.on('error', (er) => { // 某处发生了错误。如果我们现在抛出它,它将使程序崩溃 // 并显示正常的行号和栈消息。 });
domain.remove(emitter): void
EventEmitterdomain.add(emitter) 的反操作。从指定的发射器移除域处理。
domain.run(fn, ...args?): void
在域的上下文中运行提供的函数,隐式绑定所有在该上下文中创建的事件发射器、计时器和低级请求。可选地,参数可以传递给函数。
这是使用域的最基本方式。
const domain = require('node:domain'); const fs = require('node:fs'); const d = domain.create(); d.on('error', (er) => { console.error('Caught error!', er); }); d.run(() => { process.nextTick(() => { setTimeout(() => { // 模拟一些各种异步事情 fs.open('non-existent file', 'r', (er, fd) => { if (er) throw er; // 继续... }); }, 100); }); });
在此示例中,d.on('error') 处理程序将被触发,而不是使程序崩溃。
自 Node.js 8.0.0 起,Promise 的处理程序会在调用 .then() 或 .catch() 本身所在的域内运行:
const d1 = domain.create(); const d2 = domain.create(); let p; d1.run(() => { p = Promise.resolve(42); }); d2.run(() => { p.then((v) => { // 在 d2 中运行 }); });
可以使用 domain.bind(callback) 将回调绑定到特定的域:
const d1 = domain.create(); const d2 = domain.create(); let p; d1.run(() => { p = Promise.resolve(42); }); d2.run(() => { p.then(p.domain.bind((v) => { // 在 d1 中运行 })); });
域不会干扰 Promise 的错误处理机制。换句话说,对于未处理的 Promise 拒绝,不会发出 'error' 事件。