#

穩定性: 0 - 廢棄

原始碼: lib/domain.js

該模組即將被棄用。 一旦替代 API 最終確定,該模組將被完全棄用。大多數開發者 應該有理由使用這個模組。對於絕對必須使用域所提供功能的使用者,可以暫時依賴它,但應預期未來需要遷移到不同的解決方案。

域提供了一種將多個不同的 IO 操作作為一個單獨的組來處理的方法。如果任何註冊到域的事件發射器或回撥函式觸發了一個 'error' 事件,或者丟擲了一個錯誤,那麼域物件將會收到通知,而不是在 process.on('uncaughtException') 處理程式中丟失錯誤的上下文,或者導致程式立即以一個錯誤碼退出。

警告:不要忽略錯誤!#

域錯誤處理程式不能替代在發生錯誤時關閉程序。

根據 JavaScript 中 throw 的工作原理,幾乎沒有任何方法可以安全地“從中斷的地方繼續”,而不會洩漏引用,或造成其他某種未定義的脆弱狀態。

響應丟擲錯誤的最安全方式是關閉程序。當然,在一個正常的 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 模組管理的域堆疊(有關域堆疊的詳細資訊,請參見 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) 相反。從指定的發射器中移除域處理。

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