Node.js Recipes
3.23K subscribers
174 photos
7 videos
1 file
622 links
По буднях нотатки по #Nodejs розробці, по вихідним огляди конференцій та доповідей (с) @galkin_nikita
Download Telegram
Обзор Node.js v16: Упрощение доступа к util.types
#nodejs_api

Встроенный модуль util дает возможность проверять типы. Я чаще всего использую такие проверки из util.types:
➡️ isDate
➡️ isMap
➡️ isSet
➡️ isNativeError
➡️ isPromise

До 16-й версии обращение к данным методом было только через require('util').types. В 16-й появляется альтернатива require('util/types'). Для встроенных #nodejs модулей это синтаксический сахар. А вот для устанавливаемых использование конкретного файла – хорошая практика: typescript компилируется быстрее, уменьшение затраты памяти. По сути это ручной tree-shaking, т.е. удаление не используемого кода.
Как и зачем использовать AsyncLocalStorage?
#nodejs_api

С 16.4 версии Node.js AsyncLocalStorage перестал быть экспериментальным, а значит его можно смело использовать в продакшене. Данный класс позволяет сохранять контекст между шагами асинхронного флоу. Он работает и с callback, и с promise chain.

Для получения текущего значения используется asyncLocalStorage.getStore()

Для установки значения есть два способа:
asyncLocalStorage.run(store, callback[, ...args]) – устанавливает значение внутри callback. Для использования скорее всего потребуется внести изменения на верхнем уровне кода.
asyncLocalStorage.enterWith(store) – устанавливает значение до окончания текущего синхронного контекста и всех порождающих асинхронных операций. Проще в использование, но возможность его вызвать несколько раз создает сложности в поддержки. Мутировать стейт это плохо. Поэтому документация рекомендует использовать run.

Возможное использование данного апи – улучшение логгирования. В стейте храниться traceId, который пишется в лог. Так вы легко можете различить логи от нескольких запросов, которые параллельно обрабатываются #nodejs.

Статья с примерами использования из комментариев.
Пример для Nest.js.
👍1
Как использовать Node.js REPL?
#nodejs_api

REPL расшифровывается как read–eval–print loop. Для запуска #nodejs в таком режиме достаточно выполнить node без указания файла.

Работа с REPL очень похожа на работу с Console Panel в Google Chrome:
- пишешь JS код, его результат выводиться в консоль
- Cmd + K on MacOS / Ctrl + L on Windows – очистят консоль

Но есть и различия:
- Переменная _ хранит значение последней команды
- Есть команды начинающиеся с точки. Полный перечень .help
- Чтобы выйти из REPL используйте горячую клавишу Ctrl+D или Ctrl+C дважды

При желание можно написать свой REPL. Например для работы с API или базой данных, как это сделано для MongoDB. Подробней в документации.
В чем различие таймеров в Node.js и browser?
#nodejs_api

Сегодня обсудим setTimeout и setInterval. На обоих платформах данные функции доступны глобально и предназначены для запуска отложенного кода.

Первым аргументом должен быть TimerHandler. В #nodejs это может быть только функция, но в браузере это может быть еще и строка. К ней будет применен eval. Что безусловно является плохой практикой, так как может привести к проблемам с безопасностью. Используйте eslint правило no-implied-eval, чтобы следить за кодом.

Вторым аргументом является количество миллисекунд, а дальнейшие параметры будут переданы в качестве аргументов внутрь TimerHandler. Тут различий нет. Различие есть в возвращаемом значение. В браузере это будет число, а в Node.js объект типа NodeJS.Timeout.

NodeJS.Timeout имеет такие методы публичные методы:
- unref – отметить таймаут как не активный, т.е. он не будет удерживать EventLoop. Аналогичные вещи есть у сокетов, воркеров и т.д.
- ref – отменяет unref
- hasRef – дает текущее значение активности таймера.
- refresh – перезапускает таймер.
- close – аналогичен вызову clearInterval на таймере.

Чтобы отменить выполнение таймера используется clearInterval/clearTimeout. Они полностью заменяемы и в браузере, и в Node.js – по сути являются алиасами. Node.js в качестве аргумента принимает как число, так и NodeJS.Timeout объект.

На этом пора остановиться, хотя в рецепт не попали setImmediate, не публичные апи, особенности поведения в браузере в зависимости от активности таба, связь async_hooks и таймеров, event loop delay и прочее.
Как обнаружить синхронные операции в Node.js коде?
#nodejs_api

Каждый #nodejs разработчик знает, что блокировать Event Loop-а плохо. Блокировка может быть по двум причинам:
– cpu intensive операции, т.е. с большим количеством вычислений. Примеры: трансформация JSON объекта на 2 мегабайта, обход большого массива, подсчеты хешей.
– синхронные операции, т.е. операции которые до своего завершения блокируют Event Loop, читай дальнейшее выполнение JavaScript. Пример – все синхронные файловые операции.

Ваш код может использовать синхронные операции неявно. Кто его знает, что там в node_modules. Именно о том, как быстро найти такие места этот рецепт.

У Node.js есть специальный флаг --trace-sync-io. При его использование в консоль будут выводить trace для первого вызова любого синхронного вызова. Запускаем с этим флагом или c переменной окружения NODE_OPTIONS=--trace-sync-io и анализируем результаты. На своем проекте я так обнаружил, что пора выкинуть module-alias и переконфигурировать pino.

Рецепты по теме:
- Как запускать Node.js с доп. аргументами?
Обзор Node.js v16: Что такое corepack и как он работает?
#nodejs_api

В Node.js v16.9 добавили corepack. Данный инструмент является экспериментальным и упрощать работу с альтернативными менеджеров зависимостей. При обращение к yarn или pnpm (поддерживаемые менеджеры) будет происходит вызов установленных corepack-ом так называемые глобальных binary proxies. Данные прокси смотрят поле packageManager в package.json и вызывает соответствующую версию yarn/pnpm. При необходимости происходит установка отсутствующего менеджера. Если package.json нет, то используется LTS версия менеджера. Поддержки npm нет, так как npm устанавливается глобально вместе с Node.js. Инструмент улучшает Repeatability и решает проблему отсутствия аналогов nvm для yarn/pnp.

Мое личное отношение к альтернативным менеджерам – они расслаивают экосистему и комьюнити. Я стараюсь избегать таких вещей. В разработке хватает инструментов и проблем вызванных их обилием. Однако я понимаю почему данный инструмент необходим комьюнити. Уверен и вы поймете, если вспомните что в марте 2020 GitHub/Microsoft поглотили компанию npm. Именно в марте 2020 был написан первый коммит corepack, который тогда назывался pmm. Поэтому я считаю, что corepack это правильный шаг без которого слишком легко оказаться в тупике развития моностэка технологий.
Что нужно знать об Error stack trace?
#nodejs_api

В JavaScript есть встроенный объект Error, который сохраняет stack trace (на русском трассировка стека). Так называется список методов, которые были вызваны до момента, когда в приложении произошло ошибка. Он доступен как error.stack и выглядит так:
Error: Things keep happening!
at /home/user/file.js:525:2
at Frobnicator.refrobulate (/home/user/business-logic.js:424:21)
at Actor.<anonymous> (/home/user/actors.js:400:8)
at increaseSynergy (/home/user/actors.js:701:6)

Плохой практикой является использование throw c литералами или объектами не наследниками Error. У них не будет stack trace.

По умолчанию длина стэка ограничена 10 методами. Параметр можно изменить на уровне кода через Error.stackTraceLimit. На уровне v8 существует флаг --stack-trace-limit. Его можно передать как аргумент или через переменную окружения NODE_OPTIONS=--stack-trace-limit=10

Начиная с 12-ой версии Node.js у нас есть поддержка Async Stack Traces. Пример stack trace:
Error: Oops
at bar (/workspace/test.js:11:9)
at async run (/workspace/test.js:5:3)
Для поддержки этих stack trace рекомендуется делать await перед return. Подробней в nodebestpractices.

Если код скомпилирован с помощью babel или typescript, то правильным будет показывать stack trace для исходного кода, а не скомпилированного. Для этого использовался пакет source-map-support. Под капотом идет использование Error.prepareStackTrace. Однако сейчас Node.js умеет это делать из коробки с помощью флага --enable-source-maps
👍3
Как сделать object deep clone?
#nodejs_api

Как вы знаете объекты в отличие от примитивов передаются по ссылке, а не по значению. Поэтому, когда вам нужна копия объекта, приходиться прибегать к определенным ухищрениям.

Native JavaScript предлагает два варианта копирования:
– Через spread оператор const copied = { ...original }
– Через Object.assign const copied = Object.assign({}, original)

Эти варианты работают только на первом уровне объекта. Для глубокого копирования необходимо использовать рекурсию или пакеты (см lodash.cloneDeep). В простых случаях можно использовать JSON.parse(JSON.stringify(obj)), но имейте ввиду так можно потерять данные.

Текущий способ для Node.js это использование модуля v8:
const v8 = require('v8');
const copied = v8.deserialize(v8.serialize(original))

Но через год это измениться. Задача настолько часто встречается, что разработчики JS движков придумали structuredClone. Пока внедрено только в Firefox 94 и Node.js v17.
👍2
Как проверить, что строка timezone или encoding?
#nodejs_api

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

function isTimezone(str) {
try {
Intl.DateTimeFormat(undefined, { timeZone: str });
return true;
} catch (ex) {
return false;
}
}

Для проверки кодировки (encoding) можно использовать Buffer.isEncoding.

Собственно цель сегодняшнего рецепта напомнить, что часть buildin modules в #nodejs являются глобальными и их не нужно импортировать.
Как отследить работу Garbage Collector?
#web_api #nodejs_api

Плохая организация JavaScript кода может привести к утечке памяти. Для отслеживания утечки ресурсов в ES2021 появился FinalizationRegistry. Так можно вызвать callback после отработки Garbage Collector по указанному ресурсу:
const registry = new FinalizationRegistry((startTime) => {
const delay = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(`foo was garbage collected after ${delay}s`);
});
let foo = {};
registry.register(foo, Date.now());
foo = undefined; // Clear strong reference

⚠️Пример максимально упрощен. При запуске без массового создания новых объектов ждать сборки мусора придется долго.

Авторы не рекомендует использовать для построение бизнес логики, реализация сборки мусора, зависит от реализации любого конкретного движка JavaScript.

Работает в #nodejs 14.6.0 и новее. Для работы в TypeScript необходимо включить es2021.weakref.

Ссылки:
🔗 пример для запуска в браузере
🔗 MDN
🔗 TC39
🔗 Can I use
🔥6
Как работать с Event Emitter в async/await стиле?
#nodejs_api

В 2022 году асинхронных #nodejs код строиться на async/await. Однако важный модуль Event Emitter построен на callback-ах. Начиная с 12.16 версии в нем есть встроенный функционал для упрощения работы с Promise. За это отвечают две функции:
events.on(emitter, eventName[, options]) – возвращает AsyncIterator, который легко итерировать конструкцией for await
events.once(emitter, name[, options]) – возвращает Promise, который будет разрешен по событию.
В 16 Node.js добавили options для передачи AbortController.

Упрощенный пример кода демонстрирующий обе функции:
const events = require('events');
const { setTimeout } = require('timers/promises');
const server = require('http').createServer();

async function handle(req, res) {
await setTimeout(200);
res.end('Hello World');
}

async function bootstrap() {
server.listen(8080);
await events.once(server, 'listening')
for await (const [req, res] of events.on(server, 'request')) {
await handle(req, res);
}
}

bootstrap();

⚠️В пример await handle, сделано для демонстрации работы AsyncIterator. В результате такой конструкции HttpServer будет обрабатывать запросы не параллельно, а последовательно. Чтобы это исправить можно убрать await на этой строке.

Ссылки:
⚙️ Код на runkit
📕 Документация events
👩‍🍳 Обзор AbortController
👍18👎1
Как узнать является ли текущий файл точкой запуска?
#nodejs_api

Точка запуска (entry point) это файл, который указывается в качестве аргумента в команде node file_name.js. Зачем знать является ли текущий файл точкой запуска? Чтобы правильно организовывать код. Например, вы можете стартовать web server или экспортировать его для последующего использования в тестах.

Для проверки с CommonJS (стандарт в Node.js) используется require.main или его аналог process.mainModule. Пример, кода:
if (require.main === module) {
console.log('Entry point');
} else {
console.log('Not entry point');
}

Для ES модулей код выглядит так:
import { fileURLToPath } from 'url';
if (process.argv[1] === fileURLToPath(import.meta.url)) {
console.log('Entry point');
} else {
console.log('Not entry point');
}
👍29
Топ-5 заблуждений об работе JavaScript
#nodejs_api

На календаре пятница, а значит пора сбавить градус практичности. Сегодня поделюсь "перлам" с собеседований. Цель поста – ликвидация безграмотность.

👎Все логические операторы (&&, ||, !) возвращают булевы значения.
Вообще-то только ! возвращает Boolean.
Вопрос для выявления заблуждения, что будет выведено: console.log('0' && 3)

👎Расчет возвращаемого Promise-ом значения будет запущен при await.
Нет, он будет запущен в момент создания, т.е. как только мы создали Promise мы сразу запустили цепочку (promise chain). Где вы ставите await, и ставите ли, не имеет значения.

👎Все аргументы в функциях работают одинаково.
Как и в других языках программирования примитивные и ссылочные типы ведут себя по разному.
Вопрос для выявления заблуждения, что плохо в коде:
function getTomorrow(d) {
d.setDate(d.getDate() + 1);
return d;
}

👎Обработка событий происходит асинхронно.
Ошибка характерна и для Node.js с Event Emitter, и для FE разработчиков с Browser Events. Event handler-ы работают синхронно, причем их порядок вызова зависит от порядка добавления.

👎JavaScript обладает идеальной точностью арифметики.
Уже столько раз писали, о том что 0.1 + 0.2 == 0.3 это false, что многие это зазубрили. Однако, об этом забывают в доменах, где ошибка округления критична (финансы и т.п.). В таких случаях стоит использовать BigInt или подобные решения.
👍19🔥19
Що таке Event Loop Delay?
#nodejs_api

Node.js може обробляти одночасно кілька запитів. За це відповідає Event Loop. Синхронний код (require('fs').readFileSync) або підвищене споживання CPU (обробка великого JSON) гальмуватиме Event Loop. Для вимірювання, наскільки це відбувається, використовується метрика Event Loop Delay. Наприклад, якщо якісь endpoint блочить Event Loop Delay на 100ms, то процес не може опрацювати більше 10 таких запитів на секунду. Приклад

Чим можна вимірювати:
– існує Node.js апі perf_hooks.monitorEventLoopDelay
– це частина метрик у pm2
prom-client та його аналоги теж збирають цю метрику
👍26🔥9
​​Огляд Node.js v18: structuredClone
#nodejs_api

Як ви знаєте об'єкти на відміну від примітивів передаються за посиланням, а не за значенням. Тому потрібно вміти робити копію об'єкта. Раніше для цього використовували spread оператор чи Object.assign. Але це працювало лише для об'єктів з одним рівнем вкладеності.

Сьогодні всі сучасні браузери підтримують structuredClone, який підтримує  deep clone та кругові посилання. Він з'явиться в Node.js 18. Приклад із документації на зображенні.

У Node.js вже зараз його можна використовувати за допомогою Structured clone polyfill.
🔥35👍7
Node.js прибирає термін native modules
#nodejs_api
Документація та source code більше не використовує терміни native чи core modules. Натомість повсюдно використовується built-in modules. Нагадаю, що з 16-ї версії такі модулі можна імпортувати з префіксом node:. Наприклад:
const http = require('node:http')
🔗 Pull
👍22
Як користуватися промісіфікованими таймерами?
#nodejs_api
У Node.js v16 змінилися принципи роботи з таймерами: додано timers/promises аналогічно fs/promises. Розглянемо best practices по роботі з ними:

1️⃣ Як підключати? Ось приклад поганого коду:
import { setTimeout } from 'timers/promises';
Тут ми робимо variables shadowing, так як перевизначаємо в поточному файлі однойменну глобальну змінну setTimeout. А ще імена built-in модулів краще писати з префіксом node:. Тобто гарний код:
import { setTimeout as wait } from 'node:timers/promises';

2️⃣ Коли який таймер використовувати?
👉 Найчастіше використовується промісіфікований setTimeout. За допомогою нього реалізується затримка виконання.
👉 За допомогою промісіфікованого setInterval створюється AsyncIterable. Приклад:
for await (const startTime of setInterval(100, Date.now())) {
if ((Date.now() - startTime) > 1000) break;
// some logic
}
👉 setImmediate використовується щоб розблокувати EventLoop. Приклад

3️⃣ Порівняння з callback таймерами
👉 callback-based доступні глобально, а промісіфіковані потрібно імпортувати.
👉 У обох випадках використовується ref, щоб визначити чи буде таймер тримати event loop активним
👉 Скасування в callback-based відбувається через clearTimeout (і його аналоги), а у промісіфікованого через AbortController.
👉 В callback-based таймерах ми можемо передати аргументи для хендлера, а промісіфікований можна сказати, яке значення буде повертати проміс.

4️⃣ Ще раз про колізії імен
Мені не подобається, що імена в callback і промісах версій співпадають. Це не створює проблем у модулі fs, так як його методи не доступні глобально, але в таймерах це ускладнює підтримку коду. Як альтернативу я рекомендую використовувати об'єкт scheduler. Виглядає це так
import { scheduler } from 'node:timers/promises';
await scheduler.wait(100);
Його додали у лютому 2022 року з флагом experimental. Аналіз коду показує, що всі його методи це синтаксичний сахар:
👉 scheduler.yield() це теж саме що setImmediate() без аргументів.
👉 scheduler.wait(delay, options) це теж саме що setTimeout(delay, undefined, options) за винятком того, що опція ref не підтримується.
👍21🔥102