Мне хотелось бы думать, что я вполне прилично знаю Node. Вот уже три года, как ни один из сайтов, над которыми я работал, не обходится без него. Но документацию до сих пор я как следует не читал.
Мне нравится записывать полезные вещи об интерфейсах, свойствах, методах, функциях, типах данных, и обо всём прочем, что относится к веб-разработке. Так я заполняю пробелы в знаниях. Сейчас я занят документацией к Node.js, а до этого проработал материалы по HTML, DOM, по Web API, CSS, SVG и EcmaScript.
Чтение документации Node.js открыло мне немало замечательных вещей, о которых я раньше не знал. Ими я хочу поделиться в этом небольшом материале. Начну с самого интересного. Так же я обычно делаю, когда показываю новому знакомому свои гаджеты.
1. Модуль querystring как универсальный парсер
Скажем, вы получили данные из какой-нибудь эксцентричной БД, которая выдала массив пар ключ/значение в примерно таком виде:
name:Sophie;shape:fox;condition:new
. Вполне естественно полагать, что подобное можно легко преобразовать в объект JavaScript. Поэтому вы создаёте пустой объект, затем – массив, разделив строку по символу «;
». Дальше – проходитесь в цикле по каждому элементу этого массива, опять разбиваете строки, теперь уже по символу «:
». В итоге, первый полученный из каждой строки элемент становится именем свойства нового объекта, второй – значением.
Всё правильно?
Нет, не правильно. В подобной ситуации достаточно воспользоваться querystring
.
const weirdoString = `name:Sophie;shape:fox;condition:new`;
const result = querystring.parse(weirdoString, `;`, `:`);
// результат:
// {
// name: `Sophie`,
// shape: `fox`,
// condition: `new`,
// };
2. Отладка: V8 Inspector
Если запустить Node с ключом --inspect
, он сообщит URL. Перейдите по этому адресу в Chrome. А теперь – приятная неожиданность. Нам доступна отладка Node.js с помощью инструментов разработчика Chrome. Настали счастливые времена. Вот руководство на эту тему от Пола Айриша.
Надо отметить, что данная функция всё ещё носит статус экспериментальной, но я ей с удовольствием пользуюсь и до сих пор она меня не подводила.
3. Разница между nextTick и setImmediate
Как и в случае со многими другими программными механизмами, запомнить разницу между этими двумя функциями очень просто, если дать им более осмысленные имена.
Итак, функция process.nextTick()
должна называться process.sendThisToTheStartOfTheQueue()
. А setImmediate()
- sendThisToTheEndOfTheQueue()
.
Кстати, вот полезный материал об оптимизации nextTick
начиная с Node v0.10.0. Маленькое отступление. Я всегда думал, что в React props
должно называться stuffThatShouldStayTheSameIfTheUserRefreshes
, а state
– stuffThatShouldBeForgottenIfTheUserRefreshes
. То, что у этих названий одинаковая длина, считайте удачным совпадением.
4. Server.listen принимает объект с параметрами
Я приверженец передачи параметров в виде объекта, например, с именем «options», а не подхода, когда на входе в функцию ожидается куча параметров, которые, к тому же, не имеют имён, да ещё и должны быть расположены в строго определённом порядке. Как оказалось, при настройке сервера на прослушивание запросов можно использовать объект с параметрами.
require(`http`)
.createServer()
.listen({
port: 8080,
host: `localhost`,
})
.on(`request`, (req, res) => {
res.end(`Hello World!`);
});
Эта полезная возможность неплохо спряталась. В документации по http.Server
о ней – ни слова. Однако, её можно найти в описании net.Server
, наследником которого является http.Server
.
5. Относительные пути к файлам
Путь в файловой системе, который передают модулю 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) => {
// делаем что-нибудь полезное
});
6. Разбор путей к файлам
Обычно, когда мне нужно было вытащить из пути к файлу его имя и расширение, я пользовался регулярными выражениями. Теперь понимаю, что в этом нет совершенно никакой необходимости. То же самое можно сделать стандартными средствами.
myFilePath = `/someDir/someFile.json`;
path.parse(myFilePath).base === `someFile.json`; // true
path.parse(myFilePath).name === `someFile`; // true
path.parse(myFilePath).ext === `.json`; // true
7. Раскраска логов в консоли
Сделаю вид, будто я не знал, что конструкция console.dir(obj, {colors: true})
позволяет выводить в консоль объекты со свойствами и значениями, выделенными цветом. Это упрощает чтение логов.
8. Управление setInterval()
Например, вы используете setInterval()
для того, чтобы раз в день проводить очистку базы данных. По умолчанию цикл событий Node не остановится до тех пор, пока имеется код, исполнение которого запланировано с помощью setInterval()
. Если вы хотите дать Node отдохнуть (не знаю, на самом деле, какие плюсы можно от этого получить), воспользуйтесь функцией unref()
.
const dailyCleanup = setInterval(() => {
cleanup();
}, 1000 * 60 * 60 * 24);
dailyCleanup.unref();
Однако, тут стоит проявить осторожность. Если Node больше ничем не занят (скажем, нет http-сервера, ожидающего подключений), он завершит работу.
9. Константы сигнала завершения процесса
Если вам нравится убивать, то вы, наверняка, уже так делали:
process.kill(process.pid, `SIGTERM`);
Ничего плохого об этой конструкции сказать не могу. Но что, если в команду вкралась ошибка, вызванная опечаткой? В истории программирования известны такие случаи. Второй параметр здесь должен быть строкой или соответствующим целым числом, поэтому тут немудрено написать что-нибудь не то. Для того, чтобы застраховаться от ошибок, можно поступить так:
process.kill(process.pid, os.constants.signals.SIGTERM);
10. Проверка IP-адресов
В Node.js имеется встроенное средство для проверки IP-адресов. Раньше я не раз писал регулярные выражения для того, чтобы это сделать. На большее ума не хватило. Вот как это сделать правильно:
require(`net`).isIP(`10.0.0.1`)
вернёт 4
.
require(`net`).isIP(`cats`)
вернёт 0
.
Всё верно, коты – это не IP-адреса.
Возможно вы заметили, что в примерах я использую для строк одинарные кавычки. Мне так делать нравится, но я подозреваю, что выглядит это странно, поэтому считаю нужным об этом упомянуть, хотя и сам толком не знаю – зачем. В общем – это мой стиль.
11. Символ конца строки, os.EOL
Вы когда-нибудь задавали в коде символ конца строки? Да? Всё, тушите свет. Вот, специально для тех, кто так делал, замечательная штука: os.EOL
. В Windows это даст rn
, во всех остальных ОС — n
. Переход на os.EOL
позволит обеспечить единообразное поведение кода в разных операционных системах.
Тут я сделаю поправку, так как в момент написания материала недостаточно в эту тему углубился. Читатели предыдущей версии этого поста указали мне на то, что использование os.EOL
может приводить к неприятностям. Дело в том, что здесь нужно исходить из предположения, что в некоем файле может использоваться или CRLF(rn
), или LF (n
), но полностью быть уверенным в подобном предположении нельзя.
Если у вас имеется проект с открытым исходным кодом, и вы хотите принудительно использовать определённый вариант перевода строки, вот правило eslint, которое, отчасти, может в этом помочь. Правда, оно бесполезно, если с текстами поработает Git.
И, всё же, os.EOL
– не бесполезная игрушка. Например, эта штука может оказаться кстати при формировании лог-файлов, которые не планируется переносить в другие ОС. В подобном случае os.EOL
обеспечивает правильность отображения таких файлов, скажем, для просмотра которых используется Блокнот в Windows Server.
const fs = require(`fs`);
// жёстко заданный признак конца строки CRLF
fs.readFile(`./myFile.txt`, `utf8`, (err, data) => {
data.split(`rn`).forEach(line => {
// делаем что-нибудь полезное
});
});
// признак конца строки зависит от ОС
const os = require(`os`);
fs.readFile(`./myFile.txt`, `utf8`, (err, data) => {
data.split(os.EOL).forEach(line => {
// делаем что-нибудь полезное
});
});
12. Коды состояния HTTP
В Node имеется «справочник» с кодами состояния HTTP и их названиями. Я говорю об объекте http.STATUS_CODE
. Его ключи – это коды состояний, а значения – их названия.
Объект http.STATUS_CODE
Вот как этим пользоваться:
someResponse.code === 301; // true
require(`http`).STATUS_CODES[someResponse.code] === `Moved Permanently`; // true
13. Предотвращение ненужных остановок сервера
Мне всегда казалось малость странным то, что код, похожий на приведённый ниже, приводит к остановке сервера.
const jsonData = getDataFromSomeApi(); // Только не это! Нехорошие данные!
const data = JSON.parse(jsonData); // Громкий стук падающего сервера.
Для того, чтобы предотвратить подобные глупости, прямо в начале приложения для Node.js можно поместить такую конструкцию, выводящую необработанные исключения в консоль:
process.on(`uncaughtException`, console.error);
Я, конечно, нахожусь в здравом уме, поэтому пользуюсь PM2 и оборачиваю всё, что можно, в блоки try…catch
, когда программирую на заказ, но вот в домашних проектах…
Хочу обратить особое внимание на то, что такой подход никоим образом не относится к «лучшим практическим методам разработки», и его использование в больших и сложных приложениях, вероятно, идея плохая. Решайте сами, доверять ли посту в блоге, написанному каким-то чуваком, или официальной документации.
14. Пара слов об once()
В дополнение к методу on()
, у объектов EventEmitter
имеется и метод code
. Я совершенно уверен, что я – последний человек на Земле, который об этом узнал. Поэтому ограничусь простым примером, который все и так поймут.
server.once(`request`, (req, res) => res.end(`No more from me.`));
15. Настраиваемая консоль
Консоль можно настроить с помощью нижеприведённой конструкции, передавая ей собственные потоки вывода:
new console.Console(standardOut, errorOut)
Зачем? Не знаю точно. Может, вы захотите создать консоль, которая выводит данные в файл, или в сокет, или ещё куда-нибудь.
16. DNS-запросы
Мне тут одна птичка насвистела, что Node не кэширует результаты запросов к DNS. Поэтому, если вы несколько раз обращаетесь к некоему URL, на запросы, без которых можно было бы обойтись, тратятся бесценные миллисекунды. В подобном случае можно выполнить запрос к DNS самостоятельно, с помощью dns.lookup()
, и закэшировать результаты. Или – воспользоваться пакетом dnscache, который делает то же самое.
dns.lookup(`www.myApi.com`, 4, (err, address) => {
cacheThisForLater(address);
});
17. Модуль fs: минное поле
Если ваш стиль программирования похож на мой, то есть, это что-то вроде: «прочту по диагонали кусок документации и буду возиться с кодом, пока он не заработает», тогда вы не застрахованы от проблем с модулем fs
. Разработчики выполнили огромную работу, направленную на унификацию взаимодействия Node с различными ОС, но их возможности не безграничны. В результате, особенности различных операционных систем разрывают гладь океана кода как острые рифы, которые ещё и заминированы. А вы в этой драме играете роль лодки, которая может на один из рифов сесть.
К несчастью, различия, имеющие отношение к fs
, не сводятся к привычному: «Windows и все остальные», поэтому мы не можем просто отмахнуться, прикрывшись идеей: «да кто пользуется Windows». (Я сначала написал тут целую речь об анти-Windows настроениях в веб-разработке, но в итоге решил это убрать, а то у меня самого глаза на лоб полезли от этой моей проповеди).
Вот, вкратце, то, что я обнаружил в документации к модулю fs
. Уверен, кого-нибудь эти откровения могут клюнуть не хуже жареного петуха.
- Свойство
mode
объекта, возвращаемогоfs.stats(),
различается в Windows и в других ОС. В Windows оно может не соответствовать константам режима доступа к файлам, таким, какfs.constants.S_IRWXU
. - Функция
fs.lchmod()
доступна только в macOS. - Вызов
fs.symlink()
с параметромtype
поддерживается только в Windows. - Опция
recursive
, которую можно передать функцииfs.watch()
, работает только на Windows и macOS. - Функция обратного вызова
fs.watch()
принимает имя файла только в Linux и Windows. - Вызов
fs.open()
с флагомa+
для директории будет работать во FreeBSD и в Windows, но не сработает в macOS и Linux. - Параметр
position
, переданныйfs.write()
, будет проигнорирован в Linux в том случае, если файл открыт в режиме присоединения. Ядро игнорирует позицию и добавляет данные к концу файла.
(Я тут не отстаю от моды, называю ОС от Apple «macOS», хотя ещё и двух месяцев не прошло после того, как старое название, OS X, отошло в мир иной).
18. Модуль net вдвое быстрее модуля http
Читая документацию к Node.js, я понял, что модуль net
– это вещь. Он лежит в основе модуля http
. Это заставило меня задуматься о том, что если нужно организовать взаимодействие серверов (как оказалось, мне это понадобилось), стоит ли использовать исключительно модуль net
?
Те, кто плотно занимается сетевым взаимодействием систем, могут и не поверить, что подобный вопрос вообще надо задавать, но я – веб-разработчик, который вдруг свалился в мир серверов и знает только HTTP и ничего больше. Все эти TCP, сокеты, вся эта болтовня о потоках… Для меня это как японский рэп. То есть, мне вроде бы и непонятно, но звучит интригующе.
Для того, чтобы во всём разобраться, поэкспериментировать с net
и http
, и сравнить их, я настроил пару серверов (надеюсь, вы сейчас слушаете японский рэп) и нагрузил их запросами. В результате http.Server
смог обработать примерно 3400 запросов в секунду, а net.Server
– примерно 5500. К тому же, net.Server
проще устроен.
Вот, если интересно, код клиентов и серверов, с которым я экспериментировал. Если не интересно – примите извинения за то, что вам придётся так долго прокручивать страницу.
Вот код client.js.
// Здесь создаются два подключения. Одно – к TCP-серверу, другое – к HTTP (оба описаны в файле server.js).
// Клиенты выполняют множество запросов к серверам и подсчитывают ответы.
// И тот и другой работают со строками.
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);
Вот – server.js.
// Здесь созданы два сервера. Один – TCP, второй – HTTP.
// Для каждого запроса серверы преобразуют полученную строку в объект JSON, формируют с его использованием новую строку, и отправляют её в ответ на запрос.
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));
});
});
19. Хитрости режима REPL
- Если вы работаете в режиме REPL, то есть, написали в терминале
node
и нажали на Enter, можете ввести команду вроде.load someFile.js
и система загрузит запрошенный файл (например, в таком файле может быть задана куча констант). - В этом режиме можно установить переменную окружения
NODE_REPL_HISTORY=""
для того, чтобы отключить запись истории в файл. Кроме того, я узнал (как минимум – вспомнил), что файл истории REPL, который позволяет путешествовать в прошлое, хранится по адресу~/.node_repl_history
. - Символ подчёркивания «
_»
— это имя переменной, которая хранит результат последнего выполненного выражения. Думаю, может пригодиться. - Когда Node запускается в режиме REPL, модули загружаются автоматически (точнее – по запросу). Например, можно просто ввести в командной строке
os.arch()
для того, чтобы узнать архитектуру ОС. Конструкция вродеrequire(`os`).arch();
не нужна.
Итоги
Как видите, читать документацию – дело полезное. Много нового можно найти даже в той области, которую, вроде бы, знаешь вдоль и поперёк. Надеюсь, вам пригодятся мои находки.
Кстати, знаете ещё что-нибудь интересное о Node.js? Если так – делитесь :)
Автор: RUVDS.com