我在閱讀NodeJS文檔中讀出的19個(gè)套路
雖然我已經(jīng)用了三年多的NodeJS,也曾經(jīng)以為自己對(duì)其無(wú)所不知。但是我好像從未有安靜的坐下來(lái)仔細(xì)地閱讀NodeJS的完整文檔。如果有熟悉我的朋友應(yīng)該知道,我之前已經(jīng)看了HTML,DOM,Web APIs,CSS,SVG以及ECMAScript的文檔,NodeJS是我這個(gè)系列的最后一個(gè)待翻閱的山峰。在閱讀文檔的過(guò)程中我也發(fā)現(xiàn)了很多本來(lái)不知道的知識(shí),我覺(jué)得我有必要分享給大家。不過(guò)文檔更多的是平鋪直敘,因此我也以閱讀的順序列舉出我覺(jué)得需要了解的點(diǎn)。
querystring:可以用作通用解析器的模塊
很多時(shí)候我們會(huì)從數(shù)據(jù)庫(kù)或其他地方得到這種奇怪格式的字符串:name:Sophie;shape:fox;condition:new,一般來(lái)說(shuō)我們會(huì)利用字符串切割的方式來(lái)講字符串劃分到JavaScript Object。不過(guò)querystring也是個(gè)不錯(cuò)的現(xiàn)成的工具:
- const weirdoString = `name:Sophie;shape:fox;condition:new`;
- const result = querystring.parse(weirdoString, `;`, `:`);
- // result:
- // {
- // name: `Sophie`,
- // shape: `fox`,
- // condition: `new`,
- // };
V8 Inspector
以--inspect參數(shù)運(yùn)行你的Node應(yīng)用程序,它會(huì)反饋你某個(gè)URL。將該URL復(fù)制到Chrome中并打開(kāi),你就可以使用Chrome DevTools來(lái)調(diào)試你的Node應(yīng)用程序啦。詳細(xì)的實(shí)驗(yàn)可以參考這篇文章。不過(guò)需要注意的是,該參數(shù)仍然屬于實(shí)驗(yàn)性質(zhì)。
nextTick 與 setImmediate的區(qū)別
這兩貨的區(qū)別可能光從名字上還看不出來(lái),我覺(jué)得應(yīng)該給它們?nèi)€(gè)別名:
- process.nextTick()應(yīng)該為process.sendThisToTheStartOfTheQueue()
- setImmediate應(yīng)該為sendThisToTheEndOfTheQueue()
再說(shuō)句不相關(guān)的,React中的Props應(yīng)該為stuffThatShouldStayTheSameIfTheUserRefreshes,而State應(yīng)該為stuffThatShouldBeForgottenIfTheUserRefreshes。
Server.listen 可以使用Object作為參數(shù)
我更喜歡命名參數(shù)的方式調(diào)用函數(shù),這樣相較于僅按照順序的無(wú)命名參數(shù)法會(huì)更直觀。別忘了Server.listen也可以使用某個(gè)Object作為參數(shù):
- require(`http`)
- .createServer()
- .listen({
- port: 8080,
- host: `localhost`,
- })
- .on(`request`, (req, res) => {
- res.end(`Hello World!`);
- });
不過(guò)這個(gè)特性不是表述在http.Server這個(gè)API中,而是在其父級(jí)net.Server的文檔中。
相對(duì)地址
你傳入fs模塊的距離可以是相對(duì)地址,即相對(duì)于process.cwd()。估計(jì)有些人早就知道了,不過(guò)我之前一直以為是只能使用絕對(duì)地址:
- 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:路徑解析
之前我一直不知道的某個(gè)功能就是從某個(gè)文件名中解析出路徑,文件名,文件擴(kuò)展等等:
- 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})能夠以不同的色彩打印出鍵與值,這一點(diǎn)會(huì)大大增加日志的可讀性。
使用setInterval執(zhí)行定時(shí)任務(wù)
我喜歡使用setInterval來(lái)定期執(zhí)行數(shù)據(jù)庫(kù)清理任務(wù),不過(guò)默認(rèn)情況下在存在setInterval的時(shí)候NodeJS并不會(huì)退出,你可以使用如下的方法讓Node沉睡:
- const dailyCleanup = setInterval(() => {
- cleanup();
- }, 1000 * 60 * 60 * 24);
- dailyCleanup.unref();
- Use Signal Constants
如果你嘗試在NodeJS中殺死某個(gè)進(jìn)程,估計(jì)你用過(guò)如下語(yǔ)法:
- process.kill(process.pid, `SIGTERM`);
這個(gè)沒(méi)啥問(wèn)題,不過(guò)既然第二個(gè)參數(shù)同時(shí)能夠使用字符串與整形變量,那么還不如使用全局變量呢:
- process.kill(process.pid, os.constants.signals.SIGTERM);
IP Address Validation
NodeJS中含有內(nèi)置的IP地址校驗(yàn)工具,這一點(diǎn)可以免得你寫(xiě)額外的正則表達(dá)式:
- require(`net`).isIP(`10.0.0.1`) 返回 4
- require(`net`).isIP(`cats`) 返回 0
os.EOF
不知道你有沒(méi)有手寫(xiě)過(guò)行結(jié)束符,看上去可不漂亮啊。NodeJS內(nèi)置了os.EOF,其在Windows下是rn,其他地方是n,使用os.EOL能夠讓你的代碼在不同的操作系統(tǒng)上保證一致性:
- 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 狀態(tài)碼
NodeJS幫我們內(nèi)置了HTTP狀態(tài)碼及其描述,也就是http.STATUS_CODES,鍵為狀態(tài)值,值為描述:
你可以按照如下方法使用:
- someResponse.code === 301; // true
- require(`http`).STATUS_CODES[someResponse.code] === `Moved Permanently`; // true
避免異常崩潰
有時(shí)候碰到如下這種導(dǎo)致服務(wù)端崩潰的情況還是挺無(wú)奈的:
- const jsonData = getDataFromSomeApi(); // But oh no, bad data!
- const data = JSON.parse(jsonData); // Loud crashing noise.
我為了避免這種情況,在全局加上了一個(gè):
- process.on(`uncaughtException`, console.error);
當(dāng)然,這種辦法絕不是最佳實(shí)踐,如果是在大型項(xiàng)目中我還是會(huì)使用PM2,然后將所有可能崩潰的代碼加入到try...catch中。
Just this once()
除了on方法,once方法也適用于所有的EventEmitters,希望我不是最后才知道這個(gè)的:
- server.once(`request`, (req, res) => res.end(`No more from me.`));
Custom Console
你可以使用new console.Console(standardOut,errorOut),然后設(shè)置自定義的輸出流。你可以選擇創(chuàng)建console將數(shù)據(jù)輸出到文件或者Socket或者第三方中。
DNS lookup
某個(gè)年輕人告訴我,Node并不會(huì)緩存DNS查詢信息,因此你在使用URL之后要等個(gè)幾毫秒才能獲取到數(shù)據(jù)。不過(guò)其實(shí)你可以使用dns.lookup()來(lái)緩存數(shù)據(jù):
- dns.lookup(`www.myApi.com`, 4, (err, address) => {
- cacheThisForLater(address);
- });
fs 在不同OS上有一定差異
- fs.stats()返回的對(duì)象中的mode屬性在Windows與其他操作系統(tǒng)中存在差異。
- fs.lchmod()僅在macOS中有效。
- 僅在Windows中支持調(diào)用fs.symlink()時(shí)使用type參數(shù)。
- 僅僅在macOS與Windows中調(diào)用fs.watch()時(shí)傳入recursive選項(xiàng)。
- 在Linux與Windows中fs.watch()的回調(diào)可以傳入某個(gè)文件名
- 使用fs.open()以及a+屬性打開(kāi)某個(gè)目錄時(shí)僅僅在FreeBSD以及Windows上起作用,在macOS以及Linux上則存在問(wèn)題。
- 在Linux下以追加模式打開(kāi)某個(gè)文件時(shí),傳入到fs.write()的position參數(shù)會(huì)被忽略。
net 模塊差不多比http快上兩倍
筆者在文檔中看到一些關(guān)于二者性能的討論,還特地運(yùn)行了兩個(gè)服務(wù)器來(lái)進(jìn)行真實(shí)比較。結(jié)果來(lái)看http.Server大概每秒可以接入3400個(gè)請(qǐng)求,而net.Server可以接入大概5500個(gè)請(qǐng)求。
- // 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然后進(jìn)入交互狀態(tài)的模式。你可以直接輸入.load someFile.js然后可以載入包含自定義常量的文件。
- 可以通過(guò)設(shè)置NODE_REPL_HISTORY=""來(lái)避免將日志寫(xiě)入到文件中。
- _用來(lái)記錄最后一個(gè)計(jì)算值。
- 在REPL啟動(dòng)之后,所有的模塊都已經(jīng)直接加載成功??梢允褂胦s.arch()而不是require(os).arch()來(lái)使用。