域
[历史]
版本 | 变更 |
---|---|
v8.8.0 | 在 VM 上下文中创建的任何 Promise 都不再具有 .domain 属性。但是,它们的处理程序仍然在正确的域中执行,并且在主上下文中创建的 Promise 仍然拥有 .domain 属性。 |
v8.0.0 | Promise 的处理程序现在在创建链中第一个 Promise 的域中被调用。 |
v1.4.2 | 自 v1.4.2 起已弃用 |
源代码: lib/domain.js
此模块即将弃用。一旦最终确定了替代 API,此模块将被完全弃用。大多数开发者不应该使用此模块。绝对必须使用域提供的功能的用户可以暂时依赖它,但应该预计将来需要迁移到不同的解决方案。
域提供了一种将多个不同的 IO 操作作为单个组进行处理的方法。如果注册到域的任何事件发射器或回调发出 'error'
事件,或抛出错误,则域对象将收到通知,而不是在 process.on('uncaughtException')
处理程序中丢失错误上下文,或导致程序立即以错误代码退出。
警告:请勿忽略错误!
域错误处理程序不能替代在发生错误时关闭进程。
由于 JavaScript 中throw
的工作方式,几乎不可能安全地“从中断处继续”,而不会泄漏引用或创建其他某种未定义的脆弱状态。
响应抛出错误的最安全方法是关闭进程。当然,在普通的 Web 服务器中,可能存在许多打开的连接,并且由于其他人触发的错误而突然关闭这些连接是不合理的。
更好的方法是向触发错误的请求发送错误响应,同时让其他请求在其正常时间内完成,并停止侦听该工作程序中的新请求。
通过这种方式,domain
的使用与 cluster 模块协同工作,因为主进程可以在工作程序遇到错误时派生一个新的工作程序。对于扩展到多台机器的 Node.js 程序,终止代理或服务注册表可以注意到故障并相应地做出反应。
例如,这不是一个好主意:
// XXX 警告!坏主意!
const d = require('node:domain').create()
d.on('error', er => {
// 错误不会使进程崩溃,但它所做的更糟!
// 虽然我们已经阻止了进程突然重启,但如果这种情况发生,我们将泄漏
// 很多资源。
// 这与 process.on('uncaughtException') 没有什么不同!
console.log(`错误,但没关系 ${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('断开连接!')
cluster.fork()
})
} else {
// 工作进程
//
// 这是我们放置错误的地方!
const domain = require('node:domain')
// 请参阅 cluster 文档,了解有关使用
// 工作进程来服务请求的更多详细信息。它的工作原理、注意事项等。
const server = require('node:http').createServer((req, res) => {
const d = domain.create()
d.on('error', er => {
console.error(`错误 ${er.stack}`)
// 我们处于危险区域!
// 根据定义,发生了某些意外事件,
// 这可能不是我们想要的。
// 现在任何事情都可能发生!请务必小心!
try {
// 确保我们在 30 秒内关闭
const killtimer = setTimeout(() => {
process.exit(1)
}, 30000)
// 但不要仅仅为了这个而保持进程打开!
killtimer.unref()
// 停止接收新请求。
server.close()
// 让主进程知道我们已死。这将触发
// cluster 主进程中的“disconnect”事件,然后它将派生
// 一个新的工作进程。
cluster.worker.disconnect()
// 尝试向触发问题的请求发送错误
res.statusCode = 500
res.setHeader('content-type', 'text/plain')
res.end('糟糕,出现问题了!\n')
} catch (er2) {
// 哦,好吧,我们现在什么也做不了了。
console.error(`发送 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
对象通过域进行路由时,都会向其添加一些额外的字段。
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('错误', er, req.url)
try {
res.writeHead(500)
res.end('发生错误,抱歉。')
} catch (er2) {
console.error('发送 500 错误', er2, req.url)
}
})
})
.listen(1337)
})
domain.create()
- 返回值: <Domain>
类: Domain
- 继承: <EventEmitter>
Domain
类封装了将错误和未捕获的异常路由到活动 Domain
对象的功能。
要处理它捕获的错误,请监听其 'error'
事件。
domain.members
已显式添加到域的计时器和事件发射器的数组。
domain.add(emitter)
emitter
<EventEmitter> | <Timer> 要添加到域的发射器或计时器
显式地将发射器添加到域。如果发射器调用的任何事件处理程序抛出错误,或者如果发射器发出 'error'
事件,它将被路由到域的 'error'
事件,就像隐式绑定一样。
这同样适用于从 setInterval()
和 setTimeout()
返回的计时器。如果它们的回调函数抛出异常,则域 'error'
处理程序将捕获它。
如果计时器或 EventEmitter
已经绑定到一个域,它将从该域中移除,并绑定到这个域。
domain.bind(callback)
callback
<Function> 回调函数- 返回值: <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()
enter()
方法是 run()
、bind()
和 intercept()
方法使用的管道,用于设置活动域。它将 domain.active
和 process.domain
设置为该域,并隐式地将该域推入由域模块管理的域堆栈(有关域堆栈的详细信息,请参阅 domain.exit()
)。对 enter()
的调用限定了绑定到域的一系列异步调用和 I/O 操作的开始。
调用 enter()
仅更改活动域,而不更改域本身。可以在单个域上任意多次调用 enter()
和 exit()
。
domain.exit()
exit()
方法退出当前域,将其从域栈中弹出。任何时候,如果执行将切换到不同的异步调用链的上下文,确保退出当前域非常重要。exit()
的调用限定了与域绑定的异步调用和 I/O 操作链的结束或中断。
如果当前执行上下文绑定了多个嵌套域,exit()
将退出此域内的任何嵌套域。
调用 exit()
只会更改活动域,而不会更改域本身。可以在单个域上任意多次调用 enter()
和 exit()
。
domain.intercept(callback)
callback
<Function> 回调函数- 返回值: <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)
emitter
<EventEmitter> | <Timer> 要从域中移除的发射器或计时器
domain.add(emitter)
的反向操作。从指定的发射器中移除域处理。
domain.run(fn[, ...args])
fn
<Function>...args
<any>
在域的上下文中运行提供的函数,隐式绑定在该上下文中创建的所有事件发射器、计时器和低级请求。可以选择向函数传递参数。
这是使用域的最基本方法。
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')
处理程序,而不是使程序崩溃。
域和 Promise
从 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'
事件。