Skip to content

[历史]

版本变更
v8.8.0在 VM 上下文中创建的任何 Promise 都不再具有 .domain 属性。但是,它们的处理程序仍然在正确的域中执行,并且在主上下文中创建的 Promise 仍然拥有 .domain 属性。
v8.0.0Promise 的处理程序现在在创建链中第一个 Promise 的域中被调用。
v1.4.2自 v1.4.2 起已弃用

[稳定性: 0 - 已弃用]

稳定性: 0 稳定性: 0 - 已弃用

源代码: lib/domain.js

此模块即将弃用。一旦最终确定了替代 API,此模块将被完全弃用。大多数开发者不应该使用此模块。绝对必须使用域提供的功能的用户可以暂时依赖它,但应该预计将来需要迁移到不同的解决方案。

域提供了一种将多个不同的 IO 操作作为单个组进行处理的方法。如果注册到域的任何事件发射器或回调发出 'error' 事件,或抛出错误,则域对象将收到通知,而不是在 process.on('uncaughtException') 处理程序中丢失错误上下文,或导致程序立即以错误代码退出。

警告:请勿忽略错误!

域错误处理程序不能替代在发生错误时关闭进程。

由于 JavaScript 中throw 的工作方式,几乎不可能安全地“从中断处继续”,而不会泄漏引用或创建其他某种未定义的脆弱状态。

响应抛出错误的最安全方法是关闭进程。当然,在普通的 Web 服务器中,可能存在许多打开的连接,并且由于其他人触发的错误而突然关闭这些连接是不合理的。

更好的方法是向触发错误的请求发送错误响应,同时让其他请求在其正常时间内完成,并停止侦听该工作程序中的新请求。

通过这种方式,domain 的使用与 cluster 模块协同工作,因为主进程可以在工作程序遇到错误时派生一个新的工作程序。对于扩展到多台机器的 Node.js 程序,终止代理或服务注册表可以注意到故障并相应地做出反应。

例如,这不是一个好主意:

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)
})

通过使用域的上下文以及将我们的程序分成多个工作进程的弹性,我们可以更恰当地做出反应,并以更高的安全性处理错误。

js
// 好多了!

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 服务器可能使用一个域,但也许我们希望为每个请求使用一个单独的域。

这可以通过显式绑定来实现。

js
// 为服务器创建一个顶级域
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 类封装了将错误和未捕获的异常路由到活动 Domain 对象的功能。

要处理它捕获的错误,请监听其 'error' 事件。

domain.members

已显式添加到域的计时器和事件发射器的数组。

domain.add(emitter)

显式地将发射器添加到域。如果发射器调用的任何事件处理程序抛出错误,或者如果发射器发出 'error' 事件,它将被路由到域的 'error' 事件,就像隐式绑定一样。

这同样适用于从 setInterval()setTimeout() 返回的计时器。如果它们的回调函数抛出异常,则域 'error' 处理程序将捕获它。

如果计时器或 EventEmitter 已经绑定到一个域,它将从该域中移除,并绑定到这个域。

domain.bind(callback)

返回的函数将作为提供的回调函数的包装器。当调用返回的函数时,任何抛出的错误都将路由到域的 'error' 事件。

js
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.activeprocess.domain 设置为该域,并隐式地将该域推入由域模块管理的域堆栈(有关域堆栈的详细信息,请参阅 domain.exit())。对 enter() 的调用限定了绑定到域的一系列异步调用和 I/O 操作的开始。

调用 enter() 仅更改活动域,而不更改域本身。可以在单个域上任意多次调用 enter()exit()

domain.exit()

exit() 方法退出当前域,将其从域栈中弹出。任何时候,如果执行将切换到不同的异步调用链的上下文,确保退出当前域非常重要。exit() 的调用限定了与域绑定的异步调用和 I/O 操作链的结束或中断。

如果当前执行上下文绑定了多个嵌套域,exit() 将退出此域内的任何嵌套域。

调用 exit() 只会更改活动域,而不会更改域本身。可以在单个域上任意多次调用 enter()exit()

domain.intercept(callback)

此方法几乎与 domain.bind(callback) 相同。但是,除了捕获抛出的错误外,它还将拦截作为函数的第一个参数发送的 Error 对象。

通过这种方式,常见的 if (err) return callback(err); 模式可以用单个位置的单个错误处理程序替换。

js
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)

domain.add(emitter) 的反向操作。从指定的发射器中移除域处理。

domain.run(fn[, ...args])

在域的上下文中运行提供的函数,隐式绑定在该上下文中创建的所有事件发射器、计时器和低级请求。可以选择向函数传递参数。

这是使用域的最基本方法。

js
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() 本身所在的域中运行:

js
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) 将回调绑定到特定的域:

js
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' 事件。