Я думав, що знаю Node.js досить добре. Всі мої сайти, що я писав за останні три роки, використовують його. Але я ніколи серйозно не читав документацію.
Але я все ж вирішив зробити це, і був здивований. В цій статті я покажу що саме так мене здивувало.
querystring
як кращий спосіб парсингу рядків
Давайте уявимо, що ви отримали відповідь від якогось сервера у вигляді масиву пар ключ-значення. Було б непогано зробити з цього JavaScript-об'єкт. Тобто ви створюєте пустий об'єкт, потім розбиваєте стрічку на масив по ;
, проходите в циклі по кожному елементу масиву, розбиваєте по :
і додаєте в об'єкт?
А от і ні! Ви використовуєте querystring
const weirdoString = `name:Sophie;shape:fox;condition:new`;
const result = querystring.parse(weirdoString, `;`, `:`);
// result:
// {
// name: `Sophie`,
// shape: `fox`,
// condition: `new`,
// };
Інспектор V8
Запустіть node з прапорцем --inspect
і ви отримаєте URL. Вставте його в хром, і вуаля, ви дебажите Node за допомогою Chrome DevTools.
Це все ще експериментальна фіча, але я давно користуюся нею і прекрасно себе почуваю.
Різниця між nextTick
та setImmediate
Різницю між цими функціями можна зрозуміти, якщо уявити що в них є інші імена:
process.nextTick()
тепер process.sendThisToTheStartOfTheQueue()
.
setImmediate()
повинен бути sendThisToTheEndOfTheQueue()
Взагалі, це працює з багатьма речами. В React, наприклад, props
можна назвати stuffThatShouldStayTheSameIfTheUserRefreshes
, а state
може бути перейменований в stuffThatShouldBeForgottenIfTheUserRefreshes
.
Server.listen
приймає об'єкт
Я віддаю перевагу одному об'єкту-параметру, це краще ніж 5 безіменних параметрів, котрі до того ж потрібно передати в правильному порядку. Для мене було сюрпризом, що так само можна робити і з Server.listen
:
require(`http`)
.createServer()
.listen({
port: 8080,
host: `localhost`,
})
.on(`request`, (req, res) => {
res.end(`Hello World!`);
});
До того ж, така можливість не вказана в документації до http.Server, а лише в документації батьківського net.Server.
Відносні шляхи
Шляхи, що ви передаєте до модуля fs
можуть бути відносними. Вони відносні від process.cwd()
. Багато людей це знають, але не всі (як я, наприклад)
const fs = require(`fs`);
const path = require(`path`);
// Раніше я робив так
fs.readFile(path.join(__dirname, `myFile.txt`), (err, data) => {
// щось робить
});
// А можна просто ось так
fs.readFile(`./path/to/myFile.txt`, (err, data) => {
// щось робить
});
Парсинг шляхів
Одним з велосипедів з використанням регулярних виразів був розбір шляху для отримання імені файлу й його розширення. А можна було зробити ось так:
myFilePath = `/someDir/someFile.json`;
path.parse(myFilePath).base === `someFile.json`; // true
path.parse(myFilePath).name === `someFile`; // true
path.parse(myFilePath).ext === `.json`; // true
Кольорове логування
console.dir(obj, {colors: true})
буде друкувати імена та значення властивостей різними кольорами, що значно легше для розуміння.
Ви можете змусити setInterval()
тихенко сидіти в кутку
Насправді, сидіти в кутку він не буде, але от не так впливати на цикл подій може. Зазвичай, цикл подій в JS не вимикається якщо є setInterval()
, що чекає виконання. Якщо ви хочете дати Node.js можливість спати деякий час (не знаю навіщо це може знадобитися), ви можете зробити так:
const dailyCleanup = setInterval(() => {
cleanup();
}, 1000 * 60 * 60 * 24);
dailyCleanup.unref();
Але будьте обережні, адже якщо більше немає чому виконуватися, окрім setInterval, наприклад, http-серверу, то Node.js просто закінчить виконуватися.
Використання констант сигналів
Вам подобається вбивати? Якщо так, то ви, напевно, робили так раніше:
process.kill(process.pid, `SIGTERM`);
Цей код повністю правильний і робочий. Але всі ми знаємо скільки неприємностей може виникнути через опечатку. Тому ви можете використовувати константи з модуля os
:
process.kill(process.pid, os.constants.signals.SIGTERM);
Валідація IP
В Node.js є вбудований валідатор для IP-адрес. Я ж не раз робив це за допомогою регулярних виразів. Дурний я.
require(`net`).isIP(`10.0.0.1`) === 4
require(`net`).isIP(`cats`) === 0
Тому що котики це не IP, це хліб.
os.EOL
Ви колись вводили вручну символ кінця стрічки? Ну звісно!
Саме для вас створена os.EOL
, яка на Windows дорівнює \ \
і \
на інших платформах. Використання os.EOL
дозволить легше переносити ваш код.
Але зауважте, що цю константу перш за все слід використовувати для запису в файл. Адже при читанні файлу ми не знаємо напевно як там закінчується стрічка (CRLF чи просто LF).
Опис статус-кодів http
В http.STATUS_CODES
зберігається об'єкт, в якому ключ це цифровий код, а значення — текстова назва статусу.
someResponse.code === 301; // true
require(`http`).STATUS_CODES[someResponse.code] === `Moved Permanently`; // true
Запобігання падінню
Я завжди думав, що якось нелогічно, що ось цей маленький кусок коду може зламати весь сервер:
const jsonData = getDataFromSomeApi(); // О ні, якась помилка і дані прийшли не повністю
const data = JSON.parse(jsonData); // Завантаження неправильних даних
Щоб не псувати собі настрій такими сюрпризами, ви можете додати в самий початок вашого Node-додатку це:
process.on(`uncaughtException`, console.error);
Звісно, для продакшену слід використовувати PM2 і обгортати все в try...catch
. Але для невеликого pet-проекту — чому б і ні?
Just this once()
В додаток до методу on()
, у всіх EventEmitter
є метод once()
. Як ви здогадалися, обробник буде виконано тільки раз:
server.once(`request`, (req, res) => res.end(`No more from me.`));
Власна консоль
Ви можете створити власну консоль за допомогою new console.Console(standardOut, errorOut)
, передавши потрібні вихідні потоки.
Навіщо? Не знаю. Можливо, вам захочеться мати власну консоль з виводом в файл/сокети/ще кудись.
DNS lookup
Одного разу я дізнався, що Node.js не кешує відповіді від DNS. Тобто якщо ви часто робите запити за URL, ви втрачаєте дорогоцінні мілісекунди. Натомість ви можете самі отримати IP сервера за адресою за допомогою dns.lookup()
і кешувати його. Але насправді це вже зробили за вас.
dns.lookup(`www.myApi.com`, 4, (err, address) => {
cacheThisForLater(address);
});
Модуль fs
повен ОС-залежних сюрпризів
Якщо ви любите приступати до кодингу прочитавши лише "необхідний мінімум" з документації, у вас буде чимало головної болі з модулем fs
, адже деякі його методи ведуть себе по різному на різних ОС. При чом тут розподіл не на "Widows і нормальні системи", все куди складніше.
-
Властивість
mode
обєкта, що повертається зfs.stats()
буде різна на Windows і інших ОС (на Windows вона може не співпадати з константами режиму файла, такими якfs.constants.S_IRWXU
). -
fs.lchmod()
доступний тільки на macOS. -
Виклик
fs.symlink()
з параметромtype
підтримується лише на Windows. -
Опція
recursive
функціїfs.watch()
працює тільки macOS на та Windows. -
Калбек
fs.watch()
отримує імя файлу лише в Linux та Windows. -
Використання
fs.open()
з прапорцемa+
спрацює в FreeBSD та Windows, але викличе помилку в macOS та Linux.
Модуль net
вдвічі швидший за http
Читаючи документацію я дізнався що модуль net
дуже крута річ. І що на його основі побудований модуль http
. І це змусило мене задуматися: якщо я хочу зробити комунікацію між двома серверами, краще використовувати net
?
Людям, добре знайомим з мережами, може здатися дивним що я сам не здогадався що ж буде швидше, але для мене, розробника який нічого крім HTTP не знає, це було нормально. Всі ці TCP, сокети, потоки для мене як японський реп: звучить прикольно, але не зрозуміло.
Для того щоб знайти відповідь на своє питання я написав два невеличких сервера і порівняв їх. http.Server
витримав ~3,400 запитів в секунду, а net.Server
— ~5,500.
Ось код, якщо вам цікаво. Якщо ні, вибачте, що змусив стільки скролити.
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 — */
setTimeout(() => {
console.info(`Starting testNetClient()`);
testNetClient();
}, 50);
setTimeout(() => {
console.info(`Starting testHttpClient()`);
testHttpClient();
}, 2000);
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
-
Якщо ви в REPL наберете
.load someFile.js
- це завантажить файл в поточну сесію. В файлі можуть бути корисні константи або функції, наприклад. -
Щоб відключити історію команд можна встановити змінну оточення
NODE_REPL_HISTORY=""
. Сама історія зберігається в файлі~/.node_repl_history
. -
_
— змінна, що зберігає в собі результат останнього виконаного виразу. Дуже зручно! -
При запуску REPL для вас вже завантажені всі модулі, точніше вони завантажуються при першому зверненні. Вам не потрібно робити кожен раз
require("os")
.
Ще немає коментарів