Domain#

穩定度:0 - 已棄用

此模組正等待棄用。一旦替代 API 定案,此模組將被完全棄用。大多數開發者不應該有理由使用此模組。若使用者絕對需要 domain 提供的功能,目前可以依賴它,但應預期未來必須遷移至其他解決方案。

Domain 提供了一種將多個不同 IO 操作作為單一群組來處理的方法。如果註冊到 domain 的任何事件發射器(event emitter)或回呼函式觸發了 'error' 事件或拋出錯誤,則該 domain 物件會收到通知,而不是在 process.on('uncaughtException') 處理器中丟失錯誤上下文,或是導致程式因錯誤代碼而立即退出。

警告:不要忽略錯誤!#

Domain 的錯誤處理器並不能取代發生錯誤時關閉程序的操作。

基於 JavaScript 中 throw 運作的本質,幾乎沒有辦法在不洩漏參考或造成其他未定義脆弱狀態的情況下安全地「從錯誤發生處繼續」。

回應拋出錯誤最安全的方式是關閉程序。當然,在正常的網頁伺服器中,可能存在許多開啟的連線,因為某人的錯誤而突然關閉這些連線是不合理的。

更好的方法是向觸發錯誤的請求發送錯誤回應,同時讓其他請求在正常時間內完成,並停止在該工作進程(worker)中監聽新請求。

透過這種方式,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);
});

透過使用 domain 的上下文,以及將程式分離為多個工作進程的彈性,我們可以更適當地反應,並以更高的安全性處理錯誤。

// 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 物件通過 domain 路由時,會向其添加一些額外欄位。

  • error.domain 首先處理該錯誤的 domain。
  • error.domainEmitter 發出帶有該錯誤物件之 'error' 事件的事件發射器。
  • error.domainBound 綁定到 domain 的回呼函式,並接收錯誤作為其第一個參數。
  • error.domainThrown 一個布林值,指示該錯誤是被拋出、發出,還是傳遞給綁定的回呼函式。

隱式綁定#

如果正在使用 domain,則所有EventEmitter 物件(包括 Stream 物件、請求、回應等)將在建立時隱式綁定到當前活動的 domain。

此外,傳遞給低階事件迴圈請求(例如 fs.open() 或其他接收回呼的方法)的回呼函式將自動綁定到當前活動的 domain。如果它們拋出錯誤,domain 將會捕獲該錯誤。

為了防止過度的記憶體使用,Domain 物件本身不會被隱式添加為活動 domain 的子項。如果這樣做,將很容易導致請求和回應物件無法被正確的垃圾回收。

若要將 Domain 物件巢狀化為父層 Domain 的子項,必須顯式添加它們。

隱式綁定會將拋出的錯誤和 'error' 事件路由到 Domain'error' 事件,但不會在 Domain 上註冊該 EventEmitter。隱式綁定僅處理拋出的錯誤和 'error' 事件。

顯式綁定#

有時,正在使用的 domain 可能不是特定事件發射器應該使用的那個。或者,事件發射器可能是在一個 domain 的上下文中建立的,但應該綁定到另一個 domain。

例如,HTTP 伺服器可以使用一個 domain,但我們可能希望為每個請求使用一個獨立的 domain。

這可以透過顯式綁定來實現。

// 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 的事件發射器。

domain.add(emitter)#

顯式將發射器添加到 domain。如果發射器呼叫的任何事件處理器拋出錯誤,或者如果發射器發出 'error' 事件,它將被路由到 domain 的 'error' 事件,就像隱式綁定一樣。

如果 EventEmitter 已經綁定到另一個 domain,它會從那個 domain 移除,並改為綁定到此 domain。

domain.bind(callback)#

回傳的函式將是所提供回呼函式的包裝器。當呼叫回傳的函式時,任何拋出的錯誤都將路由到該 domain 的 '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。它將 domain.activeprocess.domain 設定為此 domain,並隱式地將 domain 推入由 domain 模組管理的 domain 堆疊中(關於 domain 堆疊的詳細資訊請參見 domain.exit())。對 enter() 的呼叫劃定了綁定到 domain 的非同步呼叫鏈與 I/O 操作的開端。

呼叫 enter() 只會更改活動 domain,不會改變 domain 本身。enter()exit() 可以在單一 domain 上呼叫任意次數。

domain.exit()#

exit() 方法退出當前 domain,將其從 domain 堆疊中彈出。每當執行切換到不同非同步呼叫鏈的上下文時,確保退出當前 domain 非常重要。對 exit() 的呼叫劃定了綁定到 domain 的非同步呼叫鏈與 I/O 操作的結束或中斷。

如果有多個巢狀 domain 綁定到當前執行上下文,exit() 將退出此 domain 內的所有巢狀 domain。

呼叫 exit() 只會更改活動 domain,不會改變 domain 本身。enter()exit() 可以在單一 domain 上呼叫任意次數。

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 處理。

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

在 domain 的上下文中執行所提供的函式,隱式綁定該上下文中建立的所有事件發射器、計時器和低階請求。可選擇性地向函式傳遞參數。

這是使用 domain 最基本的方式。

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') 處理器,而不是使程式崩潰。

Domain 與 Promise#

從 Node.js 8.0.0 開始,promise 的處理器會在執行 .then().catch() 呼叫所在的 domain 內執行。

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) 綁定到特定 domain。

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

Domain 不會干擾 promise 的錯誤處理機制。換句話說,不會為未處理的 Promise 拒絕事件發出 'error' 事件。