Frontender Magazine

Руководство по Node.js для начинающих

Сейчас нет недостатка в обучающих материалах по Node.js, но большинство из них охватывают либо какие-то конкретные варианты использования, либо темы, применимые уже тогда, когда у вас есть работающий Node.js То тут, то там я вижу комментарии вроде «я скачал Node.js, что теперь?». Статья ответит на этот вопрос и объяснит, как начать с самого начала.

Что есть Node.js?

Много путаницы у новичков в Node.js возникает из-за непонимания того, что же на самом деле это такое. И описание на nodejs.org не слишком помогает разобраться.

Важно понять, что Node — это не веб-сервер. Сам по себе он ничего не делает. Это не Apache. Там нет конфиг-файла, в котором указывается путь до HTML-файлов. Если вам нужен HTTP-сервер, вам нужно написать HTTP-сервер (с помощью встроенных библиотек). Node.js — это просто ещё один способ выполнять код на вашем компьютере. Это просто среда для выполнения JavaScript.

Установка Node

Установить Node.js очень просто. Если вы используете Windows или Mac, установочные файлы доступны на странице загрузки.

Я установил Node, что теперь?

Сразу после установки вам становится доступна новая команда node. Её можно использовать двумя разными способами. Первый способ — без аргументов. Откроется интерактивная оболочка (REPL: read-eval-print-loop), где вы можете выполнять обычный JavaScript-код.

$ node
> console.log('Hello World');
Hello World
undefined

Скриншот

В примере выше я написал console.log('Hello World') в оболочке и нажал Enter. Node.js выполнит этот код, и мы увидим сообщение. undefined после него выводится потому, что оболочка отображает возвращаемое значение каждой команды, а console.log ничего не возвращает.

Кроме того, мы можем передать Node файл с JavaScript для выполнения. Именно так вы и будете практически всегда делать.

hello.js

console.log('Hello World');

Теперь запустим его в терминале:

$ node hello.js
Hello World

Скриншот

В этом примере я переместил console.log в файл, который затем передал команде node в качестве аргумента. Node затем запускает JavaScript из этого файла и выводит Hello World.

Делаем что-нибудь полезное — работа с файлами

Просто выполнять код JavaScript весело и всё такое, но не очень полезно. Вот почему Node.js также включает в себя мощный набор библиотек (модулей) для серьёзных задач. В этом первом примере я собираюсь открыть файл с логами и обработать его.

example_log.txt

2013-08-09T13:50:33.166Z A 2
2013-08-09T13:51:33.166Z B 1
2013-08-09T13:52:33.166Z C 6
2013-08-09T13:53:33.166Z B 8
2013-08-09T13:54:33.166Z B 5

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

Сперва нам нужно считать содержимое файла.

my_parser.js

// Загрузка модуля fs (файловая система)
var fs = require('fs');

// Считывание содержимого файла в память
fs.readFile('example_log.txt', function (err, logData) {

  // Если произошла ошибка, то мы генерируем исключение,
  // и работа приложения завершается
  if (err) throw err;

  // logData имеет тип Buffer, переводим в string
  var text = logData.toString();
});

Работать с файлами в Node.js очень просто благодаря встроенному модулю файловой системы fs. Этот модуль содержит функцию readFile, принимающую в качестве аргументов путь до файла и коллбэк. Коллбэк вызовется, когда завершится чтение файла. Данные из файла поступают в виде объекта типа Buffer, по сути представляющего собой массив байтов. Мы можем перевести его в строку с помощью функции toString().

Теперь давайте займёмся обработкой логов. Это вполне себе обычный код JavaScript, поэтому я не буду вдаваться в детали.

my_parser.js

// Загрузка модуля fs
var fs = require('fs');

// Считывание содержимого файла в память
fs.readFile('example_log.txt', function (err, logData) {

  // Если произошла ошибка, то генерируем исключение,
  // и работа приложения завершится.
  if (err) throw err;

  // logData имеет тип Buffer, переводим в строку
  var text = logData.toString();

  var results = {};

  // Разбивка файла по строкам
  var lines = text.split('\n');

  lines.forEach(function(line) {
    var parts = line.split(' ');
    var letter = parts[1];
    var count = parseInt(parts[2]);

    if(!results[letter]) {
      results[letter] = 0;
    }

    results[letter] += parseInt(count);
  });

  console.log(results);
  // { A: 2, B: 14, C: 6 }
});

Теперь, когда вы передадите этот файл node в качестве аргумента, он выведет результат и завершит работу.

$ node my_parser.js
{ A: 2, B: 14, C: 6 }

Скриншот

Я часто использую Node.js для таких задач. Это простая и мощная альтернатива bash-скриптам.

Асинхронные коллбэки

Как вы могли видеть в предыдущем примере, типичным шаблоном в Node.js является использование асинхронных коллбэков. По сути вы говорите ему что-то сделать, и когда оно будет готово, вызвать вашу функцию (коллбэк). Всё потому, что Node.js однопоточный. Пока вы ждёте вызова коллбэка, Node.js может отвлечься и заняться другими делами, а не блокировать поток в ожидании завершения обработки запроса.

Это особенно важно для веб-серверов. Доступ к базам данных в современных веб-приложениях — обычное дело. Пока вы ждёте ответа от базы, Node.js может обработать ещё запросы. Это позволяет вам обрабатывать тысячи одновременных соединений с очень маленькими затратами, сравнимыми с созданием отдельного потока для каждого соединения.

Делаем что-нибудь полезное — HTTP-сервер

Как я уже говорил ранее, Node.js не делает ничего «из коробки». Один из встроенных модулей позволяет без особых усилий создать простой HTTP-сервер, указанный в примере на сайте Node.js.

my_web_server.js

var http = require('http');

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(8080);

console.log('Server running on port 8080.');

Когда я говорю, «простой», это значит «простой». Это не навороченный HTTP-сервер. Он не работает с HTML или изображениями. Фактически, что бы вы ни запросили, он вернёт Hello World. Тем не менее, можете запустить его, зайти на http://localhost:8080 в браузере и увидеть этот текст.

$ node my_web_server.js

Возможно, вы заметите небольшую разницу: ваше приложение не завершает работу. Так происходит потому, что вы создали сервер, и теперь он будет продолжать работать и отвечать на запросы до тех пор, пока вы не убьёте node сами.

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

Делаем что-нибудь полезное — Express

Express — это фреймворк, делающий создание большинства сайтов очень простым. Первое, что вам нужно будет сделать — установить его. Вместе с командой node у вас появилась команда npm. Этот инструмент даёт вам доступ к колоссальному количеству модулей, созданных сообществом, и Express как раз один из них.

$ cd /my/app/location
$ npm install express

Установленный модуль расположится в папке node_modules внутри каталога с вашим приложением. Оттуда вы сможете подключать его так же, как любой встроенный модуль. Давайте создадим с помощью Express простой сервер, отдающий статику.

my_static_file_server.js

var express = require('express'),
    app = express();

app.use(express.static(__dirname + '/public'));

app.listen(8080);
$ node my_static_file_server.js

Теперь у вас есть довольно мощный сервер для статического контента. Всё, что вы сложите в папку /public, может быть запрошено из браузера и будет отображено. HTML, изображения, почти всё, что душе угодно. Например, если вы положите изображение с именем my_image.png в эту папку, его можно будет запросить по адресу http://localhost:8080/my_image.png. Разумеется, у Express намного больше возможностей, но их вы можете открыть для себя, продолжив изучение самостоятельно.

NPM

Мы уже немного ознакомились с npm в предыдущем разделе, но я хотел бы подчеркнуть важность этого инструмента в контексте разработки на Node.js. Вам доступны тысячи модулей, решающих практически все типичные задачи, с которыми вы, возможно, столкнётесь. Не забывайте проверить существующие модули перед тем, как изобретать велосипед. Для приложения на Node.js нормально иметь массу зависимостей.

В предыдущем примере мы установили Express вручную. Если у вашего проекта много зависимостей, то устанавливать их таким образом не очень удобно, поэтому npm использует файлы package.json.

package.json

{
  "name" : "MyStaticServer",
  "version" : "0.0.1",
  "dependencies" : {
    "express" : "3.3.x"
  }
}

Файл package.json содержит общие сведения о вашем приложении. Он может содержать множество настроек, но выше указан необходимый минимум. Секция dependencies описывает имя и версию модулей, которые вы хотите установить. В данном случае подойдёт любая версия Express 3.3. Вы можете перечислить в этой секции столько зависимостей, сколько захотите.

Теперь вместо установки зависимостей по одной мы можем установить все сразу командой:

$ npm install

При запуске этой команды npm будет искать package.json в текущей директории, и если найдёт, то установит каждую указанную в нём зависимость.

Организация кода

Итак, до этого момента мы работали только с одним файлом, что не очень хорошо с точки зрения поддержки кода. В большинстве приложений вы будете распределять код между несколькими файлами. На данный момент какого-либо стандарта или рекомендуемой структуры файлов не существует. Это не Rails. Здесь нет концепта «представления идут сюда, а контроллеры — сюда». Можете делать так, как захотите.

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

parser.js

// Конструктор парсера
var Parser = function() {

};

// Обрабатывает переданный текст
Parser.prototype.parse = function(text) {

  var results = {};

  // Разбивает текст на строки
  var lines = text.split('\n');

  lines.forEach(function(line) {
    var parts = line.split(' ');
    var letter = parts[1];
    var count = parseInt(parts[2]);

    if(!results[letter]) {
      results[letter] = 0;
    }

    results[letter] += parseInt(count);
  });

  return results;
};

// Выносит конструктор парсера в модуль
module.exports = Parser;

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

Важная часть в этом коде — строка с module.exports. Она объясняет Node.js, что вы выносите из этого файла. В данном случае я выношу конструктор, чтобы пользователи могли создавать экземпляры моего объекта Parser. Вы сможете выносить то, что сами захотите.

Давайте теперь посмотрим, как импортировать этот файл и использовать новый объект Parser.

my_parser.js

// Подключение нового файла parser.js
var Parser = require('./parser');

// Загрузка модуля fs
var fs = require('fs');

// Считывание содержимого файла в память
fs.readFile('example_log.txt', function (err, logData) {

  // Если произошла ошибка, то генерируем исключение,
  // и работа приложения завершается.
  if (err) throw err;

  // logData имеет тип Buffer, переводим в строку
  var text = logData.toString();

  // Создаём экземпляр Parser
  var parser = new Parser();

  // вызываем функцию парсинга
  console.log(parser.parse(text));
  // { A: 2, B: 14, C: 6 }
});

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

Так как я вынес в модуль конструктор, то он и вернётся выражением require. Теперь я могу создавать экземпляры Parser и использовать их.

Итог

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

Я хочу, чтобы вы запомнили, что в Node.js всё зависит исключительно от вашего воображения. Встроенные библиотеки достаточно тщательно продуманы, чтобы вписываться в любую картину. Используйте их вместе с модулями, доступными в npm, и вы удивитесь, как быстро сможете разрабатывать сложные приложения.

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Brandon Cannaday
Автор:
Brandon Cannaday
Twitter:
@TheReddest
Сaйт:
http://modulus.io/
LinkedIn:
brandon-cannaday
Игорь Дерябин
Переводчик:
Игорь Дерябин
Сaйт:
http://rodweb.ru/
Twitter:
@ru_rodweb

Комментарии (11 комментариев, если быть точным)

Автар пользователя
efremoz

Спасибо за пояснения! цитирую: Например, если вы положите изображение с именем my_image.png в эту папку, его можно будет запросить по адресу http://localhost:8080/my_image.png.

Можно ли через сервер на nodejs перейти на домен без порта:8080? Что в сервере нужно прописать? http://domen.ru/my_image.png.

Автар пользователя
subzey

Можно. Порт HTTP по умолчанию — 80. Соответственно, нужно в вызове .listen() поменять 8080 на 80.

Автар пользователя
efremoz

Попробывал задать порт 80, выдает такую ошибку:

Server running at http://127.0.0.1:80/

events.js:72 throw er; // Unhandled 'error' event ^ Error: listen EADDRINUSE at errnoException (net.js:905:11) at Server._listen2 (net.js:1043:14) at listen (net.js:1065:10) at Server.listen (net.js:1139:5) at Object.start (/opt/nodejs/bin/server.js:16:31) at Object. (/opt/nodejs/bin/index.js:10:8) at Module._compile (module.js:456:26) at Object.Module._extensions..js (module.js:474:10) at Module.load (module.js:356:32) at Function.Module._load (module.js:312:12)

То есть сервер запускается, а потом что-то происходит. На сервере несколько доменов, по порту 80 к кому домену осуществляется переход?

Автар пользователя
subzey

EADDRINUSE означает, что этот порт уже занят другим процессом. Запустить одновременно тот же Апач и Ноду на одном порту нельзя. И это не особенность конкретно Апача или Ноды, операционка просто не смогла бы понять, в какой из процессов перенаправить сетевые данные.

Иногда вы даже можете не подозревать, что кто-то захапал себе 80 порт. Скайп, например, любит так делать.

Как быть? Для того, чтобы два (и более) разношёрстных сервиса могли пользоваться одним портом, обычно используется nginx. Мы выставляем Апач слушать какой-нибудь незанятный порт, например, 8080. Ноду - другой незанятый, например, 8081. А nginx будет слушать по 80. Затем, когда он получит запрос, он вычленит из него хостнейм и путь, и на основе этого решит, куда дальше запрос перенаправить (настраивается в конфигах).

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

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

Автар пользователя
efremoz

subzey Спасибо за подробности! Было бы здорово прочитать такую статью, пошаговая настройка работы node и nginx на linux.

Автар пользователя
lancedikson
Автар пользователя
efremoz

Благодарю!

Автар пользователя
vhajmik

Спасибо, помогла статья. Попробовал также вот это https://www.npmjs.com/package/react-ui-builder Оченно легко набрасывать UI компоненты на страницу.

Автар пользователя
AlekseyPriz

Запустил сервер из раздела "Делаем что-нибудь полезное — HTTP-сервер". Спасибо, работает, а как теперь его удалить?

Автар пользователя
iamstarkov

@AlekseyPriz руками?

Автар пользователя
36serg6

Объясните пожалуйста, зачем делается два раза parseInt() в my_parser.js?