#

稳定性: 0 - 已弃用

源代码: lib/domain.js

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

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

警告:不要忽略错误!#

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

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

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

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

这样,domain 的使用与 cluster 模块紧密结合,因为主进程可以在工作进程遇到错误时 fork 一个新的工作进程。 对于扩展到多台机器的 Node.js 程序,终止代理或服务注册表可以注意失败,并做出相应的反应。

例如,这不是一个好主意

// XXX WARNING! BAD IDEA!

const d = require('node:domain').create();
d.on('error', (er) => {
  // The error won't crash the process, but what it does is worse!
  // Though we've prevented abrupt process restarting, we are leaking
  // a lot of resources if this ever happens.
  // This is no better than process.on('uncaughtException')!
  console.log(`error, but oh well ${er.message}`);
});
d.run(() => {
  require('node:http').createServer((req, res) => {
    handleRequest(req, res);
  }).listen(PORT);
}); 

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

// Much better!

const cluster = require('node:cluster');
const PORT = +process.env.PORT || 1337;

if (cluster.isPrimary) {
  // A more realistic scenario would have more than 2 workers,
  // and perhaps not put the primary and worker in the same file.
  //
  // It is also possible to get a bit fancier about logging, and
  // implement whatever custom logic is needed to prevent DoS
  // attacks and other bad behavior.
  //
  // See the options in the cluster documentation.
  //
  // The important thing is that the primary does very little,
  // increasing our resilience to unexpected errors.

  cluster.fork();
  cluster.fork();

  cluster.on('disconnect', (worker) => {
    console.error('disconnect!');
    cluster.fork();
  });

} else {
  // the worker
  //
  // This is where we put our bugs!

  const domain = require('node:domain');

  // See the cluster documentation for more details about using
  // worker processes to serve requests. How it works, caveats, etc.

  const server = require('node:http').createServer((req, res) => {
    const d = domain.create();
    d.on('error', (er) => {
      console.error(`error ${er.stack}`);

      // We're in dangerous territory!
      // By definition, something unexpected occurred,
      // which we probably didn't want.
      // Anything can happen now! Be very careful!

      try {
        // Make sure we close down within 30 seconds
        const killtimer = setTimeout(() => {
          process.exit(1);
        }, 30000);
        // But don't keep the process open just for that!
        killtimer.unref();

        // Stop taking new requests.
        server.close();

        // Let the primary know we're dead. This will trigger a
        // 'disconnect' in the cluster primary, and then it will fork
        // a new worker.
        cluster.worker.disconnect();

        // Try to send an error to the request that triggered the problem
        res.statusCode = 500;
        res.setHeader('content-type', 'text/plain');
        res.end('Oops, there was a problem!\n');
      } catch (er2) {
        // Oh well, not much we can do at this point.
        console.error(`Error sending 500! ${er2.stack}`);
      }
    });

    // Because req and res were created before this domain existed,
    // we need to explicitly add them.
    // See the explanation of implicit vs explicit binding below.
    d.add(req);
    d.add(res);

    // Now run the handler function in the domain.
    d.run(() => {
      handleRequest(req, res);
    });
  });
  server.listen(PORT);
}

// This part is not important. Just an example routing thing.
// Put fancy application logic here.
function handleRequest(req, res) {
  switch (req.url) {
    case '/error':
      // We do some async stuff, and then...
      setTimeout(() => {
        // Whoops!
        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 服务器,但也许我们希望为每个请求使用一个单独的域。

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

// Create a top-level domain for the server
const domain = require('node:domain');
const http = require('node:http');
const serverDomain = domain.create();

serverDomain.run(() => {
  // Server is created in the scope of serverDomain
  http.createServer((req, res) => {
    // Req and res are also created in the scope of serverDomain
    // however, we'd prefer to have a separate domain for each request.
    // create it first thing, and add req and res to it.
    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()#

类:Domain#

Domain 类封装了将错误和未捕获异常路由到活动 Domain 对象的功能。

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

domain.members#

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

domain.add(emitter)#

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

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

如果 Timer 或 EventEmitter 已经绑定到一个域,它将从该域中删除,并绑定到这个域。

domain.bind(callback)#

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

const d = domain.create();

function readSomeFile(filename, cb) {
  fs.readFile(filename, 'utf8', d.bind((er, data) => {
    // If this throws, it will also be passed to the domain.
    return cb(er, data ? JSON.parse(data) : null);
  }));
}

d.on('error', (er) => {
  // An error occurred somewhere. If we throw it now, it will crash the program
  // with the normal line number and stack message.
}); 

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); 模式可以用单个位置的单个错误处理程序替换。

const d = domain.create();

function readSomeFile(filename, cb) {
  fs.readFile(filename, 'utf8', d.intercept((data) => {
    // Note, the first argument is never passed to the
    // callback since it is assumed to be the 'Error' argument
    // and thus intercepted by the domain.

    // If this throws, it will also be passed to the domain
    // so the error-handling logic can be moved to the 'error'
    // event on the domain instead of being repeated throughout
    // the program.
    return cb(null, JSON.parse(data));
  }));
}

d.on('error', (er) => {
  // An error occurred somewhere. If we throw it now, it will crash the program
  // with the normal line number and stack message.
}); 

domain.remove(emitter)#

domain.add(emitter) 相反。 从指定的 emitter 中移除域处理。

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

在域的上下文中运行提供的函数,隐式绑定在该上下文中创建的所有事件触发器、计时器和底层请求。 可选地,可以将参数传递给该函数。

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

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(() => { // Simulating some various async stuff
      fs.open('non-existent file', 'r', (er, fd) => {
        if (er) throw er;
        // proceed...
      });
    }, 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) => {
    // running in 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) => {
    // running in d1
  }));
}); 

域不会干扰 promise 的错误处理机制。 换句话说,对于未处理的 Promise 拒绝,不会发出 'error' 事件。