19 неожиданных находок в документации Node.js

от автора

Мне хотелось бы думать, что я вполне прилично знаю Node. Вот уже три года, как ни один из сайтов, над которыми я работал, не обходится без него. Но документацию до сих пор я как следует не читал.

Мне нравится записывать полезные вещи об интерфейсах, свойствах, методах, функциях, типах данных, и обо всём прочем, что относится к веб-разработке. Так я заполняю пробелы в знаниях. Сейчас я занят документацией к Node.js, а до этого проработал материалы по HTML, DOM, по Web API, CSS, SVG и EcmaScript.

image


Чтение документации 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, а statestuffThatShouldBeForgottenIfTheUserRefreshes. То, что у этих названий одинаковая длина, считайте удачным совпадением.

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 это даст \r\n, во всех остальных ОС — \n. Переход на os.EOL позволит обеспечить единообразное поведение кода в разных операционных системах.

Тут я сделаю поправку, так как в момент написания материала недостаточно в эту тему углубился. Читатели предыдущей версии этого поста указали мне на то, что использование os.EOL может приводить к неприятностям. Дело в том, что здесь нужно исходить из предположения, что в некоем файле может использоваться или CRLF(\r\n), или LF (\n), но полностью быть уверенным в подобном предположении нельзя.

Если у вас имеется проект с открытым исходным кодом, и вы хотите принудительно использовать определённый вариант перевода строки, вот правило eslint, которое, отчасти, может в этом помочь. Правда, оно бесполезно, если с текстами поработает Git.

И, всё же, os.EOL – не бесполезная игрушка. Например, эта штука может оказаться кстати при формировании лог-файлов, которые не планируется переносить в другие ОС. В подобном случае os.EOL обеспечивает правильность отображения таких файлов, скажем, для просмотра которых используется Блокнот в Windows Server.

const fs = require(`fs`); // жёстко заданный признак конца строки CRLF fs.readFile(`./myFile.txt`, `utf8`, (err, data) => {   data.split(`\r\n`).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

  1. Если вы работаете в режиме REPL, то есть, написали в терминале node и нажали на Enter, можете ввести команду вроде .load someFile.js и система загрузит запрошенный файл (например, в таком файле может быть задана куча констант).
  2. В этом режиме можно установить переменную окружения NODE_REPL_HISTORY="" для того, чтобы отключить запись истории в файл. Кроме того, я узнал (как минимум – вспомнил), что файл истории REPL, который позволяет путешествовать в прошлое, хранится по адресу ~/.node_repl_history.
  3. Символ подчёркивания « — это имя переменной, которая хранит результат последнего выполненного выражения. Думаю, может пригодиться.
  4. Когда Node запускается в режиме REPL, модули загружаются автоматически (точнее – по запросу). Например, можно просто ввести в командной строке os.arch() для того, чтобы узнать архитектуру ОС. Конструкция вроде require(`os`).arch(); не нужна.

Итоги

Как видите, читать документацию – дело полезное. Много нового можно найти даже в той области, которую, вроде бы, знаешь вдоль и поперёк. Надеюсь, вам пригодятся мои находки.

Кстати, знаете ещё что-нибудь интересное о Node.js? Если так – делитесь 🙂
ссылка на оригинал статьи https://habrahabr.ru/post/318322/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *