Skip to content

Node.js

Sᴛѧʀʟɪɴɢ edited this page Feb 24, 2019 · 1 revision

Что такое NodeJS

Node.js представляет среду выполнения кода на JavaScript, которая построена на
основе движка JavaScript Chrome V8, который позволяет транслировать вызовы на языке
JavaScript в машинный код.

В основе Node.js лежит событийно-управляемая модель с неблокирующими операциями I/O,
что делает её легкой и эффективной.

В Node.js используется libuv — кросс-платформенная библиотека поддержки с акцентом
на асинхронный ввод-вывод. С точки зрения разработчика, Node.js однопоточна, но под капотом libuv использует треды,
события файловой системы, реализует цикл событий, включает в себя тред-пулинг и так далее.
В большинстве случаев вы не будете взаимодействовать с libuv напрямую.

NodeJS прежде всего предназначен для создания серверных приложений на языке JavaScript.

Инструмент REPL (Read Eval Print Loop) представляет возможность запуска выражений на языке
JavaScript в командной строке или терминале.

В NodeJS недоступны такие базовые элементы языка JS как DOM и объекты как window, document.

Модули

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

Функция require("имя модуля / путь") используется для загрузки модулей.

Объект module.exports - это то, что возвращает функция require() при получении модуля.

Объект module представляет ссылку на текущий модуль, а его свойство module.exports
определяет экспортируемые свойства и методы модуля**.

Также в module:

  • module.filename - полное (fully resolved) имя модуля
  • module.id - уникальный идентификатор модуля, обычно совпадает с module.filename
  • module.loaded - загружен ли модуль на данный момент
  • module.children - модули, импортированные текущим модулем впервые
  • module.parent - модуль, в котором импортирован текущий модуль
  • module.require(id) - вызов require относительно модуля, на который указывает module

Подключаемые модули кэшируются, что увеличивает производительность, но может привести
к проблемам:

var greeting1 = require("./greeting.js");
console.log(`Hello ${greeting1.name}`); //Hello Alice

var greeting2 = require("./greeting.js");
greeting2.name= "Bob";

console.log(`Hello ${greeting2.name}`); //Hello Bob
// greeting1.name тоже изменилось
console.log(`Hello ${greeting1.name}`); //Hello Bob

module.exports vs exports

Переменная exports доступна в пределах области видимости модуля на уровне файла.
Ей присваивается значение из module.exports до оценки (evaluate) модуля.

module.exports и exports ссылаются на один и тот же объект, поэтому нет
разницы между ними в присвоении свойств - результат будет тем же.
Тем не менее переменную exports (как и любую другую переменную) можно переопределить.
И если это сделать, то она потеряет связь с module.exports.
Поэтому переменную exports лучше не использовать.

module.exports.hello = true; // будет экспортировано из модуля
exports = { hello: false };  // не будет экспортировано

Запись в exports не меняет то, что будет экспортировано.

Имитация require:

function require(/* ... */) {
  const module = { exports: {} };
  ((module, exports) => {
    function someFunc() {}
    exports = someFunc; // теряется связь с module.exports, модуль экспортирует {}
    module.exports = someFunc; // модуль экспортирует someFunc
  })(module, module.exports);
  return module.exports;
}

Объект global и глобальные переменные

Node.js предоставляет специальный объект global, который предоставляет доступ к
глобальным (доступным из каждого модуля) переменным и функциям.

// в одном модуле
global.date = new Date();
val = 123;
// в другом модуле
console.log(global.val);
console.log(date);

Также в каждом модуле доступны глобальные переменные __dirname и __filename;
глобальные объекты console, module, process и многие другие.

Передача параметров приложению

При запуске приложения из терминала мы можем передавать ему параметры.
Для получения параметров используется массив process.argv.

// в терминале
node app.js starling
// в app.js
let nodePath = process.argv[0]; // путь к файлу node.exe, вызывающему приложение
let appPath = process.argv[1]; // путь к приложению
let name = process.argv[2]; // кастомный параметр, значение starling

Асинхронное программирование в NodeJS

Асинхронность представляет возможность одновременно выполнять сразу несколько задач.

Слово «асинхронный» или «async» значит, что оно «занимает какое-то время» или
«случится в будущем, не сейчас».

Асинхронный ввод-вывод  —  это форма обработки ввода/вывода, позволяющая продолжить
обработку других задач, не ожидая завершения передачи.

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

Функции, которые могут принимать другие функции в качестве аргументов,
называются функциями высшего порядка.

Так появились функции обратного вызова (callback): если вы передаете функцию
другой функции в качестве параметра, вы можете вызвать её внутри функции,
когда она закончит свою работу. Нет необходимости возвращать значения,
нужно только вызывать другую функцию с этими значениями.

Особенности использования такого подхода:

  • обработка ошибок: вместо блока try-catch вы проверяете ошибку в колбеке
  • отсутствует возвращаемое значение: асинхронные функции не возвращают значения,
    но значения будут переданы в колбеки

Функция callback принимает два параметра: информацию об ошибке и данные.
Это общая модель функций обратного вызова, которые передаются в асинхронные
методы: сперва ошибка, затем данные.
Этот принцип называется "Error First".

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

const displaySync = callback => ( callback() );

console.log("Начало работы программы"); // I
setTimeout(() => console.log("timeout 500"), 500); // V
setTimeout(() => console.log("timeout 100"), 100); // IV 
displaySync(() => console.log("without timeout")); // II
console.log("Завершение работы программы"); // III

Callback Hell

Колбэки могут иметь большую вложенность, что порождает трудно поддерживаемый код.

Есть три правила, следуя которым можно избежать проблему.

Не используйте большую вложенность

Куски связного кода можно выносить в функции и давать им понятные имена.

Модульность

Цитируя Исаака Шлютера (из проекта Node.js): «Пишите маленькие модули, каждый из
которых будет выполнять одну функцию и собирайте их в более крупные модули.
Вы не сможете попасть в ад обратных вызовов, если не пойдёте туда сами».

Обработка ошибок

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

У обратных вызовов самым популярным соглашением обработки ошибок является стиль Node.js,
где первый параметр всегда резервируется для ошибок.
Это придумано для того, чтобы не было возможности забыть обработать ошибку.

http://callbackhell.ru/

Цикл событий

Цикл событий лежит в основе Node.js и JavaScript и отвечает за планирование
асинхронных операций.

Программирование с управлением по событиям (event-driven programming)
представляет собой парадигму программирования, в которой поток выполнения
программы определяется событиями: действия пользователя (щелчки мышью,
нажатия клавиш), выходы датчиков или сообщения из других программ/потоков.

Если кратко, то приложения реагируют на события.

С точки зрения разработчика Node.js является однопоточным. Это означает, что вам
не нужно иметь дело с потоками и синхронизировать их, Node.js** скрывает эту**
сложность за абстракцией. Всё, кроме кода, выполняется параллельно.

Среда выполнения JavaScript включает в себя кучу (динамически распределяемую память)
и стек вызовов (call stack).

В исходниках V8 нет setTimeout, AJAX, DOM, они - расширения браузера.

Однопоточность означает, что можно делать одно действие в один момент времени:
Один поток -> один стек вызовов -> одна вещь в единицу времени.

JavaScript - однопоточная runtime среда выполнения.

Стек вызывов имеет предельную глубину в 16 000, после чего Chrome очищает стек.

Хоть JavaScript движок может делать одну вещь в единицу времени, браузер - это больше,
чем среда выполнения. Он имеет дополнительные возможности - Web APIs. Это по сути
треды (потоки). Им можно оправлять запросы с просьбой что-то сделать. И, параллельно с основным движком JS, они выполняют свою работу.

Аналогичная схема справедлива и для Node.js, только вместо Web APIs там C++ APIs.

Когда Web API отрабатывает, результат помещается в очередь задач (task queue).

У Event Loop есть одна очень простая задача: он смотрит на стек и очередь задач. Если
есть задачи и стек пуст, то он перетаскивает задачу в стек. А стек - это уже часть
движка JS.

Рендер работает так же, как и асинхронные cb, - через очередь. Разница в том, что функция рендера имеет более высокий приоритет по сравнению с
обычными колбэками.

Каждые 16 мс в task queue попадает очередная функция render.
Она ждёт опустошения стека и выполняет перерендеринг.
Рендер не может произойти, пока какой-то код выполняется в call стеке.

Работа с файлами

Для работы с файлами предназначен модуль fs.

const fs = require("fs");
// синхронное чтение файла
const fileContent = fs.readFileSync("1.txt", "utf8");
// асинхронное чтение файла (не блокирует основной поток и выполняет cb в конце)
fs.readFile("1.txt", "utf8", (error,data) => { /*...*/ });
// синхронная запись в файл (перезапись файла)
fs.writeFileSync("1.txt", "Hi");
// асинхронная запись в файл (перезапись файла)
fs.writeFile("1.txt", "Hi", error => { /*...*/ });
// синхронное добавление записи в файл
fs.appendFileSync("1.txt", "Hi");
// асинхронное добавление записи в файл
fs.appendFile("1.txt", "Hi", error => { /*...*/ })

События

Большая часть функционала Node.js применяет асинхронную событийную архитектуру,
которая использует специальные объекты - эмиттеры для генерации различных событий,
которые обрабатываются специальными функциями - обработчиками или слушателями событий.

Все объекты, которые генерируют события, представляют экземпляры класса EventEmitter.

const EventEmitter = require("events");
let emitter = new EventEmitter();
// с помощью функции on() связываем событие и действие
emitter.on("greet", data => console.log(data));
// с помощью функции emit() генерируем событие
emitter.emit("greet", "Hi");

Наследование от EventEmitter:

const util = require("util");
const EventEmitter = require("events");
 
// ES5 + arrow function
function User() {}
util.inherits(User, EventEmitter);
User.prototype.sayHi = data => this.emit("great", data);
// ES6
class User extends EventEmitter {
    sayHi(data) { this.emit(eventName, data) }
}

let user = new User();
user.on("great", data => console.log(data)); // стрелочная функция теряет this на EventEmitter
user.sayHi("Max");

С помощью метода eventEmitter.once(), можно зарегистрировать обработчик событий,
который вызывается всего один раз для конкретного события.

При возникновении ошибки в экземпляре EventEmitter, для события 'error' генерируется
типичное действие. Такие случаи считаются особыми в Node.js.
Если EventEmitter не имеет ниодного обработчика событий для события 'error', и оно
генерируется, то выдается ошибка, печатается трассировка стека, и процесс Node.js завершается.

Для защиты от сбоев процесса Node.js, обработчик может быть зарегистрирован на событие
process object's uncaughtException.

EventListener вызывает всех слушателей синхронно в том порядке, в котором они были
зарегистрированы.
Но можно переключиться на асинхронный режим с помощью методов setImmediate() или
process.nextTick().

Stream и Pipe

Stream (поток данных)абстрактный интерфейс для работы с поступающими данными в Node.js.

Примеры стримов: запрос на HTTP сервер, process.stdout.

Типы стримов в NodeJS:

  • Открытый для чтения (Readable) – стримы, данные в которых могут быть прочитаны
    (например, fs.createReadStream())
  • Открытый для записи (Writable) – стримы, в которые можно записывать данные
    (например, fs.createWriteStream())
  • Спаренные (Duplex) – стримы, открытые для чтения и записи одновременно
    (например, net.Socket)
  • Трансформеры (Transform) – спаренные стримы, которые могут преобразовывать данные
    при чтении и записи (например, zlib.createDeflate())

Поток разбивается на ряд кусков или чанков (chunk). При считывании каждого такого
куска возникает событие data. С помощью метода on() мы можем подписаться на это
событие и вывести каждый кусок данных на консоль:

const fs = require("fs");
 
let writeableStream = fs.createWriteStream("hello.txt");
writeableStream.write("Привет мир!");
writeableStream.write("Продолжение записи \n");
writeableStream.end("Завершение записи");
let readableStream = fs.createReadStream("hello.txt", "utf8");
 
readableStream.on("data", chunk => console.log(chunk));

Pipe - это канал, который связывает поток для чтения и поток для записи и
позволяет сразу считать из потока чтения в поток записи.

const fs = require("fs");
let readableStream = fs.createReadStream("hello.txt", "utf8");
let writeableStream = fs.createWriteStream("some.txt");
 
// без pipe
readableStream.on("data", chunk => writeableStream.write(chunk));
// с pipe
readableStream.pipe(writeableStream);

Все стримы оперируют исключительно строками и объектами буфера Buffer.
Но есть возможность работы стримов с другими типами значений JS за иключением null,
который имеет специальное назначение в стримах.
Стримы, которые могут это делать, работают в объектном режиме (object mode).

Экземпляры стримов переключаются в объектный режим с помощью опции objectMode
при создании стрима.
Попытки переключить существующий стрим в объектный режим могут быть небезопасными.

Буферизация (Buffering)

Открытые для записи или чтения стримы сохраняют данные во внутреннем буфере, извлечь
информацию из которого можно посредством writable._writableState.getBuffer() или
readable._readableState.buffer соответственно.

Объем потенциально буферизируемых данных зависит от передачи опции highWaterMark
в конструктор стримов
.
Для нормальных стримов опция задает общее количество байт, для стримов, работающих в
объектном режиме
, - общее количество объектов.

Данные буферизируются в открытых для чтения стримах, когда при реализации вызывается
stream.push(chunk).

Если получатель стрима не вызывает stream.read(), то данные остаются во внутренней
очереди, пока не будут получены.

Когда общий размер внутреннего читаемого буфера достигает значения, заданного
highWaterMark, стрим временно останавливает чтение данных из предустановленного
ресурса, пока данные, которые на данный момент буферизируются, не будут получены
(это значит, что стрим прекратит вызывать внутренний метод readable._read(), который
используется для заполнения читаемого буфера).

Данные буферизируются в открытые для записи стримы, когда постоянно вызывается метод
writable.write(chunk). Если общий размер внутреннего записывающего буфера меньше
значения, установленного highWaterMark, вызов writable.write() вернет true, иначе -
false.

Ключевая цель API stream (а в частности метода stream.pipe()) – ограничить буферизацию
данных до приемлемых уровней, таких, чтобы источники и направления различных
скоростей не подавляли доступную память.

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

https://js-node.ru/site/article?id=41

I/O

I/O означает input/output.
К I/O можно отнести чтение и запись в файлы, HTTP запросы, работа с БД и многое другое.

I/O занимает время и блокирует выполнение других функций.

Чтобы провести одновременно две I/O операции, мы должны завести для каждой из них новый
поток. Но Javascript однопоточный (на самом деле - нет, но он имеет однопоточный event loop), поэтому вторая операция будет дожидаться выполнения первой (блокироваться, Blocking I/O).

Cуществуют способы не дожидаться выполнения первой I/O операции (Non-blocking I/O). Неблокирующая операция устраняет необходимость в мультипоточности, поскольку сервер
может выполнять несколько запросов одновлеменно.

I/O запросы обрабатываются в отдельном потоке в NodeJS.

Node.js runs JavaScript code in the Event Loop (initialization and callbacks), and offers a Worker Pool to handle expensive tasks like file I/O.

Clusters

https://medium.com/@willbach/principles-for-clustering-in-node-js-7f640d4c1eee https://medium.com/@CodeAndBiscuits/understanding-nodejs-clustering-in-docker-land-64ce2306afef https://medium.com/@thinkinternet/nodejs-clustering-27ba11a93b8b

https://medium.com/devschacht/node-hero-chapter-3-cae7333c7f3d https://metanit.com/web/nodejs https://js-node.ru/

Clone this wiki locally