[Перевод] 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, а 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 это даст \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. Его ключи — это коды состояний, а значения — их названия.
5ce5ec73b2f2a0a947e7210fb9b114ef.png

Объект 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? Если так — делитесь :)

Комментарии (2)

  • 23 декабря 2016 в 17:23 (комментарий был изменён)

    0

    Интересный ответ вместо 'Not Found' выдаёт на скриншоте http.STATUS_CODE для кода 404, видимо это собственное изобретение автора.

    P.S. увидел, что перевод, исправил.

  • 23 декабря 2016 в 17:29

    0

    Даже, если из всей статьи оставить только пункт 2, то эта статья не потеряет ценности. Потому что это просто неописуемо круто, когда можно дебажить удаленный проект так просто. В браузере.

© Habrahabr.ru