Текст
                    Node.js
Дэвид Хэррон


Дэвид Хэррон Node.js Разработка серверных веб-приложений на JavaScript
David Herron Node Web Development A practical introduction to Node, the exciting new server-side JavaScript web development stack Г рдг т1 open source I 1 s 1 I community experience distilled PUBLISHING 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 (анг.) ISBN 978-5-94074-809-0 (рус.) Copyright © 2011 Packt Publishing © Оформление, ДМК Пресс, 2012
Содержание Об авторе 8 Благодарности 9 О рецензентах ю Предисловие 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-cepeepoB на этапе инициализации системы 36 Использование всех процессорных ядер в многоядерной системе 40 Резюме 42 Глава 3. МОДУЛИ NODE 43 Что такое модуль? 43 Модули Node 44 Как Node ищет модули, затребованные в requireCmodule’)? 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 И НТТР-КЛИЕНТЫ 95 Отправка и получение событий с помощью объектов EventEmitter 95 Теоретические основы EventEmitter 97 HTTP Sniffer - прослушивание обмена данными по протоколу HTTP 97
Содержание 'п.ШтЩЯШяШвшШ Реализация простого веб-сервера 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 не должен быть замурован в браузере.
ииш Предисловие В главе 2 «Настройка Node» речь пойдет о настройке среды разработки для Node, в том числе о нескольких способах сборки и установки из исходного кода. Мы также коснемся вопроса о развертывании Node на производственных серверах. Глава 3 «Модули Node» посвящена модулям - единицам модульной структуры приложений для Node. Мы разберемся в том, что такое модули, и разработаем несколько. Затем познакомимся с программой npm, Node Package Manager (менеджер пакетов Node) и рассмотрим несколько способов использования npm для управления установленными пакетами, а также для разработки и распространения npm-пакетов. В главе 4 «Вариации на тему простого приложения» мы, уже вооруженные знанием основ, приступим к изучению процесса разработки приложений на платформе Node. Точнее, мы разработаем простое приложение, применяя саму Node, систему промежуточного уровня Connect и веб-каркас Express. Хотя при¬ ложение несложное, оно даст нам возможность изучить цикл обработки событий в Node, узнать о его адаптации к продолжительным вычислениям, познакомиться с синхронными и асинхронными алгоритмами и вынести громоздкие вычисления на вспомогательный сервер. В главе 5 «Простой веб-сервер, объекты EventEmitter и НТТР-клиенты» объясняется, что в 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 входит собственный веб-сервер.
Предисловие. ;"8ЩЙ8ЯН01 На кого рассчитана эта книга Данная книга рассчитана на любого программиста, ищущего приключений, сопутствующих новой программной платформе, построенной на основе новой па¬ радигмы программирования. Разработчики серверных компонентов, возможно, придут к выводу, что свежие идеи позволили им по-новому взглянуть на процесс создания веб-приложений. JavaScript - мощный язык, а асинхронная природа Node подчеркивает его сильные стороны. Разработчикам, имеющим опыт работы с JavaScript в браузере, будет интересно применить свои знания на новой территории и посмотреть, что значит писать на JavaScript без доступа к DOM. (Раз нет браузера, то нет и DOM, если только вы не установите JSDorn.) Хотя главы зависят друг от друга, порядок чтения выбираете вы сами. Мы предполагаем, что вы уже умеете писать программы и знакомы с совре¬ менными языками программирования, такими как JavaScript. Графические выделения В этой книге используются различные шрифты для обозначения типа инфор¬ мации. Ниже приведено несколько примеров с пояснениями. Фрагменты кода внутри абзаца выделяются следующим образом: «Объект http инкапсулирует протокол HTTP, а его метод http. createServer создает полно¬ ценный веб-сервер, прослушивающий порт, указанный в методе . listen». Блок кода выделяется следующим образом: var http = requiге('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 В = "a different value B"; var ml = require('./modulel'); util.log(’A='+A+' B='+B+' values='+util.inspect(m1.values())); Команды и выводимая ими информация записываются так: $ sudo /usr/sbin/update-rc.d node defaults
Предисловие Новые термины и важные фрагменты выделяются полужирным шрифтом. Например, элементы графического интерфейса в меню или диалоговых окнах вы¬ глядят в книге так: «В настоящей системе для обеспечения защиты нужно было бы ввести имя и пароль пользователя. Но мы просто попросим пользователя нажать кнопку 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 и ввести информацию об опечатке. Проверив ваше сообщение, мы разместим информацию
Предисловие ■■I об опечатке на нашем сайте или добавим ее в список замеченных опечаток в раз¬ деле Errata для данной книги. Для просмотра списка опечаток выберите название книги на странице http: //www.packtptib.com/support. Нарушение авторских прав Незаконное размещение защищенного авторским правом материала в Интер¬ нете - проблема для всех носителей информации. В издательстве Packt мы отно¬ симся к защите прав интеллектуальной собственности и лицензированию очень серьезно. Если вы обнаружите незаконные копии наших изданий в любой форме в Интернете, пожалуйста, незамедлительно сообщите нам адрес или название веб¬ сайта, чтобы мы могли предпринять соответствующие меры. Просим отправить ссылку на вызывающий подозрение в пиратстве материал по адресу copvright@packtpub.com. Мы будем признательны за помощь в защите прав наших авторов и содействие в наших стараниях предоставлять читателям полезные сведения. Вопросы Если вас смущает что-то в этой книге, то можете связаться с нами по адресу questions@packtpub.com. и мы сделаем все возможное для решения проблемы.
Глава 1. ЧТО ТАКОЕ NODE? Node - это захватывающая новая платформа для разработки веб-приложений, серверов приложений, произвольных сетевых серверов и клиентов, да и вообще для программирования. Она спроектирована так, чтобы обеспечить высочайшую масштабируемость сетевых приложений - за счет хитроумного сочетания асин¬ хронного ввода/вывода, использования JavaScript на стороне сервера, изобрета¬ тельного использования анонимных функций JavaScript и однопоточной собы¬ тийно-ориентированной архитектуры. Принятая в Node модель принципиально отличается от распространенных платформ для построения серверов приложений, в которых масштабируемость достигается за счет многопоточности. Утверждается, что благодаря событийно- ориентированной архитектуре снижается потребление памяти, повышается про¬ пускная способность и упрощается модель программирования. Сейчас платформа Node быстро развивается, и многие считают ее привлекательной альтернативой традиционному подходу к разработке веб-приложений - на базе Apache, РНР, 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? 11 НМн Что позволяет делать 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, построенные на основе библиотеки ТСР-сокетов; □ средства доступа к файловой системе; □ встроенная рудиментарная поддержка автономного тестирования с по¬ мощью утверждений. Сетевой слой Node находится на низком уровне, но работать с ним все равно просто. Например, модули HTTP позволяют реализовать HTTP-сервер (или кли¬ ент), написав всего несколько строк кода, но тем не менее на этом уровне про¬ граммист работает очень близко к реальным запросам по протоколу и может точно указать, какие HTTP-заголовки следует включать в ответ на запрос. Если про¬ граммист на РНР обычно не интересуется заголовками, то для программиста на Node они существенны. Иными словами, написать на Node HTTP-сервер очень просто, но типичному разработчику веб-приложений нет нужды работать на таком низком уровне. На¬ пример, кодируя на РНР, программист предполагает, что Apache уже присутст¬
18 ■■■lilt Что такое Node? вует, так что реализовывать серверную часть стека ему не нужно. Сообщество, сложившееся вокруг Node, создало широкий спектр каркасов для разработки веб¬ приложений, в том числе Connect, которые позволяют быстро сконфигурировать HTTP так, чтобы предоставлялось все, к чему мы привыкли, - сеансы, куки, об¬ служивание статических файлов, протоколирование и т. д. - и пусть разработчик занимается бизнес-логикой приложения. Серверный JavaScript Хватит чесать в затылке. Ведь именно этим вы сейчас и занимаетесь, правда? - чешете затылок и бормочете: «Что браузерному языку делать на сервере?» Но на самом-то деле у JavaScript есть долгая и мало кому известная история применения вне браузера. JavaScript - язык программирования, такой же, как любой другой, поэтому правильнее было бы спросить: «Почему JavaScript должен быть замуро¬ ван в браузере?» На заре эры веб инструменты для создания веб-приложений находились еще в зачаточном состоянии. Кто-то экспериментировал с написанием CGI-скриптов на Perl или на TCL, языки РНР и Java еще только разрабатывались, и даже Java¬ Script применялся на стороне сервера. Одним из первых серверов приложений был Live Wire от компании Netscape, и в нем использовался JavaScript. В некоторых вер¬ сиях технологии Microsoft ASP использовался язык J Script, версия JavaScript от Microsoft. Из относительно недавних серверных проектов, в которых используется JavaScript, назовем каркас разработки приложений RingoJS, популярный в мире Java. Он построен на базе Rhino - реализации JavaScript, написанной на Java. Node дополняет картину ранее невиданной комбинацией - сочетанием быст¬ рого событийно-ориентированного механизма ввода/вывода и быстрого движка JavaScript, такого как V8, используемого в браузере Google Chrome. Почему имеет смысл использовать Node? Язык JavaScript очень популярен благодаря присутствию в любом веб-брау- зере. Он ни в чем не уступает другим языкам, но при этом поддерживает многие современные представления о том, каким должен быть язык программирования. Благодаря широкому распространению имеется немало опытных программистов на JavaScript. Это динамический язык со слабо типизированными, динамически расширяе¬ мыми объектами, которые неформально объявляются по мере необходимости. Функции в нем являются полноценными объектами и обычно используются в виде анонимных замыканий. Это делает JavaScript более мощным языком, по сравнению с некоторыми другими, часто применяемыми для разработки веб- приложений. Теоретически наличие подобных возможностей должно повышать продуктивность программистов. Но скажем откровенно: споры между сторонни¬
Архитектура: потоки или асинхронный ввод/вывод 111 нвпна ками динамических и статических языков, а также строгой и слабой типизации до сих пор не утихли и вряд ли когда-нибудь утихнут. Один из основных недостатков JavaScript - Глобальный Объект. Все перемен¬ ные верхнего уровня «сваливаются» в Глобальный Объект, и при использовании одновременно нескольких модулей это может привести к неуправляемому хаосу. Поскольку веб-приложения обычно состоят из множества объектов, возможно, создававшихся разными организациями, то может возникнуть опасение, будто программированиедляЫос1есроднихождениюпоминномуполю,нашпигованному конфликтующими между собой глобальными объектами. Однако это не так. На самом деле в Node используется система организации модулей CommonJS, а это означает, что локальные переменные некоторого модуля так и будут локальными в нем, пусть даже выглядят как глобальные. Такое четкое разграничение между модулями решает проблему Глобального Объекта. Использование единого языка программирования на сервере и на клиенте давно уже было мечтой разработчиков для веб. Своими корнями эта мечта уходит в период становления Java, когда апплеты представлялись клиентским интерфейсом к написанным на Java серверным приложениям, a JavaScript первоначально виделся как облегченный скриптовый язык взаимодействия с апплетами. Но что- то на этом пути не сложилось, и в результате не Java, a JavaScript стал основным языком на стороне клиента-браузера. С появлением Node мы наконец сможем реализовать мечту - сделать JavaScript языком, используемым по обе стороны веб - на стороне клиента и сервера. У единого языка есть несколько потенциальных плюсов: □ одни и те же программисты могут работать над обеими сторонами прило¬ жения; □ код проще переносить с сервера на клиент и обратно; □ общий для клиента и сервера формат данных (JSON); □ общий программный инструментарий; □ общие для клиента и сервера средства тестирования и контроля качества; □ на обеих сторонах веб-приложения можно использовать общие шаблоны представлений; □ общий язык общения между группами, работающими над клиентской и серверной частью. Node упрощает реализацию этих (и других) достоинств, предлагая основа¬ тельную платформу и активное сообщество разработчиков. Архитектура: потоки или асинхронный ввод/вывод с управлением по событиям Говорят, что именно благодаря асинхронной событийно-ориентированной ар¬ хитектуре Node демонстрирует столь высокую производительность. Так и есть, только к этому надо добавить еще стремительность движка V8 JavaScript. В тра¬
Что такое Node? диционной модели сервера приложений параллелизм обеспечивается за счет ис¬ пользования блокирующего ввода/вывода и нескольких потоков. Каждый поток должен дожидаться завершения ввода/вывода, перед тем как приступить к обра¬ ботке следующего запроса. В Node имеется единственный поток выполнения, без какого-либо контекст¬ ного переключения или ожидания ввода/вывода. При любом запросе ввода/вы¬ вода задаются функции обработки, которые впоследствии вызываются из цикла обработки событий, когда станут доступны данные или произойдет еще что-то значимое. Модель цикла обработки событий и обработчика событий - вещь распространенная, именно так исполняются написанные на JavaScript скрипты в браузере. Ожидается, что программа быстро вернет управление циклу обработ¬ ки, чтобы можно было вызвать следующее стоящее в очереди задание. Чтобы повернуть наши мысли в нужном направлении, Райан Дал (в презен¬ тации «Cinco de Node») спрашивает, что происходит при выполнении такого кода: result = query('SELECT * from db); Разумеется, программа в этой точке приостанавливается на время, пока слой доступа к базе данных отправляет запрос базе, которая вычисляет результат и возвращает данные. В зависимости от сложности запроса его выполнение может занять весьма заметное время. Это плохо, потому что пока поток простаивает, мо¬ жет прийти другой запрос, а если заняты все потоки (не забывайте, что ресурсы компьютера конечны), то запрос будет просто отброшен. Расточительно это как- то. Да и контекстное переключение обходится не бесплатно; чем больше запущено потоков, тем больше времени процессор тратит на сохранение и восстановление их состояния. Кроме того, стек каждого потока занимает место в памяти. И просто за счет асинхронного событийно-ориентированного ввода/вывода Node устраняет большую часть этих накладных расходов, привнося совсем немного собственных. Часто рассказ о реализации параллелизма с помощью потоков сопровождается предостережениями типа «дорого и чревато ошибками», «ненадежные примитивы синхронизации в Java» или «проектирование параллельных программ может ока¬ заться сложным и не исключены ошибки» (фразы взяты из результатов, выданных поисковой системой). Причиной этой сложности являются доступ к разделяемым переменным и различные стратегии предотвращения взаимоблокировок и состя¬ заний между потоками. «Примитивы синхронизации в Java» - один из примеров такой стратегии, и, очевидно, многие программисты считают, что пользоваться ими трудно. Чтобы как-то скрыть сложность, присущую многопоточному парал¬ лелизму, создаются каркасы типа java.util.concurrent, но все равно некоторые счи¬ тают, что попытка упрятать сложность подальше не делает проблему проще. Node призывает подходить к параллелизму по-другому. Обратные вызовы из цикла обработки событий - гораздо более простая модель параллелизма как для понимания, так и для реализации. Чтобы пояснить необходимость асинхронного ввода/вывода, Райан Дал напо¬ минает об относительном времени доступа к объектам. Доступ к объектам в памя-
Производительность и использование процессора ти (порядка наносекунд) производится быстрее, чем к объектам на диске или по сети (миллисекунды или секунды). Время доступа к внешним объектам измеря¬ ется несметным количеством тактовых циклов и может оказаться вечностью, если ваш клиент, не дождавшись загрузки страницы в течение двух секунд, устанет пя¬ литься на окно браузера и отправится в другое место. В Node вышеупомянутый запрос следовало бы записать так: query( SELECT * from db', function (result) { // произвести какие-то операции с результатом }): Разница в том, что теперь результат запроса не возвращается в качестве значе¬ ния функции, а передается функции обратного вызова, которая будет вызвана поз¬ же. Таким образом, возврат в цикл обработки событий происходит почти сразу, и сервер может перейти к обслуживанию других запросов. Одним из таких запросов будет ответ на запрос, отправленный базе данных, и тогда будет вызвана функция обратного вызова. Такая модель быстрого возврата в цикл обработки событий по¬ вышает степень использования ресурсов серверов. Это прекрасно для владельца сервера, но еще больший выигрыш получает пользователь, перед которым быстрее открывается содержимое страницы. В наши дни веб-страницы все чаще собирают данные из десятков источни¬ ков. Каждому нужно отправить запрос и дождаться ответа на него. Асинхрон¬ ные запросы позволяют выполнять эти операции параллельно - отправить сразу все запросы, задав для каждого собственный обратный вызов, и, не дожидаясь ответа, вернуться в цикл обработки событий. А когда придет ответ, будет вызвана соответствующая ему функция. Благодаря распараллеливанию данные можно собрать гораздо быстрее, чем если бы запросы выполнялись синхронно, один за другим. И пользователь по ту сторону браузера счастлив, так как страница загружается быстрее. Производительность и использование процессора Своей притягательностью платформа Node отчасти обязана пропускной спо¬ собности (количество обслуживаемых запросов в секунду). Сравнительные тесты схожих программ, например Apache и Node, показывают фантастический вы¬ игрыш в производительности. Один из популярных эталонных тестов - следующий простой НТТР-сервер, который всего лишь возвращает сообщение «Hello World», читаемое из памяти: var http = require( http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': text/plain'}); res.end('Hello World\n');
22 ■■■111- Что такое 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://nodejs.org/cinco_de_node. pdf), который возвращает 1 Мб двоичных данных; Node на нем показала пропускную способность 822 запроса/с, a nginx - 708 запросов/с. Дал также отвечает, что nginx достигала пиковой производительности при памяти объемом 4 Мб, a 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 Wallnflfer), автор RingoJS, написал в своем блоге заметку, в которой предостерегает от принятия важных решений на основе сравнительных тестов (http://hns.github.com/2010/09/21/benchmark.htinl), а за¬ тем переходит к сравнению 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, крайне важно быстро возвращать управление в цикл обработки событий. Мы вернемся
Использование серверов, экономия затрат и экологичный Интернет ШИН к этой теме в главе 4 «Вариации на тему простого приложения», но уже сейчас заметим, что если обработчик обратного вызова выполняется «слишком долго», то Node перестает быть тем сверхбыстрым сервером, каким был задуман. В од¬ ной из ранних статей о проекте Node (http://four.livejournal.com/963421.html) Райан Дал обсуждал требование о том, что обработчики событий должны вы¬ полняться не дольше 5 мс. Большая часть идей, высказанных в той статье, так и не была реализована, но Алекс Пэйн (Alex Payne) написал по этому поводу крайне любопытную статью (http ://а13х. net/2010/07/27/node. html), в которой проводится различие между «масштабированием в малом» и «масштабированием в большом». В случае небольших веб-приложений («масштабирование в малом») реализа¬ ция на Node, а не на языках 'Р' (Perl, РНР, Python и т. д.), должна давать выигрыш в производительности. JavaScript - мощный язык, а среда Node со своей современ¬ ной быстрой виртуальной машиной имеет преимущества в части производитель¬ ности и организации параллелизма по сравнению с интерпретируемыми языками типа РНР. Далее Пэйн говорит, что «масштабирование в большом», то есть создание при¬ ложений корпоративного уровня, всегда будет трудным и сложным делом. Чтобы обслужить огромное количество пользователей по всему миру, обеспечив при этом необходимую скорость загрузки страниц, обычно приходится включать в систему балансировщики нагрузки, кэширующие серверы, многочисленные резервные ма¬ шины, расположенные в территориально разнесенных точках. Поэтому платфор¬ ма разработки, наверное, не так важна, как система в целом. Мы не узнаем, насколько хороша платформа Node в действительности, пока не увидим пример долгосрочного развертывания в крупных производственных средах. Использование серверов, экономия затрат и экологичный Интернет Смысл борьбы за максимальную эффективность (увеличение числа обрабаты¬ ваемых запросов в секунду) - не только в том, чтобы получить удовольствие от хо¬ рошо проделанной технической работы. Имеются также реальные плюсы с точки зрения бизнеса и окружающей среды. Присущая Node способность обрабатывать больше запросов в секунду означает, что можно приобрести меньше серверов. То есть сделать больше меньшими средствами. Грубо говоря, чем больше серверов, тем выше затраты и тем сильнее воздей¬ ствие на окружающую среду, и наоборот. Существует целая наука о сокращении затрат и вреда окружающей среде, причиняемого инфраструктурой веб-серверов, и эта грубая рекомендация не может охватить всех деталей. Но общая цель оче¬ видна - сократить количество серверов, снизить затраты и уменьшить ущерб, на¬ носимый окружающей среде. В опубликованной корпорацией Intel статье «Increasing Data Center Efficien¬ cy with Server Power Measurements» (http://download.intel.com/it/pdf/Server_
24 щи ааш гаадю кш ■ ■Hill Что такое Node? Power_Measurement_final.pdf) приводится методика оценки эффективности и стоимости центров обработки данных. Следует учитывать много факторов, в том числе конструкцию здания, систему охлаждения и проект вычислительной сис¬ темы. Их эффективная реализация (эффективность ЦОД, плотность ЦОД и плотность СХД) может сократить затраты и вред окружающей среде. Но все это можно свести на нет, развернув неэффективную программную систему, которая заставляет приобретать больше серверов. И наоборот, эффективная программная система позволяет усилить преимущества эффективной организации ЦОД. Как правильно: Node, Node.js или Node.JS? Правильно платформа называется Node.js, но в этой книге мы пишем Node, следуя традиции, принятой на сайте node j s. org, где говорится, что торговый знак - Node.js (j s строчными буквами), но в опубликованных материалах употребляется название Node. Резюме В этой главе мы: □ узнали, что JavaScript живет не только внутри браузеров; □ поведали о различии между асинхронным и блокирующим вводом/вы¬ водом; □ получили первое представление о платформе Node; □ поговорили о производительности Node. Теперь мы готовы приступить к практической работе с Node. В главе 2 «На¬ стройка Node» мы поговорим о конфигурировании окружения Node. Готовьтесь!
Глава 2. НАСТРОЙКА NODE Перед тем как начать работу с Node, необходимо настроить среду разработки. Да¬ лее мы будем использовать ее для разработки и развертывания в непроизводст¬ венной системе. В этой главе мы: □ узнаем, как собирать Node из исходного кода в системах Linux и Мае; □ узнаем, как установить менеджер пакетов npm и некоторые популярные инструменты; □ немного поговорим о системе модулей в Node. Итак, приступим. Системные требования Node лучше всего работает в операционных системах, совместимых со стандар¬ том POSIX. Это различные клоны UNIX (Solaris и т. п.), а также UNIX-подобные системы (Linux, Mac OS Хит. п.). На самом деле многие встроенные в Node функ¬ ции - прямые интерфейсы к системным вызовам, описанным в POSIX. Во многих зрелых языковых платформах (таких как Perl или Python) уже сфор¬ мировался стабильный набор средств и API, который включается в дистрибутивы операционных систем. Но Node все еще быстро развивается, поэтому включать в дистрибутивы ОС готовые двоичные сборки было бы преждевременно. Следо¬ вательно, предпочтительный метод установки Node - сборка из исходного кода. Для этого необходим компилятор языка С (например, GCC) и Python 2.4 (или более поздняя версия). Если вы собираетесь использовать в сетевом коде шифрование, то понадобится еще криптографическая библиотека OpenSSL. В со¬ временных клонах UNIX эти средства почти всегда включаются в дистрибутив, а конфигурационный скрипт Node (о нем речь пойдет ниже) обнаруживает их присутствие. На случай, если вам придется устанавливать их в систему, имейте в виду, что Python можно скачать с сайта http: //python, org, a 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. В описании шага ЗЬ обсуждается сборка в Windows с помощью Cyg-
ни Настройка Node win или MinGW. После того как Cygwin или MinGW установлены, все остальное делается, как в POSIX-совместимых системах. Установка в POSIX-совместимых системах (Linux, Solaris, Mac и т. п.) Получив общее представление, давайте займемся делом - покопаемся в скрип¬ тах сборки. Общая последовательность стандартна: conf igu re, make, make install, - наверное, вы уже не раз встречались с ней при сборке других программ. Официальные инструкции по установке можно найти на вики-сайте Node по адресу https://github.com/ry/node/wiki/Installation. Предварительная установка инструментария Мы уже отмечали, что для сборки Node необходимы три вещи: компилятор С, Python и библиотеки OpenSSL. В процессе установки проверяется их наличие, и если компилятор С или Python отсутствует, то установка завершится ошибкой. Способ установки инструментария зависит от операционной системы. Проверить, что необходимые программы имеются, можно так: $ сс --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: // develope г.apple.com/xcode/.
Установка средств разработки в Мае OS X Установка в свой домашний каталог Традиционно сложилось, что разработчики предпочитают устанавливать Node в свой домашний каталог. Недавние изменения, внесенные в Node 0.4.x и особенно в npm 1.0, сделали это не столь обязательным. Возможно, вы захотите установить Node в системный каталог (мы обсудим это в следующем разделе) или оставить локальную установку для тестирования либо разработки. Сначала посмотрим, как производится локальная установка Node. 1. Скачайте исходный код со страницы http: //nodej s. org/#download. Можно сделать это прямо в браузере или выполнить следующие команды: $ mkdir sre $ cd sre $ wget http://nodejs.Org/dist/node-vO.4.8.tar.gz $ tar xvfz node-v0.4.8.tar.gz $ cd node-vO.4.8 2. Перед сборкой необходимо сконфигурировать исходный код. Для этого служит типичный скрипт configure, команда ./configure -help выведет длинный список возможных параметров. Чтобы установить программу в свой домашний каталог, запустите скрипт следующим образом: $ ./configure --prefix=$H0ME/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=$H0ME/node/0.4.8/bin:${PATH>' »"/.bashrc $ . "/.bashrc Или для пользователей csh: $ echo 'setenv PATH $H0ME/node/0.4.8/bin:${PATH}’ »"/-Cshrc $ source "/.cshrc В результате должна быть создана такая структура каталогов:
28 ■■■Hi Настройка Node $ Is “/node/0.4.8/ bin include lib share $ Is Vnode/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 в систем¬ ный каталог, а пакеты, необходимые вашему приложению, - локально. Подробнее мы рассмотрим этот вопрос в следующей главе.
Установка средств разработки в Мае OS X Установка в системный каталог Для нормальной работы Node устанавливается в системный каталог, и вот по¬ чему: □ это удобнее для повседневной работы; □ это позволяет нескольким приложениям или людям сообща использовать одну установку Node; □ это предотвращает случайную перезапись файлов в инсталляционном ка¬ талоге Node; □ это позволяет запускать Node-серверы на этапе инициализации системы. Установка в системный каталог отличается от установки в домашний каталог только в двух моментах. □ Выбор инсталляционного каталога. Это делается с помощью скрипта conf igu ге, и по умолчанию (без параметра -pref ix=) программа устанавли¬ вается в каталог /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 90.4.8 (devel, net) Evented I/O for V8 JavaScript $ sudo port install nodejs ... длинный список сообщений о загрузке и установке необходимых зависимостей и самой Node
•■■■■him Настройка 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.0rg/dist/node-vO.4.8.tar.gz ######################################################### 100. ox ==> ./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 U apt-get update # apt-get install nodejs # Documentation is great. А вот как в Ubuntu: II sudo apt-get install python-software-properties # sudo add-apt-repository ppa:jerome-etienne/neoip It sudo apt-get update It sudo apt-get install nodejs Можно ожидать, что со временем в дистрибутивы Linux и других ОС Node будет включаться стандартно, как многие другие языки.
Выполним несколько комонд для проверки установки ШШШМШШШШ Установка одновременно нескольких экземпляров Node Обычно не имеет смысла устанавливать несколько версий Node, так как это только усложнит обслуживание системы. Но если вы работаете над исходным ко¬ дом самой Node, тестируете приложение в разных выпусках Node или делаете что- то подобное, то такая необходимость может возникнуть. Соответствующий способ мало чем отличается от того, что было рассмотрено выше. Вы, наверное, обратили внимание, что для установки нескольких версий Node в разные каталоги на одной машине мы задавали параметр -prefix: $ ./configure --prefix=$H0ME/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:${РАТН} Но со временем обслуживать такую систему становится утомительно. Для каждого выпуска необходимо настраивать Node, npm и все сторонние модули, а кроме того, показанная команда изменения PATH далеко не оптимальна. Изоб¬ ретательные программисты придумали несколько систем управления версиями, которые упрощают автоматическую настройку не только Node, но и npin, а заодно включают команды для более удобного изменения PATH: □ https: //github. com/visionmedia/n - менеджер версий; □ https: //github. com/kuno/neco - Nodejs Ecosystem Coordinator. Выполним несколько команд для проверки установки Установив Node, хорошо бы сделать две вещи: проверить, что установка про¬ шла успешно, и познакомиться с командными утилитами. Командные утилиты Node В базовой поставке Node есть две команды: node и node-waf. Как работает node, мы уже видели. Эта команда используется для запуска командных скриптов и сер¬
Настройка Node верных процессов. Вторая команда, node-waf, представляет собой инструмент для сборки расширений Node. В этой книге мы не будем ее рассматривать, желающие могут посмотреть онлайновую документацию на сайте nodej s. org. Простейший способ проверить, что только что установленная Node работает, одновременно является лучшим способом получить справку о Node. Наберите следующую команду1: $ node --help Usage: node [options] script.js [arguments] Options: -v, --version --debug[=port] --debug-brk[=port] --v8-options --vars --max-stack-size=val Переменные окружения: NODE PATH NODE.DEBUG N0DE_H0DULE_C0NTEXTS N0DE_DISABLE_C0L0RS печатать версию node подключиться к удаленному отладчику через заданный порт TCP, не останавливая выполнения то же, но остановиться в script.js и дождаться подключения удаленного отладчика печатать параметры командной строки v8 печатать различные заданные при компиляции переменные установить макс размер стека v8 (в байтах) список разделенных ':’ каталогов, добавляемых в начало пути поиска модулей, require.paths. печатать дополнительную отладочную информацию, если равна 1, то модули загружаются в собственные глобальные контексты, если равна 1, то отключаются цвета в REPL. Документацию см. на сайте http://nodejs.org/ или командой 'man node' Эта команда выводит справку о порядке вызова, в которой перечислены пара¬ метры командной строки. Отметим, что существуют параметры для Node и V8 (не показаны выше). На¬ помним, что Node построена на базе движка V8, а у последнего есть собственный набор параметров, относящихся в основном к деталям компиляции в байт-код, сборке мусора и алгоритмам управления кучей. Чтобы вывести их полный пере¬ чень, задайте параметр --v8-options. В командной строке можно задать параметры, единственный файл скрипта и список аргументов этого скрипта. Аргументы скриптов мы рассмотрим в следую¬ щем разделе. Запустив Node без аргументов, вы окажетесь в интерактивной оболочке Java¬ Script: $ node > console.log(*Hello, world!’); Справка печатается по-английски, но для удобства читателя переведена. {Прим. перев.)
Выполним несколько команд для проверки установки Hello, woridI > console.log(JSON.stringify(require.paths)); ["/Users/davidherron/.node_libraries”,"/opt/local/lib/node"] ■■I Здесь можно ввести произвольный скрипт для Node. Интерпретатор команд обладает неплохими ориентированными на работу с терминала средствами и очень полезен для интерактивных экспериментов с кодом. Вы ведь любите экс¬ периментировать с кодом, правда? Ну и отлично! Запуск скрипта в Node А теперь посмотрим, как в Node запускаются скрипты. Это несложно, но сна¬ чала еще раз выведем справку: $ node --help Usage: node [options] script.js [arguments] Требуется всего лишь задать имя скриптового файла и его аргументы - дело, знакомое любому, кто писал на других скриптовых языках. Для начала создайте текстовый файл Is. j s с таким содержимым: var fs = require(’fs'); var files = fs.readdirSync('.'); for (fn in files) { console.log(files[fn]); } Скачивание исходного кода примеров Код примеров для всех купленных вами книг издательства Packt можно скачать на странице своей учетной записи на сайте http: //www. PacktPub. com. Если вы купили книгу где-то еще, то можете зарегистрироваться на странице htto: //www.PacktPub. com/support, и файлы будут высланы вам по электронной почте. Теперь запустите его, набрав команду: $ node ls.js app.js ls.js Это бледное подражание команде Unix Is (что вы и так наверняка поняли из названия файла). Функция readdi rSync - близкий аналог системного вызова Unix readdi г (введите man 3 readdi г, если хотите получить дополнительные сведения), она служит для получения списка файлов в каталоге. Аргументы скрипта находятся в глобальном массиве process, argv. Чтобы по¬ нять, как этот массив работает, модифицируйте ls.js следующим образом: var fs = require( fs'); var dir = '.';
пин Настройка 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://nodej s.org). Создайте следующий файл с именем арр. 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 в своем браузере. Вы увидите такую картину: А Г»! г, «I ft http://127.0.0.1:8X24/ t T itt7»4tp:/AZ7.0.0.1:8124/ С ■ Coogie I» ■прц;П7.щтШ! Hello, World1
Установка npm - менеджера пакетов для Node Задумайтесь над вопросом, почему этот скрипт не завершается, хотя ls.js завершался. В обоих случаях поток управления доходит до конца скрипта, но процесс Node арр.js продолжает работать, a ls.js - нет. Причина - в наличии aктивньтxпpocлyшивaтeлeйcoбытий.Nodeвceгдaвxoдитвцикл обработки событий, а в скрипте арр. j s вызывается функция . listen, которая создает прослушиватель событий, реализующий протокол HTTP. Именно этот прослушиватель и не дает завершиться арр. js, пока вы не нажмете в окне терминала Control-С или не сделаете что-то подобное. В скрипте же ls.js нет ничего, что могло бы создать долгоживущий прослушиватель событий, поэтому по достижении конца програм¬ мы он завершается. Установка npm — менеджера пакетов для Node Сама по себе Node - довольно простая система и состоит только из интерпре¬ татора JavaScript и нескольких любопытных библиотек асинхронного ввода/выво¬ да. Интересной ее делает, в частности, быстро растущая экосистема модулей, раз¬ работанных третьими сторонами. В центре этой экосистемы находится программа npm. В принципе, модули можно загружать в виде исходного кода и собирать само¬ стоятельно. Но npm предлагает более простой путь; по существу, это стандартный менеджер пакетов для Node, который значительно облегчает загрузку и использо¬ вание модулей. Более подробно мы поговорим о нем в следующей главе. Для установки npm введите следующую команду, как описано на домашней странице сайта npm j s. о rg: $ curl http://npmjs.org/install.sh | sh Она скачивает и устанавливает в вашу систему скрипт оболочки. Быть может, для начала стоит выполнить такую команду, чтобы проверить, насколько свободно вы сможете ориентироваться в этом скрипте: $ curl http://npmjs.org/lnstall.sh | less Эта команда устанавливает скрипт и пакет npm в инсталляционный каталог Node. Чтобы все работало правильно, необходимо помнить о двух моментах. Во-первых, если для запуска Node должна быть установлена переменная PATH, то перед выполнением установщика npm проверьте ее значение: $ export РАТН=/path/to/node/O.n.y/biп:${PATH} $ curl http://npmjs.org/install.sh | sh Во-вторых, если Node установлена в системный каталог, для чего требовалось выполнить команду sudo make install, то установку npm следует выполнять такой командой:
36 Настройка Node $ curl http://npmjs.org/lnstall.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.j s hexy@0.2.1 /home/david/node/O.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 uireCfs-):. 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 ncC.’);.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-cepeepoB на этапе инициализации системы Выше мы запускали Node-сервер из командной строки. Это полезно для раз¬ работки и тестирования, но не годится для нормального развертывания прило¬ жения. Существуют общепринятые способы запуска серверных процессов, свои в каждой операционной системе. Node-сервер запускается так же, как любой дру¬ гой фоновый процесс (sshd, apache, MySQL и т. д.), например с помощью скриптов запуска и останова. В проект Node не включены скрипты запуска и останова для всех операционных систем. Это, пожалуй, правильно: дистрибутив Node - не место для таких скриптов. Считается, что они должны быть частью серверных приложений для Node. Тра¬ диционно инициализацией системы ведал демон init, который управляет фоно¬ выми процессами с помощью скриптов, находящихся в каталоге /etc/init.d. В дистрибутивах Fedora и Redhat этот процесс все еще существует, а в других системах применяются иные менеджеры демонов, например Upstart или launchd. Написание скриптов запуска и останова - только часть дела. Веб-серверы должны быть надежными (например, автоматически перезапускаться после сбоя), легко управляемыми (хорошо интегрироваться с принятой практикой админист-
Запуск Node-cepeepoB на этапе инициализации системы 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-cepeepoM и пере¬ запускает его после сбоя; □ 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-cepeepoM; □ в 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 ■■■111 Настройка 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 -е set -е 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 $DAEM0N ;; stop) forever stop $DAEM0N ;; force-reload|restart) forever restart SDAEMON ;; *) 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 SDAEMON ;;
Запуск Node-cepeepoe на этапе инициализации системы Скрипт инициализации можно выполнить и вручную: $ 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 Если работает серверный процесс, то к нему можно обратиться с запросом из браузера: IIMH О п О http;f7H3.79en.com: 1337{ 4 [ ► И + http://h3.7gen.com И37/ С Or CJoogte » Belle World Если сервер запущен под управлением forever, то мы увидим следующие про¬ цессы: $ ps ах | grep node 16664 ? Ssl 0:00 node /usr/local/node/О.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
II Настройка Node Использование всех процессорных ядер в многоядерной системе V8 - однопоточный движок JavaScript. Для браузера Chrome этого достаточно, однако означает, что Node-сервер, работающий на только что купленном 16-ядер¬ ном сервере, задействует всего одно ядро, а 15 остальных простаивают. Начальник может потребовать объяснений. Однопоточный процесс использует только одно процессорное ядро. Это факт, от которого никуда не уйти. Чтобы задействовать в одном процессе несколько ядер, необходима многопоточная программа. Однако принятая в Node парадигма проектирования без потоков, хотя и позволяет упростить модель программирова¬ ния, одновременно означает, что Node не использует несколько ядер. И что же вам делать? А точнее, как умиротворить начальника? Есть несколько проектов, посвященных разработкемногопроцессных конфигу¬ раций Node для повышения надежности и задействования всех имеющихся про¬ цессорных ядер. Основная идея состоит в том, чтобы запустить несколько процессов Node и распределять между ними поступающие запросы. Имея кластер из однопоточных процессов, вы сможете использовать все ядра и оправдать денежные вложения в приобретение мощного сервера - начальство останется довольно. Один из таких проектов называется Cluster (https: //github. сот/ LearnBoost/ cluster), авторы описывают его как «расширяемый менеджер многоядерных серверов для Node.js». Он запускает конфигурируемый набор дочерних процес¬ сов, перезапускает их после сбоя и располагает богатыми средствами протоко¬ лирования, управления из командной строки и сбора статистики. Более ранний проект Spark закрылся, уступив место Cluster. В состав проекта Cluster входят несколько примеров конфигурации сервера, демонстрирующих, на что он способен. Давайте установим его и на одном из при¬ меров посмотрим, как с ним работать. $ sudo npm install cluster cluster@0.6.4 ./nodejnodules/cluster I log@1.2.0 Возьмем в качестве образца пример reload, js и на основе арр. 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.reloadO) .1isten(1337);
Запуск Node-cepeepoe на этапе инициализации системы 1111ННН1Н1НУ1Н В этой конфигурации создается кластер из двух процессов, между которыми разделяется нагрузка, причем после модификации каких-либо файлов кластер бу¬ дет автоматически перезагружаться. Дополнительные сведения вы найдете в до¬ кументации по проекту 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 ? SI 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 функцией автоматической перезагрузки. ! >4 0 п. http://H3.7gen.conr 1337/ Hello Modified world
42 Настройка Node Резюме В этой главе мы многое узнали об установке Node, использовании командных утилит и о запуске Node-cepeepa. Мы также мимоходом коснулись деталей, кото¬ рые подробнее рассмотрим ниже. Наберитесь терпения. Точнее, мы рассмотрели следующие вопросы: □ скачивание и компиляция исходного кода Node; □ установка Node в домашний каталог для целей разработки и в системный каталог - для развертывания в производственной системе; □ установка npm, стандартного менеджера пакетов для Node; □ запуск Node-скриптов и Node-серверов; □ что необходимо для превращения Node в надежный фоновый процесс; □ использование нескольких процессов для задействования всех имеющихся процессорных ядер. Теперь, познакомившись с тем, как настроить систему, мы можем перейти к написанию приложений для Node. Но сначала нужно разобраться с модулями - базовыми строительными блоками, из которых составляются приложения Node. Этим мы и займемся в следующей главе.
Глава 3. МОДУЛИ NODE Прежде чем писать приложения для Node, необходимо познакомиться с модулями и пакетами Node. Модули и пакеты это строительные блоки, позволяющие раз¬ бить приложение на более мелкие части. В этой главе мы: □ узнаем, что такое модуль; □ познакомимся со спецификацией модулей CommonJS; □ узнаем, как Node ищет модули; □ познакомимся с системой управления пакетами npm. Итак, приступим. Что такое модуль? Модули - это базовые строительные блоки для конструирования приложе¬ ний Node. Мы уже видели их в действии - любой JavaScript-файл, используемый в Node, является модулем. Теперь пришло время рассказать о том, как они рабо¬ тают. В примере Is. js в главе 2 был такой код для подключения модуля fs, то есть для получения доступа к содержащимся в нем функциям: var fs = require('fs'); Функция require ищет запрошенный модуль и загружает его определение в среду исполнения Node, в результате чего становятся доступны все функции, входящие в состав модуля. В данном случае объект f s содержит код и данные, экс¬ портируемые модулем f s. Прежде чем погружаться в детали, рассмотрим простой пример. Взгляните на следующий модуль simple. j s: var count = 0; exports.next = function() { return count++; } Здесь определены экспортируемая функция и локальная переменная. Теперь воспользуемся ими.
Модули Node а о о ТътШ bash - Пх17 $ node > var s = rcquire(* ./sieple*); > s.next(); 0 > s.next(); 1 > s.nextO; 2 > s.next(); 3 >*a Функция require(' ./simple') возвращает тот самый объект, exports, полю next которого мы присвоили функцию в скрипте simple. j s. При каждом обраще¬ нии к 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 Естественно, ожидается, что файлы, имена которых заканчиваются на . j s, со¬ держат код, написанный на JavaScript. Node поддерживает также модули в виде двоичных платформенных библиотек. В таком случае имя файла должно окан¬ чиваться расширением . node. Обсуждение того, как писать двоичные модули для Node, выходит за рамки этой книги, но сказанного достаточно, чтобы вы поняли, о чем идет речь, когда столкнетесь с ними. Некоторые модули Node не являются файлами в файловой системе, а «зашиты» в исполняемый файл Node. Это модули ядра (Core), документированные на сайте nodejs .org. Изначально они существуют в виде файлов в дереве исходного кода Node, но в ходе сборки прикомпилируются к исполняемому файлу. Существуют три типа идентификаторов модулей: относительные, абсолютные и верхнего уровня. Относительные идентификаторы модулей начинаются строкой "./" или ”.. /", а абсолютные - строкой Здесь имеется полная аналогия с семантикой файловой системы, совместимой с POSIX, в которой пути записываются относительно исполняемого файла. Понятно, что абсолютные идентификаторы модулей записываются относи¬ тельно корня файловой системы. В начале идентификатора модуля верхнего уровня нет ни ".", ни "..", ни '/", это просто имя модуля. Такие модули хранятся в одном из нескольких предо¬ пределенных каталогов, например node_modules, или в каталогах, перечисленных в массиве require, paths. Мы обсудим их ниже. Локальные модули внутри приложения Все множество мыслимых модулей можно разбить на две категории: являю¬ щиеся и не являющиеся частью приложения. Модули, не являющиеся частью кон¬ кретного приложения, написаны, имея в виду какую-то обобщенную цель. Начнем с реализации модулей, используемых только в вашем приложении. В типичном приложении модули распределены по нескольким каталогам. Они хранятся в системе управления версиями и впоследствии копируются на серверы. Эти модули знают относительные пути к своим «братьям» и могут использовать эту информацию, чтобы ссылаться друг на друга но относительным идентифика¬ торам. Чтобы лучше понять, как это организовано, возьмем в качестве примера струк¬ туру одного из пакетов для Node, каркаса разработки веб-приложений Express. Он состоит из нескольких модулей, организованных в виде иерархии, которую раз¬ работчики Express находят полезной. Подобные иерархии имеет смысл создавать, когда приложение достигает определенного уровня сложности, оправдывающего его разбиение на части, большие, чем модуль, но меньшие, чем приложение. К со¬ жалению, специального термина для таких структурных компонентов в Node нет, поэтому приходится употреблять корявую фразу «разбивать на части, большие, чем модуль». Каждая такая часть представляется каталогом с несколькими моду¬ лями.
■■■Hi ¥ Ш express * Si bin [fj History.md 1 index.js ¥ ■ иь li expresses P http.js В https js |й request.js ijg response.js ▼ Ш router [i| collection.js ИЛ index.js l! methods js I routejs 11 utilsjs т Ш view p partiafjs II vlew.Js II view.js p LICENSE Makefile * Ш nodejrnodul«s * il Qs Q package.]son 13 Readme.md Модули Node В этом примере, наверное, чаще всего приходится ссылаться на модуль utils, j s по относительному пути. В зависимости от того, какой исходный файл ссылает¬ ся на utils. j s, вызов функции requi re может записывать по-разному: var utils = require(’./lib/utils'); var utils = require('./utils'); var utils = require(./utils'); Комплектация приложения с внешними зависимостями Для включения модулей, находящихся в каталоге node_modules, употребляется идентификатор верхнего уровня: var express = require('express'); Node производит поиск модулей во всех каталогах nodejnodules, а их сущест¬ вует несколько. Алгоритм начинает поиск в каталоге текущего модуля, потом до¬ бавляет в путь nodejnodules и ищет там. Если модуль не найден в каталоге node_
Как Node ищет модули, затребованные в requirefmodule’)? modules, то Node переходит к родительскому каталогу и пробует еще раз - и так до тех пор, пока не дойдет до корня файловой системы. В предыдущем примере имеется каталог node_modules, в котором есть подката¬ лог qs. При такой структуре модуль qs доступен любому модулю внутри Express, нужно лишь написать: var qs = require( qs'); Но что, если вы захотите использовать каркас Express в своем приложении? Ничего сложного, просто создайте каталог node_modules внутри дерева своего приложения и установите туда Express: т Ж projects * jg| drawapp [jjj Index.js * |§ lib i rirawjs ► Й nodejtiodules Ш svg js w Ш node_module5 ▼ Ш express ► Ш bin 1^1 History.md Л indexjs ► Si Mb [3 LICENSE Q Makefile ▼ m node^modules ► Ш QS Q paekage.json □ Readme.md jj| packagejson ▼ il nodejmodules Здесь показано гипотетическое приложение d rawapp. Если каталог node_modules расположен так, как показано на рисунке, то любой модуль внутри drawapp может получить доступ к express следующим образом: var express = require( express'); Однако те же самые модули не смогут добраться до модуля qs, скрытого внут¬ ри каталога node_modules, являющегося частью самого каркаса Express. Просмотр каталогов node_modules, содержащих искомый модуль, производится вверх по иерархии файловой системы, без захода в дочерние каталоги. Аналогично: если установить модуль в каталог lib/node_modules, то он будет доступен из d raw. j s и svg. j s, но недоступен из index. j s. Как и раньше, поиск про¬ исходит вверх от текущего каталога, а не вглубь него. При обходе каталогов node_modules Node останавливается, как только найдет искомый модуль. Так, если ссылка встречается в файле draw, js или svg. js, то бу¬ дут просмотрены следующие каталоги:
48 ■■■ИМ- Модули Node □ /home/david/projects/drawapp/lib/nodejnodules □ /home/david/projects/drawapp/node_modules □ /home/david/projects/node_modules □ /home/david/node_modules □ /home/nodejnodules □ /nodejnodules Каталог node_modules играет важнейшую роль, позволяя системе управления пакетами выпутаться из лабиринта конфликтующих версий. Вместо того чтобы помещать все модули в одно место и медленно сходить с ума, пытаясь разрешить зависимости от конфликтующих номеров версий, мы можем завести несколь¬ ко каталогов nodejnodules и при необходимости складывать конкретные версии в конкретное место. Разные версии одного модуля могут находиться в разных ка¬ талогах nodejnodules, и при условии, что эти каталоги расположёны правильно относительно друг друга, никаких конфликтов не возникнет. Пусть, например, вы написали приложение, в котором используется модуль 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/. nodejnodules",''/home/david/. node_libraries", "/usr/local/ lib/node"] Для заполнения массива require, paths используется переменная окружения N0DE_PATH: $ export NOOE_PATH=/usr/lib/node ( node > require.paths;
Как Node ищет модули, затребованные в requirefmodule')? mm ["/usr/lib/node","/home/david/.node_libraries","/usr/local/lib/node”] > Раньше в программах для Node часто применялась следующая идиома для до¬ бавления новых элементов в массив require. paths: require. paths. push( di rname). Однако теперь она не рекомендуется, потому что, как выяснилось, является источ¬ ником путаницы. Хотя так делать можно и даже еще остались модули, в которых эта идиома встречается, но смотрят на ее использование с большим неодобрением. Если несколько модулей помещают каталоги в require.paths, то результаты не¬ предсказуемы. В большинстве случаев рекомендуется устанавливать модули в каталоги node_ modules. Составные модули - модули-каталоги Составной модуль может включать несколько внутренних модулей, файлы данных, файлы шаблонов, документацию, тесты и прочее. Все это можно помес¬ тить в хорошо продуманную структуру каталогов, которую Node будет рассматри¬ вать как модуль и загружать командой require( 'moduleName'). Для этого следует добавить в каталог файл модуля index, js или файл с именем package, j son. Файл package, j son должен содержать данные, описывающие модуль, в формате, очень похожем на формат файла package. j son, используемого менеджером пакетов npm (см. ниже). В обоих случаях для совместимости с Node достаточно очень неболь¬ шого набора полей, распознаваемых npm. Точнее, Node распознает следующие поля в файле package, j son: { name: "myAwesomeLibrary", main: "./lib/awesome.js" } При таком файле package, json команда require( 'myAwesomeLibrary') найдет этот каталог и загрузит файл /path/to/node_modules/myAwesomeLibrary/lib/awesome.js Если файла package. j son нет, то Node будет вместо него искать файл index. js, то есть загрузит файл: /path/to/node_modules/myAwesomeLibrary/index.js В любом случае (index, js или package, json) реализовать составной модуль, содержащий внутренние модули и другие файлы, несложно. Если вернуться к рас¬ смотренной выше структуре пакета Express, то мы увидим, что некоторые модули пользуются относительными идентификаторами для ссылки на другие модули в пакете, а для включения модулей, разработанных кем-то другим, можно вос¬ пользоваться каталогом node_ modules.
ШШГЭГЧШ — ИИШИ&ЙЩ КаЯВВ1ВшшЯ1Г Модули Node Менеджер пакетов для Node (npm) В главе уже отмечалось, что npm - это система управления и распростране¬ ния пакетов для Node, ставшая стандартом де-факто. Концептуально она похожа на такие инструменты, как apt-get (Debian), ipm/yum (Redhat/Fedora), MacPorts (Mac OS X), CPAN (Perl) и PEAR (PHP). Ее задача - обеспечить публикацию и распространение пакетов Node через Интернет с помощью простого интерфейса командной строки. Npm позволяет быстро находить пакеты для решения конкрет¬ ной задачи, загружать и устанавливать их, а также управлять уже установленными пакетами. В npm определен формат пакета для Node, основанный на спецификации CommonJS. Формат npm-пакета Npm-пакет представляет собой структуру каталогов, описанную в файле package. j son. Именно так мы выше определили составной модуль, отличие только в том, что npm распознает значительно больше полей, чем Node. Исходной точкой для определения формата package, j son для npm послужила спецификация Com¬ monJS Packages/1.0. Получить документацию по структуре файла package, j son позволяет следующая команда: $ npm help json Простейший файл package, json выглядит следующим образом: { паше: "packageName", version: "1.0", main: "mainModuleName", modules: { "modi”: "lib/mod1", "mod2": "lib/mod2" Файл представлен в формате JSON, c. которым вы как программист на Java¬ Script, должно быть, встречались уже сотни раз. Наиболее важны поля name и version. Значение паше подставляется в 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" Людям будет проще найти пакет в прт-репозитории (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 перечисляются скриптовые команды, запускаемые на различ¬ ных этапах жизненного цикла пакета, а именно: установка, активация, удаление,
обновление и др. Для получения дополнительных сведений о скриптовых коман¬ дах введите следующую команду: $ npm help scripts Это лишь краткое введение в формат npm-пакетов, для получения полной до¬ кументации наберите прш help json. Поиск прт-пакетов Г1о умолчанию npm-модули загружаются из общедоступного репозитория пакетов по адресу http: //npmjs.org. Если вы знаете имя модуля, то для его уста¬ новки достаточно ввести такую команду: $ npm install moduleName А если не знаете? Как найти интересующие вас модули? На сайте http: //npmjs. org имеется индекс всех модулей в репозитории, а сайт http: //search. npmjs. оrg позволяет производить поиск в этом индексе. В команд¬ ном интерфейсе 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) $ npm view zombie homepage http://zombie.labnotes.org/ Команды npm У команды npm есть много подкоманд для различных операций управления пакетом на всех этапах его жизненного цикла: от публикации (автором пакета) до скачивания, использования и удаления (потребителем). пря 1* a package manager for rode. You can use it to install and publish you' node programs. It manages dependencies and does other cool stuff, One line Install ft f '4c}A?' ' f * .h «ora Than One Lin# InttaU Ш f llpfll*\Щ%{/* • * Wщ ^л^-vIщ I 1. Ш thtf 2. £>о what tbfi ШМЖ sty* to do. Ш О w ^ мл ■" J 7 A !+ Gjbtp л - ‘Jnri* Mxr»<pv j f «p*r - Nod* Pecfe**;-* r^/nprnji.ofg/ Получение справки no npm Пожалуй, важнее всего знать о том, как получить справку. Основной раздел справки, в котором перечисляются все команды npm, выглядит следующим об¬ разом: Terminal bash - - 71x15 $ npm help ® (S Usage: npm <command> where <command> is one of: adduser, author, bin, c, cache, completion, config, deprecate, docs, edit, explore, faq, find, get, help, i, info, init, install, la, link, list, 11, In, Is, outdated, owner, prefix, prune, publish, r, rb, rebuild, restart, rm, root, run-script, s, se, search, set, start, stop, tag, test, un, uninstall, unlink, unpublish, up, update, version, view, whoami Add -h to any command for quick help. A Для большинства команд получить справку по конкретной команде можно так:
54 ■■■Hilt Модули Node $ npm help <command> На сайте npm (http://npmjs.org/) имеется FAQ, включенный также в состав дистрибутива npm. Наверное, чаще всего задается вопрос: «Почему npm меня так ненавидит?» А ответ на него звучит гак: «пргп не умеет ненавидеть. Он любит всех, даже вас». Просмотр информации о пакете Команда npm view рассматривает файл package, json как данные и позволяет опрашивать его, задавая в качестве критерия имя поля. Например, вот как узнать о зависимостях пакета: $ npm view google-openid dependencies ( express: ’>= 0.0.1', openid: ’>= 0.1.1 <= 0.1.1' } В файле package. j son может быть указан 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: 100X (253/253), done. remote: Total 253 (delta 148), reused 0 (delta 0) Receiving objects: 100X (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 $ Is nodejnodules/ openid Отметим, что этот пакет установлен в локальный каталог node_modules. Паке¬ ты можно устанавливать и в другие места, для чего следует либо изменить теку¬
Менеджер пакетов для Node (npm) щий каталог, либо попросить npm выполнить глобальную установку. Например, следующая команда делает текущим каталог /va г/www и затем устанавливает пакет в каталог /var/www/node_modules, где по принятому соглашению хранятся модули, используемые в нескольких сайтах: $ cd /var/www $ npm install openid openid$0.1.6 ./nodejnodules/openid Npm проводит различие между глобальным и локальным режимами. Обыч¬ но он работает в локальном режиме и устанавливает пакеты в локальный каталог nodejnodules по соседству с кодом вашего приложения. В глобальном режиме па¬ кеты устанавливаются в инсталляционное дерево Node (каталоги, перечисленные в массиве require, paths), а не в локальный каталог nodejnodules. Первый способ установки пакетов в глобальном режиме - задать флаг -д: $ npm install -д openid openidQO.1.6 /usr/local/node/0.4.7/lib/node jnodules/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 jnodules/openid Чтобы узнать обо всех каталогах, используемых npm, введите такую команду: $ npm help folders Использование установленных пакетов Смысл установки пакета - в том, чтобы программа для Node могла воспользо¬ ваться содержащимися в нем модулями: var openid = require( openid'); Npm делает все для того, чтобы эта операция прошла гладко.
цини наш мамам ss* sw Ш * ШЖЖШШШш: St Модули Node Некоторые пакеты включают внутренние модули, которые сами по себе могут оказаться полезны. Например, в текущую версию модуля openid включен модуль кодирования и декодирования base64, полезный и другим программам: var base64 = require( openid/lib/base64').base64; Но тут есть опасность, что модуль openid изменит собственную реализацию функций base64, сделав неработоспособным ваше приложение. Некоторые пакеты структурированы специально, чтобы дать доступ к семейству взаимосвязанных подмодулей, при этом для раскрываемых подмодулей даются некоторые гарантии стабильности API. Какие пакеты у меня установлены? Команда npm list выводит список установленных пакетов, основываясь на ре¬ зультатах поиска от текущего каталога. Напомним, что Node ищет модули, начиная от каталога, в котором находится исполняемая программа. Поэтому установлен¬ ные пакеты ищутся в каталогах nodejnodules.в текущем каталоге и его родителях. Обратите внимание, как изменяется список найденных модулей в зависимо¬ сти от того, в каком каталоге вы находитесь: Terminal - bash — 87x2? $ pwd /Users/davidherron/Node.JS/chop63 $ npm list /Users/davidherron/Node.OS I— async@0.1.9 h-т connect<ai.4.2 I 1— qs<30.1.0 I— ejs<30.4.2 1—т express<32.3.11 I •— qs@0.1.0 1— muie<91.2.2 $ cd ../chap06 $ npm list /Users/davidherron/Node.OS/chop06 I— colors$0.5.0 I— ejs<30.4.2 h-т express<32.3.6 *D — connect <31.4.1 — mtmedl.2.2 — qs (§0.1.0 mongodb<30.9.4 mongoose<31.3.3 mongoq(a0.0.4 open id<30.1.6 sqlite3<32.0.12 1M По умолчанию список выводится в виде дерева, как показано на рисунке выше, но для обработки другими командами такой формат неудобен. Чтобы изменить формат списка па более пригодный для разбора, установите конфигурационный параметр parseable: $ npm set parseable=true $ npm list
Менеджер пакетов для Node (npm) /home/david/Node/chap06 /home/david/Nod6/chap06/nodejnodules/ejs /home/david/Node/chap06/nodejnodules/express /hoine/david/Node/chapOe/node.inodules/express/node.modules/connect /hom0/david/Nodo/chapO6/nod0_modul0s/0xpr0ss/nod0_modul0s/inim0 /homo/david/Nodo/chapOe/nodo.inodulos/oxpross/nodo.modulos/qs /hom0/david/Nod0/chapO6/nod0_modul0s/mongodb /hom0/david/Nodo/chapO6/nod0_modul0s/mongoos0 /hom0/david/Nod0/chapO6/nod0_modul0s/sqlit03 Скрипты в составе пакета Npm автоматически выполняет скрипты на различных этапах жизненного цикла пакета. В настоящее время таких этапов четыре: тестирование, запуск, оста¬ нов и перезапуск. В состав npm-пакета могут входить тесты, запускаемые следующим образом: $ npm test <packageName> У событий запуска, останова и перезапуска нет четко определенной семантики. Очевидное применение - запуск и останов процессов-демонов, ассоциированных с пакетом. Редактирование и просмотр содержимого установленного пакета В npm есть две команды, позволяющие просмотреть или изменить содержимое пакета. Например, ими можно воспользоваться на этапе разработки, чтобы вы¬ вести исходный код пакета (и узнать, что он делает), заглянуть в каталог постав¬ ляемых вместе с пакетом примеров или внести модификации для тестирования исправлений. Примеры: OOP Terminal — bash — 92x22 t pwd m /Users/davidherron/Node.J5/chap06 $ npm explore mongoose Exp lor ing /Users/davidherron/Node.JS/chap06/node_moduIes/mongoose Type 'exit' or AD when finished bash-3.2$ pwd /Users/davidherron/Node.OS/chap06/node_modules/mongoose bash-3.2$ Is Hi story.md README.md examples lib support Makefile docs index.js package.json test bash-3.2$ cd examples/ bash-3.2$ Is schema.js bash-3.2$ exit $ pwd /Users/dav i dherron/Node.OS/chap06 ГЧ * a Ш !■* \f _
II Модули 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 mongooseQ1.3.6 ./node_modules/mongoose current=1.3.3 Она показывает версию установленного на вашей машине пакета и его же вер¬ сию в репозитории npm. Обновить устаревшие пакеты очень просто: $ npm update express connect@1.4.1 ./nodejnodules/express/nodejnodules/connect mime§1.2.2 ./node_modules/express/nodejnodules/mime qs@0.1.0./nodejnodules/express/nodejnodules/qs express@2.3.6 ./nodejnodules/express Удаление установленного пакета Может случиться так, что пакет, о котором вы так мечтали, оказался кошма¬ ром, но даже если это не так, существует много других причин, чтобы удалить установленные пакеты. Это можно сделать следующим образом: $ npm list /home/davld/Node I openidOO.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 jnodules/tmod -> /home/david/Node/chap03/tmod А затем - ссылку на тот же пакет в своем приложении: $ npm link tmod ../nodejnodules/tmod -> /home/david/Node/0.4.7/lib/nodejnodules/tmod -> / home/david/ Node/chap03/tmod Стрелки (->) показывают, какие символические ссылки создала команда. У команды npm install есть два режима, полезных во время разработки. Во- первых, если она выполняется в корне каталога пакета, то устанавливает текущий каталог и зависимости в локальный каталог nodejnodules.
Модули Node Во-вторых, tgz-архивы можно устанавливать как из локального файла, так и по сети, указав URL. Большинство систем управления версиями поддерживают адреса¬ цию по URL, возвращая в ответ tgz-архив дерева исходного кода. Например, на стра¬ нице загрузки любого проекта, организованного на сайте githiib, имеется URL вида: $ npm install https://github.eom/havard/node-openld/tarball/v0.1.6 openideO.1.6 ../node_modules/openid Доведя свой пакет до ума, вы можете опубликовать его в общедоступном репо¬ зитории npm, чтобы им могли воспользоваться другие люди. Для этого сначала нужно завести себе учетную запись в репозитории. Это де¬ лает команда npm adduser, которая просит ввести имя пользователя, пароль и адрес электронной почты: $ npm adduser Username: my-useг-name Password: Email: me9example.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] Например: ■■■■Hi $ npm set color true $ npm set global false
Менеджер пакетов для Node (npm) $ npm config get color true $ npm config get global false Установить значения конфигурационных параметров можно с помощью пе¬ ременных окружения. Любая переменная, имя которой начинается строкой NPM_ C0NFIG_, служит для установки конфигурационных параметров. Например, пере¬ менная NPM_CONFIG_GLOBAL устанавливает значение параметра global. Конфигурационные параметры можно помещать в следующие файлы: □ SHOME/.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 express92.3.1 mime@1.2.2 ./node_modules/express/node_modules/mime connects.5.1 ./nodejnodules/express/nodejnodules/connect qsOO.2.0 ./node_modules/express/node_modules/qs express@2.3.1 ./node_modules/express
В npm есть концепция метки (tag), которой можно воспользоваться, чтобы установить самую свежую стабильную версию пакета: $ npm install saxdstable Имена меток произвольны и необязательны. Их выбирает автор пакета, и не во всех пакетах они применяются. 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.3betal). □ При сравнении номеров версий сравниваются не строки, а числа X, Y и Z. Например, 1.9.0 < 1.10.0 < 1.11.3. и l.O.Obetal < 1.0.0beta2 < 1.0.0. □ Совместимость документируется с помощью следующих соглашений о ну¬ мерации версий: • пакеты с основным номером версии 0 (X = 0) совершенно нестабильны, их API может измениться в любое время; • если изменение исправляет только ошибки и гарантирует обратную со¬ вместимость, то следует увеличивать номер исправления (Z); • дополнительный номер версии (Y) нужно увеличивать при добавлении функциональности, сохраняющей обратную совместимость (например, добавлена новая функция, а все прочие обратно совместимы);
■■■I Менеджер пакетов для Node (npm) • основной номер версии (X) следует увеличивать при внесении несовмес¬ тимых изменений. Спецификация CommonJS Система модулей Node основана на спецификации CommonJS (http://www. commonjs.org/). Хотя JavaScript - мощный язык с целым рядом современных продвинутых возможностей (например, объекты и замыкания), ему недостает стандартной библиотеки объектов, упрощающей создание приложений. Common¬ JS ставит целью восполнить этот пробел за счет соглашения о реализации модулей в JavaScript и создания набора стандартных модулей. Функция require принимает в качестве аргумента идентификатор модуля и возвращает экспортируемый этим модулем API. Если модуль затребует другие модули, то они также загружаются. Каждый модуль находится в одном JavaScript- файле, и CommonJS ничего не говорит о том, как идентификатор модуля отобра¬ жается на имя файла. Модули предоставляют простой механизм инкапсуляции для сокрытия дета¬ лей реализации и раскрывают только API. Содержимым модуля является код на JavaScript, трактуемый так, будто он написан следующим образом: (function() { ... содержимое файла модуля ... })(); Тем самым все объекты верхнего уровня инкапсулируются (скрываются) в закрытом пространстве имен, к которому никакой другой код не имеет досту¬ па. Именно так решается проблема Глобального Объекта (подробнее об этом чуть ниже). Экспортируемый API модуля - это объект, возвращаемый функцией require. Внутри модуля он реализован в виде объекта верхнего уровня с именем exports, поля которого содержат экспортируемый API. Чтобы экспортировать из модуля функцию или объект, достаточно присвоить его какому-то полю объекта exports. Демонстрация инкапсуляции в модуле Хватит слов, приведем простой пример. Создайте файл modulel. js, поместив в него такой код: var А = "value А"; var В = "value В"; exports.values = function() { return { А: А, В: В }; _} Затем создайте такой файл module2. j s: var util = require('util'); var A = "a different value A' var В = "a different value B'
64 ■ИНН Модули Node var ml = require('./modulel'); util.log( A='+A+' B='+B+' values='+util.inspect(m1.values())); Теперь запустите его следующим образом (Node должна быть уже установ¬ лена): $ node module2.js 19 May 21:36:30 - А=а different value A B=a different value В values={ A: 'value A', B: 'value B' } Этот искусственный пример демонстрирует независимость значений в modulel. js от значений в module2. js. Значения А и В в modulel. js не затираются значениями тех же переменных в module2. js, потому что они инкапсулированы в modulel . js. Значения, инкапсулированные внутри модуля, можно экспортиро¬ вать, как мы поступили с функцией . values в modulel. j s. Упомянутая выше проблема Глобального Объекта связана с тем, что перемен¬ ные, объявленные вне какой-либо функции, помещаются в глобальный контекст. В веб-браузерах существует всего один глобальный контекст, что создает проб¬ лемы, когда глобальные переменные из одного JavaScript-скрипта конфликтуют с глобальными переменными из другого скрипта. Но у модуля, следующего спе¬ цификации CommonJS, имеется собственный закрытый глобальный контекст, по¬ этому одноименные глобальные переменные в разных модулях не конфликтуют между собой. Резюме В этой главе мы многое узнали о модулях и пакетах в Node. Точнее, были рас¬ смотрены следующие вопросы: □ реализация модулей и пакетов для Node; □ управление установленными модулями и пакетами; □ алгоритм поиска модулей в Node. Теперь, зная о модулях и пакетах, мы готовы приступить к их использованию для создания приложений. Этим мы и займемся в следующей главе.
Глава 4. ВАРИАЦИИ НА ТЕМУ ПРОСТОГО ПРИЛОЖЕНИЯ Теперь самое время применить знания о модулях Node к разработке простого веб-приложения для этой платформы. В этой главе мы не будем писать ничего сложного, а воспользуемся возможностью изучить три разных каркаса разработки приложений для Node. В последующих главах мы усложним задачу, но прежде чем ходить, нужно научиться ползать. Итак, приступим. Разработка учебной программы по математике В этой главе мы займемся разработкой простого небольшого приложения Math Wizard, которое при наличии приличного пользовательского интерфейса можно было бы использовать для обучения детей математике. Но поскольку по¬ близости не было специалиста по пользовательским интерфейсам, то Math Wizard будет полезно только для обучения веб-разработке на платформе Node. Не надей¬ тесь вырастить на этом приложении математического гения. Вас предупредили. Приложение Math Wizard состоит из домашней страницы, боковой навигаци¬ онной панели и нескольких страниц для выполнения различных математических операций. Использовать ли каркас? Каркасы для разработки веб-приложений позволяют тратить время на со¬ держательную задачу, не утруждая себе деталями реализации протокола IITTP. Абстрагирование деталей - проверенный временем способ повышения продук¬ тивности программиста. И в особенности при использовании библиотеки или каркаса, которые предоставляют готовые функции, берущие на себя заботу о де¬ талях. В этой главе мы сначала напишем приложение (Math Wizard) без каркасов, а потом будем постепенно улучшать, применяя каркасы Connect и Express.
Вариации на тему простого приложения Реализация Math Wizard в Node (без каркасов) Мы начнем с передвижения ползком, чтобы впоследствии лучше оценить, на что способны каркасы. «Ползком» означает, что мы будем использовать базовый пакет Node, объект HTTP Server. Math Wizard, как и любое веб-приложение, состоит из нескольких страниц, каждая со своим URL-адресом. Все страницы имеют ряд общих элементов (еди¬ ная структура страницы и навигационная панель), а также уникальное для каждой страницы содержимое. Для Math Wizard определены следующие URL-адреса: □ /: домашняя страница; □ /square: возведение в квадрат; □ /mult: умножение двух чисел; □ /factorial: вычисление факториала числа; □ /f ibonacci: вычисление чисел Фибоначчи. Прежде всего создадим каталог для исходного кода: $ mkdlr chap04 Маршрутизация запросов в Node Каждая страница Math Wizard реализована в виде отдельного модуля, а сервер маршрутизирует к этим модулям запросы. Под словами «маршрутизация запросов» мы понимаем стратегию разбие¬ ния приложения на модули. Вместо того чтобы реализовывать все приложение в виде одной гигантской функции обратного вызова, лучше организовать модуль¬ ную структуру. Механизм маршрутизации запросов исследует входящий НТТР- запрос и вызывает нужный модуль, который его обработает. Создайте файл app-node. j s, поместив в него такой код: var http_port = 8124; var http = require(‘http ); var htutil = requireC /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 (без каркасов) // requireC./fibo2-node').get(req, res); ) else if (req.requrl.pathname === '/mult') { requireC./mult-node').get(req, res); } else { res.writeHead(404, { 'Content-Type': text/plain' }): res.end("bad URL "+ req.url); } }): ■■■I server.listen(http_port); console.log('listening to http://localhost:8124’); Этот маршрутизатор запросов абсолютно прямолинеен. Для каждого запроса вызывается функция обратного вызова, которой в аргументе req передаются данные запроса, а объект res используется для отправки ответа клиенту. Маршрутизатор анализирует URL запроса и передает запрос обработчику. Существуют несколько модулей (мы рассмотрим их ниже), каждый из кото¬ рых экспортирует функцию . get с сигнатурой f unction( req, res). Каждый модуль отвечает за реализацию одной страницы Math Wizard. Если URL запроса не соответствует никакому модулю, то мы посылаем ответ с кодом состояния 404, означающим, что страница не найдена. Обработка параметров запроса Функция htutil. loadParams разбирает URL и сохраняет результат в объекте, на который могут ссылаться другие компоненты Math Wizard. На каждой странице приложения имеется форма (тег FORM) с полями а и Ь. Когда пользователь вводит в эти поля числа и нажимает кнопку 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(); } Предполагается, что эта функция будет вызываться из обработчиков НТТР- запросов, которые передадут ей объекты req и res. Функция ищет в строке запроса параметры а и b и, как уже было сказано, присоединяет их к объекту req. Проверка с использованием оператора ?: позволяет упростить остальной код, гарантируя, что req. а и req. b являются либо значением NaN, либо объектом типа Number (в зависимости от того, был в запросе соответствующий параметр или нет). В коде встречается еще функция next, на которую пока не обращайте внимания; мы обсудим ее ниже, при рассмотрении каркаса Connect. Оставшиеся две функции в htutil. js занимаются версткой страницы. В Math Wizard у всех страниц общий макет, поэтому лучше всего собрать все, к нему относящееся, в одном месте, это уменьшит дублирование кода и упростит внесение изменений. Ниже, когда мы перейдем к каркасу Express, то будем использовать для верстки страниц шаблоны, но сейчас ограничимся только базовыми средствами Node, к каковым шаблоны не относятся. Добавьте в файл htutil. js еще две функции, которые помогут при конструи¬ ровании страниц. exports.navbaг = function() { return ["<div class='navbar'>", "<p><a href=7'>home</ax/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)</titlex/head>", "<body><h1>{title}</h1>", "<table><tr>", '<td>{navba r}</td><td>{content}</td>”, "</tr></table></bodyx/html>" ].join('\n’) . replaceCltitle}”, title, "o”) . replace("{navbar}", navbar, "g”) .replace("{content)", content, "g"); }
Реализация Math Wizard в Node (без каркасов) Эта функция определяет структуру страницы в целом. В качестве аргументов она принимает заголовок, навигационную панель и содержимое страницы и встав¬ ляет их в нужные места. Здесь мы применили регулярные выражения и функцию 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.navbarO, [ (IisNaN(req.a) && IisNaN(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: cinput 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 ■■■111 Вариации на тему простого приложения (!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)) : ""J, Сначала с помощью оператора ?: мы проверяем, что параметры заданы, и если это так, то перемножаем req. а и req. b и выводим произведение. Вычисление других математических функций Остальные модули Math Wizard похожи на mult-node. j s, то есть построены по тому же образцу, поэтому рассмотрим их бегло. Для возведения в квадрат число умножается само на себя (а * а). Создайте файл square-node, js, содержащий приведенный ниже код. var htutil = requlre('./htutil'); exports.get = function(req, res) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end( htutil.page("Square", htutil.navbar(), [ (IIsNaN(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, обозначаемым в математике п!, называется про¬ изведение целых положительных чисел от 1 до п. Он встречается во многих обла¬ стях математики. Создайте файл factorial-node. js, как показано ниже. Отметим, что функция Math. floor округляет req. а до ближайшего целого. var htutil = require('./htutil'); var math = require('./math'); exports.get = function(req, res) {
Реализация Math Wizard в Node (без каркасов) IIIMM 71 res.writeHead(200, { Content-Type': text/html' }); res.end( htutil.page( "Factorial", htutil.navbar(), [ (lisNaN(req.a) ? C<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: cinput type=text' name='a" />", "</form>” ].join('\n')) ); } Последовательностью чисел Фибоначчи называется последовательность 1, 1, 2, 3, 5, 8, 13, 21, 34, 55..., в которой каждый член, начиная с третьего, является суммой двух предыдущих. Отношение двух соседних членов последовательности приблизительно равно золотому сечению. Создайте файл fibo-node. js, содержа¬ щий код для вычисления чисел Фибоначчи: var htutil = requireC /htutil): var math = require('./math'); exports.get = function(req, res) { res.writeHead(200, { 'Content-Type': 'text/html' >): res.end( htutil.page( "Fibonacci", htutil.navbarO, [ (IisNaN(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 ■■■Hi! Вариации на тему простого приложения Внимательные читатели, вероятно, обратили внимание на модуль math. Не¬ трудно догадаться, что он содержит реализации нескольких математических функций. Создайте файл math, js и поместите в него такой код: var factorial = exports.factorial = function(n) { if (n == 0) return 1; else return n * factorial(n-l); var fibonacci = exports.fibonacci = function(n) { if (n === 1) return 1; else if (n === 2) return 1; else return fibonacci(n-l) + 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. j s прослушивает порт 8124, наберите в браузере адрес http: // localhost: 8124/. Появится следующая страница:
Реализация Math Wizard в Node (без каркасов) С И П Math Wizard ! « j * о } 1 А ] А ; + Phttp //localhost 8124/ й] .(<» С009Т» 1» ' X.' v ~ - Г' ч■~ ~ '' ~ - " " * i~ ' Math Wlyarri Math Wizard home Multiplication Square s Math Wizard Factorial's Fibonacci’s ZIZ Обобщение Math Wizard Нашим детям необходимо хорошее образование, и, быть может, этот пример станет основой для лучшей обучающей программы из когда-либо написанных. А может, и не станет. Как бы то ни было, приложение Math Wizard легко можно обобщить, добавив другие страницы, - ведь мир математики безграничен. Добав¬ лять новые страницы совсем несложно, нужно лишь следовать уже имеющемуся образцу: □ Добавить тег «а» в htutil. паvbar: Поскольку функция htutil. navba г содержит HTML-разметку навигацион¬ ной панели, то в ней нужно перечислить все реализованные страницы Math Wizard, включив URL и название страницы: "<р><а href='/newUrl>Math Function Name</a></p>\n"+ □ Добавить предложение if в файл app-node. js: Так как app-node. js содержит маршрутизатор запросов, то для каждого нового URL необходимо определить маршрут. Указанный в нем URL должен соответствовать URL в функции htutil. navba г: 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 (без каркасов) плана для разгрузки серверов переднего плана - обычное дело, и в конце этой главы мы реализуем такой математический сервер. Обработчик за¬ просов должен будет асинхронно обращаться к службам или базам данных, собирать информацию, необходимую для ответа, и, когда все будет готово, отправить ответ браузеру. Мы могли бы оптимизировать алгоритм вычисления чисел Фибоначчи, но вместо этого преобразуем нашу неасинхронную функцию в асинхронную, с об¬ ратным вызовом. Асинхронное вычисление чисел Фибоначчи - не самая лучшая мысль, но зато на этом примере мы сможем продемонстрировать переработку ал¬ горитма. Мы разобьем вычисление на последовательность обратных вызовов, ко¬ торые будут вызываться из цикла обработки событий. Первым делом добавим новую функцию вычисления чисел Фибоначчи вместо первоначального наивного варианта. С вами такое тоже может случиться - первая попытка оказывается неудачной и медленной, но впоследствии вы находите более подходящую реализацию. Добавьте в файл math. js такой код: var fibonacciAsync = exports.fibonacciAsync = function(n, done) { if (n === 1 || n === 2) done(1); else { process.nextTick(function() { fibonacciAsync(n-1, function(vall) { process.nextTick(function() { fibonacciAsync(n-2, function(val2) { done(val1+val2); }); }); >); }); Это новый асинхронный алгоритм. Мы преобразовали простую функцию в асинхронно управляемое вычисление, результат которого передается с помощью функции обратного вызова следующим образом: fibonacciAsync(n, function(value) { // произвести какие-то действия с value }); Мы вызываем функцию process. nextTick, чтобы превратить рекурсивную функцию в последовательность шагов, вызываемых диспетчером цикла обработки событий. Очередной шаг реализован в виде обратного вызова из цикла, а возврат в цикл после каждого шага производится быстро, так что сервер может продол¬ жать обработку HTTP-запросов. Это не единственный способ разбить алгоритм на шаги, вызываемые посредством цикла обработки событий. Для решения дан-
Вариации на тему простого приложения ной задачи предназначен модуль async, в котором есть много функций, призван¬ ных укротить асинхронный JavaScript. В коде fibonacciAsync функция process.nextTick заменяет следующее пред¬ ложение в первоначальном варианте алгоритма: return fibonacci(n-1)+fibonacci(n-2); Задача состоит в том, чтобы вычислить два числа Фибоначчи, сложить их и вернуть результат вызывающей функции. В новом алгоритме каждому из трех шагов соответствует анонимная функция. А чтобы их вызовы производились из цикла обработки событий, мы использовали process. nextTick. Прежде чем двигаться дальше, давайте осмыслим это решение. Мы никак не уменьшили объем вычислений, а лишь распределили его по циклу обработки со¬ бытий. Процесс Node по-прежнему нагружает процессор, поэтому выбранный нами подход - не лучший способ оптимизировать такой алгоритм, как вычисле¬ ние чисел Фибоначчи. Но он демонстрирует распределение работы с помощью цикла обработки событий; иногда эта техника полезна, иногда - не очень. Вам решать, какой способ организации длительных вычислений применить к конкретному алгоритму. Например, ниже в этой главе мы покажем, как реали¬ зовать вспомогательный HTTP-сервер и передавать ему вычислительные задачи. Создайте новый файл fibo2-node. js и измените app-node. js, включив вызов requi re('. /f ibo2- 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(), [ (IisNaN(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: cinput type='text' name='a' />", "</form>" ]. join( "\n"))
■I Реализация Math Wizard в Node (без каркасов) 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 должны обрабаты¬ ваться по-разному.
Вариации на тему простого приложения □ Нет страницы для отсутствующих 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, и функцию, которая вызывалась для каждого входящего НТТР- запроса. Работая с Connect, вы вместо этого используете функцию connect, с reateServer и присоединяете к созданному серверу различные ПУ-модули. Один из таких модулей, маршрутизатор, применяется для реализации принадлежащих приложению URL-адресов. Помня об этом, приступим к рассмотрению кода.
Реализация Math Wizard в Node (без каркасов) Установка и настройка Connect Для начала установите Connect: $ npm install connect Затем создайте файл app-connect. j s с таким содержимым: 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.getC/', require('./home-node').get); app.get('/square', htutil.loadParams, requi re('./squa re-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 каркасом для разработки прило¬ жений?
Вариации на тему простого приложения Система Connect подается авторами не как каркас для разработки приложений, а как фундамент, на котором можно строить такие каркасы. Одним из каркасов, построенных на базе Connect, является Express. Но система Connect полезна и сама по себе, и, поняв ее принципы, вам будет проще разобраться в Express, поэтому сначала немного поговорим о Connect, а потом уже перейдем к Express. Знакомство с Connect Первоначальное представление о Connect мы уже получили, теперь пригля¬ димся повнимательнее. Connect лежит в основе каркаса Express и снимает практически все ограничения, с которыми мы сталкивались при построении приложения с помощью объекта HTTP Server. Но не будем забегать вперед, а посмотрим на файл app-connect. j s. В Connect есть несколько способов настройки серверного объекта. В app- connect . js мы поступали следующим образом: var connect = require( connect'); connect.createServer() .use(connect.favicon()) . use(connec t.logge r()) .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 (без каркасов) .use(connect.router(function(app){ // настроить маршрутизатор >) Но истинная мощь кода настройки маршрутизатора проявляется, когда вы объявляете URL-адреса, распознаваемые приложением, и обработчики каждого адреса. Настройка производится по следующему образцу: app.requestName('path', function(req, res, next) {..}); Здесь requestName - один из глаголов HTTP, то есть get, put, post и т. д. Таким образом, функцию app . get можно использовать для отправки браузеру страницы, на которой находится форма с атрибутом method=POST, а функцию app. post - для обработки запросов, отправленных с помощью этой формы. Подробный пример мы рассмотрим в главе б «Обработка и выборка данных», но уже сейчас отметим, что такая конфигурация может выглядеть следующим образом (при условии, что определены подходящие функции): app.get('/form', createPageWithForm); app.post('/form', receiveValuesPostedWithForm); У функции обратного вызова на один аргумент больше, чем у обычного об¬ работчика запросов. В ее сигнатуре function( req, res, next) аргументы req и res, как и раньше, представляют HTTP-запрос и HTTP-ответ. Аргумент же next - это функция, которую предоставляет Connect; ее задача состоит в том, чтобы гаранти¬ ровать выполнение всех функций промежуточного уровня. В одном маршруте может быть указано несколько функций, как то сделано в app-connect. j s. Если правильно пользоваться аргументом next, то Connect будет вызывать все функции по очереди. В app-connect. js мы задали функцию htutil. loadParams, как и в app-node. js. Вспомните, что в этой функции вызывалась функ¬ ция next, которую, как мы теперь знаем, предоставляет Connect. Вот типичный пример настройки маршрутизатора: app.get( /square', htutil.loadParams, require('./square-node'>.get); В качестве аргументов передается строка, содержащая URL и две функции, первой - htutil. loadPa rams. При настройке маршрутизатора можно задать сколько угодно таких функций, их состав зависит от конкретного приложения. В совокупности ПУ-модули и функции маршрутизатора составляют некий вариант конечного автомата для обработки HTTP-запросов. Итак, существуют две последовательности функций. Первая - ПУ-модули, перечисленные в кон¬ фигурации сервера, вторая - только что рассмотренные функции маршрутиза¬ тора.
Вариации на тему простого приложения Реализация 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.iisten(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 Установив требуемые модули, приступим к написанию кода. Сначала создайте каталог: ■■■1111 $ mkdir views
Реализация Math Wizard с помощью Express инмиш А в нем файл 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 арр.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, но в целом все очень похоже. Из нового обратите внимание на вызовы функций арр. register и арр. 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 ■HIW Вариации на тему простого приложения 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> <trxtd> <div class='navbar> <p><a href='/'>home</ax/p> <p><a href='/mult'>Multiplication</ax/p> <p><a href ='/square' >Square' s</ax/p> <p><a href='/factorial'>Factorial's</a></p> <p><a href='/fibonacci'>Fibonacci's</a></p> </div> </td> <td><%- body %x/td> </tr> </table></bodyx/html> В Express шаблон layout является специальным. Обратите внимание, что в файле арр. js мы вызывали функцию res. render( 'fibo.html' ...), а файл layout, html нигде даже не упоминали. Так что же происходит? По умолчанию все содер¬ жимое именованного шаблона, получившееся после подстановки данных, переда¬ ется шаблону layout в виде значения переменной body. Когда арр. j s вызывает res. render(' f ibo. html' ...), сначала формируется фрагмент страницы по шаблону fibo. html, а затем - вся страница по шаблону layout. Поведение по умолчанию можно переопределить двумя способами. Во-первых, установить глобальный параметр Express, который отключает (или включает) ис¬ пользование шаблона layout: арр.set( view options', { layout: false (или true) >); Во-вторых, отменить шаблон layout для конкретной страницы: res.render( myview.ejs', { layout: false (или true) }); Можно также подключить для конкретной страницы другой шаблон вместо layout: res.render( page', { layout: mylayout.jade' }): EJS-шаблон по существу представляет собой HTML-код с тремя дополни¬ тельными разновидностями тегов. Если вам уже доводилось работать с другими системами шаблонов, то эти дополнительные теги должны быть знакомы:
Вариации на тему простого приложения □ <% code %> - небуферизуемый код (условные предложения и прочее); □ <%= code %> - экранирование HTML; □ <%- code %> - без экранирования, с буферизацией. В шаблоне layout мы видим использование экранированного HTML-кода с по¬ мощью тега <%= title %> и небуферизованных данных c. помощью тега <%- body %>. Теперь перейдем к шаблону home, html приложения Math Wizard: <p>Math Wizard</p> И это все! В шаблон mult. html добавим такой код: <% if (req.а && req.b) { %> <р class='result > <%= req.а %> * <%= 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.а && req.b) { %> содержимое, выводимое по условию <% } %> Внутри тегов <% code %> может быть произвольный JavaScript-код, но в данном случае мы воспользовались предложением if, для того чтобы вывести некоторое содержимое, если удовлетворяется заданное условие. Если бы нужно было вывес¬ ти список или массив данных, то можно было бы организовать цикл с помощью предложения while. В файл square, html поместите такой код: <% if (req.а) { %> <р class='result > <%= req.а %> squared = <%= req.result %> </p> <% } %> <p>Enter numbers to multiply</p> <form name='square' action='/square' method='get>
Реализация Math Wizard с помощью Express IIIIMB 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> Наконец, в f ibo. 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 была такая строка кода: арр.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 запроса, следующую после/use г/, и записывает ее в поле req. params. id. В качестве образца может фигурировать и регулярное выражение.
Реализация Math Wizard с помощью Express Математический сервер (и клиент) Напишем простой сервер, который будет производить математические вычис¬ ления и возвращать результаты в виде 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 ■■■lit Вариации на тему простого приложения Это первый пример, в котором нам встретилась функция res. send. Она дает гибкий способ отправлять ответы, принимая массив значений НТТР-заголовков и код состояния HTTP. В данном случае она автоматически представляет полу¬ ченный объект в формате JSON и отправляет его, добавляя правильный заголовок Content-Type. Запустим сервер: $ node math-serveг. 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-запрос, отправить его, дождаться ответа, декодировать тело ответа и использовать содержащиеся в нем данные. Все это можно сделать и в веб-приложении типа Math Wizard, но мы напи¬ шем простое консольное приложение, которое будет клиентом математического сервера. Создайте файл rnath-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 util.log( BODY: + chunk); }); }); req.endQ: }): Функция http, request создает объект HTTP-запроса, в котором отдельные компоненты URL помещены в поля объекта. В следующей главе мы поговорим об этом более подробно, а пока достаточно знать, что функция обратного вызова, объявленная в res. on, вызывается, когда приходит НТТР-ответ. Скрипт math-client. js посылает серверу math-serve г. js несколько «зашитых» запросов и печатает полученные результаты: $ node math-client.js 7 Jun 22:17:49 - BODY: {"n":”20", ,"result":2432902008176640000} 7 Jun 22:17:49 - BODY: t CM e с CO с ,"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 отвечает обработчик, заданный в арр.get. Каждый обработчик от¬ вета res.on( data'...) связан с обработчиком запроса арр.get через сокет, по ко¬ торому был отправлен HTTP-запрос. Когда обработчик запроса арр . get вызыва¬ ет res.send, отправленный им HTTP-ответ приводит к вызову обработчика res. on(' data'...), который ожидает этот ответ. От чего зависит, какой результат печатается первым? От времени, которое скрипт math-server. js затрачивает на вычисление результата, так как результат печатается только после поступления ответа. В большинстве случаев вычисление производится быстро (например, при умножении), и ответ возвращается почти мгновенно. Но с запросом на вычисле¬ ние числа Фибоначчи дело обстоит по-другому. Так как используется функция fibonacciAsync, то вычисление числа Фибоначчи происходит параллельно с об¬ работкой других запросов, и за то время, пока вычисляется 20-е число, сервер успевает обработать остальные запросы и отправить их клиенту. Если запросить второе число Фибоначчи, то результат будет вычислен быстрее, поэтому порядок прихода ответов изменится: I шшяш
92 ■■■1111: Вариации на тему простого приложения $ node math-client.js 7 Jun 22:34:49 - BODY: { и : "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 Да и ведет он себя так же, как при использовании функции f ibonacciAsync. Так что же мы выиграли? Зачем нужно было включать в архитектуру вспомогатель¬ ную службу? Для приложения Math Wizard это, пожалуй, действительно изли¬ шество, но на этом примере мы продемонстрировали широко распространенный подход к повышению производительности приложения. Вот какие факторы сле¬ дует принимать во внимание. й Иногда имеет смысл отказаться от выполнения длительных вычислений на фронтальном сервере, освободив его для взаимодействия с браузерами. □ Распределение запросов между несколькими серверами с помощью балан¬ сировщика нагрузки (облачные вычисления). □ Ответы от math-server. js детерминированы, поэтому кэширующий прок¬ си-сервер мог бы дать колоссальный прирост производительности. Зачем заново вычислять уже известные ответы? □ Открывается возможность поразить начальника модными словечками. Легкость, с которой мы реализовали math-server. j s и интегрировали его с Math Wizard, еще раз демонстрирует простоту и мощь платформы Node. Резюме В этой главе мы многому научились и готовы приступить к созданию настоя¬ щих приложений. Но сначала повторим, что мы узнали. □ Вынесение обработки конкретных запросов в отдельные модули. □ Создание веб-приложений с помощью объекта HTTP Server, а также с по¬ мощью каркасов Connect и Express. □ Получение параметров запроса из форм. □ Влияние длительных вычислений на время реакции сервера и удовлетво¬ ренность пользователей, а также способы решения проблемы. □ Использование модуля async для упрощения асинхронного программиро¬ вания. □ Некоторые черты полноценных веб-приложений, предоставляемые карка¬ сами Connect и Express.
Вариации на тему простого приложения □ Что в Connect понимается под ПО промежуточного уровня. □ Как правила маршрутизации в Connect и Express позволяют по-разному обрабатывать запросы, отправленные разными НТТР-методами. □ Параметризованные URL в Express. □ Реализация REST-совместимого сервера заднего плана для распределения вычислительной нагрузки. Теперь, когда мы так много знаем о реализации веб-приложений, рассмотрим более детально объекты сервера и клиента HTTP и систему распределения собы¬ тий в Node.
Глава S. ПРОСТОЙ ВЕБ-СЕРВЕР, ОБЪЕКТЫ EVENTEMITTER И НТТР-КЛИЕНТЫ Итак, мы видели, как создаются приложения для Node с помощью веб-каркаса Express. Теперь заглянем под капот и разберемся в деталях реализации НТТР- сервера. В этой главе мы напишем простой веб-сервер, который будет обладать некоторыми из свойств реального веб-сервера, обсуждавшимися в главе 4. Обычно заботу о деталях лучше поручить каркасу разработки веб-приложений, потому что правильно реализовать протокол HTTP нелегко. Тогда зачем же мы хотим написать HTTP-сервер самостоятельно? По нескольким причинам: □ понять, какой каркас выбрать; □ понять, почему каркас работает именно так, а не иначе; □ не всякая задача соответствует условиям, подразумевавшимся при проек¬ тировании каркаса; □ иногда, чтобы реализовать службы, отличающиеся от веб-приложений, не¬ обходимо кодировать прямо на уровне HTTP; □ быть может, у вас есть более удачные мысли, чем у авторов каркаса. Приступим. Отправка и получение событий с помощью объектов EventEmitter Объект EventEmitter - ключ к использованию Node для реализации прило¬ жений, но он так глубоко интегрирован в платформу, что можно даже не заметить его существования. Многие объекты Node являются подклассами EventEmitter и пользуются его методами для отправки событий, сигнализирующих о наступлении тех или иных условий. Эти события попадают в цикл обработки событий и в ко¬ нечном итоге приводят к обратному вызову зарегистрированных обработчиков. В этой главе мы будем работать с объектами HTTPServer и HTTPClient. Оба явля¬ ются подклассами EventEmitter и с его помощью посылают события для каждого шага протокола HTTP. Усвоение принципов работы EventEmitter поможет лучше понять не только эти, но и многие другие объекты Node. Класс EventEmitter определен в модуле Node events. Чтобы напрямую рабо¬ тать с этим классом, нужно вызвать функцию require( events'), но обычно это необязательно. Многие объекты в Node вызывают require( events') самостоя¬ тельно, так что вам об этом думать не нужно.
«■Ill Простой веб-сервер, объекты EventEmitter и HTTP-клиенты В следующем примере (pulseг. j s) демонстрируются отправка и получение со¬ бытий путем прямого обращения к классу 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.emitCpulse'); util. log('«« pulse ); }. 1000); Здесь определен класс Pulser, наследующий EventEmitter (для задания насле¬ дования применяется функция util, inherits). Его задача - один раз в секунду посылать события всем зарегистрированным приемникам. В методе start вызыва¬ ется функция setlnterval, которая инициирует повторяющееся - раз в секунду - обращение к функции обратного вызова и вызов функции emit, которая посылает событие pulse всем приемникам. В таком виде модулем pulser. js может воспользоваться любое приложение, которому необходимы регулярно повторяющиеся события таймера. Теперь посмотрим, как воспользоваться объектом Pulser: var pulser = new PulserQ; pulser on( pulse', function() { util.log( pulse received); }); pulser. start(); Здесь мы создаем объект Pulser и потребляем отправляемые им события pulse. Вызов pulser.on( pulse'...) устанавливает связь между событием pulse и вызы¬ ваемой в ответ на него функцией. Затем вызывается метод start, который запус¬ кает процесс. Поместив этот код в файл pulser. j s и выполнив его, вы должны увидеть такую распечатку: $ node pulser.js 23 May 20:30:20 - »» pulse 23 Hay 20:30:20 - pulse received 23 May 20:30:20 - «« pulse
Теоретические основы EventEmitter 23 May 20:30:21 - »» pulse 23 May 20:30:21 - pulse received 23 May 20:30:21 - «« pulse IIIMI Теоретические основы EventEmitter События, генерируемые объектом EventEmitter, имеют имена; в примере выше событие называлось pulse. Имена событий могут быть произвольны, лишь бы имели для вас смысл, и количество различных событий не ограничено. Для зада¬ ния имени события достаточно указать его при вызове функции . emit. Никакого формального реестра имен событий не существует. По принятому соглашению, имя события er гог применяется для извещения об ошибках. Для отправки события применяется функция . emit. Событие посылается всем зарегистрированным приемникам. Чтобы зарегистрировать приемник, программа вызывает метод . on объекта-отправителя и передает ему функцию обратного вы¬ зова, которая получит событие. Это видно на примере файла pulser.js. Объект Pulser вызывает self. emit(' pulse'), чтобы отправить событие, а позже в том же файле вызывается ме¬ тод pulse. on(' pulse', ...), определяющий, куда эти события будут доставляться. Часто требуется посылать вместе с событием данные. Для этого нужно просто передать дополнительные аргументы функции . emit, например: self emit('eventName', datal, data2, ...); Когда программа получит такое, событие, данные будут выглядеть как аргу¬ менты функции обратного вызова. Следовательно, приемник события должен вы¬ глядеть примерно так: emitter.on('eventName', function(data1, data2, ..) { // выполнить действия при получении события }): Практические примеры мы увидим в следующем разделе при обсуждении объ¬ ектов протокола HTTP. Все клиентские и серверные объекты HTTP наследуют EventEmitte г и посылают события, соответствующие различным шагам протокола. Так, любой входящий запрос инкапсулирован в объект Request. Этот объект по¬ сылает событие data при поступлении запроса, событие end - когда получены все данные, и событие close - если сокет закрывается до отправки события end. HTTP Sniffer - прослушивание обмена данными по протоколу HTTP Приступая к работе с объектами HTTP, давайте создадим полезный класс, ко¬ торый будет прослушивать все события, отправляемые объектом HTTP Server.
Простой веб-сервер, объекты EventEmitter и НТТР-клиенты Это не только удобный отладочный инструмент, но и хорошее средство для изуче¬ ния работы серверного объекта 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, 1 = keys.length; i < 1; i++) { var key = keys[i]; ret += i +' '+ key +': '+ req.headers[key] +'\n'; } if (req.trailers)
Теоретические основы EventEmitter ret += req.trailers +'\n'; return ret; } exports.reqToString = reqToString; Код довольно длинный, но наибольший интерес в нем представляет функция sniff On. Она обращается в функции .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 ■■■III Простой веб-сервер, объекты EventEmitter и HTTP-клиенты 6 Apr 21:14:39 - е_request 6 Apr 21:14:39 - request GET 1.1 /favicon.tco {"search":"query": {},"pathname": "/favicon, ico", "href": "/favicon.ico’’} 0 host: localhost:3000 1 user-agent: Nozilla/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 шимниш Реализация 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 }); }
Простой веб-сервер, объекты EventEmitter и НТТР-клиенты htserver.docroot = function(host, path, rootPath) { return this.addContainer(host, path, require(',/staticHandler'), { doc root: 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 соответствуют параметрам, переданным в НТТР-запросе:
Теоретические основы EventEmitter IIIIIMI var lookupContainer = function(htserver, host, path) { for (var i = 0; 1 < 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, поскольку тот и другой необходимы для диспетчеризации запроса. Как мы уже видели ранее, эта функция вызывается для каждого НТТР-запроса: var processHeaders = function(req, res) { req.basicServer.cookies = []; var keys = Object.keys(req.headers); for (var i = 0, 1 = keys.length; i < 1; i++) { var hname = keys[i]; var hval = req.headers[hname]; if (hname.toLowerCase() === "host") { req.basicServer.host = hval; } if (hname.toLowerCaseO === "cookie") { req.basicServer.cookies.push(hval); } Есть много других HTTP-заголовков (Accept, Accept-Encoding, Accept-Lan- gtiage, User-Agent); сохранять их или нет, зависит от приложения. Последняя функция, dispatchToContainer, ищет подходящий контейнер и на¬ правляет ему запрос. Как и processHeaders, она вызывается для каждого НТТР- запроса: var dispatchToContainer = function(htserver, req, res) { var container = lookupContainer(htserver, req.basicServer.host, req.basicServer.urlparsed.pathname); if (container !== undefined) {
104 ■■■Mill Простой веб-сервер, объекты 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 ffilHM 105 Этот обработчик отвечает на запросы к файлу favicon. ico. Ещеразповторим:модули-обработчикиэкспортируютфункцик^ипсиоп( req, res) с именем handle. Basic Server вызывает функцию handle того обработчика, который находится в контейнере, соответствующем запросу. Данный обработчик пытается прочитать файл с именем, указанным в свойстве iconPath, и передает его браузеру с помощью объекта res. Если обработчик обнаружит ошибку, то пошлет с помощью res соответствующую страницу с сообщением. Модуль MIME определяет правильный тип MIME, исходя из типа файла значка. Графические файлы со значками могут быть представлены в любом фор¬ мате, и мы должны сообщить браузеру тип посылаемого файла. Поскольку этот обработчик предназначен только для обработки GET-запро¬ сов, он проверяет метод запроса и возвращает код состояния 404, если метод от¬ личен от GET. Обработчик статических файлов (staticHandler.js) Теперь рассмотрим код обработки запросов на файлы с расширениями . html, . css и им подобными. Создайте файл stat icHandle г. j s и поместите в него такой код: 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. endC'file "+ fname +" not found " + err); } else { fs.readFile(fname, function(err, buf) { if (err) { res.writeHead(500, { Content-Type': text/plain' }); res.endC'file "+ fname +" not readable '' + err); } else { res.writeHead(200, { 'Content-Type': mime.lookup(fname), 'Content-Length': buf.length }); res.end(buf); } >); } });
Простой веб-сервер, объекты EventEmitter и НТТР-клиенты } Это обработчик staticHandler, отвечающий за возврат файлов из файловой сис¬ темы. Параметр doc root содержит путь к каталогу, в котором хранятся статические файлы. Обработчик просто читает указанный файл из каталога doc root и отправ¬ ляет его браузеру с помощью объекта 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 <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. Donee pulvinar porttitor dictum. Fusee 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-файлы ссылками с помощью тегов <а>. Этот HTML-файл ссылается на CSS-файл style, css, который может выгля¬ деть так: body { color: #00с; 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), значки сайтов долж¬ ны представлять собой изображение размером 32x32 или 48x48 почти в любом формате, и они будут отображаться в любом браузере, кроме Internet Explorer (который настаивает на формате ICO). Теперь запустите Basic Server: $ node server.js И в браузере зайдите на страницу по адресу http: //localhost: 4080. Поздравляем! Вот вы и работаете с сервером Basic Server. Браузер должен сейчас показать содержимое файла index, html, который вы поместили в каталог doc root:
108 Простой веб-сервер, объекты EventEmitter и HTTP-клиенты т т щ ИТ» <> А : http:? iiaczihoit'AmO i mtp://icKcihosc 4080/ ♦V + mtp:/.Mocalhosi:4080/ С j - Or СОСЩН: т,~ ”Г'22 Mfj Index ШшщщЩ pag.e..2 . . ■ , ®М - ' : torem ipsuro dolor sit amet, consecfcetur adip&cing ©fit. Mquam fringilla moiestle teo eu tincidunt Donee puMnar porttitor dictum. Fusee at eNt mauris, a ornare ipsum, Nulla congue nisi пол ante palientesque vei iobortis ^acus venus, Nam metus ante, blandit in rutrum efc, peilentesque eu velit Nuiia blandit placerat sceiensque. МогЫ odio magna, accumsan sit amet pharetra eu/varius sit amet ipsum. Aeneaa interdum llbero ut est hendrerit dictum. Suspendisse convatjts peHentesque metus ac tempor. Nam diam »ectus, posuere eu rutrum id, farilisis vei tellus. # * ' i' *' ">x ieSMMm J 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.doc root("parked.com|example.com", ''/", "./example.com");
Теоретические основы EventEmitter «МП 109 Модуль shorturl для Basic Server Часто требуется не парковать доменные имена, а сделать так, чтобы запросы к одному домену переадресовывались другому. Например, переадресовывать за- npocbiKwww. 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.endO; } else { notFound(req, res); > } else { notFound(req, res); > Это модуль обработчика для Basic Server. Для его подключения нужно доба¬ вить в файл server. js такую строку (перед определением контейнера doc root): server.addContainer(". *'", "/l/(. *)$'', require("./redirector"), { }); В конфигурации контейнера мы указали регулярные выражения для имени хоста и пути. Подходит любое имя хоста, так как оно сопоставляется с образ¬
Простой веб-сервер, объекты EventEmitter и НТТР-клиенты цом . *. Регулярное выражение для пути распознает любой путь, начинающийся с /1/, и сохраняет «хвост» в запоминаемой группе (часть, заключенная в круглые скобки). При обращении к URL http: //localhost: 4080/l/code1 сведения о сопоставле¬ нии с путем оказываются в массиве req. basicServer. pathMatches, причем запом¬ ненная группа будет находиться в элементе req. basicServer. pathMatches[ 1 ]. Если URL успешно сопоставлен, то обработчик вернет HTTP-ответ с кодом состояния 302, а в заголовке Location будет URL, извлеченный из объекта code2url. Типы MIME и прт-пакет 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 ми 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: М05-свертка содержимого. Обработка куков Еще одна важная функция - поддержка куков. В протоколе HTTP нет поня¬ тия состояния, то есть веб-сервер не может сказать, что два запроса поступили от одного и того же клиента. Но как же тогда «войти» на сайт? Обычно сервер по¬ сылает браузеру кук, идентифицирующий зашедшего посетителя. А браузер воз¬ вращает полученный кук вместе с каждым запросом к этому серверу. В Basic Server включена частичная поддержка распознавания куков, посы¬ лаемых браузером. Обработчик запроса просматривает все заголовки в объекте req и запоминает обнаруженные заголовки Cookie в массиве (http: //en. wikipedia. org/wiki/HTTP_Cookie). Таким образом, мы знаем, какие куки послал браузер, и осталось только разо¬ брать hval и извлечь значение кука: var keys = Object.keys(req.headers); for (var i = 0, 1 = keys.length; i < 1; i++) { var hname = keys[i]; var hval = req.headers[hname]; if (hname.tolowerCaseO === 'cookie') { req.basicServer.cookies.push(hval); \ Чтобы отправить кук, нужно задать заголовок Set-Cookie или Set-Cookie2: res.setHeader( Set-Cookie2', ., cookie value ..); Кук представляет собой структурированную текстовую строку, поэтому ее раз¬ бор и форматирование естественно включить в такой каркас, как Basic Server (или Connect). Для этой цели уже существует несколько готовых библиотек, например: □ https://github.com/jed/cookies/: обеспечивает достаточно полную обра¬ ботку и контроль куков, в том числе подписанных;
панниш 1 Простой веб-сервер, объекты EventEmitter и НТТР-клиенты □ https: //github. com/bmeck/node-cookiej а г: простая библиотека для разбора куков. Виртуальные серверы и маршрутизация запросов Виртуальный хостинг - это способ организации нескольких доменных имен на одном 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, которые умеют отправлять НТТР-запро- сы и показывать результаты. Создайте файл wget. js и поместите в него такой код: var http = require( http'); var url = require( url); var util = require( util'); var argUrl = process.argv[2]; var parsedUrl = url.parse(argllrl, true); var options = { host: null, port: -1, path: null, method: GET' }; options.host = parsedUrl.hostname; options.port = parsedUrl.port; options.path = parsedUrl.pathname;
Теоретические основы EventEmitter 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.endO; Запустите этот скрипт следующим образом: $ 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 ":"BiglP","connection":"close","content-length”:"0"} Видно, что пришел HTTP-ответ с кодом состояния 302 (переадресация), ко¬ торый говорит браузеру о необходимости перейти по адресу http: //www. iana .org/ domains/example/. И действительно, если вы наберете в браузере адрес http:// example. com, то попадете на страницу iana .org. Задача скрипта wget. j s - отправить 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, содер¬ жащий заголовки, которые требуется включить в запрос. Например, вот как ука¬ зывается кук: ■■■I
Простой веб-сервер, объекты EventEmitter и НТТР-клиенты 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: высокоуровневая оберт¬ ка вокруг системы IITTP-запросов; □ 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: полезная интерактивная обо¬ лочка для отправки НТТР-запросов. Резюме В этой главе мы подробно рассмотрели следующие вопросы. □ Объекты EventEmitte г и их роль в реализации клиентских и серверных объ¬ ектов ННТР. □ Использование класса EventEmitter для отделения механизма получения данных запроса от операций над ними. □ Отладка с помощью прослушивания всех событий объекта HTTP или дру¬ гого объекта EventEmitter. □ Реализация НТТР-сервера. □ Маршрутизация входящих запросов в НТТР-сервере. □ Применение спецификации MIME для идентификации типа содержимого. □ Реализация НТТР-клиента. Теперь, вооружившись знаниями о реализации веб-приложений на платформе Node, мы готовы приступить к разработке полезных программ. Под этим мы пони¬ маем сохранение данных и выполнение каких-то действий с данными. В следую¬ щей главе мы рассмотрим несколько способов сохранения и выборки данных из внешних хранилищ. МН1И1!
Глава 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
Хранение и выборка данных (https://github.com/developmentseed/node-sqlite3) реализован интерфейс между SQLite3 и Node. Установка Если имеется менеджер пакетов npm, то установка модуля не вызывает ни ма¬ лейших сложностей: $ npm install sqlite3 Для установки этого модуля необходимо, чтобы в системе уже были уста¬ новлены библиотека sqlite3 и npm-модуль, содержащий платформенный (напи¬ санный на С) код, реализующий интерфейс с этой библиотекой. Библиотека по умолчанию присутствует в Mac OS X, а если в вашем любимом дистрибутиве Linux ее нет, то достаточно воспользоваться менеджером пакетов (например, apt-get install libsqlite3). На сайте sqlite3 (http://sqlite.org/) имеется документация по использованию этой базы данных, командным утилитам для работы с ней и С 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 (создание), findNoteByld (чтение), edit (обновление) и delete (удаление). Задача этого модуля - инкапсулировать обращения к SQLite3, изолировав их от прочих частей приложения Notes. Модуль предоставляет несколько функций
SQLite3 - облегченная встраиваемая база данных на основе SQL РШШ ■■■■■га для создания базы данных, добавления записей в таблицу, выборки всех строк из таблицы и удаления записей. Это позволяет сделать шаг в направлении архитек¬ туры модель - представление - контроллер: var util = require( util'); var sqlite3 = require( sqlite3'); sqlite3.verbose(); var db = undefined; exports.connect = function(callback) { db = new sqlite3.Database("chap06.sqlite3", sqlite3.0PEN_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) "+
Хранение и выборка данных "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 ], ■■Hill
SQLite3 - облегченная встраиваемая база данных на основе SQL function(err) { if (err) { util.log('FAIL on updating table ' + err); callback(err); } else callback(null); }); Функция edit позволяет обновить заметку, записав в поля новые значения. Мы пользуемся SQL-командой UPDATE с параметрами для каждого нового значе¬ ния и временной метки, идентифицирующей заметку. exports.allNotes = function(callback) { util. logC 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.findNoteByld = 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;
Хранение и выборка данных • > } >): Функция . f indNoteByld возвращает данные из заметки, идентифицируемой временной меткой. В базе данных должна быть только одна запись с данной вре¬ менной меткой, но на случай, если это вдруг не так, мы завели флаг, гарантирую¬ щий, что функция обратного вызова будет вызвана не более одного раза. Инициализация базы данных - setup.js Node-модуль sqlite3 пользуется библиотеками sqlite3, поэтому неудивитель¬ но, что все стандартные инструменты, поставляемые вместе с SQLite3, работают с базой данных, созданной с помощью node-sqlite3. Например, базу данных можно создать с помощью утилиты sqlite3, как показано на рисунке ниже: 4 Terminal — hash—* SB к 9 $ sqlite3 chap06.sqlite3 SQLite version 3.7.6 Enter ".help" for instructions Enter SQL statements terminated with a sqlite> CREATE TABLE notes (ts DATETIME, author VARCHAR(255), note TEXT); sqlite> .schema notes CREATE TABLE notes (ts DATETIME, author VARCHAR(255), note TEXT); sqlite> [] Но можно вместо этого написать скрипт setup, js, который инициализирует базу данных точно так же, как это делает модуль notesdb: var util = require('util'); var async = requireC async'); var notesdb = require(’./notesdb-sqlit03’); // 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); 12
SQLite3 - облегченная встраиваемая база данных на основе SQL II IfMi }); > ]. 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 ■UHII Хранение и выборка данных util.log( ALL DONE); notesdb.disconnect(functiorr(err) { >); }); Результат работы этого скрипта показан на рисунке ниже. ГЪ Г" Terminal — bash — SDxlS $ node show :« 6 Jul 15:39:32 - ROW: { ts: 1309991225472, author: 'Loren Ipsuw 12', note: 'Loreni ipsum dolor sit arnet, consectetur adipiscing el it. Integer nec odio. Praese | nt libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis se» at nibh elementum imperd iet. Duis sagittis ipsum. Praesent mauris. Fusee nec tellus sed augue semper porta. Mauris ; raassa. Vestibulum lacinia arcu eget nulLa. Class aptent tacitt sociosqu ad litora torquen t per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in libero. Sed dign issira lacinia nunc.' > 6 Gul 15:39:32 - ROW: { ts: 1309991225490, author: 'Lore» Ipsum 23', note: ‘Curabitur tortor. Pellentesque nibh. Aenean quaro. In scelerisque sem at dolor, lia kJ ecenas mattis. Sed convallis tristique sem. Proin ut ligula vel nunc egestas porttitor. Mo £ rbt lectus rtsus, iaculis vel, susctplt. quts, luctus non, massa. Fusee ac turpls quts Itgu la lacinia aliquet. Mauris ipsum. Nulla metus metus, ullamcorper vel, tincidunt sed, euism , Собираем приложение Notes воедино - app.js Итак, мы видели, как обращаться к базе данных с помощью модуля notesdb- sqlite3. js, теперь соберем все вместе в простое веб-приложение на основе Ex¬ press, рассматривая модуль notesdb-sqlite3. js как модель, а модуль арр. js - как контроллер. Для представления используем шаблоны, с которыми познакомимся чуть ниже. Как и два предыдущих скрипта, show.js и setup.js, скрипт app.js написан так, чтобы было легко перейти от модуля notesdb-sqlite3. js к модулю notesdb-mongoose. j s: var util = require('util'); var url = require('url'); var express = require('express'); var nmDbEngine = 'sqlite3'; // var nmDbEngine = 'mongoose'; var notesdb = requireC./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. j s не изменяется:
SQLite3 - облегченная встраиваемая база данных на основе SQL 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.renderCviewnotes.html', { title: "Notes ("+nmDbEngine+')", notes: notes }); >); )): Здесь мы выводим в браузер список заметок. Прежде всего мы переадресуем запрос к / на адрес /view, вызывая функцию res. redirect('/view'). Страница /view служит основным интерфейсом приложения, на нее переадресуют многие функции маршрутизатора. Для отрисовки страницы используется шаблон viewnotes. html, который будет рассмотрен ниже. Ему передаются две переменные: title - строка заголовка страницы и notes - массив заметок. арр.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) {
Хранение и выборка данных if (error) throw error; res.redirect('/view'); }); Эти функции маршрутизируют запросы на добавление заметок в базу. Стоит обратить внимание на две функции маршрутизатора для URL /add. Функция арр.get('/add', ...) вызывается, когда пользователь нажимает кнопку Add. В этом случае браузер посылает GET-запрос к ресурсу с URL /add. Функция отрисовывает по шаблону addedit.html форму, в которой пользователь сможет ввести заметку и нажать кнопку Submit. Шаблон addedit. html, используемый в обеих операциях /add и /edit, ожидает, что ему будет передан объект Note. Объект notesdb.emptyNote представляет пус¬ тую заметку, то есть подходит для случая, когда объект Note еще не существует. Функция арр. post( /add , ...) вызывается в ответ на отправку формы, ког¬ да браузер посылает POST-запрос. Введенные пользователем данные передают¬ ся в теле запроса, которое обрабатывается функцией промежуточного уровня bodyParser (арр. use (express. bodyParser())) и доступно с помощью свойства req. body. Поэтому к данным можно обращаться через переменные req. body. author и req.body.note. app.get('/del', parseUrlParams, function(req, res) { notesdb.delete(req.urIP.query.id, function(error) { if (error) throw error; res.redirect('/view'); )); }); Это функция, которой направляются запросы на удаление заметок из базы. Мы используем parseUrlParams как функцию промежуточного уровня, потому что идентификатор заметки передается в строке запроса в виде параметра id. По¬ этому для получения id можно сразу написать req. urlP.query, id, а не разбирать URL самостоятельно. По завершении операции notesdb. delete мы возвращаемся в приложение, переадресуя браузер на начальную страницу /view. арр.get(’/edit‘, parseUrlParams, function(req, res) { notesdb.findNoteById(req.urlP.query.id, function(error, note) { if (error) throw error; res.renderCaddedit.html', { title: "Notes C'+nmDbEngine+")", postpath: '/edit', note: note }); }): ■■■!
ШШШ\ SQLite3 - облегченная встраиваемая база данных на основе SQL }); 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); Это функции редактирования заметок. Мы снова используем функцию pa rseU rlPa rams для получения идентификатора заметки из параметров запроса, а затем выбираем эту заметку из базы с помощью функции notesdb. f indNoteByld. Обратите внимание, что страница отрисовывается по тому же шаблону addedit. html, но на этот раз мы передаем в шаблон объект Note, выбранный из базы данных. В переменную postpath раньше записывалась строка /add, а теперь /edit. Ее значение используется в шаблоне addedit. html для задания адреса отправки формы. В результате будет вызвана либо функция арр. post('/addлибо арр. 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 в файле арр. js. Теперь обратимся к файлу viewnotes. html: <table><% notes.forEach(function(note) { %> <tr><td> <p><%= new Date(note.ts).toString() %>: by <b><%= note.author %></b></p>
Хранение и выборка данных <р><%= note.note %></р> </td><td> <form method='GET' action='/del'> <input type='submit' value='Delete' /> <input type='hidden' name='id' value='<%= note.ts %>'> </form> <br/xform method='GET' action='/edit'> <input type='submit' value='Edit' /> <input type='hidden' name='id' value='<%= note.ts %>'> </form> </tdx/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/> ctextarea rows=5 cols=40 name='note' ><%= note.note %></textarea> <br/xinput type='submit' value='Submit' /> </form> Эта форма используется для добавления (/add) и редактирования (/edit) заметок. В переменной post path передается URL, на который отправляется форма. Остальные значения берутся из объекта Note, передаваемого из app. js; это может быть и объект emptyNote. Запуск приложения SQLite3 Notes Теперь все части встали на свои места и можно запускать приложение Notes. Если раньше вы выполняли скрипт setup, js, то база данных уже подготовлена. Если нет, сделайте это сейчас. Затем выполните такую команду: ■■НИИ $ node app
SQLite3 - облегченная встраиваемая база данных на основе SQL ними Поскольку мы прослушиваем порт 3000 (арр. listen(3000)), то заходить в прило¬ жение нужно по адресу http: //localhost: 3000/. Вы должны увидеть такую страницу: Щ Note* Л + / кка! host: 3ООО/view С. \ (<*• 'И У^144^М<|!|ИИИИНИИИИрИ^ИИ||ИИ^НИНИИИН1рИИ^И^ИИИИ1 МММИвКВкЭ) 1 : Notes (sqlite3) Wed iu! (X) 2011 15:27:03 GMT-0700 (TDT): by Lorem Ipsum 12 Lorem ipsum dolor sit amet, consectetur adipiscing eKt Integer nee odio. Praesent libero. Sed cursus ante dapibus diiam. Sed nIM. Nulla qtiis sem at nibh elemenmm imperdiet. Duls sagittk ipsum. Praesent mauris Fusee ncctetlu* scd augtie semper porta. Mairrls massa. Vesiibulum ladnia nrou eget nulla. Class aptent B?cW sockv^qu ad Htnra tonjnent per crmubia nostra, per incepto* himenacos. Curabitur sodales ligula in libero. Scd dig nissim lacinia nunc. Wed Jul 06 2011 15:27:05 GMT-0700 (PDT): by Lorem Ipsum 25 Curabitnr tmror. РеПешс-sque nibh. Aerenn quam. fn scelerisqne sem at dolor. Maecenas mettis. Sed convnllte rristique sem Froln tit ligula vel nunc cjErestas porttitor. Mortis Jcctus ristis, laculis vei, snscipit quis, hietus non, massa. Fusee ac mrpis quis ligula lacinia aliquot. Mauris ipsum. NuUa metus metus, ullamcoiper vel, tinckiuntsed, euismod in, nibh. Qjisque volutpat condimentum velii. Class aptent taciti sociosqu ad fitoia tofquent per conubia nostra, per inceptos himenaeos. Wed Jui 06 2011 15:27:05 GMT-0700 (PDT): by Lorem Ipsum 34 Nam nee ante Sed lacinia. tima non tinckiunt mattls, rorror neque adipiscing diam, а enrsus Ipsum ante quis mrpis. Nulls tartM. 1Г1 ftingllla. Suspendlsse potent!. Nunc feuglat rri a tcllus consequal imperdtei. Vcsdbulum sapfon. Pruln quarn.Edam (Й) uhnces. Suspendisse in justo eu magna luctus suscipit, Scd lectui. Wed Jul 06 2011 15:27:05 GMT-0700 (PDT): by Lorem Ipsum 45 Integer euismod Incus luctus magna. Quiaque cursus, metus vitae pfcarctra auctor, sem mas»a nvattis sem, at intcnlum magna auRue.euet diam. Vcstibuhim arm ipwra jnirnjs in fawcibm orci luctus et ultriccs posuere cubilia Curae; Motbi lacinia (Ш) (Ш) <Ш) Если нажать на кнопку Delete, то окно браузера обновится, но той заметки, рядом с которой находилась нажатая кнопка, уже не будет. Обновление произо¬ шло потому, что функция арр. get (- /del ’...) вызывает notesdb. delete, а затем сразу переадресует браузер на страницу /view. Теперь можете либо добавить заметку (щелчок по ссылке Add), либо изменить (щелчок по кнопке Edit). На экране появится такая страница: 0 Ш . Notes (sqi*te3) f <1 * М О : | а : А и «■ И 1 J 1 ! ♦«.: . +, J0 http://loealhost;3000/edit?i<! С | : Or Google j # | К»1« МИМ.» j Notes (sqlite3) View 1 Add Lorem Ipsurr 12 Lorem ipsum dolor sit amet, tonsecietur adipiscing elit. Integer net odio. j Praesent libero. Sed cursus arte dapibus dlam. Sed nisi. NuHa guls sem at j nibh eernentum Imperdiet. Du is sagrttis Ipsum. Praesent maurJs. Fusee nec j teltus scd augue semper porta, Mauris massa. Vestibulum lacinia arcu eqet I nulla. Class aptent tacltl socicsqu ad litora torquent per eonubia nostra, per inceptos himenaeos. Curabitur sodales ligula in fibero. Sed dignissim lacinia nunc. { Submit } ........... Li ;—i A При нажатии кнопки Submit вызывается либо арр. post( /add'либо арр. post(' / edit',...), и обе функции обновляют базу данных и переадресуют браузер на страницу /view.
БШИИН Хранение и выборка данных Обработка ошибок и отладка Если в программе имеется дефект или возникла какая-то проблема, то прило¬ жение возбудит исключение. Отладка заключается в том, чтобы вывести информа¬ цию о том, когда и где произошла ошибка. В приложениях для Notes мы использо¬ вали для вывода такой информации функцию util. log. В модуле notesdb-sqlite3. js для отправки сведений о возникающих ошибках приложению app. js применя¬ ются функции обратного вызова. По умолчанию Express возвращает браузеру удобную для разработчика стра¬ ницу с трассировкой стека. О О http://localhost3000/view 1 ► i’-fc A JA < !ИД I ,S *'♦ + ftiuip/,k«..lhmi5Q00/v**- С a--.,I : * TypeError: /Users/davidherron/Ncde. J5/chap06/ncwap|>/viev»/viewnotoe .html*2 1j <table> » 2\ <% notea. forEacfc{function(cote) { %> 3 <tr><td> 4j <p><l* new Dete(note.ts).to8tring{) %>j by <b»<%* note.author %></bX/p> Sf <p><%“ note. not.a *></p> Object #<Object> has no method * forEach' at Object.anonymous (oval at <anonysious> ( / User */ da vidherron /Nods .JS/chap06/ node _»odu Iks / ejs / lib/в jj» . jstl95tl2}) at Object.<anonymous> {/Usors/davidhorron/Node.JS/chap06/node modules/eja/1ib/Qja.jst19?t15) at ServerReaponae._render (/Uaera/davidherron/Kode, JS/uhapO 6/nodemodultia/expruaa/lio/view. ja j 375» 21) at ServerResponse.render (/Users/davidherrcn/Ncde.JS/chapQ6/node nodules/express/lib/view.js:234i17) at Statement.<anonymoua> (/Usera/davidherrcn/Node.JS/chap06/newapp/app.ja*33117) at Statement.replacement (/Users/davidherron/Node.JS/chap06/node_snodules/sqlite3/lib/trace.jat 201 31) at Statement.replacement f/Usera/davidharrcn/Node. J6/chap06/node moduleo/aq1ito3/lib/trace, jfti 201 31) Cm formation, сИобя; Window> Для перехвата исключений, возникающих при вызове функции маршрутиза¬ ции или внутри next О, Express предоставляет функцию app. error. Если вы хотите изучить это поведение, то можете сознательно сделать какую- нибудь ошибку в коде, например попытаться вызвать метод объекта null: app.get( /del', parseUrlParams, function(req, res) { var notAllowed = null; notAllowed.deleteO; >); Однако показанная выше страница с информацией об ошибке полезна только разработчику, но никак не пользователю. Попытаемся улучшить ее. Один из спо¬ собов - включить в app. j s такой код: app.use(express errorHandler({ dumpExceptions: true })); Тогда в окне браузера появится сообщение «Internal Server Error». Оно уже не так раздражает пользователя, но все же оставляет желать лучшего. При этом интересная разработчику трассировка стека по-прежнему печатается на stderr, не обременяя пользователя ненужными деталями. Чтобы разработать по-настоящему дружелюбную к пользователю страницу ошибки, нужно написать подходящую функцию арр. е г го г, начав, например, с такой:
SQLite3 - облегченная встраиваемая база данных на основе SQL IIMI 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 %> В этом случае пользователь увидит в браузере страницу: О О Notes (sqlite3) ERROR View I Add Internal Server Error ERROR: TypcErmr: Cannot call method ’delete' of null Использование других СУБД на основе 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: //git hub. com/Sannis/node-mysql- libmysqlclient) - интерфейс к MySQL для Node на основе библиотеки lib- mysqlclient.
Хранение и выборка данных □ 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, описывающие данные, a Mongoose берет на себя заботу об их сохранении в MongoDB. Это чрезвычайно мощный инструмент, обладающий такими средствами, как встраиваемые документы, гибкая система типизации полей, контроль ввода полей, виртуальные поля и т. д. См. http://mongoosejs. сот/. Установка Mongoose Если уже установлен менеджер пакетов npm, то достаточно выполнить такую команду: $ npm install mongoose
Mongoose - интерфейс между Node и MongoDB 1 I ШИНКИ Перед тем как использовать Mongoose, необходимо установить и запустить экземпляр MongoDB. На сайте mongodb. com есть много готовых двоичных пакетов. Кроме того, они входят в большинство дистрибутивов Li ппх. Для Mac OS X пакет доступен через систему MacPorts. Дополнительные сведения можно найти на сайте проекта, особенно в кратком руководстве Quickstart для интересующей вас операционной системы (http: //www. mongodb. org/display/DOCS/Quickstart). Чтобы проверить работоспособность MongoDB, нужно проделать двухшаговую процедуру. Сначала запустите сервер MongoDB (mongod), указав локальный ката¬ лог данных, как показано на рисунке ниже: Н Terminal — mongod — 90x11 * \щ $ rm -rf data $ mkdir data $ mongod —dbpath ./data $ Thu Jul 7 15:19:22 MongoDB starting : pid=18682 port=27017 dbpath=./data 64-bit Thu Jul 7 15:19:22 db version vl.6.6-pre-, pdf lie version 4.5 Thu Jul 7 15:19:22 git version: nogitversion Thu Oul 7 15:19:22 sys Info: Darwin tippy.local 10.7.0 Darwin Kernel Version 10.7.0: Sat Jan 29 15:17:16 PST 2011; root:xnu-1504.9.37~1/RELEASE_I386 i386 B00ST_LIB_VERSI0N=1_45 Thu Jul 7 15:19:22 [initandIisten] waiting for connections on port 27017 Thu Jul 7 15:19:22 [websvr] web admin interface listening on port 28017 о Это полезно на этапе разработки. В любой момент процесс можно снять на¬ жатием Ctrl+C и с. помощью показанных выше команд перезапустить с чистым каталогом данных. Следующий шаг - убедиться, что MongoDB может работать в интерактивном режиме, описанном в кратком руководстве (см. ссылку выше): Terminal mongo 72x8 $ mongo MongoDB shell version: 1.6.6-pre- connect ing to: test > db.foo.save( { a : 1 } ) > db.foo.find() { "_id" : Objectld("4el6313464all26e2a76a030"), "a" : 1 > Здесь мы вставили документ ({ а: 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 (создание), findNoteByld (чтение), 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 мнва Здесь определяется схема, мы уже говорили о ней выше. Схема создается в предложении 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 = functioned, callback) { exports.findNoteById(id, function(err, doc) { if (err) callback(err); else { util.log(util.inspect(doc)); doc.remove(); callback(null); } >); } Процедура удаления заметки из базы состоит из двух шагов. Сначала мы из¬ влекаем заметку из базы с помощью функции f indNoteByld (см. ниже), а затем вы¬ зываем метод . remove полученного объекта. exports.edit = function(id, author, note, callback) { exports.findNoteById(id, function(err, doc) { if (err) callback(err); else {
134 НИН Хранение и выборка данных doc.ts = new DateO; 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(0, callback); exports.forAll = function(doEach, done) { Mote.find({}, function(err, docs) { if (err) { util.log(’FATAL '+ err); done(err, null); } docs.forEach(function(doc) { doEach(null, doc); »; done(null); >): } var findNoteByld = exports.findNoteByld = functioned, 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 манка В функции . findNoteByld вызывается Note. findOne для поиска заметки, иден¬ тифицируемой полем _id. Для этого мы передаем в качестве Query объект { _id: id }, чтобы модуль нашел документы, в которых значение поля _id равно id. Mon¬ goDB присваивает каждому хранимому документу гарантированно уникальный идентификатор, сохраняемый в поле _id. Его можно использовать для той же цели, для которой мы использовали поле ts в варианте приложения Notes для SQLite3, то есть для однозначной идентификации заметок. Объект Query в Mongoose умеет делать и многое другое, о чем можно прочитать на сайте mongoose j s. о rg. Инициализация базы данных - setup, js Как и в случае SQLite3, инициализировать базу данных можно двумя спосо¬ бами. Первый - воспользоваться командами оболочки mongo, как показано на ри¬ сунке ниже: m ^ rs Terminal — mongo — 76x12 $ m ? $ mongo | MongoDB shell version : 1.6.6-pre- connecting to: test > use chap06; switched to db chap06 > db.notes .save(-Г ts: ”Tue May 10 2011 20:26:38 GMT-0700 (PDT)", author: "so i meone", note: "A meaningful note" > db.notes.find(); и { ".id" : 0bjectld("4el6484f64f7b4392f563b7a"), "ts” : "Tue May 10 2011 20:2 U 6:38 GMT-0700 (PDT)" , "author" : "someone", "note" : "A meaningful note" > \t > D U-y- Второй - выполнить написанный ранее скрипт setup. j s. В нем есть две строки, выбирающие, какой модуль использовать: notesdb-sqlite3 или notesdb-mongoose. // var notesdb = require('./notesdb-sqlite3'); var notesdb = require('./notesdb-mongoose'); Перенесите комментарий с одной строки на другую, а затем выполните такую команду: $ node setup На консоль ничего не выводится, но можете воспользоваться скриптом show, js и посмотреть, что теперь находится в базе данных. Отображение заметок на консоли - show.js Для отображения всех документов в базе данных используется написанный ранее скрипт show, j s. Внесите в него такие же изменения, как в 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. j s раскрывает такой же API, как notesdb- sqlite.js, мы смогли повторно использовать скрипты setup, js и show.js с ми¬ нимальными модификациями. Это относится и к скрипту app. j s. Модификация выглядит несколько иначе, но преследует ту же цель. Однако из-за некоторых различий нам придется изменить файлы шаблонов. Внесите в app. j s такое изменение: // var nmDbEngine = 'sqlite3': var nmDbEngine = 'mongoose'; Затем создайте каталог views-mongoose и поместите в него файлы шаблонов. 1. Шаблон layout. html ничем не отличается от старого, поэтому просто ско¬ пируйте его: $ ср 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 llllll $ node app Как и прежде, зайдите из браузера на страницу http://localhost:3000/. Вы¬ глядит она почти так же, как вариант для SQLite3, только заголовок отличается: О О Ш Notes fmon^osi?) Ц ЫШ *|АМИ: I Г|(.♦ I : - Notes (mongoose) View I Add Thu Jul 07 2011 17:13:22 GMT-0700 (PDT): by Lorem Ipsum 12 €««!> Lorem Ipsum dolor sll amet, conscciccui adipbeing сШ. Integer nec odio. Praesent libero. Scd cursus ante dapibus diam. Scd nisi. Nulla quis sem at nibh cleroenmm hnperdiet. Duis sagittis ipsum. Praesent mauris. Fusee nec tellus sed augue semper porm. Mauris massa. Vestibulum luciniH arcru eget nulla. Class aptent tadti soclosqu ad litora torquent per conubla nostra, per Inceptos hlmenacos. Curabitur sodales ligula in libero. Sed dignissim lacinia cur.c. Thu Jul 07 2011 17:13:22 GMT-0700 (PDT): by Lorem Ipsum 23 Curabitur taftor. Pellentesque nibh. Acr.can quam In vcclcrisquc sem at dolor. Maecenas rnattis. Sed convallis tristiquc sem. Proin ut ligula vel nunc egestas porttitor, Morbi lectus risus, iaculis vel, suscipit quis, hictus non, massa. Fusee ас mrpis quis ligula lacinia alkjucL Mauris Ipsum. Nulla mems metus, uilamcorper (lait) vel, tinckiunt setl, euismod in, nibh, Quisque volutpat condimentum velit. Class aptent tadti soctosqu ad litora torquent per conubia nostra, per inceptos himcnacos. Thu Jul 07 2011 17:13:22 GMT-0700 (PDT): by Lorem Ipsum M Nam nec ante. Sed lacinia, uma non tincidunt mattis, tortor neque adipiscing diam, a cursus ipsum ante quis turpis. Nulla facilisi. Ut firingilia. Suspendisse potend. Nunc feugiat ml a tellus eonsequat imperdiet. (Idit) Vestibulum sapien. Proin quam. Edam ultrices. Suspendisse in jusfo eu magna luctus suscipit. Sed lectus. Ьш I —— _ - I — Этот вариант приложения 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 с синтаксисом в духе j Query. □ node-nosql-thin (https://github.com/dmcquay/node-nosql-thin) - библио¬ тека «тонкого» интерфейса к MongoDB, которая впоследствии, возможно, будет поддерживать и другие СУБД «без SQL». Краткий обзор средств аутентификации пользователей Во многих приложениях пользователи должны аутентифицироваться для доступа к привилегированным средствам. Поскольку HTTP - протокол без под¬ держки состояния, единственный способ аутентифицировать пользователя за¬ ключается в том, чтобы отправить кук его браузеру после выполнения некоего действия, подтверждающего подлинность пользователя. Кук должен содержать данные, с помощью которых приложение сможет впоследствии проверить, кто им пользуется. Мы кратко рассмотрим, как можно реализовать форму входа, послать браузеру кук и предотвратить доступ в случае отсутствия кука. Нам потребуется внести два изменения в скрипт app.js. Сначала добавим в конфигурацию серверного объекта ПУ-модуль cookieParser: var app = express.createServer(); app. use(express. loggerQ); app.use(express.cookieParser()); app.use(express.bodyParser()); Затем добавим небольшую функцию маршрутизатора, проверяющую, что пользователю разрешен доступ. В данном случае мы просто проверяем, что значе¬ ние кука равно АОК, считая, что это универсальный признак «все хорошо»: var checkAccess = function(req, res, next) { if (!req.cookies || !req.cookies.notesaccess || req.cookies.notesaccess l== "AOK") { res.redirect^ '/login'); } else { next(); > ПУ-модуль cookieParser проделывает всю грязную работу: ищет куки, разби¬ рает их и помещает значения в объект req. Если кук получен, то его значение будет присутствовать в массиве req. cookies, и мы сможем его проверить. Если куков нет вообще, или отсутствует кук notesaccess, или его значение не равно АОК, то браузер переадресует на URL-адрес /login.
Краткий обзор средств аутентификации пользователей IIIIIMI Перед тем как рассматривать обработчик 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', 'АОК'); 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, первая функция маршрутизатора отрисовывает шаблон, показанный на рисунке ниже: ' ШСЛМ {тапдсю$*>) 4 I * ! : И + //local ho* t: ЗООО/togin Cj :..Qs O.ooiyt- ***** Notes LOGIN (mongoose) Ykm»Add Click the Login to log in. В реальной системе нужно было бы, как минимум, включить поля для ввода имени и пароля пользователя. Мы же опустили эту деталь и просто предлагаем пользователю нажать кнопку Login.
Хронение и выборка данных Эта кнопка находится внутри формы, отправка которой приводит к вызову функции маршрутизатора арр. post( '/login'...). В реальной системе эта функция должна была бы проверить полученные учетные данные и отправить аутенти¬ фикационный кук лишь в случае, когда в базе данных есть такой пользователь. Мы же отправляем кук со значением АОК безусловно и переадресуем браузер на страницу /view. Хотя нашему приложению недостает некоторых существенных черт реальной системы защиты, общий механизм отражен верно. Существует много сайтов, на которых имеется форма входа и которые проверяют аутентификационный кук при запросе любой страницы. Мы реализовали функции, которые проверяют наличие кука, переадресуют пользователя на страницу входа, проверяют учетные данные и посылают аутентификационный кук браузеру. Резюме В этой главе мы многое узнали о работе с данными в Node. Так как это неотъ¬ емлемая черта многих приложений, резюмируем, чему мы научились. □ В состав Node не входит встроенная поддержка хранения данных, но со¬ общество Node разработало модули для организации интерфейса с самыми разными системами хранения, в том числе такими, о которых вы, возмож¬ но, даже не подозревали. □ Установка движка хранения данных обычно сопровождается установкой некоторых зависимостей, в том числе серверов и клиентских библиотек. □ SQLite3 - СУБД для разработки приложений на основе SQL, не требую¬ щая никакой настройки. □ Приложения, работающие с СУБД на основе SQL и с MongoDB, почти ни¬ чем не отличаются. □ Систему объектно-реляционного отображения, пожалуй, лучше использо¬ вать поверх СУБД на основе SQL, но сообщество разработало такие систе¬ мы и для MongoDB и CouchDB. □ Реализация архитектуры модель - представление - контроллер (не пол¬ ностью). □ Обработка форм в приложении на основе Express. □ Документо-ориентированные системы типа MongoDB ближе к современ¬ ным языкам программирования и приложениям, чем SQL. Мы проделали длинный путь. Начали мы с общего описания платформы Node и тех программ, которые можно на ней реализовать. Затем мы узнали, как устано¬ вить Node и npm для разработки и в производственной среде. После этого мы раз¬ работали несколько модулей и приложений для Node, чтобы понять, как строятся веб-приложения и клиент-серверные приложения на базе протокола HTTP, как устроен цикл обработки событий в Node, как преобразовать алгоритм, включаю¬ щий продолжительные вычисления, в пригодный для архитектуры с циклом об¬ работки событий, как делегировать часть работы фоновым процессам с помощью веб-служб и как работать с базами данных в приложении для Node. ■■■Ill
Предметный указатель А addedit.html, шаблон, 124, 126 add, функция, 116, 118 а! lNot.es функция, 119 app-connect.js, 81 app.js, 122, 125 арр-node.js, 66, 72 apt-get, инструмент, 50 В Basic Server виртуальный хостинг, 112 возможности, 108 конфигурирование виртуальных серверов, 108 модуль short url, 109 настройка, 105, 108 обработка favicon, 104 обработка куков, 111 обработчик статических файлов, 105 общее описание, 100 реализация, 101 ядро, 101 basicserver.js, файл, 101 bin, поле, 51 Cluster, 40 CommonJS, система организации модулей, 19, 44, 64 Connect, 18, 65, 77 настройка серверного объекта, 80 принципы работы, 80, 81 реализация Math Wizard, 78 установка, 79 connect, функция, 123 Content-Type, заголовок, 110 CouchDB, 115 С PAN, 50 createServer, функция, 102 CREATE TABLE, команда, 116 CRUD (Create, Read, Update, Delete), 115 curl, утилита, 112 D delete, функция. 116, 118 directories, поле, 51 disconnect, функция, 117, 123 dispatchToContainer, функция, 102 doc root, параметр, 106 doEach, функция, 119 done, функция, 119 E edit, функция, 116, 119 EventEmitter, объект обзор, 95 события, 97. 98 Express, 47. 65 реализация Math Wizard, 82 Express Math Wizard математический сервер, 89 обработка ошибок, 87 параметризованные URL-адреса, 88 реализация, 82 службы данных, 88 F factorial-node.js, 70 FastLegS, 130 favicon, 80, 99 обработчик, 104 faviconHandler.js, 104 fibo-node.js, 71 findNoteByld, функция, 116, 120 forAll, функция, 119 forever, 37 forms, модуль, 48 fs, модуль, 43 fugue, 37 H handle, функция, 105 home-node.js, 72 HTTPClient, объект, 95 создание запросов, 112 http.createServer, метод, 22 HTTPServer, объект, 66, 95 события, 99 HTTP Sniffer, 98 httpsniffer.js, 98 HTTP-заголовки, 111 HTTP, модули, 17 http, объект, 22 HTTP, протокол, 111 htutil.loadParams, 67 htutil.navbar, 73 htutil.page, 69 J JavaScript, 18 недостатки, 19 L launchd, скрипты для, 37 logger, ПУ-модуль, 80 M MacPorts. проект, 50 math.js, 72
вашим» Math Wizard URL-адреса, 66 макет страницы, 65 математические функции, вычисление, 70 обобщение, 73 обработка параметров запроса, 67 переработка, 92 продолжительные вычисления, 74 реализация с помощью Connect, 79 реализация с помощью Express, 82 реализация с помощью node, 66 создание, 66 Memcache, 115 MIME npm-пакет, 110 модуль, 100 спецификация, 110 MongoDB, 115, 130 поддержка, 137 Mongolia, 137 Mongoose, 130, 138 app.js, 136 модуль абстрагирования базы данных, 132 отображение заметок на консоли, 135 реализация приложения Notes, 131 установка, 130 Mongous, 138 mult-node.js, 69 MySQL, 115 N Node, 16 Math Wizard, 66 аси нхрон ная событи йно-ориенти рованная архитектура, 19 возможности, 17 движки сохранения данных, 115 достоинства, 18 запуск простого скрипта, 33 запуск сервера, 34 запуск серверов на этапе инициализации, 36 использование процессора, 21 количество серверов, 23 командные утилиты, 31 маршрутизация запросов, 66 мноюядерные системы, 40 модель, 16 модули, 43 настройка, 25 проверка установки, 31 производительность, 21 серверный JavaScript, 18 сетевой уровень, 17 средства разработки в Mac OS X, 26 установка в POSIX-совместимых системах, 26 установка нескольких экземпляров, 31 экологичный Интернет, 23 Node-DBI, 130 nodemodules, каталог, 45, 46 Node-mongodb, 137 node-mongodb-native, 137 node-mongolian, 137 Node-mysql, 129 Node-mysql-libmysqlclient, 129 Предметный указатель Node-mysql-native, 129 node-nosql-thin, 138 Node-orm, 130 NODE PATH, переменная окружения, 48 Node-postgres, 130 Node-sqlite3,130 notesdb-mongoose.js, 132 notesdb-sqlite3.js, 116 Notes, приложение, 122 реализация помощью Mongoose, 131 реализация помощью SQLite3, 116 шаблоны, 125 npm adduser, команда, 60 help, команда, 53 init, команда, 59 install, команда, 59 list, команда, 56, 61 publish, команда, 60 unpublish, команда, 60 версии пакетов, 61 использование установленных пакетов, 55 конфигурационные параметры, 60 обновление пакета, 58 описание, 50 перечень установленных пакетов, 56 редактирование установленного пакета, 57 скрипты в составе пакета, 57 установка, 35 npm-пакеты, 50 версии,61 зависимости, 62 поиск, 52 просмотр информации, 54 публикация, 60 разработка, 58 установка, 54 формат, 50 О options, объект, 102 Р package.json, файл, 49 parseUrlParams, функция, 123 PEAR, 50 persistence.js, 130 Postgres, 115 postpath, переменная, 125 processHeaders, функция, 102 process.nextTick, функция, 75 Pulser, класс, 96 pulse, событие, 96 Q qs, модуль, 47 REDIS, 115 replace, функция, 69 Request, объект, 97 require, функция, 43 req, объект, 88, 138 RingoJS, каркас разработки приложений, 18 router, ПУ-модуль, 80
Предметный указатель 143 rpm, инструмент, 50 run, функция, 118 S Sequel ize, 130 setup.js, 120, 135 shorturl, модуль, 109 show.js, 121 sniffOn, функция, 99 SQLite3,115 app.js, 122 запуск приложения Notes, 126 инициализация базы данных, 120 модуль абстрагирования базы данных, 116 обработка ошибок, 128 отображение заметок на консоли, 121 реализация приложения Notes, 116 установка, 116 шаблоны приложения Notes, 125 square-node.js, 70 start, метод, 96 staticHandler.js, 105 static, ПУ-модуль, 80 Swing, 17 T tgz-архивы, 51 U UPDATE, SQL-команда, 119 Upstart, 37 V V8, движок, 17 w wget, утилита, 112 Y YQL, 115 YSlow, 74 yum, инструмент, 50 A Абсолютные идентификаторы модуля, 45 Асинхронная событийно-ориентированная архитектура обзор, 19 сравнение с многопоточной, 20 Аутентификация пользователей, 138 Б База данных инициализация, 120 отображение на консоли, 121 управление соединением, 123 Блокирующий ввод/вывод, 20 В Веб-каркасы, 65 Виртуальный хостинг, 112 Внешние зависимости, 46 Г Глобальный Объект, 19, 64 д Движки сохранения данных, 115 И Инкапсуляция, пример, 44 К Командные утилиты Node, 31 Контейнеры, 102 Конфигурирование виртуальных серверов, 108 Куки, 111 Л Локальные модули, 45 М Математические функции, вычисление, 70 Многоядерные системы, 40 Модуль идентификаторы, 44 инкапсуляция, 63 локальный, 45 определение, 43 пример, 43 системный, 48 составной, 49 Модуль абстрагирования базы данных, SQLite3, 116 О Обработчик статических файлов, 105 Отрисовка страницы, 123 Ошибки, обработка в Express Math Wizard, 87 П Переработка алгоритма, 74 ПО промежуточного уровня, 78 поставщики, 78 фильтры, 78 Поставщики, 78 Потоки, сравнение с асинхронной событийно¬ ориентированной архитектурой, 20 Приложение, внешние зависимости, 46 Р Разработка средства, 26 установка в Linux с помощью систем управления пакетами, 30 установка в Mac OS X с помощью homebrew, 30 установка в Mac OS X с помощью MacPorts, 29 установка в домашний каталог, 27 установка в системный каталог, 29 С Семантическая версионность, 61 Системные модули, 48 Составные модули, 49 У Установка Connect, 79 Mongoose, 130 npm-пакета, 54 SQLite3,116 ф Фибоначчи числа, 71 Фильтры, 78 Функции маршрутизатора, 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. Формат 70x100 1/16. Гарнитура «Петербург». Печать офсетная. Уел. печ. л. 34,5. Тираж 1000 экз. МЬ Web-сайт издательства: www.dink-press.rn
Эта книга — прекрасная отправная точка, с которой можно начать путешествие в увлекательный мир разработки веб-приложений для Node.js. На практических при¬ мерах вы научитесь пользоваться серверным и клиентским объектами HTTP, карка¬ сами Connect и Express, освоите алгоритмы асинхронного выполнения и узнаете, как работать с базами данных на основе SQL и с MongoDB. Начав с практических рекомендаций по установке и настройке Node.js в режиме разработки и эксплуатации, вы научитесь разрабатывать клиентские и серверные HTTP-приложения. Вы узнаете различные способы работы с Node.js, в том числе применение движков баз данных и создание сайтов с применением и без приме¬ нения каркасов Connect и Express. Вы также познакомитесь с применяемой в Node.js системой организации модулей на основе спецификации CommonJS, которая по¬ зволяет реализовать представительное подмножество технологии объектно-ориен¬ тированного проектирования. Из этой книги вы узнаете: • Роль JavaScript на стороне сервера в разработке веб-приложений • Понимание архитектурных решений, принятых в Node.js для повышения производительности и пропускной способности • Установка и эксплуатация Node.js в режиме разработки и в производственной среде • Каркасы разработки приложений Connect и Express • Работа с СУБД на основе SQL и с MongoDB • В чем состоит различие между синхронными и асинхронными алгоритмами и почему это важно Если вы — разработчик, желающий использовать язык JavaScript на сто¬ роне сервера для быстрого создания высокопроизводительных веб¬ приложений, то эта книга для вас. Для ее чтения желательно знакомство хотя бы с основами JavaScript и веб-разработки. Internet-магазин: www.alians-kniga.ru Книга - почтой: e-mail: orders@alians-kniga.ru Оптовая продажа: "Альянс-книга" (495)258-9194, 258-9195 e-mail: books@alians-kniga.ru издательство ISBN 978-5- 94074-. 309-0 9 78594С 74809 >