我在閱讀NodeJS文件中讀出的19個套路

王下邀月熊發表於2016-11-12

雖然我已經用了三年多的NodeJS,也曾經以為自己對其無所不知。但是我好像從未有安靜的坐下來仔細地閱讀NodeJS的完整文件。如果有熟悉我的朋友應該知道,我之前已經看了HTML,DOM,Web APIs,CSS,SVG以及ECMAScript的文件,NodeJS是我這個系列的最後一個待翻閱的山峰。在閱讀文件的過程中我也發現了很多本來不知道的知識,我覺得我有必要分享給大家。不過文件更多的是平鋪直敘,因此我也以閱讀的順序列舉出我覺得需要了解的點。

querystring:可以用作通用解析器的模組

很多時候我們會從資料庫或其他地方得到這種奇怪格式的字串:name:Sophie;shape:fox;condition:new,一般來說我們會利用字串切割的方式來講字串劃分到JavaScript Object。不過querystring也是個不錯的現成的工具:

const weirdoString = `name:Sophie;shape:fox;condition:new`;
const result = querystring.parse(weirdoString, `;`, `:`);
// result:
// {
//   name: `Sophie`,
//   shape: `fox`,
//   condition: `new`,
// };

V8 Inspector

--inspect引數執行你的Node應用程式,它會反饋你某個URL。將該URL複製到Chrome中並開啟,你就可以使用Chrome DevTools來除錯你的Node應用程式啦。詳細的實驗可以參考這篇文章。不過需要注意的是,該引數仍然屬於實驗性質。

nextTick 與 setImmediate的區別

這兩貨的區別可能光從名字上還看不出來,我覺得應該給它們取個別名:

  • process.nextTick()應該為process.sendThisToTheStartOfTheQueue()
  • setImmediate應該為sendThisToTheEndOfTheQueue()

再說句不相關的,React中的Props應該為stuffThatShouldStayTheSameIfTheUserRefreshes,而State應該為stuffThatShouldBeForgottenIfTheUserRefreshes

Server.listen 可以使用Object作為引數

我更喜歡命名引數的方式呼叫函式,這樣相較於僅按照順序的無命名引數法會更直觀。別忘了Server.listen也可以使用某個Object作為引數:

require(`http`)
  .createServer()
  .listen({
    port: 8080,
    host: `localhost`,
  })
  .on(`request`, (req, res) => {
    res.end(`Hello World!`);
  });

不過這個特性不是表述在http.Server這個API中,而是在其父級net.Server的文件中。

相對地址

你傳入fs模組的距離可以是相對地址,即相對於process.cwd()。估計有些人早就知道了,不過我之前一直以為是隻能使用絕對地址:

const fs = require(`fs`);
const path = require(`path`);
// why have I always done this...
fs.readFile(path.join(__dirname, `myFile.txt`), (err, data) => {
  // do something
});
// when I could just do this?
fs.readFile(`./path/to/myFile.txt`, (err, data) => {
  // do something
});

Path Parsing:路徑解析

之前我一直不知道的某個功能就是從某個檔名中解析出路徑,檔名,檔案擴充套件等等:

myFilePath = `/someDir/someFile.json`;
path.parse(myFilePath).base === `someFile.json`; // true
path.parse(myFilePath).name === `someFile`; // true
path.parse(myFilePath).ext === `.json`; // true

Logging with colors

別忘了console.dir(obj,{colors:true})能夠以不同的色彩列印出鍵與值,這一點會大大增加日誌的可讀性。

使用setInterval執行定時任務

我喜歡使用setInterval來定期執行資料庫清理任務,不過預設情況下在存在setInterval的時候NodeJS並不會退出,你可以使用如下的方法讓Node沉睡:

const dailyCleanup = setInterval(() => {
  cleanup();
}, 1000 * 60 * 60 * 24);
dailyCleanup.unref();

Use Signal Constants

如果你嘗試在NodeJS中殺死某個程式,估計你用過如下語法:

process.kill(process.pid, `SIGTERM`);

這個沒啥問題,不過既然第二個引數同時能夠使用字串與整形變數,那麼還不如使用全域性變數呢:

process.kill(process.pid, os.constants.signals.SIGTERM);

IP Address Validation

NodeJS中含有內建的IP地址校驗工具,這一點可以免得你寫額外的正規表示式:

require(`net`).isIP(`10.0.0.1`) 返回 4
require(`net`).isIP(`cats`) 返回 0

os.EOF

不知道你有沒有手寫過行結束符,看上去可不漂亮啊。NodeJS內建了os.EOF,其在Windows下是rn,其他地方是n使用os.EOL能夠讓你的程式碼在不同的作業系統上保證一致性:

const fs = require(`fs`);
// bad
fs.readFile(`./myFile.txt`, `utf8`, (err, data) => {
  data.split(`\r\n`).forEach(line => {
    // do something
  });
});
// good
const os = require(`os`);
fs.readFile(`./myFile.txt`, `utf8`, (err, data) => {
  data.split(os.EOL).forEach(line => {
    // do something
  });
});

HTTP 狀態碼

NodeJS幫我們內建了HTTP狀態碼及其描述,也就是http.STATUS_CODES,鍵為狀態值,值為描述:

你可以按照如下方法使用:

someResponse.code === 301; // true
require(`http`).STATUS_CODES[someResponse.code] === `Moved Permanently`; // true

避免異常崩潰

有時候碰到如下這種導致服務端崩潰的情況還是挺無奈的:

const jsonData = getDataFromSomeApi(); // But oh no, bad data!
const data = JSON.parse(jsonData); // Loud crashing noise.

我為了避免這種情況,在全域性加上了一個:

process.on(`uncaughtException`, console.error);

當然,這種辦法絕不是最佳實踐,如果是在大型專案中我還是會使用PM2,然後將所有可能崩潰的程式碼加入到try...catch中。

Just this once()

除了on方法,once方法也適用於所有的EventEmitters,希望我不是最後才知道這個的:

server.once(`request`, (req, res) => res.end(`No more from me.`));

Custom Console

你可以使用new console.Console(standardOut,errorOut),然後設定自定義的輸出流。你可以選擇建立console將資料輸出到檔案或者Socket或者第三方中。

DNS lookup

某個年輕人告訴我,Node並不會快取DNS查詢資訊,因此你在使用URL之後要等個幾毫秒才能獲取到資料。不過其實你可以使用dns.lookup()來快取資料:

dns.lookup(`www.myApi.com`, 4, (err, address) => {
  cacheThisForLater(address);
});

fs 在不同OS上有一定差異

  • fs.stats()返回的物件中的mode屬性在Windows與其他作業系統中存在差異。
  • fs.lchmod()僅在macOS中有效。
  • 僅在Windows中支援呼叫fs.symlink()時使用type引數。
  • 僅僅在macOS與Windows中呼叫fs.watch()時傳入recursive選項。
  • 在Linux與Windows中fs.watch()的回撥可以傳入某個檔名
  • 使用fs.open()以及a+屬性開啟某個目錄時僅僅在FreeBSD以及Windows上起作用,在macOS以及Linux上則存在問題。
  • 在Linux下以追加模式開啟某個檔案時,傳入到fs.write()position引數會被忽略。

net 模組差不多比http快上兩倍

筆者在文件中看到一些關於二者效能的討論,還特地執行了兩個伺服器來進行真實比較。結果來看http.Server大概每秒可以接入3400個請求,而net.Server可以接入大概5500個請求。

// This makes two connections, one to a tcp server, one to an http server (both in server.js)
// It fires off a bunch of connections and times the response

// Both send strings.

const net = require(`net`);
const http = require(`http`);

function parseIncomingMessage(res) {
  return new Promise((resolve) => {
    let data = ``;

    res.on(`data`, (chunk) => {
      data += chunk;
    });

    res.on(`end`, () => resolve(data));
  });
}

const testLimit = 5000;

/*  ------------------  */
/*  --  NET client  --  */
/*  ------------------  */
function testNetClient() {
  const netTest = {
    startTime: process.hrtime(),
    responseCount: 0,
    testCount: 0,
    payloadData: {
      type: `millipede`,
      feet: 100,
      test: 0,
    },
  };

  function handleSocketConnect() {
    netTest.payloadData.test++;
    netTest.payloadData.feet++;

    const payload = JSON.stringify(netTest.payloadData);

    this.end(payload, `utf8`);
  }

  function handleSocketData() {
    netTest.responseCount++;

    if (netTest.responseCount === testLimit) {
      const hrDiff = process.hrtime(netTest.startTime);
      const elapsedTime = hrDiff[0] * 1e3 + hrDiff[1] / 1e6;
      const requestsPerSecond = (testLimit / (elapsedTime / 1000)).toLocaleString();

      console.info(`net.Server handled an average of ${requestsPerSecond} requests per second.`);
    }
  }

  while (netTest.testCount < testLimit) {
    netTest.testCount++;
    const socket = net.connect(8888, handleSocketConnect);
    socket.on(`data`, handleSocketData);
  }
}

/*  -------------------  */
/*  --  HTTP client  --  */
/*  -------------------  */
function testHttpClient() {
  const httpTest = {
    startTime: process.hrtime(),
    responseCount: 0,
    testCount: 0,
  };

  const payloadData = {
    type: `centipede`,
    feet: 100,
    test: 0,
  };

  const options = {
    hostname: `localhost`,
    port: 8080,
    method: `POST`,
    headers: {
      'Content-Type': `application/x-www-form-urlencoded`,
    },
  };

  function handleResponse(res) {
    parseIncomingMessage(res).then(() => {
      httpTest.responseCount++;

      if (httpTest.responseCount === testLimit) {
        const hrDiff = process.hrtime(httpTest.startTime);
        const elapsedTime = hrDiff[0] * 1e3 + hrDiff[1] / 1e6;
        const requestsPerSecond = (testLimit / (elapsedTime / 1000)).toLocaleString();

        console.info(`http.Server handled an average of ${requestsPerSecond} requests per second.`);
      }
    });
  }

  while (httpTest.testCount < testLimit) {
    httpTest.testCount++;
    payloadData.test = httpTest.testCount;
    payloadData.feet++;

    const payload = JSON.stringify(payloadData);

    options[`Content-Length`] = Buffer.byteLength(payload);

    const req = http.request(options, handleResponse);
    req.end(payload);
  }
}

/*  --  Start tests  --  */
// flip these occasionally to ensure there's no bias based on order
setTimeout(() => {
  console.info(`Starting testNetClient()`);
  testNetClient();
}, 50);

setTimeout(() => {
  console.info(`Starting testHttpClient()`);
  testHttpClient();
}, 2000);
// This sets up two servers. A TCP and an HTTP one.
// For each response, it parses the received string as JSON, converts that object and returns a string
const net = require(`net`);
const http = require(`http`);

function renderAnimalString(jsonString) {
  const data = JSON.parse(jsonString);
  return `${data.test}: your are a ${data.type} and you have ${data.feet} feet.`;
}

/*  ------------------  */
/*  --  NET server  --  */
/*  ------------------  */

net
  .createServer((socket) => {
    socket.on(`data`, (jsonString) => {
      socket.end(renderAnimalString(jsonString));
    });
  })
  .listen(8888);

/*  -------------------  */
/*  --  HTTP server  --  */
/*  -------------------  */

function parseIncomingMessage(res) {
  return new Promise((resolve) => {
    let data = ``;

    res.on(`data`, (chunk) => {
      data += chunk;
    });

    res.on(`end`, () => resolve(data));
  });
}

http
  .createServer()
  .listen(8080)
  .on(`request`, (req, res) => {
    parseIncomingMessage(req).then((jsonString) => {
      res.end(renderAnimalString(jsonString));
    });
  });

REPL tricks

  • 如果你是在REPL模式下,就是直接輸入node然後進入互動狀態的模式。你可以直接輸入.load someFile.js然後可以載入包含自定義常量的檔案。
  • 可以通過設定NODE_REPL_HISTORY=""來避免將日誌寫入到檔案中。
  • _用來記錄最後一個計算值。
  • 在REPL啟動之後,所有的模組都已經直接載入成功。可以使用os.arch()而不是require(os).arch()來使用。

相關文章