Основные понятия Node.js

Node.js — это среда выполнения JavaScript, построенная на движке Chrome V8 JavaScript. Проще говоря, Node.js позволяет JavaScript выполняться на стороне сервера, а не только в браузерах.

Ключевые особенности Node.js

1. Однопоточный цикл событий

  • Node.js использует модель однопоточного цикла событий
  • Обрабатывает конкурентные запросы с помощью событийно-ориентированных механизмов и функций обратного вызова
  • Избегает накладных расходов на переключение потоков, свойственных традиционному многопоточному программированию

2. Неблокирующий ввод-вывод

  • Все операции ввода-вывода (чтение/запись файлов, сетевые запросы и т. д.) являются асинхронными
  • Во время ожидания завершения операции ввода-вывода программа не блокируется и может продолжать выполнять другие задачи
  • Значительно повышает пропускную способность приложения

3. Кроссплатформенность

  • Поддерживает несколько операционных систем, таких как Windows, macOS и Linux
  • Напиши один раз — запускай где угодно

4. Богатая экосистема

  • npm (Node Package Manager) содержит миллионы пакетов с открытым исходным кодом
  • Активное сообщество разработчиков

Отличия от традиционных серверных технологий

1. Традиционные серверные технологии (например, Apache + PHP):

请求1 → 创建线程1 → 处理请求 → 返回响应 → 销毁线程1
请求2 → 创建线程2 → 处理请求 → 返回响应 → 销毁线程2
请求3 → 创建线程3 → 处理请求 → 返回响应 → 销毁线程3

2. Как Node.js обрабатывает запросы:

请求1 → 事件循环 → 处理请求 → 返回响应
请求2 → 事件循环 → 处理请求 → 返回响应  (复用同一线程)
请求3 → 事件循环 → 处理请求 → 返回响应

Характеристика Традиционная многопоточная модель Однопоточная модель Node.js
Использование памяти Каждый поток занимает около 2MB Однопоточный, низкое потребление памяти
Обработка конкурентности Количество потоков ограничивает конкурентность Цикл событий обрабатывает высокую конкурентность
Переключение контекста Издержки частого переключения потоков Нет издержек переключения потоков
Сложность программирования Требует обработки синхронизации потоков Избегает проблем с блокировками и синхронизацией потоков
Подходящие сценарии Задачи, интенсивно использующие CPU Задачи с интенсивным вводом-выводом

Сценарии применения Node.js

1. Подходящие сценарии применения

Веб-приложения

  • Сервисы RESTful API
  • Бэкенд-сервисы для одностраничных приложений (SPA)
  • Веб-приложения в реальном времени

Приложения в реальном времени

  • Чат-приложения
  • Онлайн-игры
  • Инструменты совместной работы (например, онлайн-редактирование документов)

Архитектура микросервисов

  • Легковесные микросервисы
  • API-шлюзы
  • Взаимодействие между сервисами

Инструменты и приложения командной строки

  • Инструменты сборки (например, Webpack, Gulp)
  • Инструменты скаффолдинга
  • Скрипты автоматизации

Приложения Интернета вещей (IoT)

  • Сбор данных с устройств
  • Обработка данных датчиков

2. Неподходящие сценарии применения

Задачи, интенсивно использующие CPU

  • Обработка изображений/видео
  • Сложные математические вычисления
  • Анализ больших данных

Приложения, требующие интенсивных вычислений

  • Обучение моделей машинного обучения
  • Научные вычисления
  • Майнинг криптовалют

Модель событийно-ориентированного и неблокирующего ввода-вывода

Пример традиционной блокирующей модели ввода-вывода:

Пример

// Псевдокод — блокирующие операции
// Каждый readFileSync «приостанавливает» программу, ожидая завершения чтения файла, прежде чем продолжить
const data1 = readFileSync('file1.txt');  // Программа приостанавливается здесь, ожидая завершения чтения
const data2 = readFileSync('file2.txt');  // После чтения data1 она снова приостанавливается в ожидании
const data3 = readFileSync('file3.txt');  // После чтения data2 она снова приостанавливается в ожидании
console.log('All files read');          // Эта строка выполняется только после того, как все три файла будут прочитаны
// Общее время = время чтения file1 + время чтения file2 + время чтения file3

Пример неблокирующей модели ввода-вывода Node.js:

Пример

// Асинхронные операции Node.js
const fs = require('fs');

// Три операции чтения файлов запускаются почти одновременно; программа не ждёт завершения ни одной из них
fs.readFile('file1.txt', (err, data1) => {
    // Эта функция обратного вызова будет автоматически вызвана после чтения file1.txt
    console.log('Чтение файла 1 завершено');
});

fs.readFile('file2.txt', (err, data2) => {
    // Эта функция обратного вызова будет автоматически вызвана после чтения file2.txt
    console.log('Чтение файла 2 завершено');
});

fs.readFile('file3.txt', (err, data3) => {
    // Эта функция обратного вызова будет автоматически вызвана после чтения file3.txt
    console.log('Чтение файла 3 завершено');
});

// Примечание: эта строка кода выполнится первой! Потому что три операции чтения файлов выше асинхронны,
// После отправки запросов на чтение программа сразу продолжает выполнение, не дожидаясь завершения чтения файлов
console.log('Программа продолжает выполнение, не дожидаясь чтения файлов');

// Пример фактического порядка вывода:
// Программа продолжает выполнение, не дожидаясь чтения файлов ← выводится первым
// Чтение файла 2 завершено ← какой файл завершится первым, тот и выводится первым, порядок не фиксирован
// Чтение файла 1 завершено
// Чтение файла 3 завершено
// Общее время ≈ время самого медленного из трёх файлов (конкурентное выполнение, а не последовательное накопление)

Механизм цикла событий:

Цикл событий — это основной механизм, с помощью которого Node.js реализует неблокирующий ввод-вывод. Он работает непрерывно, постоянно проверяя ожидающие задачи, благодаря чему достигается эффект "однопоточной конкурентности":

  1. Стек вызовов: выполняет синхронный код. Вызовы функций помещаются в стек вызовов и извлекаются из него после выполнения.
  2. Очередь событий: хранит колбэки от завершённых асинхронных операций, которые должны выполняться по порядку, когда стек вызовов пуст.
  3. Цикл событий: непрерывно отслеживает стек вызовов и очередь событий. Как только стек вызовов становится пустым, он берёт колбэк из очереди событий и помещает его в стек вызовов для выполнения.

Цикл событий обрабатывает следующие фазы в фиксированном порядке:

┌───────────────────────────┐
┌─>│           timers          │  ← 执行 setTimeout、setInterval 的到期回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  ← 执行上一轮循环中延迟的 I/O 回调(如某些系统错误回调)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  ← Node.js 内部使用,开发者一般不需要关心
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │  ← 核心阶段:获取新的 I/O 事件并执行其回调(如文件读取、网络请求完成)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │  ← 执行 setImmediate 的回调(在 poll 阶段之后立即执行)
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │  ← 执行关闭事件的回调,如 socket.on('close', ...)
   └───────────────────────────┘

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


Среда выполнения JavaScript

Введение в движок V8

V8 — это высокопроизводительный движок JavaScript, разработанный Google и являющийся ключевым компонентом браузера Chrome.

Node.js использует движок V8 для выполнения кода JavaScript.

Особенности движка V8:

Компиляция Just-in-Time (JIT)

  • Компилирует код JavaScript напрямую в машинный код
  • Не требуется промежуточный байт-код, что обеспечивает более высокую эффективность выполнения

Сборка мусора

  • Автоматическое управление памятью; разработчикам не нужно вручную освобождать память
  • Использует поколенческий алгоритм сборки мусора, разделяя память на молодое поколение и старое поколение для отдельного управления, что повышает эффективность сборки

Методы оптимизации

  • Inline Caching: кэширует результаты поиска свойств объекта, чтобы избежать повторных поисков
  • Hidden Classes: статизируют структуру объектов динамических языков, чтобы ускорить доступ к свойствам
  • Dynamic Optimization: анализирует горячий код во время выполнения и выполняет целенаправленную оптимизацию

JavaScript в браузере vs JavaScript в Node.js

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

Сходства:

  • Оба используют одинаковый синтаксис JavaScript
  • Оба поддерживают возможности ES6+
  • Оба используют движок V8 (браузер Chrome)

Различия:

Характеристика JavaScript в браузере JavaScript в Node.js
Глобальный объект window global
Система модулей Модули ES6, AMD CommonJS, модули ES6
Доступ к файловой системе Недоступен (из-за ограничений безопасности) Полный доступ
Сетевые запросы XMLHttpRequest, Fetch модули http, https
Манипуляции с DOM Поддерживается (манипуляции с элементами страницы) Не поддерживается (на стороне сервера нет страниц)
Управление процессами Не поддерживается Поддерживается (можно читать переменные окружения, завершать процесс и т. д.)

Пример браузерного окружения:

Пример

// Глобальный объект и операции с DOM в браузере
console.log(window);                      // Глобальный объект браузера, содержит все API браузера
document.getElementById('app');           // Получить элемент страницы по ID (недоступно в Node.js)
localStorage.setItem('key', 'value');     // Локальное хранилище браузера (недоступно в Node.js)

Пример окружения Node.js:

Пример

// Глобальный объект и файловые операции в Node.js
console.log(global);                              // Глобальный объект Node.js (соответствует window в браузере)
const fs = require('fs');                         // Импорт встроенного модуля файловой системы (недоступен в браузере)
fs.readFile('data.txt', 'utf8', callback);        // Чтение локального файла на сервере (недоступно в браузере)

Различия в глобальных объектах

Глобальный объект в браузере:

Пример

// Браузерное окружение
console.log(this === window);  // true, this верхнего уровня — это объект window
var globalVar = 'hello';
console.log(window.globalVar);  // 'hello', переменные, объявленные с помощью var, становятся свойствами window

Глобальный объект в Node.js:

Пример

// Среда Node.js
// Примечание: в Node.js каждый файл является независимым модулем, и this внутри модуля не равен global
console.log(this === global);  // false (в области видимости модуля this указывает на module.exports)
var globalVar = 'hello';
console.log(global.globalVar);  // undefined, переменные внутри модуля не привязываются автоматически к global

// Область видимости модуля в Node.js
console.log(this);  // {} пустой объект, то есть начальное значение module.exports
console.log(module.exports === this);  // true, this внутри модуля указывает на module.exports

Глобальные переменные, специфичные для Node.js:

Пример

console.log(__dirname);   // Абсолютный путь к каталогу, содержащему текущий модуль, например, /home/user/project
console.log(__filename);  // Абсолютный путь к файлу текущего модуля, например, /home/user/project/app.js
console.log(process);     // Объект process, позволяет читать аргументы командной строки (process.argv), переменные окружения (process.env) и т. д.
console.log(Buffer);      // Конструктор для работы с бинарными данными, обычно используется для чтения/записи файлов и сетевого взаимодействия

Преимущества и ограничения Node.js

Подробные преимущества

1. Высокая производительность при обработке одновременных подключений

Традиционному многопоточному серверу требуется около 20GB памяти для обработки 10,000 одновременных подключений (около 2MB на поток), тогда как Node.js требует очень мало памяти для обработки такого же количества подключений.

Пример

// Пример высокой конкурентности Node.js
const http = require('http');

const server = http.createServer((req, res) => {
    // Имитация асинхронной операции (например, запроса к базе данных), возвращающей ответ через 100ms
    // В течение этих 100ms цикл событий может продолжать обрабатывать другие запросы, не блокируясь
    setTimeout(() => {
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('Hello World\n');
    }, 100);
});

server.listen(3000, () => {
    console.log('Server running at http://localhost:3000/');
});

// Благодаря неблокирующему I/O и механизму цикла событий этот сервер может обрабатывать тысячи запросов одновременно без блокировки

2. Быстрая разработка

Унифицированный стек языка JavaScript (и фронтенд, и бэкенд используют JavaScript) делает разработку фронтенда и бэкенда более эффективной. Разработчикам нужно освоить только один язык, чтобы писать код как для фронтенда, так и для бэкенда, и они могут совместно использовать логику кода между двумя сторонами:

Пример

// Одну и ту же логику валидации можно повторно использовать как на фронтенде, так и на бэкенде без переписывания
function validateEmail(email) {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
}

// Использование на фронтенде (выполняется в браузере): когда пользователь отправляет форму, сначала проверьте её на фронтенде, чтобы не отправлять недействительные запросы на сервер
if (validateEmail(userInput)) {
    // Проверка пройдена, отправляем на сервер
}

// Использование на бэкенде (выполняется в Node.js): сервер снова проверяет, чтобы предотвратить прямые API-запросы в обход фронтенда
if (validateEmail(req.body.email)) {
    // Проверка пройдена, сохраняем в базу данных
}

3. Преимущества единого языкового стека

  • Повторное использование кода: фронтенд и бэкенд могут совместно использовать служебные функции, логику валидации данных, определения констант и т. д.
  • Эффективность команды: разработчики могут одновременно заниматься и фронтенд-, и бэкенд-разработкой, снижая затраты на коммуникацию
  • Упрощённый технологический стек: нужно освоить только язык JavaScript, что снижает затраты на обучение и техническую сложность

4. Богатая экосистема npm

# npm 提供了数百万个开源包,几乎任何功能都能找到现成的库
npm search express     # 搜索名为 express 的包
npm install express    # 安装 express 包到当前项目
npm update            # 更新当前项目中所有包到最新兼容版本

Анализ ограничений

1. Проблемы производительности при выполнении CPU-интенсивных задач

Пример

// Плохой пример: CPU-интенсивные задачи блокируют цикл событий
// fibonacciSync — это чисто вычислительная задача, которая постоянно занимает CPU, и в течение этого времени цикл событий не может обрабатывать другие запросы
function fibonacciSync(n) {
    if (n < 2) return n;
    return fibonacciSync(n - 1) + fibonacciSync(n - 2);
}

// Это заблокирует всё приложение на несколько секунд, в течение которых невозможно будет ответить на все запросы
console.log(fibonacciSync(40));

// Решение: используйте Worker Threads для выполнения CPU-интенсивных задач в отдельных потоках,
// так цикл событий главного потока не будет заблокирован
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
    // Главный поток: создать Worker и передать ему аргумент { n: 40 }
    // Worker выполнит текущий файл в отдельном потоке (__filename указывает на путь к текущему файлу)
    const worker = new Worker(__filename, {
        workerData: { n: 40 }
    });
   
    // Прослушивать сообщения, отправленные Worker (результат вычисления)
    worker.on('message', (result) => {
        console.log('Result:', result);  // После того как Worker завершит вычисление, главный поток получает результат здесь
    });
} else {
    // Поток Worker: получить параметры, переданные из главного потока через workerData, выполнить вычисление, затем отправить результат
    const result = fibonacciSync(workerData.n);
    parentPort.postMessage(result);  // Отправить результат обратно в главный поток
}

2. Проблема ада колбэков

Пример

// Пример ада колбэков: когда несколько асинхронных операций нужно выполнить последовательно, колбэки продолжают вкладываться друг в друга, делая код крайне нечитаемым
fs.readFile('file1.txt', (err, data1) => {
    if (err) throw err;
    fs.readFile('file2.txt', (err, data2) => {   // Первый уровень вложенности
        if (err) throw err;
        fs.readFile('file3.txt', (err, data3) => {   // Второй уровень вложенности
            if (err) throw err;
            // В реальных проектах такая вложенность может достигать десятка или более уровней, серьёзно влияя на сопровождаемость кода
            console.log('Все файлы успешно прочитаны');
        });
    });
});

// Современное решение: используйте Promise + async/await, чтобы сделать асинхронный код таким же интуитивно понятным, как синхронный
const fsPromises = require('fs').promises;

async function readFiles() {
    try {
        // await будет ждать чтения каждого файла, но вся функция не блокирует цикл событий
        // Примечание: здесь чтение выполняется последовательно; чтобы читать несколько файлов параллельно, используйте Promise.all()
        const data1 = await fsPromises.readFile('file1.txt');
        const data2 = await fsPromises.readFile('file2.txt');
        const data3 = await fsPromises.readFile('file3.txt');
        console.log('Все файлы успешно прочитаны');
    } catch (err) {
        // try/catch может единообразно обрабатывать все асинхронные ошибки, без необходимости проверять отдельно в каждом колбэке
        console.error('Не удалось прочитать файлы:', err);
    }
}

3. Хрупкость однопоточности

Пример

// Неперехваченное исключение приведёт к краху всего процесса Node.js
// В отличие от многопоточности, одна необработанная ошибка в однопоточной среде остановит весь сервис
setTimeout(() => {
    throw new Error('Неперехваченное исключение');  // Это приведёт к краху всего приложения, и все запросы перестанут отвечать
}, 1000);

// Решение: зарегистрировать глобальный обработчик исключений, чтобы логировать ошибки и корректно завершить работу до краха приложения
process.on('uncaughtException', (err) => {
    console.error('Неперехваченное исключение:', err);
    // Здесь сначала следует залогировать ошибку, затем завершить процесс, чтобы избежать молчаливых падений, из-за которых проблемы трудно диагностировать
    process.exit(1);  // Код выхода 1 указывает на аварийное завершение
});

process.on('unhandledRejection', (reason, promise) => {
    // Срабатывает, когда Promise отклонён, но не имеет обработчика .catch()
    console.error('Необработанное отклонение Promise:', reason);
    process.exit(1);
});

Анализ подходящих сценариев

1. RESTful API-сервис

Пример

// Используйте фреймворк Express для создания RESTful API-сервиса
// Сначала нужно установить express: npm install express
const express = require('express');
const app = express();

app.use(express.json());  // Middleware: автоматически разбирает JSON-данные в теле запроса

// GET-запрос: Получить список пользователей (соответствует HTTP-методу GET, путь /api/users)
app.get('/api/users', (req, res) => {
    const users = [
        { id: 1, name: 'Чжан Сань' },
        { id: 2, name: 'Ли Сы' }
    ];
    res.json(users);  // Возвращает данные в формате JSON
});

// POST-запрос: создание нового пользователя (клиент отправляет информацию о пользователе в теле запроса)
app.post('/api/users', (req, res) => {
    const newUser = req.body;          // Получить JSON-данные, отправленные клиентом
    // В реальных проектах информация о пользователе обычно сохраняется здесь в базе данных
    res.status(201).json({ message: 'Пользователь успешно создан', user: newUser });
});

// PUT-запрос: обновление информации указанного пользователя (:id — параметр маршрута, например, /api/users/1 означает обновление пользователя с ID 1)
app.put('/api/users/:id', (req, res) => {
    const { id } = req.params;         // Получить ID пользователя из пути
    const updatedData = req.body;      // Получить содержимое обновления из тела запроса
    res.json({ message:`Пользователь ${id}успешно обновлён`, data: updatedData });
});

// DELETE-запрос: удалить указанного пользователя
app.delete('/api/users/:id', (req, res) => {
    const { id } = req.params;
    res.json({ message:`Пользователь ${id}успешно удалён`});
});

app.listen(3000, () => {
    console.log('API-сервер запущен по адресу http://localhost:3000/');
});

2. Приложения реального времени

Пример

// Чат в реальном времени через WebSocket (с использованием библиотеки socket.io)
// Сначала нужно установить: npm install socket.io
const io = require('socket.io')(server);

// Прослушивать события подключения клиентов, срабатывающие каждый раз, когда подключается новый пользователь
io.on('connection', (socket) => {
    // Прослушивание события 'chat message', отправленного текущим подключенным клиентом
    socket.on('chat message', (msg) => {
        io.emit('chat message', msg);  // Рассылка сообщения всем онлайн-клиентам (включая отправителя)
    });
});

3. Промежуточное ПО и прокси-сервисы

Пример

// Пример обратного прокси-сервера HTTP
// Перенаправление запросов /api/xxx с фронтенда на реальный сервер backend API, скрывая реальный адрес; обычно используется для решения проблем CORS
// Сначала нужно установить: npm install http-proxy-middleware
const httpProxy = require('http-proxy-middleware');

const proxy = httpProxy({
    target: 'http://api.example.com',  // Адрес реального сервера backend API
    changeOrigin: true,                // Изменять Host в заголовке запроса на адрес назначения
    pathRewrite: {
        '^/api': ''                    // Удалите префикс /api из пути перед пересылкой, например, /api/users → /users
    }
});
Другие расширения