Тонкости nodejs. Часть I: пресловутый app.js
Я работаю с node.js более трех лет и за это время успел хорошо познакомиться с платформой, ее сильными и слабыми сторонами. За это время платформа сильно изменилась, как, собственно, и сам javascript. Идея использовать одну среду и на сервере и на клиенте пришлась многим по душе. Еще бы! Это удобно и просто! Но, к сожалению, на практике все оказалось не так радужно, вместе с плюсами платформа впитала в себя и минусы используемого языка, а разный подход к реализации практически свел на нет плюсы от использования единой среды. Так все попытки реализовать серверный js до ноды не взлетели, взять тот же Rhino. И, скорее всего, node ждала та же участь, если бы не легендарный V8, неблокирующий код и потрясающая производительность. Именно за это его так любят разработчики. В этой серии статей, я постараюсь рассказать о неочевидных на первый взгляд проблемах и тонкостях работы, с которыми вы столкнетесь в разработке на nodejs.
Сразу оговорюсь, что рекомендации больше применимы к большим проектам со сложной структурой.
Начать хочется с наиболее часто встречаемой и распространенной реализации приложения – главной точкой входа – app.js, на примере веб-приложения с использованием express. Обычно выглядит она так:
// config.js exports.port = 8080; // app.js var express = require('express'); var config = require('./config.js'); var app = express(); app.get('/hello', function(req, res) < res.end('Hello world'); >); app.listen(config.port);
На первый взгляд все отлично, код понятный, конфигурация вынесена в отдельный файл и может быть изменена для дева и продакшна. Подобная реализация встречается на всех ресурсах посвященных созданию веб-приложений на nodejs. Вот мы и заложили фундамент нашей ошибки в десяти строках чистейшего кода. Но обо всем по порядку.
И так, мы написали hello world. Но, это чересчур абстрактный пример. Давайте добавим конкретики и напишем приложение которое будет выводить список файлов из указаной директории и отображать содержимое отдельных файлов, запрашивая данные из mongo.
// config.js exports.port = 8080; exports.mongoUrl = 'mongodb://localhost:27017/test'; // app.js var express = require('express'); var MongoClient = require('mongodb').MongoClient; var config = require('./config.js'); // Создаем соединение с базой var db; MongoClient.connect(config.mongoUrl, function(err, client) < if (err) < console.error(err); process.exit(1); >db = client; >); // Создаем веб-сервер var app = express(); app.get('/', function(req, res, next) < db.collection('files').find(<>).toArray(function(err, list)< if (err) return next(err); res.type('text/plain').end(list.map(function(file)< return file.path; >).join('\r')); >); >); app.get('/file', function(req, res, next)< db.collection('files').findOne().toArray(function(err, file)< if (err) return next(err); res.type('text/plain').end(file.content); >); >); app.listen(config.port);
Отлично, все просто и наглядно: соединяемся с базой, создаем сервер, назначаем обработчки для путей. Но давайте подумаем, какими недостатками обладает код:
- Его тяжело тестировать, так как нет возможности напрямую проверить результат возвращаемый методами.
- Его тяжело конфигурировать – невозможно изменить конфигурацию для двух экземпляров приложения.
- Компоненты приложения недоступны для внешнего кода, а значит и для расширения.
- Ошибки никуда не передаются и должны быть обработаны на месте или же выброшены на самый верхний уровень.
// app.js var MongoClient = require('mongodb').MongoClient; var EventEmitter = require('event').EventEmitter; var util = require('util'); module.exports = App; function App(config) < var self = this; // Инициализируем event emitter EventEmitter.call(this); MongoClient.connect(config.mongoUrl, function(err, db)< if (err) return self.emit("error", err); self.db = db; >); this.list = function(callback) < self.db .collection('files') .find(<>) .toArray(function(err, files)< if (err) return callback(err); files = files.map(function(file)< return file.path >); callback(null, files); >); >; this.file = function(file, callback) < self.db .collection('files') .findOne() .toArray(callback); >; > util.inherits(App, EventEmitter); // config.js exports.mongoUrl = "mongo://localhost:27017/test"; exports.http = < port : 8080 >; // http.js var App = require('./app.js'); var express = require('express'); var configPath = process.argv[2] || process.env.APP_CONFIG_PATH || './config.js'; var config = require(configPath); var app = new App(config); app.on("error", function(err)< console.error(err); process.exit(1); >); var server = express(); server.get('/', function(req, res, next)< app.list(function(err, files)< if (err) return next(err); res.type('text/plain').end(files.join('\n')); >); >); server.get('/file', function(req, res, next)< app.file(req.query.file, function(err, file)< if (err) return next(err); res.type('text/plain').end(file); >); >); server.listen(config.http.port);
- Любой метод доступен напрямую через объект app.
- Управление конфигурацией стало гибким: можно указать путь в консоли или через export APP_CONFIG_PATH=…
- Появился централизованный доступ к компонентам.
- Ошибки приложения отлавливаются объектом app и могут быть обработаны с учетом контекста.
// cli.js var App = require('./app.js'); var program = require('commander'); var app; program .version('0.1.0') .option('-c, --config ', 'Config path', 'config.js', function(configPath)< var config = require(configPath); app = new App(config); app.on("error", function(err)< console.error(err); process.exit(1); >); >); program.command('list') .description('List files') .action(function()< app.list(function(err, files)< if (err) return app.emit("error", err); console.log(files.join('\n')); >); >); program.command('print ') .description('Print file content') .action(function(cmd)< app.file(cmd.file, function(err, file)< if (err) return app.emit("error", err); console.log(file); >); >);
// test.js var App = require('App'); var app = new App(< mongoUrl : "mongo://testhost:27017/test" >); // Пусть тестовая база содержит только один документ: // app.on("error", function(err)< console.error('Test failed', err); process.exit(1); >); // Тест написан для библиотеки nodeunit module.exports = < "Test file list":function(test) < app.list(function(err, files)< test.ok(Array.isArray(files), "Files is an Array."); test.equals(files.length, 1, "Files length is 1."); test.equals(file[0], "README.md", "Filename is 'README.md'."); test.done(); >); > >
Конечно, приложение теперь выглядит не таким минималистичным, как в примерах, зато является боле гибким. В следующей части я расскажу про отлов ошибок.
Введение в Node JS
Node.js представляет среду выполнения кода на JavaScript, которая построена на основе движка JavaScript Chrome V8, который позволяет транслировать вызовы на языке JavaScript в машинный код. Node.js прежде всего предназначен для создания серверных приложений на языке JavaScript. Хотя также существуют проекты по написанию десктопных приложений (Electron) и даже по созданию кода для микроконтроллеров. Но прежде всего мы говорим о Node.js, как о платформе для создания веб-приложений.
Особенностью Node.js является то, что Node.js использует один (основной) поток, который получает все запросы и управляет ими через очередь запросов (таким образом, Node.js является однопоточным сервером). Внутри этого потока выполняется так называемый цикл событий (event loop), который представляет собой цикл, который непрерывно проверяет запросы из очереди событий и обрабатывает события ввода и вывода.
Если пользователь отправляет запрос на сервер Node.js, в цикле событий выполняется проверка, чтобы определить, требует ли следующий запрос блокирующей операции ввода или вывода (например, обращение к базе данных или файловой системе). Если нет, то запрос обрабатывается напрямую, и пользователю отправляется результат обработки.
Если же запрос требует блокирующей операции ввода/вывода, для выполнения этой операции запускается один из нескольких внутренних обработчиков Node.js (потоков). В обработчики передается функция обратного вызова, которая, в свою очередь, вызывается, как только будет выполнена блокирующая операция ввода/вывода.
При этом основной поток не останавливается во время блокирующих операций ввода/вывода: цикл событий постоянно совершает свои обходы, проверяет новые запросы и так далее. Благодаря этой архитектуре Node.js предотвращает создание все большего количества потоков и чрезмерного использования памяти.
Node.js является открытым проектом, исходники которого можно посмотреть на github.com.
Установка
Для загрузки перейдет на официальный сайт https://nodejs.org/en/. На главной странице мы сразу увидим две возможные опции для загрузки: самая последняя версия NodeJS и LTS-версия.

Загрузим LTS-версию. В моем случае это версия 20.9.0. Для Windows установщик представляет файл с расширением msi. После запуска откроется программа установщика:

После успешной установки вы можем ввести в командной строке/терминале команду node -v , и нам отобразится текущая версия node.js:
C:\Users\eugen>node -v v20.9.0
Версии node.js для других операционных систем наряду с исходниками можно найти по адресу https://nodejs.org/en/download/
Инструменты разработки
Для разработки под Node JS достаточно простейшего текстового редактора. Можно взять любой, например, Visual Studio Code.
REPL
После установки NodeJS нам становится доступным такой инструмент как REPL. REPL (Read Eval Print Loop) представляет возможность запуска выражений на языке JavaScript в командной строке или терминале.
Так, запустим командную строку (на Windows) или терминал (на OS X или Linux) и введем команду node . После ввода этой команды мы можем выполнять различные выражения на JavaScript:
C:\WINDOWS\system32>node Welcome to Node.js v20.9.0 Type ".help" for more information. > 2+6 8 >
Или используем какую-нибудь функцию JS:
> console.log("Hello METANIT.COM"); Hello METANIT.COM undefined >
Можно определять свои функции и затем их вызывать, например, возведение числа в квадрат:
> function square(x) undefined >square(5) 25 >
Если мы введем что-то неправильно, то REPL укажет об ошибке:

Выполнение файла
Вместо того чтобы вводить весь код напрямую в консоль, удобнее вынести его во внешний файл. Например, создадим на жестком диске новый каталог, допустим, C:\node\ , в который поместим новый файл app.js со следующим кодом:
console.log("Hello world");
В командной строке перейдем с помощью команды cd к каталогу C:\node, а затем выполним команду:
node app.js
Данная команда выполнит код из файла app.js:
Создаём новое React-приложение
These docs are old and won’t be updated. Go to react.dev for the new React docs.
See Start a New React Project for the recommended ways to create an app.
Используйте встроенный набор инструментов для лучшего взаимодействия пользователя и разработчика.
На этой странице описано несколько популярных наборов инструментов React, которые помогают в таких задачах как:
- Масштабирование до большого количества файлов и компонентов.
- Использование сторонних библиотек из npm.
- Раннее обнаружение распространённых ошибок.
- Отражение изменений CSS и JS на лету в процессе разработки.
- Оптимизация кода для продакшена.
Рекомендованные на этой странице инструменты не требуют дополнительной настройки для начала работы.
Возможно, вам не нужен дополнительный набор инструментов
Если у вас нет проблем, описанных выше, или пока не чувствуете себя уверенно, используя инструменты JavaScript, рассмотрите возможность добавления React в виде простого тега на HTML-странице, при необходимости с JSX.
Также это самый простой способ добавить React в существующий веб-сайт. Вы всегда можете расширить набор инструментов, если посчитаете это нужным.
Рекомендуемый набор инструментов
Команда React в первую очередь рекомендует следующие решения:
- Если вы изучаете React или создаёте новое одностраничное приложение, используйте Create React App.
- Если вы создаёте серверный сайт с Node.js, попробуйте Next.js.
- Если вы создаёте статический контент-ориентированный сайт, попробуйте Gatsby.
- Если вы создаёте библиотеку компонентов или интегрируетесь с существующей кодовой базой, попробуйте более гибкие наборы инструментов.
Create React App
Create React App — удобная среда для изучения React и лучший способ начать создание нового одностраничного приложения на React.
Инструмент настраивает среду для использования новейших возможностей JavaScript, оптимизирует приложение для продакшена и обеспечивает комфорт во время разработки. Вам понадобятся Node.js не ниже версии 14.0.0 и npm не ниже версии 5.6 на вашем компьютере. Для создания проекта выполните команды:
npx create-react-app my-app cd my-app npm start
Create React App не обрабатывает бэкенд логику или базы данных, он только предоставляет команды для сборки фронтенда, поэтому вы можете использовать его с любым бэкэндом. «Под капотом» используются Babel и webpack, но вам не нужно ничего знать о них.
Когда ваше приложение готово к развёртыванию в продакшене, запуск команды npm run build создаст оптимизированную сборку вашего приложения в папке build . Вы можете узнать больше о Create React App из его README и его пользовательского руководства.
Next.js — это популярный легковесный фреймворк для статических и серверных приложений, использующих React. Он включает в себя готовые решения для стилизации и маршрутизации и предполагает, что вы используете Node.js в качестве серверной среды.
Gatsby — лучший способ для создания статических сайтов с помощью React. Он позволяет использовать React-компоненты, но выводит предварительно отрендеренный HTML и CSS, чтобы гарантировать минимальное время загрузки.
Более гибкие наборы инструментов
Следующие наборы инструментов предлагают больше гибкости и выбора. Мы рекомендуем их более опытным разработчикам:
- Neutrino сочетает в себе возможности webpack и простоту пресетов. Инструмент включает в себя пресеты для React-приложений и React-компонентов.
- Nx — набор инструментов для ведения фулстэк разработки в монорепозиториях, который обладает встроенной поддержкой React, Next.js, Express и так далее.
- Parcel — быстрый упаковщик веб-приложений с нулевой конфигурацией, который работает с React.
- Razzle — это фреймворк для серверного рендеринга, более гибкий чем Next.js, но не требующий обязательной настройки.
Создание набора инструментов с нуля
В набор инструментов для сборки JavaScript обычно входят:
- Менеджер пакетов, такой как Yarn или npm. Он позволяет вам использовать обширную экосистему сторонних пакетов и легко устанавливать или обновлять их.
- Сборщик, такой как webpack или Parcel. Он позволяет писать модульный код и объединять его в небольшие пакеты, чтобы оптимизировать время загрузки.
- Компилятор, такой как Babel. Он позволяет писать современный код JavaScript, который будет работать даже в старых браузерах.
Если вы предпочтёте создать свой собственный набор JavaScript-инструментов с нуля, ознакомьтесь с этим руководством, в котором воссоздаются некоторые функции Create React App.
Не забудьте убедиться, что ваш набор инструментов правильно настроен для продакшена.
Пример “Hello world”
Ниже приведен пример самого простого приложения, которое можно создать с помощью Express. Оно состоит из одного файла, в отличие от приложений, генерируемых с помощью генератора приложений Express, который обеспечивает создание основы для полноценного приложения с многочисленными файлами на JavaScript, шаблонами Jade и вложенными каталогами различного предназначения.
Вначале создайте каталог с именем myapp , перейдите в него и запустите команду npm init . Затем установите express как зависимость, следуя указаниям, приведенным в руководстве по установке.
В каталоге myapp создайте файл с именем app.js и добавьте следующий код:
const express = require('express') const app = express() const port = 3000 app.get('/', (req, res) => < res.send('Hello World!') >) app.listen(port, () => < console.log(`Example app listening on port $`) >)
Приложение запускает сервер и слушает соединения на порте 3000. Приложение выдает ответ “Hello World!” на запросы, адресованные корневому URL ( / ) или маршруту. Для всех остальных путей ответом будет 404 Not Found.
req (запрос) и res (ответ) являются теми же объектами, которые предоставляет Node, поэтому можно вызвать req.pipe() , req.on(‘data’, callback) и выполнить любые другие действия, не требующие участия Express.
Запустите приложение с помощью следующей команды:
$ node app.js