Якщо ви шукаєте "Що таке Node.js", ймовірно, ви знайдете таке визначення:
"Node.js — це безкоштовне, з відкритим вихідним кодом, кросплатформенне середовище виконання JavaScript, яке дозволяє розробникам створювати сервери, веб-додатки, інструменти командного рядка та сценарії."
Але що насправді означає "середовище виконання JavaScript"? Щоб зрозуміти це, давайте розберемо по кроках.
Розбір середовища виконання JavaScript
Уявіть, що у вас є рецепт для найсмачнішої страви. Це шедевр. Але рецепт сам по собі не може приготувати їжу — вам потрібно:
- Шеф-кухар: Людина, яка розуміє рецепт і може приготувати страву
- Кухня: Простір з інструментами та інгредієнтами для того, щоб рецепт став реальністю
Photo by Benjamin Brunner on Unsplash
У контексті Node.js:
- JavaScript код — це рецепт
- Двигун V8 (розроблений Google) — це шеф-кухар — він компілює та виконує JavaScript код
- Середовище виконання — це кухня — воно надає інструменти (як-от API та системні ресурси), які потрібні JavaScript для роботи поза браузером
Node.js розширює JavaScript поза браузер, дозволяючи розробникам створювати сервери, взаємодіяти з файловими системами та обробляти мережеві запити.
Що включає середовище виконання?
Середовище виконання надає все необхідне для виконання JavaScript коду. Це включає:
- Виконання коду:
Інструменти для компіляції або інтерпретації коду в машинні інструкції - Бібліотеки та API:
Вбудовані модулі для виконання поширених завдань (наприклад,fs
для операцій з файлами,http
для мережі) - Управління пам'яттю:
Обробка розподілу пам'яті для змінних та функцій - Взаємодія з системою:
Взаємодія з операційною системою для роботи з файлами, мережевими запитами та іншими завданнями
Як працює Node.js: розбір екосистеми
Ось детальніше про те, як Node.js працює "під капотом":
-
Код додатку (JavaScript):
Це точка відліку. Розробники пишуть JavaScript код для виконання завдань, таких як здійснення HTTP запитів, читання файлів або виконання обчислень. Код передається в двигун V8 для виконання. -
Двигун V8 (JavaScript Engine):
- Двигун V8 компілює JavaScript у машинний код для швидкого виконання
- Він обробляє синхронні завдання, такі як математичні обчислення або виклики функцій
- Однак, сам по собі V8 не може обробляти асинхронні завдання, такі як операції з файлами або мережеві запити
- API Node.js:
Node.js надає API для серверних завдань, яких не підтримує JavaScript у браузері, таких як:
- Операції з файловою системою (
fs
модуль) - Мережеві запити (
http
модуль) - Таймери (
setTimeout
,setInterval
)
- libuv та Event Loop:
libuv — це бібліотека в Node.js, яка обробляє асинхронні операції. Вона надає:
- Event Loop (Цикл подій): Механізм для неблокуючого виконання, що забезпечує ефективну обробку завдань
- Thread Pool (Пул потоків): Набір фонових потоків для таких завдань, як операції з файлами або запити до бази даних
Приклад: Операції з файловою системою в Node.js
Щоб зрозуміти, як працює Node.js "під капотом", давайте розглянемо, як виконується звичайне завдання, таке як відкриття файлу, покроково.
Проблема: чому нам потрібні API Node.js?
Двигун V8, який забезпечує роботу Node.js, має обмежену функціональність. Він призначений переважно для ефективного виконання JavaScript коду. Хоча він може обробляти базове асинхронне програмування, у нього немає інструментів для взаємодії з файловою системою, мережею або операційною системою безпосередньо — речі, які зазвичай не потрібні в браузерному середовищі.
Коли ви пишете такий код, як fs.open
, це робить щось, що виходить за межі того, що може обробити V8.
Ось тут і вступають у гру API Node.js, прив’язки (bindings), libuv та операційна система.
Крок 1: Двигун V8 обробляє код
Подорож починається, коли двигун V8 зустрічає JavaScript код:
const fs = require('fs');
// Шлях до файлу, який ви хочете відкрити
const filePath = './example.txt';
// Відкрити файл у режимі читання
fs.open(filePath, 'r', (err, fd) => {
if (err) {
console.error('Помилка при відкритті файлу:', err.message);
return;
}
console.log(`Файл успішно відкрито. Дескриптор файлу: ${fd}`);
// Завжди закривайте файл після використання
fs.close(fd, (err) => {
if (err) {
console.error('Помилка при закритті файлу:', err.message);
} else {
console.log('Файл успішно закрито.');
}
});
});
Ось що відбувається:
- Двигун V8 виконує код по черзі
- Коли він досягає функції
fs.open
, V8 розуміє, що ця функція не є частиною рідного JavaScript. Вона надається Node.js, тому V8 передає завдання до шарів API Node.js
Крок 2: API Node.js
Функція fs.open
належить до основної бібліотеки JavaScript Node.js. Ця бібліотека надає API (як-от fs
, http
тощо), які розширюють функціональність JavaScript для серверних задач. Реалізацію fs.open
можна знайти у файлі Node.js lib/fs.js:
function open(path, flags, mode, callback) {
path = getValidatedPath(path);
if (arguments.length < 3) {
callback = flags;
flags = 'r';
mode = 0o666;
} else if (typeof mode === 'function') {
callback = mode;
mode = 0o666;
} else {
mode = parseFileMode(mode, 'mode', 0o666);
}
const flagsNumber = stringToFlags(flags);
callback = makeCallback(callback);
const req = new FSReqCallback();
req.oncomplete = callback;
binding.open(path, flagsNumber, mode, req);
}
Ось що відбувається на цьому етапі:
- Функція
fs.open
перевіряє шлях до файлу, прапори та режим - Вона створює об'єкт зворотного виклику (
FSReqCallback
), щоб обробити результат - Функція потім викликає
binding.open
, що забезпечує зв'язок між JavaScript і нижчим рівнем реалізації на C++
Ключове зауваження: API Node.js обробляє JavaScript частину операції, але для доступу до нижчої функціональності використовує прив’язки (bindings).
Крок 3: Прив’язки Node.js
На цьому етапі виклик binding.open
з'єднує API JavaScript з реалізацією функціональності на C++.
Прив’язки виконують роль перекладача, дозволяючи JavaScript викликати функціональність, написану на C або C++.
Реалізація fs.open
на C++ можна знайти у файлі Node.js src/node_file.cc:
static void Open(const FunctionCallbackInfo& args) {
Environment* env = Environment::GetCurrent(args);
const int argc = args.Length();
CHECK_GE(argc, 3);
BufferValue path(env->isolate(), args[0]);
CHECK_NOT_NULL(*path);
ToNamespacedPath(env, &path);
CHECK(args[1]->IsInt32());
const int flags = args[1].As()->Value();
CHECK(args[2]->IsInt32());
const int mode = args[2].As()->Value();
if (argc > 3) { // open(path, flags, mode, req)
FSReqBase* req_wrap_async = GetReqWrap(args, 3);
CHECK_NOT_NULL(req_wrap_async);
if (AsyncCheckOpenPermissions(env, req_wrap_async, path, flags).IsNothing())
return;
req_wrap_async->set_is_plain_open(true);
AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,
uv_fs_open, *path, flags, mode);
} else { // open(path, flags, mode)
// Синхронний шлях
}
}
Ключові моменти:
- Функція
binding.open
готує шлях до файлу, прапори та режим для подальшої обробки - Потім вона викликає
uv_fs_open
з libuv, що відповідає за виконання фактичної операції з файлом
Крок 4: libuv
libuv — це бібліотека, яку використовує Node.js для обробки асинхронних операцій вводу/виводу. Вона надає цикл подій та пул потоків, що дозволяє Node.js виконувати неблокуючі операції.
Функція uv_fs_open
в libuv виглядає так (знайдена в джерельному коді libuv):
int uv_fs_open(uv_loop_t* loop,
uv_fs_t* req,
const char* path,
int flags,
int mode,
uv_fs_cb cb) {
INIT(OPEN);
PATH;
req->flags = flags;
req->mode = mode;
if (cb != NULL)
if (uv__iou_fs_open(loop, req))
return 0;
POST;
}
Ось що відбувається:
- libuv отримує запит на операцію з файлом і готує його до виконання
- Вона взаємодіє з операційною системою для відкриття файлу, безпосередньо або через пул потоків, якщо операція асинхронна
- Як тільки операція завершується, libuv повідомляє цикл подій
Крок 5: Взаємодія з операційною системою
Зрештою, libuv безпосередньо взаємодіє з операційною системою для виконання операції з файлом. На Linux це включає виклик системного виклику open
:
static ssize_t uv__fs_open(uv_fs_t* req) {
return open(req->path, req->flags | O_CLOEXEC, req->mode);
}
Саме тут файл фізично відкривається, і дескриптор файлу повертається до Node.js.
Підсумок робочого процесу
- JavaScript код: Функція
fs.open
викликається у вашому JavaScript коді - Двигун V8: V8 виконує код і передає виклик
fs.open
до API Node.js - API Node.js: API обробляє частину на JavaScript та викликає C++
binding.open
- Прив’язки Node.js: З'єднують JavaScript і C++ світи
- libuv: libuv обробляє асинхронну операцію та взаємодіє з ОС
- Операційна система: Виконує операцію з файлом і повертає результат в libuv
Висновок
Розуміння того, як Node.js працює "під капотом", розкриває багаторівневу архітектуру, яка забезпечує його неблокуючі асинхронні операції. Від JavaScript до двигуна V8, API Node.js, прив’язок (bindings), libuv та операційної системи — кожен рівень грає важливу роль у забезпеченні ефективних та масштабованих додатків.
Розбираючи функцію fs.open
, ми можемо оцінити складність і елегантність Node.js. Сподіваюся, ця стаття допоможе вам зрозуміти, як Node.js з'єднує JavaScript і системне програмування.
Перекладено з: Behind the Scenes of Node.js: A High-Level Overview