19 уроків, які я отримав читаючи документацію NodeJS

12 хв. читання

Я думав, що знаю 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`,
// };

Docs

Інспектор V8

Запустіть node з прапорцем --inspect і ви отримаєте URL. Вставте його в хром, і вуаля, ви дебажите Node за допомогою Chrome DevTools.

Це все ще експериментальна фіча, але я давно користуюся нею і прекрасно себе почуваю.

Docs | Tutorial

Різниця між nextTick та setImmediate

Різницю між цими функціями можна зрозуміти, якщо уявити що в них є інші імена:

process.nextTick() тепер process.sendThisToTheStartOfTheQueue().

setImmediate() повинен бути sendThisToTheEndOfTheQueue()

Взагалі, це працює з багатьма речами. В React, наприклад, props можна назвати stuffThatShouldStayTheSameIfTheUserRefreshes, а state може бути перейменований в stuffThatShouldBeForgottenIfTheUserRefreshes.

Docs | Docs 2 | Docs 3

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.

Docs

Відносні шляхи

Шляхи, що ви передаєте до модуля 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) => {
  // щось робить
});

Docs

Парсинг шляхів

Одним з велосипедів з використанням регулярних виразів був розбір шляху для отримання імені файлу й його розширення. А можна було зробити ось так:

myFilePath = `/someDir/someFile.json`;
path.parse(myFilePath).base === `someFile.json`; // true
path.parse(myFilePath).name === `someFile`; // true
path.parse(myFilePath).ext === `.json`; // true

Docs

Кольорове логування

console.dir(obj, {colors: true}) буде друкувати імена та значення властивостей різними кольорами, що значно легше для розуміння.

Docs

Ви можете змусити setInterval() тихенко сидіти в кутку

Насправді, сидіти в кутку він не буде, але от не так впливати на цикл подій може. Зазвичай, цикл подій в JS не вимикається якщо є setInterval(), що чекає виконання. Якщо ви хочете дати Node.js можливість спати деякий час (не знаю навіщо це може знадобитися), ви можете зробити так:

const dailyCleanup = setInterval(() => {
  cleanup();
}, 1000 * 60 * 60 * 24);
dailyCleanup.unref();

Але будьте обережні, адже якщо більше немає чому виконуватися, окрім setInterval, наприклад, http-серверу, то Node.js просто закінчить виконуватися.

Docs

Використання констант сигналів

Вам подобається вбивати? Якщо так, то ви, напевно, робили так раніше:

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, це хліб.

Docs

os.EOL

Ви колись вводили вручну символ кінця стрічки? Ну звісно!

Саме для вас створена os.EOL, яка на Windows дорівнює \ \ і \ на інших платформах. Використання os.EOL дозволить легше переносити ваш код.

Але зауважте, що цю константу перш за все слід використовувати для запису в файл. Адже при читанні файлу ми не знаємо напевно як там закінчується стрічка (CRLF чи просто LF).

Docs

Опис статус-кодів http

В http.STATUS_CODES зберігається об'єкт, в якому ключ це цифровий код, а значення — текстова назва статусу.

19 уроків, які я отримав  читаючи документацію NodeJS

someResponse.code === 301; // true
require(`http`).STATUS_CODES[someResponse.code] === `Moved Permanently`; // true

Docs

Запобігання падінню

Я завжди думав, що якось нелогічно, що ось цей маленький кусок коду може зламати весь сервер:

const jsonData = getDataFromSomeApi(); // О ні, якась помилка і дані прийшли не повністю
const data = JSON.parse(jsonData); // Завантаження неправильних даних

Щоб не псувати собі настрій такими сюрпризами, ви можете додати в самий початок вашого Node-додатку це:

process.on(`uncaughtException`, console.error);

Звісно, для продакшену слід використовувати PM2 і обгортати все в try...catch. Але для невеликого pet-проекту — чому б і ні?

Docs

Just this once()

В додаток до методу on(), у всіх EventEmitter є метод once(). Як ви здогадалися, обробник буде виконано тільки раз:

server.once(`request`, (req, res) => res.end(`No more from me.`));

Docs

Власна консоль

Ви можете створити власну консоль за допомогою new console.Console(standardOut, errorOut), передавши потрібні вихідні потоки.

Навіщо? Не знаю. Можливо, вам захочеться мати власну консоль з виводом в файл/сокети/ще кудись.

Docs

DNS lookup

Одного разу я дізнався, що Node.js не кешує відповіді від DNS. Тобто якщо ви часто робите запити за URL, ви втрачаєте дорогоцінні мілісекунди. Натомість ви можете самі отримати IP сервера за адресою за допомогою dns.lookup() і кешувати його. Але насправді це вже зробили за вас.

dns.lookup(`www.myApi.com`, 4, (err, address) => {
  cacheThisForLater(address);
});

Docs

Модуль 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.

Docs

Модуль 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));
    });
  });

Docs

Трюки з REPL

  1. Якщо ви в REPL наберете .load someFile.js - це завантажить файл в поточну сесію. В файлі можуть бути корисні константи або функції, наприклад.

  2. Щоб відключити історію команд можна встановити змінну оточення NODE_REPL_HISTORY="". Сама історія зберігається в файлі ~/.node_repl_history.

  3. _ — змінна, що зберігає в собі результат останнього виконаного виразу. Дуже зручно!

  4. При запуску REPL для вас вже завантажені всі модулі, точніше вони завантажуються при першому зверненні. Вам не потрібно робити кожен раз require("os").

Docs

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.9K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація