Вы находитесь на странице: 1из 144

David Herron

Node Web Development


A practical introduction to Node,
the exciting new server-side
JavaScript web development stack

BIRMINGHAM – MUMBAI
Дэвид Хэррон

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

Москва, 2012
УДК 004.738.5:004.45Node
ББК 32.973.202-018.2
Х99

Хэррон Д.
Х99 Node.js. Разработкасерверных веб-приложений в JavaScript: Пер. с англ. Слинкина
А. А. – М.: ДМК Пресс, 2012. – 144 с.: ил.
ISBN 978-5-94074-809-0

Книга посвящена разработке веб-приложений в Node.js – платформе, которая


выводит язык JavaScript за пределы браузера и позволяет использовать его в сер-
верных приложениях. В основе платформы лежит исключительно быстрый движок
JavaScript, заимствованный из браузера Chrome, к которому добавлена быстрая и на-
дежная библиотека асинхронного сетевого ввода/вывода. Основной упор в Node.js
делается на создании высокопроизводительных, хорошо масштабируемых клиент-
ских и серверных приложений.
На практических примерах вы научитесь пользоваться серверным и клиентским
объектами HTTP, каркасами Connect и Express, освоите алгоритмы асинхронного
выполнения и узнаете, как работать с базами данных на основе SQL и с MongoDB.
Начав с практических рекомендаций по установке и настройке Node.js в режиме
разработки и эксплуатации, вы научитесь разрабатывать клиентские и серверные
HTTP-приложения; познакомитесь с применяемой в Node.js системой организации
модулей на основе спецификации CommonJS, позволяющей реализовать подмноже-
ство технологии объектно-ориентированного проектирования.
Издание предназначено для программистов, знакомых с основами JavaScript и
веб-разработки.
УДК 004.738.5:004.45Node
ББК 32.973.202-018.2

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

ISBN 978-1-849515-14-6 (анг.) Copyright © 2011 Packt Publishing


ISBN 978-5-94074-809-0 (рус.) © Оформление, ДМК Пресс, 2012
Содержание
Об авторе ............................................................................................................ 8
Благодарности ................................................................................................. 9
О рецензентах ................................................................................................ 10
Предисловие ................................................................................................... 11
О содержании книги .......................................................................................... 11
Что необходимо для чтения этой книги .............................................................. 12
На кого рассчитана эта книга ............................................................................ 13
Графические выделения.................................................................................... 13
Отзывы.............................................................................................................. 14
Поддержка клиентов ......................................................................................... 14
Исходный код примеров.................................................................................... 14
Опечатки ........................................................................................................... 14
Нарушение авторских прав ............................................................................... 15
Вопросы ............................................................................................................ 15

Глава 1. ЧТО ТАКОЕ NODE? ....................................................................... 16


Что позволяет делать Node?.............................................................................. 17
Серверный JavaScript ........................................................................................ 18
Почему имеет смысл использовать Node?......................................................... 18
Архитектура: потоки или асинхронный ввод/вывод с управлением
по событиям...................................................................................................... 19
Производительность и использование процессора ........................................... 21
Использование серверов, экономия затрат и экологичный Интернет ................ 23
Как правильно: Node, Node.js или Node.JS? ...................................................... 24
Резюме ............................................................................................................. 24

Глава 2. НАСТРОЙКА NODE ....................................................................... 25


Системные требования ..................................................................................... 25
Установка в POSIX-совместимых системах (Linux, Solaris, Mac и т. п.) ................ 26
Предварительная установка инструментария............................................... 26
Установка средств разработки в Mac OS X......................................................... 26
Установка в свой домашний каталог ............................................................. 27
Зачем устанавливать в домашний каталог? .................................................. 28
Установка в системный каталог .................................................................... 29
Установка в Mac OS X с помощью MacPorts .................................................. 29
Установка в Mac OS X с помощью homebrew ................................................. 30
Установка в Linux с помощью систем управления пакетами .......................... 30
Установка одновременно нескольких экземпляров Node ............................. 31
Выполним несколько команд для проверки установки ....................................... 31
6 Содержание
Командные утилиты Node............................................................................. 31
Запуск скрипта в Node ................................................................................. 33
Запуск сервера в Node ................................................................................. 34
Установка npm – менеджера пакетов для Node.................................................. 35
Запуск Node-серверов на этапе инициализации системы ................................. 36
Использование всех процессорных ядер в многоядерной системе .............. 40
Резюме ............................................................................................................. 42

Глава 3. МОДУЛИ NODE .............................................................................. 43


Что такое модуль? ............................................................................................. 43
Модули Node ..................................................................................................... 44
Как Node ищет модули, затребованные в require('module')? ............................... 44
Идентификаторы модулей и пути ................................................................. 44
Локальные модули внутри приложения ........................................................ 45
Комплектация приложения с внешними зависимостями .............................. 46
Системные модули в каталогах, перечисленных в массиве require.paths ...... 48
Составные модули – модули-каталоги ......................................................... 49
Менеджер пакетов для Node (npm).................................................................... 50
Формат npm-пакета ..................................................................................... 50
Поиск npm-пакетов ...................................................................................... 52
Команды npm ............................................................................................... 53
Версии и диапазоны версий пакета.............................................................. 61
Спецификация CommonJS ........................................................................... 63
Резюме ............................................................................................................. 64

Глава 4. ВАРИАЦИИ НА ТЕМУ ПРОСТОГО ПРИЛОЖЕНИЯ ............. 65


Разработка учебной программы по математике ................................................ 65
Использовать ли каркас?.............................................................................. 65
Реализация Math Wizard в Node (без каркасов) .................................................. 66
Маршрутизация запросов в Node ................................................................. 66
Обработка параметров запроса ................................................................... 67
Умножение чисел ......................................................................................... 69
Вычисление других математических функций .............................................. 70
Обобщение Math Wizard ............................................................................... 73
Продолжительные вычисления (числа Фибоначчи) ...................................... 74
Чего не хватает до «настоящего веб-сервера»? ............................................ 77
Использование каркаса Connect для реализации Math Wizard ...................... 78
Установка и настройка Connect .................................................................... 79
Знакомство с Connect .................................................................................. 80
Реализация Math Wizard с помощью Express ..................................................... 82
Реализация Express Math Wizard .................................................................. 82
Обработка ошибок ....................................................................................... 87
Параметризованные URL и службы данных .................................................. 88
Резюме ............................................................................................................. 93

Глава 5. ПРОСТОЙ ВЕБ-СЕРВЕР, ОБЪЕКТЫ EVENTEMITTER


И HTTP-КЛИЕНТЫ .......................................................................................... 95
Отправка и получение событий с помощью объектов EventEmitter ..................... 95
Теоретические основы EventEmitter ................................................................... 97
HTTP Sniffer – прослушивание обмена данными по протоколу HTTP ............. 97
Содержание 7

Реализация простого веб-сервера ..............................................................100


Реализация Basic Server ..............................................................................101
Типы MIME и npm-пакет MIME .....................................................................110
Обработка куков ..........................................................................................111
Отправка HTTP-запросов клиентом .............................................................112
Резюме ............................................................................................................114

Глава 6. ХРАНЕНИЕ И ВЫБОРКА ДАННЫХ .........................................115


Движки сохранения данных для Node ...............................................................115
SQLite3 – облегченная встраиваемая база данных на основе SQL ....................115
Установка ....................................................................................................116
Реализация приложения Notes с помощью SQLite3 .....................................116
Использование других СУБД на основе SQL на платформе Node.................129
Mongoose – интерфейс между Node и MongoDB ..............................................130
Установка Mongoose ...................................................................................130
Реализация приложения Notes с помощью Mongoose .................................131
Отображение заметок на консоли – show.js .................................................135
Другие продукты, поддерживающие MongoDB............................................137
Краткий обзор средств аутентификации пользователей...................................138
Резюме ............................................................................................................140

Предметный указатель ..............................................................................141


Об авторе
Дэвид Хэррон вот уже больше 20 лет занимается созданием программного обес-
печения, работая в Кремниевой долине в роли разработчика и инженера по конт-
ролю качества. Его последнее место работы – архитектор по организации контро-
ля качества в компании Yahoo!, где ведутся работы по созданию новой платформы
для веб-приложений на основе Node.
В компании Sun Microsystems Дэвид занимал должность архитектора по ор-
ганизации контроля качества Java SE и работал главным образом над средства-
ми автоматизации тестирования, в том числе написанным на базе AWT классом
Robot, который ныне широко применяется в программах автоматизации тестиро-
вания графического интерфейса пользователя (ГИП). Он принимал участие в за-
пуске проектов OpenJDK и JDK-Distros, а также отвечал за организацию конкурса
Mustang Regressions Contest, идея которого состояла в том, чтобы обратиться к со-
обществу разработчиков на Java с просьбой поискать ошибки в версии 1.6.
До перехода в Sun Дэвид работал в компании VXtreme, где занимался раз-
работкой программных средств потокового видео. После покупки этой компании
корпорацией Microsoft созданный ей продукт лег в основу Windows Media Player.
В компании The Wollongong Group Дэвид работал над клиентским и серверным
ПО электронной почты и принимал участие в деятельности нескольких рабочих
групп IETF, направленной на совершенствование протоколов электронной почты.
Дэвида интересуют транспортные средства с электрическими двигателями,
мировые запасы энергии, изменение климата и вопросы охраны окружающей
среды. Он сооснователь компании Transition Silicon Valley. В качестве сетевого
журналиста он ведет раздел Green Transportation Examiner на сайте examiner.com,
пишет на темы экологии в блоге на сайте 7gen.com, организовал дискуссионный
форум по электромобилям на сайте visforvoltage.org, а также обсуждает различные
программы, в том числе Node.js, Drupal и Doctor Who на сайте davidherron.com.
Благодарности
Я благодарен многим людям.
Хочу сказать спасибо матери Эвелин за… ну, в общем, за все, отцу Джиму, сестре
Пэтти и брату Кену. Даже не представляю, как жил бы без вас!
Спасибо моей подруге Мэгги за то, что она всегда рядом, готовая поддержать
меня, за ее веру в меня, за мудрость и чувство юмора и за то, что устраивает мне
головомойки, когда это необходимо. Пусть так будет и дальше.
Хочу поблагодарить доктора Кена Кубота из Университета штата Кентукки,
который поверил в меня и принял на первую работу, связанную с компьютерами.
Шесть лет я учился у него не только обслуживанию компьютерных систем, но и
многому другому.
Спасибо моим бывшим работодателям, математическому факультету Универ-
ситета штата Кентукки, компаниям The Wollongong Group, MainSoft, VXtreme,
Sun Microsystems и Yahoo!, а также всем, с кем мне довелось работать в разных
компаниях. Я благодарен своей бывшей начальнице Тине Су, которая побуждала
меня выступать на публичных мероприятиях и писать, хотя это совсем не свой-
ственно инженеру-программисту с интровертным складом ума. Особенно я бла-
годарен компании Yahoo!, которая предоставила мне возможность поработать над
внутренним проектом на базе Node.js и понять, что существует потребность в этой
книге.
Я признателен издательству Packt Publishing за предоставленную возмож-
ность написать эту книгу. Благодаря сотрудникам издательства, которые помога-
ли мне на протяжении всей работы, я понял, что писать книги – моя мечта.
Я благодарю Райана Дала, Айзека Шлютера и других разработчиков ядра Node
за их мудрость и провидческий дар, необходимый для создания такой наполняю-
щей сердце радостью подвижной платформы для разработки ПО. Есть платфор-
мы, с которыми работать откровенно трудно, но эта не из их числа. И чтобы реа-
лизовать ее настолько хорошо, необходимы проницательность и дальновидность.
О рецензентах
Благовест Дачев, пишет программы для веб с 2002 года. Он прошел все стадии:
начал с HTML, CSS и JavaScript, а затем перебрался в мир серверов и баз данных.
Благовест одним из первых принял платформу Node.js и внес вклад в развитие
нескольких проектов с открытым исходным кодом. В настоящее время он работа-
ет инженером-программистом в компании Dow Jones & Company, где занимается
разработкой системы виджетов, позволяющей третьим сторонам искать и публи-
ковать новости на своих сайтах.
Благовест учился в Массачусетском университете в Амхерсте, где участвовал
в исследованиях по информационно-поисковым системам, завершил проекты
в рамках двух программ Google Summer of Code подряд и был соавтором несколь-
ких статей.

Я хотел бы поблагодарить свою маму Татьяну за любовь, неустанную заботу и силу


духа, которые вдохновляли меня на протяжении многих лет, а также отца Йордана за
счастливые детские воспоминания.

Мэтт Рэнни, одним из первых принял платформу Node.js и внес свой вклад
в ее развитие. Он один из основателей компании Voxer, использующей Node на
своих серверах.
Предисловие
Добро пожаловать в мир разработки ПО на базе Node (другое название – Node.
js). Node – это недавно появившаяся платформа, которая выводит язык JavaScript
за пределы браузера и позволяет использовать его в серверных приложениях.
В основе платформы лежит исключительно быстрый движок JavaScript, заим-
ствованный из браузера Chrome, V8, к которому добавлена быстрая и надежная
библиотека асинхронного сетевого ввода/вывода. Основной упор в Node делается
на создании высокопроизводительных, хорошо масштабируемых клиентских и
серверных приложений для «веб реального времени».
Эту платформу разработал Райан Дал (Ryan Dahl) в 2009 году, после двух
лет экспериментирования с созданием серверных веб-компонентов на Ruby
и других языках. В ходе своих исследований он пришел к выводу, что вместо
традиционной модели параллелизма на основе потоков следует обратиться
к событийно-ориентированным системам. Эта модель была выбрана за простоту
(хорошо известно, что многопоточные системы трудно реализовать правильно),
за низкие накладные расходы, по сравнению с идеологией «один поток на каждое
соединение», и за быстродействие. Цель Node – предложить «простой способ
построения масштабируемых сетевых серверов». При проектировании за образец
были взяты такие системы, как Event Machine (Ruby) и каркас Twisted (Python).
В настоящей книге рассматривается в первую очередь вопрос о построении
веб-приложений с помощью Node. Мы познакомимся с важными концепциями,
которые необходимо понимать, чтобы повысить быстродействие приложения.
Для этого мы будем писать реальные приложения, подробно анализировать их
составные части и обсуждать, как применить эти идеи в своих программах. Мы
установим Node и npm и научимся устанавливать и разрабатывать npm-пакеты и
модули Node. Мы создадим несколько приложений, изучим, как отражаются на
отзывчивости цикла обработки событий продолжительные вычисления, расска-
жем о двух способах распределения нагрузки между серверами, поработаем с кар-
касом Express и прочее.

О содержании книги
Глава 1 «Что такое Node?» содержит введение в платформу Node. Мы погово-
рим о том, где она применяется, об архитектурных решениях, принятых в Node,
о ее истории и об истории использования JavaScript на стороне сервера, а также
о том, почему JavaScript не должен быть замурован в браузере.
12 Предисловие
В главе 2 «Настройка Node» речь пойдет о настройке среды разработки для
Node, в том числе о нескольких способах сборки и установки из исходного кода.
Мы также коснемся вопроса о развертывании Node на производственных серверах.
Глава 3 «Модули Node» посвящена модулям – единицам модульной структуры
приложений для Node. Мы разберемся в том, что такое модули, и разработаем
несколько. Затем познакомимся с программой npm, Node Package Manager
(менеджер пакетов Node) и рассмотрим несколько способов использования
npm для управления установленными пакетами, а также для разработки и
распространения npm-пакетов.
В главе 4 «Вариации на тему простого приложения» мы, уже вооруженные
знанием основ, приступим к изучению процесса разработки приложений на
платформе Node. Точнее, мы разработаем простое приложение, применяя саму
Node, систему промежуточного уровня Connect и веб-каркас Express. Хотя при-
ложение несложное, оно даст нам возможность изучить цикл обработки событий
в Node, узнать о его адаптации к продолжительным вычислениям, познакомиться
с синхронными и асинхронными алгоритмами и вынести громоздкие вычисления
на вспомогательный сервер.
В главе 5 «Простой веб-сервер, объекты EventEmitter и HTTP-клиенты»
объясняется, что в Node клиентские и серверные объекты – это соответственно
передний план и центр. Мы подробно рассмотрим обе стороны протокола HTTP,
для чего разработаем клиентское и серверное приложения на основе HTTP.
В главе 6 «Хранение и выборка данных» речь пойдет о том, что в большинстве
приложений необходимо какое-то надежное хранилище для длительного хране-
ния данных. Мы рассмотрим применение для этой цели СУБД на основе SQL и
MongoDB. Попутно поговорим о применении каркаса Expess для аутентифика-
ции пользователей и создания более привлекательной страницы с сообщением об
ошибке.

Что необходимо для чтения


этой книги
В настоящее время мы обычно собираем Node из исходного кода. Лучше все-
го платформа работает в системах на базе Unix или совместимых со стандартом
POSIX. Предъявляемые Node требования достаточно скромны, а самым важным
инструментом является то, что находится у вас между ушами.
Для установки из исходного кода необходима система на базе Unix/POSIX
(Linux, Mac, FreeBSD, OpenSolaris и т. д.), современный компилятор C/C++, биб-
лиотеки OpenSSL и Python версии не ниже 2.4.
Программы для Node можно набирать в любом текстовом редакторе, но лучше
использовать такой, который понимает синтаксис JavaScript, HTML, CSS и т. д.
Хотя эта книга посвящена разработке веб-приложений, иметь веб-сервер не-
обязательно. В состав Node входит собственный веб-сервер.
Предисловие 13

На кого рассчитана эта книга


Данная книга рассчитана на любого программиста, ищущего приключений,
сопутствующих новой программной платформе, построенной на основе новой па-
радигмы программирования.
Разработчики серверных компонентов, возможно, придут к выводу, что свежие
идеи позволили им по-новому взглянуть на процесс создания веб-приложений.
JavaScript – мощный язык, а асинхронная природа Node подчеркивает его сильные
стороны.
Разработчикам, имеющим опыт работы с JavaScript в браузере, будет интересно
применить свои знания на новой территории и посмотреть, что значит писать на
JavaScript без доступа к DOM. (Раз нет браузера, то нет и DOM, если только вы
не установите JSDom.)
Хотя главы зависят друг от друга, порядок чтения выбираете вы сами.
Мы предполагаем, что вы уже умеете писать программы и знакомы с совре-
менными языками программирования, такими как JavaScript.

Графические выделения
В этой книге используются различные шрифты для обозначения типа инфор-
мации. Ниже приведено несколько примеров с пояснениями.
Фрагменты кода внутри абзаца выделяются следующим образом: «Объект
http инкапсулирует протокол HTTP, а его метод http.createServer создает полно-
ценный веб-сервер, прослушивающий порт, указанный в методе .listen».
Блок кода выделяется следующим образом:

var http = require('http');


http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(8124, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8124/');

Если мы хотим привлечь внимание к конкретной части кода, то выделяем ее


полужирным шрифтом:

var util = require('util');


var A = "a different value A";
var B = "a different value B";
var m1 = require('./module1');
util.log('A='+A+' B='+B+' values='+util.inspect(m1.values()));

Команды и выводимая ими информация записываются так:

$ sudo /usr/sbin/update-rc.d node defaults


14 Предисловие
Новые термины и важные фрагменты выделяются полужирным шрифтом.
Например, элементы графического интерфейса в меню или диалоговых окнах вы-
глядят в книге так: «В настоящей системе для обеспечения защиты нужно было бы
ввести имя и пароль пользователя. Но мы просто попросим пользователя нажать
кнопку Login (Войти)».

Отзывы
Мы всегда рады отзывам читателей. Расскажите нам, что вы думаете об этой
книге – что вам понравилось или, быть может, не понравилось. Читательские от-
зывы важны для нас, так как помогают выпускать книги, из которых вы черпаете
максимум полезного для себя.
Чтобы отправить обычный отзыв, просто пошлите письмо на адрес feedback@
packtpub.com, указав название книги в качестве темы.
Если вы хотите предложить нам тему для книги, изложите свое предложение,
заполнив форму SUGGEST A TITLE на сайте www.packtpub.com, или в письме на
адрес suggest@packtpub.com.
Если вы являетесь специалистом в некоторой области и хотели бы стать авто-
ром или соавтором книги, познакомьтесь с инструкциями для авторов по адресу
www.packtpub.com/authors.

Поддержка клиентов
Счастливым обладателям книг Packt мы можем предложить ряд услуг, кото-
рые позволят извлечь из своего приобретения максимум пользы.

Исходный код примеров


Скачать исходный код примеров для купленных вами книг Packt можно на
странице своей учетной записи на сайте http://www.PacktPub.com. Если вы при-
обрели книгу где-то в другом месте, то можете зарегистрировать ее на странице
http://www.PacktPub.com/support, тогда файлы будут отправлены вам по элек-
тронной почте.

Опечатки
Мы проверяли содержимое книги очень тщательно, но какие-то ошибки все
же могли проскользнуть. Если вы найдете в нашей книге ошибку, в тексте или
в коде, пожалуйста, сообщите нам о ней. Так вы избавите других читателей от
разочарования и поможете нам сделать следующие издания книги лучше. При
обнаружении опечатки просьба зайти на страницу http://www.packtpub.com/
support, выбрать книгу, щелкнуть по ссылке errata submission form и ввести
информацию об опечатке. Проверив ваше сообщение, мы разместим информацию
Предисловие 15

об опечатке на нашем сайте или добавим ее в список замеченных опечаток в раз-


деле Errata для данной книги. Для просмотра списка опечаток выберите название
книги на странице http://www.packtpub.com/support.

Нарушение авторских прав


Незаконное размещение защищенного авторским правом материала в Интер-
нете – проблема для всех носителей информации. В издательстве Packt мы отно-
симся к защите прав интеллектуальной собственности и лицензированию очень
серьезно. Если вы обнаружите незаконные копии наших изданий в любой форме
в Интернете, пожалуйста, незамедлительно сообщите нам адрес или название веб-
сайта, чтобы мы могли предпринять соответствующие меры.
Просим отправить ссылку на вызывающий подозрение в пиратстве материал
по адресу copyright@packtpub.com.
Мы будем признательны за помощь в защите прав наших авторов и содействие
в наших стараниях предоставлять читателям полезные сведения.

Вопросы
Если вас смущает что-то в этой книге, то можете связаться с нами по адресу
questions@packtpub.com, и мы сделаем все возможное для решения проблемы.
Глава 1. ЧТО ТАКОЕ NODE?
Node – это захватывающая новая платформа для разработки веб-приложений,
серверов приложений, произвольных сетевых серверов и клиентов, да и вообще
для программирования. Она спроектирована так, чтобы обеспечить высочайшую
масштабируемость сетевых приложений – за счет хитроумного сочетания асин-
хронного ввода/вывода, использования JavaScript на стороне сервера, изобрета-
тельного использования анонимных функций JavaScript и однопоточной собы-
тийно-ориентированной архитектуры.
Принятая в Node модель принципиально отличается от распространенных
платформ для построения серверов приложений, в которых масштабируемость
достигается за счет многопоточности. Утверждается, что благодаря событийно-
ориентированной архитектуре снижается потребление памяти, повышается про-
пускная способность и упрощается модель программирования. Сейчас платформа
Node быстро развивается, и многие считают ее привлекательной альтернативой
традиционному подходу к разработке веб-приложений – на базе Apache, PHP,
Python и т. п.
В основе Node лежит автономная виртуальная машина JavaScript с расши-
рениями, делающими ее пригодной для программирования общего назначения
с упором на разработку серверов приложений. Платформу Node не имеет смыс-
ла напрямую сравнивать ни с языками программирования, которые обычно ис-
пользуются для создания веб-приложений (PHP/Python/Ruby/Java и прочие),
ни с контейнерами, реализующими протокол HTTP (Apache/Tomcat/Glassfish и
т. д.). В то же время многие считают, что потенциально она может заменить тради-
ционные стеки веб-приложений.
В основе реализации лежит цикл обработки событий неблокирующего ввода/
вывода и библиотеки файлового и сетевого ввода/вывода, причем все это построе-
но поверх движка V8 JavaScript (заимствованного из веб-браузера Chrome). Биб-
лиотека ввода/вывода обладает достаточной общностью для реализации любого
протокола на базе TCP или UDP: DNS, HTTP, IRC, FTP и др. Но хотя она под-
держивает разработку серверов и клиентов произвольного протокола, чаще всего
применяется для создания обычных веб-сайтов, где заменяет Apache/PHP или
Rails.
Эта книга представляет собой введение в платформу Node. Мы предполага-
ем, что вы уже умеете писать программы, знакомы с языком JavaScript и знаете,
как разрабатываются веб-приложения на других языках. Мы напишем несколь-
ко работоспособных приложений и убедимся, что учиться лучше всего, копаясь
в коде.
Что позволяет делать Node? 17

Что позволяет делать Node?


Node – платформа для написания JavaScript-приложений вне веб-браузера.
Это не тот JavaScript, с которым все мы знакомы по опыту работы с браузерами.
В Node не встроена ни объектная модель документа (DOM), ни какие-либо еще
возможности браузера. Именно язык JavaScript в сочетании с асинхронным вво-
дом/выводом делает Node мощной платформой для разработки приложений.
Но вот для чего Node непригодна, так это для разработки персональных при-
ложений с графическим интерфейсом пользователя (ГИП). На сегодняшний день
в Node нет встроенного эквивалента Swing (или SWT, если вам больше нравит-
ся эта библиотека). Нет и подключаемой библиотеки ГИП для Node, и внедрить
Node в браузер тоже нельзя. Если бы для Node существовала библиотека ГИП, то
на этой платформе можно было строить и персональные приложения. Недавно
появилось несколько проектов по созданию интерфейса между Node и GTK, ито-
гом которых должна стать кросс-платформенная библиотека ГИП. В состав движ-
ка V8, используемого в Node, входят API-расширения, позволяющие писать на
C/C++ код для расширения JavaScript или интеграции движка с платформенны-
ми библиотеками.
Помимо встроенного умения исполнять код на JavaScript, включенные в со-
став дистрибутива модули предоставляют и другие возможности:
‰ утилиты командной строки (для включения в скрипты оболочки);
‰ средства написания интерактивных консольных программ (цикл «чте-
ние – выполнение – печать»);
‰ великолепные функции управления процессами для наблюдения за дочер-
ними процессами;
‰ объект Buffer для работы с двоичными данными;
‰ механизм для работы с сокетами TCP и UDP с полным комплектом обрат-
ных вызовов в ответ на события;
‰ поиск в системе DNS;
‰ средства для создания серверов и клиентов протоколов HTTP и HTTPS,
построенные на основе библиотеки TCP-сокетов;
‰ средства доступа к файловой системе;
‰ встроенная рудиментарная поддержка автономного тестирования с по-
мощью утверждений.
Сетевой слой Node находится на низком уровне, но работать с ним все равно
просто. Например, модули HTTP позволяют реализовать HTTP-сервер (или кли-
ент), написав всего несколько строк кода, но тем не менее на этом уровне про-
граммист работает очень близко к реальным запросам по протоколу и может точно
указать, какие HTTP-заголовки следует включать в ответ на запрос. Если про-
граммист на PHP обычно не интересуется заголовками, то для программиста на
Node они существенны.
Иными словами, написать на Node HTTP-сервер очень просто, но типичному
разработчику веб-приложений нет нужды работать на таком низком уровне. На-
пример, кодируя на PHP, программист предполагает, что Apache уже присутст-
18 Что такое Node?
вует, так что реализовывать серверную часть стека ему не нужно. Сообщество,
сложившееся вокруг Node, создало широкий спектр каркасов для разработки веб-
приложений, в том числе Connect, которые позволяют быстро сконфигурировать
HTTP так, чтобы предоставлялось все, к чему мы привыкли, – сеансы, куки, об-
служивание статических файлов, протоколирование и т. д. – и пусть разработчик
занимается бизнес-логикой приложения.

Серверный JavaScript
Хватит чесать в затылке. Ведь именно этим вы сейчас и занимаетесь, правда? –
чешете затылок и бормочете: «Что браузерному языку делать на сервере?» Но на
самом-то деле у JavaScript есть долгая и мало кому известная история применения
вне браузера. JavaScript – язык программирования, такой же, как любой другой,
поэтому правильнее было бы спросить: «Почему JavaScript должен быть замуро-
ван в браузере?»
На заре эры веб инструменты для создания веб-приложений находились еще
в зачаточном состоянии. Кто-то экспериментировал с написанием CGI-скриптов
на Perl или на TCL, языки PHP и Java еще только разрабатывались, и даже Java-
Script применялся на стороне сервера. Одним из первых серверов приложений был
LiveWire от компании Netscape, и в нем использовался JavaScript. В некоторых вер-
сиях технологии Microsoft ASP использовался язык JScript, версия JavaScript от
Microsoft. Из относительно недавних серверных проектов, в которых используется
JavaScript, назовем каркас разработки приложений RingoJS, популярный в мире
Java. Он построен на базе Rhino – реализации JavaScript, написанной на Java.
Node дополняет картину ранее невиданной комбинацией – сочетанием быст-
рого событийно-ориентированного механизма ввода/вывода и быстрого движка
JavaScript, такого как V8, используемого в браузере Google Chrome.

Почему имеет смысл


использовать Node?
Язык JavaScript очень популярен благодаря присутствию в любом веб-брау-
зере. Он ни в чем не уступает другим языкам, но при этом поддерживает многие
современные представления о том, каким должен быть язык программирования.
Благодаря широкому распространению имеется немало опытных программистов
на JavaScript.
Это динамический язык со слабо типизированными, динамически расширяе-
мыми объектами, которые неформально объявляются по мере необходимости.
Функции в нем являются полноценными объектами и обычно используются
в виде анонимных замыканий. Это делает JavaScript более мощным языком, по
сравнению с некоторыми другими, часто применяемыми для разработки веб-
приложений. Теоретически наличие подобных возможностей должно повышать
продуктивность программистов. Но скажем откровенно: споры между сторонни-
Архитектура: потоки или асинхронный ввод/вывод 19

ками динамических и статических языков, а также строгой и слабой типизации до


сих пор не утихли и вряд ли когда-нибудь утихнут.
Один из основных недостатков JavaScript – Глобальный Объект. Все перемен-
ные верхнего уровня «сваливаются» в Глобальный Объект, и при использовании
одновременно нескольких модулей это может привести к неуправляемому хаосу.
Поскольку веб-приложения обычно состоят из множества объектов, возможно,
создававшихся разными организациями, то может возникнуть опасение, будто
программирование для Node сродни хождению по минному полю, нашпигованному
конфликтующими между собой глобальными объектами. Однако это не так. На
самом деле в Node используется система организации модулей CommonJS, а это
означает, что локальные переменные некоторого модуля так и будут локальными
в нем, пусть даже выглядят как глобальные. Такое четкое разграничение между
модулями решает проблему Глобального Объекта.
Использование единого языка программирования на сервере и на клиенте
давно уже было мечтой разработчиков для веб. Своими корнями эта мечта уходит в
период становления Java, когда апплеты представлялись клиентским интерфейсом
к написанным на Java серверным приложениям, а JavaScript первоначально
виделся как облегченный скриптовый язык взаимодействия с апплетами. Но что-
то на этом пути не сложилось, и в результате не Java, а JavaScript стал основным
языком на стороне клиента-браузера. С появлением Node мы наконец сможем
реализовать мечту – сделать JavaScript языком, используемым по обе стороны
веб – на стороне клиента и сервера.
У единого языка есть несколько потенциальных плюсов:
‰ одни и те же программисты могут работать над обеими сторонами прило-
жения;
‰ код проще переносить с сервера на клиент и обратно;
‰ общий для клиента и сервера формат данных (JSON);
‰ общий программный инструментарий;
‰ общие для клиента и сервера средства тестирования и контроля качества;
‰ на обеих сторонах веб-приложения можно использовать общие шаблоны
представлений;
‰ общий язык общения между группами, работающими над клиентской и
серверной частью.
Node упрощает реализацию этих (и других) достоинств, предлагая основа-
тельную платформу и активное сообщество разработчиков.

Архитектура: потоки
или асинхронный ввод/вывод
с управлением по событиям
Говорят, что именно благодаря асинхронной событийно-ориентированной ар-
хитектуре Node демонстрирует столь высокую производительность. Так и есть,
только к этому надо добавить еще стремительность движка V8 JavaScript. В тра-
20 Что такое Node?
диционной модели сервера приложений параллелизм обеспечивается за счет ис-
пользования блокирующего ввода/вывода и нескольких потоков. Каждый поток
должен дожидаться завершения ввода/вывода, перед тем как приступить к обра-
ботке следующего запроса.
В Node имеется единственный поток выполнения, без какого-либо контекст-
ного переключения или ожидания ввода/вывода. При любом запросе ввода/вы-
вода задаются функции обработки, которые впоследствии вызываются из цикла
обработки событий, когда станут доступны данные или произойдет еще что-то
значимое. Модель цикла обработки событий и обработчика событий – вещь
распространенная, именно так исполняются написанные на JavaScript скрипты
в браузере. Ожидается, что программа быстро вернет управление циклу обработ-
ки, чтобы можно было вызвать следующее стоящее в очереди задание.
Чтобы повернуть наши мысли в нужном направлении, Райан Дал (в презен-
тации «Cinco de Node») спрашивает, что происходит при выполнении такого кода:

result = query('SELECT * from db');

Разумеется, программа в этой точке приостанавливается на время, пока слой


доступа к базе данных отправляет запрос базе, которая вычисляет результат и
возвращает данные. В зависимости от сложности запроса его выполнение может
занять весьма заметное время. Это плохо, потому что пока поток простаивает, мо-
жет прийти другой запрос, а если заняты все потоки (не забывайте, что ресурсы
компьютера конечны), то запрос будет просто отброшен. Расточительно это как-
то. Да и контекстное переключение обходится не бесплатно; чем больше запущено
потоков, тем больше времени процессор тратит на сохранение и восстановление их
состояния. Кроме того, стек каждого потока занимает место в памяти. И просто за
счет асинхронного событийно-ориентированного ввода/вывода Node устраняет
большую часть этих накладных расходов, привнося совсем немного собственных.
Часто рассказ о реализации параллелизма с помощью потоков сопровождается
предостережениями типа «дорого и чревато ошибками», «ненадежные примитивы
синхронизации в Java» или «проектирование параллельных программ может ока-
заться сложным и не исключены ошибки» (фразы взяты из результатов, выданных
поисковой системой). Причиной этой сложности являются доступ к разделяемым
переменным и различные стратегии предотвращения взаимоблокировок и состя-
заний между потоками. «Примитивы синхронизации в Java» – один из примеров
такой стратегии, и, очевидно, многие программисты считают, что пользоваться
ими трудно. Чтобы как-то скрыть сложность, присущую многопоточному парал-
лелизму, создаются каркасы типа java.util.concurrent, но все равно некоторые счи-
тают, что попытка упрятать сложность подальше не делает проблему проще.
Node призывает подходить к параллелизму по-другому. Обратные вызовы из
цикла обработки событий – гораздо более простая модель параллелизма как для
понимания, так и для реализации.
Чтобы пояснить необходимость асинхронного ввода/вывода, Райан Дал напо-
минает об относительном времени доступа к объектам. Доступ к объектам в памя-
Производительность и использование процессора 21

ти (порядка наносекунд) производится быстрее, чем к объектам на диске или по


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

query('SELECT * from db', function (result) {


// произвести какие-то операции с результатом
});

Разница в том, что теперь результат запроса не возвращается в качестве значе-


ния функции, а передается функции обратного вызова, которая будет вызвана поз-
же. Таким образом, возврат в цикл обработки событий происходит почти сразу, и
сервер может перейти к обслуживанию других запросов. Одним из таких запросов
будет ответ на запрос, отправленный базе данных, и тогда будет вызвана функция
обратного вызова. Такая модель быстрого возврата в цикл обработки событий по-
вышает степень использования ресурсов серверов. Это прекрасно для владельца
сервера, но еще больший выигрыш получает пользователь, перед которым быстрее
открывается содержимое страницы.
В наши дни веб-страницы все чаще собирают данные из десятков источни-
ков. Каждому нужно отправить запрос и дождаться ответа на него. Асинхрон-
ные запросы позволяют выполнять эти операции параллельно – отправить сразу
все запросы, задав для каждого собственный обратный вызов, и, не дожидаясь
ответа, вернуться в цикл обработки событий. А когда придет ответ, будет вызвана
соответствующая ему функция. Благодаря распараллеливанию данные можно
собрать гораздо быстрее, чем если бы запросы выполнялись синхронно, один
за другим. И пользователь по ту сторону браузера счастлив, так как страница
загружается быстрее.

Производительность
и использование процессора
Своей притягательностью платформа Node отчасти обязана пропускной спо-
собности (количество обслуживаемых запросов в секунду). Сравнительные тесты
схожих программ, например Apache и Node, показывают фантастический вы-
игрыш в производительности.
Один из популярных эталонных тестов – следующий простой HTTP-сервер,
который всего лишь возвращает сообщение «Hello World», читаемое из памяти:

var http = require('http');


http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
22 Что такое Node?
}).listen(8124, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8124/');

Это один из самых простых веб-серверов, которые только можно построить на


платформе Node. Объект http инкапсулирует протокол HTTP, а его метод http.
createServer создает полнофункциональный веб-сервер, прослушивающий порт,
заданный в методе .listen. Каждый запрос (к любому URL любого вида – хоть
GET, хоть PUT) приводит к вызову указанной функции. Сервер очень простой
и совсем мало «весит». В данном случае вне зависимости от URL возвращается
строка «Hello World» типа text/plain.
Благодаря такому минимализму это приложение должно демонстрировать
максимальную пропускную способность. Поэтому во многих опубликованных ис-
следованиях перечень эталонных тестов начинается с этого простейшего HTTP-
сервера.
Райан Дал привел пример простого теста (http://nodejs.org/cinco_de_node.
pdf), который возвращает 1 Мб двоичных данных; Node на нем показала пропускную
способность 822 запроса/с, а nginx – 708 запросов/с. Дал также отвечает, что nginx
достигала пиковой производительности при памяти объемом 4 Мб, а Node – при
64 Мб.
Дастин Маккуэй (Dustin McQuay) (http://www.synchrosinteractive.com/blog/
9-nodejs/22-nodejs-has-a-bright-future) продемонстрировал две, по его словам,
очень похожие программы на платформах Node и PHP/Apache:
‰ PHP/Apache 3187 запросов/с;
‰ Node.js 5569 запросов/с.
Ханнес Вальнёфер (Hannes Wallnöfer), автор RingoJS, написал в своем блоге
заметку, в которой предостерегает от принятия важных решений на основе
сравнительных тестов (http://hns.github.com/2010/09/21/benchmark.html), а за-
тем переходит к сравнению RingoJS с Node. RingoJS – это сервер приложений,
построенный на базе движка Rhino JavaScript для Java. При некоторых сценариях
производительность RingoJS и Node довольно близка. Исследования показывают,
что в приложениях, где требуется быстро выделять память для буферов или строк,
Node работает хуже, чем RingoJS. В последующей заметке (http://hns.github.
com/2010/09/29/benchmark2.html) Ханнес тестировал довольно типичную задачу
разбора JSON-строки и обнаружил, что RingoJS работает гораздо быстрее.
Микито Такада (Mikito Takada) рассказал в блоге о сравнительном тести-
ровании производительности Node и Django на примере написанного им при-
ложения «48 hour hackathon» (http://blog.mixu.net/2011/01/17/performance-
benchmarking-the-node-js-backend-of-our-48h-product-wehearvoices-net/).
Неоптимизированная версия для Node оказалась чуть медленнее (по времени
отклика), но несложная оптимизация (добавление пула соединений с MySQL,
кэширования и т. д.) дала значительный прирост производительности, легко
обогнав Django. Окончательный график показывает значение числа запросов
в секунду, близкое к вышеупомянутому серверу «Hello World».
Чтобы в полной мере раскрыть потенциальные возможности Node, крайне
важно быстро возвращать управление в цикл обработки событий. Мы вернемся
Использование серверов, экономия затрат и экологичный Интернет 23

к этой теме в главе 4 «Вариации на тему простого приложения», но уже сейчас


заметим, что если обработчик обратного вызова выполняется «слишком долго»,
то Node перестает быть тем сверхбыстрым сервером, каким был задуман. В од-
ной из ранних статей о проекте Node (http://four.livejournal.com/963421.html)
Райан Дал обсуждал требование о том, что обработчики событий должны вы-
полняться не дольше 5 мс. Большая часть идей, высказанных в той статье, так и
не была реализована, но Алекс Пэйн (Alex Payne) написал по этому поводу крайне
любопытную статью (http://al3x.net/2010/07/27/node.html), в которой проводится
различие между «масштабированием в малом» и «масштабированием в большом».
В случае небольших веб-приложений («масштабирование в малом») реализа-
ция на Node, а не на языках 'P' (Perl, PHP, Python и т. д.), должна давать выигрыш
в производительности. JavaScript – мощный язык, а среда Node со своей современ-
ной быстрой виртуальной машиной имеет преимущества в части производитель-
ности и организации параллелизма по сравнению с интерпретируемыми языками
типа PHP.
Далее Пэйн говорит, что «масштабирование в большом», то есть создание при-
ложений корпоративного уровня, всегда будет трудным и сложным делом. Чтобы
обслужить огромное количество пользователей по всему миру, обеспечив при этом
необходимую скорость загрузки страниц, обычно приходится включать в систему
балансировщики нагрузки, кэширующие серверы, многочисленные резервные ма-
шины, расположенные в территориально разнесенных точках. Поэтому платфор-
ма разработки, наверное, не так важна, как система в целом.
Мы не узнаем, насколько хороша платформа Node в действительности, пока
не увидим пример долгосрочного развертывания в крупных производственных
средах.

Использование серверов, экономия


затрат и экологичный Интернет
Смысл борьбы за максимальную эффективность (увеличение числа обрабаты-
ваемых запросов в секунду) – не только в том, чтобы получить удовольствие от хо-
рошо проделанной технической работы. Имеются также реальные плюсы с точки
зрения бизнеса и окружающей среды. Присущая Node способность обрабатывать
больше запросов в секунду означает, что можно приобрести меньше серверов. То
есть сделать больше меньшими средствами.
Грубо говоря, чем больше серверов, тем выше затраты и тем сильнее воздей-
ствие на окружающую среду, и наоборот. Существует целая наука о сокращении
затрат и вреда окружающей среде, причиняемого инфраструктурой веб-серверов,
и эта грубая рекомендация не может охватить всех деталей. Но общая цель оче-
видна – сократить количество серверов, снизить затраты и уменьшить ущерб, на-
носимый окружающей среде.
В опубликованной корпорацией Intel статье «Increasing Data Center Efficien-
cy with Server Power Measurements» (http://download.intel.com/it/pdf/Server_
24 Что такое Node?
Power_Measurement_final.pdf) приводится методика оценки эффективности и
стоимости центров обработки данных. Следует учитывать много факторов, в том
числе конструкцию здания, систему охлаждения и проект вычислительной сис-
темы. Их эффективная реализация (эффективность ЦОД, плотность ЦОД и
плотность СХД) может сократить затраты и вред окружающей среде. Но все это
можно свести на нет, развернув неэффективную программную систему, которая
заставляет приобретать больше серверов. И наоборот, эффективная программная
система позволяет усилить преимущества эффективной организации ЦОД.

Как правильно: Node, Node.js


или Node.JS?
Правильно платформа называется Node.js, но в этой книге мы пишем Node,
следуя традиции, принятой на сайте nodejs.org, где говорится, что торговый знак –
Node.js (js строчными буквами), но в опубликованных материалах употребляется
название Node.

Резюме
В этой главе мы:
‰ узнали, что JavaScript живет не только внутри браузеров;
‰ поведали о различии между асинхронным и блокирующим вводом/вы-
водом;
‰ получили первое представление о платформе Node;
‰ поговорили о производительности Node.
Теперь мы готовы приступить к практической работе с Node. В главе 2 «На-
стройка Node» мы поговорим о конфигурировании окружения Node. Готовьтесь!
Глава 2. НАСТРОЙКА NODE
Перед тем как начать работу с Node, необходимо настроить среду разработки. Да-
лее мы будем использовать ее для разработки и развертывания в непроизводст-
венной системе.
В этой главе мы:
‰ узнаем, как собирать Node из исходного кода в системах Linux и Mac;
‰ узнаем, как установить менеджер пакетов npm и некоторые популярные
инструменты;
‰ немного поговорим о системе модулей в Node.
Итак, приступим.

Системные требования
Node лучше всего работает в операционных системах, совместимых со стандар-
том POSIX. Это различные клоны UNIX (Solaris и т. п.), а также UNIX-подобные
системы (Linux, Mac OS X и т. п.). На самом деле многие встроенные в Node функ-
ции – прямые интерфейсы к системным вызовам, описанным в POSIX.
Во многих зрелых языковых платформах (таких как Perl или Python) уже сфор-
мировался стабильный набор средств и API, который включается в дистрибутивы
операционных систем. Но Node все еще быстро развивается, поэтому включать
в дистрибутивы ОС готовые двоичные сборки было бы преждевременно. Следо-
вательно, предпочтительный метод установки Node – сборка из исходного кода.
Для этого необходим компилятор языка C (например, GCC) и Python 2.4
(или более поздняя версия). Если вы собираетесь использовать в сетевом коде
шифрование, то понадобится еще криптографическая библиотека OpenSSL. В со-
временных клонах UNIX эти средства почти всегда включаются в дистрибутив,
а конфигурационный скрипт Node (о нем речь пойдет ниже) обнаруживает их
присутствие. На случай, если вам придется устанавливать их в систему, имейте
в виду, что Python можно скачать с сайта http://python.org, а OpenSSL – с сайта
http://openssl.org.
Хотя ОС Windows не совместима с POSIX, Node можно установить на нее,
пользуясь POSIX-совместимыми средами (в версии Node 0.4.x и более ранних).
Начиная с версии 0.6.x, разработчики Node намереваются обеспечить возможность
сборки с помощью естественных для Windows средств. Инструкции по сборке
Node в Windows изменяются слишком быстро, чтобы стоило приводить их в книге,
но самые свежие можно найти на странице https://github.com/ry/node/wiki/
Installation. В описании шага 3b обсуждается сборка в Windows с помощью Cyg-
26 Настройка Node
win или MinGW. После того как Cygwin или MinGW установлены, все остальное
делается, как в POSIX-совместимых системах.

Установка в POSIX-совместимых
системах (Linux, Solaris, Mac и т. п.)
Получив общее представление, давайте займемся делом – покопаемся в скрип-
тах сборки. Общая последовательность стандартна: configure, make, make install, –
наверное, вы уже не раз встречались с ней при сборке других программ.
Официальные инструкции по установке можно найти на вики-сайте Node по
адресу https://github.com/ry/node/wiki/Installation.

Предварительная установка
инструментария
Мы уже отмечали, что для сборки Node необходимы три вещи: компилятор C,
Python и библиотеки OpenSSL. В процессе установки проверяется их наличие, и
если компилятор C или Python отсутствует, то установка завершится ошибкой.
Способ установки инструментария зависит от операционной системы.
Проверить, что необходимые программы имеются, можно так:

$ cc --version
i686-apple-darwin10-gcc-4.2.1 (GCC) 4.2.1 (Apple Inc. build 5666) (dot 3)
Copyright (C) 2007 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ python
Python 2.6.6 (r266:84292, Feb 15 2011, 01:35:25)
[GCC 4.2.1 (Apple Inc. build 5664)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

Установка средств разработки


в Mac OS X
В системе Mac OS X средства разработки (в том числе GCC) не входят в обя-
зательный комплект. Получить их можно двумя способами, причем оба бесплат-
ны. На инсталляционном DVD OS X имеется каталог Optional Installs, в котором
есть установщики различных дополнительных пакетов, в том числе Xcode, содер-
жащего средства разработки.
Другой способ – скачать (бесплатно) последнюю версию Xcode с сайта http://
developer.apple.com/xcode/.
Установка средств разработки в Mac OS X 27

Установка в свой домашний каталог


Традиционно сложилось, что разработчики предпочитают устанавливать Node
в свой домашний каталог. Недавние изменения, внесенные в Node 0.4.x и особенно
в npm 1.0, сделали это не столь обязательным. Возможно, вы захотите установить
Node в системный каталог (мы обсудим это в следующем разделе) или оставить
локальную установку для тестирования либо разработки.
Сначала посмотрим, как производится локальная установка Node.
1. Скачайте исходный код со страницы http://nodejs.org/#download. Можно
сделать это прямо в браузере или выполнить следующие команды:
$ mkdir src
$ cd src
$ wget http://nodejs.org/dist/node-v0.4.8.tar.gz
$ tar xvfz node-v0.4.8.tar.gz
$ cd node-v0.4.8
2. Перед сборкой необходимо сконфигурировать исходный код. Для этого
служит типичный скрипт configure, команда ./configure –help выведет
длинный список возможных параметров. Чтобы установить программу в
свой домашний каталог, запустите скрипт следующим образом:
$ ./configure --prefix=$HOME/node/0.4.8
Checking for program g++ or c++ : /usr/bin/g++
Checking for program cpp : /usr/bin/cpp
Checking for program ar : /usr/bin/ar
Checking for program ranlib : /usr/bin/ranlib
...
Скорее всего, скрипт завершится успешно и подготовит дерево исходного
кода к установке в выбранный каталог. В случае ошибки будет напечатано
сообщение о том, что необходимо исправить. Закончив конфигурирование,
переходите к следующему шагу.
3. Откомпилируйте программу:
$ make
... выводится длинный список сообщений компилятора
$ make install
4. По завершении установки не забудьте добавить инсталляционный каталог
в переменную PATH:
$ echo 'export PATH=$HOME/node/0.4.8/bin:${PATH}' >>~/.bashrc
$ . ~/.bashrc
Или для пользователей csh:
$ echo 'setenv PATH $HOME/node/0.4.8/bin:${PATH}' >>~/.cshrc
$ source ~/.cshrc
В результате должна быть создана такая структура каталогов:
28 Настройка Node

$ ls ~/node/0.4.8/
bin include lib share
$ ls ~/node/0.4.8/bin
node node-waf

Сделав всё это, переходите к разделу «Выполним несколько команд для про-
верки установки».

Зачем устанавливать в домашний каталог?


Причин для установки Node в домашний каталог две:
‰ для тестирования и разработки;
‰ из соображений безопасности.
Во-первых, у разработчика может возникнуть желание поэкспериментировать
с по-разному сконфигурированными экземплярами Node, проверить, как работает
приложение с разными версиями Node, или даже покопаться в самом коде Node.
В этих (и некоторых других) случаях лучше производить установку в домашний
каталог.
Соображения безопасности менее очевидны, поэтому остановимся на них по-
дробнее.
Допустим, вы работаете с клоном Unix, не имеете прав администратора, но
хотите использовать Node. Тогда вполне подойдет установка в домашний ка-
талог.
Другая причина – скачивание и исполнение скриптов, входящих в состав
Node, или относящихся к ней инструментов (например, Node Package Manager,
npm). Можете ли вы доверять их авторам? Быть может, номер версии 0.1.x или
0.2.x наводит на мысль о недостаточной стабильности или безопасности? Как бы
то ни было, старые версии npm выдавали грозные предупреждения при попытке
запуска под управлением sudo, и не без причины.
До выхода версии npm 1.0 все модули приходилось устанавливать в каталог
самого экземпляра Node. И ничего страшного в этом не было бы, если бы Node
не устанавливалась в системный каталог; в этом случае нужны права супер-
пользователя root, а на этапе установки пакета запускаются некоторые скрипты.
Но прав root у вас может не быть, а кроме того, локальные политики безопасности,
возможно, запрещают запускать неизвестные программы скачивания от имени
root. Устанавливая Node локально, вы ограничиваете потенциальный ущерб
своему домашнему каталогу. Счастливчик!
Начиная с версий Node 0.4.x и npm 1.0.x, принято устанавливать пакеты
в каталог приложения, а не в экземпляр Node. Для этого права root не нужны.
Таким образом, благодаря гибкому алгоритму нахождения пакетов теперь
можно установить контролируемый администратором экземпляр Node в систем-
ный каталог, а пакеты, необходимые вашему приложению, – локально. Подробнее
мы рассмотрим этот вопрос в следующей главе.
Установка средств разработки в Mac OS X 29

Установка в системный каталог


Для нормальной работы Node устанавливается в системный каталог, и вот по-
чему:
‰ это удобнее для повседневной работы;
‰ это позволяет нескольким приложениям или людям сообща использовать
одну установку Node;
‰ это предотвращает случайную перезапись файлов в инсталляционном ка-
талоге Node;
‰ это позволяет запускать Node-серверы на этапе инициализации системы.
Установка в системный каталог отличается от установки в домашний каталог
только в двух моментах.
‰ Выбор инсталляционного каталога. Это делается с помощью скрипта
configure, и по умолчанию (без параметра –prefix=) программа устанавли-
вается в каталог /usr/local:
$ ./configure # for /usr/local
$ ./configure –prefix=/usr/local/node/0.4.8

Вы можете выбрать каталог по своему усмотрению и с помощью configure


установить в него Node.
‰ Второе различие проявляется на шаге make install. Поскольку системные
каталоги почти всегда защищены от записи обычным пользователем, то
установку необходимо производить с правами root:
$ sudo make install
Обратите внимание, что если Node устанавливается в каталог, уже
присутствующий в переменной PATH, то изменять ее нет необходимости.

Установка в Mac OS X с помощью MacPorts


Конечно, можно установить Node в системе Mac OS X, применяя любой из
описанных выше способов. Поскольку это совместимая с UNIX система, то оба
будут прекрасно работать.
Однако существует проект MacPorts (http://www.macports.org/), в рамках
которого вот уже много лет собираются пакеты многочисленных программ
с открытым исходным кодом для установки в Mac OS X. Вот и для Node подготовлен
такой пакет. Если вы предварительно поставите MacPorts с помощью имеющегося
на сайте проекта установщика, то установка Node станет гораздо проще:

$ sudo port search nodejs


nodejs @0.4.8 (devel, net)
Evented I/O for V8 JavaScript
$ sudo port install nodejs
... длинный список сообщений о загрузке и установке необходимых зависимостей и самой Node
30 Настройка Node
Однако для npm такого пакета не существует.

Установка в Mac OS X с помощью homebrew


homebrew – еще один менеджер программных пакетов для Mac OS X с откры-
тым исходным кодом, который, по мнению некоторых, является идеальной за-
меной MacPorts. Его можно скачать с сайта http://mxcl.github.com/homebrew/.
После установки в соответствии с приведенными на сайте инструкциями восполь-
зоваться им для установки Node совсем просто:

$ brew search node


leafnode node
$ brew install node
==> Downloading http://nodejs.org/dist/node-v0.4.8.tar.gz
######################################################### 100.0%
==> ./configure --prefix=/usr/local/Cellar/node/0.4.8
==> make install
.. etc
$ brew search npm
npm can be installed thusly by following the instructions at
http://npmjs.org/

Установка в Linux с помощью систем


управления пакетами
Хотя включать готовый пакет Node в дистрибутивы Linux и других операцион-
ных систем пока преждевременно, это вовсе не означает, что его нельзя установить
с помощью менеджеров пакетов. На вики-сайте Node приведен перечень пакетов
Node для Debian, Ubuntu, OpenSUSE и Arch Linux. См. https://github.com/joyent/
node/wiki/Installing-Node.js-via-package-manager.
Например, вот как это делается в Debian:

# echo deb http://ftp.us.debian.org/debian/ sid main > /etc/apt/sources. list.d/sid.list


# apt-get update
# apt-get install nodejs # Documentation is great.

А вот как в Ubuntu:

# sudo apt-get install python-software-properties


# sudo add-apt-repository ppa:jerome-etienne/neoip
# sudo apt-get update
# sudo apt-get install nodejs

Можно ожидать, что со временем в дистрибутивы Linux и других ОС Node


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

Установка одновременно
нескольких экземпляров Node
Обычно не имеет смысла устанавливать несколько версий Node, так как это
только усложнит обслуживание системы. Но если вы работаете над исходным ко-
дом самой Node, тестируете приложение в разных выпусках Node или делаете что-
то подобное, то такая необходимость может возникнуть. Соответствующий способ
мало чем отличается от того, что было рассмотрено выше.
Вы, наверное, обратили внимание, что для установки нескольких версий Node
в разные каталоги на одной машине мы задавали параметр –prefix:

$ ./configure --prefix=$HOME/node/0.4.8

$ ./configure --prefix=/usr/local/node/0.4.8

На этом начальном шаге определяется инсталляционный каталог. Когда вый-


дет версия 0.4.9 или 0.6.1 или еще какая-то, достаточно будет изменить префикс, и
новая версия спокойно встанет бок о бок с предыдущими.
Чтобы переключиться с одной версии на другую, нужно всего лишь изменить
переменную PATH (в POSIX-совместимых системах):

$ export PATH=/usr/local/node/0.6.1/bin:${PATH}

Но со временем обслуживать такую систему становится утомительно. Для


каждого выпуска необходимо настраивать Node, npm и все сторонние модули,
а кроме того, показанная команда изменения PATH далеко не оптимальна. Изоб-
ретательные программисты придумали несколько систем управления версиями,
которые упрощают автоматическую настройку не только Node, но и npm, а заодно
включают команды для более удобного изменения PATH:
‰ https://github.com/visionmedia/n – менеджер версий;
‰ https://github.com/kuno/neco – Nodejs Ecosystem COordinator.

Выполним несколько команд


для проверки установки
Установив Node, хорошо бы сделать две вещи: проверить, что установка про-
шла успешно, и познакомиться с командными утилитами.

Командные утилиты Node


В базовой поставке Node есть две команды: node и node-waf. Как работает node,
мы уже видели. Эта команда используется для запуска командных скриптов и сер-
32 Настройка Node
верных процессов. Вторая команда, node-waf, представляет собой инструмент для
сборки расширений Node. В этой книге мы не будем ее рассматривать, желающие
могут посмотреть онлайновую документацию на сайте nodejs.org.
Простейший способ проверить, что только что установленная Node работает,
одновременно является лучшим способом получить справку о Node. Наберите
следующую команду1:

$ node --help
Usage: node [options] script.js [arguments]
Options:
-v, --version печатать версию node
--debug[=port] подключиться к удаленному отладчику через заданный
порт TCP, не останавливая выполнения
--debug-brk[=port] то же, но остановиться в script.js и
дождаться подключения удаленного отладчика
--v8-options печатать параметры командной строки v8
--vars печатать различные заданные при компиляции
переменные
--max-stack-size=val установить макс размер стека v8 (в байтах)

Переменные окружения:
NODE_PATH список разделенных ':' каталогов,
добавляемых в начало пути поиска модулей,
require.paths.
NODE_DEBUG печатать дополнительную отладочную информацию.
NODE_MODULE_CONTEXTS если равна 1, то модули загружаются в
собственные глобальные контексты.
NODE_DISABLE_COLORS если равна 1, то отключаются цвета в REPL.
Документацию см. на сайте http://nodejs.org/ или командой 'man node'

Эта команда выводит справку о порядке вызова, в которой перечислены пара-


метры командной строки.
Отметим, что существуют параметры для Node и V8 (не показаны выше). На-
помним, что Node построена на базе движка V8, а у последнего есть собственный
набор параметров, относящихся в основном к деталям компиляции в байт-код,
сборке мусора и алгоритмам управления кучей. Чтобы вывести их полный пере-
чень, задайте параметр --v8-options.
В командной строке можно задать параметры, единственный файл скрипта и
список аргументов этого скрипта. Аргументы скриптов мы рассмотрим в следую-
щем разделе.
Запустив Node без аргументов, вы окажетесь в интерактивной оболочке Java-
Script:

$ node
> console.log('Hello, world!');

1
Справка печатается по-английски, но для удобства читателя переведена. (Прим. перев.)
Выполним несколько команд для проверки установки 33

Hello, world!
> console.log(JSON.stringify(require.paths));
["/Users/davidherron/.node_libraries","/opt/local/lib/node"]

Здесь можно ввести произвольный скрипт для Node. Интерпретатор команд


обладает неплохими ориентированными на работу с терминала средствами и
очень полезен для интерактивных экспериментов с кодом. Вы ведь любите экс-
периментировать с кодом, правда? Ну и отлично!

Запуск скрипта в Node


А теперь посмотрим, как в Node запускаются скрипты. Это несложно, но сна-
чала еще раз выведем справку:

$ node --help
Usage: node [options] script.js [arguments]

Требуется всего лишь задать имя скриптового файла и его аргументы – дело,
знакомое любому, кто писал на других скриптовых языках.
Для начала создайте текстовый файл ls.js с таким содержимым:

var fs = require('fs');
var files = fs.readdirSync('.');
for (fn in files) {
console.log(files[fn]);
}

Скачивание исходного кода примеров


Код примеров для всех купленных вами книг издательства Packt можно скачать на
странице своей учетной записи на сайте http://www.PacktPub.com. Если вы купили
книгу где-то еще, то можете зарегистрироваться на странице http://www.PacktPub.
com/support, и файлы будут высланы вам по электронной почте.

Теперь запустите его, набрав команду:

$ node ls.js
app.js
ls.js

Это бледное подражание команде Unix ls (что вы и так наверняка поняли из


названия файла). Функция readdirSync – близкий аналог системного вызова Unix
readdir (введите man 3 readdir, если хотите получить дополнительные сведения),
она служит для получения списка файлов в каталоге.
Аргументы скрипта находятся в глобальном массиве process.argv. Чтобы по-
нять, как этот массив работает, модифицируйте ls.js следующим образом:

var fs = require('fs');
var dir = '.';
34 Настройка Node
if (process.argv[2]) dir = process.argv[2];
var files = fs.readdirSync(dir);
for (fn in files) {
console.log(files[fn]);
}

И запустите:
$ node ls2.js ../0.4.8/bin
node
node-waf

Запуск сервера в Node


Очень часто вы будете писать скрипты, являющиеся серверными процессами.
Поскольку мы все еще проверяем работоспособность установленного экземпля-
ра Node, а заодно знакомимся с использованием платформы, то запустим совсем
простенький HTTP-сервер. Позаимствуем его код с домашней страницы Node
(http://nodejs.org).
Создайте следующий файл с именем app.js:

var http = require('http');


http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello, World!\n');
}).listen(8124, '127.0.0.1');
console.log('Server running at http://127.0.0.1:8124');

И запустите его:
$ node app.js
Server running at http://127.0.0.1:8124

Это простейший веб-сервер на платформе Node. Если вам интересно, как он


работает, загляните в главы 4–6. А пока просто зайдите на страницу по адресу
http://127.0.0.1:8124 в своем браузере. Вы увидите такую картину:
Установка npm – менеджера пакетов для Node 35

Задумайтесь над вопросом, почему этот скрипт не завершается, хотя ls.js


завершался. В обоих случаях поток управления доходит до конца скрипта, но
процесс Node app.js продолжает работать, а ls.js – нет. Причина – в наличии
активных прослушивателей событий. Node всегда входит в цикл обработки событий,
а в скрипте app.js вызывается функция .listen, которая создает прослушиватель
событий, реализующий протокол HTTP. Именно этот прослушиватель и не
дает завершиться app.js, пока вы не нажмете в окне терминала Control-C или не
сделаете что-то подобное. В скрипте же ls.js нет ничего, что могло бы создать
долгоживущий прослушиватель событий, поэтому по достижении конца програм-
мы он завершается.

Установка npm – менеджера пакетов


для Node
Сама по себе Node – довольно простая система и состоит только из интерпре-
татора JavaScript и нескольких любопытных библиотек асинхронного ввода/выво-
да. Интересной ее делает, в частности, быстро растущая экосистема модулей, раз-
работанных третьими сторонами. В центре этой экосистемы находится программа
npm. В принципе, модули можно загружать в виде исходного кода и собирать само-
стоятельно. Но npm предлагает более простой путь; по существу, это стандартный
менеджер пакетов для Node, который значительно облегчает загрузку и использо-
вание модулей. Более подробно мы поговорим о нем в следующей главе.
Для установки npm введите следующую команду, как описано на домашней
странице сайта npmjs.org:

$ curl http://npmjs.org/install.sh | sh

Она скачивает и устанавливает в вашу систему скрипт оболочки. Быть может,


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

$ curl http://npmjs.org/install.sh | less

Эта команда устанавливает скрипт и пакет npm в инсталляционный каталог


Node. Чтобы все работало правильно, необходимо помнить о двух моментах.
Во-первых, если для запуска Node должна быть установлена переменная PATH,
то перед выполнением установщика npm проверьте ее значение:

$ export PATH=/path/to/node/0.n.y/bin:${PATH}
$ curl http://npmjs.org/install.sh | sh

Во-вторых, если Node установлена в системный каталог, для чего требовалось


выполнить команду sudo make install, то установку npm следует выполнять такой
командой:
36 Настройка Node

$ curl http://npmjs.org/install.sh | sudo sh

Наличие sudo sh означает, что процесс, в котором выполняется установка npm


(/bin/sh), работает с правами root.
Установив программу npm, подвергнем ее небольшому испытанию:

$ npm install -g hexy


/home/david/node/0.4.7/bin/hexy -> /home/david/node/0.4.7/lib/node_ modules/hexy/bin/
hexy_cmd.js
hexy@0.2.1 /home/david/node/0.4.7/lib/node_modules/hexy

$ hexy --width 12 ls.js


00000000: 7661 7220 6673 203d 2072 6571 var.fs.=.req
0000000c: 7569 7265 2827 6673 2729 3b0a uire('fs');.
00000018: 7661 7220 6669 6c65 7320 3d20 var.files.=.
00000024: 6673 2e72 6561 6464 6972 5379 fs.readdirSy
00000030: 6e63 2827 2e27 293b 0a66 6f72 nc('.');.for
0000003c: 2028 666e 2069 6e20 6669 6c65 .(fn.in.file
00000048: 7329 207b 0a20 2063 6f6e 736f s).{...conso
00000054: 6c65 2e6c 6f67 2866 696c 6573 le.log(files
00000060: 5b66 6e5d 293b 0a7d 0a [fn]);.}.

Более тесное знакомство с npm отложим до следующей главы. Утилита


hexy является одновременно библиотекой для Node и скриптом для распечатки
содержимого файла в шестнадцатеричном виде.

Запуск Node-серверов на этапе


инициализации системы
Выше мы запускали Node-сервер из командной строки. Это полезно для раз-
работки и тестирования, но не годится для нормального развертывания прило-
жения. Существуют общепринятые способы запуска серверных процессов, свои
в каждой операционной системе. Node-сервер запускается так же, как любой дру-
гой фоновый процесс (sshd, apache, MySQL и т. д.), например с помощью скриптов
запуска и останова.
В проект Node не включены скрипты запуска и останова для всех операционных
систем. Это, пожалуй, правильно: дистрибутив Node – не место для таких скриптов.
Считается, что они должны быть частью серверных приложений для Node. Тра-
диционно инициализацией системы ведал демон init, который управляет фоно-
выми процессами с помощью скриптов, находящихся в каталоге /etc/init.d.
В дистрибутивах Fedora и Redhat этот процесс все еще существует, а в других
системах применяются иные менеджеры демонов, например Upstart или launchd.
Написание скриптов запуска и останова – только часть дела. Веб-серверы
должны быть надежными (например, автоматически перезапускаться после сбоя),
легко управляемыми (хорошо интегрироваться с принятой практикой админист-
Запуск Node-серверов на этапе инициализации системы 37

рирования системы), допускающими наблюдение (сохранять все, что выведено на


STDOUT в журналы) и т. д. Node – это, скорее, конструктор, включающий детали
для построения серверов, а не готовый, законченный сервер. Для реализации
настоящего веб-сервера на платформе Node необходимо написать скрипты для
интеграции со средствами управления фоновыми процессами, имеющимися
в ОС, для ведения журналов, для обеспечения защиты от вредоносных клиентов,
например DoS-атак, и многое другое.
Ниже перечислены инструменты и методики интегрирования Node-серверов
со средствами управления фоновыми процессами в нескольких операционных си-
стемах, позволяющие гарантировать непрерывное присутствие сервера, начиная
с момента инициализации системы. Чуть ниже мы вкратце рассмотрим также ис-
пользование программы Forever в системе Debian. Итак, вот перечень способов
запустить Node как работающий в фоновом режиме демон на разных платформах:
‰ nodejs-autorestart (https://github.com/shimondoodkin/nodejs-autorestart) –
управление экземпляром Node в тех дистрибутивах Linux, где используется
Upstart (Ubuntu, Debian и др.);
‰ fugue (https://github.com/pgte/fugue) наблюдает за Node-сервером и пере-
запускает его после сбоя;
‰ forever (https://github.com/indexzero/forever) – небольшая командная ути-
лита Node, которая гарантирует, что скрипт будет работать «вечно». О том,
что такое «вечно», Чарли Роббинс (Charlie Robbins) написал статью в блоге
(http://blog.nodejitsu.com/ keep-a-nodejs-server-up-with-forever);
‰ node-init (https://github.com/frodwith/node-init) – Node-скрипт, который
превращает приложение для Node в LSB-совместимый скрипт, запускаемый
на этапе инициализации. LSB (Linux Standard Base) – спецификация
совместимости с Linux;
‰ launchtool для Debian (http://people.debian.org/~enrico/launchtool.html) –
системная утилита для управления запуском любой команды, в том числе
в форме демона;
‰ средство Upstart для Ubuntu (http://upstart.ubuntu.com/), можно ис-
пользовать автономно (http://caolanmcmahon.com/posts/deploying_node_
js_with_upstart) или вместе с monit (http://howtonode.org/deploying-node-
upstart-monit) для управления Node-сервером;
‰ в Mac OS X необходимо написать скрипт для launchd. Apple разместила
руководство по созданию launchd-скриптов по адресу http://developer.
apple.com/library/mac/documentation/MacOSX/Conceptual/ BPSystemStartup/
Articles/LaunchOnDemandDaemons.html.
Чтобы продемонстрировать, о чем идет речь, воспользуемся утилитой forever
в сочетании со скриптом инициализации, совместимым с LSB, для реализации
простенького Node-сервера. В качестве сервера используется виртуальный вы-
деленный сервер с системой Debian. Предполагается, что Node и npm установлены
в каталог /usr/local/node/0.4.8. Следующий серверный скрипт помещен в файл
/usr/local/app.js (не самое подходящее место для установки приложения, но для
демонстрации сойдет):
38 Настройка Node

#!/usr/bin/env node
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(1337);

Обратите внимание на первую строку этого скрипта. Этот общепринятый


в Unix/POSIX прием позволяет сделать скрипт исполняемой программой.
Утилита forever устанавливается следующим образом:

$ sudo npm install -g forever

Она управляет фоновыми процессами: умеет перезапускать их после сбоя,


перенаправляет стандартный вывод и стандартный вывод для ошибок в файлы
журналов и обладает рядом других полезных функций. С ней стоит познакомить-
ся поближе.
Последний фрагмент общей картины – скрипт /etc/init.d/node, аналогичный
любому другому скрипту инициализации в каталоге /etc/init.d/:

#! /bin/sh -e
set -e
PATH=/usr/local/node/0.4.8/bin:/bin:/usr/bin:/sbin:/usr/sbin
DAEMON=/usr/local/app.js
case "$1" in
start) forever start $DAEMON ;;
stop) forever stop $DAEMON ;;
force-reload|restart)
forever restart $DAEMON ;;
*) echo "Usage: /etc/init.d/node {start|stop|restart|force-reload}"
exit 1
;;
esac
exit 0

В системе Debian для конфигурирования этого скрипта инициализации нуж-


но выполнить такую команду:

$ sudo /usr/sbin/update-rc.d node defaults

Она конфигурирует систему так, чтобы при загрузке вызывался скрипт /etc/
init.d/node с параметром start, а при останове – он же с параметром stop. Иначе
говоря, во время загрузки или останова выполняются соответственно следующие
строки:

start) forever start $DAEMON ;;


stop) forever stop $DAEMON ;;
Запуск Node-серверов на этапе инициализации системы 39

Скрипт инициализации можно выполнить и вручную:

$ sudo /etc/init.d/node start


info: Running action: start
info: Forever processing file: /usr/local/app.js

Этот скрипт исполняется под управлением forever, и мы можем попросить


forever вывести состояние всех запущенных ей процессов:

$ sudo forever list


info: Running action: list
info: Forever processes running
[0] node /usr/local/app.js [16666, 16664] /home/me/.forever/7rd6.log 0:0:1:24.817

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


браузера:

Если сервер запущен под управлением forever, то мы увидим следующие про-


цессы:

$ ps ax | grep node
16664 ? Ssl 0:00 node /usr/local/node/0.4.8/bin/forever start /usr/local/ app.js
16666 ? S 0:00 node /usr/local/app.js

Когда вам надоест и вы захотите остановить сервер, выполните такую команду:

$ sudo /etc/init.d/node stop


info: Running action: stop
info: Forever processing file: /usr/local/app.js
info: Forever stopped process:
[0] node /usr/local/app.js [5712, 5711] /home/me/.forever/Gtex.log 0:0:0:28.777
$ sudo forever list
info: Running action: list
info: No forever processes running
40 Настройка Node

Использование всех процессорных ядер


в многоядерной системе
V8 – однопоточный движок JavaScript. Для браузера Chrome этого достаточно,
однако означает, что Node-сервер, работающий на только что купленном 16-ядер-
ном сервере, задействует всего одно ядро, а 15 остальных простаивают. Начальник
может потребовать объяснений.
Однопоточный процесс использует только одно процессорное ядро. Это факт,
от которого никуда не уйти. Чтобы задействовать в одном процессе несколько
ядер, необходима многопоточная программа. Однако принятая в Node парадигма
проектирования без потоков, хотя и позволяет упростить модель программирова-
ния, одновременно означает, что Node не использует несколько ядер. И что же вам
делать? А точнее, как умиротворить начальника?
Есть несколько проектов, посвященных разработке многопроцессных конфигу-
раций Node для повышения надежности и задействования всех имеющихся про-
цессорных ядер.
Основная идея состоит в том, чтобы запустить несколько процессов Node и
распределять между ними поступающие запросы. Имея кластер из однопоточных
процессов, вы сможете использовать все ядра и оправдать денежные вложения
в приобретение мощного сервера – начальство останется довольно.
Один из таких проектов называется Cluster (https://github.com/ LearnBoost/
cluster), авторы описывают его как «расширяемый менеджер многоядерных
серверов для Node.js». Он запускает конфигурируемый набор дочерних процес-
сов, перезапускает их после сбоя и располагает богатыми средствами протоко-
лирования, управления из командной строки и сбора статистики. Более ранний
проект Spark закрылся, уступив место Cluster.
В состав проекта Cluster входят несколько примеров конфигурации сервера,
демонстрирующих, на что он способен. Давайте установим его и на одном из при-
меров посмотрим, как с ним работать.

$ sudo npm install cluster


cluster@0.6.4 ./node_modules/cluster
log@1.2.0

Возьмем в качестве образца пример reload.js и на основе app.js создадим та-


кой скрипт cluster-app.js:

#!/usr/bin/env node
var http = require('http');
var cluster = require('cluster');
var server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
})
cluster(server).set('workers', 2).use(cluster.reload())
.listen(1337);
Запуск Node-серверов на этапе инициализации системы 41

В этой конфигурации создается кластер из двух процессов, между которыми


разделяется нагрузка, причем после модификации каких-либо файлов кластер бу-
дет автоматически перезагружаться. Дополнительные сведения вы найдете в до-
кументации по проекту Cluster.
Данный скрипт можно запустить командой node cluster-app.js, но лучше
сделать это из /etc/init.d/node. Для этого достаточно записать в переменную
DAEMON такое значение:

DAEMON=/usr/local/cluster-app.js

А затем:

$ sudo /etc/init.d/node start


info: Running action: start
info: Forever processing file: /usr/local/cluster-app.js
$ sudo forever list
info: Running action: list
info: Forever processes running
[0] node /usr/local/cluster-app.js [6522, 6521]
$ ps ax | grep node
6521 ? Ssl 0:00 node /usr/local/node/0.4.8/bin/forever start /usr/local/ cluster-
app.js
6522 ? Sl 0:15 node /usr/local/cluster-app.js
6541 ? S 0:00 /usr/local/node/0.4.8/bin/node /usr/local/cluster-app. js
6542 ? S 0:00 /usr/local/node/0.4.8/bin/node /usr/local/cluster-app.js

Теперь у нас запущен многопроцессный Node-сервер. Команда ps показывает


два процесса, и убедиться в том, что они работают, можно, набрав в браузере
адрес http://example.com:1337/, – появится сообщение «Hello, World». Теперь
немного модифицируем скрипт cluster-app.js и обновим страницу в браузере (не
перезагружая сервера). Появится такое сообщение:

Это результат того, что мы воспользовались встроенной в Cluster функцией


автоматической перезагрузки.
42 Настройка Node

Резюме
В этой главе мы многое узнали об установке Node, использовании командных
утилит и о запуске Node-сервера. Мы также мимоходом коснулись деталей, кото-
рые подробнее рассмотрим ниже. Наберитесь терпения.
Точнее, мы рассмотрели следующие вопросы:
‰ скачивание и компиляция исходного кода Node;
‰ установка Node в домашний каталог для целей разработки и в системный
каталог – для развертывания в производственной системе;
‰ установка npm, стандартного менеджера пакетов для Node;
‰ запуск Node-скриптов и Node-серверов;
‰ что необходимо для превращения Node в надежный фоновый процесс;
‰ использование нескольких процессов для задействования всех имеющихся
процессорных ядер.
Теперь, познакомившись с тем, как настроить систему, мы можем перейти
к написанию приложений для Node. Но сначала нужно разобраться с модулями –
базовыми строительными блоками, из которых составляются приложения Node.
Этим мы и займемся в следующей главе.
Глава 3. МОДУЛИ NODE
Прежде чем писать приложения для Node, необходимо познакомиться с модулями
и пакетами Node. Модули и пакеты – это строительные блоки, позволяющие раз-
бить приложение на более мелкие части.
В этой главе мы:
‰ узнаем, что такое модуль;
‰ познакомимся со спецификацией модулей CommonJS;
‰ узнаем, как Node ищет модули;
‰ познакомимся с системой управления пакетами npm.
Итак, приступим.

Что такое модуль?


Модули – это базовые строительные блоки для конструирования приложе-
ний Node. Мы уже видели их в действии – любой JavaScript-файл, используемый
в Node, является модулем. Теперь пришло время рассказать о том, как они рабо-
тают.
В примере ls.js в главе 2 был такой код для подключения модуля fs, то есть
для получения доступа к содержащимся в нем функциям:

var fs = require('fs');

Функция require ищет запрошенный модуль и загружает его определение


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

var count = 0;
exports.next = function() { return count++; }

Здесь определены экспортируемая функция и локальная переменная. Теперь


воспользуемся ими.
44 Модули Node

Функция require('./simple') возвращает тот самый объект, exports, полю


next которого мы присвоили функцию в скрипте simple.js. При каждом обраще-
нии к s.next вызывается функция next из simple.js, которая возвращает (и ин-
крементирует) значение переменной count. Именно поэтому каждое обращение к
s.next возвращает число, на единицу большее предыдущего.
Правило следующее: все, что присвоено полям объекта exports (функции, объ-
екты), экспортируется из модуля, а объекты, определенные внутри модуля, но не
присвоенные полям exports, вне этого модуля не видны. Это пример инкапсуляции.
Теперь, получив общее представление о модулях, копнем глубже.

Модули Node
На реализацию модулей Node оказала большое влияние спецификация моду-
лей CommonJS (описывается в конце главы), хотя есть и отличия. Но эти отличия
проявляются, только если вы собираетесь писать общий код для Node и других
систем, основанных на CommonJS. Беглый взгляд на спецификацию Modules/1.1.1
показывает, что различия второстепенные, и для нашей цели – научиться работать
с Node – вникать в них необязательно.

Как Node ищет модули,


затребованные в require('module')?
В Node модули хранятся в файлах, в каждом файле по одному модулю. Есть
несколько подходов к именованию модулей и к размещению их в файловой сис-
теме. В целом получается весьма гибкая система, особенно в сочетании с npm,
стандартным менеджером пакетов для Node.

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


Вообще говоря, имя модуля – это путь, но без расширения имени файла. Та-
ким образом, когда мы пишем require('./simple'), Node знает, что необходимо
добавить к имени файла расширение .js и загрузить файл simple.js.
Как Node ищет модули, затребованные в require('module')? 45

Естественно, ожидается, что файлы, имена которых заканчиваются на .js, со-


держат код, написанный на JavaScript. Node поддерживает также модули в виде
двоичных платформенных библиотек. В таком случае имя файла должно окан-
чиваться расширением .node. Обсуждение того, как писать двоичные модули для
Node, выходит за рамки этой книги, но сказанного достаточно, чтобы вы поняли,
о чем идет речь, когда столкнетесь с ними.
Некоторые модули Node не являются файлами в файловой системе, а «зашиты»
в исполняемый файл Node. Это модули ядра (Core), документированные на сайте
nodejs.org. Изначально они существуют в виде файлов в дереве исходного кода
Node, но в ходе сборки прикомпилируются к исполняемому файлу.
Существуют три типа идентификаторов модулей: относительные, абсолютные
и верхнего уровня.
Относительные идентификаторы модулей начинаются строкой "./" или
"../", а абсолютные – строкой "/". Здесь имеется полная аналогия с семантикой
файловой системы, совместимой с POSIX, в которой пути записываются
относительно исполняемого файла.
Понятно, что абсолютные идентификаторы модулей записываются относи-
тельно корня файловой системы.
В начале идентификатора модуля верхнего уровня нет ни ".", ни "..", ни "/",
это просто имя модуля. Такие модули хранятся в одном из нескольких предо-
пределенных каталогов, например node_modules, или в каталогах, перечисленных
в массиве require.paths. Мы обсудим их ниже.

Локальные модули внутри приложения


Все множество мыслимых модулей можно разбить на две категории: являю-
щиеся и не являющиеся частью приложения. Модули, не являющиеся частью кон-
кретного приложения, написаны, имея в виду какую-то обобщенную цель. Начнем
с реализации модулей, используемых только в вашем приложении.
В типичном приложении модули распределены по нескольким каталогам. Они
хранятся в системе управления версиями и впоследствии копируются на серверы.
Эти модули знают относительные пути к своим «братьям» и могут использовать
эту информацию, чтобы ссылаться друг на друга по относительным идентифика-
торам.
Чтобы лучше понять, как это организовано, возьмем в качестве примера струк-
туру одного из пакетов для Node, каркаса разработки веб-приложений Express. Он
состоит из нескольких модулей, организованных в виде иерархии, которую раз-
работчики Express находят полезной. Подобные иерархии имеет смысл создавать,
когда приложение достигает определенного уровня сложности, оправдывающего
его разбиение на части, большие, чем модуль, но меньшие, чем приложение. К со-
жалению, специального термина для таких структурных компонентов в Node нет,
поэтому приходится употреблять корявую фразу «разбивать на части, большие,
чем модуль». Каждая такая часть представляется каталогом с несколькими моду-
лями.
46 Модули Node

В этом примере, наверное, чаще всего приходится ссылаться на модуль utils.


js по относительному пути. В зависимости от того, какой исходный файл ссылает-
ся на utils.js, вызов функции require может записывать по-разному:

var utils = require('./lib/utils');


var utils = require('./utils');
var utils = require('../utils');

Комплектация приложения с внешними


зависимостями
Для включения модулей, находящихся в каталоге node_modules, употребляется
идентификатор верхнего уровня:

var express = require('express');

Node производит поиск модулей во всех каталогах node_modules, а их сущест-


вует несколько. Алгоритм начинает поиск в каталоге текущего модуля, потом до-
бавляет в путь node_modules и ищет там. Если модуль не найден в каталоге node_
Как Node ищет модули, затребованные в require('module')? 47

modules, то Node переходит к родительскому каталогу и пробует еще раз – и так до


тех пор, пока не дойдет до корня файловой системы.
В предыдущем примере имеется каталог node_modules, в котором есть подката-
лог qs. При такой структуре модуль qs доступен любому модулю внутри Express,
нужно лишь написать:

var qs = require('qs');

Но что, если вы захотите использовать каркас Express в своем приложении?


Ничего сложного, просто создайте каталог node_modules внутри дерева своего
приложения и установите туда Express:

Здесь показано гипотетическое приложение drawapp. Если каталог node_modules


расположен так, как показано на рисунке, то любой модуль внутри drawapp может
получить доступ к express следующим образом:

var express = require('express');

Однако те же самые модули не смогут добраться до модуля qs, скрытого внут-


ри каталога node_modules, являющегося частью самого каркаса Express. Просмотр
каталогов node_modules, содержащих искомый модуль, производится вверх по
иерархии файловой системы, без захода в дочерние каталоги.
Аналогично: если установить модуль в каталог lib/node_modules, то он будет
доступен из draw.js и svg.js, но недоступен из index.js. Как и раньше, поиск про-
исходит вверх от текущего каталога, а не вглубь него.
При обходе каталогов node_modules Node останавливается, как только найдет
искомый модуль. Так, если ссылка встречается в файле draw.js или svg.js, то бу-
дут просмотрены следующие каталоги:
48 Модули Node
‰ /home/david/projects/drawapp/lib/node_modules
‰ /home/david/projects/drawapp/node_modules
‰ /home/david/projects/node_modules
‰ /home/david/node_modules
‰ /home/node_modules
‰ /node_modules
Каталог node_modules играет важнейшую роль, позволяя системе управления
пакетами выпутаться из лабиринта конфликтующих версий. Вместо того чтобы
помещать все модули в одно место и медленно сходить с ума, пытаясь разрешить
зависимости от конфликтующих номеров версий, мы можем завести несколь-
ко каталогов node_modules и при необходимости складывать конкретные версии
в конкретное место. Разные версии одного модуля могут находиться в разных ка-
талогах node_modules, и при условии, что эти каталоги расположены правильно
относительно друг друга, никаких конфликтов не возникнет.
Пусть, например, вы написали приложение, в котором используется модуль
forms (https://github.com/caolan/forms) для построения форм, и, когда у вас уже
накопились сотни форм, авторы модуля внесли в него несовместимые изменения.
Переделывать и заново тестировать все формы сразу вам не хочется, лучше делать
это постепенно. Для этого надо будет создать в приложении два каталога, органи-
зовать в каждом свой подкаталог node_modules и поместить в них разные версии
модуля forms. Затем, по мере перевода очередной формы на новый модуль forms,
ее код перемещается в каталог, где находится новая версия.

Системные модули в каталогах,


перечисленных в массиве require.paths
При поиске каталогов node_modules Node не ограничивается деревом приложе-
ния. Алгоритм доходит до корня файловой системы, поэтому можно создать ката-
лог /node_modules и организовать в нем глобальный репозиторий модулей. Имен-
но здесь будет завершаться поиск модуля, не найденного ни в каком другом месте.
Но Node предоставляет и еще один механизм, основанный на переменной
require.paths. Это массив имен каталогов, в которых следует искать модули.
Приведем пример:

$ node
> require.paths;
["/home/david/.node_modules","/home/david/.node_libraries","/usr/local/ lib/node"]

Для заполнения массива require.paths используется переменная окружения


NODE_PATH:

$ export NODE_PATH=/usr/lib/node
$ node
> require.paths;
Как Node ищет модули, затребованные в require('module')? 49

["/usr/lib/node","/home/david/.node_libraries","/usr/local/lib/node"]
>

Раньше в программах для Node часто применялась следующая идиома для до-
бавления новых элементов в массив require.paths: require.paths.push(__dirname).
Однако теперь она не рекомендуется, потому что, как выяснилось, является источ-
ником путаницы. Хотя так делать можно и даже еще остались модули, в которых
эта идиома встречается, но смотрят на ее использование с большим неодобрением.
Если несколько модулей помещают каталоги в require.paths, то результаты не-
предсказуемы.
В большинстве случаев рекомендуется устанавливать модули в каталоги node_
modules.

Составные модули – модули-каталоги


Составной модуль может включать несколько внутренних модулей, файлы
данных, файлы шаблонов, документацию, тесты и прочее. Все это можно помес-
тить в хорошо продуманную структуру каталогов, которую Node будет рассматри-
вать как модуль и загружать командой require('moduleName'). Для этого следует
добавить в каталог файл модуля index.js или файл с именем package.json. Файл
package.json должен содержать данные, описывающие модуль, в формате, очень
похожем на формат файла package.json, используемого менеджером пакетов npm
(см. ниже). В обоих случаях для совместимости с Node достаточно очень неболь-
шого набора полей, распознаваемых npm.
Точнее, Node распознает следующие поля в файле package.json:

{ name: "myAwesomeLibrary",
main: "./lib/awesome.js" }

При таком файле package.json команда require('myAwesomeLibrary') найдет


этот каталог и загрузит файл

/path/to/node_modules/myAwesomeLibrary/lib/awesome.js

Если файла package.json нет, то Node будет вместо него искать файл index.js,
то есть загрузит файл:

/path/to/node_modules/myAwesomeLibrary/index.js

В любом случае (index.js или package.json) реализовать составной модуль,


содержащий внутренние модули и другие файлы, несложно. Если вернуться к рас-
смотренной выше структуре пакета Express, то мы увидим, что некоторые модули
пользуются относительными идентификаторами для ссылки на другие модули
в пакете, а для включения модулей, разработанных кем-то другим, можно вос-
пользоваться каталогом node_ modules.
50 Модули Node

Менеджер пакетов для Node (npm)


В главе уже отмечалось, что npm – это система управления и распростране-
ния пакетов для Node, ставшая стандартом де-факто. Концептуально она похожа
на такие инструменты, как apt-get (Debian), rpm/yum (Redhat/Fedora), MacPorts
(Mac OS X), CPAN (Perl) и PEAR (PHP). Ее задача – обеспечить публикацию и
распространение пакетов Node через Интернет с помощью простого интерфейса
командной строки. Npm позволяет быстро находить пакеты для решения конкрет-
ной задачи, загружать и устанавливать их, а также управлять уже установленными
пакетами.
В npm определен формат пакета для Node, основанный на спецификации
CommonJS.

Формат npm-пакета
Npm-пакет представляет собой структуру каталогов, описанную в файле
package.json. Именно так мы выше определили составной модуль, отличие только
в том, что npm распознает значительно больше полей, чем Node. Исходной точкой
для определения формата package.json для npm послужила спецификация Com-
monJS Packages/1.0. Получить документацию по структуре файла package.json
позволяет следующая команда:

$ npm help json

Простейший файл package.json выглядит следующим образом:

{ name: "packageName",
version: "1.0",
main: "mainModuleName",
modules: {
"mod1": "lib/mod1",
"mod2": "lib/mod2"
}
}

Файл представлен в формате JSON, с которым вы как программист на Java-


Script, должно быть, встречались уже сотни раз.
Наиболее важны поля name и version. Значение name подставляется в URL-
адреса и названия команд, поэтому выбирайте его с учетом безопасности в этих
контекстах. Если вы собираетесь опубликовать пакет в общедоступном репозито-
рии npm-пакетов, то проверьте, не занято ли выбранное вами имя. Для этого мож-
но обратиться к сайту http://search.npmjs.org с помощью следующей команды:

$ npm search packageName


Менеджер пакетов для Node (npm) 51

Поле main служит той же цели, что и в составных модулях (см. предыдущий
раздел). Оно ссылается на модуль, который следует загружать при вызове функ-
ции require('packageName'). Пакеты могут содержать много модулей, и все их
можно перечислить в списке модулей.
Пакеты можно упаковывать в tgz-архивы, что особенно удобно для распро-
странения через Интернет.
Пакет может объявлять зависимости от других пакетов. Именно благодаря
этой возможности npm способен автоматически устанавливать модули, необходи-
мые тому, который устанавливается явно. Зависимости объявляются следующим
образом:

"dependencies":
{ "foo" : "1.0.0 - 2.9999.9999"
, "bar" : ">=1.0.2 <2.1.2"
}

Людям будет проще найти пакет в npm-репозитории (http://search.npmjs.


org), если пакет снабжен описанием (поле description) и ключевыми словами
(поле keywords). Для сведений о владельце предназначены поля homepage, author
и contributors:

"description": "My wonderful packages walks dogs",


"homepage": "http://npm.dogs.org/dogwalker/",
"author": dogwhisperer@dogs.org

В состав некоторых npm-пакетов входят исполняемые программы, которые


должны быть установлены в каталог, упомянутый в переменной PATH для данного
пользователя. Они объявляются с помощью поля bin. Это словарь, отображающий
имена команд на имена реализующих их скриптов. Командные скрипты уста-
навливаются в каталог, содержащий исполняемый файл node, под указанным
именем.

bin: {
'nodeload.js': './nodeload.js', 'nl.js': './nl.js'
},

В поле directories документируется структура каталогов пакета. Каталог lib


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

directories: { lib: './lib', bin: './bin' },

В поле scripts перечисляются скриптовые команды, запускаемые на различ-


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

$ npm help scripts

Это лишь краткое введение в формат npm-пакетов, для получения полной до-
кументации наберите npm help json.

Поиск npm-пакетов
По умолчанию npm-модули загружаются из общедоступного репозитория
пакетов по адресу http://npmjs.org. Если вы знаете имя модуля, то для его уста-
новки достаточно ввести такую команду:

$ npm install moduleName

А если не знаете? Как найти интересующие вас модули?


На сайте http://npmjs.org имеется индекс всех модулей в репозитории, а сайт
http://search.npmjs.org позволяет производить поиск в этом индексе. В команд-
ном интерфейсе npm тоже имеется возможность поискать в индексе:

$ npm search mp3


mediatags Tools extracting for media meta-data tags =coolaj86 util m4a aac mp3 id3
jpeg exiv xmp
node3p An Amazon MP3 downloader for NodeJS. =ncb000gt

Ну а найдя нужный модуль, вы можете установить его как обычно:

$ npm install mediatags

После установки модуля часто возникает желание почитать документацию,


которую можно найти на сайте модуля. URL-адрес этого сайта указан в поле
homepage файла package.json. Узнать его проще всего, выведя содержимое файла
package.json командой npm view:

$ npm view zombie


...
{ name: 'zombie',
description: 'Insanely fast, full-stack, headless browser testing using Node.js',

version: '0.9.4',
homepage: 'http://zombie.labnotes.org/',

npm ok

Команда npm view позволяет также напечатать произвольное поле из файла


package.json; например, вот как можно посмотреть одно лишь поле homepage:
Менеджер пакетов для Node (npm) 53

$ npm view zombie homepage


http://zombie.labnotes.org/

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

Получение справки по npm


Пожалуй, важнее всего знать о том, как получить справку. Основной раздел
справки, в котором перечисляются все команды npm, выглядит следующим об-
разом:

Для большинства команд получить справку по конкретной команде можно так:


54 Модули Node

$ npm help <command>

На сайте npm (http://npmjs.org/) имеется FAQ, включенный также в состав


дистрибутива npm. Наверное, чаще всего задается вопрос: «Почему npm меня так
ненавидит?» А ответ на него звучит так: «npm не умеет ненавидеть. Он любит всех,
даже вас».

Просмотр информации о пакете


Команда npm view рассматривает файл package.json как данные и позволяет
опрашивать его, задавая в качестве критерия имя поля. Например, вот как узнать
о зависимостях пакета:

$ npm view google-openid dependencies


{ express: '>= 0.0.1',
openid: '>= 0.1.1 <= 0.1.1' }

В файле package.json может быть указан URL-адрес репозитория пакетов. По-


этому если вы захотите скачать исходный код пакета, то можете поступить сле-
дующим образом:

$ npm view openid repository.url


git://github.com/havard/node-openid.git

$ git clone git://github.com/havard/node-openid.git


Cloning into node-openid...
remote: Counting objects: 253, done.
remote: Compressing objects: 100% (253/253), done.
remote: Total 253 (delta 148), reused 0 (delta 0)
Receiving objects: 100% (253/253), 63.29 KiB, done.
Resolving deltas: 100% (148/148), done.

Какая версия Node необходима для работы пакета?

$ npm view openid engines


node >= 0.4.1

Установка npm-пакета
Команда npm install упрощает установку найденного вам предела мечтаний:

$ npm install openid


openid@0.1.6 ./node_modules/openid

$ ls node_modules/
openid

Отметим, что этот пакет установлен в локальный каталог node_modules. Паке-


ты можно устанавливать и в другие места, для чего следует либо изменить теку-
Менеджер пакетов для Node (npm) 55

щий каталог, либо попросить npm выполнить глобальную установку. Например,


следующая команда делает текущим каталог /var/www и затем устанавливает пакет
в каталог /var/www/node_modules, где по принятому соглашению хранятся модули,
используемые в нескольких сайтах:

$ cd /var/www
$ npm install openid
openid@0.1.6 ./node_modules/openid

Npm проводит различие между глобальным и локальным режимами. Обыч-


но он работает в локальном режиме и устанавливает пакеты в локальный каталог
node_modules по соседству с кодом вашего приложения. В глобальном режиме па-
кеты устанавливаются в инсталляционное дерево Node (каталоги, перечисленные
в массиве require.paths), а не в локальный каталог node_modules.
Первый способ установки пакетов в глобальном режиме – задать флаг –g:

$ npm install -g openid


openid@0.1.6 /usr/local/node/0.4.7/lib/node_modules/openid

$ which node
/usr/local/node/0.4.7/bin/node

Куда будет установлен модуль в глобальном режиме, зависит от того, куда


были установлены файлы Node.
Второй способ глобальной установки – изменить конфигурационные парамет-
ры npm. Таких параметров много, и в свое время мы их обсудим, а пока упомянем
тот, что относится к обсуждаемому вопросу:

$ npm set global=true


$ npm get global
true
$ npm install openid
openid@0.1.6 /usr/local/node/0.4.7/lib/node_modules/openid

Чтобы узнать обо всех каталогах, используемых npm, введите такую команду:

$ npm help folders

Использование установленных пакетов


Смысл установки пакета – в том, чтобы программа для Node могла воспользо-
ваться содержащимися в нем модулями:

var openid = require('openid');

Npm делает все для того, чтобы эта операция прошла гладко.
56 Модули Node
Некоторые пакеты включают внутренние модули, которые сами по себе могут
оказаться полезны. Например, в текущую версию модуля openid включен модуль
кодирования и декодирования base64, полезный и другим программам:

var base64 = require('openid/lib/base64').base64;

Но тут есть опасность, что модуль openid изменит собственную реализацию


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

Какие пакеты у меня установлены?


Команда npm list выводит список установленных пакетов, основываясь на ре-
зультатах поиска от текущего каталога. Напомним, что Node ищет модули, начиная
от каталога, в котором находится исполняемая программа. Поэтому установлен-
ные пакеты ищутся в каталогах node_modules в текущем каталоге и его родителях.
Обратите внимание, как изменяется список найденных модулей в зависимо-
сти от того, в каком каталоге вы находитесь:

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


но для обработки другими командами такой формат неудобен. Чтобы изменить
формат списка на более пригодный для разбора, установите конфигурационный
параметр parseable:

$ npm set parseable=true


$ npm list
Менеджер пакетов для Node (npm) 57

/home/david/Node/chap06
/home/david/Node/chap06/node_modules/ejs
/home/david/Node/chap06/node_modules/express
/home/david/Node/chap06/node_modules/express/node_modules/connect
/home/david/Node/chap06/node_modules/express/node_modules/mime
/home/david/Node/chap06/node_modules/express/node_modules/qs
/home/david/Node/chap06/node_modules/mongodb
/home/david/Node/chap06/node_modules/mongoose
/home/david/Node/chap06/node_modules/sqlite3

Скрипты в составе пакета


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

$ npm test <packageName>

У событий запуска, останова и перезапуска нет четко определенной семантики.


Очевидное применение – запуск и останов процессов-демонов, ассоциированных
с пакетом.

Редактирование и просмотр содержимого


установленного пакета
В npm есть две команды, позволяющие просмотреть или изменить содержимое
пакета. Например, ими можно воспользоваться на этапе разработки, чтобы вы-
вести исходный код пакета (и узнать, что он делает), заглянуть в каталог постав-
ляемых вместе с пакетом примеров или внести модификации для тестирования
исправлений.
Примеры:
58 Модули Node
Как легко видеть, команда explore порождает дочернюю оболочку, в которой
текущим является каталог, куда установлен модуль. Если набрать exit или нажать
control-D, то произойдет возврат из дочерней оболочки в исходную.
При желании можно и редактировать входящие в пакет файлы. Но после этого
пакет, возможно, придется перестроить:

$ npm rebuild mongoose


mongoose@1.3.3 /home/david/Node/chap06/node_modules/mongoose

Обновление устаревших пакетов


Авторы пакетов не сидят сложа руки, а продолжают кодировать, поэтому ранее
установленные пакеты покрываются пылью, если их вовремя не обновлять. Чтобы
узнать, не устарели ли установленные пакеты, выполните следующую команду:

$ npm outdated
express@2.3.6 ./node_modules/express current=2.3.3
mongoose@1.3.6 ./node_modules/mongoose current=1.3.3

Она показывает версию установленного на вашей машине пакета и его же вер-


сию в репозитории npm. Обновить устаревшие пакеты очень просто:

$ npm update express


connect@1.4.1 ./node_modules/express/node_modules/connect
mime@1.2.2 ./node_modules/express/node_modules/mime
qs@0.1.0 ./node_modules/express/node_modules/qs
express@2.3.6 ./node_modules/express

Удаление установленного пакета


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

$ npm list
/home/david/Node
openid@0.1.6
$ npm uninstall openid
$ npm list
/home/david/Node
(empty)

Разработка и публикация npm-пакетов


Теперь, получив представление о том, как работать с npm, зайдем с другого
конца и посмотрим, как разрабатываются npm-пакеты. В npm есть несколько
команд для поддержки процесса разработки.
Менеджер пакетов для Node (npm) 59

Первым делом следует создать файл package.json, и тут на помощь приходит


команда npm init, которая создает заготовку. Она задает несколько вопросов, пос-
ле чего создает примерно такой файл:

{
"author": "I.M. Awesome <awesome@example.com>",
"name": "tmod",
"description": "Test Module",
"version": "0.0.1",
"repository": {
"url": ""
},
"engines": {
"node": ">0.4.1"
},
"dependencies": {},
"devDependencies": {}
}

Далее, очевидно, нужно написать исходный код, но тут npm вам ничем не по-
может. Вы программист, вот и программируйте. Только не забывайте обновлять
файл package.json по мере добавления в пакет новых компонентов. Впрочем,
в npm все-таки есть парочка команд, которые будут вам полезны при разработке
пакета.
Одна из них – npm link – представляет собой упрощенный метод установки
пакетов. Разница между ней и npm install заключается в том, что npm link просто
создает символическую ссылку на каталог с исходным кодом, так что вы можете
спокойно редактировать файлы, не тратя времени на обновление пакета после
каждого изменения. Это позволяет постепенно строить и тестировать пакет, не
отвлекаясь на пересборку.
Использование npm link подразумевает выполнение двух шагов. Сначала вы
делаете ссылку на свой проект в инсталляционном дереве Node:

$ cd tmod
$ npm link
../../0.4.7/lib/node_modules/tmod -> /home/david/Node/chap03/tmod

А затем – ссылку на тот же пакет в своем приложении:

$ npm link tmod


../node_modules/tmod -> /home/david/Node/0.4.7/lib/node_modules/tmod -> / home/david/
Node/chap03/tmod

Стрелки (->) показывают, какие символические ссылки создала команда.


У команды npm install есть два режима, полезных во время разработки. Во-
первых, если она выполняется в корне каталога пакета, то устанавливает текущий
каталог и зависимости в локальный каталог node_modules.
60 Модули Node
Во-вторых, tgz-архивы можно устанавливать как из локального файла, так и по
сети, указав URL. Большинство систем управления версиями поддерживают адреса-
цию по URL, возвращая в ответ tgz-архив дерева исходного кода. Например, на стра-
нице загрузки любого проекта, организованного на сайте github, имеется URL вида:

$ npm install https://github.com/havard/node-openid/tarball/v0.1.6


openid@0.1.6 ../node_modules/openid

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


зитории npm, чтобы им могли воспользоваться другие люди.
Для этого сначала нужно завести себе учетную запись в репозитории. Это де-
лает команда npm adduser, которая просит ввести имя пользователя, пароль и адрес
электронной почты:

$ npm adduser
Username: my-user-name
Password:
Email: me@example.com

Затем выполните команду npm publish из корневого каталога своего пакета:

$ npm publish

Если все пройдет нормально, то по завершении этой команды вы сможете зай-


ти на сайт http://search.npmjs.org и поискать свой пакет. Появиться он должен
довольно быстро.
Команда npm unpublish, как легко понять из названия, удаляет пакет из репо-
зитория npm.

Конфигурационные параметры npm


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

npm config set <key> <value> [--global]


npm config get <key>
npm config delete <key>
npm config list
npm config edit
npm get <key>
npm set <key> <value> [--global]

Например:

$ npm set color true


$ npm set global false
Менеджер пакетов для Node (npm) 61

$ npm config get color


true
$ npm config get global
false

Установить значения конфигурационных параметров можно с помощью пе-


ременных окружения. Любая переменная, имя которой начинается строкой NPM_
CONFIG_, служит для установки конфигурационных параметров. Например, пере-
менная NPM_CONFIG_GLOBAL устанавливает значение параметра global.
Конфигурационные параметры можно помещать в следующие файлы:
‰ $HOME/.npmrc
‰ <Инсталляционный каталог Node>/etc/npmrc
Конфигурационный файл содержит пары name=value (см. ниже) и обновляется
командой npm config set:

$ cat ~/.npmrc
global = false
color = true

Версии и диапазоны версий пакета


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

$ npm view express version


2.4.0

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

$ npm install express@2.3.1


mime@1.2.2 ./node_modules/express/node_modules/mime
connect@1.5.1 ./node_modules/express/node_modules/connect
qs@0.2.0 ./node_modules/express/node_modules/qs
express@2.3.1 ./node_modules/express
62 Модули Node
В npm есть концепция метки (tag), которой можно воспользоваться, чтобы
установить самую свежую стабильную версию пакета:

$ npm install sax@stable

Имена меток произвольны и необязательны. Их выбирает автор пакета, и не во


всех пакетах они применяются.
Npm позволяет просмотреть зависимости от других пакетов, хранящиеся
в файле package:

$ npm view mongoose dependencies


{ hooks: '0.1.9' }

$ npm view express dependencies


{ connect: '>= 1.5.1 < 2.0.0',
mime: '>= 0.0.1',
qs: '>= 0.0.6' }

Зависимости – это механизм, с помощью которого npm узнает, какие еще мо-
дули необходимо установить. Во время установки модуля npm смотрит, от чего он
зависит, и устанавливает те модули, которые не были установлены ранее.
Внимательный читатель, вероятно, обратил внимание на знаки < и > в при-
веденном выше примере. Npm поддерживает диапазоны номеров версий; например
пакет Express объявляет, что готов работать с любой версией Connect от 1.5.1 до 2.0.0.
Хотя эта система проста и понятна всякому, кто хоть раз имел дело с программ-
ным обеспечением, за ней стоит строгая модель. Реализуя систему нумерации
версий, автор npm пользовался спецификацией семантической версионности, опуб-
ликованной на сайте http://semver.org. Формулируется она следующим образом:
‰ Версии представляются строками вида X.Y.Z, где X, Y и Z обычно являются
целыми числами; X – основной номер, Y – дополнительный номер, Z – но-
мер исправления (например, 1.2.3).
‰ После номера исправления в строке может указываться произвольный
текст, описывающий так называемые «специальные версии» (например,
1.2.3beta1).
‰ При сравнении номеров версий сравниваются не строки, а числа X, Y и Z.
Например, 1.9.0 < 1.10.0 < 1.11.3. и 1.0.0beta1 < 1.0.0beta2 < 1.0.0.
‰ Совместимость документируется с помощью следующих соглашений о ну-
мерации версий:
• пакеты с основным номером версии 0 (X = 0) совершенно нестабильны,
их API может измениться в любое время;
• если изменение исправляет только ошибки и гарантирует обратную со-
вместимость, то следует увеличивать номер исправления (Z);
• дополнительный номер версии (Y) нужно увеличивать при добавлении
функциональности, сохраняющей обратную совместимость (например,
добавлена новая функция, а все прочие обратно совместимы);
Менеджер пакетов для Node (npm) 63

• основной номер версии (X) следует увеличивать при внесении несовмес-


тимых изменений.

Спецификация CommonJS
Система модулей Node основана на спецификации CommonJS (http://www.
commonjs.org/). Хотя JavaScript – мощный язык с целым рядом современных
продвинутых возможностей (например, объекты и замыкания), ему недостает
стандартной библиотеки объектов, упрощающей создание приложений. Common-
JS ставит целью восполнить этот пробел за счет соглашения о реализации модулей
в JavaScript и создания набора стандартных модулей.
Функция require принимает в качестве аргумента идентификатор модуля и
возвращает экспортируемый этим модулем API. Если модуль затребует другие
модули, то они также загружаются. Каждый модуль находится в одном JavaScript-
файле, и CommonJS ничего не говорит о том, как идентификатор модуля отобра-
жается на имя файла.
Модули предоставляют простой механизм инкапсуляции для сокрытия дета-
лей реализации и раскрывают только API. Содержимым модуля является код на
JavaScript, трактуемый так, будто он написан следующим образом:

(function() { … содержимое файла модуля … })();

Тем самым все объекты верхнего уровня инкапсулируются (скрываются)


в закрытом пространстве имен, к которому никакой другой код не имеет досту-
па. Именно так решается проблема Глобального Объекта (подробнее об этом чуть
ниже).
Экспортируемый API модуля – это объект, возвращаемый функцией require.
Внутри модуля он реализован в виде объекта верхнего уровня с именем exports,
поля которого содержат экспортируемый API. Чтобы экспортировать из модуля
функцию или объект, достаточно присвоить его какому-то полю объекта exports.

Демонстрация инкапсуляции в модуле


Хватит слов, приведем простой пример. Создайте файл module1.js, поместив
в него такой код:

var A = "value A";


var B = "value B";
exports.values = function() {
return { A: A, B: B };
}

Затем создайте такой файл module2.js:

var util = require('util');


var A = "a different value A";
var B = "a different value B";
64 Модули Node
var m1 = require('./module1');
util.log('A='+A+' B='+B+' values='+util.inspect(m1.values()));

Теперь запустите его следующим образом (Node должна быть уже установ-
лена):

$ node module2.js
19 May 21:36:30 - A=a different value A B=a different value B values={ A: 'value A',
B: 'value B' }

Этот искусственный пример демонстрирует независимость значений в


module1.js от значений в module2.js. Значения A и B в module1.js не затираются
значениями тех же переменных в module2.js, потому что они инкапсулированы
в module1.js. Значения, инкапсулированные внутри модуля, можно экспортиро-
вать, как мы поступили с функцией .values в module1.js.
Упомянутая выше проблема Глобального Объекта связана с тем, что перемен-
ные, объявленные вне какой-либо функции, помещаются в глобальный контекст.
В веб-браузерах существует всего один глобальный контекст, что создает проб-
лемы, когда глобальные переменные из одного JavaScript-скрипта конфликтуют
с глобальными переменными из другого скрипта. Но у модуля, следующего спе-
цификации CommonJS, имеется собственный закрытый глобальный контекст, по-
этому одноименные глобальные переменные в разных модулях не конфликтуют
между собой.

Резюме
В этой главе мы многое узнали о модулях и пакетах в Node. Точнее, были рас-
смотрены следующие вопросы:
‰ реализация модулей и пакетов для Node;
‰ управление установленными модулями и пакетами;
‰ алгоритм поиска модулей в Node.
Теперь, зная о модулях и пакетах, мы готовы приступить к их использованию
для создания приложений. Этим мы и займемся в следующей главе.
Глава 4. ВАРИАЦИИ НА ТЕМУ
ПРОСТОГО ПРИЛОЖЕНИЯ
Теперь самое время применить знания о модулях Node к разработке простого
веб-приложения для этой платформы. В этой главе мы не будем писать ничего
сложного, а воспользуемся возможностью изучить три разных каркаса разработки
приложений для Node. В последующих главах мы усложним задачу, но прежде чем
ходить, нужно научиться ползать.
Итак, приступим.

Разработка учебной программы


по математике
В этой главе мы займемся разработкой простого небольшого приложения
Math Wizard, которое при наличии приличного пользовательского интерфейса
можно было бы использовать для обучения детей математике. Но поскольку по-
близости не было специалиста по пользовательским интерфейсам, то Math Wizard
будет полезно только для обучения веб-разработке на платформе Node. Не надей-
тесь вырастить на этом приложении математического гения. Вас предупредили.
Приложение Math Wizard состоит из домашней страницы, боковой навигаци-
онной панели и нескольких страниц для выполнения различных математических
операций.

Использовать ли каркас?
Каркасы для разработки веб-приложений позволяют тратить время на со-
держательную задачу, не утруждая себе деталями реализации протокола HTTP.
Абстрагирование деталей – проверенный временем способ повышения продук-
тивности программиста. И в особенности при использовании библиотеки или
каркаса, которые предоставляют готовые функции, берущие на себя заботу о де-
талях.
В этой главе мы сначала напишем приложение (Math Wizard) без каркасов,
а потом будем постепенно улучшать, применяя каркасы Connect и Express.
66 Вариации на тему простого приложения

Реализация Math Wizard в Node


(без каркасов)
Мы начнем с передвижения ползком, чтобы впоследствии лучше оценить, на
что способны каркасы. «Ползком» означает, что мы будем использовать базовый
пакет Node, объект HTTP Server.
Math Wizard, как и любое веб-приложение, состоит из нескольких страниц,
каждая со своим URL-адресом. Все страницы имеют ряд общих элементов (еди-
ная структура страницы и навигационная панель), а также уникальное для каждой
страницы содержимое. Для Math Wizard определены следующие URL-адреса:
‰ /: домашняя страница;
‰ /square: возведение в квадрат;
‰ /mult: умножение двух чисел;
‰ /factorial: вычисление факториала числа;
‰ /fibonacci: вычисление чисел Фибоначчи.
Прежде всего создадим каталог для исходного кода:

$ mkdir chap04

Маршрутизация запросов в Node


Каждая страница Math Wizard реализована в виде отдельного модуля, а сервер
маршрутизирует к этим модулям запросы.
Под словами «маршрутизация запросов» мы понимаем стратегию разбие-
ния приложения на модули. Вместо того чтобы реализовывать все приложение
в виде одной гигантской функции обратного вызова, лучше организовать модуль-
ную структуру. Механизм маршрутизации запросов исследует входящий HTTP-
запрос и вызывает нужный модуль, который его обработает.
Создайте файл app-node.js, поместив в него такой код:

var http_port = 8124;

var http = require('http');


var htutil = require('./htutil');

var server = http.createServer(function (req, res) {


htutil.loadParams(req, res, undefined);
if (req.requrl.pathname === '/') {
require('./home-node').get(req, res);
} else if (req.requrl.pathname === '/square') {
require('./square-node').get(req, res);
} else if (req.requrl.pathname === '/factorial') {
require('./factorial-node').get(req, res);
} else if (req.requrl.pathname === '/fibonacci') {
require('./fibo-node').get(req, res);
Реализация Math Wizard в Node (без каркасов) 67

// require('./fibo2-node').get(req, res);
} else if (req.requrl.pathname === '/mult') {
require('./mult-node').get(req, res);
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end("bad URL "+ req.url);
}
});

server.listen(http_port);
console.log('listening to http://localhost:8124');

Этот маршрутизатор запросов абсолютно прямолинеен. Для каждого запроса


вызывается функция обратного вызова, которой в аргументе req передаются данные
запроса, а объект res используется для отправки ответа клиенту. Маршрутизатор
анализирует URL запроса и передает запрос обработчику.
Существуют несколько модулей (мы рассмотрим их ниже), каждый из кото-
рых экспортирует функцию .get с сигнатурой function(req, res). Каждый модуль
отвечает за реализацию одной страницы Math Wizard.
Если URL запроса не соответствует никакому модулю, то мы посылаем ответ
с кодом состояния 404, означающим, что страница не найдена.

Обработка параметров запроса


Функция htutil.loadParams разбирает URL и сохраняет результат в объекте,
на который могут ссылаться другие компоненты Math Wizard. На каждой странице
приложения имеется форма (тег FORM) с полями a и b. Когда пользователь вводит
в эти поля числа и нажимает кнопку Submit, формируется URL, в котором строка
запроса имеет вид:

http://localhost:8124/mult?a=3&b=7

Параметры будут присутствовать только в том случае, когда в поля что-то


введено. Это означает, что каждая страница Math Wizard должна обрабатывать
случаи, когда параметр задан или не задан. Функция htutil.loadParams ищет эти
параметры, избавляя нас от необходимости писать один и тот же код в каждом
модуле.
Создайте файл htutil.js с таким содержимым:

var url = require('url');


exports.loadParams = function(req, res, next) {
req.requrl = url.parse(req.url, true);
req.a = (req.requrl.query.a && !isNaN(req.requrl.query.a))
? new Number(req.requrl.query.a)
: NaN;
req.b = (req.requrl.query.b && !isNaN(req.requrl.query.b))
68 Вариации на тему простого приложения
? new Number(req.requrl.query.b)
: NaN
if (next) next();
}

Предполагается, что эта функция будет вызываться из обработчиков HTTP-


запросов, которые передадут ей объекты req и res. Функция ищет в строке запроса
параметры a и b и, как уже было сказано, присоединяет их к объекту req. Проверка
с использованием оператора ?: позволяет упростить остальной код, гарантируя,
что req.a и req.b являются либо значением NaN, либо объектом типа Number
(в зависимости от того, был в запросе соответствующий параметр или нет). В коде
встречается еще функция next, на которую пока не обращайте внимания; мы
обсудим ее ниже, при рассмотрении каркаса Connect.
Оставшиеся две функции в htutil.js занимаются версткой страницы. В Math
Wizard у всех страниц общий макет, поэтому лучше всего собрать все, к нему
относящееся, в одном месте, это уменьшит дублирование кода и упростит внесение
изменений. Ниже, когда мы перейдем к каркасу Express, то будем использовать для
верстки страниц шаблоны, но сейчас ограничимся только базовыми средствами
Node, к каковым шаблоны не относятся.
Добавьте в файл htutil.js еще две функции, которые помогут при конструи-
ровании страниц.

exports.navbar = function() {
return ["<div class='navbar'>",
"<p><a href='/'>home</a></p>",
"<p><a href='/mult'>Multiplication</a></p>",
"<p><a href='/square'>Square's</a></p>",
"<p><a href='/factorial'>Factorial's</a></p>",
"<p><a href='/fibonacci'>Fibonacci's</a></p>",
"</div>"].join('\n');
}

Эта функция возвращает фрагмент HTML со ссылками на страницы, который


будет служить навигационной панелью.

exports.page = function(title, navbar, content) {


return ["<html><head><title>{title)</title></head>",
"<body><h1>{title}</h1>",
"<table><tr>",
"<td>{navbar}</td><td>{content}</td>",
"</tr></table></body></html>"
].join('\n')
.replace("{title}", title, "g")
.replace("{navbar}", navbar, "g")
.replace("{content}", content, "g");
}
Реализация Math Wizard в Node (без каркасов) 69

Эта функция определяет структуру страницы в целом. В качестве аргументов


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

Умножение чисел
Теперь посмотрим, как создать «математические» страницы для приложения
Math Wizard. Сначала займемся умножением чисел.
Создайте файл mult-node.js, содержащий такой код:

var htutil = require('./htutil');


exports.get = function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/html'
});
var result = req.a * req.b;
res.end(
htutil.page("Multiplication", htutil.navbar(), [
(!isNaN(req.a) && !isNaN(req.b) ?
("<p class='result'>{a} * {b} = {result}</p>"
.replace("{a}", req.a)
.replace("{b}", req.b)
.replace("{result}", req.a * req.b))
: ""),
"<p>Enter numbers to multiply</p>",
"<form name='mult' action='/mult' method='get'>",
"A: <input type='text' name='a' /><br/>",
"B: <input type='text' name='b' />",
"<input type='submit' value='Submit' />",
"</form>"
].join('\n'))
);
}

Модуль умножения, как и все прочие модули Math Wizard, решает две задачи.
Во-первых, он выводит результат операции, а во-вторых, отображает форму для
ввода одного или двух значений.
Прежде всего обратите внимание на использование функции htutil.page. Она
формирует макет страницы, и в нее мы передаем только содержимое основной об-
ласти. Содержимое представляет собой массив строк, которые конкатенируются
функцией .join().
Основная часть следующего кода посвящена выводу результата в случае, когда
пользователь задал оба параметра:
70 Вариации на тему простого приложения

(!isNaN(req.a) && !isNaN(req.b) ?


("<p class='result'>{a} * {b} = {result}</p>"
.replace("{a}", req.a)
.replace("{b}", req.b)
.replace("{result}", req.a * req.b))
: ""),

Сначала с помощью оператора ?: мы проверяем, что параметры заданы, и если


это так, то перемножаем req.a и req.b и выводим произведение.

Вычисление других математических


функций
Остальные модули Math Wizard похожи на mult-node.js, то есть построены по
тому же образцу, поэтому рассмотрим их бегло.
Для возведения в квадрат число умножается само на себя (a * a). Создайте
файл square-node.js, содержащий приведенный ниже код.

var htutil = require('./htutil');


exports.get = function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(
htutil.page("Square", htutil.navbar(), [
(!isNaN(req.a) ?
("<p class='result'>{a} squared = {sq}</p>"
.replace("{a}", req.a)
.replace("{sq}", req.a*req.a))
: ""),
"<p>Enter a number to see its square</p>",
"<form name='square' action='/square' method='get'>",
"A: <input type='text' name='a' />",
"</form>"
].join('\n'))
);
}

Факториалом целого числа n, обозначаемым в математике n!, называется про-


изведение целых положительных чисел от 1 до n. Он встречается во многих обла-
стях математики. Создайте файл factorial-node.js, как показано ниже. Отметим,
что функция Math.floor округляет req.a до ближайшего целого.

var htutil = require('./htutil');


var math = require('./math');

exports.get = function(req, res) {


Реализация Math Wizard в Node (без каркасов) 71

res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(
htutil.page("Factorial", htutil.navbar(), [
(!isNaN(req.a) ?
("<p class='result'>{a} factorial = {fact}</p>"
.replace("{a}", req.a)
.replace("{fact}",
math.factorial(Math.floor(req.a))))
: ""),
"<p>Enter a number to see it's factorial</p>",
"<form name='factorial' action='/factorial' method='get'>",
"A: <input type='text' name='a' />",
"</form>"
].join('\n'))
);
}

Последовательностью чисел Фибоначчи называется последовательность 1,


1, 2, 3, 5, 8, 13, 21, 34, 55…, в которой каждый член, начиная с третьего, является
суммой двух предыдущих. Отношение двух соседних членов последовательности
приблизительно равно золотому сечению. Создайте файл fibo-node.js, содержа-
щий код для вычисления чисел Фибоначчи:

var htutil = require('./htutil');


var math = require('./math');

exports.get = function(req, res) {


res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(
htutil.page("Fibonacci", htutil.navbar(), [
(!isNaN(req.a) ?
("<p class='result'>fibonacci {a} = {fibo}</p>"
.replace("{a}", Math.floor(req.a))
.replace("{fibo}", math.fibonacci(Math.floor(req.a))))
: ""),
"<p>Enter a number to see its fibonacci</p>",
"<form name='fibonacci' action='/fibonacci' method='get'>",
"A: <input type='text' name='a' />",
"</form>"
].join('\n'))
);
}
72 Вариации на тему простого приложения
Внимательные читатели, вероятно, обратили внимание на модуль math. Не-
трудно догадаться, что он содержит реализации нескольких математических
функций. Создайте файл math.js и поместите в него такой код:

var factorial = exports.factorial = function(n) {


if (n == 0)
return 1;
else
return n * factorial(n-1);
}

var fibonacci = exports.fibonacci = function(n) {


if (n === 1)
return 1;
else if (n === 2)
return 1;
else
return fibonacci(n-1) + fibonacci(n-2);
}

Это сравнительно простые реализации стандартных математических функ-


ций. Как мы вскоре увидим, такой подход к вычислению чисел Фибоначчи весьма
наивен и потребляет особенно много ресурсов процессора.
Нам нужна еще домашняя страница Math Wizard. Создайте такой файл home-
node.js:

var htutil = require('./htutil');


exports.get = function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(
htutil.page("Math Wizard",
htutil.navbar(),
"<p>Math Wizard</p>")
);
}

Введите следующую команду:

$ node app-node.js

Так как app-node.js прослушивает порт 8124, наберите в браузере адрес http://
localhost:8124/. Появится следующая страница:
Реализация Math Wizard в Node (без каркасов) 73

Обобщение Math Wizard


Нашим детям необходимо хорошее образование, и, быть может, этот пример
станет основой для лучшей обучающей программы из когда-либо написанных.
А может, и не станет. Как бы то ни было, приложение Math Wizard легко можно
обобщить, добавив другие страницы, – ведь мир математики безграничен. Добав-
лять новые страницы совсем несложно, нужно лишь следовать уже имеющемуся
образцу:
‰ Добавить тег «a» в htutil.navbar:
Поскольку функция htutil.navbar содержит HTML-разметку навигацион-
ной панели, то в ней нужно перечислить все реализованные страницы Math
Wizard, включив URL и название страницы:
"<p><a href='/newUrl'>Math Function Name</a></p>\n"+
‰ Добавить предложение if в файл app-node.js:
Так как app-node.js содержит маршрутизатор запросов, то для каждого
нового URL необходимо определить маршрут. Указанный в нем URL
должен соответствовать URL в функции htutil.navbar:
if (req.requrl.pathname === '/newUrl') {
require('./moduleName').get(req, res);
}
‰ Добавить модуль, который содержит обработчик страницы и экспортирует
метод get:
Несколько таких модулей мы уже видели (mult-node.js и прочие), по их
образцу нетрудно написать новые.
74 Вариации на тему простого приложения

Продолжительные вычисления
(числа Фибоначчи)
Приложение Math Wizard наглядно демонстрирует основную проблему при-
ложений для Node. Если какая-нибудь функция обратного вызова долго не воз-
вращает управление циклу обработки событий, то все приложение перестает от-
вечать на запросы.
Чтобы убедиться в этом, зайдите на страницу вычисления чисел Фибоначчи и
введите «большое» число, например 50. На его вычисление уйдет ОЧЕНЬ много
времени (часы или даже дни), при этом процесс Node будет потреблять почти все
процессорное время, и ни в каком другом окне браузера получить ответ от прило-
жения Math Wizard будет невозможно. А все потому, что наш алгоритм нахожде-
ния последовательности чисел Фибоначчи выполняет очень большой объем вы-
числений. А почему не отвечает браузер? Потому что пока программа занимается
вычислениями, она не дает работать циклу обработки событий, и, значит, Node не
может ответить на запросы.
Поскольку в Node имеется всего один поток выполнения, для обработки за-
просов необходимо, чтобы обработчики быстро возвращали управления в цикл
обработки событий. Обычная практика написания асинхронных программ пред-
полагает, что возврат в этот цикл производится быстро. Это справедливо даже
в случае, когда для обработки запроса нужно загружать данные с сервера на дру-
гом конце земного шара, поскольку вследствие неблокирующего ввода/вывода
управление быстро возвращается в цикл. Но наша наивная функция вычисления
чисел Фибоначчи не удовлетворяет этому условию, так как выполняет длительную
блокирующую операцию. В таком случае система не может обрабатывать новые
запросы, и Node перестает быть тем, чем задумана, – суперскоростным веб-сервером.
В данном примере проблема очевидна. Время обработки возрастает так быст-
ро, что уже для вычисления числа Фибоначчи, отстоящего сравнительно недалеко
от начала последовательности, его уходит столько, что вы вполне успеете съездить
в отпуск на Тибет. Но в вашем приложении замедление реакции может быть не
так заметно, и как же тогда узнать, какие запросы обрабатываются слишком дол-
го? Один из способов – воспользоваться каким-нибудь встраиваемым в браузер
средством измерения задержки, например YSlow. Эвристическое правило простое:
если браузером пользуется человек, то на загрузку страницы должно уходить не
больше двух секунд, иначе вы рискуете потерять посетителя.
В Node существуют два общих способа решения этой проблемы.
‰ Переработка алгоритма. Например, выбранный нами алгоритм вычисле-
ния чисел Фибоначчи не оптимален, его можно заменить более быстрым.
А если это не получается, то нужно разбить алгоритм на кусочки и пору-
чить управление ими циклу обработки событий. Один пример такого под-
хода мы рассмотрим ниже.
‰ Создание фоновой службы. Можете вы себе представить специальный
вспомогательный сервер, который занимается только вычислением чисел
Фибоначчи? Пожалуй, нет, но, вообще говоря, выделение серверов заднего
Реализация Math Wizard в Node (без каркасов) 75

плана для разгрузки серверов переднего плана – обычное дело, и в конце


этой главы мы реализуем такой математический сервер. Обработчик за-
просов должен будет асинхронно обращаться к службам или базам данных,
собирать информацию, необходимую для ответа, и, когда все будет готово,
отправить ответ браузеру.
Мы могли бы оптимизировать алгоритм вычисления чисел Фибоначчи, но
вместо этого преобразуем нашу неасинхронную функцию в асинхронную, с об-
ратным вызовом. Асинхронное вычисление чисел Фибоначчи – не самая лучшая
мысль, но зато на этом примере мы сможем продемонстрировать переработку ал-
горитма. Мы разобьем вычисление на последовательность обратных вызовов, ко-
торые будут вызываться из цикла обработки событий.
Первым делом добавим новую функцию вычисления чисел Фибоначчи вместо
первоначального наивного варианта. С вами такое тоже может случиться – первая
попытка оказывается неудачной и медленной, но впоследствии вы находите более
подходящую реализацию. Добавьте в файл math.js такой код:

var fibonacciAsync = exports.fibonacciAsync = function(n, done) {


if (n === 1 || n === 2)
done(1);
else {
process.nextTick(function() {
fibonacciAsync(n-1, function(val1) {
process.nextTick(function() {
fibonacciAsync(n-2, function(val2) {
done(val1+val2);
});
});
});
});
}
}

Это новый асинхронный алгоритм. Мы преобразовали простую функцию


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

fibonacciAsync(n, function(value) {
// произвести какие-то действия с value
});

Мы вызываем функцию process.nextTick, чтобы превратить рекурсивную


функцию в последовательность шагов, вызываемых диспетчером цикла обработки
событий. Очередной шаг реализован в виде обратного вызова из цикла, а возврат
в цикл после каждого шага производится быстро, так что сервер может продол-
жать обработку HTTP-запросов. Это не единственный способ разбить алгоритм
на шаги, вызываемые посредством цикла обработки событий. Для решения дан-
76 Вариации на тему простого приложения
ной задачи предназначен модуль async, в котором есть много функций, призван-
ных укротить асинхронный JavaScript.
В коде fibonacciAsync функция process.nextTick заменяет следующее пред-
ложение в первоначальном варианте алгоритма:

return fibonacci(n-1)+fibonacci(n-2);

Задача состоит в том, чтобы вычислить два числа Фибоначчи, сложить их и


вернуть результат вызывающей функции. В новом алгоритме каждому из трех
шагов соответствует анонимная функция. А чтобы их вызовы производились из
цикла обработки событий, мы использовали process.nextTick.
Прежде чем двигаться дальше, давайте осмыслим это решение. Мы никак не
уменьшили объем вычислений, а лишь распределили его по циклу обработки со-
бытий. Процесс Node по-прежнему нагружает процессор, поэтому выбранный
нами подход – не лучший способ оптимизировать такой алгоритм, как вычисле-
ние чисел Фибоначчи. Но он демонстрирует распределение работы с помощью
цикла обработки событий; иногда эта техника полезна, иногда – не очень.
Вам решать, какой способ организации длительных вычислений применить
к конкретному алгоритму. Например, ниже в этой главе мы покажем, как реали-
зовать вспомогательный HTTP-сервер и передавать ему вычислительные задачи.
Создайте новый файл fibo2-node.js и измените app-node.js, включив вызов
require('./fibo2- node'), чтобы использовался новый модуль вычисления чисел
Фибоначчи. Эта строка уже есть в файле app-node.js, только закомментирована.
Вам остается лишь перенести комментарий с одной строки на другую:

var htutil = require('./htutil');


var math = require('./math');

function sendResult(req, res, a, fiboval) {


res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(
htutil.page("Fibonacci", htutil.navbar(), [
(!isNaN(fiboval) ?
("<p class='result'>fibonacci {a} = {fibo}</p>"
.replace("{a}", a)
.replace("{fibo}", fiboval))
: ""),
"<p>Enter a number to see its fibonacci</p>",
"<form name='fibonacci' action='/fibonacci' method='get'>",
"A: <input type='text' name='a' />",
"</form>"
].join('\n'))
);
}
Реализация Math Wizard в Node (без каркасов) 77

exports.get = function(req, res) {


if (!isNaN(req.a)) {
math.fibonacciAsync(Math.floor(req.a), function(val) {
sendResult(req, res, Math.floor(req.a), val);
});
} else {
sendResult(req, res, NaN, NaN);
}
}

Мы вынесли всю работу в функцию sendResult, которая вызывается по-раз-


ному в зависимости от того, задал пользователь номер числа Фибоначчи или нет.

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

Чего не хватает до «настоящего


веб-сервера»?
Как мы увидим при обсуждении каркаса Connect, та весьма упрощенная функ-
ция диспетчеризации, которая реализована в приложении Math Wizard, не делает
нескольких вещей, ожидаемых от реального веб-сервера. Одной лишь реализации
протокола HTTP недостаточно, чтобы веб-сервер или веб-приложение могли счи-
таться полными, потому что недостает нескольких важных черт, признанных не-
обходимыми на протяжении 20 лет разработки веб-приложений.
‰ Math Wizard не анализирует метод запроса (GET, PUT, POST и т. д.). Семантика
HTTP подразумевает, что запросы типа GET, PUT и POST должны обрабаты-
ваться по-разному.
78 Вариации на тему простого приложения
‰ Нет страницы для отсутствующих URL-адресов (ошибка 404).
‰ Обработчики запросов не защищены от атак путем внедрения скриптов.
‰ Не поддерживаются куки, а значит, и сеансы.
‰ Запросы не протоколируются.
‰ Не поддерживается аутентификация.
‰ Не обрабатываются статические файлы: изображения, CSS, JavaScript,
HTML.
‰ Не налагается ограничений ни на размер страницы, ни на время выполнения.
Ниже, при рассмотрении Connect и Express, мы увидим, что веб-каркасы для
Node реализуют большую часть перечисленных функций.

Использование каркаса Connect


для реализации Math Wizard
В описании системы Connect (http://senchalabs.github.com/connect/) гово-
рится, что это не каркас для разработки веб-приложений, а каркас промежуточного
уровня для платформы Node. В его состав входят «11 готовых средств», и су-
ществует «богатый выбор сторонних средств промежуточного уровня». Если
вас смущает термин «промежуточный уровень» (middleware), то это и понятно –
слово-то довольно многозначное. Поэтому для начала поговорим о том, что оно
означает в данном контексте.
Т. Дж. Холовайчук (TJ Holowaychuck) говорит, что «ПО промежуточного
уровня» предоставляет разработчикам для Node простые подключаемые модули
(ПУ-модули), которые можно «соединять» в любом порядке. Эти модули спо-
собствуют быстрой разработке приложения, так как содержат готовую реализацию
такой общей функциональности, как маршрутизация запросов, аутентификация,
протоколирование запросов, обработка куков и т. д. (http://tjholowaychuk.com/
post/664516126/connect-middleware-for-nodejs).
ПУ-модули бывают двух видов:
‰ Фильтры: располагаются в середине конвейера обработки входящего и ис-
ходящего трафиков, но сами не отвечают на запросы. Примером фильтра
может служить ПУ-модуль протоколирования «logger», который выводит
в журнал информацию в определенном пользователем формате.
‰ Поставщики: это «оконечные точки» конвейера, то есть обработка входя-
щего запроса завершается по достижении поставщика, именно он посылает
ответ пользователю. Примером может служить ПУ-модуль «static», обслу-
живающий статические файлы.
В предыдущем разделе мы видели приложение, построенное с помощью http.
createServer, и функцию, которая вызывалась для каждого входящего HTTP-
запроса. Работая с Connect, вы вместо этого используете функцию connect.
createServer и присоединяете к созданному серверу различные ПУ-модули. Один
из таких модулей, маршрутизатор, применяется для реализации принадлежащих
приложению URL-адресов.
Помня об этом, приступим к рассмотрению кода.
Реализация Math Wizard в Node (без каркасов) 79

Установка и настройка Connect


Для начала установите Connect:

$ npm install connect

Затем создайте файл app-connect.js с таким содержимым:

var connect = require('connect');


var htutil = require('./htutil');

connect.createServer()
.use(connect.favicon())
.use(connect.logger())
.use('/filez', connect.static(__dirname + '/filez'))
.use(connect.router(function(app){
app.get('/',
require('./home-node').get);
app.get('/square', htutil.loadParams,
require('./square-node').get);
app.get('/factorial', htutil.loadParams,
require('./factorial-node').get);
app.get('/fibonacci', htutil.loadParams,
require('./fibo2-node').get);
app.get('/mult', htutil.loadParams,
require('./mult-node').get);
})).listen(8124);
console.log('listening to http://localhost:8124');

Теперь запустите сервер:

$ node app-connect.js

Поскольку сервер прослушивает порт 8124 (.listen(8124)), наберите в адрес-


ной строке браузера URL http://localhost:8124/.
Примите поздравления! Вы только что запустили свое первое Node-прило-
жение на основе Connect.
Обратите внимание, что оно ведет себя и выглядит точно так же, как преды-
дущая инкарнация Math Wizard. И объясняется это тем, что в app-connect.js по-
вторно используются модули из app-node.js. Функции app.get просто передают
запросы одному из существующих модулей.
Но если поведение не изменилось, то стоила ли игра свеч?
Разница в том, что Connect предлагает каркас для обработки и диспетчеризации
запросов, упрощающий разработку приложения. Он берет на себя многие выше-
упомянутые функции «полного веб-сервера», позволяя вам сосредоточиться на
логике приложения. Но делает это ли Connect каркасом для разработки прило-
жений?
80 Вариации на тему простого приложения
Система Connect подается авторами не как каркас для разработки приложений,
а как фундамент, на котором можно строить такие каркасы. Одним из каркасов,
построенных на базе Connect, является Express. Но система Connect полезна
и сама по себе, и, поняв ее принципы, вам будет проще разобраться в Express,
поэтому сначала немного поговорим о Connect, а потом уже перейдем к Express.

Знакомство с Connect
Первоначальное представление о Connect мы уже получили, теперь пригля-
димся повнимательнее. Connect лежит в основе каркаса Express и снимает
практически все ограничения, с которыми мы сталкивались при построении
приложения с помощью объекта HTTP Server. Но не будем забегать вперед,
а посмотрим на файл app-connect.js.
В Connect есть несколько способов настройки серверного объекта. В app-
connect.js мы поступали следующим образом:

var connect = require('connect');


connect.createServer()
.use(connect.favicon())
.use(connect.logger())
.use('/filez', connect.static(__dirname + '/filez'))
.use(connect.router(function(app){
// настроить маршрутизатор
})).listen( ... номер порта ...);

Метод .use позволяет присоединить ПУ-модуль к серверу на базе Connect. Вы-


ше мы настроили цепочку ПУ-модулей, вызываемых при каждом запросе. Какие
ПУ-модули использовать, разумеется, зависит от конкретного приложения.
Вызовы метода .use можно сцеплять, получая изящные программные конст-
рукции (server.use().use().use().use()).
В данном случае мы подключили ПУ-модули favicon, logger, static и router.
ПУ-модуль logger используется для создания журнала запросов по аналогии
с веб-сервером Apache. По умолчанию он выводит данные на терминал, но можно
настроить его для вывода в любом формате в любой файл.
ПУ-модуль static реализует «статический веб-сервер», который отправляет
файлы, находящиеся в указанном каталоге. Это означает, что если имеются
каталоги для хранения файлов с расширениями .html, .css или .js, то отправку
их браузеру можно поручить ПУ-модулю connect.static.
Значками сайта (favicon) называются небольшие картинки, которые некото-
рые браузеры показывают в адресной строке и на вкладках, это еще один спо-
соб поведать миру о своем бренде. Их обслуживанием занимается ПУ-модуль
favicon.
Задача ПУ-модуля router – передать запрос к определенному URL-адресу
правильному обработчику. Его настройка начинается так:
Реализация Math Wizard в Node (без каркасов) 81

.use(connect.router(function(app){
// настроить маршрутизатор
})

Но истинная мощь кода настройки маршрутизатора проявляется, когда вы


объявляете URL-адреса, распознаваемые приложением, и обработчики каждого
адреса. Настройка производится по следующему образцу:

app.requestName('path', function(req, res, next) {..});

Здесь requestName – один из глаголов HTTP, то есть get, put, post и т. д. Таким
образом, функцию app.get можно использовать для отправки браузеру страницы,
на которой находится форма с атрибутом method=POST, а функцию app.post – для
обработки запросов, отправленных с помощью этой формы. Подробный пример
мы рассмотрим в главе 6 «Обработка и выборка данных», но уже сейчас отметим,
что такая конфигурация может выглядеть следующим образом (при условии, что
определены подходящие функции):

app.get('/form', createPageWithForm);
app.post('/form', receiveValuesPostedWithForm);

У функции обратного вызова на один аргумент больше, чем у обычного об-


работчика запросов. В ее сигнатуре function(req, res, next) аргументы req и res,
как и раньше, представляют HTTP-запрос и HTTP-ответ. Аргумент же next – это
функция, которую предоставляет Connect; ее задача состоит в том, чтобы гаранти-
ровать выполнение всех функций промежуточного уровня.
В одном маршруте может быть указано несколько функций, как то сделано
в app-connect.js. Если правильно пользоваться аргументом next, то Connect будет
вызывать все функции по очереди. В app-connect.js мы задали функцию htutil.
loadParams, как и в app-node.js. Вспомните, что в этой функции вызывалась функ-
ция next, которую, как мы теперь знаем, предоставляет Connect.
Вот типичный пример настройки маршрутизатора:

app.get('/square', htutil.loadParams,
require('./square-node').get);

В качестве аргументов передается строка, содержащая URL и две функции,


первой – htutil.loadParams. При настройке маршрутизатора можно задать сколько
угодно таких функций, их состав зависит от конкретного приложения.
В совокупности ПУ-модули и функции маршрутизатора составляют некий
вариант конечного автомата для обработки HTTP-запросов. Итак, существуют
две последовательности функций. Первая – ПУ-модули, перечисленные в кон-
фигурации сервера, вторая – только что рассмотренные функции маршрутиза-
тора.
82 Вариации на тему простого приложения

Реализация Math Wizard


с помощью Express
Освоив Connect, отправим Math Wizard на следующую ступень эволюции –
с применением Express. Express – это веб-каркас, построенный на базе каркаса
промежуточного уровня Connect. Это означает, что в фокусе внимания Express
находится создание веб-приложений, в том числе предоставление системы шабло-
нов, тогда как основная задача Connect – обеспечить инфраструктуру веб-сервера.
Поскольку Express и Connect разрабатывали одни и те же люди, неудивительно,
что их API очень похожи.
Вот, например, как выглядит программа «Здравствуй, мир», написанная для
Express:

var app = require('express').createServer();


app.get('/', function(req, res) {
res.send('Здравствуй, мир!');
});
app.listen(3000);

Очень похоже на код для Connect, рассмотренный в предыдущем разделе,


только к объекту, который возвращает функция createServer, уже подключены
функции ПУ-модулей. Ощущение такое, будто мы пропустили этап настройки
промежуточного уровня и сразу перешли в маршрутизатору. Но, конечно, никто
не мешает присоединить и дополнительные ПУ-модули:

var express = require('express');


var app = express.createServer(
express.logger(),
express.bodyParser()
);

Для установки Express и EJS (системы шаблонов) достаточно выполнить сле-


дующую команду:

$ npm install express ejs


qs@0.1.0 ../node_modules/express/node_modules/qs
express@2.3.11 ../node_modules/express
ejs@0.4.2 ../node_modules/ejs

Реализация Express Math Wizard


Установив требуемые модули, приступим к написанию кода. Сначала создайте
каталог:

$ mkdir views
Реализация Math Wizard с помощью Express 83

А в нем файл app-express.js, содержащий такой код:

var htutil = require('./htutil');


var math = require('./math');
var express = require('express');
var app = express.createServer(
express.logger()
);

app.register('.html', require('ejs'));
// Необязательно, т.к. Express по умолчанию ищет шаблоны в каталоге /views
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');

app.configure(function(){
app.use(app.router);
app.use(express.static(__dirname + '/filez'));
app.use(express.errorHandler({
dumpExceptions: true, showStack: true }));
});

Здесь мы настроили сервер и сконфигурировали необходимые ПУ-модули.


В деталях есть некоторых различия – например, мы подключаем express.logger
вместо connect.logger, но в целом все очень похоже.
Из нового обратите внимание на вызовы функций app.register и app.set.
В показанной конфигурации система шаблонов настроена так, что html-файлы об-
рабатываются движком EJS. Ниже мы увидим, что функция res.render применя-
ется для подстановки данных в шаблоны с использованием одного из нескольких
существующих движков шаблонов.
Теперь настроим маршрутизатор (по-прежнему в файле app-express.js):

app.get('/', function(req, res) {


res.render('home.html', { title: "Math Wizard" });
});
app.get('/mult', htutil.loadParams, function(req, res) {
if (req.a && req.b) req.result = req.a * req.b;
res.render('mult.html', {
title: "Math Wizard" , req: req });
});
app.get('/square', htutil.loadParams, function(req, res) {
if (req.a) req.result = req.a * req.a;
res.render('square.html', {
title: "Math Wizard" , req: req });
});
app.get('/fibonacci', htutil.loadParams, function(req, res) {
if (req.a) {
math.fibonacciAsync(Math.floor(req.a), function(val) {
req.result = val;
84 Вариации на тему простого приложения
res.render('fibo.html', {
title: "Math Wizard" , req: req });
});
} else {
res.render('fibo.html', {
title: "Math Wizard" , req: req });
}
});
app.get('/factorial', htutil.loadParams, function(req, res) {
if (req.a) req.result = math.factorial(req.a);
res.render('factorial.html', {
title: "Math Wizard" , req: req });
});

app.get('/404', function(req, res) {


res.send('NOT FOUND '+req.url);
});

app.listen(8124);
console.log('listening to http://localhost:8124');

Настройка маршрутизатора в Express осуществляется в основном так же, как


в Connect, только фильтрация производится в параллельной вселенной. Как уже
отмечалось, функции маршрутизатора доступны непосредственно через объект
сервера. Основное различие заключается в том, что именно делается в функциях
маршрутизатора, и это различие обусловлено наличием системы шаблонов.
В Express страницы отправляются пользователю функцией res.render, а не
с помощью res.writeHead и res.end, как раньше. Функция res.render формирует
результат, подставляя данные в шаблон, что позволяет провести четкую границу
между логикой и представлением.
EJS – лишь одна из систем шаблонов, доступных в Express. Мы настроили сер-
вер так, что она используется для любого файла в каталоге views с расширением
.html.
Существуют и другие движки шаблонов; если конфигурация не задана явно,
то движок определяется расширением имени файла по следующим правилам:

res.render('index.haml', {...данные...}); // использовать Haml


res.render('index.jade', {...данные...}); // использовать Jade
res.render('index.ejs', {...данные...}); // использовать EJS
res.render('index.coffee', {...данные...}); // использовать CoffeeKup
res.render('index.jqtpl', {...данные...}); // использовать jQueryTemplates

С помощью функции app.set можно также подменить движок, подразумевае-


мый по умолчанию:

app.set('view engine', 'haml'); // использовать Haml


app.set('view engine', 'jade'); // использовать Jade
app.set('view engine', 'ejs'); // использовать EJS
Реализация Math Wizard с помощью Express 85

Теперь, определившись с кодом, перейдем к созданию файлов шаблонов. Все


они должны находиться в каталоге views.
Сначала создайте файл, поместив в него такой код:

<html>
<head><title><%= title %></title></head>
<body>
<h1><%= title %></h1>
<table>
<tr><td>
<div class='navbar'>
<p><a href='/'>home</a></p>
<p><a href='/mult'>Multiplication</a></p>
<p><a href='/square'>Square's</a></p>
<p><a href='/factorial'>Factorial's</a></p>
<p><a href='/fibonacci'>Fibonacci's</a></p>
</div>
</td>
<td><%- body %></td>
</tr>
</table></body></html>

В Express шаблон layout является специальным. Обратите внимание, что


в файле app.js мы вызывали функцию res.render('fibo.html' ...), а файл layout.
html нигде даже не упоминали. Так что же происходит? По умолчанию все содер-
жимое именованного шаблона, получившееся после подстановки данных, переда-
ется шаблону layout в виде значения переменной body. Когда app.js вызывает res.
render('fibo.html' ...), сначала формируется фрагмент страницы по шаблону
fibo.html, а затем – вся страница по шаблону layout.
Поведение по умолчанию можно переопределить двумя способами. Во-первых,
установить глобальный параметр Express, который отключает (или включает) ис-
пользование шаблона layout:

app.set('view options', { layout: false (или true) });

Во-вторых, отменить шаблон layout для конкретной страницы:

res.render('myview.ejs', { layout: false (или true) });

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


layout:

res.render('page', { layout: 'mylayout.jade' });

EJS-шаблон по существу представляет собой HTML-код с тремя дополни-


тельными разновидностями тегов. Если вам уже доводилось работать с другими
системами шаблонов, то эти дополнительные теги должны быть знакомы:
86 Вариации на тему простого приложения
‰ <% code %> – небуферизуемый код (условные предложения и прочее);
‰ <%= code %> – экранирование HTML;
‰ <%- code %> – без экранирования, с буферизацией.
В шаблоне layout мы видим использование экранированного HTML-кода с по-
мощью тега <%= title %> и небуферизованных данных с помощью тега <%- body %>.
Теперь перейдем к шаблону home.html приложения Math Wizard:

<p>Math Wizard</p>

И это все!
В шаблон mult.html добавим такой код:

<% if (req.a && req.b) { %>


<p class='result'>
<%= req.a %> * <%= req.b %> = <%= req.result %>
</p>
<% } %>
<p>Enter numbers to multiply</p>
<form name='mult' action='/mult' method='get'>
A: <input type='text' name='a' /><br/>
B: <input type='text' name='b' />
<input type='submit' value='Submit' />
</form>

Здесь мы встречаем теги <% code %> с условным предложением, позволяющим


выводить результат только в случае, когда есть входные данные:

<% if (req.a && req.b) { %>

содержимое, выводимое по условию

<% } %>

Внутри тегов <% code %> может быть произвольный JavaScript-код, но в данном
случае мы воспользовались предложением if, для того чтобы вывести некоторое
содержимое, если удовлетворяется заданное условие. Если бы нужно было вывес-
ти список или массив данных, то можно было бы организовать цикл с помощью
предложения while.
В файл square.html поместите такой код:

<% if (req.a) { %>


<p class='result'>
<%= req.a %> squared = <%= req.result %>
</p>
<% } %>
<p>Enter numbers to multiply</p>
<form name='square' action='/square' method='get'>
Реализация Math Wizard с помощью Express 87

A: <input type='text' name='a' />


<input type='submit' value='Submit' />
</form>

В файл factorial.html поместите такой код:

<% if (req.a) { %>


<p class='result'>
<%= req.a %> factorial = <%= req.result %>
</p>
<% } %>
<p>Enter a number to see it's factorial</p>
<form name='factorial' action='/factorial' method='get'>
A: <input type='text' name='a' />
<input type='submit' value='Submit' />
</form>

Наконец, в fibo.html поместите такой код:

<% if (req.a) { %>


<p class='result'>
fibonacci <%= req.a %> = <%= req.result %>
</p>
<% } %>
<p>Enter a number to see it's fibonacci</p>
<form name='fibonacci' action='/fibonacci' method='get'>
A: <input type='text' name='a' />
<input type='submit' value='Submit' />
</form>

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

$ node app-express.js

Теперь заходите в приложение, набрав в адресной строке браузера URL http://


localhost:8124/, и работайте с Math Wizard в свое удовольствие.

Обработка ошибок
Ошибки неизбежны. И лучше узнать о них как можно раньше, потому что чем
раньше мы обнаружим ошибку, тем дешевле будет ее исправить. Express предлага-
ет два способа перехвата ошибок.
В Math Wizard была такая строка кода:

app.use(express.errorHandler({
dumpExceptions: true, showStack: true
}));
88 Вариации на тему простого приложения
Это обработчик ошибок, подразумеваемый по умолчанию, он выводит полез-
ную разработчику информацию с кучей деталей, в том числе трассой стека. Но
показывать ее обычному посетителю вы вряд ли захотите. А предпочтете, навер-
ное, показать, скажем, кита, которого поднимает над океаном стая птиц. Чтобы
показать понятное пользователю сообщение об ошибке, нужно первым делом
установить обработчик ошибок с помощью функции app.error. Отметим, что эта
функция принимает дополнительный параметр err, в котором передается объект
ошибки:

app.error(function(err, req, res, next) {


// …
res.send(... страница ошибки); // или res.render('template'...)
});

Именно здесь вы можете проявить фантазию, нарисовав потешную страницу


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

Параметризованные URL и службы данных


До сих пор мы имели дело с приложениями, которые посылают браузеру
HTML-код. Это, конечно, важный частный случай, но Express (и Connect) можно
использовать и для других целей. Например, протокол HTTP часто применяется
для создания REST-совместимых служб, которые посылают данные, предназна-
ченные для какого-то приложения, а не HTML-страницу, адресованную человеку.
Ранее мы рассмотрели (и отвергли) возможность использования сервера для
вычисления чисел Фибоначчи, чтобы разгрузить сервер переднего плана. Но те-
перь все же создадим такой сервер, просто чтобы понять, как это делается. Попут-
но мы рассмотрим механизм параметризованной маршрутизации и представление
ответа в виде данных. Вперед!

Параметризованные URL-адреса в Express


Система маршрутизации в Express допускает наличие в URL-адресах марке-
ров, значения которых становятся доступны в объекте req. Это позволяет сделать
программу более гибкой. Делается это путем сопоставления URL запроса с за-
данными вами образцами; Express извлекает сопоставившиеся части URL и под-
ставляет их в поля объекта req.
Пример поможет прояснить ситуацию:

app.get('/user/:id', function(req, res){


res.send('user ' + req.params.id);
});

В параметризованном URL /user/:id встречается маркер id. Express выделяет


часть URL запроса, следующую после /user/, и записывает ее в поле req.params.id.
В качестве образца может фигурировать и регулярное выражение.
Реализация Math Wizard с помощью Express 89

Математический сервер (и клиент)


Напишем простой сервер, который будет производить математические вычис-
ления и возвращать результаты в виде JSON-объекта. Он будет поддерживать те
же четыре операции, что и Math Wizard.
Создайте файл math-server.js и поместите в него такой код:

var math = require('./math');


var express = require('express');
var app = express.createServer(
//express.logger()
);
app.configure(function(){
app.use(app.router);
app.use(express.errorHandler({
dumpExceptions: true, showStack: true }));
});

app.get('/fibonacci/:n', function(req, res, next) {


math.fibonacciAsync(Math.floor(req.params.n),
function(val) {
res.send({ n: req.params.n, result: val });
});
});
app.get('/factorial/:n', function(req, res, next) {
res.send({
n: req.params.n,
result: math.factorial(Math.floor(req.params.n))
});
});
app.get('/mult/:a/:b', function(req, res, next) {
res.send({
a: req.params.a, b: req.params.b,
result: req.params.a * req.params.b
});
});
app.get('/square/:a', function(req, res, next) {
res.send({
a: req.params.a,
result: req.params.a * req.params.a
});
});
app.listen(3002);

Это полный сервер; мы лишь не показали модуль math, который ничем не отли-
чается от разработанного ранее. Конфигурация немного урезана, и номер прослу-
шиваемого порта равен 3002, чтобы этот сервер мог играть роль вспомогательного
для Math Wizard.
Маршруты очень простые, в них предусмотрены маркеры для аргументов
функций, выполняющих операций.
90 Вариации на тему простого приложения
Это первый пример, в котором нам встретилась функция res.send. Она дает
гибкий способ отправлять ответы, принимая массив значений HTTP-заголовков
и код состояния HTTP. В данном случае она автоматически представляет полу-
ченный объект в формате JSON и отправляет его, добавляя правильный заголовок
Content-Type.
Запустим сервер:

$ node math-server.js &


[1] 10483

$ curl -f http://localhost:3002/square/34.2
{"a":"34.2","result":1169.64}

$ curl -f http://localhost:3002/mult/3.3/3
{"a":"3.3","b":"3","result":9.899999999999999}

$ curl -f http://localhost:3002/factorial/20
{"n":"20","result":2432902008176640000}

$ curl -f http://localhost:3002/fibonacci/20
{"n":"20","result":6765}

Ну хорошо, сервер мы реализовали, а как быть с клиентом?


Поскольку сервер работает по протоколу HTTP, клиентские программы долж-
ны посылать ему HTTP-запросы. В состав Node входит замечательный объект
HTTP Client, и в главе 5 «Простой веб-сервер, объекты EventEmitter и HTTP-
клиенты» мы рассмотрим его более подробно.
Задача состоит в том, чтобы сконструировать HTTP-запрос, отправить его,
дождаться ответа, декодировать тело ответа и использовать содержащиеся в нем
данные. Все это можно сделать и в веб-приложении типа Math Wizard, но мы напи-
шем простое консольное приложение, которое будет клиентом математического
сервера.
Создайте файл math-client.js и поместите в него такой код:

var http = require('http');


var util = require('util');
[
"/fibonacci/20", "/factorial/20",
"/mult/10/20", "/square/12"
].forEach(function(path) {
var req = http.request({
host: "localhost",
port: 3002,
path: path,
method: 'GET'
}, function(res) {
res.on('data', function (chunk) {
Реализация Math Wizard с помощью Express 91

util.log('BODY: ' + chunk);


});
});
req.end();
});

Функция http.request создает объект HTTP-запроса, в котором отдельные


компоненты URL помещены в поля объекта. В следующей главе мы поговорим
об этом более подробно, а пока достаточно знать, что функция обратного вызова,
объявленная в res.on, вызывается, когда приходит HTTP-ответ.
Скрипт math-client.js посылает серверу math-server.js несколько «зашитых»
запросов и печатает полученные результаты:

$ node math-client.js
7 Jun 22:17:49 - BODY: {"n":"20","result":2432902008176640000}
7 Jun 22:17:49 - BODY: {"a":"12","result":144}
7 Jun 22:17:49 - BODY: {"a":"10","b":"20","result":200}
7 Jun 22:17:49 - BODY: {"n":"20","result":6765}

Внимательный читатель, наверное, заметил, что ответы поступают не в том


порядке, в котором отправлялись запросы. Последним пришел результат вычис-
ления числа Фибоначчи, хотя этот запрос был отправлен первым. Напомним, что
функции обратного вызова вызываются асинхронно, по мере того как ответы на
запросы попадают в цикл обработки событий, а для вычисления числа Фибонач-
чи требуется заметное время. В данном случае на вычисление двадцатого числа
Фибоначчи ушло больше времени, чем на все остальные операции. Клиент math-
client.js посылает запросы очень быстро, потому что делать ему почти ничего не
надо, а распечатка, которую мы видим на экране, – это результат вызова обработ-
чиков, заданных при обращении к res.on('data'…). На запросы клиента в сервере
math-server.js отвечает обработчик, заданный в app.get. Каждый обработчик от-
вета res.on('data'…) связан с обработчиком запроса app.get через сокет, по ко-
торому был отправлен HTTP-запрос. Когда обработчик запроса app.get вызыва-
ет res.send, отправленный им HTTP-ответ приводит к вызову обработчика res.
on('data'…), который ожидает этот ответ.
От чего зависит, какой результат печатается первым? От времени, которое
скрипт math-server.js затрачивает на вычисление результата, так как результат
печатается только после поступления ответа.
В большинстве случаев вычисление производится быстро (например, при
умножении), и ответ возвращается почти мгновенно. Но с запросом на вычисле-
ние числа Фибоначчи дело обстоит по-другому. Так как используется функция
fibonacciAsync, то вычисление числа Фибоначчи происходит параллельно с об-
работкой других запросов, и за то время, пока вычисляется 20-е число, сервер
успевает обработать остальные запросы и отправить их клиенту. Если запросить
второе число Фибоначчи, то результат будет вычислен быстрее, поэтому порядок
прихода ответов изменится:
92 Вариации на тему простого приложения

$ node math-client.js
7 Jun 22:34:49 - BODY: {"n":"2","result":1}
7 Jun 22:34:49 - BODY: {"a":"10","b":"20","result":200}
7 Jun 22:34:49 - BODY: {"n":"20","result":2432902008176640000}
7 Jun 22:34:49 - BODY: {"a":"12","result":144}

Переработка Math Wizard с использованием


математического сервера
Имея клиентскую функцию, несложно внедрить ее в обработчик запросов
в Math Wizard. Раньше мы уже подумывали о том, как сделать так, чтобы фрон-
тальный сервер быстро обрабатывал запросы к вящему удовольствию клиентов,
но в то же время мог производить длительные вычисления. Вычисление последо-
вательности чисел Фибоначчи – пример длительного вычисления; если произво-
дить его на фронтальном сервере, то клиенты вряд ли останутся довольны.
Раньше мы решили эту проблему путем переработки алгоритма – разбив вы-
числение на части и осуществляя диспетчеризацию с помощью очереди событий.
В некоторых случаях такой подход годится, но фронтальный сервер при этом все
равно должен выполнять вычисление целиком, так что переработанный алгоритм
может оказаться даже менее эффективным, чем первоначальный. Скрипт math-
client.js решает задачу иначе, делегируя вычисление серверу заднего плана или
даже балансируемому кластеру таких серверов.
Замените в файле app-express.js обработчик запроса /fibonacci таким:

app.get('/fibonacci', htutil.loadParams, function(req, res) {


if (req.a) {
var httpreq = require('http').request({
host: "localhost",
port: 3002,
path: "/fibonacci/"+Math.floor(req.a),
method: 'GET'
}, function(httpresp) {
httpresp.on('data', function (chunk) {
var data = JSON.parse(chunk);
req.result = data.result;
res.render('fibo.html',
{ title: "Math Wizard", req: req });
});
});
httpreq.end();
//math.fibonacciAsync(Math.floor(req.a), function(val) {
//req.result = val;
//res.render('fibo.html',
//{ title: "Math Wizard" , req: req });
//});
} else {
res.render('fibo.html',
Резюме 93

{ title: "Math Wizard" , req: req });


}
});

Здесь новый обработчик запроса сам отправляет HTTP-запрос серверу задне-


го плана (math-server.js). Это простейшая форма REST-совместимой службы.
Сервер заднего плана определяет несколько URL для GET-запросов по протоко-
лу HTTP и возвращает данные в формате JSON, а наш обработчик разбирает эти
данные и возвращает окончательный результат клиенту.
Запускается фронтальный сервер точно так же, как и раньше:

$ node app-express.js

Да и ведет он себя так же, как при использовании функции fibonacciAsync. Так
что же мы выиграли? Зачем нужно было включать в архитектуру вспомогатель-
ную службу? Для приложения Math Wizard это, пожалуй, действительно изли-
шество, но на этом примере мы продемонстрировали широко распространенный
подход к повышению производительности приложения. Вот какие факторы сле-
дует принимать во внимание.
‰ Иногда имеет смысл отказаться от выполнения длительных вычислений
на фронтальном сервере, освободив его для взаимодействия с браузерами.
‰ Распределение запросов между несколькими серверами с помощью балан-
сировщика нагрузки (облачные вычисления).
‰ Ответы от math-server.js детерминированы, поэтому кэширующий прок-
си-сервер мог бы дать колоссальный прирост производительности. Зачем
заново вычислять уже известные ответы?
‰ Открывается возможность поразить начальника модными словечками.
Легкость, с которой мы реализовали math-server.js и интегрировали его с Math
Wizard, еще раз демонстрирует простоту и мощь платформы Node.

Резюме
В этой главе мы многому научились и готовы приступить к созданию настоя-
щих приложений. Но сначала повторим, что мы узнали.
‰ Вынесение обработки конкретных запросов в отдельные модули.
‰ Создание веб-приложений с помощью объекта HTTP Server, а также с по-
мощью каркасов Connect и Express.
‰ Получение параметров запроса из форм.
‰ Влияние длительных вычислений на время реакции сервера и удовлетво-
ренность пользователей, а также способы решения проблемы.
‰ Использование модуля async для упрощения асинхронного программиро-
вания.
‰ Некоторые черты полноценных веб-приложений, предоставляемые карка-
сами Connect и Express.
94 Вариации на тему простого приложения
‰ Что в Connect понимается под ПО промежуточного уровня.
‰ Как правила маршрутизации в Connect и Express позволяют по-разному
обрабатывать запросы, отправленные разными HTTP-методами.
‰ Параметризованные URL в Express.
‰ Реализация REST-совместимого сервера заднего плана для распределения
вычислительной нагрузки.
Теперь, когда мы так много знаем о реализации веб-приложений, рассмотрим
более детально объекты сервера и клиента HTTP и систему распределения собы-
тий в Node.
Глава 5. ПРОСТОЙ ВЕБ-СЕРВЕР,
ОБЪЕКТЫ EVENTEMITTER
И HTTP-КЛИЕНТЫ
Итак, мы видели, как создаются приложения для Node с помощью веб-каркаса
Express. Теперь заглянем под капот и разберемся в деталях реализации HTTP-
сервера. В этой главе мы напишем простой веб-сервер, который будет обладать
некоторыми из свойств реального веб-сервера, обсуждавшимися в главе 4.
Обычно заботу о деталях лучше поручить каркасу разработки веб-приложений,
потому что правильно реализовать протокол HTTP нелегко. Тогда зачем же мы
хотим написать HTTP-сервер самостоятельно? По нескольким причинам:
‰ понять, какой каркас выбрать;
‰ понять, почему каркас работает именно так, а не иначе;
‰ не всякая задача соответствует условиям, подразумевавшимся при проек-
тировании каркаса;
‰ иногда, чтобы реализовать службы, отличающиеся от веб-приложений, не-
обходимо кодировать прямо на уровне HTTP;
‰ быть может, у вас есть более удачные мысли, чем у авторов каркаса.
Приступим.

Отправка и получение событий


с помощью объектов EventEmitter
Объект EventEmitter – ключ к использованию Node для реализации прило-
жений, но он так глубоко интегрирован в платформу, что можно даже не заметить
его существования. Многие объекты Node являются подклассами EventEmitter и
пользуются его методами для отправки событий, сигнализирующих о наступлении
тех или иных условий. Эти события попадают в цикл обработки событий и в ко-
нечном итоге приводят к обратному вызову зарегистрированных обработчиков.
В этой главе мы будем работать с объектами HTTPServer и HTTPClient. Оба явля-
ются подклассами EventEmitter и с его помощью посылают события для каждого
шага протокола HTTP. Усвоение принципов работы EventEmitter поможет лучше
понять не только эти, но и многие другие объекты Node.
Класс EventEmitter определен в модуле Node events. Чтобы напрямую рабо-
тать с этим классом, нужно вызвать функцию require('events'), но обычно это
необязательно. Многие объекты в Node вызывают require('events') самостоя-
тельно, так что вам об этом думать не нужно.
96 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты
В следующем примере (pulser.js) демонстрируются отправка и получение со-
бытий путем прямого обращения к классу EventEmitter:

var events = require('events');


var util = require('util');

function Pulser() {
events.EventEmitter.call(this);
}

util.inherits(Pulser, events.EventEmitter);

Pulser.prototype.start = function() {
var self = this;
this.id = setInterval(function() {
util.log('>>>> pulse');
self.emit('pulse');
util.log('<<<< pulse');
}, 1000);
}

Здесь определен класс Pulser, наследующий EventEmitter (для задания насле-


дования применяется функция util.inherits). Его задача – один раз в секунду
посылать события всем зарегистрированным приемникам. В методе start вызыва-
ется функция setInterval, которая инициирует повторяющееся – раз в секунду –
обращение к функции обратного вызова и вызов функции emit, которая посылает
событие pulse всем приемникам.
В таком виде модулем pulser.js может воспользоваться любое приложение,
которому необходимы регулярно повторяющиеся события таймера.
Теперь посмотрим, как воспользоваться объектом Pulser:

var pulser = new Pulser();


pulser.on('pulse', function() {
util.log('pulse received');
});
pulser.start();

Здесь мы создаем объект Pulser и потребляем отправляемые им события pulse.


Вызов pulser.on('pulse'…) устанавливает связь между событием pulse и вызы-
ваемой в ответ на него функцией. Затем вызывается метод start, который запус-
кает процесс.
Поместив этот код в файл pulser.js и выполнив его, вы должны увидеть такую
распечатку:

$ node pulser.js
23 May 20:30:20 - >>>> pulse
23 May 20:30:20 - pulse received
23 May 20:30:20 - <<<< pulse
Теоретические основы EventEmitter 97

23 May 20:30:21 - >>>> pulse


23 May 20:30:21 - pulse received
23 May 20:30:21 - <<<< pulse
...

Теоретические основы EventEmitter


События, генерируемые объектом EventEmitter, имеют имена; в примере выше
событие называлось pulse. Имена событий могут быть произвольны, лишь бы
имели для вас смысл, и количество различных событий не ограничено. Для зада-
ния имени события достаточно указать его при вызове функции .emit. Никакого
формального реестра имен событий не существует. По принятому соглашению,
имя события error применяется для извещения об ошибках.
Для отправки события применяется функция .emit. Событие посылается всем
зарегистрированным приемникам. Чтобы зарегистрировать приемник, программа
вызывает метод .on объекта-отправителя и передает ему функцию обратного вы-
зова, которая получит событие.
Это видно на примере файла pulser.js. Объект Pulser вызывает self.
emit('pulse'), чтобы отправить событие, а позже в том же файле вызывается ме-
тод pulse.on('pulse', …), определяющий, куда эти события будут доставляться.
Часто требуется посылать вместе с событием данные. Для этого нужно просто
передать дополнительные аргументы функции .emit, например:

self.emit('eventName', data1, data2, ...);

Когда программа получит такое событие, данные будут выглядеть как аргу-
менты функции обратного вызова. Следовательно, приемник события должен вы-
глядеть примерно так:

emitter.on('eventName', function(data1, data2, ...) {


// выполнить действия при получении события
});

Практические примеры мы увидим в следующем разделе при обсуждении объ-


ектов протокола HTTP. Все клиентские и серверные объекты HTTP наследуют
EventEmitter и посылают события, соответствующие различным шагам протокола.
Так, любой входящий запрос инкапсулирован в объект Request. Этот объект по-
сылает событие data при поступлении запроса, событие end – когда получены все
данные, и событие close – если сокет закрывается до отправки события end.

HTTP Sniffer – прослушивание обмена


данными по протоколу HTTP
Приступая к работе с объектами HTTP, давайте создадим полезный класс, ко-
торый будет прослушивать все события, отправляемые объектом HTTP Server.
98 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты
Это не только удобный отладочный инструмент, но и хорошее средство для изуче-
ния работы серверного объекта HTTP.
В Node объект HTTP Server наследует EventEmitter, а программа HTTP Sniffer
просто получает все отправляемые сервером события и печатает содержащуюся
в них информацию.
Создайте файл httpsniffer.js и поместите в него такой код:

var util = require('util');


var url = require('url');

exports.sniffOn = function(server) {
server.on('request', function(req, res) {
util.log('e_request');
util.log(reqToString(req));
});

server.on('close', function(errno) {
util.log('e_close errno='+ errno);
});

server.on('checkContinue', function(req, res) {


util.log('e_checkContinue');
util.log(reqToString(req));
res.writeContinue();
});

server.on('upgrade', function(req, socket, head) {


util.log('e_upgrade');
util.log(reqToString(req));
});

server.on('clientError', function() {
util.log('e_clientError');
};

// server.on('connection', ..);
}

var reqToString = function(req) {


var ret = 'request ' + req.method +' '+ req.httpVersion +' '+
req.url +'\n';
ret += JSON.stringify(url.parse(req.url, true)) +'\n';
var keys = Object.keys(req.headers);
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
ret += i +' '+ key +': '+ req.headers[key] +'\n';
}
if (req.trailers)
Теоретические основы EventEmitter 99

ret += req.trailers +'\n';


return ret;
}
exports.reqToString = reqToString;

Код довольно длинный, но наибольший интерес в нем представляет функция


sniffOn. Она обращается в функции .on для связывания функций-приемников
с событиями, генерируемыми объектом HTTP Server. События соответствуют
шагам взаимодействия между клиентом и сервером по протоколу HTTP.
В качестве примера использования HTTP Sniffer возьмем следующую моди-
фицированную версию простого веб-сервера «Здравствуй, мир» (hwserver.js):

var http = require('http');


var sniffer = require('./httpsniffer');

var server = http.createServer(function (req, res) {


res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello, World!\n');
});
sniffer.sniffOn(server);
server.listen(3000);

Запустите этот сервер:

$ node hwserver.js

и зайдите из браузера на адрес http://localhost:3000/. На консоли появятся по-


казанные ниже сообщения. Обратите внимание, что было отправлено два запроса:
на адрес / и на адрес /favicon.ico. Favicon – это представляющий ваш сайт зна-
чок, который показывают некоторые браузеры. Рассматриваемый сервер не под-
держивает запросов к этому файлу, но ниже мы покажем, как реализовать данную
возможность.

$ node hwserver.js
6 Apr 21:14:38 - e_request
6 Apr 21:14:38 - request GET 1.1 /
{"search":"","query":{},"pathname":"/","href":"/"}
0 host: localhost:3000
1 user-agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-us)
AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27
2 accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/
plain;q=0.8,image/png,*/*;q=0.5
3 cache-control: max-age=0
4 accept-language: en-us
5 accept-encoding: gzip, deflate
6 connection: keep-alive
100 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты
6 Apr 21:14:39 - e_request
6 Apr 21:14:39 - request GET 1.1 /favicon.ico
{"search":"","query":{},"pathname":"/favicon.ico","href":"/favicon.ico"}
0 host: localhost:3000
1 user-agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-us)
AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27
2 referer: http://localhost:3000/
3 cache-control: max-age=0
4 accept: */*
5 accept-language: en-us
6 accept-encoding: gzip, deflate
7 connection: keep-alive

Итак, у нас есть инструмент для прослушивания серверных событий HTTP.


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

Реализация простого веб-сервера


В этом разделе приведена реализация простого веб-сервера Basic Server. В со-
став Node входит великолепный объект HTTP Server, но его необходимо попол-
нить некоторыми элементами протокола и службами, считающимися неотъемле-
мой принадлежностью любого веб-сайта.
Basic Server действительно прост. На его примере мы продемонстрируем реа-
лизацию следующих возможностей:
‰ гибкая маршрутизация запросов;
‰ автоматическое создание объекта, представляющего различные компонен-
ты URL;
‰ автоматическое извлечение заголовка Host (для организации виртуально-
го хостинга);
‰ автоматическое извлечение заголовка Cookie;
‰ обслуживание запросов на получение файла favicon.ico;
‰ обслуживание запросов к статическим файлам (HTML, JS, PNG, GIF, JPEG
и т. д);
‰ гибкая настройка сервера.
Представленный ниже код включает четыре Node-модуля, CSS-файл и один
или несколько HTML-файлов. Код совсем невелик, что свидетельствует о гибко-
сти и мощи платформы Node.
В качестве подготовительного этапа нужно установить модуль MIME, отве-
чающий за порождение правильных заголовков Content-Type. Если вам интерес-
но, что это такое, мы обсудим этот модуль позже. А пока выполните такую команду:

$ npm install mime


Теоретические основы EventEmitter 101

Реализация Basic Server


Прежде чем приступать к кодированию, поразмыслим на тему общей страте-
гии реализации поставленных целей. Без каких-либо дополнений Node предлага-
ет такую архитектуру:

var server = http.createServer(function (req, res) {


// обработать запрос
});
server.listen(port);

Наш список целей предполагает, что предстоит написать обработчик, который


анализирует запрос и в зависимости от его характеристик поручает обработку
подходящим функциям. Такая архитектура позволит отделить логику анализа и
диспетчеризации от бизнес-логики приложения.

Ядро Basic Server (basicserver.js)


В основе Basic Server лежит модуль, который создает объект HTTP Server и
присоединяет к нему функции, которые анализируют запрос и направляют его
подходящим обработчикам.
Создайте файл basicserver.js и поместите в него такой код:

var http = require('http');


var url = require('url');

exports.createServer = function() {
var htserver = http.createServer(function(req, res) {
req.basicServer = {
urlparsed: url.parse(req.url, true)
};
processHeaders(req, res);
dispatchToContainer(htserver, req, res);
});
htserver.basicServer = { containers: [] };
htserver.addContainer = function(host, path,
module, options) {
if (lookupContainer( htserver, host, path) !== undefined) {
throw new Error("Already mapped "+host+"/"+path);
}
htserver.basicServer.containers.push({
host: host, path: path,
module: module, options: options });
return this;
}
htserver.useFavIcon = function(host, path) {
return this.addContainer(host, "/favicon.ico",
require('./faviconHandler'),
{ iconPath: path });
}
102 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты
htserver.docroot = function(host, path, rootPath) {
return this.addContainer(host, path,
require('./staticHandler'),
{ docroot: rootPath });
}
return htserver;
}

Итак, ядро Basic Server составляет функция createServer, которая создает


и возвращает объект HTTP Server, попутно наделяя его дополнительной функ-
циональностью. Самое интересное здесь – функция обработки запросов. Наша
стратегия состоит в том, чтобы сначала добавить в объект запроса полезную ин-
формацию (в функции processHeaders), а затем передать запрос подходящему
обработчику (в функции dispatchToContainer). Второй модуль предназначен для
того, чтобы сконфигурировать сервер в соответствии с потребностями прило-
жения. Один пример такой конфигурации мы рассмотрим чуть ниже.
Мы собираемся добавить полезные данные в объекты сервера (htserver) и
запроса (req). Благодаря слабой типизации JavaScript позволяет сделать все, что
нам нужно. Добавления вносятся в объект basicServer, который присоединяется
в виде свойства к объектам htserver и req. Таким образом, мы можем добавить
произвольные данные в htserver, а скрыв их в объекте htserver.basicServer, мы
можем не опасаться интерференции с другим кодом.
Далее мы добавляем три функции для управления списком контейнеров.
Понятие контейнера приблизительно соответствует ПУ-модулю router в Express,
с которым мы встречались в предыдущей главе. Первая функция (addContainer)
добавляет указанный контейнер в сервер, а две остальные пользуются ей для
добавления двух конкретных контейнеров: для обработки Favicon (useFavIcon) и
статических файлов (docroot).
Каждый контейнер характеризуется четырьмя свойствами:
‰ регулярное выражение для сопоставления с заголовком Host;
‰ регулярное выражение для сопоставления с URL запроса;
‰ объект options;
‰ функция обработки.
Все вместе, они позволяют реализовать виртуальный хостинг по именам, то
есть Basic Server сможет отвечать на запросы к разным доменным именам путем
сравнения заголовка Host с объектами-контейнерами. Подробнее об этом ниже.
Объект options предназначен для передачи конфигурационных данных из
модуля конфигурирования модулю обработчика, и содержимое этого объекта
определяется модулем обработчика.
Например, обработчик Favicon содержит путь к графическому файлу, который
возвращается в ответ на запросы значка сайта. Браузер всегда запрашивает файл
с путем /favicon.ico, поэтому мы жестко «зашиваем» его в контейнер.
Есть еще несколько функций, которыми мы уже пользовались, но пока не объ-
ясняли. Первая, lookupContainer, ищет в массиве containers элемент, для которого
свойства host и path соответствуют параметрам, переданным в HTTP-запросе:
Теоретические основы EventEmitter 103

var lookupContainer = function(htserver, host, path) {


for (var i = 0; i < htserver.basicServer.containers.length; i++) {
var container = htserver.basicServer.containers[i];
var hostMatches = host.toLowerCase().match(container.host);
var pathMatches = path.match(container.path);
if (hostMatches !== null && pathMatches !== null) {
return {
container: container, host: hostMatches, path: pathMatches };
}
}
return undefined;
}

Она просто просматривает массив с начала до конца, сопоставляя переданные


значения host и path с регулярными выражениями, заданными в текущем контей-
нере. Если элемент найден, функция возвращает его, в противном случае возвра-
щается undefined.
Следующая функция, processHeaders, ищет в массиве req.headers заголовки
Cookie и Host, поскольку тот и другой необходимы для диспетчеризации запроса.
Как мы уже видели ранее, эта функция вызывается для каждого HTTP-запроса:

var processHeaders = function(req, res) {


req.basicServer.cookies = [];
var keys = Object.keys(req.headers);
for (var i = 0, l = keys.length; i < l; i++) {
var hname = keys[i];
var hval = req.headers[hname];
if (hname.toLowerCase() === "host") {
req.basicServer.host = hval;
}
if (hname.toLowerCase() === "cookie") {
req.basicServer.cookies.push(hval);
}
}
}

Есть много других HTTP-заголовков (Accept, Accept-Encoding, Accept-Lan-


guage, User-Agent); сохранять их или нет, зависит от приложения.
Последняя функция, dispatchToContainer, ищет подходящий контейнер и на-
правляет ему запрос. Как и processHeaders, она вызывается для каждого HTTP-
запроса:

var dispatchToContainer = function(htserver, req, res) {


var container = lookupContainer(htserver,
req.basicServer.host,
req.basicServer.urlparsed.pathname);
if (container !== undefined) {
104 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты
req.basicServer.hostMatches = container.host;
req.basicServer.pathMatches = container.path;
req.basicServer.container = container.container;
container.container.module.handle(req, res);
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end("no handler found for "+
req.host +"/"+ req.urlparsed.path);
}
}

Если ни один контейнер не найден, пользователь увидит страницу с сообще-


нием об ошибке (с кодом состояния 404).
Модули обработчиков экспортируют функцию handle с сигнатурой function
(req,res). Именно внутри dispatchToContainer сервер Basic Server диспетчеризует
запросы, вызывая функцию handle.

Обработчик Favicon (faviconHandler.js)


В состав Basic Server входят два встроенных модуля обработки, которые мы
еще не видели. Первый, faviconHandler.js, обрабатывает запросы на значок сайта
(Favicon). Он устанавливается, если в модуле конфигурирования используется
функция useFavIcon:

var fs = require('fs');
var mime = require('mime');
exports.handle = function(req, res) {
if (req.method !== "GET") {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end("invalid method " + req.method);
} else if (req.basicServer.container.options.iconPath!== undefined) {
fs.readFile(req.basicServer.container.options.iconPath,
function(err, buf) {
if (err) {
res.writeHead(500, {
'Content-Type': 'text/plain' });
res.end( req.basicServer.container.options.iconPath +" not found");
} else {
res.writeHead(200, {
'Content-Type':
mime.lookup(req.basicServer.container.options.iconPath),
'Content-Length': buf.length
});
res.end(buf);
}
});
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end("no favicon");
}
}
Теоретические основы EventEmitter 105

Этот обработчик отвечает на запросы к файлу favicon.ico.


Еще раз повторим: модули-обработчики экспортируют функцию function(req,
res) с именем handle. Basic Server вызывает функцию handle того обработчика,
который находится в контейнере, соответствующем запросу. Данный обработчик
пытается прочитать файл с именем, указанным в свойстве iconPath, и передает его
браузеру с помощью объекта res. Если обработчик обнаружит ошибку, то пошлет
с помощью res соответствующую страницу с сообщением.
Модуль MIME определяет правильный тип MIME, исходя из типа файла
значка. Графические файлы со значками могут быть представлены в любом фор-
мате, и мы должны сообщить браузеру тип посылаемого файла.
Поскольку этот обработчик предназначен только для обработки GET-запро-
сов, он проверяет метод запроса и возвращает код состояния 404, если метод от-
личен от GET.

Обработчик статических файлов (staticHandler.js)


Теперь рассмотрим код обработки запросов на файлы с расширениями .html,
.css и им подобными. Создайте файл staticHandler.js и поместите в него такой код:

var fs = require('fs');
var mime = require('mime');
var sys = require('sys');
exports.handle = function(req, res) {
if (req.method !== "GET") {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end("invalid method " + req.method);
} else {
var fname = req.container.options.docroot + req.urlparsed.pathname;
if (fname.match(/\/$/)) fname += "index.html";
fs.stat(fname, function(err, stats) {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end("file "+ fname +" not found " + err);
} else {
fs.readFile(fname, function(err, buf) {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end("file "+ fname +" not readable " + err);
} else {
res.writeHead(200, {
'Content-Type': mime.lookup(fname),
'Content-Length': buf.length });
res.end(buf);
}
});
}
});
106 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты
}
}

Это обработчик staticHandler, отвечающий за возврат файлов из файловой сис-


темы.
Параметр docroot содержит путь к каталогу, в котором хранятся статические
файлы. Обработчик просто читает указанный файл из каталога docroot и отправ-
ляет его браузеру с помощью объекта res. Если файл не найден или при его чтении
произошла ошибка, то возвращается страница с ее описанием.
Особенность этого обработчика – использование модуля MIME (его можно
скачать из репозитория npm), чтобы определить правильный заголовок Content-
Type. Тип MIME необходим браузеру для корректной интерпретации данных. Мы
еще вернемся к этой теме ниже.
Особый случай возникает, когда запрошенный URL оканчивается символом
/; в этом случае обработчик изменяет запрос, добавляя в конец строку index.html.

Конфигурирование Basic Server (server.js)


Мы рассмотрели все компоненты Basic Server и теперь можем взглянуть, как
собирается работоспособный веб-сервер. Создайте файл server.js и поместите
в него такой код:

var port = 4080;


var server = require('./basicserver').createServer();
server.useFavIcon("localhost", "./docroot/favicon.png");
server.docroot("localhost", "/", "./docroot");
require('./httpsniffer').sniffOn(server);
server.listen(port);

В этой конфигурации указан каталог docroot – корневой каталог для статиче-


ских файлов. Указывается, что находящийся в этом каталоге графический файл
с именем favicon.png содержит значок сайта. Иными словами, мы сконфигуриро-
вали простой веб-сервер, не обслуживающий запросы на динамически генерируе-
мые страницы.
Далее мы подключаем модуль HTTP Sniffer, так чтобы на консоль выводились
все неприглядные детали запросов, приходящих от браузера.
Перед тем как запускать сервер, подумаем, что поместить в каталог docroot.
Было бы полезно записать туда несколько HTML-файлов, чтобы было на что
смотреть. Выглядеть они могут примерно так (приведенный ниже файл можете
назвать index.html):

<html>
<head>
<link href="/style.css" rel="stylesheet">
</head>
<body>
Теоретические основы EventEmitter 107

<h1>Index</h1>
<p><a href="page2.html">page 2</a></p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam
fringilla molestie leo eu tincidunt. Donec pulvinar porttitor
dictum. Fusce at elit mauris, a ornare ipsum. Nulla congue nisi
non ante pellentesque vel lobortis lacus varius. Nam metus ante,
blandit in rutrum et, pellentesque eu velit. Nulla blandit
placerat scelerisque. Morbi odio magna, accumsan sit amet
pharetra eu, varius sit amet ipsum. Aenean interdum libero ut est
hendrerit dictum. Suspendisse convallis pellentesque metus
ac tempor. Nam diam lectus, posuere eu rutrum id, facilisis vel
tellus.
</p>
</body>

Можете создать несколько аналогичных файлов, воспользовавшись своим


любимым генератором Lorem Ipsum (см., например, http://www.lipsum.com/). Для
удобства можно связать HTML-файлы ссылками с помощью тегов <a>.
Этот HTML-файл ссылается на CSS-файл style.css, который может выгля-
деть так:

body {
color: #00c;
font-family: Verdana, Arial, Helvetica, sans-serif;
background-color: #cf9
}
H1 {
color: #ff6;
background-color: #090;
border: solid 5px #0f9
}

И наконец, создайте небольшой графический файл с именем favicon.png.


Согласно википедии (http://en.wikipedia.org/wiki/Favicon), значки сайтов долж-
ны представлять собой изображение размером 32×32 или 48×48 почти в любом
формате, и они будут отображаться в любом браузере, кроме Internet Explorer
(который настаивает на формате ICO).
Теперь запустите Basic Server:

$ node server.js

И в браузере зайдите на страницу по адресу http://localhost:4080.


Поздравляем! Вот вы и работаете с сервером Basic Server. Браузер должен
сейчас показать содержимое файла index.html, который вы поместили в каталог
docroot:
108 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты

Basic Server обладает высокой гибкостью, но его можно расширить в следую-


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

Поддержка виртуальных серверов в Basic Server


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

// Два независимых домена с различным содержимым


bs.useFavIcon("example.com", "./example.com/favicon.png");
bs.docroot("example.com", "/", "./example.com");
bs.useFavIcon("example2.com", "./example2.com/favicon.png");
bs.docroot("example2.com", "/", "./example2.com");

// Парковка одного доменного имени на другое


bs.useFavIcon("parked.com", "./example.com/favicon.png");
bs.docroot("parked.com", "/", "./example.com");

Можно запарковать домен поверх другого домена (задать конфигурацию так,


что два доменных имени ссылаются на один и тот же контейнер). Это можно реа-
лизовать также с помощью регулярных выражений:

bs.useFavIcon("parked.com|example.com",
"./example.com/favicon.png");
bs.docroot("parked.com|example.com", "/", "./example.com");
Теоретические основы EventEmitter 109

Модуль shorturl для Basic Server


Часто требуется не парковать доменные имена, а сделать так, чтобы запросы
к одному домену переадресовывались другому. Например, переадресовывать за-
просы к www. example.com домену example.com (удаляя www). Другой пример – службы
типа tinyurl.com, которые предоставляют короткий URL, переадресующий на
длинный URL.
В обоих случаях требуется послать ответ с кодом состояния 301 (Перемещен
постоянно) или 302 (Перемещен временно), содержащий заголовок Location, в ко-
тором указан конечный адрес. Это сочетание говорит браузеру о необходимости
переадресовать запрос в другое место.
Реализуем в Basic Server обработчик коротких URL, который будет посылать
ответ с кодом 302 для адресов, включенных в список. Создайте такой файл
redirector.js:

var util = require('util');


var code2url = {
'ex1': 'http://example1.com',
'ex2': 'http://example2.com',
};
var notFound = function(req, res) {
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end("no matching redirect code found for "+
req.basicServer.host +"/"+ req.basicServer.urlparsed.pathname);
}
exports.handle = function(req, res) {
if (req.basicServer.pathMatches[1]) {
var code = req.basicServer.pathMatches[1];
if (code2url[code]) {
var url = code2url[code];
res.writeHead(302, { 'Location': url });
res.end();
} else {
notFound(req, res);
}
} else {
notFound(req, res);
}
}

Это модуль обработчика для Basic Server. Для его подключения нужно доба-
вить в файл server.js такую строку (перед определением контейнера docroot):

server.addContainer(".*", "/l/(.*)$", require('./redirector'), { });

В конфигурации контейнера мы указали регулярные выражения для имени


хоста и пути. Подходит любое имя хоста, так как оно сопоставляется с образ-
110 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты
цом .*. Регулярное выражение для пути распознает любой путь, начинающийся
с /l/, и сохраняет «хвост» в запоминаемой группе (часть, заключенная в круглые
скобки).
При обращении к URL http://localhost:4080/l/code1 сведения о сопоставле-
нии с путем оказываются в массиве req.basicServer.pathMatches, причем запом-
ненная группа будет находиться в элементе req.basicServer.pathMatches[1]. Если
URL успешно сопоставлен, то обработчик вернет HTTP-ответ с кодом состояния
302, а в заголовке Location будет URL, извлеченный из объекта code2url.

Типы MIME и npm-пакет MIME


Чтобы правильно реализовать веб-сервер, нужно учесть многие детали про-
токола HTTP. Одна из таких деталей – заголовок Content-Type, взятый из специ-
фикации MIME.
Спецификация MIME первоначально была разработана в начале 1990-х го-
дов для расширения возможностей электронной почты. Протокол HTTP раз-
рабатывался примерно в то же время и был предназначен для решения схожих
задач, а именно идентифицировать формат вложения в сообщение электронной
почты или в HTTP-запрос. Расширения имени файла для этой цели недостаточ-
но, потому что трех (или около того) символов мало для создания осмысленных
идентификаторов, к тому же расширения имен не стандартизированы. Поэтому
в качестве обобщенной системы обозначения типов данных был предложен за-
головок Content-Type и целый стандарт на типы MIME. Это решение оказалось
пригодным как для электронной почты, так и для HTTP.
Но оставим в стороне историю и просто постулируем, что заголовок Content-
Type обязателен. Вопрос в том, откуда приложение знает, каким должно быть
значение этого заголовка? В некоторых случаях это известно точно, потому что
приложение посылает заведомо известные ему объекты. Так обычно бывает в не-
больших приложениях.
Однако модуль staticHandler позволяет отправлять любой файл и в общем
случае ничего не знает о том, каким должен быть Content-Type. Можно было бы
зашить в его код список известных расширений и соответствующих им заголовков
Content-Type, но, как мы уже сказали, это решение неэффективно. Рекомендуемый
подход состоит в использовании внешнего конфигурационного файла, который
обычно входит в состав операционной системы.
Npm-пакет MIME пользуется файлом mime.types, который является частью
Apache и содержит свыше 600 описаний Content-Type. Модуль mime также позво-
ляет добавлять собственные типы MIME на случай, если потребуется поддержать
что-то необычное.
Установите этот модуль:

$ npm install mime

А затем добавьте в код такие строки:


Теоретические основы EventEmitter 111

var mime = require('mime');


var mimeType = mime.lookup('image.gif'); // ==> image/gif
res.setHeader('Content-Type', mimeType);

Стоит подумать также о поддержке следующих HTTP-заголовков (http://www.


w3.org/Protocols/ и http://en.wikipedia.org/wiki/List_of_HTTP_header_fields).
‰ Content-Encoding: обозначает способ кодирования данных, например gzip.
‰ Content-Language: язык содержимого.
‰ Content-Length: длина содержимого в байтах.
‰ Content-Location: альтернативное местоположение, из которого следует
получать данные.
‰ Content-MD5: MD5-свертка содержимого.

Обработка куков
Еще одна важная функция – поддержка куков. В протоколе HTTP нет поня-
тия состояния, то есть веб-сервер не может сказать, что два запроса поступили от
одного и того же клиента. Но как же тогда «войти» на сайт? Обычно сервер по-
сылает браузеру кук, идентифицирующий зашедшего посетителя. А браузер воз-
вращает полученный кук вместе с каждым запросом к этому серверу.
В Basic Server включена частичная поддержка распознавания куков, посы-
лаемых браузером. Обработчик запроса просматривает все заголовки в объекте
req и запоминает обнаруженные заголовки Cookie в массиве (http://en.wikipedia.
org/wiki/HTTP_Cookie).
Таким образом, мы знаем, какие куки послал браузер, и осталось только разо-
брать hval и извлечь значение кука:

var keys = Object.keys(req.headers);


for (var i = 0, l = keys.length; i < l; i++) {
var hname = keys[i];
var hval = req.headers[hname];
if (hname.toLowerCase() === "cookie") {
req.basicServer.cookies.push(hval);
}
}

Чтобы отправить кук, нужно задать заголовок Set-Cookie или Set-Cookie2:

res.setHeader('Set-Cookie2', .. cookie value ..);

Кук представляет собой структурированную текстовую строку, поэтому ее раз-


бор и форматирование естественно включить в такой каркас, как Basic Server (или
Connect). Для этой цели уже существует несколько готовых библиотек, например:
‰ https://github.com/jed/cookies/: обеспечивает достаточно полную обра-
ботку и контроль куков, в том числе подписанных;
112 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты
‰ https://github.com/bmeck/node-cookiejar: простая библиотека для разбора
куков.

Виртуальные серверы и маршрутизация запросов


Виртуальный хостинг – это способ организации нескольких доменных имен
на одном IP-адресе. Как видно на примере Basic Server, Node способна поддержать
виртуальный хостинг по именам.
Для этого HTTP-запрос должен содержать заголовок Host с доменным именем:

GET /path/to/request HTTP/1.1


Host: example.com

В Node объект req содержит массив headers, в который помещается в том чис-
ле и заголовок Host. Для реализации виртуального хостинга достаточно найти
указанное в запросе доменное имя в массиве headers и направить запрос соот-
ветствующему обработчику.

Отправка HTTP-запросов клиентом


Подробно рассмотрев серверный объект HTTP, отправимся на другой конец
провода. В состав Node входит объект HTTP Client, с помощью которого удобно
отправлять запросы по протоколу HTTP. Его возможностей достаточно для от-
правки любого запроса, но он не имитирует настоящий браузер, так что не строй-
те иллюзий – это не полноценный инструмент автоматизации тестирования. Но
с его помощью вы можете сами построить эмулятор браузера или любой другой
HTTP-клиент. Например, можно вызывать произвольную REST-совместимую
веб-службу.
Начнем с аналога утилит wget или curl, которые умеют отправлять HTTP-запро-
сы и показывать результаты. Создайте файл wget.js и поместите в него такой код:

var http = require('http');


var url = require('url');
var util = require('util');

var argUrl = process.argv[2];


var parsedUrl = url.parse(argUrl, true);

var options = {
host: null,
port: -1,
path: null,
method: 'GET'
};

options.host = parsedUrl.hostname;
options.port = parsedUrl.port;
options.path = parsedUrl.pathname;
Теоретические основы EventEmitter 113

if (parsedUrl.search) options.path += "?"+parsedUrl.search;


var req = http.request(options, function(res) {
util.log('STATUS: ' + res.statusCode);
util.log('HEADERS: ' + util.inspect(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
util.log('BODY: ' + chunk);
});
res.on('error', function(err) {
util.log('RESPONSE ERROR: ' + err);
});
});
req.on('error', function(err) {
util.log('REQUEST ERROR: ' + err);
});
req.end();

Запустите этот скрипт следующим образом:

$ node wget.js http://example.com


11 Apr 21:34:35 - STATUS: 302
11 Apr 21:34:35 - HEADERS: {"location":"http://www.iana.org/domains/examp le/","server
":"BigIP","connection":"close","content-length":"0"}

Видно, что пришел HTTP-ответ с кодом состояния 302 (переадресация), ко-


торый говорит браузеру о необходимости перейти по адресу http://www.iana.org/
domains/example/. И действительно, если вы наберете в браузере адрес http://
example.com, то попадете на страницу iana.org.
Задача скрипта wget.js – отправить HTTP-запрос и показать все, что пришло
в ответе.
Отправка HTTP-запроса производится методом http.request:

var http = request('http');


var options = {
host: 'example.com',
port: 80,
path: null,
method: 'GET'
};
var request = http.request(options,
function(response) { .. });

Объект options описывает состав запроса, а функция callback вызывается,


когда приходит ответ. В объекте options задаются поля host, port и path, описы-
вающие запрашиваемый URL. Поле method должно совпадать с одним из глаголов
HTTP (GET, PUT, POST и т. д.). Можно также включить массив headers, содер-
жащий заголовки, которые требуется включить в запрос. Например, вот как ука-
зывается кук:
114 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты

var options={
headers: {
'Cookie': '.. cookie value'
}
};

Объект response наследует классу EventEmitter и умеет генерировать события


data и error. Событие data происходит, когда поступают данные, а событие error –
в случае ошибки.
Объект request наследует классу WritableStream, полезному для отправки
HTTP-запросов, содержащих данные, например типа PUT или POST. Формат
данных в HTTP-запросе определяется спецификацией MIME. Например, для
HTML-форм, отправляемых методом POST, заголовок Content-Type будет иметь
значение multipart/form-data.
Чтобы отправить данные в запросе, достаточно вызвать функцию .write и пе-
редать ей данные в правильном формате. Формат данных определен протоколом
HTTP, который допускает разнообразные вариации для обеспечения необходи-
мой гибкости. Описание всех вариантов HTTP-запросов выходит за рамки данной
книги, но вы можете воспользоваться готовыми библиотеками:
‰ https://github.com/coolaj86/abstract-http-request: высокоуровневая оберт-
ка вокруг системы HTTP-запросов;
‰ https://github.com/danwrong/restler: клиентская библиотека REST;
‰ https://github.com/maxpert/Reston: клиентская библиотека REST;
‰ https://github.com/pfleidi/node-wwwdude: клиентская библиотека REST;
‰ https://github.com/cloudhead/http-console: полезная интерактивная обо-
лочка для отправки HTTP-запросов.

Резюме
В этой главе мы подробно рассмотрели следующие вопросы.
‰ Объекты EventEmitter и их роль в реализации клиентских и серверных объ-
ектов HHTP.
‰ Использование класса EventEmitter для отделения механизма получения
данных запроса от операций над ними.
‰ Отладка с помощью прослушивания всех событий объекта HTTP или дру-
гого объекта EventEmitter.
‰ Реализация HTTP-сервера.
‰ Маршрутизация входящих запросов в HTTP-сервере.
‰ Применение спецификации MIME для идентификации типа содержимого.
‰ Реализация HTTP-клиента.
Теперь, вооружившись знаниями о реализации веб-приложений на платформе
Node, мы готовы приступить к разработке полезных программ. Под этим мы пони-
маем сохранение данных и выполнение каких-то действий с данными. В следую-
щей главе мы рассмотрим несколько способов сохранения и выборки данных из
внешних хранилищ.
Глава 6. ХРАНЕНИЕ И ВЫБОРКА
ДАННЫХ
В завершение книги мы рассмотрим имеющиеся в Node методы хранения дан-
ных. Каким бы мощным ни был веб-каркас Express, без умения сохранять дан-
ные от него мало толку. Обычно данные сохраняют в какой-нибудь базе данных.
Сегодня имеются технологии баз данных на самые разные случаи – традицион-
ные хранилища на основе SQL, документо-ориентированные базы данных без
использования SQL, простые хранилища ключей и значений или веб-службы
запросов типа YQL.
В этой главе мы напишем два варианта простого веб-приложения Notes. На его
примере мы продемонстрируем основы CRUD (Create, Read, Update, Delete – соз-
дание, чтение, обновление, удаление), используя Node-модули для работы с SQL
и MongoDB.

Движки сохранения данных для Node


В Node не предусмотрена встроенная поддержка какой-нибудь системы
хранения данных, если не считать чтение и запись в файловую систему. Для работы
с системами хранения, в частности с базами данных, необходимо использовать
соответствующий модуль. На вики-сайте Node перечислено два десятка таких
модулей для работы с CouchDB, MongoDB, MySQL, Postgres, SQLite3, Memcache,
REDIS, YQL и другими системами. См. https://github.com/joyent/node/wiki/
modules#database.
В общем случае необходимо установить как сам модуль, так и его зависимости,
в том числе платформенный код клиентских библиотек конкретной СУБД. Напри-
мер, модулям для работы с MySQL необходимы сервер MySQL и соответствую-
щая клиентская библиотека.

SQLite3 – облегченная встраиваемая


база данных на основе SQL
СУБД на основе SQL необязательно подразумевает наличие тяжеловесного
сервера и высокооплачиваемых администраторов баз данных. Установить SQLite3
(http://www.sqlite.org/) очень просто – это всего лишь автономная библиотека,
компонуемая вместе с приложением, она не нуждается ни в сервере, ни в настрой-
ке и тем не менее обеспечивает полноценную работу с SQL. В проекте node-sqlite3
116 Хранение и выборка данных
(https://github.com/developmentseed/node-sqlite3) реализован интерфейс между
SQLite3 и Node.

Установка
Если имеется менеджер пакетов npm, то установка модуля не вызывает ни ма-
лейших сложностей:

$ npm install sqlite3

Для установки этого модуля необходимо, чтобы в системе уже были уста-
новлены библиотека sqlite3 и npm-модуль, содержащий платформенный (напи-
санный на C) код, реализующий интерфейс с этой библиотекой. Библиотека по
умолчанию присутствует в Mac OS X, а если в вашем любимом дистрибутиве Linux
ее нет, то достаточно воспользоваться менеджером пакетов (например, apt-get
install libsqlite3). На сайте sqlite3 (http://sqlite.org/) имеется документация
по использованию этой базы данных, командным утилитам для работы с ней и
C API.

Реализация приложения Notes


с помощью SQLite3
Чтобы научиться работать с sqlite3, мы напишем простое приложение для
ввода и отображения заметок. Впоследствии мы модифицируем его под MongoDB.
Поскольку SQLite3 основана на SQL, то схема базы данных Notes описывается
на языке SQL. Команда CREATE TABLE в файле notesdb-sqlite3.js, который рас-
сматривается в следующем разделе, выглядит так:

CREATE TABLE IF NOT EXISTS notes (


ts DATETIME,
author VARCHAR(255),
note TEXT
)

В поле ts находится временная метка, идентифицирующая заметку, в поле


author – имя автора заметки, а в поле note – ее текст.

notesdb-sqlite3.js – модуль абстрагирования базы


данных
Это библиотека интерфейса с базой данных, которая скрывает команды SQL
в одном модуле и используется во всех остальных частях приложения. Она реа-
лизует четыре столпа CRUD с помощью функций add (создание), findNoteById
(чтение), edit (обновление) и delete (удаление).
Задача этого модуля – инкапсулировать обращения к SQLite3, изолировав их
от прочих частей приложения Notes. Модуль предоставляет несколько функций
SQLite3 – облегченная встраиваемая база данных на основе SQL 117

для создания базы данных, добавления записей в таблицу, выборки всех строк из
таблицы и удаления записей. Это позволяет сделать шаг в направлении архитек-
туры модель – представление – контроллер:

var util = require('util');


var sqlite3 = require('sqlite3');
sqlite3.verbose();
var db = undefined;
exports.connect = function(callback) {
db = new sqlite3.Database("chap06.sqlite3",
sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
function(err) {
if (err) {
utils.log('FAIL on creating database ' + err);
callback(err);
} else
callback(null)
}
);
}
exports.disconnect = function(callback) {
callback(null);
}
exports.setup = function(callback) {
db.run("CREATE TABLE IF NOT EXISTS notes "+
"(ts DATETIME, author VARCHAR(255), note TEXT)",
function(err) {
if (err) {
util.log('FAIL on creating table ' + err);
callback(error);
} else
callback(null);
});
}

Это служебный код, в котором мы подгружаем необходимые модули и опреде-


ляем функции для открытия и закрытия базы данных, а также для создания таб-
лицы. Мы жестко зашили в код имя базы данных, поэтому при вызове функций
connect и setup в текущем каталоге создается файл chap06.sqlite, если его еще не
существует.
Этот модуль раскрывает точно такой же API, как модуль notesdb-mongoose.
js для работы с базой данных MongoDB, который мы рассмотрим ниже. Правда,
здесь функция disconnect по существу ничего не делает, тогда как в модуле Mon-
goose она действительно разрывает соединение с базой данных.

exports.emptyNote = { "ts": "", author: "", note: "" };


exports.add = function(author, note, callback) {
db.run("INSERT INTO notes ( ts, author, note) "+
118 Хранение и выборка данных
"VALUES ( ?, ? , ? );",
[ new Date(), author, note ],
function(error) {
if (error) {
util.log('FAIL to add ' + error);
callback(error);
} else
callback(null);
});
}

Функция add добавляет в базу данных новую запись, это прямое обращение
к SQL.
В случае SQLite3 функция .run принимает параметризованную строку, в
которой могут быть маркеры в виде вопросительных знаков. При вызове функ-
ции передается массив значений, подставляемых вместо маркеров. Такой подход
к реализации команды SQL принят во всех языках программирования. В SQLite3
предполагается, что каждому вопросительному знаку соответствует свой элемент
массива. Интерфейсная библиотека берет на себя заботу о корректном экраниро-
вании значений, подставляемых в SQL-команду.
Обратите внимание, что вызывающая программа предоставляет функцию об-
ратного вызова, с помощью которой передается информация об ошибках. Модели
неизвестно, как показывать эту информацию пользователю; она полагает, что вы-
зывающей функции лучше знать, что делать с ошибками.

exports.delete = function(ts, callback) {


db.run("DELETE FROM notes WHERE ts = ?;",
[ ts ],
function(err) {
if (err) {
util.log('FAIL to delete ' + err);
callback(err);
} else
callback(null);
});
}

Функция delete удаляет заметки из базы данных.


Обратите внимание, что для идентификации удаляемой заметки используется
временная метка, да и вообще во всем модуле именно так определяется запись,
к которой применяется операция. Поле ts инициализируется в функции add.

exports.edit = function(ts, author, note, callback) {


db.run("UPDATE notes "+
"SET ts = ?, author = ?, note = ? "+
"WHERE ts = ?",
[ ts, author, note, ts ],
SQLite3 – облегченная встраиваемая база данных на основе SQL 119

function(err) {
if (err) {
util.log('FAIL on updating table ' + err);
callback(err);
} else
callback(null);
});
}

Функция edit позволяет обновить заметку, записав в поля новые значения.


Мы пользуемся SQL-командой UPDATE с параметрами для каждого нового значе-
ния и временной метки, идентифицирующей заметку.

exports.allNotes = function(callback) {
util.log(' in allNote');
db.all("SELECT * FROM notes", callback);
}
exports.forAll = function(doEach, done) {
db.each("SELECT * FROM notes", function(err, row) {
if (err) {
util.log('FAIL to retrieve row ' + err);
done(err, null);
} else {
doEach(null, row);
}
}, done);
}

Функции allNotes и forAll дают два способа работы с множеством всех за-
меток. Функция allNotes копирует строки из базы данных в массив. Функции же
forAll передаются две функции обратного вызова: doEach, которая вызывается для
каждой строки в результирующем множестве, и done, которая вызывается после
обработки последней строки.
Понятно, что allNotes потребляет больше памяти, чем forAll, которая в каж-
дый момент времени работает всего с одной строкой.

exports.findNoteById = function(ts, callback) {


var didOne = false;
db.each("SELECT * FROM notes WHERE ts = ?",
[ ts ],
function(err, row) {
if (err) {
util.log('FAIL to retrieve row ' + err);
callback(err, null);
} else {
if (!didOne) {
callback(null, row);
didOne = true;
120 Хранение и выборка данных
}
}
});
}

Функция .findNoteById возвращает данные из заметки, идентифицируемой


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

Инициализация базы данных – setup.js


Node-модуль sqlite3 пользуется библиотеками sqlite3, поэтому неудивитель-
но, что все стандартные инструменты, поставляемые вместе с SQLite3, работают
с базой данных, созданной с помощью node-sqlite3. Например, базу данных можно
создать с помощью утилиты sqlite3, как показано на рисунке ниже:

Но можно вместо этого написать скрипт setup.js, который инициализирует


базу данных точно так же, как это делает модуль notesdb:

var util = require('util');


var async = require('async');
var notesdb = require('./notesdb-sqlite3');
// var notesdb = require('./notesdb-mongoose');
notesdb.connect(function(error) {
if (error) throw error;
});
notesdb.setup(function(error) {
if (error) {
util.log('ERROR ' + error);
throw error;
}
async.series([
function(cb) {
notesdb.add("Lorem Ipsum ",
"Cras metus. Sed aliquet risus a tortor. Integer id quam.
Morbi .. fermentum non, convallis id, sagittis at, neque.",
function(error) {
if (error) util.log('ERROR ' + error);
cb(error);
SQLite3 – облегченная встраиваемая база данных на основе SQL 121

});
}
],
function(error, results) {
if (error) util.log('ERROR ' + error);
notesdb.disconnect(function(err) { });
}
);
});

Обратите внимание, что в коде есть два обращения к функции require для
загрузки двух разных модулей notesdb, но фактически выполняется только вызов
require('notesdb-sqlite3'). Позже мы воспользуемся этим же скриптом при ра-
боте с модулем Mongoose, а поскольку API одинаковый, то для переключения
с одной СУБД на другую будет достаточно изменить лишь имя модуля.
Таким образом, мы заранее заполняем базу данных и можем повторять вы-
полнение notesdb.add сколько угодно раз. Важный момент здесь – точка, в которой
вызывается функция .disconnect. Если вызвать ее перед завершением всех опера-
ций add, то некоторые останутся невыполненными. Напомним, что эти функции
вызываются асинхронно, поэтому операции могут выполняться не в том порядке,
в котором встречаются в коде.
Модуль async применяется для правильной организации последовательности
операций add, за которыми следует disconnect. Обычно обратные вызовы выпол-
няются в фоновом режиме, и если скрипт сначала несколько раз вызовет функцию
notesdb.add, а потом notesdb.disconnect, то может случиться так, что операция раз-
рыва соединения завершится до того, как выполнены все операции добавления.
В таких случаях модуль async оказывается очень полезен. У него много разных
возможностей, в частности функция async.series позволяет выполнять функции
строго по порядку, гарантируя, что последняя будет выполнена по завершении
всех, предшествующих ей.

Отображение заметок на консоли – show.js


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

var util = require('util');


var notesdb = require('./notesdb-sqlite3');
// var notesdb = require('./notesdb-mongoose');
notesdb.connect(function(error) {
if (error) throw error;
});
notesdb.forAll(function(error, row) {
util.log('ROW: ' + util.inspect(row));
}, function(error) {
if (error) throw error;
122 Хранение и выборка данных
util.log('ALL DONE');
notesdb.disconnect(function(err) { });
});

Результат работы этого скрипта показан на рисунке ниже.

Собираем приложение Notes воедино – app.js


Итак, мы видели, как обращаться к базе данных с помощью модуля notesdb-
sqlite3.js, теперь соберем все вместе в простое веб-приложение на основе Ex-
press, рассматривая модуль notesdb-sqlite3.js как модель, а модуль app.js – как
контроллер. Для представления используем шаблоны, с которыми познакомимся
чуть ниже. Как и два предыдущих скрипта, show.js и setup.js, скрипт app.js
написан так, чтобы было легко перейти от модуля notesdb-sqlite3.js к модулю
notesdb-mongoose.js:

var util = require('util');


var url = require('url');
var express = require('express');
var nmDbEngine = 'sqlite3';
// var nmDbEngine = 'mongoose';
var notesdb = require('./notesdb-'+nmDbEngine);
var app = express.createServer();
app.use(express.logger());
app.use(express.bodyParser());
app.register('.html', require('ejs'));
app.set('views', __dirname + '/views-'+nmDbEngine);
app.set('view engine', 'ejs');

Это служебный код для загрузки необходимых модулей и настройки компо-


нентов Express.
Здесь следует остановиться на использовании переменной nmDbEngine. Она со-
держит имя движка баз данных и позволяет выбрать нужную реализацию notesdb
и правильный каталог views. То и другое зависит от используемой СУБД, а сам
скрипт app.js не изменяется:
SQLite3 – облегченная встраиваемая база данных на основе SQL 123

var parseUrlParams = function(req, res, next) {


req.urlP = url.parse(req.url, true);
next();
}
notesdb.connect(function(error) {
if (error) throw error;
});
app.on('close', function(errno) {
notesdb.disconnect(function(err) { });
});

Для управления соединением с базой данных используются функции connect


и disconnect.
Функция parseUrlParams вызывается из функций маршрутизатора для разбора
присутствующих в URL параметров запроса.

app.get('/', function(req, res) { res.redirect('/view'); });


app.get('/view', function(req, res) {
notesdb.allNotes(function(err, notes) {
if (err) {
util.log('ERROR ' + err);
throw err;
} else
res.render('viewnotes.html', {
title: "Notes ("+nmDbEngine+")", notes: notes
});
});
});

Здесь мы выводим в браузер список заметок.


Прежде всего мы переадресуем запрос к / на адрес /view, вызывая функцию res.
redirect('/view'). Страница /view служит основным интерфейсом приложения,
на нее переадресуют многие функции маршрутизатора.
Для отрисовки страницы используется шаблон viewnotes.html, который будет
рассмотрен ниже. Ему передаются две переменные: title – строка заголовка
страницы и notes – массив заметок.

app.get('/add', function(req, res) {


res.render('addedit.html', {
title: "Notes ("+nmDbEngine+")",
postpath: '/add',
note: notesdb.emptyNote
});
});
app.post('/add', function(req, res) {
notesdb.add(req.body.author, req.body.note,
function(error) {
124 Хранение и выборка данных
if (error) throw error;
res.redirect('/view');
});
});

Эти функции маршрутизируют запросы на добавление заметок в базу.


Стоит обратить внимание на две функции маршрутизатора для URL /add.
Функция app.get('/add', …) вызывается, когда пользователь нажимает кнопку
Add. В этом случае браузер посылает GET-запрос к ресурсу с URL /add. Функция
отрисовывает по шаблону addedit.html форму, в которой пользователь сможет
ввести заметку и нажать кнопку Submit.
Шаблон addedit.html, используемый в обеих операциях /add и /edit, ожидает,
что ему будет передан объект Note. Объект notesdb.emptyNote представляет пус-
тую заметку, то есть подходит для случая, когда объект Note еще не существует.
Функция app.post('/add', …) вызывается в ответ на отправку формы, ког-
да браузер посылает POST-запрос. Введенные пользователем данные передают-
ся в теле запроса, которое обрабатывается функцией промежуточного уровня
bodyParser (app.use(express.bodyParser())) и доступно с помощью свойства req.
body. Поэтому к данным можно обращаться через переменные req.body.author и
req.body.note.

app.get('/del', parseUrlParams, function(req, res) {


notesdb.delete(req.urlP.query.id,
function(error) {
if (error) throw error;
res.redirect('/view');
});
});

Это функция, которой направляются запросы на удаление заметок из базы.


Мы используем parseUrlParams как функцию промежуточного уровня, потому
что идентификатор заметки передается в строке запроса в виде параметра id. По-
этому для получения id можно сразу написать req.urlP.query.id, а не разбирать
URL самостоятельно. По завершении операции notesdb.delete мы возвращаемся
в приложение, переадресуя браузер на начальную страницу /view.

app.get('/edit', parseUrlParams, function(req, res) {


notesdb.findNoteById(req.urlP.query.id,
function(error, note) {
if (error) throw error;
res.render('addedit.html', {
title: "Notes ("+nmDbEngine+")",
postpath: '/edit',
note: note
});
});
SQLite3 – облегченная встраиваемая база данных на основе SQL 125

});
app.post('/edit', function(req, res) {
notesdb.edit(req.body.id, req.body.author, req.body.note,
function(error) {
if (error) throw error;
res.redirect('/view');
});
});
app.listen(3000);

Это функции редактирования заметок.


Мы снова используем функцию parseUrlParams для получения идентификатора
заметки из параметров запроса, а затем выбираем эту заметку из базы с помощью
функции notesdb.findNoteById. Обратите внимание, что страница отрисовывается
по тому же шаблону addedit.html, но на этот раз мы передаем в шаблон объект
Note, выбранный из базы данных.
В переменную postpath раньше записывалась строка /add, а теперь /edit.
Ее значение используется в шаблоне addedit.html для задания адреса отправки
формы. В результате будет вызвана либо функция app.post('/add',…), либо app.
post('/edit',…).

Шаблоны в приложении Notes


Перед тем как запускать приложение Notes, необходимо подготовить шаб-
лоны, на которые есть ссылки в файле app.js: viewnotes.html, addedit.html и
layout.html. Все они должны быть помещены в каталог views-sqlite3. Позже мы
создадим другой каталог, views-mongoose, для шаблонов, применяемых в версии,
работающей с модулем Mongoose.
Начнем с layout.html:

<html>
<head><title><%= title %></title></head>
<body>
<h1><%= title %></h1>
<p><a href='/view'>View</a> | <a href='/add'>Add</a></p>
<%- body %>
</body>
<html>

Это макет всех страниц приложения Notes, ничего сложного в нем нет. Для
вывода заголовка используется переменная title, передаваемая при вызовах res.
render в файле app.js.
Теперь обратимся к файлу viewnotes.html:

<table><% notes.forEach(function(note) { %>


<tr><td>
<p><%= new Date(note.ts).toString() %>:
by <b><%= note.author %></b></p>
126 Хранение и выборка данных
<p><%= note.note %></p>
</td><td>
<form method='GET' action='/del'>
<input type='submit' value='Delete' />
<input type='hidden' name='id' value='<%=
note.ts %>'>
</form>
<br/><form method='GET' action='/edit'>
<input type='submit' value='Edit' />
<input type='hidden' name='id' value='<%=
note.ts %>'>
</form>
</td></tr><% }); %>
</table>

Это версия для приложения SQLite3 Notes. В ней отображаются временная


метка, заголовок и содержимое заметки, а также две формы, позволяющие удалить
(/del) или изменить (/edit) заметку.
В форме имеется скрытое поле id, в котором в случае приложения SQLite3
Notes хранится временная метка, идентифицирующая заметку. Выше в функциях
удаления и редактирования мы разбирали URL, выделяя параметр id, который
как раз и поступает из этого скрытого поля.
Теперь рассмотрим шаблон addedit.html:

<form method='POST' action='<%= postpath %>'>


<% if (note) { %>
<input type='hidden' name='id' value='<%= note.ts %>'>
<% } %>
<input type='text' name='author' value='<%= note.author %>'/>
<br/>
<textarea rows=5 cols=40 name='note' ><%=
note.note
%></textarea>
<br/><input type='submit' value='Submit' />
</form>

Эта форма используется для добавления (/add) и редактирования (/edit)


заметок. В переменной postpath передается URL, на который отправляется форма.
Остальные значения берутся из объекта Note, передаваемого из app.js; это может
быть и объект emptyNote.

Запуск приложения SQLite3 Notes


Теперь все части встали на свои места и можно запускать приложение Notes.
Если раньше вы выполняли скрипт setup.js, то база данных уже подготовлена.
Если нет, сделайте это сейчас. Затем выполните такую команду:

$ node app
SQLite3 – облегченная встраиваемая база данных на основе SQL 127

Поскольку мы прослушиваем порт 3000 (app.listen(3000)), то заходить в прило-


жение нужно по адресу http://localhost:3000/. Вы должны увидеть такую страницу:

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


рядом с которой находилась нажатая кнопка, уже не будет. Обновление произо-
шло потому, что функция app.get('/del'…) вызывает notesdb.delete, а затем сразу
переадресует браузер на страницу /view.
Теперь можете либо добавить заметку (щелчок по ссылке Add), либо изменить
(щелчок по кнопке Edit). На экране появится такая страница:

При нажатии кнопки Submit вызывается либо app.post('/add',…), либо app.


post('/ edit',…), и обе функции обновляют базу данных и переадресуют браузер
на страницу /view.
128 Хранение и выборка данных

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


Если в программе имеется дефект или возникла какая-то проблема, то прило-
жение возбудит исключение. Отладка заключается в том, чтобы вывести информа-
цию о том, когда и где произошла ошибка. В приложениях для Notes мы использо-
вали для вывода такой информации функцию util.log. В модуле notesdb-sqlite3.
js для отправки сведений о возникающих ошибках приложению app.js применя-
ются функции обратного вызова.
По умолчанию Express возвращает браузеру удобную для разработчика стра-
ницу с трассировкой стека.

Для перехвата исключений, возникающих при вызове функции маршрутиза-


ции или внутри next(), Express предоставляет функцию app.error.
Если вы хотите изучить это поведение, то можете сознательно сделать какую-
нибудь ошибку в коде, например попытаться вызвать метод объекта null:

app.get('/del', parseUrlParams, function(req, res) {


var notAllowed = null;
notAllowed.delete();

});

Однако показанная выше страница с информацией об ошибке полезна только


разработчику, но никак не пользователю. Попытаемся улучшить ее. Один из спо-
собов – включить в app.js такой код:

app.use(express.errorHandler({ dumpExceptions: true }));

Тогда в окне браузера появится сообщение «Internal Server Error». Оно уже
не так раздражает пользователя, но все же оставляет желать лучшего. При этом
интересная разработчику трассировка стека по-прежнему печатается на stderr, не
обременяя пользователя ненужными деталями.
Чтобы разработать по-настоящему дружелюбную к пользователю страницу
ошибки, нужно написать подходящую функцию app.error, начав, например, с такой:
SQLite3 – облегченная встраиваемая база данных на основе SQL 129

app.error(function(err, req, res) {


res.render('500.html', {
title: "Notes ("+nmDbEngine+") ERROR", error: err
});
});

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


в зависимости от вида объекта ошибки или нарисовать картинку с изображением
птиц, поднимающих кита из океана. Решение за вами, а мы для демонстрации
остановимся на таком шаблоне 500.html:

<b>Internal Server Error</b>


ERROR: <%= error %>

В этом случае пользователь увидит в браузере страницу:

Использование других СУБД на основе SQL


на платформе Node
SQLite3 ни в коем случае не является единственной и неповторимой базой
данных на основе SQL. Мы выбрали ее только за простоту настройки. Исполь-
зовать SQLite3 имеет смысл лишь в случае, когда база данных может находиться
на одном компьютере с приложением. У других СУБД есть куда более развитые
возможности, в частности распределенный доступ к данным, высокая пропускная
способность, зеркалирование и многое другое.
Перечислим некоторые другие модули низкого уровня (близкие к SQL).
‰ Node-mysql (https://github.com/felixge/node-mysql) – реализация прото-
кола взаимодействия с MySQL для Node на чистом JavaScript.
‰ Node-mysql-native (https://github.com/sidorares/nodejs-mysql-native) –
обертка вокруг «родной» клиентской библиотеки MySQL в виде Node-
модуля.
‰ Node-mysql-libmysqlclient (https://github.com/Sannis/node-mysql-
libmysqlclient) – интерфейс к MySQL для Node на основе библиотеки lib-
mysqlclient.
130 Хранение и выборка данных
‰ Node-postgres (https://github.com/brianc/node-postgres) – тщательно про-
тестированный Node-клиент для СУБД Postgres. Имеются реализация на
чистом JavaScript и интерфейс с «родной» библиотекой.
‰ Node-sqlite3 (https://github.com/developmentseed/node-sqlite3) – асин-
хронный неблокирующий интерфейс с SQLite3 для Node.
‰ Node-DBI (https://github.com/DrBenton/Node-DBI) – слой абстрагирования
баз данных на основе SQL.
А это перечень модулей более высокого уровня (со средствами объектно-реля-
ционного отображения – ORM):
‰ FastLegS (https://github.com/didit-tech/FastLegS) – ORM для Post-
greSQL, построенная поверх node-postgres.
‰ Node-orm (https://github.com/dresende/node-orm) – система ORM, рассчи-
танная на работу с несколькими СУБД.
‰ persistence.js (https://github.com/zefhemel/persistencejs) – написанная на
JavaScript библиотека асинхронного объектно-реляционного отображения,
пригодная для использования в браузере и на Node-сервере.
‰ Sequelize (https://github.com/sdepold/sequelize) – система ORM для Node
и MySQL.

Mongoose – интерфейс между Node


и MongoDB
MongoDB – одна из лидирующих «nosql» СУБД (nosql означает, что она
не основана на языке SQL). В описании говорится, что это «масштабируемая,
высокопроизводительная, документо-ориентированная СУБД с открытым исход-
ным кодом». Она позволяет хранить документы в формате, близком к JSON, без
строго определенной схемы, и обладает целым рядом передовых возможностей.
Дополнительные сведения и документацию можно найти на сайте проекта http://
www.mongodb.org/.
Mongoose – один из нескольких модулей для доступа к MongoDB, представ-
ляющий собой средство объектного моделирования, то есть ваша программа опре-
деляет объекты Schema, описывающие данные, а Mongoose берет на себя заботу об
их сохранении в MongoDB. Это чрезвычайно мощный инструмент, обладающий
такими средствами, как встраиваемые документы, гибкая система типизации
полей, контроль ввода полей, виртуальные поля и т. д. См. http://mongoosejs.
com/.

Установка Mongoose
Если уже установлен менеджер пакетов npm, то достаточно выполнить такую
команду:

$ npm install mongoose


Mongoose – интерфейс между Node и MongoDB 131

Перед тем как использовать Mongoose, необходимо установить и запустить


экземпляр MongoDB. На сайте mongodb.com есть много готовых двоичных пакетов.
Кроме того, они входят в большинство дистрибутивов Linux. Для Mac OS X пакет
доступен через систему MacPorts. Дополнительные сведения можно найти на
сайте проекта, особенно в кратком руководстве Quickstart для интересующей вас
операционной системы (http://www.mongodb.org/display/DOCS/Quickstart).
Чтобы проверить работоспособность MongoDB, нужно проделать двухшаговую
процедуру. Сначала запустите сервер MongoDB (mongod), указав локальный ката-
лог данных, как показано на рисунке ниже:

Это полезно на этапе разработки. В любой момент процесс можно снять на-
жатием Ctrl+C и с помощью показанных выше команд перезапустить с чистым
каталогом данных.
Следующий шаг – убедиться, что MongoDB может работать в интерактивном
режиме, описанном в кратком руководстве (см. ссылку выше):

Здесь мы вставили документ ({ a: 1 } в нотации JSON) в коллекцию с именем


foo. Команда db.foo.find служит для опроса коллекции foo, и поскольку никаких
параметров в запросе не задано, то возвращает все элементы, которые печатаются
в нотации JSON. На сайте MongoDB имеется полная документация по работе
с этой базой данных, в том числе и с оболочкой Mongo.

Реализация приложения Notes


с помощью Mongoose
Для изучения Mongoose мы реализуем еще один вариант приложения Notes.
Мы будем использовать схему, похожую на предыдущую, но запишем ее в объект-
ной нотации Mongoose:
132 Хранение и выборка данных

var NoteSchema = new Schema({


ts : { type: Date, default: Date.now },
author : String,
note : String
});
mongoose.model('Note', NoteSchema);

Назначение полей такое же, как в SQL-схеме. В качестве типов данных ука-
заны типы JavaScript, потому что именно с ними работает Mongoose. Для поля
ts задано значение по умолчанию на случай, если при создании объекта значение
временной метки не указано явно.
Теперь займемся кодированием.

notesdb-mongoose.js – модуль абстрагирования


базы данных
Как и в случае приложения Notes с использованием модуля sqlite3, это биб-
лиотека для организации интерфейса с базой данных; она будет использоваться
в остальных частях приложения. В ней реализованы все четыре столпа CRUD:
функции add (создание), findNoteById (чтение), edit (обновление) и delete (уда-
ление).
Как и в предыдущем варианте, модуль notesdb-mongoose.js соответствует час-
ти «модель» архитектуры модель – представление – контроллер.

var util = require('util');


var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var dburl = 'mongodb://localhost/chap06';
exports.connect = function(callback) {
mongoose.connect(dburl);
}
exports.disconnect = function(callback) {
mongoose.disconnect(callback);
}

Это служебный код, в котором мы подгружаем необходимые модули и опре-


деляем функции .connect и .disconnect. Переменная dburl необходима для соеди-
нения с работающей СУБД MongoDB. Предполагается, что любая программа вы-
зывает .connect в начале работы и .disconnect – перед завершением.

exports.setup = function(callback) { callback(null); }


var NoteSchema = new Schema({
ts : { type: Date, default: Date.now },
author : String,
note : String
});
mongoose.model('Note', NoteSchema);
var Note = mongoose.model('Note');
exports.emptyNote = { "_id": "", author: "", note: "" };
Mongoose – интерфейс между Node и MongoDB 133

Здесь определяется схема, мы уже говорили о ней выше. Схема создается


в предложении var NoteSchema = new Schema(...), а затем регистрируется в моде-
ли Mongoose:

mongoose.model('Note', NoteSchema);
var Note = mongoose.model('Note');

Зарегистрировав схему, программа может приступить к созданию документов


в базе:

exports.add = function(author, note, callback) {


var newNote = new Note();
newNote.author = author;
newNote.note = note;
newNote.save(function(err) {
if (err) {
util.log('FATAL '+ err);
callback(err);
} else
callback(null);
});
}

В Mongoose для этого следует создать новый экземпляр объекта, записать дан-
ные в его поля и вызвать метод .save. В данном случае полю ts не присвоено зна-
чение, но зато в схеме для него объявлено значение по умолчанию.

exports.delete = function(id, callback) {


exports.findNoteById(id, function(err, doc) {
if (err)
callback(err);
else {
util.log(util.inspect(doc));
doc.remove();
callback(null);
}
});
}

Процедура удаления заметки из базы состоит из двух шагов. Сначала мы из-


влекаем заметку из базы с помощью функции findNoteById (см. ниже), а затем вы-
зываем метод .remove полученного объекта.

exports.edit = function(id, author, note, callback) {


exports.findNoteById(id, function(err, doc) {
if (err)
callback(err);
else {
134 Хранение и выборка данных
doc.ts = new Date();
doc.author = author;
doc.note = note;
doc.save(function(err) {
if (err) {
util.log('FATAL '+ err);
callback(err);
} else
callback(null);
});
}
});
}

Обновление заметки также состоит из двух шагов: извлечь объект из базы и


записать в его поля новые значения, а затем вызвать метод .save:

exports.allNotes = function(callback) {
Note.find({}, callback);
}
exports.forAll = function(doEach, done) {
Note.find({}, function(err, docs) {
if (err) {
util.log('FATAL '+ err);
done(err, null);
}
docs.forEach(function(doc) {
doEach(null, doc);
});
done(null);
});
}
var findNoteById = exports.findNoteById = function(id,
callback) {
Note.findOne({ _id: id }, function(err, doc) {
if (err) {
util.log('FATAL '+ err);
call back(err, null);
}
callback(null, doc);
});
}

Здесь представлены все три функции для выборки заметок из базы данных.
В функциях allNotes и forAll используется метод Notes.find. Этот метод
и скрытый за кулисами объект Query – важная составная часть Mongoose. По
существу, это аналог фразы WHERE в SQL-команде SELECT, но синтаксис понятнее и
проще воспринимается. В обеих функциях объект Query пуст, поэтому Mongoose
выбирает все документы.
Mongoose – интерфейс между Node и MongoDB 135

В функции .findNoteById вызывается Note.findOne для поиска заметки, иден-


тифицируемой полем _id. Для этого мы передаем в качестве Query объект { _id:
id }, чтобы модуль нашел документы, в которых значение поля _id равно id. Mon-
goDB присваивает каждому хранимому документу гарантированно уникальный
идентификатор, сохраняемый в поле _id. Его можно использовать для той же цели,
для которой мы использовали поле ts в варианте приложения Notes для SQLite3,
то есть для однозначной идентификации заметок. Объект Query в Mongoose умеет
делать и многое другое, о чем можно прочитать на сайте mongoosejs.org.

Инициализация базы данных – setup.js


Как и в случае SQLite3, инициализировать базу данных можно двумя спосо-
бами. Первый – воспользоваться командами оболочки mongo, как показано на ри-
сунке ниже:

Второй – выполнить написанный ранее скрипт setup.js. В нем есть две строки,
выбирающие, какой модуль использовать: notesdb-sqlite3 или notesdb-mongoose.

// var notesdb = require('./notesdb-sqlite3');


var notesdb = require('./notesdb-mongoose');

Перенесите комментарий с одной строки на другую, а затем выполните такую


команду:

$ node setup

На консоль ничего не выводится, но можете воспользоваться скриптом show.


js и посмотреть, что теперь находится в базе данных.

Отображение заметок на консоли – show.js


Для отображения всех документов в базе данных используется написанный
ранее скрипт show.js. Внесите в него такие же изменения, как в setup.js, и запус-
тите:

$ node show
7 Jul 17:20:58 - ROW: { doc:
136 Хранение и выборка данных
{ ts: Fri, 08 Jul 2011 00:13:22 GMT,
_id: 4e164ba289dc189149000001,
note: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Integer nec odio. Praesent .. Sed dignissim lacinia nunc.',
author: 'Lorem Ipsum 12' },
activePaths:
{ paths:
{ note: 'init',
author: 'init',
_id: 'init',
ts: 'init' },
states: { init: [Object], modify: {}, require: {} },
stateNames: [ 'require', 'modify', 'init' ] },
saveError: null,
isNew: false,
pres: { save: { serial: [Object], parallel: [] } },
errors: undefined }

Собираем приложение воедино – app.js


Поскольку модуль notesdb-mongoose.js раскрывает такой же API, как notesdb-
sqlite.js, мы смогли повторно использовать скрипты setup.js и show.js с ми-
нимальными модификациями. Это относится и к скрипту app.js. Модификация
выглядит несколько иначе, но преследует ту же цель. Однако из-за некоторых
различий нам придется изменить файлы шаблонов.
Внесите в app.js такое изменение:

// var nmDbEngine = 'sqlite3';


var nmDbEngine = 'mongoose';

Затем создайте каталог views-mongoose и поместите в него файлы шаблонов.


1. Шаблон layout.html ничем не отличается от старого, поэтому просто ско-
пируйте его:
$ cp views-sqlite3/layout.html views-mongoose/layout.html
2. Шаблон viewnotes.html отличается от старого только в строке, где опреде-
ляется скрытый идентификатор. Она должна теперь выглядеть так:
<input type='hidden' name='id' value='<%= note._id %>'>
3. Аналогично скопируйте addedit.html и измените строку со скрытым иден-
тификатором:
<input type='hidden' name='id' value='<%= note._id %>'>

Различие в описании поля скрытого идентификатора обусловлено тем, что


MongoDB автоматически присваивает каждому хранимому документу уникаль-
ный идентификатор _id.
Теперь все готово и можно запускать приложение Mongoose Notes:
Mongoose – интерфейс между Node и MongoDB 137

$ node app

Как и прежде, зайдите из браузера на страницу http://localhost:3000/. Вы-


глядит она почти так же, как вариант для SQLite3, только заголовок отличается:

Этот вариант приложения Notes ведет себя точно так же, как предыдущий. Та-
ким образом, мы продемонстрировали работу с хранилищами данных на основе
SQL и MongoDB.

Другие продукты, поддерживающие


MongoDB
Поддержка MongoDB в Node не ограничивается модулем Mongoose. Среди
прочего вас может удивить различие между API, предоставляемыми оболочкой
MongoDB и модулем Node MongoDB. Поскольку в оболочке MongoDB использу-
ется командный интерпретатор JavaScript, то можно было бы ожидать, что API бу-
дут одинаковы. Но, несмотря на то что в описаниях многих модулей декларируется
сходство с оболочкой MongoDB, ни в одном не применяется точно такой же API.
‰ Node-mongodb (https://github.com/orlandov/node-mongodb) – эксперимен-
тальный асинхронный интерфейс между Node и MongoDB.
‰ node-mongodb-native (https://github.com/christkv/node-mongodb-native) –
еще один драйвер.
‰ node-mongolian (https://github.com/marcello3d/node-mongolian) – «преис-
полненный благоговения» драйвер, который «пытается максимально точно
воспроизвести оболочку MongoDB».
‰ Mongolia (https://github.com/masylum/mongolia) – гибкий «немагический»
слой над MongoDB, но не ORM.
138 Хранение и выборка данных
‰ Mongoose (http://www.learnboost.com/mongoose/) – модуль, с которым мы
только что работали, ORM поверх MongoDB.
‰ Mongous (https://github.com/amark/mongous) – «чертовски простой» ин-
терфейс к MongoDB с синтаксисом в духе jQuery.
‰ node-nosql-thin (https://github.com/dmcquay/node-nosql-thin) – библио-
тека «тонкого» интерфейса к MongoDB, которая впоследствии, возможно,
будет поддерживать и другие СУБД «без SQL».

Краткий обзор средств


аутентификации пользователей
Во многих приложениях пользователи должны аутентифицироваться для
доступа к привилегированным средствам. Поскольку HTTP – протокол без под-
держки состояния, единственный способ аутентифицировать пользователя за-
ключается в том, чтобы отправить кук его браузеру после выполнения некоего
действия, подтверждающего подлинность пользователя. Кук должен содержать
данные, с помощью которых приложение сможет впоследствии проверить, кто им
пользуется. Мы кратко рассмотрим, как можно реализовать форму входа, послать
браузеру кук и предотвратить доступ в случае отсутствия кука.
Нам потребуется внести два изменения в скрипт app.js. Сначала добавим
в конфигурацию серверного объекта ПУ-модуль cookieParser:

var app = express.createServer();


app.use(express.logger());
app.use(express.cookieParser());
app.use(express.bodyParser());

Затем добавим небольшую функцию маршрутизатора, проверяющую, что


пользователю разрешен доступ. В данном случае мы просто проверяем, что значе-
ние кука равно AOK, считая, что это универсальный признак «все хорошо»:

var checkAccess = function(req, res, next) {


if (!req.cookies
|| !req.cookies.notesaccess
|| req.cookies.notesaccess !== "AOK") {
res.redirect('/login');
} else {
next();
}
}

ПУ-модуль cookieParser проделывает всю грязную работу: ищет куки, разби-


рает их и помещает значения в объект req. Если кук получен, то его значение будет
присутствовать в массиве req.cookies, и мы сможем его проверить. Если куков нет
вообще, или отсутствует кук notesaccess, или его значение не равно AOK, то браузер
переадресует на URL-адрес /login.
Краткий обзор средств аутентификации пользователей 139

Перед тем как рассматривать обработчик URL /login, добавим ПУ-модуль


cookieParser во все маршруты приложения Notes. Это просто:

app.get('/view', checkAccess, function(req, res) {



});

Точнее, мы добавляем обращение к checkAccess в определение каждой функции


маршрутизатора. Тем самым гарантируется, что checkAccess будет вызываться для
каждого URL приложения Notes и, значит, все его URL защищены. Если какой-то
URL защищать не нужно, то в соответствующий маршрут функция checkAccess не
добавляется. Следующие две функции обрабатывают URL /login:

app.get('/login', function(req, res) {


res.render('login.html', {
title: "Notes LOGIN ("+nmDbEngine+")",
});
});
app.post('/login', function(req, res) {
// СДЕЛАТЬ! Проверять введенные в форму учетные данные
res.cookie('notesaccess', 'AOK');
res.redirect('/view');
});

И наконец, добавим шаблон login.html:

<form method='POST' action='/login'>


<p>Click the <i>Login</i> to log in.</p>
<input type='submit' value='Login' />
</form>

Если вы собираетесь реализовать настоящую систему защиты, то надо будет


сделать еще несколько вещей.
Когда функция checkAccess переадресует браузер на страницу /login, первая
функция маршрутизатора отрисовывает шаблон, показанный на рисунке ниже:

В реальной системе нужно было бы, как минимум, включить поля для ввода
имени и пароля пользователя. Мы же опустили эту деталь и просто предлагаем
пользователю нажать кнопку Login.
140 Хранение и выборка данных
Эта кнопка находится внутри формы, отправка которой приводит к вызову
функции маршрутизатора app.post('/login'…). В реальной системе эта функция
должна была бы проверить полученные учетные данные и отправить аутенти-
фикационный кук лишь в случае, когда в базе данных есть такой пользователь.
Мы же отправляем кук со значением AOK безусловно и переадресуем браузер на
страницу /view.
Хотя нашему приложению недостает некоторых существенных черт реальной
системы защиты, общий механизм отражен верно. Существует много сайтов, на
которых имеется форма входа и которые проверяют аутентификационный кук
при запросе любой страницы. Мы реализовали функции, которые проверяют
наличие кука, переадресуют пользователя на страницу входа, проверяют учетные
данные и посылают аутентификационный кук браузеру.

Резюме
В этой главе мы многое узнали о работе с данными в Node. Так как это неотъ-
емлемая черта многих приложений, резюмируем, чему мы научились.
‰ В состав Node не входит встроенная поддержка хранения данных, но со-
общество Node разработало модули для организации интерфейса с самыми
разными системами хранения, в том числе такими, о которых вы, возмож-
но, даже не подозревали.
‰ Установка движка хранения данных обычно сопровождается установкой
некоторых зависимостей, в том числе серверов и клиентских библиотек.
‰ SQLite3 – СУБД для разработки приложений на основе SQL, не требую-
щая никакой настройки.
‰ Приложения, работающие с СУБД на основе SQL и с MongoDB, почти ни-
чем не отличаются.
‰ Систему объектно-реляционного отображения, пожалуй, лучше использо-
вать поверх СУБД на основе SQL, но сообщество разработало такие систе-
мы и для MongoDB и CouchDB.
‰ Реализация архитектуры модель – представление – контроллер (не пол-
ностью).
‰ Обработка форм в приложении на основе Express.
‰ Документо-ориентированные системы типа MongoDB ближе к современ-
ным языкам программирования и приложениям, чем SQL.
Мы проделали длинный путь. Начали мы с общего описания платформы Node
и тех программ, которые можно на ней реализовать. Затем мы узнали, как устано-
вить Node и npm для разработки и в производственной среде. После этого мы раз-
работали несколько модулей и приложений для Node, чтобы понять, как строятся
веб-приложения и клиент-серверные приложения на базе протокола HTTP, как
устроен цикл обработки событий в Node, как преобразовать алгоритм, включаю-
щий продолжительные вычисления, в пригодный для архитектуры с циклом об-
работки событий, как делегировать часть работы фоновым процессам с помощью
веб-служб и как работать с базами данных в приложении для Node.
Предметный указатель
A EventEmitter, объект
addedit.html, шаблон, 124, 126 обзор, 95
add, функция, 116, 118 события, 97, 98
allNotes функция, 119 Express, 47, 65
app-connect.js, 81 реализация Math Wizard, 82
app.js, 122, 125 Express Math Wizard
app-node.js, 66, 72 математический сервер, 89
apt-get, инструмент, 50 обработка ошибок, 87
параметризованные URL-адреса, 88
B реализация, 82
Basic Server службы данных, 88
виртуальный хостинг, 112
возможности, 108
F
factorial-node.js, 70
конфигурирование виртуальных серверов, 108
FastLegS, 130
модуль shorturl, 109
favicon, 80, 99
настройка, 105, 108
обработчик, 104
обработка favicon, 104
faviconHandler.js, 104
обработка куков, 111
fibo-node.js, 71
обработчик статических файлов, 105
findNoteById, функция, 116, 120
общее описание, 100
forAll, функция, 119
реализация, 101
forever, 37
ядро, 101
forms, модуль, 48
basicserver.js, файл, 101
fs, модуль, 43
bin, поле, 51
fugue, 37
C H
Cluster, 40
handle, функция, 105
CommonJS, система организации
home-node.js, 72
модулей, 19, 44, 64
HTTPClient, объект, 95
Connect, 18, 65, 77
создание запросов, 112
настройка серверного объекта, 80
http.createServer, метод, 22
принципы работы, 80, 81
HTTPServer, объект, 66, 95
реализация Math Wizard, 78
события, 99
установка, 79
HTTP Sniffer, 98
connect, функция, 123
httpsniffer.js, 98
Content-Type, заголовок, 110
HTTP-заголовки, 111
CouchDB, 115
HTTP, модули, 17
CPAN, 50
http, объект, 22
createServer, функция, 102
HTTP, протокол, 111
CREATE TABLE, команда, 116
htutil.loadParams, 67
CRUD (Create, Read, Update, Delete), 115
htutil.navbar, 73
curl, утилита, 112
htutil.page, 69
D J
delete, функция, 116, 118
JavaScript, 18
directories, поле, 51
недостатки, 19
disconnect, функция, 117, 123
dispatchToContainer, функция, 102 L
docroot, параметр, 106 launchd, скрипты для, 37
doEach, функция, 119 logger, ПУ-модуль, 80
done, функция, 119
M
E MacPorts, проект, 50
edit, функция, 116, 119 math.js, 72
142 Предметный указатель
Math Wizard Node-mysql-native, 129
URL-адреса, 66 node-nosql-thin, 138
макет страницы, 65 Node-orm, 130
математические функции, вычисление, 70 NODE_PATH, переменная окружения, 48
обобщение, 73 Node-postgres, 130
обработка параметров запроса, 67 Node-sqlite3, 130
переработка, 92 notesdb-mongoose.js, 132
продолжительные вычисления, 74 notesdb-sqlite3.js, 116
реализация с помощью Connect, 79 Notes, приложение, 122
реализация с помощью Express, 82 реализация помощью Mongoose, 131
реализация с помощью node, 66 реализация помощью SQLite3, 116
создание, 66 шаблоны, 125
Memcache, 115 npm
MIME adduser, команда, 60
npm-пакет, 110 help, команда, 53
модуль, 100 init, команда, 59
спецификация, 110 install, команда, 59
MongoDB, 115, 130 list, команда, 56, 61
поддержка, 137 publish, команда, 60
Mongolia, 137 unpublish, команда, 60
Mongoose, 130, 138 версии пакетов, 61
app.js, 136 использование установленных пакетов, 55
модуль абстрагирования базы данных, 132 конфигурационные параметры, 60
отображение заметок на консоли, 135 обновление пакета, 58
реализация приложения Notes, 131 описание, 50
установка, 130 перечень установленных пакетов, 56
Mongous, 138 редактирование установленного пакета, 57
mult-node.js, 69 скрипты в составе пакета, 57
MySQL, 115 установка, 35
npm-пакеты, 50
N версии, 61
Node, 16 зависимости, 62
Math Wizard, 66 поиск, 52
асинхронная событийно-ориентированная просмотр информации, 54
архитектура, 19 публикация, 60
возможности, 17 разработка, 58
движки сохранения данных, 115 установка, 54
достоинства, 18 формат, 50
запуск простого скрипта, 33
запуск сервера, 34 O
запуск серверов на этапе инициализации, 36 options, объект, 102
использование процессора, 21
количество серверов, 23 P
командные утилиты, 31 package.json, файл, 49
маршрутизация запросов, 66 parseUrlParams, функция, 123
многоядерные системы, 40 PEAR, 50
модель, 16 persistence.js, 130
модули, 43 Postgres, 115
настройка, 25 postpath, переменная, 125
проверка установки, 31 processHeaders, функция, 102
производительность, 21 process.nextTick, функция, 75
серверный JavaScript, 18 Pulser, класс, 96
сетевой уровень, 17 pulse, событие, 96
средства разработки в Mac OS X, 26 Q
установка в POSIX-совместимых системах, 26 qs, модуль, 47
установка нескольких экземпляров, 31
экологичный Интернет, 23 R
Node-DBI, 130 REDIS, 115
node_modules, каталог, 45, 46 replace, функция, 69
Node-mongodb, 137 Request, объект, 97
node-mongodb-native, 137 require, функция, 43
node-mongolian, 137 req, объект, 88, 138
Node-mysql, 129 RingoJS, каркас разработки приложений, 18
Node-mysql-libmysqlclient, 129 router, ПУ-модуль, 80
Предметный указатель 143
rpm, инструмент, 50 И
run, функция, 118 Инкапсуляция, пример, 44
S К
Sequelize, 130 Командные утилиты Node, 31
setup.js, 120, 135 Контейнеры, 102
shorturl, модуль, 109 Конфигурирование виртуальных серверов, 108
show.js, 121 Куки, 111
sniffOn, функция, 99
SQLite3, 115 Л
app.js, 122 Локальные модули, 45
запуск приложения Notes, 126
инициализация базы данных, 120
М
модуль абстрагирования базы данных, 116 Математические функции, вычисление, 70
обработка ошибок, 128 Многоядерные системы, 40
отображение заметок на консоли, 121 Модуль
реализация приложения Notes, 116 идентификаторы, 44
установка, 116 инкапсуляция, 63
шаблоны приложения Notes, 125 локальный, 45
square-node.js, 70 определение, 43
start, метод, 96 пример, 43
staticHandler.js, 105 системный, 48
static, ПУ-модуль, 80 составной, 49
Swing, 17 Модуль абстрагирования базы данных,
SQLite3, 116
T
tgz-архивы, 51 О
Обработчик статических файлов, 105
U Отрисовка страницы, 123
UPDATE, SQL-команда, 119
Ошибки, обработка в Express Math Wizard, 87
Upstart, 37
V П
Переработка алгоритма, 74
V8, движок, 17
ПО промежуточного уровня, 78
W поставщики, 78
wget, утилита, 112 фильтры, 78
Поставщики, 78
Y Потоки, сравнение с асинхронной событийно-
YQL, 115 ориентированной архитектурой, 20
YSlow, 74 Приложение, внешние зависимости, 46
yum, инструмент, 50
Р
А Разработка средства, 26
Абсолютные идентификаторы модуля, 45 установка в Linux с помощью систем
Асинхронная событийно-ориентированная управления пакетами, 30
архитектура установка в Mac OS X с помощью homebrew, 30
обзор, 19 установка в Mac OS X с помощью MacPorts, 29
сравнение с многопоточной, 20 установка в домашний каталог, 27
Аутентификация пользователей, 138 установка в системный каталог, 29
Б С
База данных
Семантическая версионность, 61
инициализация, 120
Системные модули, 48
отображение на консоли, 121
Составные модули, 49
управление соединением, 123
Блокирующий ввод/вывод, 20 У
Установка
В
Connect, 79
Веб-каркасы, 65
Mongoose, 130
Виртуальный хостинг, 112
npm-пакета, 54
Внешние зависимости, 46
SQLite3, 116
Г
Глобальный Объект, 19, 64
Ф
Фибоначчи числа, 71
Д Фильтры, 78
Движки сохранения данных, 115 Функции маршрутизатора, 138
Книги издательства «ДМК Пресс» можно заказать в торгово-издатель-
ском холдинге «АЛЬЯНС-КНИГА» наложенным платежом, выслав от-
крытку или письмо по почтовому адресу: 123242, Москва, а/я 20 или по
электронному адресу: orders@alians-kniga.ru.
При оформлении заказа следует указать адрес (полностью), по которо-
му должны быть высланы книги; фамилию, имя и отчество получателя. Же-
лательно также указать свой телефон и электронный адрес.
Эти книги вы можете заказать и в Интернет-магазине: www.alians-kniga.ru.
Оптовые закупки: тел. (495) 258-91-94, 258-91-95; электронный адрес
books@alians-kniga.ru.

Дэвид Хэррон

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

Главный редактор Мовчан Д. А.


dm@dmk-press.ru
Перевод с английского Слинкин А. А.
Корректор Синяева Г. И.
Верстка Чаннова А. А.
Дизайн обложки Мовчан А. Г.

Подписано в печать 12.12.2011. Формат 70х100 1/16 .


Гарнитура «Петербург». Печать офсетная.
Усл. печ. л. 34,5. Тираж 1000 экз.

Web-сайт издательства: www.dmk-press.ru

Оценить