2-е издание вдеис ■ Алекс Янг Брэдли Мек Майк Кантелон А также Тим Оксли Марк Хартер Т. Дж. Головайчук Натан Райлих I I H A N N IN G ПИТЕР Node.js in Action S e c o n d E d it io n ALEXYOUNG BRADLEY MECK MIKE CANTELON WITH TIM OXLEY MARC HARTER T.J. HOLOWAYCHUK NATHAN RAJLICH ■1 MANNI NG S helter Islan d Алекс Янг, Брэдли Мек, Майк Кантелон А также Тим Оксли, М арк Хартер, Т. Дж. Головайчук, Натан Райлих в действии 2-е издание Санкт-Петербург •Москва •Екатеринбург •Воронеж Нижний Новгород •Ростов-на-Дону Самара •Минск 2018 ББК 32.988-02-018 УДК 004.738.5 Я60 Я нг А., М ек Б., Кантелон М. Я60 Node.js в действии. 2-е изд. — СПб.: Питер, 2018. — 432 с.: ил. — (Серия «Для профессионалов»). ISBN 978-5-496-03212-4 Второе издание «Node.js в действии» было полностью переработано, чтобы отражать реалии, с ко­ торыми теперь сталкивается каждый Node-разработчик. Вы узнаете о системах построения интерфейса и популярных веб-фреймворках Node, а также научитесь строить веб-приложения на базе Express с нуля. Теперь вы сможете узнать не только о Node и JavaScript, но и получить всю информацию, включая системы построения фронтэнда, выбор веб-фреймворка, работу с базами данных в Node, тестирование и развертывание веб-приложений. Технология Node все чаще используется в сочетании с инструментами командной строки и на­ стольными приложениями на базе Electron, поэтому в книгу были включены главы, посвященные обеим областям. 16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 4Э6-ФЗ.) ББК 32.988-02-018 УДК 004.738.5 Права на издание получены по соглашению с Manning. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как на­ дежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 978-1617292576 англ. 978-5-496-03212-4 © © © © 2017 by Manning Publications Co. All rights reserved Перевод на русский язык ООО Издательство «Питер», 2018 Издание на русском языке, оформление ООО Издательство «Питер», 2018 Серия «Для профессионалов», 2018 Краткое содержание Часть I. Знакомство с Node......................................................................21 Глава 1. Знакомство с N o d e .js............................................................................................ 22 Глава 2. Основы программирования N o d e .................................................................... 41 Глава 3. Что представляет собой веб-приложение N ode?.......................................... 76 Часть II. Веб-разработка с использованием Node ............................. 93 Глава 4. Системы построения фронтэнда....................................................................... 94 Глава 5. Ф реймворки на стороне сервера...................................................................... 110 Глава 6. Connect и E x p re ss.................................................................................................142 Глава 7. Ш аблонизация веб-приложений..................................................................... 201 Глава 8. Хранение данных в прилож ениях....................................................................228 Глава 9. Тестирование приложений Node...................................................................... 278 Глава 10. Развертывание и обеспечение доступности приложений N ode............308 Часть III. За пределами веб-разработки ............................................325 Глава 11. Написание приложений командной строки ............................................... 326 Глава 12. Разработка настольных приложений с использованием Electron ......339 Приложения............................................................................................. 359 A. Установка Node ............................................................................................................... 360 Б. Автоматизированное извлечение веб-данны х........................................................365 B. Официально поддерживаемые промежуточные ком пон ен ты .......................... 378 Глоссарий....................................................................................................................... 418 Оглавление Предисловие ....................................................................................................................15 Благодарности ................................................................................................................ 16 О книге.............................................................................................................................. 17 Структура ......................................................................................................................... 17 Правила оформления и загрузка примеров ко д а................................................... 18 Об авторах....................................................................................................................... 19 Алекс Янг ...................................................................................................................19 Брэдли Мек ...............................................................................................................19 Иллюстрация на обложке ........................................................................................... 20 От издательства ............................................................................................................. 20 Часть I. Знакомство с Node......................................................................21 Глава 1. Знакомство с Node .js .................................................................................... 22 1.1. Типичное веб-приложение Node ....................................................................... 22 1.1.1. Неблокирующий ввод/вывод ....................................................................23 1.1.2. Цикл событий ............................................................................................... 25 1.2. ES2015, Node и V8 .................................................................................................. 26 1.2.1. Node и V 8 ........................................................................................................... 29 1.2.2. Работа с функциональными гр у п п ам и .................................................... 30 1.2.3. График выпуска версий Node ....................................................................31 1.3. Установка Node ........................................................................................................ 31 1.4. Встроенные средства N o d e ................................................................................... 32 1.4.1. npm ...................................................................................................................33 1.4.2. Базовые модули ........................................................................................... 34 1.4.3. О тл ад ч и к...........................................................................................................35 1.5. Три основных типа программ N ode.................................................................... 36 1.5.1. Веб-приложения ........................................................................................... 37 1.5.2. Средства командной строки и д е м о н ы .................................................... 38 1.5.3. Настольные приложения ........................................................................... 39 1.5.4. Приложения, хорошо подходящие для Node ........................................ 39 1.6. Заключение ............................................................................................................. 40 Оглавление 7 Глава 2. Основы программирования N o d e ................................................................41 2.1. Структурирование и повторное использование функциональности N o d e ..................................................................................................................................... 41 2.2. Создание нового проекта Node .......................................................................... 44 2.2.1. Создание модулей........................................................................................... 44 2.3. Настройка создания модуля с использованием m odule.exports................ 47 2.4. Повторное использование модулей с папкой node_m odules..................... 48 2.5. Потенциальные проблемы ................................................................................... 48 2.6. Средства асинхронного программирования ................................................... 51 2.7. Обработка одноразовых событий в обратных вызовах .............................. 52 2.8. Обработка повторяющихся событий с генераторами событий ................ 56 2.8.1. Пример генератора собы тий ........................................................................56 2.8.2. Реакция на событие, которое должно происходить только один раз .......................................................................................................................58 2.8.3. Создание генераторов событий: п убл и кац и я/п одп и ска.....................58 2.8.4. Доработка генератора событий: отслеживание содержимого ф ай л о в ...........................................................................................................................62 2.9. Проблемы с асинхронной разработкой ............................................................ 64 2.10. Упорядочение асинхронной логики................................................................. 65 2.11. Когда применяется последовательный поток в ы п о л н ен и я..................... 67 2.12. Реализация последовательного потока вы п о л н ен и я................................. 68 2.13. Реализация параллельного потокавы п олн ен и я.......................................... 71 2.14. Средства, разработанные в сообществе ......................................................... 73 2.15. Заклю чение............................................................................................................. 75 Глава 3 . Что представляет собой веб-приложение N o d e ? .................................... 76 3.1. Структура веб-приложения N o d e....................................................................... 77 3.1.1. Создание нового веб -п ри л ож ен и я............................................................77 3.1.2. Сравнение с другими платформами ........................................................79 3.1.3. Что дальше? ................................................................................................... 79 3.2. Построение R EST-совместимой веб-службы ................................................ 80 3.3. Добавление базы д а н н ы х ...................................................................................... 83 3.3.1. Проектирование собственного API м одели ............................................ 84 3.3.2. Преобразование статей в удобочитаемую форму и их сохранение для чтения в будущем ........................................................................................... 87 3.4. Добавление пользовательского интерфейса ................................................... 88 3.4.1. Поддержка разных форматов ....................................................................89 3.4.2. Визуализация шаблонов ........................................................................... 89 3.4.3. Использование npm для зависимостей на стороне клиента .............90 3.5. Заключение ............................................................................................................. 92 8 Оглавление Часть II. Веб-разработка с использованием Node ............................. 93 Глава 4. Системы построения фронтэнда ................................................................94 4.1. Ф ронтэнд-разработка с использованием Node ............................................. 94 4.2. Использование npm для запуска сценариев ................................................... 95 4.2.1. Создание специализированных сценариев n p m .................................... 97 4.2.2. Настройка средств построения ф р о н тэн д а............................................ 98 4.3. Автоматизация с использованием G u lp ............................................................ 98 4.3.1. Добавление Gulp в п р о ек т............................................................................99 4.3.2. Создание и выполнение задач Gulp .................................................... 100 4.3.3. Отслеживание и зм ен ен и й ........................................................................ 102 4.3.4. Использование отдельных файлов в больших проектах................. 102 4.4. Построение веб-приложений с использованием webpack.......................... 104 4.4.1. Пакеты и плагины ....................................................................................... 104 4.4.2. Настройка и запуск w ebpack.................................................................... 105 4.4.3. Использование сервера для разработки w e b p a c k ............................. 106 4.4.4. Загрузка модулей и активов CommonJS ............................................ 107 4.5. Заклю чение............................................................................................................... 109 Глава 5 . Фреймворки на стороне сервера ............................................................ 110 5.1. Персонажи ............................................................................................................... 110 5.1.1. Фил: штатный разработчик .................................................................... 111 5.1.2. Надин: разработчик открытого к о д а .................................................... 111 5.1.3. Элис: разработчик п родукта.................................................................... 112 5.2. Что такое ф рейм ворк?........................................................................................... 112 5.3. K o a .............................................................................................................................. 113 5.3.1. Настройка ................................................................................................... 115 5.3.2. Определение маршрутов ........................................................................ 116 5.3.3. REST A P I....................................................................................................... 116 5.3.4. Сильные сторон ы ....................................................................................... 117 5.3.5. Слабые стороны........................................................................................... 117 5.4. K ra k e n ........................................................................................................................ 117 5.4.1. Настройка ................................................................................................... 118 5.4.2. Определение маршрутов ........................................................................ 118 5.4.3. REST A P I....................................................................................................... 119 5.4.4. Сильные сторон ы ....................................................................................... 119 5.4.5. Слабые стороны........................................................................................... 120 5.5. h a p i.............................................................................................................................. 120 5.5.1. Настройка ................................................................................................... 121 5.5.2. Определение маршрутов ........................................................................ 121 5.5.3. Плагины ....................................................................................................... 122 Оглавление 9 5.5.4. REST A P I....................................................................................................... 123 5.5.5. Сильные сторон ы ....................................................................................... 124 5.5.6. Слабые стороны........................................................................................... 124 5.6. Sails.js ........................................................................................................................ 124 5.6.1. Настройка ................................................................................................... 125 5.6.2. Определение маршрутов ........................................................................ 126 5.6.3. REST A P I....................................................................................................... 126 5.6.4. Сильные сторон ы ....................................................................................... 127 5.6.5. Слабые стороны........................................................................................... 127 5.7. DerbyJS ..................................................................................................................... 127 5.7.1. Настройка ................................................................................................... 128 5.7.2. Определение маршрутов ........................................................................ 129 5.7.3. REST A P I....................................................................................................... 130 5.7.4. Сильные сторон ы ....................................................................................... 130 5.7.5. Слабые стороны........................................................................................... 130 5.8. Flatiron.js .................................................................................................................. 131 5.8.1. Настройка ................................................................................................... 131 5.8.2. Определение маршрутов ........................................................................ 132 5.8.3. REST A P I....................................................................................................... 133 5.8.4. Сильные сторон ы ....................................................................................... 133 5.8.5. Слабые стороны........................................................................................... 134 5.9. LoopBack .................................................................................................................. 134 5.9.1. Настройка ................................................................................................... 135 5.9.2. Определение маршрутов ........................................................................ 137 5.9.3. REST A P I....................................................................................................... 137 5.9.4. Сильные сторон ы ....................................................................................... 138 5.9.5. Слабые стороны........................................................................................... 138 5.10. С равнение............................................................................................................... 138 5.10.1. Серверы H T T P и маршруты ................................................................ 140 5.11. Написание модульного кода ............................................................................ 140 5.12. Выбор персонажей ..............................................................................................141 5.13. Заклю чение............................................................................................................ 141 Глава 6 . Connect и E xpress........................................................................................ 142 6.1. Connect ..................................................................................................................... 142 6.1.1..Настройка приложения C o n n ect............................................................ 143 6.1.2. Как работают промежуточные компоненты Connect ..................... 143 6.1.3. Объединение промежуточных компонентов .................................... 144 6.1.4. Упорядочение ком п он ен тов.................................................................... 145 6.1.5. Создание настраиваемых промежуточных компонентов ............. 146 10 Оглавление 6.1.6. Использование промежуточных компонентов для обработки ошибок ....................................................................................... 148 6.2. Express........................................................................................................................ 151 6.2.1. Генерирование заготовки п р и л о ж ен и я................................................ 152 6.2.2. Настройка конфигурации Express и п рилож ени я............................. 157 6.2.3. Визуализация представлений ................................................................ 159 6.2.4. Знакомство с маршрутизацией в E xpress............................................ 165 6.2.5. Аутентификация п ользователей ............................................................ 173 6.2.6. Регистрация новых пользователей........................................................ 179 6.2.7. Вход для зарегистрированных пользователей ................................ 185 6.2.8. Промежуточный компонент для загрузки пользовательских дан ны х....................................................................................................................... 189 6.2.9. Создание открытого REST API ............................................................ 191 6.2.10. Согласование контента............................................................................ 197 6.3. Заклю чение............................................................................................................... 200 Глава 7 . Шаблонизация веб-приложений ............................................................ 201 7.1. Поддержка чистоты кода путем ш аблонизации............................................ 201 7.1.1. Ш аблонизация в действии........................................................................203 7.1.2. Визуализация H TM L без ш аблона........................................................ 205 7.2. Ш аблонизация с E JS ..............................................................................................207 7.2.1. Создание шаблона....................................................................................... 207 7.2.2. Интеграция шаблонов EJS в приложение ........................................ 209 7.2.3. Использование EJS в клиентских приложениях ............................. 210 7.3. Использование язы ка M ustache с шаблонизатором H ogan........................211 7.3.1. Создание шаблона....................................................................................... 212 7.3.2. Теги M ustache............................................................................................... 212 7.3.3. Тонкая настройка Hogan............................................................................215 7.4. Ш аблоны Pug ..........................................................................................................215 7.4.1. Основные сведения о P u g ........................................................................217 7.4.2. Программная логика в шаблонах Pug ................................................ 219 7.4.3. Организация шаблонов P u g ....................................................................223 7.5. Заклю чение............................................................................................................... 227 Глава 8 . Хранение данных в приложениях............................................................ 228 8.1. Реляционные базы д ан н ы х .................................................................................. 228 8.2. P ostgreS Q L ............................................................................................................... 228 8.2.1. Установка и настройка............................................................................... 229 8.2.2. Создание базы данных............................................................................... 229 8.2.3. Подключение к Postgres из N ode............................................................230 8.2.4. Определение таблиц ............................................................................... 230 Оглавление 11 8.2.5. Вставка д ан н ы х ........................................................................................... 231 8.2.6. Обновление дан ны х................................................................................... 231 8.2.7. Запросы на выборку данны х....................................................................232 8.3. Knex ........................................................................................................................... 232 8.3.1. jQ uery для баз данны х............................................................................... 233 8.3.2. Подключение и выполнение запросов в K nex.................................... 234 8.3.3. Переход на другую базу д а н н ы х ............................................................236 8.3.4. Остерегайтесь ненадежных абстракций ............................................ 236 8.4. M ySQL и P o stg re S Q L ........................................................................................... 237 8.5. Гарантии ACID .......................................................................................................238 8.5.1. Атомарность ............................................................................................... 238 8.5.2. Согласованность ....................................................................................... 238 8.5.3. И зо л яц и я....................................................................................................... 239 8.5.4. У стойчивость............................................................................................... 239 8.6. N o S Q L ........................................................................................................................ 239 8.7. Распределенные базы д а н н ы х ............................................................................ 240 8.8. M o ngoD B .................................................................................................................. 241 8.8.1. Установка и настройка............................................................................... 242 8.8.2. Подключение к M ongoD B ........................................................................242 8.8.3. Вставка докум ентов................................................................................... 243 8.8.4. Получение инф орм ации............................................................................243 8.8.5. Идентификаторы M ongoD B ....................................................................245 8.8.6. Реплицированные н аборы ........................................................................247 8.8.7. Уровень з а п и с и ........................................................................................... 248 8.9. Хранилища «ключ-значение» ............................................................................ 250 8.10. Redis ........................................................................................................................ 251 8.10.1. Установка и н астр о й к а............................................................................252 8.10.2. Выполнение и н и ц и ал и зац и и ................................................................252 8.10.3. Работа с парами «ключ-значение» .................................................... 253 8.10.4. Работа с ключами ................................................................................... 254 8.10.5. Кодирование и типы д а н н ы х ................................................................254 8.10.6. Работа с х еш ам и ....................................................................................... 256 8.10.7. Работа со списками ............................................................................... 257 8.10.8. Работа со множествами............................................................................258 8.10.9. Реализация паттерна «публикация/подписка» на базе каналов 259 8.10.10. Повышение быстродействия Redis.................................................... 260 8.11. Встроенные базы данных .................................................................................. 260 8.12. LevelDB .................................................................................................................. 261 8.12.1. LevelUP и LevelDOW N............................................................................262 8.12.2. Установка ................................................................................................... 262 12 Оглавление 8.12.3. Обзор A P I ................................................................................................... 263 8.12.4. И нициализация ....................................................................................... 263 8.12.5. Кодирование ключей и зн а ч е н и й ........................................................ 264 8.12.6. Чтение и запись пар «ключ-значение» ............................................ 264 8.12.7. Заменяемые подсистемы базы данных ............................................ 265 8.12.8. М одульная база данных ........................................................................267 8.13. Затратные операции сериализации и десериализации............................. 268 8.14. Хранение данных в б р ау зер е............................................................................ 269 8.14.1. Веб-хранилище: localStorage и sessionStorage ................................ 269 8.14.2. Чтение и запись значений ....................................................................270 8.14.3. localForage................................................................................................... 273 8.14.4. Чтение и запись ....................................................................................... 273 8.15. Виртуальное х ран ен и е........................................................................................274 8.15.1. S 3 ...................................................................................................................275 8.16. Какую базу данных выбрать?............................................................................ 276 8.17. Заклю чение............................................................................................................ 276 Глава 9 . Тестирование приложений Node ............................................................ 278 9.1. Модульное тестирование..................................................................................... 279 9.1.1..Модуль a s s e rt............................................................................................... 280 9.1.2. Mocha ...........................................................................................................284 9.1.3. Vows ...............................................................................................................289 9.1.4. Chai ...............................................................................................................292 9.1.5. Библиотека should.js ............................................................................... 293 9.1.6. Ш пионы и заглушки в Sinon.JS ............................................................296 9.2. Ф ункциональное тестирование......................................................................... 298 9.2.1. Selenium ....................................................................................................... 299 9.3. Ошибки при прохождении тестов ................................................................... 302 9.3.1. Получение более подробных журналов................................................ 303 9.3.2. Получение расширенной трассировки стека .................................... 305 9.4. Заклю чение............................................................................................................... 307 Глава 10 . Развертывание и обеспечение доступности приложений Node . . . . 308 10.1. Хостинг Node-прилож ений............................................................................... 308 10.1.1. Платформа как с е р в и с ............................................................................309 10.1.2. С ерверы .......................................................................................................311 10.1.3. К о н тей н ер ы ............................................................................................... 312 10.2. Основы развертывания ..................................................................................... 314 10.2.1. Развертывание из репозитория G i t .................................................... 314 10.2.2. Поддержание работы N ode-приложения ........................................ 315 10.3. М аксимизация времени доступности и производительности п рилож ений..................................................................................................................... 317 Оглавление 13 10.3.1. Поддержание доступности приложения с U p s ta r t.........................318 10.3.2. Кластерный A P I ....................................................................................... 320 10.3.3. Хостинг статических файлов и представительство.........................322 10.4. Заклю чение............................................................................................................ 324 Часть III. За пределами веб-разработки ............................................325 Глава 11. Написание приложений командной строки..........................................326 11.1. Соглашения и философия.................................................................................. 326 11.2. Знакомство с parse-json....................................................................................... 328 11.3. Аргументы командной строки ......................................................................... 328 11.3.1. Разбор аргументов командной строки................................................ 328 11.3.2. Проверка аргументов ............................................................................329 11.3.3. Передача stdin в виде файла ................................................................330 11.4. Использование программ командной строки с npm................................... 331 11.5. Связывание сценариев с каналами .................................................................332 11.5.1. Передача данных parse-json....................................................................332 11.5.2. Ошибки и коды завершения ................................................................333 11.5.3. Использование каналов в N o d e ............................................................335 11.5.4. Каналы и последовательность выполнения команд .....................336 11.6. Интерпретация реальных сц ен ари ев..............................................................337 11.7. Заклю чение............................................................................................................ 338 Глава 12 . Разработка настольных приложений с использованием Electron...........................................................................................................................339 12.1. Знакомство с Electron ........................................................................................339 12.1.1. Технологический стек E le c tro n ............................................................340 12.1.2. Проектирование и н тер ф ей са................................................................341 12.2. Создание приложения Electron ...................................................................... 342 12.3. Построение полнофункционального настольного п ри л ож ен и я............344 12.3.1. Исходная настройка React и Babel .................................................... 345 12.3.2. Установка зависимостей ........................................................................345 12.3.3. Настройка webpack ............................................................................... 346 12.4. Приложение React ..............................................................................................348 12.4.1. Определение компонента Request .................................................... 349 12.4.2. Определение компонента R esp o n se.................................................... 352 12.4.3. Взаимодействие между компонентами React ................................ 354 12.5. Построение и распространение ...................................................................... 355 12.5.1. Построение приложений с использованием Electron Packager .. 356 12.5.2. Упаковка ................................................................................................... 357 12.6. Заклю чение............................................................................................................ 358 14 Оглавление Приложения............................................................................................. 359 Приложение А. Установка N o d e ..............................................................................360 А.1. Установка Node с использованием программы установки ........................360 А.1.1. Программа установки для m a c O S ........................................................ 360 А.1.2. Программа установки для W in d o w s.................................................... 362 A.2. Другие способы установки N o d e ...................................................................... 363 A.2.1. Установка Node из исходного кода .................................................... 363 A.2.2. Установка Node из менеджера п акетов................................................ 363 Приложение Б . Автоматизированное извлечение веб-данных ...................... 365 Б.1. Извлечение веб-данных........................................................................................365 Б.1.1. Применение извлечения веб-данных ................................................ 366 Б.1.2. Необходимые инструм енты ....................................................................367 Б.2. Простейшее извлечение веб-данных с использованием cheerio...............368 Б.3. Обработка динамического контента с jsd o m ..................................................371 Б.4. Обработка «сырых» д ан н ы х ............................................................................... 374 Б.5. Заключение ............................................................................................................ 377 Приложение В . Официально поддерживаемые промежуточные компоненты 378 B.1. Разбор cookie, тел запросов и строк информационных зап росов............378 B.1.1. cookie-parser: разбор H T T P-cookie........................................................ 379 В.1.2. Разбор строк запросов ............................................................................383 В.1.3. body-parser: разбор тел запросов............................................................384 В.1.4. Сжатие ответов........................................................................................... 391 В.2. Реализация базовых функций веб-приложения ......................................... 393 В.2.1. morgan: ведение журнала запросов .................................................... 393 В.2.2. serve-favicon: значки адресной строки и закладки .........................397 В.2.3. m ethod-override — имитация методов H T T P .................................... 398 В.2.4. vhost: виртуальный хости н г....................................................................401 В.2.5. express-session: управление сеансами ................................................ 402 В.3. Безопасность веб-приложений ......................................................................... 407 В.3.1. basic-auth: базовая H T T P -аутентификация .................................... 408 В.3.2. csurf: защита от атак CSRF ....................................................................410 В.3.3. errorhandler: — обработка ошибок при разработке........................... 412 В.4. Предоставление статических файлов ..............................................................414 В.4.1. serve-static — автоматическое предоставление статических файлов браузеру ................................................................................................... 414 В.4.2. serve-index: генерирование списков содержимого каталогов........416 Глоссарий 418 Предисловие С момента публикации первого издания «Node.js в действии» проект Node объ­ единился с io.js, а модель управления радикально изменилась. Менеджер пакетов Node выделился в успешную новую компанию npm, а такие технологии, как Babel и Electron, изменили панораму разработки. Тем не менее в базовых библиотеках Node изменений не так уж много. Сам язык JavaScript изменился: многие разработчики теперь используют возможности ES2015, поэтому исходные листинги были переписаны для «стрелочных» функций, констант и деструктуризации. При этом библиотеки и встроенные инструменты Node все еще очень похожи на свои аналоги Node до выхода версий 4.x, поэтому мы обратились к сообществу за идеями обновления этого издания. Чтобы отразить реалии, с которыми теперь сталкивается N ode-разработчик, мы изменили структуру книги. Express и Connect уделено меньше внимания, и книга в большей степени ориентирована на ш ирокий спектр технологий. Вы найдете в книге всю информацию, необходимую для разработки в полном технологиче­ ском стеке, включая системы построения фронтэнда, выбор веб-фреймворка, ра­ боту с базами данных в Node, написание тестов и развертывание веб-приложений. Помимо разработки веб-приложений в книгу также были добавлены главы о на­ писании приложений командной строки и приложений Electron. Они помогут вам извлечь максимум практической пользы из ваших навыков использования Node и JavaScript. Node и экосистема этой технологии —не единственная тема книги. Я по возможности постарался добавить историческую справку о том, что повлияло на развитие Node. Наряду с обычными темами Node и JavaScript рассматриваются такие вопросы, как философия Unix и корректное, безопасное использование баз данных. Хочется верить, что у вас сформируется достаточно широкая картина Node и JavaScript, которая поможет вам в поиске ваших собственных решений современных задач. Алекс Янг Благодарности Эта книга основана на материале авторов предыдущего издания и очень многим обязана их труду: это Майк Кантелон (M ike Cantelon), Марк Хартер (M arc Harter), Т. Дж. Головайчук (T J. Holowaychuk) и Натан Райлих (Nathan Rajlich). Это издание увидело свет только благодаря активной помощи рабочей группы из издательства M anning. Синтия Кейн (C ynthia Kane), руководитель проекта, помогала мне не отвлекаться от сути во время долгого процесса обновления исходного материала. Без тщательной научной редактуры, проведенной Дугом Уорреном (D oug Warren), книга и примеры кода не были бы и наполовину так хороши. Наконец, мы хотим по­ благодарить многих рецензентов, поделившихся своим мнением во время написания и разработки: Остин Кинг (Austin King), Карл Хоуп (C arl Hope), Крис Солч (Chris Salch), Кристофер Рид (C hristopher Reed), Дейл Ф рэнсис (D ale Francis), Хафиз Вахид уд дин (Hafiz W aheed ud din), Харинат М аллепалли (H arinath Mallepally), Д ж еф ф Смит (Jeff Sm ith), М арк-Ф илипп Хуге (M arc-P hilippe H uget), Мэттью Бертони (M atthew Bertoni), Ф илипп Ш арьер (Philippe Charriere), Рэнди Камрадт (R andy Kamradt), Сандер Россел (Sander Rossel), Скотт Дирбек (S cott Dierbeck) и Уильям Уилер (W illiam W heeler). Алекс Янг О книге П ервое издание книги «Node.js в действии» было посвящ ено разработке веб­ приложений, при этом особое внимание уделялось веб-фреймворкам C onnect и Express. Второе издание «Node.js в действии» было переработано в соответствии с изменившимися требованиями в области разработки Node. Вы узнаете о системах построения интерфейса и популярных веб-фреймворках Node, а также научитесь строить веб-приложения на базе Express с нуля. Также в книге рассказано о том, как строить автоматизированные тесты и развертывать веб-приложения Node. Технология Node все чаще используется в сочетании с инструментами командной строки и настольными прилож ениями на базе Electron, поэтому в книгу были включены главы, посвященные обеим областям. Предполагается, что читатель знаком с основными концепциями программирования. В первой главе приведен краткий обзор JavaScript и ES2015 для читателей, которые только начинают вникать в тонкости современного JavaScript. Структура Книга состоит из трех частей. В части I рассматриваются основы Node.js и фундаментальные методики, исполь­ зуемые для разработки приложений на этой платформе. В главе 1 описываются характеристики JavaScript и Node, с приведением примеров кода. Глава 2 проведет вас поэтапно через фундаментальные концепции программирования Node.js. Глава 3 представляет собой полное руководство по построению веб-приложений с нуля. Часть II — самая большая — посвящ ена разработке веб-приложений. В главе 4 рассеиваются некоторые заблуждения по поводу frond-end систем сборки; если вы когда-либо использовали webpack или Gulp в проекте, но не понимали эти тех­ нологии в полной мере, эта глава написана для вас. В главе 5 описаны некоторые популярные фреймворки стороны сервера для Node, а в главе 6 технологии Connect и Express рассматриваются более подробно. Глава 7 посвящена языкам сценариев, которые могут значительно повысить эф ­ фективность вашей работы при написании кода на стороне сервера. Большинству веб-приложений нужна база данных, поэтому в главе 8 рассматриваются многие 18 О книге разновидности баз данных, которые могут использоваться с Node, от реляционных до баз данных NoSQL. В главах 9 и 10 рассматриваются процессы тестирования и разработки, здесь также упоминается развертывание в облаке. Часть III выходит за рамки разработки веб-приложений. Глава 11 посвящена по­ строению приложений командной строки с использованием Node, чтобы вы могли создавать текстовые интерфейсы, удобные для разработчика. Если вас интересуют перспективы построения настольных приложений на базе Node (например, Atom), обращайтесь к главе 12 — она полностью посвящена Electron. Также в книгу включены три подробных приложения. В приложении А приведены инструкции по установке Node для систем MacOS и Windows. Приложение Б содер­ жит подробное руководство по извлечению веб-данных (web scraping), а в приложе­ нии В представлены все компоненты среднего звена, официально поддерживаемые для веб-фреймворка Connect. Правила оформления и загрузка примеров кода Примеры кода, приведенные в книге, оформляются в соответствии со стандартным соглашением по оформлению JavaScript-кода. Для создания отступов в коде вместо символов табуляции применяются пробелы. Существует ограничение на длину строки кода, равное 80 символам. Код, приведенный в листингах, сопровождается комментариями, которые поясняют ключевые концепции. Каждая инструкция занимает отдельную строку и завершается точкой с запятой. Блоки кода, содержащие несколько инструкций, заключены в фигурные скобки. Л евая фигурная скобка находится в первой (открывающей) строке блока. Правая фигурная скобка закрывает блок кода и выравнивается по вертикали с открыва­ ющей скобкой. Примеры кода, используемые в книге, можно загрузить с веб-сайта www.manning. com/books/node-js-in-action-second-edition. Об авторах Алекс Янг Алекс — веб-разработчик, ж ивущ ий в Лондоне; автор книги Node.js in Practice (M anning, 2014). Алекс ведет популярный блог DailyJS по тематике JavaScript. В настоящее время работает на Sky старшим разработчиком для NOW TV. Он есть на G itH ub ( https://github.com/alexyoung) и в Twitter (@alex_young). Брэдли Мек Брэдли — участник TC39 и Node.js Foundation. В свободное время он разрабаты­ вает инструментарий JavaScript, занимается садоводством и преподает. До работы в G oD addy он долго использовал Node.js на благо других компаний, таких как NodeSource и Nodejitsu. Всегда готовый обучать и объяснять, он старается под­ держивать у людей мотивацию, потому что учиться для него так же сложно, как и для всех остальных. Иллюстрация на обложке Иллю страция на обложке второго издания книги называется «M an about Town» («Прожигатель жизни») и была позаимствована из изданного в XIX веке во Ф ран­ ции четырехтомного каталога Сильвена М арешаля (Sylvain M arechal). В каталоге представлена одежда, характерная для разных регионов Франции. Каждая иллюстра­ ция красиво нарисована и раскрашена от руки. Иллюстрации из каталога Марешаля напоминают о культурных различиях между городами и весями мира, имевшими место почти двести лет назад. Люди, проживавшие в изолированных друг от друга регионах, говорили на разных языках и диалектах. По одежде человека можно было определить, в каком городе, поселке или поселении он проживает. С тех пор дресс-код сильно изменился, да и различия между разными регионами стали не столь явно выраженными. В наше время довольно трудно узнать жите­ лей разных континентов, не говоря уже о жителях разных городов или регионов. Возможно, мы отказались от культурных различий в пользу более разнообразной личной жизни, и конечно, в пользу более разнообразной и стремительной техно­ логической жизни. Сейчас, когда все компьютерные книги похожи друг на друга, издательство Manning стремится к разнообразию и помещает на обложки книг иллюстрации, показываю­ щие особенности жизни в разных регионах Ф ранции два века назад. От издательства Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! На веб-сайте издательства www.piter.com вы найдете подробную информацию о на­ ших книгах. Знакомство с Node В наши дни Node уже можно назвать зрелой платформой веб-разработки. В главах 1-3 рассматриваются основные возможности Node, включая использование базовых модулей и npm. Также вы узнаете, как Node ис­ пользует современные версии JavaScript и как построить веб-приложение с нуля. После чтения этих глав у вас сформируется хорошее понимание того, что может делать Node и как создавать ваши собственные проекты. Знакомство с Node.js Node.js — асинхронная уп равляем ая собы тиям и исп олни тельная платф орм а JavaScript с мощной, но компактной стандартной библиотекой. Ее сопровождением и поддержкой занимается Node.js Foundation — отраслевой консорциум с откры­ той моделью управления. Существует две активно поддерживаемые версии Node: текущ ая (C u rren t) и пользующаяся долгосрочной поддержкой (LTS, Long Term Support). Если вы захотите больше узнать о том, как осуществляется управление Node, на официальном веб-сайте имеется достаточно подробная документация (https://nodejs.org). С момента появления Node.js в 2009 году язык JavaScript прошел долгий путь от еле сносного браузерного язы ка до одного из важнейших языков во всех областях раз­ работки программного обеспечения. Отчасти это изменение связано с появлением спецификации ECM AScript 2015, устранившей ряд важных недостатков в преды­ дущих версиях языка. Node использует JavaS cript-ядро Google V8, основанное на шестой версии стандарта ECM AScript (иногда она называется ES6 и обозначается сокращением ES2015). Также на ситуацию повлияли такие инновационные техно­ логии, как Node, React и Electron; они позволяют применять JavaScript буквально повсеместно: от сервера до браузера и в платформенных мобильных приложениях. Самые крупные компании постепенно принимают JavaScript, а компания Microsoft даже внесла свой вклад в успех Node. В этой главе мы расскаж ем о технологии Node, о ее неблокирую щ ей модели, управляемой событиями, а также о некоторых факторах, благодаря которым язык JavaScript стал отличным языком программирования общего назначения. Д ля на­ чала давайте рассмотрим типичное веб-приложение Node. 1.1. Типичное веб-приложение Node Одна из сильных сторон Node и JavaScript вообще — однопоточная модель про­ граммирования. Программные потоки (threads) являются стандартным источником 1.1. Типичное веб-приложение Node 23 ошибок, и хотя некоторые из недавно появивш ихся языков программирования, включая Go и Rust, пытаются предоставить безопасные инструменты параллельного программирования, Node работает с моделью, используемой в браузере. Браузерный код представляет собой последовательность команд, которые выполняются одна за одной; код не выполняется параллельно. Д ля пользовательских интерфейсов такая модель не имеет смысла: пользователь не хочет дожидаться завершения медленных операций (например, обращений к данным по сети или к файлам). Д ля решения этой проблемы в браузерах используются события: когда пользователь щелкает на кнопке, инициируется событие, и выполняется функция, которая была определена ранее, но еще не выполнялась. Тем самым предотвращаются некоторые проблемы, встречающиеся в многопоточном программировании, включая взаимные блоки­ ровки (deadlocks) ресурсов и состояния гонки (race conditions). 1.1.1. Неблокирующий ввод/вывод Что это означает в контексте программирования на стороне сервера? Ситуация аналогична: запросы ввода/вы вода (например, обращения к диску или сетевым ресурсам) также выполняются относительно медленно, поэтому исполнительная среда не должна блокировать выполнение бизнес-логики во время чтения файлов или передачи сообщений по сети. Д ля этого в Node используются три концепции: события, асинхронные API, и неблокирующий ввод/вывод. Неблокирующий ввод/ вывод — низкоуровневый термин с точки зрения программиста Node. Он означает, что программа может обратиться с запросом к сетевому ресурсу и заняться чем-то другим. А потом, когда сетевая операция будет завершена, выполняется функция обратного вызова, которая обработает результат. На рис. 1.1 изображено типичное веб-приложение Node, использующее библиотеку веб-программирования Express для обработки заказов в магазине. Браузеры выда­ ют запросы на приобретение продукта; приложение проверяет текущее состояние складских запасов, создает учетную запись для пользователя, отправляет квитанцию по электронной почте и возвращает H T T P -ответ в формате JSO N . Одновременно могут происходить другие операции: квитанция отправляется по электронной почте, а база данных обновляется информацией от пользователя и данными заказа. По сути, перед нами прямолинейный императивный код JavaScript, но исполнительная среда работает параллельно, потому что она использует неблокирующий ввод/вывод. На рис. 1.1 приложение обращается к базе данных по сети. В Node сетевые операции выполняются без блокировки, потому что Node при помощи библиотеки libuv (h ttp :// libuv.org/) использует неблокирующие сетевые вызовы операционной системы. Эта функциональность по-разному реализована для Linux, macOS и Windows, но вам придется иметь дело только с удобной библиотекой JavaScript для работы с базами данных. Хотя вы пишете команды типа d b .in se rt(q u e ry J e r r => {}), Node во внутрен­ ней реализации выполняет оптимизированные неблокирующие сетевые операции. го Ваше приложение Тело запроса: информация о заказе Регистрация пользователя Браузер SQL INSERT Квитанция по электронной почте Загрузка шаблона с диска вызывается next (error) Маршрутизатор HTTP Ответ в формате JSON Глава 1. Знакомство с Node.js Проверка складских запасов Node и Express Неблокирующий сетевой ввод/вывод Ответ HTTP Обработчик ошибки Квитанция по электронной почте Объект ошибки JavaScript libuv Рис. 1.1. Асинхронные и неблокирующие компоненты в приложениях Node 1.1. Типичное веб-приложение Node 25 Обращения к диску происходят примерно так же, но, как ни странно, полного совпа­ дения нет. Когда приложение генерирует квитанцию, отправляемую по электронной почте, и шаблон сообщения читается с диска, libuv использует пул потоков для создания иллю зии использования неблокирующего вызова. Управление пулом потоков — довольно тяжелое дело, но понять команду e m a il.s e n d ('te m p la te .e js ', ( e r r , html) => {}) определенно намного проще. Истинное преимущество использования асинхронных A PI с неблокирующими операциями ввода/вывода заключается в том, что Node может заниматься други­ ми делами во время выполнения относительно медленных процессов. И хотя вы ­ полняться может только однопоточное и однопроцессное веб-приложение Node, в любой момент времени оно может обрабатывать сразу несколько подключений от тысяч потенциальных посетителей сайта. Чтобы понять, как это происходит, необходимо познакомиться с циклом событий. 1.1.2. Цикл событий А теперь сосредоточимся на одном конкретном аспекте рис. 1.1: обработке запросов браузера. В этом приложении встроенная библиотека H TTP-сервера Node —базовый модуль с именем h t t p . Server — обрабатывает запрос с использованием потоков, со­ бытий и парсера H T T P -запросов Node, который содержит платформенный код. При этом инициируется выполнение функции обратного вызова в вашем приложении, которая была добавлена средствами библиотеки веб-приложений Express (h ttp s:// expressjs.com/). Выполняемая функция обратного вызова выдает запрос к базе дан­ ных, и в конечном итоге приложение отвечает данными JS O N с использованием HTTP. Весь процесс использует как минимум три неблокируемых сетевых вызова: один для запроса, один для базы данных и один для ответа. Как Node планирует все эти неблокирующие сетевые операции? Ответ: при помощи цикла событий. На рис. 1.2 показано, как цикл событий используется для этих трех сетевых операций. Цикл событий работает в одном направлении (он реализуется очередью FIFO — «первым пришел, первым вышел») и проходит через несколько фаз. На рис. 1.2 показана упрощ енная последовательность важнейш их фаз, выполняемых при каждой итерации цикла. Сначала выполняются таймеры, выполнение которых за­ планировано функциями JavaScript setTimeout и s e tIn te r v a l. Затем выполняются обратные вызовы ввода/вывода, поэтому, если один из неблокирующих сетевых вызовов вернул какой-либо ввод/вывод, в этот момент будет инициирован ваш обратный вызов. В фазе опроса читаются новые события ввода/вывода, а в конце инициируются обратные вызовы, запланированные функцией setIm m ediate. Это особый случай, потому что он позволяет запланировать немедленное выполнение обратных вызовов после текущих обратных вызовов ввода/вывода, уже находя­ щихся в очереди. Пока это звучит абстрактно, но сейчас вы должны уяснить одно: Node использует однопоточную модель, но при этом предоставляет необходимые средства для написания эффективного и масштабируемого кода. 26 Глава 1. Знакомство с Node.js Цикл событий Выполнение таймеров Активизация незавершенных обратных вызовов Опрос ввода/вывода База данных 1. Вызов запроса 2. Вызов базы данных 3. Вызов ответа Функция: отправка/ заказ Создание пользователя Отправка данных JSON браузеру Рис. 1.2. Цикл событий Обратите внимание на то, что в примерах используются «стрелочные» функции ES2015. Node поддерживает много новых функций JavaScript, и прежде чем дви­ гаться дальше, мы рассмотрим некоторые новые языковые средства, используемые для написания более эффективного кода. 1.2. ES2015, Node и V8 Если вы когда-либо работали с Jav a S c rip t и вас огорчило отсутствие классов и странные правила видимости — радуйтесь: в Node многие проблемы были реше­ ны! Теперь вы можете создавать классы и использовать конструкции видимости co n st и l e t (вместо var). В Node 6 можно использовать параметры функций по умолчанию , оставш иеся параметры (rest param eters), оператор sp read , циклы fo r...o f, ш аблонны е строки, деструктуризацию , генераторы и многое другое. Замечательная сводка новых возможностей ES2015 в Node доступна по адресу http://node.green. Начнем с классов. В ES5 и более ранних версиях для создания конструкций, напо­ минающих классы, использовались объекты-прототипы: function User() { / / Конструктор } User.prototype.method = function() { / / Метод }; 1.2. ES2015, Node и V8 27 В Node 6 и ES2015 тот же код можно написать с использованием классов: class User { constructor() {} method() {} } Код получается более компактным и лучше читается. Но это еще не все: Node также поддерживает субклассирование, su p er и статические методы. Д ля пользователей с опытом работы на других языках поддержка синтаксиса классов делает техноло­ гию Node более доступной по сравнению с временами, когда мы были ограничены возможностями ES5. Другая важная возможность Node 4 и выше — добавление конструкций const и le t. В ES5 все переменные создавались с ключевым словом var. Проблема в том, что var определяет переменные в области видимости функции или глобальной области видимости, поэтому вы не могли определить переменную уровня блока в команде i f , f o r или в другом блоке. CONST ИЛИ LET? Когда вы колеблетесь между const и let, почти всегда следует сделать выбор в пользу const. Так как в большей части вашего кода будут использоваться экземпляры ваших собственных классов, объектные литералы или значе­ ния, которые не должны изменяться, почти во всех случаях уместно const. Даже экземпляры объектов с изменяемыми свойствами могут объявляться с ключевым словом const — ведь const означает лишь то, что ссылка доступна только для чтения, а не неизменность самого значения. В Node такж е поддерж иваю тся обещ ания (prom ises) и генераторы. Обещания поддерживаются многими библиотеками, что позволяет писать асинхронный код в стиле динамических интерфейсов (fluent interface). Вероятно, вы уже знакомы с динамическими интерфейсами: их видел каждый, кто когда-либо использовал такие API, как jQuery, или хотя бы массивы JavaScript. Следующий короткий при­ мер демонстрирует применение сцепленных вызовов для манипуляций с массивами в JavaScript: [1, 2, 3] .map(n => n * 2) .f ilte r ( n => n > 3); Генераторы нужны для использования синхронного стиля программирования с асинхронны м вводом /вы водом . Если вас интересует практический пример использования генераторов в Node, обратитесь к библиотеке веб-приложений 28 Глава 1. Знакомство с Node.js Koa (h ttp ://ko a js.com /). При использовании обещаний или других генераторов с Koa вы можете прибегнуть к y ie ld со значениями вместо вложения обратных вызовов. Еще одна полезная возможность ES2015 в Node — шаблонные строки. В ES5 стро­ ковые литералы не поддерживали интерполяцию или деление на несколько строк. Теперь при использовании символа обратного апострофа ( ' ) можно вставлять значения и использовать разбиения по строкам. Д анная возможность особенно полезна при вставке коротких фрагментов H TM L в веб-приложениях: this.body = ' <div> <h1>Hello from Node</h1> <p>Welcome, ${user.name}!</p> </div> В ES5 этот пример пришлось бы записывать следующим образом: this.body this.body this.body this.body this.body = '\ n '; += '<div>\n'; += ' <h1>Hello from Node</h1>\n'; += ' <p>Welcome, ' + user.name + '</p>\n'; += '<div>\n'; Старый стиль не только занимает больше места, но и повышает риск случайных ошибок. П оследняя новая возможность, особенно важ ная для программистов Node, — стрелочные функции, которые способствую т упрощ ению синтаксиса. Например, если вы пишете функцию обратного вызова, которая получает один аргумент и возвращ ает значение, трудно представить себе более компактный синтаксис: [1, 2, 3].map(v => v * 2); В Node обычно используются два аргумента, потому что первым аргументом функ­ ции обратного вызова часто является объект ошибки. В этом случае аргументы должны заключаться в круглые скобки: const fs = re q u ire ('fs '); fs.readFile('package.json', (err, text) => console.log('Length:', text.length) ); Если тело функции должно содержать более одной строки, необходимо использовать фигурные скобки. Полезность стрелочных функций не ограничивается упрощением синтаксиса; она также имеет отношение к областям видимости JavaScript. В ES5 и ранее при определении функций внутри других функций ссылка t h i s становится 1.2. ES2015, Node и V8 29 глобальным объектом. И з-за этого в следующем классе в стиле ES5 присутствует скрытая ошибка: function User(id) { / / Конструктор th is .id = id; } User.prototype.load = function() { var self = th is; var query = 'SELECT * FROM users WHERE id = ?'; sql.query(query, th is .id , function(err, users) { self.name = users[0].name; }; }); В строке, в которой присваивается значение self.nam e, невозможно использовать запись this.nam e, потому что t h i s внутри функции будет глобальным объектом. Когда-то применялось обходное решение с сохранением значения t h i s в точке входа родительской функции или метода. Со стрелочными функциями связывание выполняется правильно. В ES2015 предыдущий пример можно записать в гораздо более естественном виде: class User { constructor(id) { th is .id = id; } load() { const query = 'SELECT * FROM users WHERE id = ?'; sql.query(query, th is .id , (e rr, users) => { this.name = users[0].name; }); } } Вы не только можете использовать c o n st для более эффективного моделирования запроса к базе данных, но и отпадает необходимость в неуклюжей переменной se lf. В ES2015 появилось много новых замечательных возможностей, благодаря которым код Node лучше читается; но давайте посмотрим, на чем строится их реализация в Node и как они связаны с уже рассмотренными средствами неблокирующего ввода/вывода. 1.2.1. Node и V8 Технология Node основана на ядре JavaScript V8, разработанном проектом Chromium для Google Chrome. К отличительным особенностям V8 относится прямая компиля­ ция в машинный код и средства оптимизации, обеспечивающие высокую скорость 30 Глава 1. Знакомство с Node.js работы Node. В разделе 1.1.1 был упомянут еще один из встроенных компонентов Node —libuv. В этой части речь идет о вводе/выводе; V8 обеспечивает интерпретацию и выполнение кода JavaScript. Чтобы использовать libuv с V8, следует применить связующую прослойку C++. На рис. 1.3 изображены все программные компоненты, из которых состоит Node. Ваше замечательное приложение app.js Код JavaScript платформы Node.js, библиотеки С и C++ г Базовые модули JavaScript для Node V У V8 Связующие прослойки C++ / libuv c-ares http ч Операционная система Рис. 1.3. Программный стек Node Таким образом, конкретная функциональность JavaScript, доступная для Node, определяется набором возможностей, поддерживаемых V8. Управление этой под­ держкой осуществляется через функциональные группы. 1.2.2. Работа с функциональными группами Node включает функциональность ES2015, основанную на возможностях, поддер­ живаемых V8. Функциональность группируется по категориям: завершенные, за ­ планированные и находящиеся в разработке. Завершенные возможности включаются по умолчанию, но для включения запланированных и находящихся в разработке возможностей используются флаги командной строки. Если вы хотите использовать запланированные возможности, которые почти готовы, но не считаются завершен­ ными группой V8, запустите Node с флагом --harmony. Возможности, находящиеся в разработке, менее стабильны, а для их включения используются специальные флаги. Д ля получения списка всех возможностей Node, находящихся в разработке, в документации Node рекомендуется провести поиск по строке «in progress»: node --v8-options | grep "in progress" Список будет изменяться в зависимости от выпусков Node. У Node также имеется график управления версиями, определяющий доступные API. 1.3. Установка Node 31 1.2.3. График выпуска версий Node Выпуски Node делятся на пользую щ иеся долгосрочной поддержкой, или LTS (Long-Term Support), текущие (C u rren t) и ежедневные (N ightly). Д ля выпусков LTS обеспечивается 18-месячная поддержка с последующей 12-месячной под­ держкой сопровождения. Д ля выпусков прим еняется система семантического управления версиями (SemVer). В этой системе выпуску назначается основной (m ajor), дополнительный (m inor) и оперативный (patch) номера версии. Н апри­ мер, у выпуска 6.9.1 имеется основной номер версии 6, дополнительный номер 9 и оперативный 1. Каждый раз, когда у Node изменяется основной номер версии, это может означать, что некоторые API стали несовместимыми с вашими проекта­ ми и вам придется заново протестировать их для новой версии Node. Кроме того, в терминологии выпуска Node увеличение основного номера версии означает по­ явление новой текущей версии. Ежедневные сборки автоматически генерируются каждые 24 часа с новейшими изменениями, но, как правило, используются только для тестирования новой функциональности Node. Выбор версии зависит от проекта и организации. Одни предпочитают LTS из-за более редких обновлений: этот вариант хорошо подходит для крупных организа­ ций, в которых частые обновления создают проблемы. Но если вас интересуют новейшие усовершенствования в области быстродействия и функциональности, выбирайте текущую версию. 1.3. Установка Node Простейший способ установить Node — использовать программу установки с сайта https://nodejs.org. Новейшая текущая версия (6.5 на момент написания книги) уста­ навливается средствами Mac или Windows. Загрузите исходный код самостоятельно или выполните установку при помощи менеджера пакетов вашей операционной системы. Такие пакеты существуют для Debian, U buntu, Arch, Fedora, FreeBSD, G entoo и SUSE. Также имеются пакеты для Hom ebrew и Sm artOS. Если пакета для вашей операционной системы нет, проведите построение из исходного кода. ПРИМЕЧАНИЕ Дополнительная информация об установке Node приведена в приложении А. Полный список пакетов находится на сайте Node (https://nodejs.org/en/dow nload/ package-manager/), а исходный код доступен на G itH ub (https://github.com /nodejs/ node). Исходный код также пригодится вам в том случае, если вы хотите заняться изучением кода Node без его загрузки. После того как платформа Node будет установлена, вы можете немедленно проверить результат — введите команду node -v в терминале. Команда должна вывести номер 32 Глава 1. Знакомство с Node.js только что загруженной и установленной версии Node. Создайте файл с именем hello.js, содержимое которого должно выглядеть так: console.log("hello from Node"); Сохраните файл и запустите его командой node h e l l o .j s . Поздравляем! Теперь вы готовы к тому, чтобы заняться написанием собственных приложений на базе Node. БЫСТРОЕ НАЧАЛО РАБОТЫ В WINDOWS, LINUX И MACOS Если вы недавно занялись программированием и у вас еще нет любимого текстового редактора, для Node хорошо подойдет Visual Studio Code (h ttp s :// code.visualstudio.com /). Этот редактор создан компанией Microsoft, но он распространяется с открытым кодом, доступен для бесплатной загрузки и поддерживает Windows, Linux и macOS. Начинающие программисты оценят такие возможности Visual Studio Code, как цветовое выделение синтаксиса JavaScript и автозавершение для базовых модулей Nodе; ваш код JavaScript будет выглядеть более четко, и вы будете видеть списки поддерживаемых методов и объектов в процессе ввода. Вы также можете открыть интерфейс командной строки, в котором Node можно вызвать простым вводом команды Node. Этот способ особенно полезен для выполнения Node и команд npm. Возможно, пользователи W indows пред­ почтут его использованию cmd.exe. Все листинги были протестированы в W indows и Visual Studio Code, поэтому для выполнения примеров ничего особенного вам не потребуется. Возможно, в начале работы вам стоит познакомиться с учебником по Node.js для Visual Studio Code ( https://code.visualstudio.com /docs/runtim es/nodejs). Когда вы установите Node, в вашем распоряж ении также окажутся некоторые бесплатные инструменты. Node — не просто интерпретатор: это целое семейство инструментов, образующих платформу Node. В следующем разделе инструменты, включенные в поставку Node, рассматриваются более подробно. 1.4. Встроенные средства Node В поставку Node входит встроенный менеджер пакетов, базовые модули JavaScript для поддержки самых разнообразных функций, от файлового и сетевого ввода/вы ­ вода до сжатия zlib, а также отладчик. Менеджер пакетов npm является важнейшим компонентом инфраструктуры, поэтому мы рассмотрим его более подробно. Если вы хотите убедиться в том, что установка Node была выполнена правильно, выполните команды node -v и npm -v в командной строке. Эти команды выводят номера только что установленных версий Node и npm. 1.4. Встроенные средства Node 33 1.4.1. npm Чтобы запустить программу командной строки npm, введите команду npm. П ро­ грамма может использоваться для установки пакетов из центрального реестра npm, но также и для поиска и организации совместного использования ваших проектов с открытым или закрытым кодом. У каждого пакета npm в реестре существует сайт, на котором хранится Readme-файл, информация об авторе и статистика о загрузках. Впрочем, это описание нельзя назвать исчерпывающим. Программой npm занимает­ ся npm, Inc. — компания, обеспечивающая работу сервиса npm и предоставляющая услуги коммерческим организациям. Сюда входит хостинг приватных пакетов npm; вы также можете вносить ежемесячную плату за хостинг исходного кода, принадлежащего вашей компании, чтобы разработчики JavaS cript могли легко устанавливать его с использованием npm. При установке пакетов командой npm i n s t a l l вам придется решить, хотите ли вы до­ бавить их в свой текущий проект или установить их глобально. Глобальная установка пакетов обычно используется для служебных программ —чаще всего это программы, запускаемые из командной строки. Хорошим примером служит пакет gulp-cli. Чтобы использовать npm, создайте файл package.json в каталоге, содержащем проект Node. Проще всего поручить создание файла package.json менеджеру npm. Введите следующую команду в командной строке: mkdir example-project cd example-project npm in it -y Открыв файл package.json, вы увидите простые данные в формате JSO N с описанием вашего проекта. Если теперь установить модуль с сайта www.npmjs.com с ключом --save, npm автоматически обновит файл package.json. Попробуйте сами — введите команду npm i n s t a l l , или сокращенно npm i: npm i --save express Если теперь открыть файл package.json, вы увидите, что в свойстве dependencies до­ бавился пакет express. Кроме того, в папке node_modules появился каталог express. Он содержит только что установленную версию Express. Вы также можете установить модули глобально с ключом - -g lo b al. Старайтесь по возможности использовать локальные модули, но глобальные модули могут быть полезны для инструментов командной строки, которые должны использоваться за пределами JavaS cript-кода Node. В качестве примера средства командной строки, устанавливаемого из npm, можно привести ESLint (http://eslint.org/). Начиная работать с Node, вы часто будете пользоваться пакетами из npm. Node поставляется с множеством полезных встроенных библиотек, которые обычно на­ зываются базовыми модулями. Рассмотрим их более подробно. 34 Глава 1. Знакомство с Node.js 1.4.2. Базовые модули Базовые модули Node напоминают стандартные библиотеки других языков; эти инструменты понадобятся вам для написания кода JavaScript на стороне сервера. Сами стандарты JavaScript не включают никаких средств для работы с сетью — и даже файлового ввода/вывода в том виде, в котором его представляет большин­ ство разработчиков серверного кода. Чтобы Node можно было использовать для серверного программирования, в эту платформу необходимо было добавить средства для работы с файлами и сетевых операций T C P/IP. Файловая система В поставку Node включается библиотека файловой системы (fs, path), клиенты и серверы T C P (n et), поддержка H T T P (h ttp и h ttp s) и разреш ения доменных имен (dns). Также имеется полезная библиотека, которая используется в основном для написания тестов (assert), и библиотека операционной системы для запроса информации о платформе (os). Node также содержит ряд уникальны х библиотек. М одуль events — небольшая библиотека для работы с событиями — используется в качестве основы для многих API Node. Например, модуль stream использует модуль events для предоставления абстрактных интерфейсов для работы с потоками данных. Поскольку все потоки данных в Node используют одни и те же API, вы можете легко составлять программ­ ные компоненты; если у вас имеется объект чтения файлового потока, вы можете направить его через преобразование zlib, которое выполняет сжатие данных, а затем через объект записи в файловый поток для записи данных в файл. В следующем листинге продемонстрировано использование модуля Node fs для создания потоков чтения и записи, которые могут направляться через другой поток (gzip) для преобразования данных — в данном случае их сжатия. Листинг 1.1. Использование базовых модулей и потоков const const const const fs = re q u ire ('fs '); zlib = re q u ire ('z lib '); gzip = zlib.createG zip(); outStream = fs.createW riteStream ('output.js.gz'); fs.createReadStream ('./node-stream .js') .pi pe(gz ip) .pipe(outStream); Сетевые операции Существует распространенное мнение, что создание простого сервера H T T P было настоящим примером программы «Hello World» для Node. Чтобы построить сервер на базе Node, необходимо загрузить модуль http и передать ему функцию. Функция 1.4. Встроенные средства Node 35 получает два аргумента: входной запрос и выходной ответ. В листинге 1.2 приведен пример, который вы можете выполнить в терминале. Листинг 1.2. Программа «Hello World» с использованием модуля http для Node const http = re q u ire ('h ttp '); const port = 8080; const server = http.createServer((req, res) => { res.end('H ello, w orld.'); }); serv er.listen (p o rt, () => { console.log('Server listening on: h ttp ://lo calh o st:% s', port); }); Сохраните листинг 1.2 в файле hello.js и запустите его командой node h e ll o .j s . О т­ крыв адрес http://localhost:8080, вы увидите сообщение из строки 4. Базовые модули Node предоставляют минимальную функциональность, но при этом они достаточно мощны. Часто многие задачи удается решить простым ис­ пользованием этих модулей, даже без установки дополнительных пакетов из npm. З а дополнительной информацией о базовых модулях обращайтесь по адресу http s:// nodejs.org/api/. Последний встроенный инструмент Node — отладчик. В следующем разделе рас­ сматривается пример его практического применения. 1.4.3. Отладчик В поставку Node вклю чается отладчик с поддержкой пошагового выполнения и R EPL (R ead-E val-P rint Loop). Работа отладчика основана на взаимодействии с вашей программой по сетевому протоколу. Чтобы запустить программу в отлад­ чике, укажите аргумент debug в командной строке. Предположим, вы отлаживаете код из листинга 1.2: node debug h ello .js Результат должен выглядеть так: < Debugger listen in g on [::]:5858 connecting to 127.0.0.1:5858 . . . ok break in node-h ttp .js:1 > 1 const http = re q u ire ('h ttp '); 2 const port = 8080; 3 Среда Node запускает вашу программу и активизирует ее отладку подключением к порту 5858. Теперь вы можете ввести команду help, чтобы просмотреть список 36 Глава 1. Знакомство с Node.js доступных команд, а затем команду c для продолжения выполнения программы. Node всегда запускает программу в прерванном состоянии, поэтому вам всегда при­ дется продолжать ее выполнение, прежде чем делать что-либо другое. Вы можете прервать выполнение отладчика, включив команду debugger в любую позицию кода. При обнаружении команды debugger отладчик останавливается, и вы можете вводить команды. Представьте, что вы написали REST API для соз­ дания учетных записей новых пользователей, а код создания не сохраняет хеш пароля нового пользователя в базе данных. Включите команду debugger в метод save класса User, выполните в пошаговом режиме каждую команду и посмотрите, что происходит. ИНТЕРАКТИВНАЯ ОТЛАДКА Node поддерживает отладочный протокол Chrome. Чтобы отладить сценарий с использованием средств разработчика Chrome, укажите флаг --inspect при запуске программы: node --inspect --debug-brk Node запускает отладчик и преры вает вы полнение в первой строке. На консоль выводится U R L-адрес; откройте его в Chrome для использования встроенного отладчика Chrome. Отладчик Chrome позволяет выполнять код строку за строкой, при этом он выводит значения всех переменных и объектов. Этот способ намного удобнее ввода команды console log. Отладка более подробно рассматривается в главе 9. Если вы захотите опробовать ее прямо сейчас, лучш е всего начать с описания отладки в документации Node (https://nodejs.org/api/debugger.html). До настоящего момента мы говорили о том, как работает платформа Node и какие возможности она предоставляет разработчикам. Вероятно, вы с нетерпением ждете рассказа о том, что можно сделать с помощью Node на практике. В следующем раз­ деле рассматриваются разные типы программ, создаваемых на платформе Node. 1.5. Три основных типа программ Node Программы Node делятся на три основных типа: веб-приложения, средства ко ­ мандной строки и демоны и настольные приложения. К категории веб-приложений относятся простые приложения, предоставляющие одностраничные приложения, микрослужбы REST и веб-приложения полного стека. Возможно, вы уже исполь­ зовали средства командной строки, написанные с использованием Node, например npm, Gulp и webpack. Демоны (daemons) представляют собой фоновые службы. Хо­ рошим примером служит менеджер процессов PM2 (www.npmjs.com/package/pm2). 1.5. Три основных типа программ Node 37 Настольные приложения обычно строятся с применением фреймворка Electron (http://electron.atom .io/), использующего Node во внутренней реализации для на­ стольных веб-приложений. Примеры такого рода — текстовые редакторы Atom (h ttp s://atom .io/) и Visual Studio Code (https://code.visualstudio.com/). 1.5.1. Веб-приложения Технология Node предназначена для работы с JavaScript на стороне сервера, по­ этому ее выбор в качестве платформы для построения веб-приложений выглядит логично. Выполнение JavaScript на стороне клиента и сервера открывает возмож­ ности для повторного использования кода в разных средах. Веб-приложения Node обычно пишутся с применением таких фреймворков, как Express (http://expressjs. com/). В главе 6 рассматриваются основные серверные фреймворки, доступные для Node. Глава 7 посвящена Express и Connect, а темой главы 8 станет шаблонизация веб-приложений. Чтобы создать простейшее веб-приложение Express, создайте новый каталог и уста­ новите модуль Express: mkdir hello_express cd hello_express npm in it -y npm i express --save Теперь включите следующий код JavaScript в файл с именем server.js. Листинг 1.3. Веб-приложение Node const express = req uire('express'); const app = express(); a p p .g e t('/', (req, res) => { res.send('H ello World!'); }); app.listen(3000, () => { console.log('Express web app on localhost:3000'); }); Введите команду npm s t a r t , и в вашей системе появляется веб-сервер Node, рабо­ тающий на порту 3000. Открыв адрес http://localhost:3000 в браузере, вы увидите текст из строки res.send. Node также играет важную роль в мире разработки интерфейсов, потому что это основной инструмент, используемый при транспиляции других языков (например, TypeScript в JavaScript). Транспиляторы преобразуют код, написанный на одном высокоуровневом языке, на другой язык; в этом отношении они отличаются от тра­ диционных компиляторов, которые преобразуют код с высокоуровневого язы ка на 38 Глава 1. Знакомство с Node.js низкоуровневый. Глава 4 посвящена системам фронтэнда; в ней рассматриваются сценарии npm, Gulp и webpack. Не вся веб-разработка связана с построением веб-приложений. Иногда разработ­ чику приходится решать такие задачи, как извлечение данных со старого сайта, которые должны использоваться при его обновлении. Приложение Б полностью посвящено извлечению веб-данных для демонстрации возможностей исполни­ тельной среды JavaScript в Node с моделью DOM (D ocum ent O bjet Model), а также использования Node за пределами привы чной «ком фортной зоны» типичны х веб-приложений Express. Если же вы просто хотите быстро создать простейшее веб-приложение, в главе 3 приведено обучающее руководство по построению веб-приложений Node. 1.5.2. Средства командной строки и демоны Node используется для написания средств командной строки, таких как менеджеры процессов и транспиляторы JavaScript, используемые разработчиками JavaScript. Однако Node также может стать удобной платформой для написания удобных средств командной строки, которые выполняют другие операции, включая преоб­ разование графики, и сценариев для управления воспроизведением мультимедий­ ных материалов. Н иже приведен простой пример, который вы можете опробовать. Создайте новый файл с именем cli.js и добавьте в него следующие строки: const [nodePath, scriptPath, name] = process.argv; console.log('H ello', name); Запустите сценарий командой c l i . j s yourName; вы увидите сообщ ение H ello yourName. В сценарии функциональность деструктуризации ES2015 используется для извлечения третьего аргумента из p ro c e s s .a rg v . Объект p ro c ess доступен в любой программе; с его помощью программа получает аргументы, заданные пользователем при запуске. У программ командной строки Node также имеются полезные возможности. Если добавить в начало программы строку, которая начинается с #!, и предоставить фай­ лу разрешение на исполнение (chmod +x c l i . j s ) , вы сможете заставить командный интерпретатор использовать Node при запуске программы. После этого программы Node можно будет запускать точно так же, как любые другие сценарии командного интерпретатора. Для систем семейства Unix добавляется следующая строка: #!/usr/bin/env node При таком использовании Node вы сможете заменить свои сценарии командного интерпретатора командами Node. Это означает, что Node можно будет исполь­ зовать с любыми другими инструментами командной строки, включая фоновые 1.5. Три основных типа программ Node 39 программы. Программы Node могут запускаться при помощи cron или работать в фоновом режиме как демоны. Если все эти термины вам незнакомы, не беспокойтесь: глава 11 познакомит вас с написанием программ командной строки, и вы узнаете, как программы этого типа помогают Node проявить свои сильные стороны. Например, в программах командной строки широко используются потоки данных (stream s) как универсаль­ ный API, а потоки также относятся к числу самых полезных возможностей Node. 1.5.3. Настольные приложения Если вы работали с текстовым редактором Atom или Visual Studio Code — знайте, что вы все это время использовали Node. Фреймворк Electron использует Node для реализации своих операций, поэтому каждый раз, когда требуется выполнить ввод/ вывод с диском или сетью, Electron обращается к Node. Также Electron использует Node для управления зависимостями; это означает, что вы сможете добавлять па­ кеты из npm в проекты Electron. Чтобы быстро опробовать Electron на практике, клонируйте репозиторий Electron и запустите приложение: g it clone https://github.com /electron/electron-quick-start cd electron-quick-start npm in s ta ll && npm sta rt curl localhost:8081 О том, как написать приложение на базе Electron, рассказано в главе 12. 1.5.4. Приложения, хорошо подходящие для Node М ы рассмотрели некоторы е разновидности прилож ений, которы е можно п о­ строить с Node, но сущ ествуют некоторые типы приложений, в которых Node особенно зам етно превосходит конкурентов. N ode обычно и спользуется для создания веб-прилож ений реального времени; к этой категории мож ет отно­ ситься что угодно, от пользовательских прилож ений (скажем, чат-серверов) до внутренних систем сбора аналитики. Так как ф ункции в Jav a S c rip t являю тся полноправными объектами, а в Node имеется встроенная модель событий, на­ писание асинхронных программ реального времени проходит более естественно, чем в других язы ках сценариев. Если вы строите традиционные веб-приложения MVC (M odel-V iew -Controller), Node хорошо подходит и для них. На базе Node строятся многие популярные си­ стемы ведения блогов, например Ghost (https://ghost.org/); платформа Node хорошо зарекомендовала себя для подобных веб-приложений. Стиль разработки отличается от системы WordPress, построенной с использованием PHP, но Ghost поддерживает 40 Глава 1. Знакомство с Node.js многие аналогичные возможности, включая шаблоны и многопользовательские средства администрирования. Node также может делать то, что в других языках делается намного сложнее. Node базируется на JavaScript, поэтому в Node возможно выполнение браузерного кода JavaScript. Сложные клиентские приложения могут адаптироваться для выполнения на сервере Node, что позволяет серверам заранее генерировать веб-приложения; это сокращает время визуализации страниц в браузере и упрощает работу поисковых систем. Наконец, если вы занимаетесь построением настольных или мобильных приложе­ ний, опробуйте фреймворк Electron, работающий на базе Node. В наши дни, когда веб-интерфейсы пользователя не уступают настольным, настольные приложения Electron могут конкурировать с платформенными веб-приложениями при сокращен­ ном времени разработки. Electron также поддерживает три основные платформы, поэтому ваш код будет работать в Windows, Linux и macOS. 1.6. Заключение О Node — неблокирующая, управляемая событиями платформа для построения приложений JavaScript. О В качестве исполнительной среды JavaScript используется ядро V8. О Библиотека libuv обеспечивает быстрый кросс-платформенный неблокирующий ввод/вывод. О Node содержит небольшую стандартную библиотеку — базовые модули, которые добавляют в JavaScript поддержку сетевого и дискового ввода/вывода. О В поставку Node входит отладчик и менеджер зависимостей (npm). О Node используется для построения веб-приложений, средств командной строки и даже настольных приложений. Основы программирования Node В отличие от многих платформ с открытым кодом, Node легко настраивается и не предъявляет особых требований к памяти и дисковому пространству. Программи­ рование не требует применения сложных интегрированных средств разработки или систем построения. Тем не менее некоторые фундаментальные знания сильно помогут вам в начале работы. В этой главе рассматриваются две основные проблемы, с которыми сталкиваются начинающие разработчики Node: О организация кода; О асинхронное программирование. В этой главе будут представлены важные приемы асинхронного программирова­ ния, которые позволят вам держать под контролем процесс выполнения вашего приложения. Вы научитесь: О реагировать на одноразовые события; О реагировать на повторяющиеся события; О определять последовательность отработки асинхронной логики. Но для начала посмотрим, как для решения проблемы организации кода исполь­ зуются модули — основной механизм Node для организации кода и его упаковки для удобства повторного использования. 2.1. Структурирование и повторное использование функциональности Node При создании приложения, с использованием Node или без, часто наступает момент, когда хранить весь код в одном файле становится слишком неудобно. В традицион­ ном решении этой проблемы, представленном на рис. 2.1, разработчик брал файл с большим объемом кода и разбивал его на отдельные файлы. 42 Глава 2. Основы программирования Node Служебные функции Служебные функции | Команды і index.js 1----- ► 1 lib/utilityFunctions.js Команды lib/commands.js index.js __________________________ L. / / Весь код в одном файле Взаимосвязанная логика группируется в разных файлах Рис. 2.1. В коде удобнее ориентироваться, если он разбит по разным каталогам и файлам, а не хранится в одном огромном файле В реализациях некоторых языков (таких как P H P и Ruby) внедрение логики из другого файла (включаемого) означает, что вся логика, выполняемая в файле, влияет на глобальную область видимости. Все создаваемые переменные и все функции, объявленные во включаемом файле, заменяют переменные и функции, созданные и объявленные приложением. Предположим, вы программируете на PHP, и ваше приложение содержит следу­ ющую логику: function uppercase_trim($text) { return trim (strtoupper($text)); } include('string_handlers.php'); Если файл string_handlers.php также попытается определить функцию uppercase_trim, вы получите сообщение об ошибке: Fatal error: Cannot redeclare uppercase_trim() В P H P эта проблема решается за счет использования пространств имен; аналогич­ ную функциональность Ruby предлагает в виде модулей. Однако Node в принципе избегает эту потенциальную проблему, не предоставляя простых возможностей для случайного загрязнения глобального пространства имен. ПРОСТРАНСТВА ИМЕН PHP, МОДУЛИ RUBY Пространства имен P H P описаны в руководстве по язы ку P H P по адресу http://php .n et/m anual/en/language.nam espaces.php . М одули Ruby рассма­ триваются в документации Ruby: http://ruby-doc.org/core-2.3.1/M odule.htm l. 2.1. Структурирование и повторное использование функциональности Node 43 Модули Node упаковывают код для повторного использования, но при этом не и з­ меняют глобальную область видимости. Предположим, вы разрабатываете систему управления контентом (C M S) с открытым кодом на языке P H P и хотите исполь­ зовать стороннюю библиотеку API, которая не использует пространства имен. Библиотека может содержать класс с таким же именем, как у вашего приложения, и этот класс нарушит работу вашего приложения, если только вы не переименуете свой класс в приложении или библиотеке. Однако изменение имени класса в при­ ложении может создать проблемы у других разработчиков, которые пользуются вашей CM S-системой в своих проектах. После переименования класса в библиотеке вам придется помнить о необходимости повторять этот трюк каждый раз, когда вы обновляете библиотеку в дереве исходного кода вашего приложения. Конфликты имен — проблема, которую лучше избегать. Имена модулей позволяют выбрать, какие функции и переменные из включенного ф айла будут доступны для приложения. Если модуль возвращает более одной функции или переменной, модуль может указать их заданием свойств объекта с именем e x p o rts . Если модуль возвращ ает одну ф ункцию или переменную, вместо этого можно задать свойство m odule.exports. Н а рис. 2.2 показано, как работает эта схема. J Логика модуля заполняет module.exports или exports Рис. 2.2. Заполнение свойства module.exports или объекта exports позволяет модулю выбрать, какая информация должна быть доступна приложению Если описание кажется запутанным, не беспокойтесь; мы разберем некоторые при­ меры в этой главе. Избегая загрязнения глобальной области видимости, система модулей Node предотвращает конфликты имен или упрощает повторное использо­ вание кода. После этого модули могут быть опубликованы в реестре npm (менеджер пакетов), сетевой подборке готовых модулей Node, и распространяться в сообществе Node. При этом пользователям модулей не нужно беспокоиться о том, что модуль случайно заменит переменные и функции другого модуля. 44 Глава 2. Основы программирования Node Чтобы помочь вам в организации логики по модулям, мы рассмотрим следующие вопросы: О как создаются модули; О как модули хранятся в файловой системе; О о чем нужно знать при создании и использовании модулей. Чтобы с ходу взяться за изучение системы модулей Node, мы создадим новый про­ ект Node, а затем определим для него простой модуль. 2.2. Создание нового проекта Node Создать новый проект Node несложно: создайте папку и выполните команду npm i n i t . И все! Команда npm задаст несколько вопросов, вы можете ответить на все вопросы утвердительно. Полный пример: mkdir my_module cd my_module npm in it -y Ф лаг -y означает подтверждение (yes). Таким образом, npm создаст файл package. json со значениями по умолчанию. Если вы хотите полностью управлять процессом создания проекта, опустите флаг -y; npm задаст ряд вопросов о лицензии проекта, имени автора и т. д. После того как все будет сделано, просмотрите содержимое файла package.json. Вы можете отредактировать его вручную, но помните: файл должен содержать корректную разметку JSON. Пустой проект успешно создан, теперь можно переходить к созданию модуля. 2.2.1. Создание модулей Модуль может представлять собой как отдельный файл, так и каталог с одним или несколькими файлами (рис. 2.3). Если модуль оформлен в виде каталога, основному файлу в этом каталоге, который будет обрабатываться, обычно присваивается имя index.js (хотя и его можно переопределить: см. раздел 2.5). Ё m y_m odule.Js ▼ сЭ m y_m odule Рис. 2.3. Модули Node могут создаваться либо в виде файлов (пример 1), либо в виде каталогов (пример 2) Чтобы создать типичный модуль, создайте файл, определяющий свойства объекта ex p o rts с любыми данными (строками, объектами, функциями и т. д.). 2.2. Создание нового проекта Node 45 Чтобы продемонстрировать, как создаются базовые модули, мы добавим простей­ шую функциональность перерасчета валют в файл currency.js. Этот файл, представ­ ленный в следующем листинге, содержит две функции для пересчета канадских долларов в американские, и наоборот. Листинг 2.1. Определение модуля Node (currency.js) const canadianDollar = 0.91; function roundTwo(amount) { return Math. round(amount * 100) / 100; exports.canadianToUS exports.USToCanadian Функция canadianToUS определяется Bexports, чтобыона могла использоваться в коде, требующемэтот модуль. = canadian => roundTwo(canadian * canadianDollar);-<— = us => roundTwo(us / canadianDollar); -<■ Функция USToCanadian также определяется в exports. Обратите внимание: задаются только два свойства объекта exports. Таким обра­ зом, для приложения, включающего модуль, будут доступны только две функции: canadianToUS и USToCanadian. Переменная can ad ian D o llar закрыта от внешнего доступа: она влияет на логику работы canadianToUS и USToCanadian, но приложение не сможет обратиться к ней напрямую. Ч тобы использовать новы й модуль в программе, восп ользуй тесь ф ункцией Node re q u ire и передайте путь к нужному модулю в аргументе. Node выполняет синхронны й поиск м одуля и загруж ает его содержимое. С начала N ode ищет файлы среди базовых модулей, затем в текущем каталоге, и наконец, в каталоге node modules. REQUIRE И СИНХРОННЫЙ ВВОД/ВЫВОД R equire — одна из немногих синхронных операций ввода/вы вода в Node. Так как модули часто используются и обычно включаются в начале файла, синхронность require помогает сохранить чистоту, упорядоченность и удо­ бочитаемость кода. Старайтесь избегать использования require в частях вашего приложения, вы­ полняющих интенсивный ввод/вывод. Любой синхронный вызов блокирует работу, не позволяя Node делать что-либо до завершения вызова. Например, если вы запустили H T T P -сервер, выполнение require для каждого входного запроса снизит быстродействие приложения. Как правило, именно по этой причине require и другие синхронные операции используются только при начальной загрузке приложения. В файле test-currency.js (листинг 2.2) модуль currency.js включается вызовом require. 46 Глава 2. Основы программирования Node Листинг 2.2. Включение модуля (test-currency.js) Использует функциюcanadianToUS модуля currency. Впути означает, что модуль находится в одном каталоге со сценарием приложения. const currency = req u ire('./cu rren cy '); < console.log('50 Canadian dollars equals th is amount of US d o lla rs :'); ►console.log(currency.canadianToUS(50)); console.log('30 US dollars equals th is amount of Canadian d o lla rs :'); console.log(currency.USToCanadian(30)); -<---Использует функциюUSToCanadian модуля currency. Включение модуля с путем, начинающимся с . / , означает, что если вы создаете свой сценарий приложения с именем test-currency.js в каталоге currency_app, то файл модуля currency.js (рис. 2.4) также должен существовать в каталоге currency_app. При необходимости расширение .js подставляется по умолчанию, так что при желании его можно опустить. Если расширение .js не указано, Node также проверит файл .json с заданным именем. Ф айлы JS O N загружаются как объекты JavaScript. Рис. 2.4. Если аргумент require начинается с ./, Node проверяет тот каталог, в котором находится выполняемый файл После того как Node найдет и обработает ваш модуль, функция req u ire возвращает содержимое объекта e x p o rts, определенного в модуле. После этого вы сможете использовать две функции, возвращаемые модулем, для пересчета сумм в другую валюту. Если вы захотите определить структуру модулей, разместите модули в подкаталогах. Если, например, вы хотите хранить модуль currency в папке lib/, приведите строку re q u ire к следующему виду: const currency = re q u ire ('./lib /c u rre n c y '); Заполнение объекта exports модуля предоставляет простой механизм распределе­ ния повторно используемого кода по разным файлам. 2.3. Настройка создания модуля с использованием module.exports 47 2.3. Настройка создания модуля с использованием module.exports Хотя заполнение объекта exports функциями и переменными хорошо подходит для большинства случаев, время от времени требуется создать модуль с отклонением от этой модели. Например, модуль пересчета валют из предыдущего раздела можно переписать таким образом, чтобы он возвращал одну функцию-конструктор Currency вместо объекта, содержащего функции. Объектно-ориентированная реализация может выглядеть примерно так: const Currency = req u ire('./cu rren cy '); const canadianDollar = 0.91; const currency = new Currency(canadianDollar); console.log(currency.canadianToUS(50)); Возвращение функции из re q u ire (вместо объекта) сделает ваш код более элегант­ ным — если это единственное, что требуется от модуля. Казалось бы, чтобы создать модуль, возвращающий одну переменную или ф унк­ цию, нужно присвоить exports то, что вы хотите вернуть. Однако такое решение не сработает, потому что Node не ожидает, что exports будет присваиваться любой другой объект, функция или переменная. Код модуля в следующем листинге пы ­ тается присвоить exports функцию (листинг 2.3). Листинг 2.3. Модуль работать не будет class Currency { constructor(canadianDollar) { this.canadianDollar = canadianDollar; } roundTwoDecimals(amount) { return Math.round(amount * 100) / 100; } canadianToUS(canadian) { return this.roundTwoDecimals(canadian * this.canadianDollar) } USToCanadian(us) { return this.roundTwoDecimals(us / this.canadianDollar); } } exports = Currency; -<------- Ошибка; Node не позволяет переписывать exports. Чтобы приведенный код модуля работал так, как ожидается, нужно заменить exports на module .exports. Механизм module .exports позволяет экспортировать одну перемен­ ную, функцию или объект. Если вы создаете модуль, который заполняет как exports, так и m odule.exports, то возвращается m odule.exports, а exports игнорируется. 48 Глава 2. Основы программирования Node ЧТО В ДЕЙСТВИТЕЛЬНОСТИ ЭКСПОРТИРУЕТСЯ В конечном итоге в вашем прилож ении экспортируется m odule.exports. exports задается как глобальная ссылка на module.exports — изначально это пустой объект, к которому можно добавлять свойства. exports.myFunc — со­ кращенная запись для module.exports.myFunc. В результате присваивание exports другой ссылки разрывает связь между module.exports и exports. Так как экспортируется module.exports, exports ра­ ботает не так, как ожидается, — он уже не ссылается на module.exports. Чтобы сохранить эту связь, снова включите в module.exports ссылку на exports: module.exports = exports = Currency; Использовав exports или module.exports в зависимости от ваших потребно­ стей, вы сможете распределить функциональность по модулям и избежать проблем с постоянно растущими сценариями приложения. 2.4. Повторное использование модулей с папкой node_modules Включение модулей с указанием их местонахождения в файловой системе от­ носительно прилож ения полезно для организации кода, специфического для данного прилож ения. И наче дело обстоит с кодом, предназначенны м для и с­ пользован и я в нескольких прилож ени ях или распространения среди других разработчиков. Node имеет уникальны й механизм повторного использования кода, которы й позволяет вклю чать модули без точной инф орм ации об их м е­ стонахождении в файловой системе. Этот механизм основан на использовании каталогов node_modules. В приведенном ранее примере вклю чался модуль ./c u r r e n c y . Если убрать . / и включить просто currency, Node начинает искать этот модуль по схеме, пред­ ставленной на рис. 2.5. Переменная окружения NODE_PATH позволяет выбрать альтернативные каталоги для хранения модулей Node. Если переменная NODE_PATH используется, ей должен быть присвоен список каталогов, разделенных символом точки с запятой (;) в Windows или двоеточием (:) в других операционных системах. 2.5. Потенциальные проблемы Хотя система модулей Node устроена достаточно прямолинейно, вы должны учи­ тывать два обстоятельства. 2.5. Потенциальные проблемы Рис. 2.5. Последовательность поиска модуля 49 50 Глава 2. Основы программирования Node Во-первых, если модуль представляет собой каталог, ф айл в каталоге модуля, который будет обработан, должен назы ваться index.js, если только обратное не указано в файле package.json в каталоге модуля. Чтобы задать другой файл вме­ сто index.js, файл package.json должен содержать данные JS O N (JavaScript Object N otation), определяющие объект с ключом main и значением, которое определяет путь к основному файлу в каталоге модуля. Эти правила обобщены в блок-схеме на рис. 2.6. Рис. 2.6. Файл package.json в каталоге модуля позволяет определить модуль с использованием файла, отличного от index.js Пример файла package.json, который назначает основным файлом currency.js: { "main": "currency.js" } Другое обстоятельство, о котором следует помнить, —это способность Node кэширо­ вать модули как объекты. Если два файла в приложении включают один и тот же мо­ дуль, то данные, возвращенные для первого вызова require, будут сохранены в памяти, поэтому второму вызову require не нужно будет проверять исходные файлы модуля. А это означает, что загрузка модуля с вызовом req u ire в том же процессе вернет тот же объект. Представьте, что вы создаете веб-приложение MVC, у которого имеется 2.6. Средства асинхронного программирования 51 основной объект приложения. Вы можете настроить этот объект, экспортировать его, а затем включить в любой точке проекта вызовом require. Если в объект приложения были добавлены полезные данные конфигурации, вы сможете обратиться к ним из других файлов — при условии, что проект имеет следующую структуру каталогов: проект app.j s models p o st.js На рис. 2.7 показано, как работает эта схема. app.js models/post.js Рис. 2.7. Общий объект приложения в веб-приложении Лучший способ освоиться с системой модулей Node — поэкспериментировать с ней и самостоятельно проверить поведение, описанное в этом разделе. Теперь, когда вы в общих чертах понимаете, как работают модули, можно переходить к средствам асинхронного программирования. 2.6. Средства асинхронного программирования Если вы когда-либо занимались разработкой фронтэнда, в которой события (н а­ пример, щелчки мышью) инициируют выполнение логики, — значит, вы уже за­ нимались асинхронным программированием. Асинхронное программирование на стороне сервера работает по тем же принципам: происходят события, по которым срабатывает ответная логика. В мире Node для управления логикой ответов и с­ пользуются две популярные модели: обратные вызовы и слушатели событий. Обратные вызовы обычно определяют логику для одноразовых ответов. Например, при выполнении запроса к базе данных можно назначить функцию обратного вы ­ зова, определяющую, что нужно сделать с результатами запроса. Ф ункция обрат­ ного вызова может вывести результаты, выполнить с ними некие вычисления или выполнить другую функцию обратного вызова, использовав результаты запроса в качестве аргумента. 52 Глава 2. Основы программирования Node Слушатели событий представляют собой обратные вызовы, связанные с концеп­ туальной сущностью (событием). Скажем, щелчок кнопкой мыши — это событие, которое обрабатывается в браузере. А код Node в сервере H TTP генерирует событие req u est при выдаче запроса HTTP. Вы можете прослушать событие req u est и до­ бавить логику ответа. В следующем примере функция handleRequest будет вызы­ ваться каждый раз, когда генерируется событие request; для этого вызов метода E v en tE m itter.p ro to ty p e.o n связывает слушателя события с сервером: serv er.o n ('req u est', handleRequest) Экземпляр сервера H TTP в Node является примером генератора событий — класса (E ventE m itter), который может использоваться при наследовании и который до­ бавляет возможность генерирования и обработки событий. Многие аспекты базовой функциональности Node наследуются от EventEm itter; вы также можете создать собственный генератор событий. Итак, мы выяснили, что логика ответа обычно организуется в Node одним из двух способов. Теперь можно перейти непосредственно к асинхронному программиро­ ванию; будут рассмотрены следующие темы: О как обрабатывать одноразовые события с обратными вызовами; О как реагировать на повторяющиеся события при помощи слушателей событий; О как преодолеть некоторые трудности асинхронного программирования. Начнем с одного из самых распространенных способов организации асинхронного кода: функций обратного вызова. 2.7. Обработка одноразовых событий в обратных вызовах Ф ункция обратного вызова (callback) — функция, которая передается в аргументе асинхронной функции и описывает, что нужно делать по завершении асинхронной операции. Функции обратного вызова часто применяются в разработке Node —чаще, чем генераторы событий, и с ними проще работать. Чтобы продемонстрировать использование обратных вызовов в приложении, по­ смотрим, как создать простой сервер HTTP, который: О асинхронно читает заголовки последних сообщений, хранящиеся в файле JSON; О асинхронно читает базовый шаблон HTML; О строит страницу HTM L с заголовками; О отправляет страницу HTM L пользователю. Примерный результат показан на рис. 2.8. 2.7. Обработка одноразовых событий в обратных вызовах 53 Файл JSO N (titles.json), приведенный в листинге 2.4, отформатирован в виде массива с заголовками сообщений. • f- D localhost:8000 -> С х _ Alex О lo c a lh o st:8 0 0 0 : Latest Posts • Kazakhstan is a huge country... what goes on there? • This weather is making me craaazy • My neighbor sort o f howls at night Рис. 2.8. HTML-ответ от веб-сервера, который читает заголовки из файла JSON и возвращает результаты в виде веб-страницы Листинг 2.4. Список заголовков сообщений [ "Kazakhstan is a huge country... what goes on there?", "This weather is making me craaazy", "My neighbor sort of howls at night" ] Ф айл шаблона H TM L (template.html) из листинга 2.5 включает только базовую структуру для вставки заголовков сообщений. Листинг 2.5. Базовый шаблон HTML для вывода заголовков <!doctype html> <html> <head></head> <body> <h1>Latest Posts</h1> <ul><li>%</li></ul> < ------</body> </html> %заменяется данными заголовка. Код чтения данных из файла JSO N и построения веб-страницы приведен в листин­ ге 2.6 (файл blog_recent.js). Листинг 2.6. Использование обратных вызовов в простом приложении Создает сервер HTTPи использует обратный вызовдля определения логики ответа. const http = re q u ire ('h ttp '); const fs = re q u ire ('fs '); http.createServer((req, res) => { < ­ i f (req.url == ' / ' ) { f s .r e a d F ile ( './title s .js o n ', (err, data) => { Читает файл JSON и использует обратный вызовдля определения того, как поступить с его содержимым. 54 Глава 2. Основы программирования Node i f (err) { Если возникает ошибка, зарегистрировать ее console.error(err); и вернуть клиентусообщение «Server Error». res.end('Server E rror'); } else { const t i t l e s = JSON.parse(data.toString()); ■<— Разбирает данные из текста JSON. fs.rea d F ile('./tem p late.h tm l', (err, data) => { Читает шаблон HTML i f (err) { и использует обратный console.error(err); вызов призавершении res.end('Server E rror'); загрузки. } else { const tmpl = data.toString(); const html = tm pl.replace('% ', title s .jo in ( '< /li> < li> ') ) ; Собирает res.writeHead(200, { 'Content-Type': 'text/htm l' }); страницу HTML res.end(html); <■ Отправляет страницу с заголовками } HTMLпользователю. сообщений. }); } }); } } ).liste n '127. 3.1'); В этом примере используются три уровня вложенности обратных вызовов: http.createServer((req, res) => { ... f s .r e a d F ile ( './title s .js o n ', (err, data) => { ... fs.rea d F ile('./tem p late.h tm l', (err, data) => { ... В трехуровневой структуре нет ничего плохого, но чем больше уровней вы и с­ пользуете, тем более громоздко выглядит ваш код и тем больше проблем возникает с его рефакторингом и тестированием, поэтому глубину влож ения желательно ограничить. Создавая именованные функции для обработки отдельных уровней, вы можете выражать ту же логику способом, который требует больше строк кода — но этот код будет проще в сопровождении, тестировании и рефакторинге. Следующий листинг функционально эквивалентен листингу 2.6. Листинг 2.7. Сокращение вложенности за счет определения промежуточных функций const http = re q u ire ('h ttp '); Клиентский запрос const fs = re q u ir e ('f s '); поступает здесь. http.createServer((req, res) => { < ---g etT itles(res); < ---Управление передается getTitles. }).listen(8000, '127.0.0.1'); function getT itles(res) { f s .r e a d F ile ( './title s .js o n ', (err, data) => { i f (err) { hadError(err, res); } else { getTemplate(JSON.parse(data.toString()), res); } getTitles извлекает заголовки и передает управление getTemplate. 2.7. Обработка одноразовых событий в обратных вызовах 55 }); } function getTem plate(titles, res) { getTemplate читает файл f s . 2ead F ile('./tem plate.htm l', (e rr, data) => { шаблона и передает i f (err) { управление formatHtml. hadE22 o r(e 22 , res); } else { formatHtm l(titles, data.toS tring(), res); formatHtml читает заголовки } и шаблон, после чего строит ответ }); для клиента. } function formatHtm l(titles, tmpl, res) { const html = tm pl.replace('% ', title s .jo in ( '< /li> < li> ') ) ; 2es.w2ІteHead(200, {'Content-Type': 'tex t/h tm l'}); res.end(html); } function hadE rror^rr, res) { < Если возникает ошибка, C0ns0le.e2202(e22); hadError выводит ее на res.end('Server E rror'); консоль и возвращает клиенту } сообщение «Server Error». Уровень вложенности также можно сократить блоками i f / e l s e с другой идиомой в разработке Node — ранним возвратом из функции. Следующий листинг ф унк­ ционально эквивалентен предыдущему, но избегает дальнейш его влож ения за счет раннего возврата управления. Кроме того, он явно указывает, что функция не должна продолжать выполнение. Листинг 2.8. Сокращение вложения за счет раннего возврата const http = re q u ire ('h ttp '); const fs = re q u ire ('fs '); http.createS erver^req, res) => { g etT itles(res); }).listen(8000, '127.0.0.1'); function getT itles(res) { f s .r e a d F ile ( './title s .js o n ', (err, data) => { i f (err) return hadE rror^rr, res); Вместо того чтобысоздавать ветвь else, getTemplatepSON.parse^ata.toStringQ), res); вывозвращаете управление, потому что }); вслучае возникновения ошибки } продолжать выполнение этой функции function getTem plate(titles, res) { не нужно. fs.rea d F ile('./tem p late.h tm l', (e rr, data) => { i f (err) return hadError(err, res); formatH tm l(titles, data.toS tring(), res); }); } function formatHtm l(titles, tmpl, res) { const html = tm pl.replace('% ', title s .jo in ( '< /li> < li> ') ) ; res.writeHead(200, { 'Content-Type': 'tex t/h tm l'} ); res.end(html); } function hadE rror^rr, res) { 56 Глава 2. Основы программирования Node console.error(err); res.end('Server E rror'); } Итак, теперь вы знаете, как использовать обратные вызовы для обработки одно­ разовых событий для таких задач, как определение ответов при чтении файлов и запросов веб-серверов, и мы можем перейти к организации событий с использо­ ванием генераторов событий. СОГЛАШЕНИЯ NODE ДЛЯ АСИНХРОННЫХ ОБРАТНЫХ ВЫЗОВОВ Б ольш инство встроенны х модулей N ode использует обратны е вызовы с двумя аргументами: первый аргумент предназначен для ошибки (если она возникнет), а второй — для результатов. Аргумент ошибки error часто сокращается до err. Типичный пример сигнатуры функции: const fs = re q u ire ('fs '); f s .r e a d F ile ( './title s .js o n ', (err, data) => { i f (err) throw err; / / Если ошибки не было, что-то сделать с данными }); 2.8. Обработка повторяющихся событий с генераторами событий Генераторы событий инициируют события и включают возможность обработки этих событий при их инициировании. Некоторые важные компоненты Node API — серверы HTTP, серверы T C P и потоки — реализую тся как генераторы событий. Разработчик также может создавать собственные генераторы. Как упоминалось ранее, события обрабатываются при помощи слушателей. Слуша­ тель (listener) представляет связь события с функцией обратного вызова, которая выполняется каждый раз при возникновении события. Например, у сокета TCP в Node имеется событие с именем data, которое инициируется каждый раз при по­ явлении новых данных в сокете: sock et.o n ('d ata', handleData); Посмотрим, как использовать события data для создания эхо-сервера. 2.8.1. Пример генератора событий Простой пример повторяющихся событий встречается в эхо-сервере. Когда вы от­ правляете данные эхо-серверу, он просто выводит полученные данные (рис. 2.9). 2.8. Обработка повторяющихся событий с генераторами событий П П 2. Shell 57 О Рис. 2.9. Эхо-сервер повторяет отправленные ему данные В листинге 2.9 приведен код, необходимый для реализации эхо-сервера. Каждый раз при подключении клиента создается сокет. Сокет представляет собой генератор событий, к которому можно добавить слушателя методом on для реакции на со­ бытия data. Эти события данных генерируются каждый раз при появлении новых данных в сокете. Листинг 2.9. Использование метода on для реакции на события const net = i^ q ijii^ C i^ t'); События data обрабатываются каждый const server = net.createServer^ocket => { раз при чтении н°выхдашых. so cket.on('data', data => { -<---socket.w rite(data); <Данные записываются на сторону клиента. server.listen(8888); Эхо-сервер запускается следующей командой: node echo_se2 ve2 .]s После того как эхо-сервер будет запущен, вы можете подключиться к нему следу­ ющей командой: teln et 127.0.0.1 8888 Каждый раз, когда подключенный сеанс teln et отправляет данные серверу, эти данные будут продублированы в сеансе telnet. TELNET В СИСТЕМЕ WINDOWS Если вы используете операционную систему Microsoft Windows, поддержка telnet может не устанавливаться по умолчанию, и вам придется устанавливать ее само­ стоятельно. Инструкции для различных версий Windows доступны на Technet: http:// mng.bz/egzr. 58 Глава 2. Основы программирования Node 2.8.2. Реакция на событие, которое должно происходить только один раз Слушатели могут определяться для многократной реакции на события, как в пре­ дыдущем примере, или же для однократной реакции. В следующем листинге, использующем метод once, приведенный ранее пример с эхо-сервером и зм еня­ ется так, чтобы он воспроизводил только первый фрагмент отправленных ему данных. Листинг 2.10. Использование метода once для реакции на одно событие const net = re q u ire ('n e t'); const server = net.createServer(socket => { socket.once('data', data => { < ------socket.w rite(data); }); }); server.listen(8888); Событие data будет обработано всего один раз. 2.8.3. Создание генераторов событий: публикация/подписка В предыдущем примере использовался встроенный Node API, использующий ге­ нераторы событий. Однако встроенный модуль событий Node позволяет создавать собственные генераторы событий. Следующий код определяет генератор событий channel с одним слушателем, ко­ торый реагирует на присоединение пользователя к каналу. Обратите внимание на использование on (или в более длинной альтернативной форме ad d L isten er) для добавления слушателя к генератору событий: const EventEmitter = require('events').EventEm itter; const channel = new EventEmitter(); channel.on('join', () => { console.log('Welcome!'); }); Однако функция обратного вызова jo in никогда не будет вызвана, потому что ника­ кие события еще не генерируются. Добавьте в листинг строку, которая инициирует событие функцией emit: channel.em it('join'); ИМЕНА СОБЫТИЙ События — ключи, с которыми может быть связано любое строковое значение: data, join и вообще произвольное длинное имя. Только одно событие с именем error играет особую роль; вскоре мы рассмотрим его. 2.8. Обработка повторяющихся событий с генераторами событий 59 Давайте посмотрим, как реализовать собственную логику публикации/подписки с использованием EventEm itter для создания канала передачи информации. З а ­ пустив сценарий из листинга 2.11, вы получите простой чат-сервер. Канал чатсервера реализуется как генератор событий, который реагирует на события jo in , генерируемые клиентами. Когда клиент присоединяется к каналу, логика слушателя jo in в свою очередь добавляет дополнительного слушателя для данного клиента к каналу широковещательного события, который будет записывать все рассылаемые сообщения в клиентский сокет. Имена типов событий (такие, как jo in и broadcast) выбираются абсолютно произвольно. При желании вы можете использовать другие имена для этих типов событий. Листинг 2.11. Простая система «публикация/подписка» с использованием генератора событий const events = req u ire('ev en ts'); const net = re q u ire ('n e t'); Добавляет для события join const channel = new events.EventEmitter(); слушателя, который сохраняет channel.clients = {}; объект client, чтобы приложение channel.subscriptions = {}; могло отправитьданные channel.on('join', function(id, client) { обратно пользователю. th is .c lie n ts [id ] = client; th is.su b scriptions[id] = (senderld, message) => { i f (id != senderld) { <Игнорирует данные, если они передаются this.clients[id].w rite(m essage); напрямуюв широковещательной рассылке. } }; th is.o n ('b ro ad cast', th is.su b scrip tio n s[id ]); ■<— Добавляет слушателя, }); относящегося ктекущему const server = net.createServer(client => { пользователю, для события const id = '${client.remoteAddress}:${client.remotePort} broadcast. channel.em it('join', id, clie n t); -<Генерирует событие join при подключении пользователя c lie n t.o n ('d a ta ', data => { ксерверу, с указанием идентификатора пользователя data = data.toString(); иобъекта client. channel.em it('broadcast', id, data); Генерирует событие }); ш ироковещательной рассылки }); в канале с указанием идентификатора server.listen(8888); пользователя исообщения при отправке данныхлюбым пользователем. После того как чат-сервер заработает, откройте новое окно командной строки и введите следующий код для входа в чат: teln et 127.0.0.1 8888 Если вы открыли несколько окон командной строки, вы увидите, что все данные, вводимые в одном окне, воспроизводятся в других. У этого чат-сервера есть серьезная проблема: при закрытии подключений и вы ­ ходе пользователя из чат-комнаты остается слушатель, который будет пытаться 60 Глава 2. Основы программирования Node записы вать данные клиенту, который уже не подключен. Конечно, произойдет ошибка. Чтобы решить ее, необходимо добавить слушателя в генератор событий channel (листинг 2.12) и добавить логику в слушателя события close для генериро­ вания события leave канала. Событие leave удаляет слушателя broadcast, который был изначально добавлен для клиента. Листинг 2.12. Создание слушателя для завершения при отключении клиента Создает слушателя для события leave. channel.on('leave', function(id) { channel.removeListener( 'broadcast', this.subscriptions[id] ); channel.em it('broadcast', id, '${id} has le f t the chatroom.\n'); }); ^ server = net.createServer(client ^ ^ , / ! • - . . => . {г const c lie n t.o n ('c lo se ', () => { channel.em it('leave', id); }); }); server.listen(8888); •<— Удаляет широковещ ательного r слушателя для конкретного клиента. Генерирует события leave для отключения клиента. Если по какой-то причине вы захотите отключить чат без завершения сервера, вы также можете использовать метод генератора события rem o v eA llL isten ers для удаления всех слушателей заданного типа. Следующий код показывает, как эта возможность реализуется для нашего чат-сервера: channel.on('shutdown', () => { channel.em it('broadcast', ' ' , 'The server has shut down.\n'); channel.removeAllListeners('broadcast'); }); Затем можно добавить поддержку команды чата, завершающей общение. Д ля этого измените слушателя для события data в следующем коде: c lie n t.o n ('d a ta ', data => { data = data.toString(); if (data === 'shutdown\r\n') { channel.emit('shutdown'); } channel.em it('broadcast', id, data); }); Теперь при вводе любым участником в чате команды shutdown все участники будут отключены. 2.8. Обработка повторяющихся событий с генераторами событий 61 ОБРАБОТКА ОШИБОК Стандартный прием, часто применяемый при создании генераторов собы­ тий, — генерирование события типа error вместо прямого инициирования ошибки. Это позволяет вам определять специальную логику реакции на события посредством назначения одного или нескольких слушателей для данного типа события. Следующий код показывает, как слушатель событий обрабатывает сгенери­ рованную ошибку, выводя информацию на консоль: const events = re q u ire^ ev e n ts'); const myEmitter = new events.EventEmitter(); m yEm itter.on^error', err => { console.log('ERROR: S^rr.m essage}'); }); m yEm itter.em it^error', new Error^Something is wrong.')); Если слушатель для этого типа события не определен при генерировании типа события error, генератор события выводит трассировку стека (список команд программы, выполнявшихся до точки возникновения ошибки) и пре­ рывает выполнение. В трассировке стека обозначен тип ошибки, заданный вторым аргументом вызова emit. Это поведение присуще исключительно событиям типа error; при генерировании других типов событий, не имеющих слушателей, ничего не происходит. Если событие типа error генерируется без передачи объекта error во втором аргументе, в трассировке стека будет указано «неперехваченное неопреде­ ленное событие ошибки ‘error’», и приложение аварийно завершается. С у­ ществует устаревший механизм, который может применяться для обработки таких ошибок, — вы можете определить собственную реакцию, определив глобальный обработчик в следующем коде: process.on('uncaughtException', err => { co n so le.erro r^ rr.stac k ); p ro cess.ex it(l); }); Были разработаны некоторые альтернативы для такого решения — например, домены ( h ttp ://nodejs.org/api/dom ain.htm l), но они не считаются готовыми для применения в реально эксплуатируемом коде. Если вы захотите предоставить пользователям, подключающимся к чату, счетчик пользователей, активных в настоящий момент, можно использовать следующий метод l i s t e n e r s , который возвращ ает массив слуш ателей для заданного типа события: 62 Глава 2. Основы программирования Node channel.on('join', function(id, client) { const welcome = ' Welcome! Guests online: ${th is.listen ers('b ro ad cast').len g th } ; client.write('${welcom e}\n'); Чтобы увеличить количество слушателей у генератора событий и чтобы избежать предупреждений, которые Node выдает при более 10 слушателях, также можно воспользоваться методом setM ax L isten ers. Если взять наш генератор событий в качестве примера, для увеличения количества разрешенных слушателей можно использовать следующий код: channel.setMaxListeners(50); 2.8.4. Доработка генератора событий: отслеживание содержимого файлов Чтобы расширить поведение генератора событий, вы можете создать новый класс JavaScript, наследующий от генератора событий. Например, вы можете создать класс Watcher для обработки файлов, находящихся в заданном каталоге файловой системы. После этого класс может использоваться для создания программ, отсле­ живающих содержимое каталога (все файлы, помещенные в каталог, переимено­ вываются с приведением символов к нижнему регистру, после чего копируются в отдельный каталог). После создания объекта Watcher необходимо дополнить методы, унаследованные от EventEm itter, двумя новыми методами из листинга 2.13. Листинг 2.13. Расширение функциональности генератора событий const fs = re q u ire ('fs '); const events = req u ire('ev en ts'); class Watcher extends events.EventEmitter { <constructor(watchDir, processedDir) { super(); this.watchDir = watchDir; this.processedDir = processedDir; } watch() { f s . re ad d ir(th is.watchDir, (e rr., file s ) => { ■< i f (err) throw err; for (var index in file s ) { th is.em it('p ro c ess', files[index]); } }); Расширяет объект EventEmitter методом, обрабатывающимфайлы. I Обрабатывает каждый файл I в отслеживаемом каталоге. 2 .{3. Обработка повторяющихся событий с генераторами событий } s ta 2 t ( ) { fs.w atchF ile(this.w atchD i 2 , () => { th is.w a tc h (); }); } 63 Добавляет метод для начала отслеживания. } module.exports = Watcher; М етод watch перебирает содержимое каталога и обрабатывает все найденные файлы. М етод s t a r t запускает отслеж ивани е каталога. Д л я отслеж ивани я используется ф у н к ц и я N ode f s .w a t c h F i l e ; когда в катал о ге что-то происходит, срабаты вает метод watch, которы й перебирает содерж им ое каталога и выдает собы тие p ro cess д ля каж дого обнаруж енного файла. После того как класс Watcher будет определен, его можно использовать для создания объекта W atcher следую щ ей командой: const watcher = new W atcher^atchD ir, processedDir); Д л я созданного объекта W atcher вы зы вается метод on, унаследованны й от класса генератора собы тия; он задает логику обработки каж дого файла: w a tc h er.o n ^p ro cess', ( f ile ) => { const watchFile = '${w atchD ir}/${file}'; const processedFile = ^ { p ro cessed D ir^S file.to L o w erC aseQ } '; fs.renam e(w atchFile, processedFile, e rr => { i f (e rr) throw e rr; }); }); В ся необходи м ая л о ги ка готова. З а п у с т и т е отслеж и ван и е каталога следую щ им кодом: w a tc h e r.s ta rt(); П осле р азм ещ ен и я кода W atcher в сцен ар и и и созд ан и я каталогов watch и done зап усти те сцен ари й с и сп о льзо ван и ем N o d e и скоп и руй те ф ай л ы в катал ог о т ­ слеж иван ия — вы увидите, что ф айлы появляю тся (с переим енованием в ниж ний регистр) в итоговом каталоге. Этот прим ер показывает, как использовать генератор собы тий д ля создания новы х классов. Н аучивш ись использовать обратные вы зовы для определения асинхронной логики и использовать генераторы собы тий для многократного срабаты вания асинхронной логики, вы приблизитесь на ш аг к умению управлять при лож ениям и Node. Однако отдельны й обратны й вы зов и ли слуш атель генератора собы тий мож ет вклю чать логику вы полнения дополнительны х асинхронных задач. Если порядок выполнения 64 Глава 2. Основы программирования Node этих задач важен, вы м ож ете столкнуться с новой проблемой: как управлять тем, когда им енно будет вы полняться каж дая задача в серии асинхронны х задач? П реж де чем зан яться управлением потоком вы полнен ия (эта тем а будет рассм а­ триваться в разделе 2.10), стоить рассм отреть некоторы е проблемы, с которы м и вы с больш ой вероятностью столкнетесь при нап исании асинхронного кода. 2.9. Проблемы с асинхронной разработкой П ри создании асинхронны х при лож ений необходимо вним ательно следить за п о ­ током вы полнен ия при лож ения и состоянием прилож ения: услови ям и ци кл а со­ бытий, перем енны ми при лож ения и лю бы м и другими ресурсами, изм еняю щ им ися в ходе вы полнен ия л огики программы. Ц и к л собы тий N ode, наприм ер, о тслеж ивает асинхрон ную логику, вы полнен ие которой еще не заверш илось. П ока остается незаверш енная асинхронная логика, вы ­ ход из процесса N ode не происходит. П оведение постоянно вы полняемого процесса N ode хорош о подходит д ля такого прилож ения, как веб-сервер, но неж елательно д л я процессов, которы е долж ны завер ш и ться по истечении некоторого периода вр ем ени (н ап р и м ер , програм м ком ан дной строки ). Ц и к л собы тий отслеж ивает все подклю чения к базе данных, пока они не будут закры ты , не п озволяя N ode з а ­ верш ить работу прилож ения. П ерем енны е при лож ения такж е могут неож иданно изм еняться при недостаточном соблю дении осторож ности. В ли сти н ге 2.14 приведен прим ер того, как порядок вы п о л н ен и я асинхронного кода м ож ет вы звать путаницу. Е сли бы прим ер кода вы п о л н ял ся синхронно, м ож но предполож ить, что было бы выведено сообщ ение «T he color is blue». Н о поскольку прим ер вы полняется асинхронно, значение пере­ м енной c o lo r и зм ен яется до вы п о л н ен и я c o n s o le .lo g , и вы вод и тся сообщ ение «T he color is green». Листинг 2.14. Ошибки, вызванные изменением области видимости function asyncFunction(callback) { setTim eout(callback, 200); } l e t color = 'b lu e '; asyncFunction(() => { console.log('The color is $ {co lo r} '); < ------ Выполняется в последнюю очередь (на 200 мс позже). }); color = 'g re e n '; Чтобы «заморозить» содержимое переменной color, можно изменить логику и исполь­ зовать замыкание (closure) JavaScript. В листинге 2.15 вызов asyncFunction заключен в анонимную функцию, которая получает аргумент color. Затем анонимная функция, которой отправляется текущее содержимое co lo r, немедленно выполняется. Так как 2.10. Упорядочение асинхронной логики 65 c o lo r становится аргументом анонимной функции, значение становится локальным для области видимости этой функции, и при изменении значения c o lo r за пределами анонимной функции локальная копия остается без изменений. Листинг 2.15. Использование анонимной функции для сохранения значения глобальной переменной function asyncFunction(callback) { setTim eout(callback, 200); } le t color = 'b lu e '; (color => { asyncFunction(() => { console.log('T he color i s ' , color); }); } )(c o lo r); color = 'g re e n '; Это всего лиш ь один из м ногочисленны х приемов из области програм м ирования на Jav aS crip t, которы е встретятся вам в ходе разработки Node. ЗАМЫКАНИЯ За дополнительной информацией о замыканиях обращайтесь к документации JavaScript от Mozilla: https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures. Теперь вы знаете, как использовать зам ы кан и я д ля уп равлени я состоянием п р и ­ л о ж ен и я , и м ы м ож ем п ер ей ти к тем е у п о р я д о ч е н и я ас и н х р о н н о й л о ги к и д ля уп равлени я потоком вы полнен ия прилож ения. 2.10. Упорядочение асинхронной логики В процессе вы полнен ия асинхронной програм мы некоторы е задачи могут в ы п ол ­ н яться в лю бой м ом ент независим о от того, что происходит в остальном коде п ро­ граммы, без каких-либо проблем. О днако некоторы е задачи долж ны вы полняться только до и ли после других задач. В сообщ естве N ode концепцию упорядочени я групп асинхронны х задач принято назы вать «потоком вы полнения». С ущ ествует две разнови дн ости потока вы п ол ­ нения: последовательны й и параллельны й (рис. 2.10). Зад ачи , которы е д олж ны в ы п о л н яться одна после другой, назы ваю тся последо­ вательными. П ростой при м ер — задачи со зд ан и я каталога и сохранени я ф ай л а в каталоге. Ф ай л невозм ож но сохранить до того, как каталог будет создан. 66 Глава 2. Основы программирования Node Serial execution Parallel execution Рис. 2.10. Последовательное выполнение асинхронных задач концептуально сходно с синхронной логикой: задачи выполняются одна за другой. С другой стороны, параллельные задачи могут выполняться одновременно друг с другом Задачи, которые не обязаны вы полняться одна за другой, назы ваю тся параллельны­ ми. О тносительны й порядок запуска и остановки этих задач мож ет быть неважен, но все эти задачи долж ны бы ть заверш ен ы до вы п ол н ен и я дальн ейш ей логики. П рим ер — загрузка нескольких ф айлов, которы е позднее будут сж аты в zip-архив. Ф ай л ы могут загруж аться одновременно, но все загрузки долж ны быть заверш ены до создания архива. У п р авлен и е п о сл ед о в ател ьн ы м и п ар ал л ел ь н ы м потоком в ы п о л н ен и я требует некоторы х служ ебны х операций на програм м ном уровне. П ри реализаци и после­ довательного потока вы полнен ия приходится отслеж ивать задачу, вы полняем ую в н астоящ и й момент, и ли ж е поддерж ивать очередь н евы полн енны х задач. П ри реализаци и параллельного вы полнен ия необходимо хранить инф орм ацию о том, сколько задач отработало до заверш ения. С редства у п равлени я потоком вы полнен ия берут на себя рутинны е операции, что упрощ ает группировку асинхронны х последовательны х или параллельны х задач. В сообщ естве было создано достаточно много дополнений, предназначенны х для упорядочения асинхронной логики, сам остоятельная реализация этих средств сн и ­ м ает с них покров тайны и помогает вам глубж е понять, как реш аю тся проблемы асинхронного програм м ирования. 2.11. Когда применяется последовательный поток выполнения 67 В следую щ их разделах мы покажем: О когда использовать последовательны й поток вы полнения; О как реализовать последовательны й поток вы полнения; О как реализовать параллельны й поток вы полнения; О как использовать сторонние м одули д ля у п равлени я потоком вы полнения. Д л я начала стоит разобраться, когда и как в асинхронном м ире при м ен яется п о ­ следовательны й поток вы полнения. 2.11. Когда применяется последовательный поток выполнения Д л я последовательного вы полнения нескольких асинхронных задач можно восполь­ зоваться обратны м и вы зовами. Н о если таких задач достаточно много, их придется как-то организовать. Если этого не сделать, код получится слиш ком гром оздким из-за чрезм ерного влож ени я обратны х вызовов. Н иж е приведен пример последовательного вы полнения задач с применением обрат­ ны х вызовов. В этом примере ф ункц ия setTim eout используется для моделирования задач, вы полнение которы х требует времени: первая задача вы полняется за одну секунду, вторая — за половину секунды, и последняя — за десятую долю секунды. Вы зов se tT im e o u t — и скусственн ая модель; реал ьн ы й код мож ет читать ф айлы , вы давать запросы H T T P и т. д. И хотя код этого прим ера достаточно короткий, он не отли чается элегантностью и не позволяет легко добавить еще одну задачу на програм м ном уровне. setTimeout(() => { c o n so le .lo g ('I execute f i r s t . ') ; setTimeout(() => { c o n so le .lo g ('I execute n e x t.') ; setTim eout(() => { c o n so le .lo g ('I execute l a s t . ') ; }, 100); }, 500); }, 1000); Также для вы полнен ия этих задач м ож но воспользоваться средствам и управления потоком вы полнен ия — например, A sync (http://caolan.github.io/async/). М одуль A sync прост в и сп о л ь зо в ан и и , а среди его д о стои н ств м ож н о вы д ел и ть малую кодовую базу (всего 837 байт после м и н и ф и к а ц и и и сж ати я). У становка Async устан авли вается следую щ ей командой: npm in s t a l l async 68 Глава 2. Основы программирования Node К од в следую щ ем листинге заново реализует приведенны й вы ш е ф рагм ент с п о ­ следовательны м потоком вы полнения. Листинг 2.16. Организация последовательного выполнения с использованием дополнений, разработанных в сообществе const async = re q u ire ('a s y n c '); asy n c .se rie s([ -<-------Массив функций, которые должны быть выполненыAsync - одна за другой. callback => { setTimeout(() => { c o n so le .lo g ('I execute f i r s t . ' ) ; callb ack (); }, 1000); }, callback => { setTimeout(() => { c o n so le .lo g ('I execute n e x t.') ; callb ack (); }, 500); }, callback => { setTimeout(() => { c o n so le .lo g ('I execute l a s t . ') ; callb ack (); }, 100); } ]); Х отя реализаци я, использую щ ая поток вы полнения, повы ш ает количество строк кода, обы чно она создает м еньш е проблем с чтением и сопровож дением . С корее всего, вы не будете использовать это реш ение постоянно, но, когда вы столкнетесь с ситуацией, в которой потребуется избеж ать влож ени я обратны х вы зовов, этот прием пом ож ет вам получить более удобочитаем ы й код. И так, мы рассм о тр ел и при м ер о р ган и зац и и последовательного у п р ав л ен и я п о ­ током с и сп о л ьзо в ан и ем с п ец и ал и зи р о в ан н ы х средств. Теперь посм отрим , как реализовать его с нуля. 2.12. Реализация последовательного потока выполнения Ч тобы организовать последовательное вы полнение нескольких асинхронных задач с использованием явного упр авл ен и я потоком, сначала нуж но пом естить задачи в массив в ж елательном порядке их вы полнения. К ак видно из рис. 2.11, этот массив определяет очередь: при завершении одной задачи из массива извлекается следующая. К аж дая задача представлена в массиве функцией. П ри заверш ении задача долж на вы звать ф ункцию -обработчик для указания статуса ош ибки и результатов. Ф унк- 2.12. Реализация последовательного потока выполнения 69 Задачи хранятся в желательном порядке выполнения _____________________________________ ,л,____________________________________ . г > \ Задача выполняет свою функцию, \ после чего вызывает функцию диспетчеризации для выполнения следующей задачи из очереди Рис. 2.11. Управление последовательным потоком выполнения ция-обработчик в этой реализации преры вает вы полнение в случае ош ибки. Если ош ибки не было, обработчик извлекает из очереди следующую задачу и выполняет ее. Ч тобы продем онстрировать реализацию у п равлени я последовательны м потоком вы полнения, мы создадим простое прилож ение, которое вы водит заголовок одной статьи и U R L -адрес случайно выбранного канала RSS. Список возм ож ны х каналов RSS храни тся в текстовом ф айле. В ы вод п р и лож ения вы глядит прим ерно так: Of Course ML Has Monads! http://lam bda-the-ultim ate.org/node/4306 В наш ем пр и м ер е и сп о л ьзу ю тся два в сп о м о гател ьн ы х м од ул я из реестра npm. О ткройте окно ком андной строки и введите следую щ ие команды, чтобы создать каталог для прим ера и установить вспом огательны е модули: mkdir listing_217 cd listing_217 npm i n i t -y npm in s t a l l --save request@2.60.0 npm in s t a l l --save htmlparser@1.7.7 М одуль re q u e s t реализует упрощ енного клиента H TTP, которы й мож ет исп ользо­ ваться для вы борки данны х RSS. М одуль h tm lp arse r обладает функциональностью , которая позволяет преобразовать низкоуровневые данные RSS в структуры данных JavaS crip t. Затем создайте в новом каталоге ф айл index.js с кодом из листинга 2.17. Листинг 2.17. Реализация управления последовательным потоком выполнения в простом приложении const fs = r e q u i r e ( 'f s ') ; const request = re q u ire ('re q u e s t'); const htmlparser = re q u ire ('h tm lp a rse r'); const configFilename = './r s s _ f e e d s .t x t'; function checkForRSSFile() { <- Задача 1: Убедиться в том, что файл со списком URL-адресов канала RSS существует. 70 Глава 2. Основы 2рограммирования Node fs.exists(configF ilenam e, (e x ists) => { i f (!e x ists) return next(new Error('M issing RSS f i l e : ${configFilenam e}')); ■< n ex t(n u ll, configFilename); При возникновении ошибки }); вернуть управление. } function readRSSFile(configFilename) { •<Задача 2: Прочитать и разобрать fs.readFile(configFilenam e, ( e rr, feedL ist) => { файл с URL-адресами канала. i f (e rr) return n e x t(e rr); feedL ist = feedL ist •<Преобразует список URL-адресов поставки .to S trin g () канала в строку, а затем в массив. .re p la c e (/A\s+ |\s+ $ /g , ' ' ) . s p l i t ( '\ n ') ; const random = Math.floor(Math.random() * feed L ist.len g th ); n ex t(n u ll, feedList[random]); Выбирает случайный }); URL-адрес из массива. } function downloadRSSFeed(feedUrl) { Задача 3: Выполнить запрос HTTP request({ u ri: feedUrl }, (e rr, re s, body) => { и получить данные выбранного канала. i f (e rr) return n e x t(e rr); i f (res.statusC ode !== 200) return next(new Error('Abnormal response sta tu s co d e')); n ex t(n u ll, body); }); Задача 4: Разобрать данные } RSS в массив. function parseRSSFeed(rss) { const handler = new htmlparser.RssHandler(); const parser = new htm lparser.P arser(handler); parser.parseC om plete(rss); i f (!handler.dom .item s.length) Выводит заголовок и URL-адрес return next(new Error('No RSS items fo u n d')); первого элемента, если он const item = handler.d o m .item s.sh ift(); существует. c o n s o le .lo g (ite m .title ); ■< co n so le.lo g (item .lin k ); } const task s = [ checkForRSSFile, ■<-----Добавляет каждую выполняемуюзадачу в массив в порядке выполнения. readRSSFile, downloadRSSFeed, parseRSSFeed ]; function n e x t(e rr, re s u lt) { Функция с именем next выполняет каждуюзадачу. i f (e rr) throw e rr; <.— — Выдает исключение, если в ходе выполнения задачи происходит ошибка. const currentTask = t a s k s .s h if t( ) ; < -------Следующая задача берется из массива задач. i f (currentTask) { cu rren tT ask (resu lt); <.-------Выполняет текущуюзадачу. } } next(); Запускает последовательное выполнение задач. Прежде чем тестировать прилож ение, создайте ф айл rss_feeds.txt в одном каталоге со сценарием прилож ения. Если у вас нет под рукой ни одного канала RSS, опробуйте кан ал блога N ode по адресу http://blog.nodejs.org/feed/. П ом ести те U R L -адреса 2.13. Реализация параллельного потока выполнения 71 канала RSS в текстовы й ф айл, по одному в каж дой строке ф айла. П осле того как ф айл будет создан, откройте окно командной строки и введите следующие команды, чтобы перейти в каталог при лож ения и вы полнить сценарий: cd listing_217 node index.js П оследовательны й поток вы полнения, как показы вает реализаци я примера, позво­ ляет вводить обратны е вы зовы в действие тогда, когда они необходимы — вместо простого влож ения. Разобравш ись с реализаци ей последовательного потока вы полнения, м ож но пере­ ходить к параллельном у вы полнению асинхронны х задач. 2.13. Реализация параллельного потока выполнения Чтобы несколько асинхронны х задач вы полнялись параллельно, задачи также необ­ ходимо поместить в массив, но на этот раз порядок их следования неважен. Каж дая задача долж на вы зы вать ф ункцию -обработчик, которая увеличивает количество заверш ен ны х задач. П осле того как все задачи будут заверш ены , ф у н к ц и я -о б р а­ ботчик долж на вы полнить некоторую заверш аю щ ую логику. В к ач естве п р и м ер а п а р а л л е л ь н о го п о то к а в ы п о л н е н и я мы созд ад и м п ростое при лож ение, которое ч итает содерж им ое текстовы х ф ай л ов и вы водит частоты исп ользован ия слов в ф айлах. Чтение содержимого текстовых файлов будет производиться при помощ и асинхрон­ ной ф ун кц и и re a d F ile , чтобы операции ч тен и я из ф айлов м огли осущ ествляться параллельно. Н а рис. 2.12 показано, как работает это прилож ение. Результат вы глядит прим ерно так (хотя, скорее всего, он будет намного длиннее): would: 2 wrench: 3 w riteable: 1 you: 24 О ткройте окно ком андной строки и введите следую щ ие команды, чтобы создать два каталога: один для примера, а другой, внутри него, — д ля текстовы х ф айлов, которы е нуж но проанализировать: mkdir listing_218 cd listing_218 mkdir te x t 72 Глава 2. Основы программирования Node З атем создайте в каталоге listing_218 ф айл word_count.js, содерж ащ ий код из л и с­ тинга 2.18. Листинг 2.18. Реализация управления параллельным потоком выполнения в простом приложении const f s = r e q u i r e ( 'f s ') ; const task s = []; const wordCounts = {}; const file s D ir = '. / t e x t '; l e t completedTasks = 0; f unct i on checkIfComp let e () { Когда все задачи будут завершены, вывести список comp let edTa sks++; всех слов, использованных в файлах, и количество i f (completedTasks === ta sk s.le n g th ) { вхождений каждого слова. for ( le t index in wordCounts) { < ----console.log('${index}: ${wordCounts[index]}'); } } } function addWordCount(word) { wordCounts[word] = (wordCounts[word]) ? wordCounts[word] + 1 : 1; } function countWordslnText(text) { const words = te x t .to S trin g () .toLowerCase() .sp lit(/\W + /) .s o r t( ) ; words .filte r(w o rd => word) .forEach(word => addWordCount(word)); } < -------- Подсчитывает вхождения слов втексте. fs .r e a d d ir ( f ile s D ir, ( e rr, f il e s ) => { Получает список файлов в каталоге. i f (e rr) throw e rr; f ile s .fo rE a c h (file => { -< const task = ( f il e => { Определяет задачудля обработки каждого return () => { файла. Каждая задача включает вызов функции, fs .r e a d F ile ( f ile , (e rr, te x t) => { которая асинхронно читает содержимое файла, i f (e rr) throw e rr; после чего подсчитывает в нем вхождения слов. countWordslnText(text); checkIfComplete(); }); }; Добавляет каждуюзадачу в массив функций, } )('$ { file s D ir} /$ { file } '); вызываемыхдля параллельного выполнения. task s.p u sh (task ); } tasks.forE ach(task => ta s k ( ) ) ; Запускает параллельное выполнение задач. }); 2.14. Средства, разработанные в сообществе 73 Каждый файл читается, и подсчет слов в каждом файле ведется асинхронно. Рис. 2.12. Управление параллельным потоком выполнения П реж де чем тестировать прилож ение, создайте несколько текстовы х ф айлов в к а ­ талоге, созданном ранее. О ткройте окно ком андной строки и введите следую щ ие команды , чтобы перейти в каталог п р и лож ения и вы полнить сценарий: cd word_count node word_count.js Теперь, когда вы знаете, как работает последовательное и парал л ельн ое в ы п о л ­ нение задач, рассм отрим инструм ентарий, разработанны й в сообщ естве. Э тот и н ­ струм ентарий позволяет легко управлять потоком вы полнения, и вам не придется реализовы вать его сам остоятельно. 2.14. Средства, разработанные в сообществе М ногие дополнения, разработанны е в сообществе, предоставляю т удобные средства уп равлени я потоком вы полнения. Н аибольш ей популярностью пользую тся такие 74 Глава 2. Основы программирования Node дополнения, как Async, Step и Seq. И хотя каж дое из них заслуж ивает вним ания, мы снова используем A sync в другом примере. ДОПОЛНЕНИЯ ДЛЯ УПРАВЛЕНИЯ ПОТОКОМ ВЫПОЛНЕНИЯ За дополнительной информацией о дополнениях для управления потоком выполне­ ния, разработанных в сообществе, обращайтесь к статье «Virtual Panel: How to Survive Asynchronous Programming in JavaScript» Вернера Шустера (Werner Schuster) и Дио Синодиноса (Dio Synodinos) на InfoQ: http://mng.bz/wKnV В л и сти н ге 2.19 п ри веден п ри м ер и сп о л ьзо в ан и я A sync д л я у п о р я д о ч ен и я в ы ­ полнен ия задач в сценарии, использую щ ем параллельны й поток вы полнен ия для одноврем енной загрузки двух ф айлов с их последую щ ей архивацией. СЛЕДУЮЩИЙ ПРИМЕР НЕ БУДЕТ РАБОТАТЬ В MICROSOFT WINDOWS Так как команды tar и curl не входят в комплект поставки операционной системы Windows, следующий пример не будет работать в этой операционной системе. Листинг 2.19. Использование средств управления потоком, разработанных в сообществе, в простом приложении Загружает исходный код Node const async = re q u ire ('a s y n c '); для заДанной версии const exec = re q u ire ('c h ild _ p ro c e ss').e x e c ; function downloadNodeVersion(version, d e stin a tio n , callback) { ■<----const u rl = ' h ttp ://n o d e ]s.o rg /d ist/v $ { v e rsio n } /n o d e -v $ { v ersio n } .ta r.g z'; const file p a th = '$ { d estin atio n } /$ { v ersio n } .tg z'; e x e c ('c u rl ${url} > $ { file p a th } ', callback); } asy n c .se rie s([ -<-------Последовательно выполняет серию задач. callback => { a sy n c .p a ra lle l([ < -------Выполняет загрузки параллельно. callback => { console.log('Downloading Node v 4 . 4 . 7 ...') ; downloadNodeVersion('4.4.7', '/tm p ', callback); }, callback => { console.log('Downloading Node v 6 . 3 . 0 ...') ; downloadNodeVersion('6.3.0', '/tm p ', callback); } ], callback); }, callback => { co nsole.log('C reating archive of downloaded f i l e s . . . ' ) ; exec( < -------Создает файл архива. 't a r cvf n o d e_ d istro s.tar /tm p /4 .4 .7 .tg z /tm p /6 .3 .0 .tg z ', e rr => { i f (e rr) throw e rr; 2.15. Заключение 75 co n so le.lo g ('A ll done!'); callback(); } ); } ]); В этом примере последовательны й поток вы полнения используется для того, чтобы загрузка гарантированно бы ла заверш ена до начала архивации. С ценарий определяет вспомогательную функцию , которая загруж ает заданную по­ ставляемую версию исходного кода Node. Затем последовательно вы полняю тся две задачи: п араллельная загрузка двух версий N ode и упаковка загруж енны х версий в новы й архивны й файл. 2.15. Заключение К од N ode м ож ет о ф о р м л я т ьс я в м одули, п р ед н азн ач ен н ы е д л я повторного и с ­ пользования. О Ф у н к ц и я r e q u ir e используется для загрузки модулей. О О бъекты m odule.exports и ex p o rts предназначены для предоставления внешнего доступа к ф ун кц и ям и перем енны м из модуля. О Ф ай л package.json определяет зависи м ости и ф айл, экспортируем ы й как основ­ ной. О Д л я у п равлени я асинхронной логикой м ож но использовать влож енны е обрат­ ны е вызовы, генераторы собы тий и средства уп равлени я потоком вы полнения. Что представляет собой веб-приложение Node? Э та глава полностью посвящ ена веб-прилож ениям Node. П рочитав ее, вы поймете, как вы глядят веб-п рилож ени я N ode и как приступить к их построению . Вы будете делать все, что делает соврем енны й веб-разработчик при создании прилож ений. М ы создадим веб-прилож ение по образцу таких поп улярн ы х сайтов отлож енного чтения, как In stap ap er (www.instapaper.com) и P o ck et (getpocket.com). В процессе работы нам пр ед сто и т со зд ать н о вы й п роект N ode, у п р а в л я ть зави си м о стям и , создать R E S T -совм естимы й A PI, сохранить инф орм ацию в базе данных, а такж е создать ин терф ейс на основе ш аблонов. Н а первы й взгляд довольно много, но все концепции этой главы будут более подробно исследованы в последую щ их главах. Н а рис. 3.1 показано, как вы глядит результат. Рис. 3.1. Веб-приложение для отложенного чтения 3.1. Структура веб-приложения Node 77 Н а странице при лож ения слева с целевого сайта удалены все элем енты навигации, оставлен только основной контент и заголовок. Ч то еще важнее, статья сохран я­ ется в базе данных; это позволит вам прочитать ее позднее, когда оригинал может оказаться недоступным. П реж де чем строить веб-приложение, следует сначала создать новый проект. В сле­ дую щ ем проекте показано, как создать проект N ode с нуля. 3.1. Структура веб-приложения Node Типичное веб-прилож ение N ode состоит из следую щ их компонентов: О package.json — ф айл со списком зависим остей и ком андой запуска прилож ения; О public/ — папка статических активов (C SS, кли ен тский код Jav aS crip t); О node_modules/ — место д ля устан овки зависим остей проекта; О один или несколько ф айлов Ja v a S c rip t с кодом прилож ения. Код п ри лож ен и я часто подразделяется следую щ им образом: О app.js и ли index.js — код подготовки прилож ения; О models/ — м одели баз данных; О views/ — ш аблоны, используем ы е для генерировани я страниц прилож ения; О controllers/ и ли routes/ — обработчики запросов H T T P ; О middleware/ — пром еж уточны е компоненты . Н е существует правил, диктую щ их структуру прилож ения; обычно веб-фреймворки обладаю т достаточной гибкостью и требую т настройки. Тем не менее описанная схема встречается во м ногих проектах. Всему этом у гораздо прощ е научиться на практике. Д авайте создадим заготовку веб-п рилож ени я так, как это делаю т опы тны е программисты Node. 3.1.1. Создание нового веб-приложения Ч тобы создать новое веб-п рилож ени е, необходим о создать новы й проект Node. Е сли вам потребуется освеж ить пам ять — обращ айтесь к главе 2; а мы в двух сл о­ вах напомним, что д ля этого нуж но создать каталог и вы полнить ком анду npm i n i t с настройкам и по умолчанию : mkdir la te r cd la te r npm i n i t -fy 78 Глава 3. Что представляет собой веб-приложение Node? Н о вы й проект создан; что д альш е? Б о л ьш и н ство лю дей д обавят в npm модуль, упрощ аю щ ий веб-разработку. В Node имеется встроенный модуль h ttp с сервером, но прощ е воспользоваться чем-то таким, что сокращ ает служ ебны й код, необходимый д ля типичны х задач веб-разработки. А теперь посмотрим, как установить Express. Добавление зависимости Д л я д о б ав л ен и я за в и си м о сти в п роект и сп о л ьзу й те npm . С л едую щ ая ком анда устанавливает Express: npm in s ta l l --save express Е сли теперь просм отреть ф айл package.json, вы увидите, что в него был добавлен модуль Express. С оответствую щ ий ф рагм ент вы глядит так: "dependencies": { "express": "Л4.14.0" } М одуль Express находится в папке node_modules/ проекта. Если вы захотите удалить Express из проекта, вы полните ком анду npm rm e x p re ss - -save. М одуль удаляется из node_modules/ и обновляет ф айл package.json. Простой сервер E xpress ориен ти руется на построение м одели при лож ен и я в контексте запросов и ответов H T T P и строится на базе встроенного м одуля N ode h ttp . Ч тобы создать простейшее приложение, следует создать экземпляр прилож ения вызовом e x p re s s (), добавить обработчик марш рута, а затем связать прилож ение с портом TCP. П олны й код примера: const express = re q u ire ('e x p re s s '); const app = express(); const port = process.env.PORT | | 3000; a p p .g e t ( '/ ', (req, res) => { res.sen d ('H ello W orld'); }); a p p .lis te n (p o rt, () => console.log('E xpress web app available at localhost: ${p o rt} '); }; Все не так сложно, как каж ется! С охраните код в ф айле с именем index.js и зап у ­ стите его ком андой node in d e x .js . Затем откройте стран ицу http://localhost:3000 для просмотра результатов. Ч тобы не приходилось запоминать, как именно должно запускаться каж дое прилож ение, м ногие разработчики использую т сценарии npm д ля упрощ ения процесса. 3.1. Структура веб-приложения Node 79 Сценарии npm Ч тобы сохранить ком анду запуска сервера (node in d e x .j s ) как сценарий npm, от­ кройте ф айл package.json и добавьте в раздел s c r i p t s новое свойство с именем s t a r t : "sc rip ts " : { " s ta r t" : "node in d ex .js", " te s t" : "echo \"E rro r: no t e s t sp e c ifie d \" && e x it 1" }, Теперь прилож ение можно запустить командой npm s t a r t . Если вы получите ошибку из-за того, что порт 3000 уж е используется на ваш ей маш ине, используйте другой порт командой PORT=3001 npm s t a r t . Сценарии npm использую тся для самых разных целей: построения пакетов на стороне клиента, зап уска тестов и генерировани я докум ентации. В них м ож но разм естить все, что угодно; по сути это м еханизм з а ­ пуска м ини-сценариев. 3.1.2. Сравнение с другими платформами Д л я сравнения приведем эквивалентное прилож ение P H P H ello W orld: <?php echo '<p>Hello World</p>'; ?> Код пом ещ ается в одной строке, он интуитивно понятен — каким и ж е преи м ущ е­ ствам и обладает более слож ны й прим ер N ode? Р азл и ч и я проявляю тся в парадигме программирования: с P H P ваш е прилож ение я вл яется страницей, а с N ode — серве­ ром. П ример для Node полностью управляет запросом и ответом, поэтому вы можете делать все, что угодно, без настройки сервера. Е сли вы хотите использовать сжатие H T T P или перенаправление U R L, эти возм ож ности м ож но реализовать как часть логики прилож ения. Вам не нуж но отделять логику H T T P от логики прилож ения; и то и другое я в л яется частью прилож ения. Вместо того чтобы создавать отдельную кон ф и гурац ию сервера H TTP, вы можете хранить ее в том ж е месте (то есть в том же каталоге). Это обстоятельство упрощ ает разверты ван ие при лож ений N ode и управление ими. Д ругая особенность, упрощ аю щ ая разверты ван ие при лож ений Node, — npm. П о ­ скольку зависимости устанавливаю тся на уровне проекта, у вас не будет конфликтов меж ду проектам и в одной системе. 3.1.3. Что дальше? И так, вы получили представление о создании проектов ком андой npm i n i t и уста­ новке зависи м остей ком андой npm i n s t a l l - -sa v e, вы смож ете быстро создавать новы е проекты . И это очень хорош о, потом у что вы см ож ете опробовать новые идеи без кон ф ли ктов с другим и проектами. Если п ояви тся новы й зам ечательны й 80 Глава 3. Что представляет собой веб-приложение Node? веб-ф рейм ворк, которы й вам захочется опробовать, создайте новы й каталог, вы ­ полните ком анду npm i n i t и установите м одуль из npm. К огда все будет готово, м ож н о пер ех о д и ть к н ап и сан и ю кода. Н а этой стадии в проект м ож но добавить ф айлы J a v a S c rip t и загрузить м одули, устан овлен ны е ком андой npm - -sav e, вы зовом re q u ire . С осредоточим ся на том, что больш инство веб-разработчиков делает после этого: на добавлении R E S T -совм естим ы х м арш ­ рутов. Это пом ож ет вам определить A P I п р и лож ения и понять, какие м одели базы данны х нуж ны в ваш ей ситуации. 3.2. Построение REST-совместимой веб-службы Н аш е прилож ение будет представлять собой R E S T -совместимую веб-службу, кото­ рая позволяет создавать и сохранять статьи по аналогии с Instapaper или Pocket. Она будет использовать модуль, которы й берет за основу исходную служ бу R eadability (www.readability.com) для преобразования неряш ли вы х веб-страниц в элегантны е статьи, которы е вы смож ете прочитать позднее. П ри проектировании R E S T -совм естим ой служ бы необходимо подумать над тем, какие операции вам потребую тся, и связать их с м арш рутам и в Express. В данном случае необходимо иметь возм ож ность сохранять статьи, организовы вать их вы ­ борку д ля чтения, получать список всех статей и удалять те статьи, которы е вам уж е не нужны. Э ти операции соответствую т следую щ им марш рутам: О POST / a r t i c l e s — создание новой статьи; О GET / a r t i c l e s / : i d — получение одной статьи; О GET / a r t i c l e s — получение всех статей; О DELETE / a r t i c l e s / : i d — удаление статьи. П реж де чем переходить к таким вопросам , как базы дан ны х и веб-интерф ейсы , остановим ся на создании R E S T -совм естимы х ресурсов в Express. Вы м ож ете вос­ пользоваться cU R L д ля вы дачи запросов к прилож ению -примеру, чтобы получить некоторое представление о происходящ ем , а потом перейти к более слож ны м опе­ рац и ям (таким , как сохранение данны х), чтобы прилож ение было больш е похож е на полноценное веб-прилож ение. В следую щ ем листинге приведено простое прилож ение Express, которое реализует эти м арш руты с использованием м ассива Ja v a S c rip t для хранени я статей. Листинг 3.1. Пример REST-совместимых маршрутов const express = re q u ire ('e x p re s s '); const app = express(); const a r tic le s = [{ t i t l e : 'Example' }]; 3.2. Построение REST-совместимой веб-службы 81 a p p .s e t( 'p o r t', process.env.PORT | | 3000); a p p .g e t ( '/ a r t ic l e s ', (req, re s, next) => { re s .s e n d (a rtic le s ); }); a p p .p o s t ( '/ a r t ic l e s ', (req, re s, next) => { res.send('O K '); }); -<■ (1) Получает все статьи. <- a p p .g e t ( '/ a r t i c l e s / : i d ', (req, re s, next) => { const id = req.param s.id; c o n so le .lo g ('F e tc h in g :', id ); re s .s e n d (a rtic le s [id ]); }); (2) Создает статью. <- a p p .d e l e te ( '/ a r t ic l e s /: i d ', (req, re s, next) => { (3) Получает одну статью. (4) Удаляет статью. a p p .lis te n ( a p p .g e t( 'p o r t') , () => { console.log('App sta rte d on p o r t', a p p .g e t( 'p o rt')); }); module.exports = app; С охраните этот код в ф айле index.js; если все сделано правильно, он долж ен зап у ­ скаться ком андой node in d e x .js . Ч тобы использовать этот пример, вы полните следую щ ие действия: mkdir listing3_1 cd listing3_1 npm i n i t -fy run npm in s t a l l --save express@4.12.4 С оздание новы х проектов N ode более подробно рассм атривается в главе 2. ЗАПУСК ПРИМЕРОВ И ВНЕСЕНИЕ ИЗМЕНЕНИЙ Чтобы запустить эти примеры, не забудьте перезапускать сервер после каждой м оди ф и кац и и кода. Д л я этого остановите процесс N ode клавиш ам и C trl+ C и запустите его снова ком андой node index.js. П римеры приводятся в виде фрагментов, поэтому вы сможете последователь­ но объединять их д ля получения работоспособного прилож ения. Е сли они почем у-либо не будут работать, попробуйте загрузить исходны й код книги по адресу h ttp s://g ith u b .c o m /a le x y o u n g /n o d e jsin a c tio n . 82 Глава 3. Что представляет собой веб-приложение Node? В листинге 3.1 присутствует встроенны й массив данных, который используется при вы даче списка всех статей в ф орм ате J S O N (1 ) методом Express re s .s e n d . Express автоматически преобразует массив в действительны й ответ JS O N , что очень удобно при создании просты х R E S T A PI. Э тот прим ер такж е м ож ет вы дать ответ с одной статьей по тому ж е принципу (3). Вы даж е можете удалить статью (4 ), использовав клю чевое слово Jav a S c rip t d e le te с числовы м идентиф икатором, заданны м в URL. Ч тобы получить значения из URL, вклю чите их в строку м арш рута ( / a r t i c l e s / : i d ) и прочитайте нуж ное значение re q .p a ra m s .id . Л и сти н г 3.1 не позволяет создавать статьи (2 ), потому что д ля этого нуж ен парсер тела запроса; эта тема рассм атривается в следую щ ем разделе. А пока посмотрим, как использовать этот прим ер с cU R L (http://curl.haxx.se). П осле зап у ск а пр и м ер а ком ан дой node i n d e x . j s вы м ож ете об ращ аться к нем у с запросам и из браузера и ли cU R L. Ч тобы получить одну статью, вы полните сл е­ дующ ую команду: curl h ttp ://lo c a lh o s t:3 0 0 0 /a rtic le s /0 Ч тобы получить все статьи, обратитесь с запросом к /articles: curl h ttp ://lo c a lh o s t:3 0 0 0 /a rtic le s С татьи даж е м ож но удалять: curl -X DELETE h ttp ://lo c a lh o s t:3 0 0 0 /a rtic le s /0 Н о почему мы сказали, что вам не удастся создать статью ? Главная причина закл ю ­ чается в том, что реал и зац и я запроса P O S T требует разбора тела запроса. Ранее в поставку Express входил встроенны й парсер тела запроса, но возмож ны х способов реализации было столько, что разработчики реш или ввести отдельную зависимость. П арсер тела запросов знает, как приним ать тела запросов P O S T в кодировке MIME (M u ltip u rp o se In te rn e t M ail E xtensions) и преобразовывать их в данные, которые вы можете использовать в своем коде. О бы чно вы получаете данные JS O N , с которыми удобно работать. К аж ды й раз, когда вы отправляете данны е ф орм ы на сайте, где-то в программном обеспечении на стороне сервера задействуется парсер тела запросов. Ч тобы добавить оф ици альн о поддерж иваем ы й парсер тела запросов, вы полните следую щ ую ком анду npm: npm in s ta l l --save body-parser Теперь загрузи те парсер тела запросов в своем п ри лож ен и и (п о б л и ж е к началу ф ай л а), как показано в листинге 3.2. Е сли вы повторяете при водим ы е примеры, сохраните его в одной папке с листингом 3.1 (listing3_1), но мы такж е сохранили его в отдельной папке в исходном коде кн иги (ch03-what-is-a-node-web-app/listing3_2). 3.3. Добавление базы данных 83 Листинг 3.2. Добавление парсера тела запросов const const const const express = re q u ire ('e x p re s s '); app = express(); a r tic le s = [{ t i t l e : 'Example' }]; bodyParser = re q u ire ('b o d y -p a rse r'); a p p .s e t( 'p o r t', process.env.PORT | | 3000); (1) Поддерживает тела запросов, закодированные в формате JSON. app.use(bodyParser.json()); < ---app.use(bodyP arser. urlencoded({ extended: tru e } )); a p p .p o s t ( '/ a r t ic l e s ', (req, re s, next) => { const a r tic le = { t i t l e : re q .b o d y .title }; a rtic le s .p u s h ( a r tic le ) ; re s .s e n d (a rtic le ); -<---- 1 (2 ) Поддерживает тела запросов I в кодировке формы. }); В этом листинге добавляю тся два полезны х аспекта: разбор тела запроса в формате JS O N ( 1 ) и тела запроса в кодировке ф орм ы (2 ). Также добавляется базовая р еа­ л и зац и я создания статей: если вы создадите запрос P O S T с полем t i t l e , в массив статей будет добавлена новая статья! Соответствующ ая команда cU R L вы глядит так: curl --data "title=Example 2" h ttp ://lo c a lh o s t:3 0 0 0 /a rtic le s С ейчас мы подош ли достаточно близко к построению реального веб-прилож ения. О стается добавить еще две возмож ности: м еханизм постоянного хранени я данны х в базе данны х и механизм генерирования удобочитаемой версии статей, найденных в И нтернете. 3.3. Добавление базы данных Заран ее определенного м еханизм а вклю чения баз данны х в при лож ения N ode не существует, но процесс обычно состоит из следую щ их шагов. 1. О пределить базу данны х, которую вы хотите использовать. 2. И зучить популярны е м одули npm, реализую щ ие драйвер или об ъектн о-реля­ ционное отображ ение (O R M , O b ject-R elational M apping). 3. Д обавить в проект модуль ком андой npm -sav e. 4. С оздать модули, инкапсулирую щ ие обращ ения к базе данны х в Ja v a S c rip t API. 5. Д обавить м одели к м арш рутам Express. П реж де чем д о бавл ять поддерж ку базы данны х, п род олж им работать с Express и разработаем код обработки м арш рутов с ш ага 5. О бработчики марш рутов H T T P в E x p re ss-части п р и л о ж е н и я вы даю т п р о сты е вы зо вы к м о д ел я м баз данны х. П ример: 84 Глава 3. Что представляет собой веб-приложение Node? a p p .g e t( '/ a r t i c l e s ', (req, re s, e rr) => { A r tic le .a ll( e r r , a r tic le s ) => { i f (e rr) return n e x t(e rr); re s .s e n d (a rtic le s ); }); }); В данном случае м арш рут H T T P предназначен д ля получения списка всех статей, поэтом у метод м одели мож ет иметь вид A r t i c l e . a l l . Его кон кретная ф орм а и зм е­ няется в зависи м ости от A P I баз данных; типичны е прим еры — A r t i c l e .f i n d ( { } , c b )1 и A r t i c l e . f e t c h A l l ( ) . t h e n ( c b ) 2. О братите внимание: cb в этих прим ерах — со­ кращ ение от «callback», то есть «обратны й вызов». С ущ ествует великое м нож ество баз данных; как ж е определить, какая вам нуж на? Н иж е мы изл о ж и л и причины , по которы м д ля этого прим ера бы ла вы брана база данны х SQ Lite. КАКАЯ БАЗА ДАННЫХ? Д л я наш его проекта будет использоваться база данны х SQ L ite ( w w w.sqlite. org) с популярны м м одулем sqlite3 ( h ttp ://n p m js.c o m /p a c k a g e /sq lite 3 ). Б аза данных SQ Lite удобна тем, что она является внутрипроцессной базой данных: вам не нуж но устанавливать сервер, вы полняем ы й на заднем плане в ваш ей системе. Л ю бы е добавляем ы е данны е запи сы ваю тся в ф айл, которы й сохра­ н яется при остановке и перезапуске при лож ения, поэтому S Q L ite хорош о подходит д ля начала работы с базам и данных. 3.3.1. Проектирование собственного API модели П рилож ен ие долж но поддерж ивать создание, вы борку и удаление статей. С ледо­ вательно, класс модели A r t ic l e долж ен содерж ать следую щ ие методы: О A r t i c l e . a l l ( c b ) — возвращ ает все статьи; О A r t i c l e . f i n d ( i d , cb) — находит заданную статью по идентиф икатору; О A r t i c l e . c r e a t e ( { t i t l e , c o n te n t }, cb) — создает статью с заголовком и к о н ­ тентом; О A r t i c l e . d e l e t e ( i d , cb) — удаляет статью по идентиф икатору. Все эти методы м ож но реализовать с помощ ью м одуля sqlite3. Э тот модуль п озво­ ляет вы бирать несколько строк результатов вы зовом d b .a l l и ли одну строку — вы ­ зовом d b .g e t. Н о сначала нуж но создать подклю чение к базе данных. 1 Mongoose: http://m ongoosejs.com . 2 Bookshelf.js http://bookshelfjs.org. 3.3. Добавление базы данных 85 Л и сти н г 3.3 дем онстрирует вы полнение этих операций с SQ L ite в Node. Этот код следует сохранить в ф айле db.js в одной папке с кодом из листинга 3.1. Листинг 3.3. Модель Article const sq lite 3 = r e q u ire ('s q lite 3 ').v e rb o s e (); const dbName = 'l a t e r . s q l i t e '; const db = new sqlite3.Database(dbName); < -------- (1) Подключается кфайлу базыданных. d b .s e ria liz e (() => { const sql = ' CREATE TABLE IF NOT EXISTS a r tic le s (id integer primary key, t i t l e , content TEXT) ; d b .ru n (sq l); < -------- (2) Создает таблицу articles, если она еще не существует. }); class A rticle { s ta tic a ll(c b ) { db.all('SELECT * FROM a r t i c l e s ', cb); } <-------- (3) Выбирает все записи. s ta tic fin d (id , cb) { db.get('SELECT * FROM a r tic le s WHERE id = ? ', id , cb); } (4) Выбирает конкретную статью. s ta tic cre a te (d a ta , cb) { const sql = 'INSERT INTO a r t i c l e s ( t i t l e , content) VALUES (?, ? ) '; db .ru n (sq l, d a t a . t i t l e , d ata.co n ten t, cb); < ---- (5) Вопросительные знаки задают параметры. } s ta tic d e le te (id , cb) { i f (!id ) return cb(new E rro r('P lease provide an i d ') ) ; db.run('DELETE FROM a r tic le s WHERE id = ? ', id , cb); } } module.exports = db; m odule.exports.A rticle = A rticle; В этом прим ере создается объект с именем A r tic le , которы й м ож ет создавать, ч и ­ тать и удалять данны е с использованием стандартного синтаксиса SQ L и м одуля sqlite3. Сначала файл базы данных открывается вызовом sq lite 3 .D a ta b a se (1), после чего создается таблиц а a r t i c l e s (2 ). С интаксис SQ L IF NOT EXISTS здесь особенно удобен, потом у что он позволяет снова вы полнить код без случайного удален ия и повторного создания таблицы a r t i c l e s . Когда база данны х и таблиц ы будут подготовлены , при лож ение готово к выдаче запросов. Д л я получения всех статей используется метод sqlite3 a l l (3 ). Д л я п о ­ лучен и я конкретной статьи используется синтаксис запросов с вопросительны м знаком (4); модуль sqlite3 подставляет в запрос идентификатор. Наконец, вы можете вставлять и удалять данны е методом run (5 ). 86 Глава 3. Что представляет собой веб-приложение Node? Ч тобы этот прим ер работал, необходимо установить м одуль sqlite3 командой npm i n s t a l l - -sav e s q l i te 3 . Н а м ом ент нап исания книги последней бы ла версия 3.1.8. П осле того как базовая ф ункциональность баз данны х будет подготовлена, ее н е­ обходимо добавить в м арш руты H T T P из листинга 3.2. С ледую щ ий ли сти н г дем онстрирует добавление всех методов, кром е PO ST. (О н рассм атри вается отдельно, потом у что д ля него пон адобится м одуль readability, которы й мы еще не устан овили.) Листинг 3.4. Добавление модуля Article к маршрутам HTTP const const const const express = re q u ire ('e x p re s s '); bodyParser = re q u ire ('b o d y -p a rse r'); app = express(); A rticle = r e q u ir e ( './d b ') .A r tic le ; -<-------- (1) Загружает модуль базыданных. a p p .s e t( 'p o r t', process.env.PORT | | 3000); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: tru e })); a p p .g e t( '/ a r t i c l e s ', (req, re s, next) => { A r tic le .a ll( ( e r r , a r tic le s ) => { -<-------i f (e rr) return n e x t(e rr); re s .s e n d (a rtic le s ); }); }); (2) Получает все статьи. a p p .g e t ( '/ a r t i c l e s / : i d ', (req, re s, next) => { const id = req.param s.id; A rtic le .fin d (id , (e rr, a r tic le ) => { < -------i f (e rr) return n e x t(e rr); re s .s e n d (a rtic le ); }); }); (3) Находит конкретную статью. a p p . d e l e t e ( '/ a r t i c l e s / : id ', (req, re s, next) => { const id = req.param s.id; A rtic le .d e le te (id , (e rr) => { •<-------- (4) Удаляет статью. i f (e rr) return n e x t(e rr); res.send({ message: 'D eleted' }); }); }); a p p .lis te n ( a p p .g e t( 'p o r t') , () => { console.log('App sta rte d on p o r t', a p p .g e t('p o r t')) ; }); module.exports = app; Л и сти н г 3.4 предполагает, что вы сохранили листинг 3.3 в ф айле db.js в том ж е к а ­ талоге. N ode загруж ает этот модуль (1 ) и затем использует его для вы борки каждой статьи (2 ), поиска конкретной статьи (3 ) и удален ия (4). 3.3. Добавление базы данных 87 П оследн ее, что остается сделать, — до бави ть п од держ ку со зд ан и я статей. Д л я этого необходим о им еть возм ож н ость загруж ать статьи и преобразовы вать при пом ощ и «волш ебного» алгоритм а удобочитаем ости. Д л я этого нам пон адобится м одуль npm. 3.3.2. Преобразование статей в удобочитаемую форму и их сохранение для чтения в будущем И так, вы построили R E S T -совм естимы й A PI, а данны е могут сохраняться в базе данны х. Теперь следует добавить код д ля преобразования веб-страниц в их уп р о ­ щ енные версии «для чтения». К счастью, вам не придется делать это самостоятельно; вместо этого м ож но воспользоваться модулем из npm. П оиск в npm по слову «read ab ility » дает достаточно м ного модулей. П опробуем воспользоваться модулем n o d e-read ab ility (сущ ествует в версии 1.0.1 на момент н ап и сан и я кн и ги ). У становите м одуль ком андой npm i n s t a l l n o d e - r e a d a b i l i t y --s a v e . М одуль предоставляет асинхронную ф ункц ию д ля загрузки U R L -адреса и преобразования разм етки H T M L в упрощ енное представление. С ледую щ ий ф рагм ент показы вает, как используется модуль node-readability; если вы захотите опробовать его, добавьте следую щ ий ф рагм ент в ф айл index.js в д о ­ полнение к листингу 3.5: const read = re q u ire ('n o d e -re a d a b ility '); const u rl = ' http://www.manning.com/cantelon2/'; re ad (u rl, (e r r , result)=> { / / re s u lt содержит . t i t l e и .content }); М одуль node-read ab ility м ож ет использоваться с классом базы данны х д ля сохра­ н ения статей методом A r t i c l e .c r e a t e : re ad (u rl, (e r r , re s u lt) => { A rtic le .c re a te ( { t i t l e : r e s u l t . t i t l e , content: re su lt.c o n te n t }, (e r r , a r tic le ) => { / / Статья сохраняется в базе данных } ); }); Чтобы использовать модуль в прилож ении, откройте ф айл index.js и добавьте новый обработчик м арш рута a p p .p o s t д ля загрузки и сохранения статей. О б ъеди н яя все это с тем, что вы узнали о H T T P P O S T в Express и парсере тела запроса, мы п о л у ­ чаем прим ер из листинга 3.5. 88 Глава 3. Что представляет собой веб-приложение Node? Листинг 3.5. Генерирование удобочитаемых статей и их сохранение const read = re q u ire ('n o d e -re a d a b ility '); / / . . . Оставшаяся часть файла in d ex .js из листинга 3.4 a p p .p o s t ( '/a r t i c l e s ', (req, re s, next) => { const u rl = req.body.url; < -------(1) Получает URLиз тела POST. re a d (u rl, (e rr, re s u lt) => { -<-------- (2) Использует режим удобочитаемости от выборки URL. i f (e rr || !re su lt) re s.sta tu s(5 0 0 ).se n d ('E rro r downloading a r t i c l e ') ; A rtic le .c re a te ( { t i t l e : r e s u l t . t i t l e , content: re su lt.c o n te n t }, (e rr, a r tic le ) => { i f (e rr) return n e x t(e rr); res.send('O K '); -<-------- (3) После сохранения статьи возвращает код 200. } ); }); }); З д есь мы сначала получаем U R L из тела P O S T (1 ), а затем используем модуль n o d e-read ab ility д ля п олуч ен и я U R L (2 ). С татья сохраняется с исп ользован ием класса м одели A r tic le . Если произойдет ош ибка, ее обработка передается пром е­ ж уточном у стеку Express (3 ); в противном случае представление статьи в ф орм ате JS O N отправляется клиенту. Ч то бы создать зап рос PO ST , р аботаю щ ий с этим прим ером , и сп ользуй те клю ч --d a ta : curl --d ata "url= http://m anning.com /cantelon2/" h ttp ://lo c a lh o s t:3 0 0 0 /a rtic le s В преды дущ ем разделе мы добавили модуль базы данных, создали инкапсулирую ­ щ ий его Ja v a S c rip t A PI и связали с R E S T -совместимы м H T T P A PI. Это значитель­ ны й объем работы, которы й составляет основную часть ваш их усилий в качестве разработчика на стороне сервера. Б азы данны х будут более подробно рассмотрены позднее в этой книге, когда мы займ ем ся M ongoD B и Redis. О т с о х р а н е н и я с т а т е й и их п р о г р а м м н о й в ы б о р к и м ы п ер ей д е м к д о б а в л е ­ нию веб-и нтерф ейса, чтобы п о л ьзо вател и такж е м огли прочитать сохраненны е статьи. 3.4. Добавление пользовательского интерфейса Д обавлени е ин терф ейса в проект E xpress состоит из нескольких шагов. П ервы й ш аг — использование ядр а шаблонов; вскоре мы покажем, как установить его и вы ­ полнить визуализац ию ш аблонов. 3.4. Добавление пользовательского интерфейса 89 Ваш е пр и ло ж ен и е такж е долж но п редоставлять статически е ф ай л ы (наприм ер, разм етку C SS). Н о преж де чем вы полнять визуализацию ш аблонов и писать стили CSS, вы долж ны узнать, как застави ть об работчи ки м арш рутов из преды дущ их прим еров отвечать данны м и в ф орм ате JS O N и H T M L при необходимости. 3.4.1. Поддержка разных форматов Д о настоящ его м ом ента мы использовали метод r e s .s e n d ( ) для отправки объектов Jav aS crip t клиенту. Д л я создания запросов использовался модуль cURL, и в данном случае ф орм ат JS O N удобен, потому что легко читается с консоли. Н о чтобы п р и ­ лож ение могло реально использоваться, оно долж но такж е поддерж ивать H TM L. К ак обеспечить поддерж ку обоих ф орм атов? П рощ е всего воспользоваться методом r e s .f o r m a t, предоставляем ы м Express. Он п о зво л я ет пр и ло ж ен и ю вы дать ответ в подходящ ем ф орм ате в зав и си м о сти от вопроса. Д л я этого прилож ению предоставляется список ф орм атов с ф ункциям и, которы е отвечаю т нуж ны м образом: res.form at({ html: () => { r e s .r e n d e r ( 'a r ti c le s .e js ', { a r tic le s : a r tic le s }); }, json: () => { re s .s e n d (a rtic le s ); } }); В этом ф рагм енте r e s .r e n d e r вы полняет визуализац ию ш аблона articles.ejs в п ап ­ ке views. Н о чтобы этот способ сработал, необходимо установить ядро ш аблонов и создать несколько ш аблонов. 3.4.2. Визуализация шаблонов Сущ ествует м ного ядер ш аблонов; мы вы берем простое ядро, которое достаточно легко и зучается, — EJS (E m b e d d e d J a v a S c rip t). У становите м одул ь EJS из npm (E JS сущ ествует в версии 2.3.1 на м ом ент написания книги): npm in s t a l l e js --save Теперь r e s .r e n d e r м ож ет генерировать ф айлы H T M L , отф орм атированны е с и с­ пользованием EJS. Если заменить r e s .s e n d ( a r t i c l e s ) в обработчике м арш рута app. g e t ( ' / a r t i c l e s ' ) из листинга 3.4, посещ ение адреса http://localhost:3000/articles в браузере приведет к попы тке визу ал и зац и и articles.ejs. Д алее необходимо создать ш аблон articles.ejs в папке views. В листинге 3.6 приведен полны й ш аблон, которы й вы м ож ете использовать. 90 Глава 3. Что представляет собой веб-приложение Node? Листинг 3.6. Шаблон для списка статей <% include head %> < -------- (1) Включает другой шаблон. <ul> <% a rtic le s .fo rE a c h ((a rtic le ) => { %> -<----- (2) Перебирает все статьи и проводит их визуализацию. <li> <a href="/articles/<% = a r t i c l e .id %>"> <%= a r t i c l e . t i t l e %> -<-------(3) Включает заголовок статьи в качестве текста ссылки. </a> </li> <% }) %> </ul> <% include foot %> Шаблон для списка статей использует шаблоны для заголовка (1 ) и завершителя, которые включаются в следующие примеры кода. Это делается для того, чтобы избежать дублирования заголовка и завершителя в каждом шаблоне. Для пере­ бора списка статей (2 ) применяется стандартный цикл JavaScript forEach, после чего идентификаторы статей и заголовки внедряются в шаблон синтаксической конструкцией EJS <%= value %> (3). Пример шаблона заголовка из файла views/head.ejs: <html> <head> < title > L a te r< /title > </head> <body> <div class="container"> А вот соответствующий завершитель (сохраняется в файле views/foot.ejs): </div> </body> </html> Метод res.form at также может использоваться для вывода конкретных статей. Здесь ситуация становится более интересной, потому что для того, чтобы при­ ложение приносило реальную пользу, статьи должны быть аккуратно оформлены и легко читаться. 3.4.3. Использование npm для зависимостей на стороне клиента Когда шаблоны будут на своих местах, пора сделать следующий шаг — добавить стилевое оформление. Вместо того чтобы создавать таблицу стилей, проще по­ вторно использовать существующие стили, причем это можно сделать даже с npm! Популярный клиентский фреймворк Bootstrap (http://getbootstrap.com/) доступен в npm ( www.npmjs.com/package/bootstrap); добавьте его в проект: 3.4. Добавление пользовательского интерфейса 91 npm in s t a l l bootstrap --save Загл я н у в в каталог node_modules/bootstrap/, вы найдете в нем исходны й код п р о ­ екта B o o tstrap . В папке dist/css х р ан я тся ф айлы CSS, п оставляем ы е с B ootstrap. Ч тобы использовать их в проекте, при лож ение долж но быть способно поставлять статические файлы . Статические файлы Д л я о тп р ав к и б р ау зер у кл и ен тск о го кода Ja v a S c rip t, гр а ф и к и и C SS в E xpress им еется в с т р о ен н а я п р о с л о й к а e x p r e s s . s t a t i c . Ч тоб ы и с п о л ьзо в ат ь ее, п е р е ­ д ай те катал о г со стати ч ески м и ф ай л ам и , и эти ф ай л ы стан ут д оступ н ы м и для браузера. В начале ф ай л а п р и лож ения Express (index.js) им ею тся строки, загруж аю щ ие п ро­ грам м ны е прослойки, необходимы е д ля проекта: app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: tru e })); Ч тобы загрузить CSS ф рейм ворка B ootstrap, используйте e x p r e s s . s t a t i c д ля р е ­ гистрации ф ай л а с нуж ны м U R L -адресом: app.use( '/c s s /b o o ts tra p .c s s ', e x p re ss.sta tic ('n o d e _ m o d u le s/b o o tstra p /d ist/c ss/b o o tstra p .css') ); П осле этого м ож но добавить ф айл /css/bootstrap.css в ш аблоны, чтобы использовать впечатляю щ ие стили B ootstrap. Ф айл views/head.ejs должен выглядеть примерно так: <html> <head> < title > la te r ;< /title > <link rel="stylesheet" href=”/css/bootstrap.css” > </head> <body> <div class="container"> Это только разм етка CSS; в поставку B o o tstrap такж е входят другие ф айлы , вкл ю ­ ч ая значки, ш риф ты и плагины jQ uery. Вы мож ете добавить в свой проект другие ф айлы B o o tstrap или ж е упаковать их в один ф айл д ля удобства загрузки. Npm и разработка на стороне клиента: новые возможности В преды дущ ем при м ере прод ем онстрировано простое исп ользован ие б и бл и оте­ ки, предназначенной д л я браузеров, с помощ ью npm . В еб-разработчики обычно загруж аю т ф айлы B o o tstrap и добавляю т их в проект вручную (к а к правило, веб­ разработчики, заним аю щ иеся просты м и статическим и сайтам и). 92 Глава 3. Что представляет собой веб-приложение Node? О днако соврем енны е разработчики клиентской (и н терф ей сн ой ) части и сп ользу­ ют npm как д ля получения библиотек, так и д ля загрузки их в кли ен тском коде Jav a S c rip t. Такие инструм енты , как B row serify (http://browserify.org/) и w ebpack (http://webpack.github.io/), п р едоставляю т в ваш е р асп оряж ен и е всю мощ ь npm и r e q u ir e д ля загрузки зависим остей. П редставьте, что вы мож ете ввести команду c o n s t React = r e q u i r e ( 'r e a c t ') не только в коде Node, но и в коде разработчика кли ен тской части! Э та тема вы ходит за рам ки настоящ ей главы, и все ж е она дает некоторое представление о том, чего можно добиться при объединении механизмов програм м ирования N ode с прием ам и програм м ирования кли ен тской части. 3.5. Заключение О В еб-прилож ение N ode м ож но бы стро построить с нуля при помощ и команды npm i n i t и Express. О Д л я установки зависим остей используется ком анда npm i n s t a l l . О E x p re ss п о з в о л я е т с о з д а в а т ь в е б -п р и л о ж е н и я с и с п о л ь з о в а н и е м R E S T совм естимы х API. О Выбор подходящ ей базы данны х и м одуля базы данны х требует предваритель­ ного анализа и зависи т от конкретны х требований. О S Q L ite хорош о подходит д ля м алы х проектов. О EJS предоставляет просты е средства для визуал и зац и и ш аблонов в Express. О Express поддерж ивает разны е яд р а ш аблонов, вклю чая P ug и M ustache. II Веб-разработка с использованием Node Итак, вы готовы к более глубокому изучению разработки на стороне сервера. Технология Node нашла важную нишу за пределами серверного кода: системы построения фронтэнда. В этой части вы узнаете, как за­ пускать проекты с использованием webpack и Gulp. Также мы представим самые популярные веб-фреймворки и сравним их с точки зрения разных разработчиков, чтобы помочь вам определиться с выбором идеального фреймворка для ваших проектов. Если вы захотите больше узнать о Connect и Express, глава 6 полностью посвящена построению веб-приложений с этими модулями. Также имеется глава, посвященная шаблонам и использованию баз данных с Node. Обзор полностековой веб-разработки на базе Node завершается главами, посвященными тестированию и развертыванию. Они помогут вам подго­ товить свое первое приложение Node к работе. 4 Системы построения фронтэнда В соврем енной веб-разработке N ode все чащ е исп ользуется д ля запуска и н стр у ­ м ентов и сервисов, от которы х зав и ся т ф рон тэн д -разработчи ки . В озмож но, вам как N o d e -програм м исту при дется отвечать за н астройку и сопровож дени е этих инструм ентов. А как полностековом у р азработчику вам стоит исп ользовать эти ин струм енты д ля создан ия более бы стры х и надеж ны х веб-п рилож ени й. В этой главе вы научитесь использовать сценарии npm, G ulp и w ebpack д ля построения проектов, удобны х в сопровож дении. П реим ущ ества от использования систем построения ф ронтэнда могут быть огром­ ными. Такие системы помогаю т писать более пон ятны й и устойчивы й к будущ им изм енениям код. Н ет необходимости беспокоиться о поддерж ке браузером ES2015, если при лож ение обрабаты вается тр анспилятором Babel. К роме того, благодаря возм ож н ости генерировани я карт исходного кода, сохраняется возм ож ность о т ­ ладки на базе браузера. В сл еду ю щ ем р а зд ел е п р и в о д и тс я к р а т к о е в вед ен и е во ф р о н т эн д -р а зр а б о т к у с и сп о льзо ван и ем N ode. П осле этого будут рассм отрен ы при м еры соврем енны х тех н о л о ги й — нап рим ер, R eact, которы е вы см ож ете и сп о л ьзо в ать в своих п р о ­ ектах. 4.1. Фронтэнд-разработка с использованием Node В последнее врем я как разработчики фронтэнда, так и разработчики кода на сто­ роне сервера стали прим енять npm для передачи Jav aS crip t. Это означает, что npm используется как для м одулей ф ронтэнда (таких, как R eact), так и для серверного кода (наприм ер, Express). О днако некоторы е м одули трудно четко отнести к той или иной стороне: lodash — прим ер библиотеки общего назначения, которая может использоваться N ode и браузерами. П ри тщ ательной упаковке lodash один модуль 4.2. Использование npm для запуска сценариев 95 может использоваться как Node, так и браузерами, а зависимостями в проекте можно будет управлять при помощ и npm. В озм ож но, вам уж е встр еч ал и сь други е м о ду л ьн ы е систем ы , п ред назначенны е д ля разработки на стороне клиента, — такие, как Bower (http://bower.io/). Н икто не запрещ ает вам использовать их, но как разработчику N ode вам стоит подумать об исп ользован ии npm. Впрочем, распространение пакетов — не единственная область прим енения Node. Ф рон тэн д-разработчики все чащ е полагаю тся на средства создания портируемого кода J a v a S c rip t с обратной совм естимостью . Т ранспи ляторы — такие, как Babel (https://babeljs.io/), — использую тся для преобразования современного кода ES2015 в более ш ироко поддерж иваем ы й код ES5. Среди других средств можно упом януть м и н и ф и каторы (наприм ер, U glifyJS; https://github.com/mishoo/UglifyJS) и и н стру­ менты статического анализа (наприм ер, ESLint; i) д ля проверки правильности кода перед его распространением . N ode такж е часто управляет систем ам и запуска тестов. Вы мож ете запускать тесты д ля U I -кода в процессе N ode или ж е использовать сценарий N ode д ля управления тестами, вы полняем ы м в браузере. Т акж е эти ср едства д о во льн о ч асто и сп о л ь зу ю тся вм есте. К огда вы начинаете ж онглировать траспилятором , м иниф икатором , програм мой статического анализа и систем ой запуска тестов, вам нуж но будет каким -то образом заф икси ровать, как работает процесс построения. В одних проектах использую тся сценарии npm; в дру­ гих — использую т G ulp или w ebpack. В этой главе мы рассм отрим все эти подходы, а такж е некоторы е практические приемы, связанны е с ними. 4.2. Использование npm для запуска сценариев N ode п оставляется с npm , а в npm сущ ествую т встроенны е средства д ля запуска сценариев. С ледовательно, вы мож ете рассчиты вать на то, что коллеги или п ользо­ ватели смогут вы полнять такие команды, как npm s t a r t и npm t e s t . Ч тобы добавить собственную ком анду для npm s t a r t , вклю чите ее в свойство s c r i p t s ф айла package. json своего проекта: { "s c rip ts " : { " s ta r t" : "node se rv e r.js" }, } Даже если не определять s t a r t , значение node s e r v e r .js используется по умолчанию; строго говоря, вы мож ете оставить его пусты м — только не забудьте создать ф айл 96 Глава 4. Системы построения фронтэнда с им енем server.js. Такж е полезно определить свойство t e s t , потому что вы можете вклю чить свой тестовы й ф рейм ворк как зависимость и запустить его командой npm t e s t . П редполож им , вы исп ользуете д ля тести рован и я ф рей м ворк M ocha (www. npmjs.com/package/mocha) и у стан о в и л и его ком ан дой npm i n s t a l l - -s a v e - d e v . Ч тобы вам не приходилось устанавливать M ocha глобально, вы мож ете добавить следую щ ую ком анду в ф айл package.json: { "s c rip ts " : { " te s t" : "./node_modules/.bin/mocha t e s t / * .js " }, } О братите внимание: в преды дущ ем прим ере M ocha передаю тся аргументы. Также при запуске сценариев npm м ож но передать аргум енты через два дефиса: npm t e s t -- t e s t / * . j s В табл. 4.1 приведена сводка некоторы х ком анд npm. Таблица 4.1. Команды npm Команда Свойство package.json Примеры использования start scripts.start Запуск сервера веб-приложения или приложе­ ния Electron stop scripts.stop restart install, postinstall Остановка веб-сервера Последовательное выполнение команд stop и start scripts.install, scripts.postinstall Выполнение собственных команд построения после установки пакета. Обратите внимание: postinstall может выполняться только в команде npm run postinstall П оддерживаю тся многие команды, вклю чая команды очистки пакетов перед публи­ кацией и ком анды м играции м еж ду версиям и пакетов. Впрочем, д ля больш инства задач веб-разработки вам хватит ком анд s t a r t и t e s t . М ногие задачи, которые вы будете определять, не уклады ваю тся в поддерживаемые им ена команд. Н апример, предполож им , что вы работаете над просты м проектом, написанны м на ES2015, и хотите транспили ровать его на ES5. Это м ож но сделать командой npm run. В следующем разделе рассматривается учебный пример, в котором будет создан новы й проект д ля построения ф айлов ES2015. 4.2. Использование npm для запуска сценариев 97 4.2.1. Создание специализированных сценариев npm К ом анда npm run (си н о н и м д ля npm r u n - s c r i p t ) и сп ользуется д л я определен ия п рои звольны х сценариев, которы е запускаю тся ком андой npm run имя-сценария. П осмотрим, как создать такой сценарий д ля построения сценария на стороне к л и ­ ента с использованием Babel. С оздайте новы й проект и установите необходимые зависимости: mkdir es2015-example cd es2015-example npm i n i t -y npm in s t a l l --save-dev b a b e l-c li babel-preset-es2015 echo '{ "p resets": ["es2015"] }' > .babelrc В результате вы п о л н ен и я этих ком анд долж ен бы ть создан новы й проект N ode с базовы м и инструм ентам и и плагинам и Babel д ля ES2015. Затем откройте ф айл package.json и добавьте в раздел s c r i p t s свойство babel. Оно долж но вы полнять сценарий, установленны й в папке node_modules/.bin проекта: "babel": "./node_m odules/.bin/babel brow ser.js -d build/" Н иж е приведен прим ер ф айла в синтаксисе ES2015, которы й вы мож ете исп оль­ зовать; сохраните его в ф айле browser.js: class Example { render() { return '<h1>Example</h1>'; } } const example = new Example(); console.log(exam ple.render()); Ч тобы протестировать этот пример, введите команду npm run babel. Если все было настроено прави льн о, д о лж н а п о я в и ть ся п ап ка п остроен и я с ф айлом browser.js. О ткройте browser.js и убедитесь в том, что это действительно ф айл ES5 (поищ ите конструкцию вида v a r _ c r e a te C la s s в начале ф айла. Если ваш проект при построении ничего более не делает, ему можно присвоить имя b u ild вместо b ab el в ф айле package.json. Н о м ож но пойти еще дальш е и добавить UglifyJS: npm i --save-dev u glify -es U glifyJS вы зы вается ком андой n o d e _ m o d u le s /.b in /u g lify js ; добавьте эту команду в package.json из раздела s c r i p t s с именем u g lify : ./node_m odules/.bin/uglifyjs build/brow ser.js -o build/brow ser.m in.js 98 Глава 4. Системы построения фронтэнда Теперь вы см ож ете вы полни ть ком анду npm run u g lif y . Всю ф ун кц и он альн ость можно связать воедино, объединив оба сценария. Добавьте еще одно свойство s c r ip t с именем b u ild д ля вы зова обеих задач: "build": "npm run babel && npm run uglify" О ба сц ен ар и я зап ускаю тся ком ан дой npm run b u ild . У частн ики ваш ей ком анды м огут объединить несколько средств у п ак овки кл и ен тской части вы зовом этой простой команды. Такое реш ение работает, потому что Babel и UglifyJS могут вы ­ по л н яться как сценарии ком андной строки, они получаю т аргументы командной строки и легко добавляю тся как однострочны е команды в ф айл package.json. Также Babel позволяет определить сложное поведение в ф айле .b a b e lrc , что было сделано ранее в этой главе. 4.2.2. Настройка средств построения фронтэнда В общем случае возм ож ны три варианта настройки средств построения ф ронтэнда при исп ользован ии со сценариям и npm: О передача аргументов командной строки (наприм ер, . /node_m odules/. b in /u g lif y --so u rce-m ap ); О создание конф игурационного ф ай л а для конкретного проекта. Ч асто п р и м ен я­ ется д ля Babel и E SL int; О вклю чение парам етров кон ф и гурац ии в ф айл package.json. Babel такж е поддер­ ж ивает эту возможность. Что, если ваш и требования к построению содержат дополнительны е этапы с такими операциям и, как копирование, кон катенаци я или перем ещ ение ф ай л ов? М ож но создать сценарий ком андного интерпретатора и запустить его из сценария npm, но ваш им коллегам, сведущ им в JavaS cript, будет удобнее, если вы будете использовать Jav aS crip t. М ногие системы построения предоставляю т Ja v a S crip t A PI для автома­ ти зац и и построения. В следую щ ем разделе описано одно из таких реш ений: Gulp. 4.3. Автоматизация с использованием Gulp G ulp (http://gulpjs.com/) — систем а построения, работаю щ ая на основе потоков (stream s). П ользователь м ож ет сводить потоки воедино д ля создан ия процессов построения, не ограничиваю щ и хся простой тр ан сп и л яц и ей и ли м и н и ф и кац и ей кода. Представьте, что у вас им еется проект с адм инистративной областью, постро­ енной на базе Angular, и с общ едоступной областью, построенной на базе React; оба подпроекта имею т некоторы е общ ие требования к построению . С G ulp вы сможете повторно использовать части процесса построения д ля каж дой стадии. Н а рис. 4.1 представлены прим еры двух процессов построения с общ ей ф ункциональностью . 4.3. Автоматизация с использованием Gulp Административная область на базе Angular 99 Общедоступная область на базе React "\ Browserify • admin/index.js => build/admin.js Конка­ тенация • build/admin.js • lib/shared.js => build/admin.js React * public/index.js => build/public.js Конка­ тенация • build/public.js • lib/shared.js => build/public.js • build/admin.js => assets/admin.min.js Minify Обработка графики • Retina • CSS sprite sheets • Branding shared between admin and public Minify Обработка графики • build/admin.js => assets/public.min.js • Retina • CSS sprite sheets • Branding shared between admin and public Рис. 4.1. Два процесса построения с общей функциональностью G ulp позволяет обеспечить вы сокую степень повторного исп ользован ия кода с по­ мощ ью двух приемов: пр и м ен ен и я плагинов и определен ия ваш их собственны х задач построения. Как видно из иллю страции, процесс построения представляет собой поток, поэтом у задачи и плагины м ож но объединять в цепочку. Н апример, R eact-часть приведенного прим ера можно обработать с использованием G ulp Babel (www.npmjs.com/package/gulp-babel/) и встроенны х средств gulp.src: g u lp .s rc ('p u b lic /in d e x .js x ') .pipe(babel({ p resets: ['e s2 0 1 5 ', 'r e a c t '] })) .pipe(m inify()) .p ip e (g u lp .d e s t( 'b u ild /p u b lic .js ') ); В эту цепочку м ож но даж е достаточно легко добавить стадию конкатенации. Но, преж де чем описы вать синтаксис более подробно, посмотрим, как создать неболь­ ш ой проект Gulp. 4.3.1. Добавление Gulp в проект Ч тобы добавить G ulp в проект, необходим о установить пакеты gulp-cli и gulp из npm. М ногие п ользователи устан авли ваю т gulp-cli глобально, поэтом у прим еры 100 Глава 4. Системы построения фронтэнда G ulp м ож но запустить просты м вводом ком анды gulp. Учтите, что, если пакет gulp был ранее установлен глобально, следует вы полнить команду npm rm - -g lo b a l gulp. В ы полните следую щ ий фрагмент, чтобы установить gulp-cli глобально и создать новы й проект N ode с зависим остью от Gulp: npm i --g lo b al g u lp -c li mkdir gulp-example cd gulp-example npm i n i t -y npm i -save-dev gulp Затем создайте ф айл gulpfile.js: touch g u lp f ile .js О ткройте ф айл. Д алее мы воспользуем ся G ulp д ля построения небольш ого п ро­ екта R eact, в котором исп ользую тся пакеты gulp-babel (www.npmjs.com/package/ gulp-babel), gulp-sourcem aps и gulp-concat: npm i --save-dev gulp-sourcemaps gulp-babel babel-preset-es2015 npm i --save-dev gulp-concat react react-dom babel-p reset-react Н е забудьте и сп о льзо вать npm с клю чом - -sa v e -d e v , когда вы хотите добавить плагины G ulp в проект. Если вы эксперим ентируете с новы ми плагинам и и позднее реш ите удалить их, используйте ком анду npm u n i n s t a l l - -sav e-d ev , чтобы удалить их из ./node_modules и обновить ф айл package.json проекта. 4.3.2. Создание и выполнение задач Gulp С оздание задач Gulp требует написания кода Node с Gulp A PI в файле с именем gulp­ file.js. G ulp A P I содерж ит методы для таких операций, как поиск ф айлов и передача их через плагины, которы е каким -то образом их преобразовываю т. П опробуйте сами: откройте ф айл gulpfile.js и создайте задачу построения, которая использует gulp.src д ля поиска ф айлов JS X , Babel д ля обработки ES2015 и React, а затем вы полняет конкатенацию д ля сл ияния ф айлов, как показано в листинге 4.1. Листинг 4.1. Gulp-файл для ES2015 и React с использованием Babel Плагины Gulp загружаются также, gulp = re q u ire ('g u lp '); как и стандартные модули Node. sourcemaps = require('gulp-sourcem aps'); .<■ babel = re q u ire ('g u lp -b a b e l'); concat = re q u ire ('g u lp -c o n c a t'); Встроенні средства gulp.src используются для Встроенные поиска всех файлов React с расширением JSX. g u lp .ta s k ( 'd e f a u lt', () => { Запускает анализ файлов для построения return g u lp .s rc ('a p p /* .js x ') отладочных карт исходного кода. .pipe(sourcem aps.init()) < .pipe(babel({ const const const const 4.3. Автоматизация с использованием Gulp 101 p resets: ['e s2 0 1 5 ', 'r e a c t'] < ---- Настраивает gulp-babel для использования ES2015 и React (jsx). })) .p ip e ( c o n c a t( 'a ll.J s ') ) < -------- Объединяет все файлы с исходным кодом вall.js. .p ip e (so u rc e m a p s.w rite('.')) < -------- Записывает файлы с картами отдельно. .p ip e ( g u lp .d e s t( 'd is t') ) ; < -------Перенаправляет все файлы в dist/folder. }); В листинге 4.1 исп ользую тся плагины G ulp д ля получения, обработки и записи ф айлов. С н ач ал а все входны е ф ай л ы нах о д ятся по ш аблону, после чего плагин gulp-sourcem aps используется д ля сбора м етрик карты исходного кода для отладки на стороне клиента. О братите вним ание на то, что д ля работы с картам и исходно­ го кода нуж ны две фазы: в одной вы указы ваете, что хотите использовать карты, а в другой записы ваете ф айлы карт. Также gulp-babel настраивается д ля обработки ф айлов с использованием ES2015 и React. Э та задача G ulp м ож ет быть вы полнена ком андой gulp в терминале. В этом прим ере все ф айлы преобразую тся с использованием одного плагина. Так уж выш ло, что B abel и транспили рует код R eact JS X , и преобразует ES2015 и ES5. Когда это будет сделано, вы полняется конкатенация ф айлов с использованием пла­ гина gulp-concat. П осле заверш ен ия тран сп и л яц и и происходит безопасная запись карт исходного кода, и итоговая сборка пом ещ ается в папку dist. Ч тобы опробовать этот g u lp -ф айл, создайте ф айл JS X с именем app/index.jsx. П р о ­ стой ф ай л JS X , кото р ы й м ож ет и сп о л ьзо в аться д л я т е сти р о ван и я G ulp, мож ет вы глядеть так: import React from 'r e a c t '; import ReactDOM from 'react-dom '; ReactDOM.render( <h1>Hello, world!</h1>, document.getElementByld('example') ); Gulp позволяет легко описать стадии построения на JavaScript, а при помощ и метода g u lp .ta s k ( ) вы смож ете добавить в этот ф айл собственны е задачи. Зад ачи обычно строятся по одной схеме: О ввод — получение исходны х ф айлов; О тр асп и л яц и я — прохож дение через плагин, которы й преобразует их; О кон катенаци я — объединение ф айлов д ля создания м онолитной сборки; О вы вод — назначение м естонахож дения или перем ещ ение вы ходны х ф айлов. В преды дущ ем прим ере sourcem aps я в л яется особым случаем, потому что требует двух конвейеров: д ля ко н ф и гурац ии и д ля вы вода ф айлов. Это логично, потому 102 Глава 4. Системы построения фронтэнда что карты исходного кода за в и с я т от со о тветстви я и сход ной н ум ерац и и строк и нум ерации строк в транспили рован ной сборке. 4.3.3. Отслеживание изменений П оследнее, что потребуется ф ронтэнд-разработчику, — цикл « п о строен и е/об н ов­ ление». Д л я упрощ ения процесса легче всего воспользоваться плагином G ulp для отслеж ивания изм енений в ф айловой системе. Впрочем, существуют и альтернатив­ ные реш ения. Н екоторы е библиотеки хорош о работаю т с «горячей» перезагрузкой, более общие проекты на базе D O M и CSS хорош о работаю т с проектом LiveReloard (http://livereload.com/). Н априм ер, к проекту из листинга 4.1 м ож но добавить gulp-w atch (www.npmjs.com/ package/gulp-watch). Д обавьте пакет в проект: npm i --save-dev gulp-watch Н е забудьте загрузить пакет в gulpfile.js: const watch = req u ire('g u lp -w atch '); А теперь добавьте задачу watch, которая вы зы вает задачу по ум олчанию из преды ­ дущ его примера: g u lp .ta sk ('w a tc h ', () => { w a tc h ('a p p /* * .jsx ', () => g u lp .s ta r t( 'd e f a u lt') ) ; }); Э тот ф рагмент определяет задачу с именем watch, а затем использует вызов w atch() д ля отслеж ивани я изм енений в ф айлах R eact JSX . П ри каж дом изм енении ф айла вы полняется задача построения по умолчанию . С небольш им и изм ен ен иям и этот рецепт может использоваться для построения ф айлов SASS (S yntactically Awesome Style Sheets), оптим изации граф ики и вообще практически всего, что только может понадобиться в проектах кли ен тской части. 4.3.4. Использование отдельных файлов в больших проектах П о мере роста проекта обычно приходится добавлять новые задачи G ulp. Рано или поздно образуется больш ой файл, в котором трудно разобраться. Впрочем, проблема реш ается: разбейте свой код на модули. К ак вы уже видели, G ulp использует систему модулей N ode д ля загрузки плагинов. С пециальной системы загрузки плагинов не существует; использую тся стандартные модули. Также система модулей Node может использоваться для разбивки длинны х g u lp -ф айлов с целью упрощ ения сопровож дения. Ч тобы использовать отдельны е ф айлы , вы полните следую щ ие действия. 4.3. Автоматизация с использованием Gulp 103 1. С оздайте папку с именем gulp, а в ней — влож енную папку tasks. 2. О пределите свои задачи с использованием обычного синтаксиса g u lp .ta s k ( ) в отдельны х файлах. Х орош ее практическое правило — создать отдельны й ф айл д ля каж дой задачи. 3. С оздайте ф айл с именем gulp/index.js для вклю чения всех ф айлов задач Gulp. 4. В клю чите ф айл gulp/index.js в gulpfile.js. И ерар х и я ф айлов долж на вы глядеть так: g u lp f ile .js gulp/ gulp/in d ex .js gulp/tasks/developm ent-build.js g u lp /task s/p ro d u ctio n -b u ild .js О п и сан н ы й прием пом огает у поряд очи ть структуру проектов со слож ны м и з а ­ дачам и построения, но он такж е м ож ет работать в сочетани и с м одулем gulp-help (www.npmjs.com/package/gulp-help). Э тот м одуль позвол яет д окум ентировать з а ­ дачи G ulp; ком ан да g u lp h e lp вы водит и н ф орм ац и ю о каж дой задаче. О н удобен при работе в группе и ли если вам п р и х о ди тся работать со м нож еством проектов, исп ользую щ их G ulp. Н а рис. 4.2 показано, как вы гл яд и т результат вы п ол н ен и я команды . -/Projects/world-domination: gulp help [10:33:383 Using gulpfile -/Projects/world-domination/gulpfile.js [10:33:38] Starting 'help'... Usage gulp [TASK] [OPTIONS...] Available tasks help Display this help text, version prints the version. Aliases: v, V [10:33:38] Finished 'help' after 1.2 ms Рис. 4.2. Пример вывода gulp-help G ulp — средство автом атизации проектов общего назначения. G ulp хорош о р аб о­ тает при добавлении в проекты кроссплатф орм енны х сценариев д ля вы полнения служебны х операций — например, проведения сложных тестов на стороне клиентов или создания тестов над общим набором объектов для баз данных. Х отя Gulp может использоваться д ля построения активов на стороне клиента, д ля этой цели такж е сущ ествую т специальны е инструм енты — как правило, они требую т меньш его объ­ ема кода и настройки, чем Gulp. О дним из таких инструментов явл яется w ebpack — пакет, п р ед назначенны й д л я у п ак о вки м одулей J a v a S c rip t и CSS. В следую щ ем разделе продем онстрировано использование w ebpack в проекте React. 104 Глава 4. Системы построения фронтэнда 4.4. Построение веб-приложений с использованием webpack П акет w ebpack специально предназначен д ля построения веб-прилож ений. П ред­ ставьте, что вы работаете с дизайнером , которы й уж е создал статический сайт для одностраничного веб-приложения, и теперь хотите адаптировать его для построения более эф ф ективного кода ES2015 Jav aS crip t и CSS. С Gulp вы пишете код JavaS cript д ля у п равлени я систем ой построения, поэтом у вам предстоит написать g u lp -ф айл и несколько других задач. С w ebpack вы пиш ете кон ф и гурац и он н ы й ф айл, а з а ­ тем подклю чаете новую ф ункциональность при помощ и плагинов и загрузчиков. В некоторы х сл у чаях н и к ак ая д о п о л н и тел ьн ая настрой ка вообщ е не требуется; достаточно ввести команду webpack в ком андной строке, передать в аргументе путь к исходном у коду — и она построит проект. Если вас интересует, как это выглядит, обратитесь к разделу 4.4.4. О дно из преимущ еств w ebpack заклю чается в том, что эта система позволяет быстро н астр о и ть си стем у п остроен и я, поддерж иваю щ ую и н кр ем ен тн ы й реж им . Если настроить ее для автоматического построения при изм енении ф айлов, ей не об яза­ тельно будет перестраивать весь проект при изм енении одного файла. В результате процесс построения упрощ ается и становится более понятны м. В этом разделе показано, как использовать w ebpack для небольш их проектов React. Н о сначала определим ся с терм инологией, п ри нятой в webpack. 4.4.1. Пакеты и плагины П реж де чем создавать проект w ebpack, необходимо разобраться с терминологией. П лагины w ebpack изм еняю т поведение процесса построения. Н апример, они м о ­ гут вклю чать такие операции, как автом атическая отправка активов в Am azon S3 (https://github.com/MikaAK/s3-plugin-webpack) или удаление дубликатов ф айлов из вывода. В отличие от плагинов, загрузчики (lo ad ers) представляю т преобразования, п р и ­ м еняем ы е к ф айлам ресурсов. Е сли вам потребуется преобразовать SASS в CSS (и л и ES2015 в ES5), значит, вам нуж ен загрузчик. Загрузч и ки представляю т собой функции, преобразующие входной текст в выходной; они могут работать асинхронно или синхронно. П лагины представляю т собой экзем пляры классов, которые могут подклю чаться к w ebpack A P I более низкого уровня. Е сли вам нуж но преобразовать код R eact, C offeeScript, SASS и ли любого другого тран сп и л и р у ем о го язы ка, вам нуж ен загрузчик. Е сли ж е вам нуж но обработать Jav aS crip t или вы полнить какие-то операции с группами файлов, вам нуж ен плагин. В следую щ ем разделе вы увидите, как использовать загрузчи к Babel д ля п реоб ­ разован и я проекта R eact ES2015 в пакет, адаптированны й д ля браузера. 4.4. Построение веб-приложений с использованием webpack 105 4.4.2. Настройка и запуск webpack В этом разделе мы воссоздадим прим ер R eact из листинга 4.1 с использованием w ebpack. Д л я начала установите R eact в новом проекте: mkdir webpack-example npm i n i t -y npm in s t a l l --save react react-dom npm in s t a l l --save-dev webpack babel-loader babel-core npm in s t a l l --save-dev babel-preset-es2015 b ab el-p reset-react П о сл е д н яя стр о ка у стан а в л и в а е т п л аги н B abel д л я E S2015 и п р ео б р азо вател ь R eact д ля Babel. Д алее необходимо создать ф айл с именем webpack.config.js, кото­ ры й сообщ ает webpack, где искать входной ф айл, куда записать результат и какие загру зчи ки следует использовать. М ы воспользуем ся загрузчи ком b a b e l- lo a d e r с дополнительны м и настройкам и д ля React, как показано в следую щ ем листинге. Листинг 4.2. Файл webpack.config.js const path = r e q u ire ('p a th '); const webpack = require('w ebpack'); module.exports = { entry: './a p p /in d e x .js x ', -<-------Входной файл output: { p a t h : _dirname, filename: 'd is t/b u n d le .js ' }, <.-------- Выходной файл. module: { loaders: [ { t e s t : /.js x ? $ /, < -------Находит все файлы JSX. loader: 'b a b e l-lo a d e r', exclude: /node_modules/, query: { p resets: ['e s2 0 1 5 ', 'r e a c t '] < -------Использует плагины React и Babel ES2015. } } ] }, }; К онф игураци онны й ф айл содерж ит все необходимое для успеш ного построения при лож ений R eact с ES2015. П роцесс настройки достаточно прост: определите зн а­ чение e n try с главны м файлом, загруж аю щ им прилож ение. Затем укаж ите каталог, в которы й долж ен запи сы ваться вывод; если каталог еще не существует, он будет создан. Затем определите загрузчи к и свяж ите его с поиском ф айлов по ш аблону при помощ и свойства t e s t . Н аконец, задайте все необходимые парам етры загр у з­ чика. В данном прим ере эти парам етры загруж аю т B abel-плагины ES2015 и React. Вклю чите код R eact JS X в ф айл app/index.jsx; используйте ф рагмент из раздела 4.3.2. Теперь ком анда ./n o d e _ m o d u le s/.b in /w e b p a c k отком пилирует ES5-версию ф айла с зависи м остям и React. 106 Глава 4. Системы построения фронтэнда 4.4.3. Использование сервера для разработки webpack Если вы хотите избежать необходимости перестраивать проект при каж дом изм ене­ нии ф айла React, используйте сервер для разработки w ebpack (http://webpack.github. io/docs/webpack-dev-server.html). В исходном коде книги он размещ ается в примере w ebpack-hotload-exam ple (ch04-front-end/webpack-hotload-example). Этот компактный сервер Express запускает w ebpack с кон ф и гурац ионн ы м ф айлом w ebpack при и з ­ м енении ф айлов, а затем предоставляет изм енивш иеся активы браузеру. Запустите его с указанием порта, отличного от порта основного веб-сервера; это означает, что теги s c r i p t долж ны вклю чать другие U R L -адреса на стадии разработки. Сервер строит активы и сохраняет их в пам яти (вм есто вы ходной папки w ebpack). Сервер д л я разр або тки такж е м ож ет и сп о льзо ваться д ля горячей загрузки м одулей (по аналогии с серверам и LiveReload). Ч тобы добавить сервер д ля разработки w ebpack, вы полните следую щ ие действия. 1. У станови те w e b p a c k - d e v - s e r v e r к о м ан д ой npm i - - s a v e - d e v w eb p ac k -d ev server@ 1.14.1. 2. Д обавьте парам етр p u b lic P a th в раздел o u tp u t в ф айле webpack.config.js. 3. Добавьте в каталог построения ф айл index.html, которы й будет управлять загруз­ кой пакетов Ja v a S c rip t и CSS. П роследите за тем, чтобы порт совпадал с портом, заданны м на следую щ ем шаге. 4. Зап у сти те сервер с парам етрам и, наприм ер w eb p ack -d ev -serv er -- h o t - - i n l i n e - - c o n te n t- b a s e d i s t / - - p o r t 3001. 5. П осетите http://localhost:3001/ и загрузите прилож ение. О ткройте ф айл webpack.config.js из листинга 4.2, изм ените свойство o u tp u t и д о ­ бавьте в него p u b lic P a th : output: { path: p ath .reso lv e(_dirname, 'd i s t ') , filename: 'b u n d le .js ', publicPath: '/ a s s e t s / ' }, С оздайте новы й ф айл dist/index.html, как показано в листинге 4.3. Листинг 4.3. Пример шаблона HTML для веб-приложения React <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Warning: Dev server o n ly < /title> </head> <body> <div id="example"></div> 4.4. Построение веб-приложений с использованием webpack <s c r ipt s rc ="/a sse ts/b u n d le . j s "></ s c r i pt > </body> </html> 107 < ---- 1 общедоступный путь пакета, построенного с webpack. О тк р о й те package.json и добавьте ко м ан ду зап у ск а сервера w ebpack в свойство s c r ip t s : "sc rip ts " : { "server:dev": "webpack-dev-server --hot -in lin e --content-base d is t/ --p o rt 3001" }, П арам етр - -h o t заставл яет сервер д ля разр аботки исп ользовать реж им горячей перезагрузки модулей. Е сли отредактировать R ea ct-ф ай л прим ера в app/index.jsx, браузер долж ен обновиться. М еханизм обновления задается параметром - - in lin e . В реж име i n l i n e сервер для разработки внедряет код для управления обновлением пакета. Также сущ ествует версия ifram e, которая заклю чает всю страницу в ifram e. Теперь запустите сервер д ля разработки: npm run server:dev Зап уск сервера для разработки w ebpack инициирует построение и запускает сервер с про сл у ш и ван и ем порта 3001. Ч тобы про тести ровать результат, введи те адрес http://localhost:3001 в браузере. ГОРЯЧАЯ ПЕРЕЗАГРУЗКА И з-за R eact и других ф рейм ворков, вклю чая AngularJS, существуют проекты горячей п ерезагрузки м одулей, предназначенны е д л я кон кретны х ф р ей м ­ ворков. Н екоторы е из них задействую т ф рейм ворки потоков данны х (такие, как R edux и R elay); это означает, что код м ож ет обновляться с сохранением текущ его состояния. Это идеальны й способ вы полнен ия перезагрузки кода, потом у что вам не придется заново проделы вать все необходимое для вос­ создания состоян ия пользовательского интерф ейса, над которы м вы рабо­ таете. О днако п ри веден н ы й прим ер в м еньш ей степени п ри вязан к R eact и хорош о подходит д ля начала работы с серверам и д ля разработки webpack. О бязательно поэксперим ентируйте и найдите тот вариант, которы й лучш е подходит д ля ваш его проекта. 4.4.4. Загрузка модулей и активов CommonJS В этой главе мы и сп о л ьзо в ал и R eact и Babel, но если вы и сп о льзу ете w ebpack с более традиц ионн ы м и проектам и C om m onJS, w ebpack мож ет предоставить всю необходимую ф ункц иональн ость без браузерной оболочки совм естимости (shim ) Com m onJS. О н даж е м ож ет загруж ать ф айлы CSS. 108 Глава 4. Системы построения фронтэнда WEBPACK и COMMONJS Ч тобы исп ользовать син такси с м одулей C om m onJS с w ebpack, вам не придется ничего настраивать. Д опустим, им еется ф айл, в котором используется re q u ire : const hello = r e q u i r e ( './h e l l o ') ; h e llo (); И другой ф айл, определяю щ ий ф ункцию h e llo : module.exports = function() { return 'h e llo '; }; В этом случае будет достаточно небольш ого конф игурационного ф ай л а w ebpack д ля определения точки входа (п ер вы й ф рагм ент) и вы ходного пути построения: const path = r e q u ire ('p a th '); const webpack = require('w ebpack'); module.exports = { entry: './a p p /in d e x .js ', output: { path: _dirname, filename: 'd is t/b u n d le .js ' }, }; Э тот прим ер показы вает, насколько сильно различаю тся G ulp и webpack. w ebpack полностью сп ец и али зи р у ется на построении пакетов и в кон тексте этой задачи может генерировать пакеты с оболочками совместимости Com m onJS. О ткры в файл dist/bundle.js, вы увидите в начале ф айла оболочку совместимости webpackBootstrap, а затем все ф айлы из исходного дерева кода, вклю ченны е в зам ы кан ия для м одели­ ровани я системы модулей. С ледую щ ий ф рагм ент я в л я ет ся частью пакета: function(module, exports, _webpack_require__) { const hello = _webpack_require__(1); h e llo (); /***/ }, /* 1 */ /***/ function(module, exports) { module.exports = function() { return 'h e llo '; }; К ом м ентарии в коде показы ваю т, где о пред еляю тся м одули, а ф айлы получаю т доступ к объектам module и e x p o rts как аргументам своих зам ы кан ий д ля м одел и ­ ровани я A P I м одуля Com m onJS. 4.5. Заключение 109 Использование пакетов NPM с webpack Вы мож ете сделать следую щ ий ш аг и вклю чить модули, загруж енны е из npm. Д о ­ пустим, вы хотите использовать jQ uery. Вместо того чтобы вклю чать соответствую ­ щ ий тег s c r i p t в страницу, вы можете установить jQ u e ry командой npm i --sav e -d ev jq u e ry , а потом загрузить как обы чны й модуль Node: const Jquery = re q u ire ('jq u e ry '); Это означает, что w ebpack предоставляет в ваш е распоряж ение м одули C om m onJS и доступ к м одулям из npm без какой-либо д ополнительной конфигурации! ПОИСК ЗАГРУЗЧИКОВ И ПЛАГИНОВ Н а сайте w ebpack доступны списки загрузчиков ( h ttp s://w e b p a c k .g ith u b .io / d o c s/list-o f-lo a d e rs.h tm l) и плагин ов ( h ttp s ://w e b p a c k .g ith u b .io /d o c s /lis tof-plugins.htm l). И нструм енты w ebpack такж е м ож но найти в npm; хорош ей отправной точкой станет поиск по клю чевом у слову w ebpack ( www.npm js. co m /b ro w se /k e y w o rd /w e b p ac k ). 4.5. Заключение О Если вам понадобится автоматизировать простые задачи или запускать сценарии, сценарии npm идеально подходят д ля этой цели. О G ulp м ож ет использоваться д ля нап исания более слож ны х задач на Jav a S crip t и я в л яе т с я кросс-платф орм енной технологией. О Е сли g u lp -ф ай л ы п олуч аю тся сли ш ком д ли нны м и , код м ож н о р азд ел и ть на несколько ф айлов. О w ebpack мож ет использоваться для генерирования пакетов на стороне клиента. О Е сли вам нуж но построить пакет на стороне клиента, реш ение с w ebpack может быть м енее трудоем ким , чем настройка соответствую щ его сценари я с Gulp. О w ebpack поддерж ивает горячую перезагрузку модулей; это означает, что и зм е­ нения в коде будут видны без об новления браузера. 5 Фреймворки на стороне сервера Э та глава полностью посвящ ена веб-разработке на стороне сервера. Вы найдете в ней ответы на разны е вопросы: как вы брать идеальны й ф рейм ворк для заданного проекта? К аки м и достоин ствам и и недостаткам и обладает каж ды й ф рейм ворк? Зад ача вы бора правильного ф рейм ворка сложна, потому что их слож но сравнивать в систем е едины х кри тери ев. У м ногих разработч и ков нет врем ени и зучать все ф реймворки, поэтому мы склонны приним ать субъективные реш ения относительно тех ф рейм ворков, с которы м и у нас им еется опыт работы. И ногда разны е ф р ей м ­ ворки использую тся совместно. Н апример, Express мож ет использоваться с более кр у п н ы м и п р и л о ж е н и я м и , а м и к р о сер ви сы , обесп ечи ваю щ и е раб о ту б ольш их прилож ений, м огут быть написаны на hapi. П редставьте, что вы стр о и те си стем у у п р а в л е н и я ко н тен то м (C M S ). С и стем а п р ед н азн ач ен а д л я у п р ав л ен и я ю ри ди чески м и д окум ентам и, соби раем ы м и и с ­ следо вательско й ф ирм ой. Т акая систем а м ож ет бы ть построена с прим енением разны х ф рейм ворков: О загрузка, отправка и чтение докум ентов — Express; О м икросервис генератора P D F — hapi; О ком понент электронной ком м ерции — Sails.js. И д еальн ы й ф рей м ворк д ля заданного проекта зависи т от потребностей проекта и группы, работаю щ ей над этим проектом. В этой главе д ля вы бора ф реймворка, подходящ его для конкретного типа проекта, использую тся персонажи, то есть вы ­ м ы ш ленны е люди. Вы взглянете на Koa, hapi, Sails.js, D erbyJS, F latiro n и LoopBack глазами этих воображаемых программистов. П ерсонажи определяю тся в следующем разделе. 5.1. Персонажи М ы не с о б и р а е м с я р а с х в а л и в а т ь н а все л ад ы од и н ф р е й м в о р к , к о т о р ы й м о ­ ж ет и сп о льзо ваться в каж дом проекте. Гораздо лучш е д ействовать многогранно 5.1. Персонажи 111 и исп ользовать набор инструм ентов, подходящ их д ля каж дой задачи. П рактика прим енения персонаж ей для проведения анализа в ходе проектирования получила ш ирокое распространение еще и потому, что она помогает проектировщ икам со ­ переж ивать своим пользователям . В этой главе персонаж и помогут вам взглянуть на фрейм ворки со стороны и понять, как разны е классы проектов подходят д ля разны х реш ений. П ерсонаж и опред еля­ ю тся в контексте проф ессиональной ситуации и средств разработки. С корее всего, вы найдете что-то общее хотя бы с одним из трех персонаж ей, придум анны х нами д ля этой главы. 5.1.1. Фил: штатный разработчик Ф и л три года проработал полностековы м веб-разработчиком . У него им еется н е­ которы й опы т работы на Ruby, P y th o n и клиентском JavaS cript: О Положение — ш татны й работник, полностековы й разработчик. О Тип работы — работа с кли ен тской частью , разработка на стороне сервера. О Компьютер — M acB ook Pro. О Инструменты — Sublim e Text, Dash, xScope, Pixelm ator, Sketch, G itH ub. О Подготовка — среднее общ ее образование; начинал карьеру как программ истлю битель. Типичны й рабочий день Ф и л а вклю чает работу с дизайнерам и и U X -экспертам и на встречах в agile-стиле, где обсуж даю тся или разрабаты ваю тся новые возможности, а такж е рассм атриваю тся вопросы сопровож дения или и сп равлени я ошибок. 5.1.2. Надин: разработчик открытого кода Н адин переш ла на контрактную систему после успеш ного начала карьеры на долж ­ ности корпоративного веб-разработчика: О Положение — внеш татны й работник, специалист по Jav aS crip t. О Тип работы — програм м ирование на стороне сервера, иногда программирование на Go и Erlang. Также пиш ет поп улярн ое веб-прилож ение с откры ты м кодом — базу данны х ф ильмов. О Компьютер — вы сокоп роизводительн ы й PC , Linux. О Инструменты — Vim, tm ux, M ercurial, средства командного интерпретатора. О Подготовка — диплом в области ком пью терны х технологий. В ходе своего рабочего дня Н адин обычно распределяет время между двумя своими крупны м и кли ен там и и работой над своим проектом с откры ты м кодом. В своей 112 Глава 5. Фреймворки на стороне сервера кон трактн ой работе она при м ен яет разработку через тестирование, тогда как ее проекты с откры ты м кодом ориентированы на разработку ф ункциональности. 5.1.3. Элис: разработчик продукта Э лис работает над успеш ны м прилож ением д ля iOS, но такж е участвует в р азр а­ ботке веб-A PI своей компании: О Положение — ш татны й работник, программист. О Тип работы — разр або тка д ля iO S; такж е отвечает за веб-п ри л ож ен и я и веб ­ службы. О Компьютер — M acB ook Pro, iPad Pro. О Инструменты — Xcode, Atom , Babel, Perforce. О Подготовка — у ч ен ая степень; один из первы х п я ти сотр у д н и ко в стартапа, в котором она работает. Э лис вы нуж денно работает с Xcode, O bjective-C и Swift, но втайне предпочитает Ja v a S c rip t и с больш им интересом относится к ES2015 и Babel. С удовольствием зани м ается разработкой новы х веб-служ б д ля поддерж ки при лож ений iO S и н а­ стольных прилож ений своей компании и хочет чаще работать над веб-приложениями на базе React. О пределивш ись с персонаж ами, перейдем к определению терм ина «ф реймворк». 5.2. Что такое фреймворк? Н екоторы е ф рейм ворки на стороне сервера, рассм атриваем ы е в этой главе, с тех­ нической точки зр ен и я ф рейм воркам и вообщ е не являю тся. К сожалению , термин «ф рейм ворк» перегруж ен изобилием см ы слов и несет разны е п он яти я д ля разны х программистов. В сообществе Node больш инство этих проектов было бы правильнее назы вать м одулям и, но при прям ом сравнении этого сем ейства библиотек ж ел а­ тельно иметь более точное определение. В проекте L oopB ack (http://loopback.io/resources/#compare) использую тся следу­ ю щ ие определения: О API-фреймворк — библи отека д ля построений веб -A PI с поддерж кой ф р ей м ­ ворка, упрощ аю щ его стр у кту р и р о в ан и е п р и ло ж ен и я. С ам проект L oopB ack определяется как ф рейм ворк этого типа. О Библиотека HTTP-сервера — к этой категории относятся все разработки на базе Express, вклю чая Koa и Kraken.js. Э ти библиотеки помогают строить приложения, основанны е на ком андах и м арш рутах H TTP. 5.3. Koa 113 О Фреймворк HTTP-сервера — ф рейм ворк д ля построения м одульны х серверов, использую щ их протокол H T T P д ля ком м уникаций. П рим ер ф рейм ворков т а ­ кого рода — hapi. О Веб-фреймворк MVC — к этой категории относятся ф рейм ворки на базе паттерна «М одель-П редставление-К онтроллер», вклю чая Sails.js. О Полностековый фреймворк — эти ф рейм ворки использую т Jav aS crip t на сервере и в браузере и могут совместно использовать клиентский и серверный код (такой код назы вается изоморфным). В частности, D erbyJS я в л яе т с я полностековы м ф рейм ворком MVC. М ногие разработчики N ode поним аю т под термином «ф рейм ворк» второй пункт: библиотеку H T T P -сервера. В следую щ ем разделе вы познаком итесь с K oa — б и ­ б лиотекой сервера, которая использует генераторы (н овы й синтаксис ES2015) как уни кальны й подход к построению пром еж уточного П О (ф у н кц и й промеж уточной обработки) H TTP. 5.3. Koa Koa (http://koajs.com/) базируется на Express, но использует синтаксис генераторов ES2015 д ля определения пром еж уточного П О (ф у н к ц и й пром еж уточной обработ­ ки). Это означает, что промежуточное П О можно писать практически в синхронном стиле. О тчасти это реш ает проблем у пром еж уточного ПО, сильно зависящ его от обратны х вызовов. С K oa вы мож ете воспользоваться клю чевы м словом y ie ld для выхода и последую щ его возврата к пром еж уточном у ПО. В табл. 5.1 приведена сводка основны х возм ож ностей Koa. Таблица 5.1. Основные возможности Koa Тип библиотеки Библиотека HTTP-сервера Функциональность Промежуточное ПО с использованием генераторов, мо­ дель «запрос/ответ» Рекомендуемое приме­ нение Облегченные веб-приложения, нежесткие HTTP API, одностраничные веб-приложения Архитектура плагинов Промежуточное ПО Документация http://koajs.com / Популярность 10 000 звезд на GitHub Лицензия MIT Л и сти н г 5.1 показы вает, как использовать K oa д ля хроном етраж а запросов. Д ля этого управление уступается следую щ ему ком поненту промеж уточного П О , после заверш ен ия которого управление продолж ается на стороне вызова. 114 Глава 5. Фреймворки на стороне сервера Листинг 5.1. Хронометраж запросов в Koa const koa = re q u ire ('k o a '); const app = koa(); app.use(function*(next) { (2) Уступает управление следующему const s ta r t = new Date; компоненту промежуточного ПО. yield next; <— const ms = new Date - s ta r t; console.log('% s %s - %s', this.m ethod, t h i s .u r l , ms); }); app.use(function*() { this.body = 'Hello World'; }); (1) Использует синтаксис генераторов для функций промежуточного ПО. (3) Управление передается в позицию исходного вызова yield. ap p.listen(3000); В листинге 5.1 генераторы ( 1 ) использую тся д ля переклю чения контекста между двум я ком понентам и пром еж уточного ПО. О братите вним ание на использование клю чевого слова fu n c tio n * — в данном случае стрелочная ф ункц ия использоваться не может. П ри исп ользован ии клю чевого слова y ie ld (2 ) вы полнение передается вни з по стеку пром еж уточного П О , а затем возвращ ается обратно при возврате из следую щ его ком п онента (3 ). Д о п о л н и тел ьн ое преи м ущ ество и сп ользован и я ф у н кц ии-генератора заклю чается в том, что вы м ож ете просто задать th is .b o d y , тогда как Express использует ф ункцию д ля отправки ответов: re s .s e n d ( re s p o n s e ). В терм инологии K oa t h i s назы вается контекстом. Контекст создается для каждого запроса и используется и д ля и н капсуляции N o d e-объектов запроса, и д ля ответа H T T P (https://nodejs.org/api/http.html). Когда вам нужно обратиться к каким -то дан­ ны м из запроса (наприм ер, парам етрам G E T или cookie), вы используете контекст. Это относится и к ответу: как было показано в листинге 5.1, вы мож ете управлять тем, какие данны е отправляю тся браузеру, задавая значения в th is .b o d y . Е сли преж де вы исп о льзо вал и пром еж уточное П О E xpress и син такси с генерато­ ров, изучить K oa будет неслож но. Е сли х отя бы что-то из этого окаж ется д л я вас новы м , вам будет достаточно трудно пон ять л огику K oa — и л и по край н ей м ере увидеть, чем хорош этот стиль. Н а рис. 5.1 более подробно показано, как y ie ld передает вы полнен ие м еж ду ко м п онентам и пром еж уточного ПО. К аж дая ф аза на рис. 5.1 соответствует ном ерам в листинге 5.1. С начала в первом ком поненте пром еж уточного П О устан авли вается тайм ер (1 ), а затем управление уступается второму компоненту промежуточного ПО, которы й строит тело (2 ). П о­ сле того как ответ будет отправлен, управление возвращ ается первому компоненту промеж уточного П О , где вы чи сляется врем я вы полнен ия (3 ). О но вы водится на терминал вызовом c o n s o le . log, и обработка запроса на этом заверш ается. О братите вним ание: стадия (4 ) в листинге 5.1 не видна; она обрабаты вается K oa и H T T P сервером Node. 5.3. Koa О В первом компоненте промежуточного ПО запускается таймер О Затраченное время выводится на консоль 115 О Управление уступается второму компоненту промежуточного ПО О С ответом управление возвращается первому компоненту Рис. 5.1. Порядок выполнения компонентов промежуточного ПО в Koa 5.3.1. Настройка Н астройка проекта с Koa требует установки модуля и определения промежуточного ПО. Если вам нуж на более ш ирокая ф ункциональность (например, A PI м арш рути­ зации, упрощ аю щ ий определение различны х типов запросов H T T P и реакцию на них), необходимо установить пром еж уточное П О м арш рутизации. Таким образом, при типичном рабочем процессе используемое ваш им проектом промежуточное ПО долж но плани роваться заранее, поэтом у вы долж ны заранее собрать инф орм ацию о популярны х модулях. РАЗМЫШЛЕНИЯ ПЕРСОНАЖЕЙ Элис: «М не как разработчику продукта нравится миним ализм ф ункц иональ­ ности Koa, потому что у нашего проекта существуют уникальны е требования и мы хотим сформировать весь стек в соответствии с наш ими потребностями». Ф и л: «М не как ш татном у програм м исту каж ется, что ф аза и зуч ен и я п р о ­ м еж уточного П О создает слиш ком м ного проблем. Я предпочел бы, чтобы это бы ло сделано за меня, потому что во м ногих м оих проектах действую т похож ие требовани я и я не хочу м ногократно устан авли вать одни и те же м одули для вы полнен ия основны х операций». 116 Глава 5. Фреймворки на стороне сервера В следующем разделе продемонстрирован сторонний модуль, реализую щ ий мощную библиотеку м арш рутизац ии д ля Koa. 5.3.2. Определение маршрутов О д ним из п о п у л я р н ы х ком п о н ен то в п р ом еж уточного П О д л я м ар ш р у ти зац и и я в л яе т с я k o a-ro u ter (https://www.npmjs.com/package/koa-router). Как и Express, он базируется на ком андах H TTP, но в отличие от Express поддерж ивает A P I с в о з­ можностью сцепления. Следую щ ий фрагмент показывает, как определяю тся группы марш рутов: router .p o s t( '/p a g e s ', function*(next) { / / Создание страницы }) . g e t( '/p a g e s /:id ', function*(next) { / / Визуализация страницы }) .p u t('p a g e s-u p d a te ', '/p a g e s /:id ', function*(next) { / / Обновление страницы }); И м ена марш рутов задаю тся дополнительны м аргументом. Это очень удобно, п о ­ том у что вы мож ете генерировать U R L -адреса, которы е поддерж иваю тся не всеми веб-ф рейм воркам и Node. Пример: ro u te r.u rl('p a g e s -u p d a te ', '9 9 ') ; Э тот м одуль предоставляет уникальное сочетание возм ож ностей Express и других веб-ф рейм ворков. РАЗМЫШЛЕНИЯ ПЕРСОНАЖЕЙ Ф ил: «Эта библиотека марш рутизации напоминает мне многое из того, что мне понравилось в R uby on Rails. П ожалуй, к Koa все ж е стоит присмотреться!» Н адин: «Я виж у возм ож ности д ля разб и ен и я м оих сущ ествую щ их проектов на м одули и последую щ его распространения кода в сообществе». 5.3.3. REST API Koa не включает в себя инструменты, необходимые для создания R E ST -совместимых A P I без р еализаци и некоторого пром еж уточного П О обработки марш рутов. П р и ­ веденны й прим ер м ож ет быть расш ирен д ля реализаци и R E S T -совместимого A PI в Koa. 5.4. Kraken 117 5.3.4. Сильные стороны Б ы ло бы легко сказать, что сильны е стороны Koa обусловлены ранним п р и н я ти ­ ем син такси са генераторов, но теперь, когда стандарт ES2015 получил ш ирокое распространение в сообщ естве N ode, эта возм ож ность уж е перестала быть такой уникальной. В настоящ ее врем я главное преимущ ество K oa — плавность и удобство работы при наличии превосходных сторонних модулей; дополнительную инф орм а­ цию м ож но найти в вики K oa (https://github.eom/koajs/koa/wiki#middleware). Р а з ­ работчики продуктов лю бят Koa за элегантны й синтаксис и возможность адаптации к проектам со специф ическим и требованиям и. 5.3.5. Слабые стороны Ш ирота возможностей настройки Koa отталкивает некоторых разработчиков. Созда­ ние множ ества м елких проектов с Koa может привести к низкой степени повторного исп ользован ия кода м еж ду проектам и, если только у вас нет готовы х стратегий. 5.4. Kraken K raken базируется на Express, но добавляет новую ф ункциональность через специ­ альные модули, разработанные PayPal. О дин из особенно полезных модулей — Lusca (https://github.com/krakenjs/lusca) — предоставляет прослойку безопасности п р и ­ лож ения. И хотя Lusca м ож ет использоваться и без K raken, одним из преимущ еств K raken я в л яется предопределенная структура прилож ения. П ри л о ж ен и я Express и K oa не требую т ни какой конкретной структуры проекта, и если вы пож елаете упростить работу над новы м проектом, K raken помож ет вам в этом. В табл. 5.2 приведена сводка основной ф ункц иональн ости Kraken. Таблица 5.2. Основные возможности Kraken Тип библиотеки Библиотека HTTP-сервера Функциональность Жесткая структура проекта, модели, шаблоны (Dust), укрепление безопасности (Lusca), на­ стройка конфигурации, интернационализация Рекомендуемое применение Корпоративные веб-приложения Архитектура плагинов Промежуточное ПО Express Документация https://www.kraken.com/help/api Популярность 4000 звезд на GitHub Лицензия Apache 2.0 118 Глава 5. Фреймворки на стороне сервера 5.4.1. Настройка Е сли у вас уж е им еется проект Express, K raken м ож но д обавить как ком понент промеж уточного ПО: const express = re q u ire ('e x p re s s '), const kraken = re q u ire ('k ra k e n -js '); const app = express(); app.use(kraken()); app.listen(3000); Н о если вы хотите создать новы й проект, попробуйте воспользоваться Yeoman — генератором K raken, которы й упрощ ает генерирование новы х проектов. П ри п о ­ мощ и генераторов Yeoman м ож но создавать подготовленны е проекты д ля разны х ф рейм ворков. Н иж е приведена последовательность действий д ля создания специ­ ал и зирован ного проекта K raken с исп ользован ием Yeoman, с предпочти тельной структурой ф айловой системы Kraken: $ npm in s t a l l -g yo generator-kraken bower g ru n t-c li $ yo kraken hh / '_ _ \ l(@)(@)l Release the Kraken! ) __ ( / , ') ) ( ( '. \ (( (( )) )) ' \ ') ( ' / ' Tell me a b it about your applicatio n : [?] Name: kraken-test [?] D escription: A Kraken application [?] Author: Alex R. Young Генератор создает новы й каталог, так что вам не придется делать это самостоятельно. П осле того как генератор заверш ит работу, вы смож ете запустить сервер, откры ть адрес http://localhost:8000 и убедиться в этом. 5.4.2. Определение маршрутов В K ra k e n м ар ш р у ты о п р е д е л я ю тс я н а р я д у с контроллером. В м есто того ч т о ­ бы р а зд е л я т ь о п р е д е л ен и я м ар ш р у то в и о б р аб о тч и к и м ар ш рутов, как это д е ­ л ается в E xpress, K rak en исп о льзу ет реш ение, вдохн овл ен н ое паттерн ом MVC; оно пол у ч ается д остаточно ком п актны м б лагод аря исп ользован и ю стрелочны х ф у н к ц и й ES6: 5.4. Kraken 119 module.exports = (ro u ter) => { r o u t e r . g e t ( '/ ', (req, res) => { re s .re n d e r('in d e x '); }); }; М арш руты могут вклю чать в себя парам етры в URL: module.exports = (ro u ter) => { r o u te r .g e t( '/p e o p le /:id ', (req, res) => { const people = { alex: { name: 'Alex' } }; re s .re n d e r('p e o p le /e d it', people[req.param .id]); }); }; A PI м арш рутизации K raken — express-enrouten (https://github.com/krakenjs/expressenrouten) — частично вы числяет м арш рут на основании каталога, в котором нахо­ дится файл. П редполож им , им еется ф ай л о вая структура следую щ его вида: co n tro lle rs |-u ser |- c r e a te .j s |- lis t.js K raken генерирует м арш руты вида / u s e r / c r e a t e и / u s e r / l i s t . 5.4.3. REST API K raken мож ет использоваться д ля ф орм ирования R E ST A PI, но не предоставляет конкретной поддерж ки для них. В озможности express-enrouten в сочетании с разбо­ ром JS O N означают, что вы можете использовать K raken для реализации R EST API. В средствах марш рутизации K raken имеется поддерж ка команд H T T P для D ELETE, GET, PO ST, P U T и т. д., благодаря чем у р еал и зац и я R E S T им еет м ного общего с Express. 5.4.4. Сильные стороны Так как в поставку K raken вклю чается генератор, на высоком уровне проекты Kraken похож и друг на друга. Е сли проекты Express м огут иметь сильно различаю щ ую ся структуру, в п р о ек тах K rak en обы чно и сп о л ьзу ется ед и н ая схем а разм ещ ен и я ф айлов и каталогов. Так как K raken предоставляет как библиотеку ш аблонизации (D u st), так и средства ин терн ац ионализац ии (M ak ara), эти две области идеально интегрированы . Ч тобы написать ш аблоны D u st с поддерж кой интернационализации, необходимо задать соответствую щ ий ключ: <h1>{@pre type="content" key="greeting"/}</h1> 120 Глава 5. Фреймворки на стороне сервера Затем добавьте ф айл . p r o p e r tie s в locales/language-code/view-name.properties. Ф айлы свойств представляю т собой просты е пары «клю ч-значение»; если преды дущ ий прим ер находится в ф айле представления с именем public/templates/profile.dust, то ф айлом .profile будет ф айл locales/US/en/profile.properties. РАЗМЫШЛЕНИЯ ПЕРСОНАЖЕЙ Ф и л: « М ен я раду ет то, что K rak en и сп о л ьзу ет оп ред ел ен н ую стр у кту р у ф ай л о во й систем ы и кон троллеры д л я м арш рутов. Н екоторы е участни ки м оей группы уж е знаю т D jango и R uby on Rails, поэтому переход д ля них будет несложным. Похоже, у K raken хорош ая документация; и в блоге много полезной инф орм ации». Элис: «М не нрави тся идея повы ш ения безопасности за счет использования Lusca, но K raken предоставляет м ного ф ункц иональн ости, которая мне на самом деле не нуж на. П оэтом у пока я попробую просто ограничиться и с ­ пользованием Lusca». 5.4.5. Слабые стороны K raken слож нее в изучении, чем K oa или Express. Н екоторы е задачи, реализуем ы е в E xpress на програм м ном уровне, в ы п о л н яю тся из кон ф и гурац и он н ы х ф айлов JS O N , и иногда бы вает трудно точно разобраться в том, какие свойства JS O N п о ­ надобятся д ля того, чтобы добиться ж елаем ого эф ф екта. 5.5. hapi hapi (http://hapijs.com/) — серверный ф реймворк для создания A PI веб-приложений. Он имеет собственны й A PI плагинов и не вклю чает никакой поддерж ки на стороне Таблица 5.3. Основные возможности hapi Тип библиотеки Фреймворк HTTP-сервера Функциональность Абстракция высокоуровневого серверного контейнера, заголовки безопасности Рекомендуемое при­ менение Одностраничные веб-приложения, HTTP API Архитектура плагинов Плагины hapi Документация http://hapijs.com/api Популярность 6000 звезд на GitHub Лицензия BSD 3 Clause 5.5. hapi 121 клиента или уровня модели базы данных; поддерживает A PI марш рутизации и имеет собственную обертку д ля сервера H T T P. В hapi вы проекти руете A PI, о р и ен ти ­ руясь на сервер как на основную абстракцию . Б л агод аря встроенной серверной ф ункц иональн ости подклю чений и ведения ж урн ала hapi хорош о п роявляет себя в области м асш табирования и у п равлени я с точки зрен и я D evO ps. В табл. 5.3 п р и ­ ведена сводка основной ф ункц иональн ости hapi. 5.5.1. Настройка С начала создайте новы й проект N ode и установите hapi: mkdir listing5_2 cd listing5_2 npm i n i t -y npm in s t a l l --save hapi Затем создайте новы й ф айл с именем server.js. Д обавьте код из листинга 5.2. Листинг 5.2. Базовый сервер hapi const Hapi = r e q u ire ('h a p i'); const server = new H api.Server(); server.connection({ host: 'lo c a lh o s t', port: 8000 }); s e r v e r .s ta r t( ( e r r ) => { i f (e rr) { throw e rr; } console.log('S erver running a t : ' , s e rv e r.in fo .u ri); }); П рим ер м ож но запустить в том виде, в котором он приведен в листинге, но без м арш рутов он особой пользы не принесет. В следую щ ем разделе вы узнаете, как в hapi организована обработка марш рутов. 5.5.2. Определение маршрутов В hapi имеется встроенны й A PI без создания марш рутов. Вы долж ны предоставить объект, вклю чаю щ и й свой ства д л я м етода запроса, U R L и ф ун кц и ю обратного вы зова, назы ваем ую обработчиком (h a n d le r). В л и сти н ге 5.3 п ри веден прим ер определения м арш рута с м етодом -обработчиком . Листинг 5.3. Сервер Hello World на hapi const Hapi = r e q u ire ('h a p i'); const server = new H api.Server(); 122 Глава 5. Фреймворки на стороне сервера server.connection({ host: 'lo c a lh o s t', port: 8000 }); server.route({ method: 'GET', p a th :'/h e llo ', handler: (request, reply) => { return re p ly ('h e llo w orld'); } }); s e r v e r .s ta r t( ( e r r ) => { i f (e rr) { throw e rr; } co nsole.log('S erver running a t : ' , s e rv e r.in fo .u ri); }); Добавьте этот код в предыдущ ий листинг для определения марш рута и обработчика, которы й отвечает текстом «H ello W orld». П рим ер м ож но запустить командой npm s t a r t . Введите адрес http://localhost:8000/hello, чтобы увидеть ответ. hapi не вклю чает предопределенной структуры папок или какой-либо ф ункциональ­ ности MVC; вся структура полностью базируется на серверах. В этом отнош ении hapi м ож но сравнить с Express. О днако обратите вним ание на клю чевое отличие: сигнатура обработчика марш рута r e q u e s t, re p ly отличается от сигнатуры Express re q , re s. О бъекты запроса и ответа hapi такж е отличаю тся от своих эквивалентов Express: вы долж ны вызвать rep ly вместо того, чтобы оперировать с объектом Express re s. У Express больш е общего со встроенны м H T T P -сервером Node. Ч тобы вы йти за пределы этого простого примера и получить доступ к расш иренной ф ункциональности (наприм ер, предоставлению статических ф айлов), вам понадо­ бятся плагины. 5.5.3. Плагины hapi использует собственную архитектуру плагинов, и больш инству проектов для предоставления такой ф ункциональности, как аутен ти ф и каци я и проверка ввода, требуется несколько плагинов. П ростой плагин, необходим ы й больш инству п ро­ ектов, — in e rt (https://github.com/hapijs/inert) — добавляет поддерж ку статических ф айлов и обработчики каталогов. Ч тобы вклю чить in e rt в проект hapi, необходимо сначала зарегистрировать плагин методом s e r v e r . r e g i s t e r . П ри этом добавляю тся метод r e p l y . f i l e д ля отправки одиночны х ф айлов и встроенны й обработчик каталогов. Рассм отрим обработчик каталогов. 5.5. hapi 123 Убедитесь в том, что вы создали проект из листинга 5.2. Затем установите inert: npm in s t a l l --save in e rt Теперь плагин м ож но загрузить и зарегистрировать. О ткройте ф айл server.js и д о ­ бавьте следую щ ие строки: Листинг 5.4. Добавление плагина в hapi const In e rt = r e q u ir e ( 'in e r t') ; s e r v e r.re g is te r(In e rt, () => {}); server.route({ method: 'GET', path: '/{param *}', handler: { d irecto ry : { path: ' . ' , redirectToSlash: tru e , index: tru e } } }); М арш руты hapi м огут получать не только ф ункции, но и объекты конф игурации д л я плагинов. В этом ли сти н ге объект d i r e c t o r y вклю чает настрой ки in e rt для предоставления ф айлов из текущ его пути и вы вода индекса ф айлов в этом ката­ логе. В этом отнош ении hapi отличается от промеж уточного П О Express; пример показы вает, как плагины могут расш ирять поведение сервера в п ри лож ениях hapi. 5.5.4. REST API hapi поддерж ивает команды H T T P и параметризацию U RL, что позволяет реализо­ вать R E S T A P I с использованием стандартного A PI марш рутов hapi. В следующем ф рагм енте представлен м арш рут для обобщ енного м етода удаления: server.route({ method: 'DELETE', path: '/ite m s /{ id } ', handler: (req, reply) => { / / Удалить "item" на основании req.param s.id re p ly (tru e ); } }); Кроме того, плагины упрощ аю т создание R E S T -совместимы х A PI, например, hapisequelize-crud (https://www.npmjs.com/package/hapi-sequelize-crud) автоматически генерирует R E S T -совм естимы й A P I на основании м оделей Sequelize (http://docs. sequelizejs.com/en/latest/). 124 Глава 5. Фреймворки на стороне сервера РАЗМЫШЛЕНИЯ ПЕРСОНАЖЕЙ Ф и л: «М не определенно хочется опробовать hapi-sequelize-crud, потому что у нас уж е есть прилож ения, использую щ ие P o stg reS Q L и M ySQ L, поэтом у Sequelize м ож ет бы ть подходящ им вариантом . Н о hapi не обладает такой ф ун кц и о н ал ьн о стью во встроенном виде; кто знает — вдруг плагин п ер е­ станет поддерж иваться? Я не уверен, насколько хорош о hapi подходит для ш татной работы». Элис: «К ак разработчик продукта я считаю, что hapi представляет интерес. К ак и Express, hapi м иним алистичен, однако A P I плагинов более ф орм ален и вы разителен». Надин: «Я виж у ряд возмож ностей для создания плагинов с открыты м кодом для hapi, а сущ ествую щ ие плагины написаны хорош о. Похоже, у hapi техн и ­ чески подкованная аудитория, и мне это нравится». 5.5.5. Сильные стороны A P I плагинов — одно из самы х больш их преимущ еств исп ользован ия hapi. П л а­ гины могут расш ирять сервер hapi, но при этом они такж е добавляю т другие виды поведения, от проверки данны х до ш аблонизации. К ром е того, поскольку hapi б а­ зируется на серверах H TTP, эта технология хорош о подходит д ля некоторы х типов сценариев развертывания. Если вы запускаете несколько серверов, которые должны быть связаны друг с другом или д ля которы х нуж но организовать распределение нагрузки, серверны й A P I hapi м ож ет оказаться предпочтительны м по сравнению с Express или Koa. 5.5.6. Слабые стороны Н едостатки hapi прим ерно те же, что у Express: м ин им ализм и вы текаю щ ее из него отсутствие правил структуры проекта. Вы никогда не мож ете быть уверены в том, когда может прекратиться разработка того или иного плагина, поэтому зависимость от слиш ком больш ого количества плагинов мож ет создать проблем ы с сопрово­ ж дением в будущем. 5.6. Sails.js Ф рейм ворки, которы е бы ли рассм отрены до настоящ его момента, представляли собой м ин им альны е серверны е библиотеки. Sails (http://sailsjs.org/) — ф рейм ворк M VC (M o d el-V iew -C o n tro ller), при нци пиально отли чаю щ и йся от серверной б и ­ блиотеки. Sails вклю чает в себя библиотеку объектно-реляционного отображ ения 5.6. Sails.js 125 (O R M , O b ject-R elatio n al M apping) для работы с базам и данны х и м ож ет автом а­ ти ч еск и ген ери ровать R E S T A P I. Т акж е под держ и ваю тся м ногие соврем енны е возм ож ности, вклю чая встроенную поддерж ку W ebSocket. А поклонников React или A ngular порадует независим ость от клиентской части: Sails не яв л яе тс я полно­ стековы м ф рей м во р ко м и поэтом у м ож ет и сп ользоваться п ракти чески с лю бой библи отекой и ли ф рейм ворком кл и ен тско й части. В табл. 5.4 приведена сводка основной ф ункц иональн ости hapi. Таблица 5.4. Основные возможности Sails Тип библиотеки Фреймворк MVC Функциональность Поддержка баз данных с ORM, генерирование REST API, WebSocket Рекомендуемое применение Приложения MVC в стиле Rails Архитектура плагинов Промежуточное ПО Express Документация http://sailsjs.org/documentation/concepts/ Популярность 6000 звезд на GitHub Лицензия BSD 3 Clause РАЗМЫШЛЕНИЯ ПЕРСОНАЖЕЙ Ф и л: «Д а ведь это им енно то, что мне нуж но, — в чем подвох?!» Э лис: « С н ач ал а я подум ала, что м не это не подойдет, потом у что мы уже п о тр ати л и вр ем я р азр аб о тк и на п р и ло ж ен и е R eact. Н о если это реш ение ориентировано на сервер, возможно, оно сработает д ля наш его продукта». 5.6.1. Настройка В поставку Sails вклю чен генератор проектов, поэтому лучш е вы полнить глобаль­ ную установку, чтобы упростить создание новы х проектов. Установите Sails из npm и введите ком анду s a i l s new, чтобы создать проект: npm i n s ta ll -g s a ils s a ils new example-project К ом анда создает новы й каталог с ф айлом package.json для базовы х зависимостей Sails. В новы й проект вклю чается собственно Sails, EJS и G ru n t. Вы м ож ете з а ­ пустить сервер ком андой npm s t a r t и ли ввести команду s a i l s l i f t . Когда сервер заработает, по адресу http://localhost:1337 будет доступ н а встроен н ая стран ица с вводной инф орм ацией. 126 Глава 5. Фреймворки на стороне сервера 5.6.2. Определение маршрутов Чтобы добавить марш руты (в Sails они назы ваю тся нестандартными маршрутами), откройте ф айл config/routes.js и добавьте свойство в экспортируем ы е марш руты . С войство состоит из ком анды H T T P и частичного U R L -адреса. Н апример, н еко­ торы е действительны е м арш руты Sails м огут вы глядеть так: m odule.exports.routes = { 'g e t /exam ple': { view: 'example' }, 'p o st /ite m s ': 'Item C o n tro ller.create }; П ервы й м арш рут ожидает получить ф айл с именем views/example.ejs. Второй м арш ­ рут ож идает получить ф айл с именем api/controllers/ItemController и методом c re a te . Ч тобы сгенерировать этот контроллер с методом c re a te , вы полните команду s a i l s g e n e ra te c o n t r o l l e r item c r e a te . А налогичны е команды могут использоваться для бы строго создания R E S T -совм естимы х A PI. 5.6.3. REST API Sails объединяет модели баз данны х и контроллеры в A PI; чтобы быстро создать заглуш ки д ля R E S T -совм естимы х A PI, введите ком анду s a i l s g e n e ra te a p i имяресурса. Ч тобы использовать базу данных, сначала необходимо установить адаптер базы данны х. Д л я д о бавл ен и я M y S Q L необходим о н ай ти и м я пакета W aterlin e M yS Q L (https://github.com/balderdashy/waterline), а затем добавить его в проект: npm in s ta l l --save w aterline sails-m ysql Затем откройте ф айл config/connections.js и введите подробную инф орм ацию о под­ клю чении д ля ваш его сервера M ySQ L. Ф ай л ы м оделей Sails позволяю т вам опре­ делить подклю чение к базе данных, так что вы мож ете использовать разны е модели с разны м и базам и данны х. Н априм ер, это позволяет реализовать такие ситуации, как хранение базы данны х пользовательских сеансов в R edis с хранением других, более д олгосрочны х ресурсов в р ел яц и о н н ой базе дан ны х (напри м ер, M ySQ L ). У W aterline — библиотеки баз данных для Sails — имеется собственный репозиторий с документацией ( https://github.com/balderdashy/waterline-docs). Помимо поддержки разны х баз данны х W aterlin e обладает и другим и полезны м и ф ункциям и: вы м о ­ ж ете определять им ена столбцов и таблиц д ля поддерж ки унаследованны х схем, а A PI запросов поддерж ивает обещания (prom ises), чтобы запросы бы ли в больш ей степени похож и на соврем енны й код Jav aS cript. 5.7. DerbyJS 127 РАЗМЫШЛЕНИЯ ПЕРСОНАЖЕЙ Ф и л: «П ростота создания A P I и поддерж ка м оделям и W aterline сущ еству­ ющ их схем баз данны х? Похоже, Sails идеально подходит д ля нас. У нас есть клиенты , которы е хотят постепенно перейти с M yS Q L на P ostgreS Q L ; в о з­ можно, W aterlin e позволит нам это сделать. Н екоторы е разработчики и про­ ектировщ ики уж е работали с R uby on Rails, и я думаю, что они моментально освоят соврем енны й синтаксис ES2015 в Node». Элис: « Ф р ей м во р к предоставляет возм ож ности, которы е в наш ем проекте не нужны. Я считаю, что K oa или hapi лучш е подойдет нам». 5.6.4. Сильные стороны Н ал и ч и е встроенны х средств создан ия проектов и генерировани я A P I означает, что операции настройки проектов и добавления типичных R EST A PI вы полняю тся быстро. Такж е Sails хорош о подходит для создания новы х проектов и организации совм естной работы, потому что проекты Sails имею т одинаковую структуру ф а й ­ л овой системы . С оздатели Sails, М айк М акн ил (M ik e M cN eil) и И рл Н атан (Irl N ath an ), нап исали книгу Sails.js in Action (M an n in g P ublications, 2017), в которой показано, что Sails помогает и начинаю щ им програм м истам Node. 5.6.5. Слабые стороны Sails также обладает рядом недостатков, общих для всех ф реймворков MVC на сторо­ не сервера: A P I м арш рутизации означает, что прилож ение долж но проектироваться с учетом средств м арш рутизации Sails, и вам может быть трудно адаптировать свою схему д ля того подхода, которы й используется в W aterline. 5.7. DerbyJS D erb y JS — п о л н о стеко вы й ф рейм ворк, которы й поддерж ивает син хронизац ию данны х и серверную визуализацию представлений. В этом он зависит от M ongoD B и Redis. Уровень син хронизац ии данны х предоставляется ShareJS и поддерж ивает автом атическое разреш ение кон ф ли ктов. В табл. 5.5 приведена сводка основной ф ункц иональн ости D erbyJS. 128 Глава 5. Фреймворки на стороне сервера Таблица 5.5. Основные возможности DerbyJS Тип библиотеки Полностековый фреймворк Функциональность Поддержка баз данных с ORM (Racer), изоморфизм Рекомендуемое приме­ нение Одностраничные веб-приложения с поддержкой на сто­ роне сервера Архитектура плагинов Плагины DerbyJS Документация http://derbyjs.com/docs/derby-0.6 Популярность 4000 звезд на GitHub Лицензия MIT 5.7.1. Настройка Е сли у вас установлен M ongoD B или Redis, вам придется установить оба пакета д ля запуска прим еров D erbyJS. Д окум ентация D erbyJS объясянет, как это делается в M ac OS, L inux и W indow s (http://derbyjs.com/started#environment). Ч тобы бы стро создать новы й проект D erbyJS, устан овите derb y и derb y -starter. П акет d e rb y -sta rte r используется д ля базовой ин иц и али зац и и при лож ения Derby: mkdir example-derby-app cd example-derby-app npm i n i t -f npm in s ta l l --save derby d e rb y -sta rte r derby-debug П р и л о ж ен и я D erb y разб и ваю тся на несколько м алы х при лож ений ; создайте н о ­ вы й каталог пр и ло ж ен и й с тр ем я ф айлам и: index.js, server, js и index.html. В л и ст и н ­ ге 5.5 при ведено простое п р и ло ж ен и е Derby, которое вы п ол н яет ви зуали зац и ю ш аблона. Листинг 5.5. Файл Derby app/index.js const app = module.exports = re q u ire ('d e rb y ') .c re a te A p p ('h e llo ', _filenam e); app.loadViews(_dirname); a p p .g e t ( '/ ', (page, model) => { const message = m o d el.at('h ello .m essag e'); m essage.subscribe(err => { i f (e rr) return n e x t(e rr); m essag e.createN u ll(''); page.render(); }); }); 5.7. DerbyJS 129 С ерверны й ф айл долж ен загрузить только модуль derby-starter, как показано в сле­ дую щ ем фрагменте. С охраните его в ф айле app/server.js: re q u ire ( 'd e r b y - s ta r te r ') .r u n (_dirname, { port: 8005 }); Ф ай л app/index.html строит поле in p u t и сообщ ение, введенное пользователем: <Body:> H oller: <input value="{{hello.message}}"> <h2>{{hello.message}}</h2> П р и л о ж ен и е д о лж н о за п у с к а ть с я из катал о га example-derby-app ком ан дой node d e r b y / s e r v e r .j s . П осле того как оно заработает, редактирование ф ай л а app/index. html приведет к запуску прилож ения; редактирование кода и ш аблонов сопрово­ ж дается автом атическим и обновлениям и в реальном времени. 5.7.2. Определение маршрутов D erbyJS использует d erb y -ro u ter для м арш рутизации. П оскольку в основе DerbyJS леж ит Express, A P I м арш рутизации для марш рутов на стороне сервера аналогичен, и в браузере используется тот ж е модуль м арш рутизации. П ри щ елчке на ссы лке в при лож ении D erbyJS делается попы тка построить ответ в клиенте. D erbyJS — полностековы й ф рейм ворк, поэтом у процедура д обавления м арш рутов отличается от других библиотек, рассм отренны х в этой главе. С ам ы й идиом атиче­ ский способ добавления базового м арш рута основан на добавлении представления. О ткройте ф айл apps/app/index.js и добавьте м арш рут следую щ им вызовом: a p p .g e t( 'h e llo ', '/ h e l l o ') ; О ткройте ф айл apps/app/views/hello.pug и добавьте простой ш аблон Pug: index: h2 Hello p Hello world Теперь откройте ф айл apps/app/views/index.pug и им портируйте шаблон: im p o rt:(src= "./h ello ") Если вы вы полнили ком анду npm s t a r t , проект долж ен постоянно обновляться, по­ этом у при откры тии адреса http://localhost:3000/hello теперь долж но отображ аться новое представление. С трока in d ex : содерж ит пространство имен д ля представления. В D erbyJS и м е­ на п р е д с та в л е н и й сн аб ж аю тся п р о с т р а н с т в а м и им ен, о тд ел ен н ы х д в о е т о ч и я ­ ми, поэтому вы ф актически создаете им я h e l l o : index. И нкапсуляция представлений в пространствах имен нуж на для предотвращ ения конф ликтов в больш их проектах. 130 Глава 5. Фреймворки на стороне сервера 5.7.3. REST API В проектах D erbyJS R E S T -совм естимы е A P I создаю тся добавлением марш рутов и обработчиков м арш рутов в Express. Ваш проект D erbyJS содерж ит ф айл server, js, которы й использует Express для создания сервера. О ткры в ф айл server/routes.js, вы найдете в нем пример марш рута, определенного с использованием стандартного A P I м арш рутизац ии Express. В серверном ф айле марш рутов вы мож ете использовать ap p .u se д ля м онтирования другого при лож ения Express, поэтом у R E S T A P I м ож но см оделировать в виде со­ верш енно отдельного п р и лож ения Express, которое м онтируется основным п р и ­ лож ением D erbyJS. 5.7.4. Сильные стороны В D erbyJS имеется A PI м одели базы данны х и A PI синхронизации данных. DerbyJS мож ет использоваться как д ля построения одностраничны х веб-прилож ений, так и д ля соврем енны х п р и лож ений реального врем ени. Б л агодаря встроенной п о д ­ держ ке W ebSocket и синхронизац ии вам не придется беспокоиться о том, какую библиотеку W ebSocket стоит использовать и ли как организовать синхронизацию данны х м еж ду клиентом и сервером. РАЗМЫШЛЕНИЯ ПЕРСОНАЖЕЙ Ф ил: «О дин из наш их клиентов интересуется построением проекта ви зуал и ­ заци и данны х на основании инф орм ации, получаем ой в реальном времени, и я думаю, что D erbyJS хорошо подойдет для этой цели. Но, похоже, освоение этой технологии потребует серьезных усилий. Не уверен, что смогу уговорить наш их разработчиков». Элис: «К ак разработчик продукта я не представляю , как приспособить п о ­ требности наш его продукта к архитектуре D erbyJS, поэтом у вряд ли этот вариант подойдет для моего проекта». 5.7.5. Слабые стороны Лю дей, уж е им ею щ их опы т работы с библиотекам и на стороне сервера и ли к л и ­ ента, довольно трудно убедить использовать D erbyJS. Н апример, разработчики на стороне клиента, лю бящ ие R eact, обы чно не хотят использовать D erbyJS. Р а зр а ­ ботчики на стороне сервера, которы м нрави тся создавать R E ST A PI и ли проекты M VC и которы е уверенно чувствую т себя с W ebSocket, такж е не понимают, зачем им изучать D erbyJS. 5.8. Flatiron.js 131 5.8. Flatiron.js F latiro n — веб-ф рейм ворк, вклю чаю щ ий в себя ф ункциональность м арш рутизации U R L, у п р ав л ен и я данны м и, пром еж уточного П О , плагинов и веден и я ж урнала. В отличие от б ольш ин ства веб-ф рейм ворков, м одули F latiro n проекти ровали сь в расчете на м аксим альную логическую изоляцию , поэтому вам не при дется и с­ пользовать их все. Вы даж е мож ете использовать их в собственны х проектах: если, скаж ем , вам п о н р ав и тся м о д у л ь в ед ен и я ж у р н ала, вы м ож ете п од кл ю чи ть его к проекту Express. В отличие от м ногих ф рейм ворков Node, уровни м арш рутизации U R L и пром еж уточного П О в F latiro n не написаны с использованием Express или C onnect, хотя пром еж уточное П О обладает обратной совместимостью с Connect. В табл. 5.6 приведена сводка основной ф ункц иональн ости Flatiron. Таблица 5.6. Основные возможности Flatiron Тип библиотеки Модульный фреймворк MVC Функциональность Уровень управления базой данных (Resourceful); изо­ лированные модули, пригодные для повторного исполь­ зования Рекомендуемое приме­ нение Облегченные приложения MVC, использование модулей Flatiron в других фреймворках Архитектура плагинов API плагинов Broadway Документация https://github.com/flatiron Популярность 1500 звезд на GitHub Лицензия MIT 5.8.1. Настройка Установка F latiro n требует глобальной установки средств ком андной строки для создания новы х проектов Flatiron: npm in s t a l l -g f la tir o n f la tir o n create exam ple-flatiron-app П осле вы полнен ия этих ком анд вы найдете новы й каталог с ф айлом package.json и необходимыми зависимостями. Выполните команду npm i n s t a l l , чтобы установить зависим ости, а затем запустите прилож ение ком андой npm s t a r t . О сновной ф айл app.js вы глядит как типичное прилож ение Express: const f la tir o n = r e q u ir e ( 'f la tir o n ') ; const path = r e q u ire ('p a th '); const app = fla tiro n .a p p ; 132 Глава 5. Фреймворки на стороне сервера ap p .c o n fig .file ({ f i l e : p a th .jo in (_dirname, 'c o n fig ', 'c o n fig .js o n ') }); a p p .u se (fla tiro n .p lu g in s .h ttp ); a p p .r o u te r .g e t( '/', () => { th is .r e s .js o n ( { 'h e l l o ': 'w orld' }) }); ap p.start(3000); О братите внимание: систем а м арш рутизац ии отличается как от Express, так и от Koa. О тветы возвращ аю тся через t h i s . r e s вместо аргумента ф ун кц и и обратного вы зова. Рассм отрим м арш руты F latiro n более подробно. 5.8.2. Определение маршрутов Б и б л и о тека м арш р у ти зац и и F la tiro n н азы вается D irector. И хотя она м ож ет и с ­ п о л ь зо в а т ь с я д л я сер в ер н ы х м ар ш р у то в , она т ак ж е п о д д ер ж и в ает м арш руты в браузерах, поэтом у она мож ет при м ен яться и д ля одностраничны х прилож ений. D irecto r вы зы вает м арш руты с ком андам и H T T P в стиле Express: ro u te r.g e t('/e x a m p le ', example); ro u te r.p o st('/e x a m p le ', examplePost); М арш руты могут иметь параметры, и параметры могут определяться регулярны ми вы раж ениям и: ro u te r.p a ra m ('id ', / ( [ \ \ w \\ - ] + ) / ) ; r o u te r .o n ( '/p a g e s /:id ', pageld => {}); Ч тобы сгенерировать ответ, используйте re s.w rite H e a d д ля отправки заголовков и re s .e n d д ля отправки тела ответа: r o u t e r . g e t ( '/ ', () => { this.res.w riteH ead(200, { 'c o n te n t-ty p e ': 'te x t/p la in ' }); th is .re s .e n d ('H e llo , W orld'); }); A PI м арш рутизации также может использоваться в виде класса — с объектом табли­ цы м арш рутизации. Ч тобы использовать его, создайте экзем пляр м арш рутизатора и используйте метод d is p a tc h при поступлении запросов H T T P: const h ttp = r e q u ir e ( 'h ttp ') ; const d ire c to r = r e q u ir e ( 'd ir e c to r ') ; const router = new d irecto r.h ttp .R o u ter({ '/exam ple': { ge t: () => { this.res.w riteH ead(200, { 'Content-Type': 'te x t/p la in ' }) th is .r e s .e n d ( 'h e llo w orld'); } 5.8. Flatiron.js 133 } }); const server = h ttp .c re a te S e rv e r((re q , res) => ro u te r.d isp a tc h (req , res); }); И сп о л ьзо ван и е A P I м ар ш р у ти зац и и в виде класса такж е означает возм ож ность подклю чения к потоковом у A PI. Это позволяет организовать быструю и легкую обработку больш их запросов, что хорош о подходит д ля таки х задач, как разбор отправленны х данны х с ранним выходом: const d ire c to r = r e q u ir e ( 'd ir e c to r ') ; const router = new d ire c to r.h ttp .R o u te r(); r o u t e r . g e t ( '/ ', { stream: tru e }, () => { th is .r e q .o n ( 'd a ta ', (chunk) => { console.log(chunk); }); }); A P I м арш рутизац ии D irecto r мож ет быть полезен д ля создания R E ST API. 5.8.3. REST API R EST A PI могут создаваться стандартными методами в стиле H T T P -команд Express или же с использованием средств м арш рутизации D irector. Это позволяет группи­ ровать м арш руты на основании ф рагм ентов и параметров URL: ro u te r.p a th (/\/u s e rs \/(\w + )/, () => { th is .g e t( ( id ) => {}); th is .d e le te ( ( id ) => {}); th is .p u t( ( id ) => {}); }); F latiro n также предоставляет высокоуровневую R E S T -обертку Resourceful (https:// github.com/flatiron/resourceful), которая поддерж ивает C ouchD B , M ongoD B, Socket. IO и проверку данных. 5.8.4. Сильные стороны Ф р ей м во р к ам достаточно трудно завоевать популярность; им енно по этой п р и ­ ч ине логическая и зо л яц и я ком понентов F la tiro n я в л яет с я одной из его сильны х сторон. Н еко то р ы е из м оду л ей F la tiro n м огут п р и м ен я ться без и сп о л ьзо ван и я всего ф рейм ворка. Н априм ер, м одуль веден ия ж урн ала W in sto n (https://github. com/winstonjs/winston) присутствует во м ногих проектах, которы е не использую т остальны е ком поненты F latiron. Это означает, что некоторы е компоненты F latiron получают достаточно серьезный творческий вклад от сообщества с открытым кодом. 134 Глава 5. Фреймворки на стороне сервера A PI U R L -м арш рутизации D irector изоморфен; это означает, что он может использо­ ваться в реш ениях для разработки как на стороне клиента, так и на стороне сервера. A P I D ire c to r такж е о тли чается от A P I м ар ш рути зац и и в стиле Express: D irecto r использует упрощ енны й потоковы й A PI, а объект м арш рутизац ии генерирует со­ бы ти я до и после вы полнен ия марш рутов. В отличие от больш ин ства веб-ф рейм ворков N ode, F latiro n содерж ит менедж ер плагинов. П лагины , поддерж иваем ы е сообщ еством, упрощ аю т расш и рение п р о ­ ектов Flatiron. РАЗМЫШЛЕНИЯ ПЕРСОНАЖЕЙ Н адин: «М не нрави тся м одульн ая структура F latiron, а м енедж ер плагинов просто зам ечателен. Я уж е представляю себе некоторы е плагины, которы е я хотела бы создать». Элис: «Н е могу сказать, что все м одули F latiro n мне нужны, но мне хотелось бы опробовать F la tiro n с другой систем ой O R M и библ и отекой ш аблонизации». 5.8.5. Слабые стороны Ф р ей м во р к F latiro n не настолько прост в исп ользован ии с больш им и проектам и в стиле MVC, как другие ф рейм ворки. Н апример, Sails прощ е настраивается. Если вы создаете несколько традиционны х веб-прилож ений среднего размера, F latiron мож ет сработать хорошо. В озм ож ность настройки F latiro n м ож ет стать д о п ол н и ­ тельны м преимущ еством , но проследите за тем, чтобы ф рейм ворк сначала прош ел оценку наряду с другим и вариантами. К ч ислу сильны х конкурентов при надлеж ит LoopB ack — последний ф рейм ворк, описанны й в этой главе. 5.9. LoopBack Ф р ей м во р к LoopB ack был создан S trongL oop — компанией, предоставляю щ ей н е­ сколько ком м ерческих сервисов, поддерж иваю щ их разработку веб-п рилож ени й Node. Ф о рм ально это ф рейм ворк A PI, но благодаря некоторы м своим возм ож н о­ стям он хорош о подходит д ля баз данны х и при лож ений MVC. О н даж е вклю чает в себя веб-интерфейс для анализа и управления R E ST API. Если вы ищете решение, которое помож ет создавать веб-A PI д ля м обильны х и настольны х клиентов, ф у н к ­ циональность LoopBack подойдет идеально. В табл. 5.7 приведена сводка основной ф ункц иональн ости LoopBack. 5.9. LoopBack 135 Таблица 5.7. Основные возможности LoopBack Тип библиотеки Фреймворк API Функциональность ORM, API пользовательского интерфейса, WebSocket, клиентский SDK (включая iOS) Рекомендуемое приме­ нение API для поддержки разных клиентов (мобильные, на­ стольные, веб) Архитектура плагинов Промежуточное ПО Express Документация http://loopback.io/doc/ Популярность 6500 звезд на GitHub Лицензия Двойная лицензия: MIT и соглашение о подписке StrongLoop L o o p B a c k р а с п р о с т р а н я е т с я с о т к р ы т ы м ко д ом , и с м о м е н т а п р и о б р е т е н и я StrongLoop компанией IBM фреймворк получил серьезную коммерческую поддерж ­ ку. В результате он стал уникальны м предложением в сообществе Node. В частности, в него вклю чены генераторы Yeoman для быстрой настройки оснастки прилож ений. В следую щ ем разделе показано, как создать новое прилож ение LoopBack. 5.9.1. Настройка Д л я создания нового проекта L oopB ack необходимо воспользоваться програм мой ком андной строки StrongL oop (www.npmjs.com/package/strongloop). П осле глобаль­ ной установки пакета strongloop ин струм ентарий ком андной строки запускается ком ан дой s l c . П акет вклю чает в себя средства у п р ав л ен и я процессам и, но нас интересует преж де всего генератор проектов LoopBack: npm in s t a l l -g strongloop slc loopback П рограм м а ком андной строки S trongL oop пом огает вам пройти все дей стви я по созданию нового проекта. Введите им я проекта, а затем вы берите заготовку п р и ­ лож ения a p i-s e rv e r. Когда генератор закончит устанавливать зависимости проекта, он вы ведет полезны е реком ендации по работе с новым проектом (рис. 5.2). Чтобы запустить проект, введите команду node ., а чтобы создать модель — команду slc :lo o p b a c k :m o d e l. Вы будете регулярно использовать ком анду s lc при создании нового проекта LoopBack. К о гд а п р о е к т за р а б о т а е т , вы см о ж е т е о б р а т и т ь с я к A P I E x p lo re r по ад р есу http://0.0.0.0:3000/explorer/. Щ елкните на ссы лке User, чтобы раскры ть узел. Вы увидите больш ой список доступн ы х м етодов A PI, вкл ю чая стан дартны е R E ST совм естимы е марш руты — такие, как PUT /U se rs и DELETE /U s e rs /{ id } . A P I Explorer показан на рис. 5.3. Глава 5. Фреймворки на стороне сервера 136 • • 3. alex@AI«xs-ѴлсВоок-Рго; -/Docum*nts/Codt (a h ) sic loopback I I I— <o)— I -------- ' .-----------------------------. I I Let's create a LoopBack | application! | ( _U _ ) |e ' Y ? What's the name of your application? loopback-example ? Enter name of the directory to contain the project: loopback-example create loopback-example/ info change the working directory to loopback-example 7 What kind of application do you have in mind? api-server (A LoopBack API server with local Use r auth) Generating .yo-rc.json I'm all done. Running ry running the command yourself. create create create create create create for you to install the required dependencies. If this fails, t .editorconfig .jshintignore .jshintrc server/boot/root.js server/middleware.json server/middleware.production.json Рис. 5.2. Генератор проектов LoopBack Рис. 5.3. StrongLoop API Explorer с маршрутами User 5.9. LoopBack 137 5.9.2. Определение маршрутов В LoopB ack м арш руты м ож но добавлять на уровне Express. Д обавьте новы й ф айл с именем server/boot/routes.js, после чего добавьте марш рут через экзем пляр Loopback R outer: module.exports = (app) => { const router = app.loopback.Router(); r o u te r .g e t( '/h e llo ', (req, res) => { re s.se n d ('H e llo , w orld'); }); app.use(router); }; Теперь при посещ ении http://localhost:3000/hello выдается ответ H ello , world. Впро­ чем, добавление м арш рутов таким способом нетипично д л я проектов LoopBack. В озм ож но, оно необходим о д л я некоторы х необы чны х кон ечн ы х точек A PI, но, как правило, м арш руты добавляю тся автом атически при генерировании моделей. 5.9.3. REST API С ам ы й простой способ со зд ан и я R E S T A P I в проекте L oopB ack основан на и с­ п ол ьзо ван и и генератора м оделей, которы й я в л я е т с я частью ф ун кц и он ал ьн ости команды slc . Н апример, чтобы добавить новую модель с именем product, выполните ком анду s l c loopback:m odel: slc loopback:model product К ом анда s l c вы полняет всю последовательность действий по созданию модели, п о зв о л я я вам вы брать, я в л я е т с я ли м одель чисто серверной, а такж е задать н е­ которы е свойства и валидаторы . П осле того как вы добавите модель, взгляните на соответствую щ ий ф айл JS O N — это долж ен быть ф айл common/models/product.json. Этот ф айл JS O N я вл яется облегченным способом определения поведения моделей, вклю чаю щ им в себя все свойства, заданны е на преды дущ ем шаге. Е сли вы хотите добавить новы е свойства, введите команду s l c lo o p b a c k :p ro p e rty . Н овы е свойства м огут добавляться в м одели в лю бой момент. РАЗМЫШЛЕНИЯ ПЕРСОНАЖЕЙ Ф и л: «Н аш им группам нрави тся идея LoopB ack — в основном из-за способ­ ности бы стро добавлять R E S T -совм естим ы е ресурсы и просм атривать их в A PI Explorer. Н о мне LoopBack нравится тем, что эта технология достаточно гибка д ля поддерж ки наш их унаследованны х веб-прилож ений MVC. М ожно подклю читься к старой базе данны х и перевести эти проекты в Node». 138 Глава 5. Фреймворки на стороне сервера Элис: «Это единственный фреймворк, который в действительности ориентиро­ ван на iO S и Android, а также полнофункциональных веб-клиентов. У LoopBack имею тся клиентские библиотеки д ля iO S и A ndroid, а это очень важно для нас — разработчиков продуктов, зависящ их от м обильны х прилож ений». 5.9.4. Сильные стороны Д аже из этого краткого введения долж но быть очевидно, что одна из сильных сторон LoopBack — отсутствие необходимости в написании стереотипного кода. Программа командной строки генерирует практически все необходимое для облегченных R E ST совм естимы х веб-A PI, даж е м оделей баз данны х и проверки данных. В то же время LoopB ack ничего особенно не диктует в отнош ении кода кли ен тской части. К тому ж е LoopBack позволяет вам думать о том, какие м одели долж ны быть доступны для браузера, а какие сущ ествую т только на стороне сервера. Н екоторы е ф рейм ворки действую т неправильно и отправляю т браузеру все, что можно. Е сли ваш и м оби льны е п р и ло ж ен и я долж ны взаим одействовать с веб -A PI, п р и ­ см отритесь к клиентском у S D K -пакету L oopB ack (http://loopback.io/doc/en/lb2/ Client-SDKs.html). LoopBack поддерж ивает интеграцию A P I и отправку сообщ ений как д ля iO S, так и д ля A ndroid. 5.9.5. Слабые стороны A P I м одели L oopB ack на базе JS O N отличается от больш инства A P I баз данны х на основе Jav aS crip t. Возможно, вы не сразу поймете, как он соответствует схеме базы данны х текущ его проекта. И поскольку уровень H T T P базируется на Express, его возм ож ности частично ограничиваю тся тем, что поддерж ивает Express. И хотя Express — добротная библиотека H T T P -сервера, сущ ествую т более новы е библи о­ теки д ля N ode с более соврем енны м и A P I. У LoopBack не сущ ествует конкретного A PI плагинов. Вы можете использовать промежуточное П О Express, но этот вариант не настолько удобен, как A P I плагинов F latiro n и ли hapi. Н а этом описание ф рейм ворков в этой главе подходит к концу. П реж де чем п ере­ ходить к следую щ ей главе, давайте сравним ф рейм ворки, чтобы помочь в вы боре наиболее подходящ его варианта д ля вашего следую щ его проекта. 5.10. Сравнение Если вы следовали за разм ы ш лени ям и персонаж ей в этой главе, возмож но, вы уже реш или, какой ф рейм ворк вам стоит использовать. А если еще не реш или — в остав­ ш ейся части этой главы сравниваю тся сильны е стороны всех ф рейм ворков. Если ж е и сравнение не проясни т ситуацию , рис. 5.4 пом ож ет вам вы брать правильны й ф рейм ворк, ответив на несколько вопросов. Р ис. 5.4. Выбор фреймворка Node 140 Глава 5. Фреймворки на стороне сервера Если взглянуть на описания популярны х фреймворков Node на стороне сервера, все они каж утся одинаковы ми. Все они предоставляю т облегченные H T T P A PI и и с­ пользую т м одель сервера вместо м одели страницы, используем ой в PH P. О днако различия в их архитектуре имеют значительные последствия для проектов, созданных на их основе, поэтому сравнение таких ф реймворков мы начнем на уровне HTTP. 5.10.1. Серверы HTTP и маршруты Б ольш ин ство ф рейм ворков N ode базируется на C onnect или Express. В этой главе бы ли представлены три примера, которые не базирую тся на Express и представляют собственны е реш ения д ля H T T P A PI: Koa, hapi и Flatiron. Ф р ей м в о р к K oa бы л создан тем ж е автором, что и Express, но предлагает более свеж ий подход за счет исп ользован ия более соврем енны х средств Jav aS crip t. Если вам нрави тся Express, но вы хотите использовать синтаксис генераторов ES2015, возмож но, K oa вам подойдет. A PI сервера hapi и м арш рутизации обладают вы сокой степенью модульности и вос­ приним аю тся не так, как Express. Если синтаксис Express каж ется вам неуклю жим, опробуйте hapi. hapi упрощ ает работу с серверами H TTP, так что, если вам нуж но вы полнять такие операции, как подклю чение к серверам и ли кластеризация, в о з­ можно, вы предпочтете hapi потом кам Express. С истем а м арш рутизац ии F latiro n обладает обратной совместимостью с Express, но такж е и дополнительной ф ункциональностью . О на генерирует собы тия и исп оль­ зует таблицу м арш рутизации. В этом отнош ении она отличается от стека в стиле Express и ли ком п онентов M iddlew are. С истем е м арш р у ти зац и и F la tiro n мож но передать объектны й литерал. М арш рути зация такж е работает в браузерах, и если у вас есть серверны е разработчики, которы е пы таю тся освоить соврем енную р а з­ работку на стороне клиента, они могут более уверенно чувствовать себя с Flatiron, чем пускаться во все тяж ки е с R eact R outer. 5.11. Написание модульного кода Н е все ф рейм ворки, рассм отренны е здесь, напрям ую поддерж иваю т плагины, но все они в той или иной степени поддерж иваю т расш ирение. Ф рей м ворки на базе Express могут использовать пром еж уточное П О C onnect, но hapi и F latiro n имеют собственны е A P I плагинов. Х орош о определенны й A PI плагинов полезен, потому что он упрощ ает расш ирение ф рейм ворка д ля его новы х пользователей. Если вы используете больш ой ф рейм ворк MVC (наприм ер, Sails.js или LoopBack), A P I плагинов упрощ ает создание нового проекта. LoopBack частично обходит н е­ обходимость в A PI плагинов, предоставляя в распоряж ение программиста мощ ные средства управлени я проектами. О бративш ись к учетной запи си StrongL oop в npm 5.13. Заключение 141 (www.npmjs.com/~strongloop), вы увидите м нож ество L oopB ack-проектов д ля д о ­ бавлен ия поддерж ки Angular, нескольких баз данны х и т. д. 5.12. Выбор персонажей П ерсонаж и этой главы получили достаточно информ ации, чтобы принять правиль­ ное реш ение для своего следую щ его проекта: Ф ил: «В итоге я реш ил остановиться на LoopBack. Это был трудны й выбор; и Sails, и K rak en им ею т п ревосход ную ф у н к ц и о н ал ь н о сть, ко то р ая п о н р ави л ась моей команде. Н о нам показалось, что L oopB ack обладает более сильной долгосрочной поддерж кой и избавляет от м ногих у силий в разработке на стороне сервера». Н адин: « К ак р азработч и к проектов с откры ты м кодом я вы брала F latiro n . Этот ф рейм ворк хорош о адаптируется к различны м проектам, над которы м и я работаю. Н апри м ер, одни проекты о гр ан и ч и ваю тся и сп ользован и ем W in sto n и D irector, а другие использую т весь стек». Элис: «Я вы брала hapi для своего следующего проекта. Этот ф реймворк минимален, поэтому я могу приспособить его к уникальным требованиям проекта. Больш ая часть кода будет относиться к Node, не полагаясь на какой-то конкретны й ф реймворк. Я чувствую , что это реш ение хорош о работает с hapi». 5.13. Заключение О K oa — облегченны й м и н и м ал ьн ы й ф рейм ворк, исп ользую щ ий син такси с ге­ нераторов для пром еж уточного П О . О н хорош о подходит д ля хостинга од н о­ страничны х веб-прилож ений, зависящ их от внеш них веб-API. О hapi ориен ти руется на серверы H T T P и м арш руты . hapi хорош о подходит для облегченны х внутренних подсистем, состоящ их из м нож ества м елких сервисов. О F latiro n — набор несвязны х модулей, которы е могут использоваться либо как веб-ф рейм ворк MVC, либо как более облегченная библиотека Express. F latiron обладает совм естимостью с пром еж уточны м П О Connect. О K raken строится на базе Express с добавлением средств безопасности; м ож ет и сп о льзо ваться д л я р еал и зац и и MVC, содерж ит п оддерж ку O R M и систем у ш аблонизации. О Sails.js — ф рейм ворк MVC, построенны й под влияни ем R ails/D jango; содерж ит O R M и систему ш аблонов. О D erb y JS — и зо м о р ф н ы й ф р ей м в о р к, хорош о п од х о д ящ и й д л я п р и л о ж ен и й реального времени. О LoopBack снимает необходимость в написании стереотипного кода для быстрого генерирования R E ST A PI с полноценной поддерж кой баз данных и A PI Explorer. Connect и Express И з главы 3 вы узнали, как построить простое при лож ение Express. В этой главе Express и C o n n ect рассм атриваю тся более подробно. Э ти два поп улярн ы х модуля N ode использую тся м ногим и веб-разработчикам и. Э та глава показывает, как стро­ ить в еб -п р и л о ж ен и я и R E S T A P I с при м ен ением наи более часто п ри м ен яем ы х паттернов. CONNECT И EXPRESS Концепции, обсуждаемые в следующем разделе, непосредственно применимы к вы сокоуровневом у ф рейм ворку Express, потому что он расш иряет и допол­ няет C o n n ect вы сокоуровневы м и вспом огательны ми средствами. П осле чте­ ни я этого раздела вы будете твердо понимать, как работаю т промеж уточны е ком поненты C o n n ect и как объединять их для создания прилож ения. Д ругие веб -ф р ей м в о р к и N ode работаю т ан алогичны м образом, так что и зучени е C o n n ect даст вам преим ущ ества при изучении новы х ф рейм ворков. Д ля начала мы покажем, как создать базовое прилож ение Connect. Далее в этой главе вы увидите, как построить более слож ное прилож ение Express с использованием поп улярн ы х приемов Express. 6.1. Connect В этом разделе вы познакомитесь с Connect. Вы увидите, как промежуточные компо­ ненты могут использоваться для построения просты х веб-прилож ений и насколько важ ную роль играет порядок пром еж уточны х компонентов. П озднее это помож ет вам при построении при лож ений Express с более вы сокой степенью модульности. 6.1. Connect 143 6.1.1. Настройка приложения Connect Express строится на базе C onnect, но знаете ли вы, что полностью ф ун кц и он ал ь­ ное веб-прилож ение м ож но построить и просто на базе C onnect? Ч тобы загрузить и установить C o n n ect из реестра npm, введите следую щ ую команду: $ npm in s t a ll connect@3.4.0 М иним альное при лож ение C o n n ect вы глядит так: const app = re q u ire ('c o n n e c t')(); app.use((req, re s, next) => { res.en d ('H ello , w o rld !'); }); app.listen(3000); Это простое при лож ение (находящ ееся в папке ch06-connect-and-express/hello-world в архиве прим еров) отвечает сообщ ением H e llo , w orld!. Ф у н кц и я, передаваем ая ap p .u se , представляет собой промежуточный компонент (m iddlew are com ponent), которы й заверш ает запрос отправкой текста H e llo , w orld! как ответа. П ром еж у­ точн ы е ком п оненты образую т основу д л я всех п ри ло ж ен и й C o n n e c t и Express. Рассм отрим их более подробно. 6.1.2. Как работают промежуточные компоненты Connect В C o n n ect промежуточный компонент представляет собой ф ункц ию Jav aS crip t, к отор ая по соглаш ению получает три аргум ента: объект запроса, объект ответа и аргумент, котором у обычно при сваивается им я next, — ф ун кц и я обратного вы ­ зова, указы ваю щ ая, что ком п онент завер ш и л свою работу и вы полнен ие м ож ет быть продолж ено следую щ им пром еж уточны м компонентом. Д о того как в аш и п р о м еж у то ч н ы е к о м п о н ен ты н ач н ут в ы п о л н я т ь ся , C o n n e c t исп о льзу ет диспетчера, ко то р ы й получает запросы и передает их первом у п р о ­ м еж у то ч н о м у ком поненту, д о б ав л ен н о м у в п ри л о ж ен и и . Н а рис. 6.1 показан о ти п и чн о е п р и ло ж ен и е C o n n ect, состоящ ее из д и сп етчера и набора п р о м еж у то ч ­ ны х ком п онентов, вклю чаю щ его в себя ком п онент веден и я ж урн ала, парсер тела запроса, сервер стати ч ески х ф ай л о в и специ альное пром еж уточное програм м ное обеспечение. К ак видите, архитектура A P I пром еж уточного П О означает, что более слож ное по­ ведение мож ет строиться из м еньш их структурны х блоков. В следую щ ем разделе вы увидите, как это делается посредством объединения компонентов. 144 Глава 6. Connect и Express GET /img/logo.png а POST /user/save Диспетчер о logger О Запрос регистрируется в журнале и передается следующему компоненту вызовом next() nextQ О Тело запроса разбирается nextQ О (если оно существует), после чего передается следующему компоненту вызовом next() bodyParser nextQ О res.endQ О Диспетчер получает запрос и передает его первому промежуточному компоненту static nextQ customMiddleware ;.end() 0 О Если запрос обращен к статическому файлу, ответ отправляется с этим файлом, и next() не вызывается; в противном случае запрос перемещается к следующему промежуточному компоненту ѳ Запрос обрабатывается специальным промежуточным компонентом, и ответ завершается Рис. 6.1. Жизненный цикл двух запросов HTTP в сервере HTTP 6.1.3. Объединение промежуточных компонентов C o n n e c t п р ед о став л я ет м етод с им енем u se д л я о б ъ ед и н ен и я п ром еж уточ н ы х ком понентов. О пределим две ф у н кц и и пром еж уточны х ком понентов и добавим их в прилож ение: первая — уж е у п ом и навш аяся ранее ф ун кц и я «H ello , World», а д ругая — ф у н кц и я lo g g er. Листинг 6.1. Использование нескольких промежуточных компонентов Connect const connect = re q u ire ('c o n n e c t'); function logger(req, re s, next) { ■<---- Выводит метод HTTP и URLзапроса, после чего вызывает next(). console.log('% s %s', req.method, re q .u rl); n ext(); } function h ello (req , res) { < -------Завершает ответ на запрос HTTPтекстом «hello world». res.setH eader('C ontent-T ype', 't e x t /p la i n ') ; re s.e n d ('h e llo w orld'); } connect() .use(logger) .use(hello) .listen (3 0 0 0 ); П ром еж уточны е ком поненты имею т две сигнатуры: с next и без. Это объясняется тем, что ком понент заверш ает ответ H T T P и возвращ ать управление диспетчеру ему не нужно. 6.1. Connect 145 Ф у н к ц и я u s e () возвращ ает экзем пляр при лож ения C on n ect д ля поддерж ки сц е­ п лени я методов, как было показано выше. О братите внимание: сцепление вызовов .u s e ( ) при этом не требуется: const app = connect(); app.use(logger); app.use(hello); app.listen(3000); Итак, у вас заработало простое прилож ение «Hello World»; теперь можно посмотреть, почему важен порядок вы зовов .u s e ( ) пром еж уточны х ком понентов и как страте­ гически использовать этот порядок для внесения изм енений в работу прилож ения. 6.1.4. Упорядочение компонентов П орядок пром еж уточны х ком понентов в ваш ем при лож ении мож ет кардинально влиять на его поведение. О пустив n e x t( ) , вы остановите вы полнение прилож ения, а объединение ком понентов позволяет реализовать такие ф ункц и он ал ьн ы е в о з­ м ож ности, как аутентиф икация. Ч то произойдет, если пром еж уточны е ком п оненты не вы зовут n e x t? В ернем ся к предыдущ ему примеру «H ello W orld», в котором первым используется компонент logger, а за ним следует компонент h e llo . В этом примере C onnect направляет вывод в s td o u t, а затем реагирует на запрос H TTP. Н о что произойдет, если компоненты будут следовать в другом порядке? Листинг 6.2. Ошибка: компонент hello предшествует компоненту logger Всегда вызывает next(), поэтому const connect = re q u ire ('c o n n e c t'); следующий компонент будет вызван. function logger(req, re s, next) { console.log('% s %s', req.method, re q .u rl); next(); Не вызывает next(), потому что } компонент отвечает на запрос. function h ello (req , res) { res.setH eader('C ontent-T ype', 'te x t / p l a in ') ; re s.e n d ('h e llo w orld'); } const app = connect() .use(h ello ) <Компонент logger никогда не будет .use(logger) вызван, потому что hello не вызывает .listen (3 0 0 0 ); next(). В этом прим ере пром еж уточны й ком понент h e llo вы зы вается первы м и реагирует на запрос H T T P, как и ож идалось. О днако ком понент lo g g e r вы зван никогда не будет, потом у что h e llo никогда не вы зы вает n e x t( ), поэтому управлени е никогда не возвращ ается диспетчеру для вы зова следую щ его промеж уточного компонента. М ораль: если ком понент не вы зы вает n e x t( ) , то остальны е ком поненты в цепочке вы зы ваться не будут. 146 Глава 6. Connect и Express Рис. 6.2. Порядок следования промежуточных компонентов важен Р и су н о к 6.2 показы вает, как в этом при м ере обходится ком понент lo g g e r и как исправить эту проблему. К ак видите, разм ещ ать h e l l o перед lo g g e r достаточно бессмысленно, но при п р а­ вильном исп ользован ии упорядочение м ож ет принести пользу. 6.1.5. Создание настраиваемых промежуточных компонентов Вы усвоили некоторы е основы исп ользован ия пром еж уточны х компонентов; пора рассмотреть тему более подробно, чтобы понять, как создавать более универсальные и общ ие пром еж уточны е компоненты . О б ы чн о п ри п о стр о ен и и п р о м еж у то ч н ы х ко м п о н ен то в и с п о л ь зу ет с я простое соглаш ение, которое о ткры вает перед разработч и ком возм ож н ости д л я к о н ф и ­ гурации: и сп о льзо ван и е ф ункц ии, ко торая возвращ ает другую ф ун кц и ю (за м ы ­ кан и е). Б а зо в а я структура настраиваем ы х пром еж уточны х ком понентов такого рода вы гл яд и т так: 6.1. Connect 147 function setup(options) { / / Логика инициализации Здесь выполняется дополнительная инициализация промежуточного компонента. return fu nction(req, re s, next) { / / Логика промежуточного компонента <} } Значение options остается доступным, хотя внешняя функция вернула управление. П рим ер исп ользован ия таких компонентов: app.use(setup({ some: 'o p tio n s' })); О братите внимание: ф у н кц и я se tu p вы зы вается в строке ap p .u se , тогда как в п р е­ ды дущ их прим ерах мы просто передавали ссы лку на ф ункцию . В этом разделе данны й прием будет применен для построения трех настраиваемых, пригодны х д ля повторного исп ользован ия пром еж уточны х компонентов: О ком понента lo g g e r с настраиваем ы м ф орм атом вывода; О ком понента м арш рутизац ии д ля вы зова ф ун кц и й в зависи м ости от зап р аш и ­ ваемого URL; О ко м п о н ен та п ер езап и си U R L , п р еоб разую щ его ал ьтер н ати вн ы е ч асти U R L (slu g s) в идентиф икаторы . Н ачнем с д оработки ком п онента lo g g e r, чтобы расш и рить возм ож н ости его н а­ стройки . П ро м еж у то чн ы й ком п онент lo g g e r , созд ан н ы й ранее в этой главе, не поддерж ивал настройку. О н был ж естко запрограм м ирован д ля вы вода значений req.m ethod и r e q . u r l при вызове. Н о что, если в какой-то м ом ент вы захотите и з ­ м енить состав вы водим ой инф орм ации? Н а практике использование настраиваемого промеж уточного компонента почти не отличается от использования любого другого промежуточного компонента, который создавался до настоящ его момента, если не считать того, что ком поненту можно передать дополнительны е аргументы для изм енения его поведения. И спользование н астраиваем ого ком п онента в при ло ж ен и и обы чно вы гл яд и т прим ерно так, как в следую щ ем прим ере (где lo g g e r получает строку с описанием ф орм ата вывода): const app = connect() .use(logger(':m ethod : u r l ') ) .u se (h e llo ); Д л я р еализаци и настраиваем ого ком понента lo g g e r сначала необходимо опреде­ лить ф ункцию se tu p , которая получает один строковы й аргум ент (в этом примере ему присваивается им я fo rm at). П ри вы зове se tu p возвращ ается ф ункц ия, которая будет использоваться пром еж уточны м ком понентом C onnect. В озвращ аемы й ком ­ понент сохраняет доступ к перем енной fo rm at даж е после того, как ф ун кц и я setu p 148 Глава 6. Connect и Express вернет управление, потом у что он о пределяется в том ж е зам ы кан и и Jav aS crip t. Затем lo g g e r зам еняет лексем ы в ф орм атной строке соответствую щ им и свойства­ ми запроса из объекта req, вы водит инф орм ацию в s td o u t и вы зы вает n e x t() , как показано в листинге 6.3. Листинг 6.3. Настраиваемый промежуточный компонент logger для Connect Функция setup может вызываться Компонент logger использует многократно с разными регулярное выражение для поиска конфигурациями. свойств запроса. function setup(form at) { -<— const regexp = /:(\w + )/g ; return function createLogger(req, re s, next) { const s tr = form at.replace(regexp, (match, property) => return req[property]; Выводит регистрационную }); запись о запросе на консоль. c o n so le .lo g (str); .<next(); Передает управление } следующему компоненту. } module.exports = setup; Напрямуюэкспортирует функцию setup компонента logger. Компонент logger, который будет использоваться Connect. Использует регулярное выражение для форматирования выводимой информации. Так как ком понент lo g g e r создавался как настраиваем ы й, вы мож ете м ногократно вы зы вать .u s e ( ) для lo g g e r в одном при лож ении с разны м и кон ф и гурац иям и или ж е использовать этот код в других прилож ениях, которы е вы разработаете в бу­ дущем. П ростая кон цепц ия настраиваем ы х пром еж уточны х ком понентов хорош о известна в сообщ естве C o n n ect и при м ен яется ко всем базовы м промеж уточны м ком понентам C o n n ect д ля последовательности. Ч т о б ы и с п о л ь з о в а т ь к о м п о н ен т l o g g e r из л и с т и н г а 6.3, п ер ед ай те ем у с т р о ­ к у с в к л ю ч е н и е м н е к о то р ы х св о й ств о б ъ е к та r e q u e s t . Н а п р и м ер , с в ы зо во м .u s e ( s e tu p ( ':m e th o d : u r l ') ) д ля каж дого запроса будет вы водиться метод H T T P (G ET, P O S T и т. д.) и U R L -адрес. П реж де чем переходить к Express, посмотрим, как в C o n n ect поддерж ивается об­ работка ошибок. 6.1.6. Использование промежуточных компонентов для обработки ошибок Во всех при лож ениях возникаю т ош ибки (как на системном уровне, так и на п оль­ зовательском ), и вы сильно упростите себе жизнь, если будете готовы к ош ибочным ситуациям — даж е к непредвиденны м . C o n n ect реализует разновидность пром е­ ж уточны х ком понентов с обработкой ош ибок, которая следует тем ж е правилам, что и обычная, но наряду с объектами запроса и ответа получает объект ош ибки. 6.1. Connect 149 О бработка ош ибок в C onnect намеренно сделана минимальной, чтобы разработчик мог сам задать способ обработки ошибок. Н апример, через промежуточное П О могут проходить только систем ны е ош ибки и ош ибки п ри лож ения (наприм ер, перем ен­ ная fo o не определена), и ли ош ибки пользователя (пароль недействителен), или их н екоторая ком бинация. C o n n ect позволяет вы брать, что лучш е подходит для ваш его прилож ения. В этом разделе мы используем обе разновидности, и вы узнаете, как работаю т про­ меж уточны е компоненты обработки ошибок. Также будут представлены некоторые полезны е паттерны , которы е могут при м ен яться в следую щ их областях: О использование обработчика ош ибок C o n n ect по умолчанию ; О сам остоятельная обработка ош ибок прилож ения; О использование нескольких ком понентов обработки ош ибок. А теперь посмотрим, как C o n n ect обрабаты вает ош ибки без какой-либо настройки. Обработчик ошибок Connect по умолчанию Р ассм о тр и м сл еду ю щ и й п р о м еж у то ч н ы й ком п онент, к о то р ы й вы д ает ош ибку R e fe re n c eE rro r, потому что ф у н кц и я f o o ( ) не определена прилож ением: const connect = re q u ire ('c o n n e c t') connect() .u se((req , res) => { fo o (); res.setH eader('C ontent-T ype', 't e x t/ p la i n ') ; re s .e n d ('h e llo w orld'); }) .listen(3000) По умолчанию C onnect отвечает кодом статуса 500, телом ответа с текстом «Internal Server E rro r» и дополнительной и н ф орм аци ей о сам ой ош ибке. Это неплохо, но в лю бом реальном при лож ении вы, скорее всего, предпочтете реализовать более специализированную обработку ош ибок — например, отправлять их демону веде­ н и я ж урнала. Самостоятельная обработка ошибок приложения C o n n e c t такж е предоставляет средства д л я сам остоятельной обработки ош ибок с использованием пром еж уточны х компонентов. Н апример, в ходе разработки вы мож ете отвечать кли ен ту J S O N -представлением ош ибки д ля быстрого и простого получения инф орм ации, а в итоговой версии при лож ение будет отвечать простым сообщ ением «Server error», чтобы не раскры вать конф иденциальную внутренню ю ин ф орм ацию (трасси ровку стека, им ена ф айлов, ном ера строк) потенциальном у атакующему. 150 Глава 6. Connect и Express Ф у н к ц и я обработки ош ибок пром еж уточны х компонентов долж на определяться с ч еты р ьм я аргум ентам и, e r r , req, r e s и n e x t (л и с т и н г 6.4), тогда как обы чны е п р о м еж уточны е ком п оненты получаю т аргум енты req, r e s и n e x t. С ледую щ ий листинг демонстрирует прим ер промежуточного компонента с обработкой ошибок. З а полны м прим ером сервера обращ айтесь к архиву исходного кода в папке ch06connect-and-express/listing6_4. Листинг 6.4. Промежуточный компонент обработки ошибок в Connect const env = process.env.NODE_ENV | | 'development'; Промежуточный компонент обработки ошибок использует четыре аргумента. function errorH andler(err, req, re s, next) { res.statusC ode = 500; switch (env) { <­ Поведение промежуточного case 'development': компонента errorHandler зависит c o n s o le .e rro r('E rro r:'); от значения NODE ENV. c o n so le .e rro r(e rr); res.setH eader('C ontent-T ype', 'a p p lic a tio n /js o n '); res.en d (JS O N .strin g ify (err)); break; defau lt: res.en d ('S erv er e r r o r ') ; } } module.exports = errorHandler; Использование Node_Env для определения режима работы О дин из распространенны х приемов C o n n ect — использование перем енной о кр у ­ ж ен и я NODE_ENV (process.env.NODE_ENV) д ля переклю чения поведения меж ду в а­ риан там и условий работы сервера — например, реж имом разработки и реж имом реальной эксплуатации. О б н ар у ж и в ош ибку, C o n n e c t п ер ек л ю ч ается на вы зов только пром еж уточ н ы х компонентов, предназначенны х д ля обработки ош ибок (рис. 6.3). П редставьте, что ваш е п р и лож ение предоставляет возм ож ность вы п ол н ен и я ау ­ тен ти ф и к ац и и д ля работы в адм и н и стр ати вн ой области блога. Е сли ком понент м арш рутизац ии д ля пользовательских м арш рутов вы зы вает ошибку, компоненты b lo g и admin будут пропущ ены , потом у что они не я в л я ю т ся ком п онентам и о б ­ работки ош ибок — они определяю т только три аргумента. Затем C o n n ect видит, что e rro rH a n d le r получает аргум ент ош ибки, и активи зирует его. П ромеж уточны е ком поненты вы глядят прим ерно так: connect() .u s e (ro u te r ( r e q u ir e ( './r o u te s /u s e r') ) ) .u s e ( r o u te r(re q u ire ('./ro u te s /b lo g '))) / / Пропускается .u s e (ro u te r(re q u ire ('./ro u te s /a d m in '))) / / Пропускается .use(errorH andler); 6.2. Express 151 О Запрос HTTP к URL-адресу, вызывающий ошибку на сервере Диспетчер передает запрос по стеку промежуточных компонентов, как обычно і ОйІ В компоненте маршрутизации произошла ошибка! О Компонент hello пропускается, потому что он не был определен как промежуточный компонент для обработки ошибок ѳ Компонент errorHandler получает объект ошибки, созданный компонентом logger, и может ответить на запрос в контексте ошибки Рис. 6.3. Жизненный цикл запроса HTTP, порождающего ошибку сервера Connect Ф ун кци ональность ускоренной обработки в зависи м ости от хода вы полнен ия — одна из ф ундам ентальны х концепций, прим еняем ы х для организации прилож ений Express. Теперь, когда вы и зу ч и л и основы C o n n ect, м ож но п ерейти к более подробном у изучению Express. 6.2. Express Express — поп улярн ы й веб-ф рейм ворк, которы й некогда строился на базе C onnect, но и сейчас остается совм ести м ы м с п ром еж уточ н ы м и ком п он ен там и C onnect. Х отя Express вклю чает в себя базовую ф ункциональность (такую , как предостав­ ление статических ф айлов, м ар ш р у ти зац и я U R L и кон ф и гурац и я при лож ения), этот ф рейм ворк все равно остается м иним альны м . О н предоставляет достаточную структуру д ля создания блоков, подходящ их д ля повторного использования, без изли ш н их ограничений д ля ваш ей практики разработки. 152 Глава 6. Connect и Express В н еск о льк и х б ли ж ай ш и х р азд ел ах мы р еал и зуем п ри лож ен и е E xpress при п о ­ мощ и генератора при лож ений Express. П роцесс будет описан более подробно, чем кратки й обзор в главе 3, поэтом у в конце главы вы будете достаточно хорош о п о ­ нимать Express для самостоятельного построения веб-прилож ений Express и RESTсовм естимы х A PI. По ходу главы к заготовке прилож ения будет добавляться новая ф ункциональность, а к ее концу будет построено полноценное прилож ение. 6.2.1. Генерирование заготовки приложения Express не требует от разработчика соблю дения определенной структуры п р и л о ­ ж ения. Вы мож ете разм естить м арш руты в лю бом количестве ф айлов, разм естить откры ты е активы в лю бом каталоге на свое усм отрение и т. д. М ин им альное п р и ­ лож ение Express получается совсем коротким — как в листинге 6.5, реализую щ ем полностью работоспособны й сервер HTTP. Листинг 6.5. Минимальное приложение Express const express = re q u ire ('e x p re s s '); const app = express(); a p p .g e t ( '/ ', (req, res) => { -<-------Отвечает на любой веб-запрос к/. re s .s e n d ('H e llo '); <.-------- Отправляет строку «Hello» как текст ответа. }); app.listen (3 0 0 0 ); ■<-------- Прослушивает порт 3000. П рограм м а ком ан дной строки e x p r e s s ( l ) , вход ящ ая в пакет e x p r e s s - g e n e r a to r (www.npmjs.com/package/express-generator), создает заготовку при лож ения за вас. С генерированное при лож ение — хорош ая отправная точка д ля начинаю щ их р а з­ работчиков Express, так как в сгенерированное прилож ение вклю чаю тся шаблоны, откры ты е активы, ко н ф и гурац ия и многое другое. С тан д ар тн ая заго то в к а п р и л о ж ен и я , ген ер и р у ем ая e x p r e s s ( l ) , состои т из н е ­ скольких каталогов и ф айлов (рис. 6.4). Этот набор был разработан для того, чтобы разработчики м огли освоиться с Express за считанны е минуты, но структура п р и ­ л ож ени я полностью определяется вами и ваш ей командой. В примере этой главы используются ш аблоны Em bedded JavaS cript (E JS), которые по своей структуре напоминают разметку H TM L. Язы к EJS близок к PHP, JS P (для Java) и ERB (д ля Ruby): серверны й код Jav aS crip t встраивается в документ H T M L и вы ­ полняется перед отправкой клиенту. EJS более подробно рассматривается в главе 7. В этом разделе будут рассм отрены следую щ ие операции: О глобальная установка Express с npm; О генерирование прилож ения; О анализ п р и лож ения и установка зависимостей. 6.2. Express ф 153 О alex@locohost: ~/Documents/Code/nodeinaction/ch07-connect-and-express/li. — — app.js bin — — package.json public images javascripts stylesheets 1— style, css routes I— index, js '— users, js views error.jade index.jade layout.jade I--- WWW Ё — — Ё 7 directories, 9 files Рис. 6.4. Структура стандартной заготовки приложения с использованием шаблонов EJS Установка Express В ы полните глобальную установку e x p r e s s - g e n e ra to r с npm: $ npm in s t a ll -g express-generator Д л я просм отра всех доступны х парам етров используется ф лаг - -h e lp (рис. 6.5). alex@locohost: ~/Documents/Code/nodeinaction/ch07-connect-and-express/li... > express --help Usage: express [options] [dir] Options: -h, --help output usage information -V, --version output the version number add ejs engine support (defaults to jade) -e, --ejs --hbs add handlebars engine support add hogan.js engine support -H, --hogan -c, --css <engine> aao styiesneet <engine> suppi |compass|sass) (defaults to plain css) --git add .gitignore -f, --force force on non-empty directory Рис. 6.5. Справочная информация Express 154 Глава 6. Connect и Express Н екоторы е из этих парам етров генерирую т небольш ие части п ри лож ения за вас. Н априм ер, вы мож ете приказать сгенерировать ф и ктивн ы й ш аблонны й ф айл для вы бранного я д р а ш аблонизации. А налогичны м образом, если задать п реп роцес­ сор CSS при пом ощ и парам етра - -c s s , д ля него будет сгенерирован ф и кти вн ы й ш аблонны й файл. П осле того как исполняем ы е ф айлы будут установлены , посмотрим, как сгенери­ ровать прилож ение, над которы м мы будем работать в этой главе. Генерирование приложения В наш ем при лож ении будет использован ф лаг -е (и л и - - e j s ) , вы бираю щ ий ядро ш аб л о н и зац и и EJS. В ы полни те ком анду e x p r e s s -е shoutbox. Ч тобы в о сп р о и з­ вести прим еры кода из наш его репози тори я G itH u b , введите команду e x p re ss -е lis tin g 6 _ 6 . П ри лож ен и е будет создано в каталоге shoutbox. О но содерж ит ф айл package.json для описания проекта и зависим остей, сам ф айл прилож ения, каталоги с общ едо­ ступны м и ф ай лам и и каталог для обработчиков марш рутов. Анализ приложения Д авай те п об ли ж е п р и см о тр и м ся к тому, что ж е бы ло сгенерировано. О ткрой те ф а й л package.json в р ед ак то р е, ч то б ы п р о см о т р е ть за в и с и м о с т и п р и л о ж е н и я (рис. 6.6). Express не м ож ет угадать, какая версия зависим остей вам понадобится, поэтом у реком ендуется указать о сновн ую /дополнительную версию и версию и с ­ п р авл ен и я м одуля, чтобы не создать случайны е ош ибки. Н априм ер, вы раж ение " e x p r e s s " :" ~ 4 .1 3 .1 " яв н о задает нуж ны е версии и обеспечивает исп ользован ие идентичного кода во всех вариантах установки. 1 { 2 3 4 5 6 "name": "listing6_6", "version": "0.0.0", "privet*": true, "scripts": { "start": "node ./bin/www" >, 8 9 10 11 12 13 14 15 "dependencies": { "body-parser": "~1.13.2", "cookie-parser": "**1.3.5", "debug": "-2.2.0", "express": "*4.13.1", "jade": "*1.11.0", "morgan": "*1.6.1", "serve-favicon": "*2.3.0" is 17 > } Рис. 6.6. Содержимое сгенерированного файла package.json 6.2. Express 155 Рассмотрим файл приложения, сгенерированный e x p re s s (l) (листинг 6.6). Пока этот ф айл останется в исходном виде. П ром еж уточны е компоненты уж е долж ны быть знакомы вам по разделам этой главы, посвящ енным Connect, но вам стоит взглянуть на то, как задается конф игурация промежуточных компонентов по умолчанию. Листинг 6.6. Сгенерированная заготовка приложения Express var var var var var var var var var express = re q u ire ('e x p re s s '); path = re q u ir e ( 'p a th ') ; I Предоставляет значок приложения favicon = re q u ire ('s e rv e -fa v ic o n '); -<---- 1 по умолчанию. logger = require('m organ'); cookieParser = re q u ire ('c o o k ie -p a rs e r'); bodyParser = re q u ire ('b o d y -p a rse r'); routes = re q u ir e ( './r o u te s /in d e x ') ; users = r e q u ir e ( './r o u te s /u s e r s ') ; app = express(); a p p .s e t('v ie w s ', p a th .jo in (_dirname, 'v ie w s')); app.set('v iew en g in e', 'e j s ') ; Выводит журналы в формате, удобномдля разработки. ap p .u se (lo g g e r('d e v ')); app.use(bodyParser.json()); < -------Разбирает тела запросов. app.use(bodyParser.urlencoded({ extended: fa ls e })); app.use(cookieParser()); a p p .u se (e x p re ss.sta tic (p a th .jo in (_dirname, 'p u b lic ') ) ) ; Предоставляет статические файлы из каталога ./public. a p p .u s e ( '/', ro u tes); -<-------- Залает маршруты приложения. a p p .u s e ('/u s e rs ', users); app.use(function(req, re s, next) { le t e rr = new E rror('N ot Found'); e r r .s ta tu s = 404; n e x t(e rr); }); i f (a p p .g e t('e n v ') === 'development') { app .u se(fu n ctio n (err, req, re s, next) { r e s .s ta tu s ( e r r .s ta tu s || 500); r e s .r e n d e r ( 'e r r o r ', { message: err.m essage, e rro r: e rr }); }); } ap p .u se(fu n ctio n (err, req, re s, next) { r e s .s ta tu s ( e r r .s ta tu s || 500); r e s .r e n d e r ( 'e r r o r ', { message: err.message, e rro r: {} }); }); module.exports = app; Выводит страницы ошибок в формате HTMLв режиме разработки. 156 Глава 6. Connect и Express Ф ай л ы package.json и app.js существуют, но прилож ение еще не работает, потому что зависим ости еще не установлены . К аж ды й раз, когда вы генерируете ф айл package. json из e x p r e s s ( l) , для него необходимо установить зависимости. Выполните ком ан­ ду npm i n s t a l l , а затем запустите при лож ение ком андой npm s t a r t . Проверьте работоспособность прилож ения, открыв адрес http://localhost:3000 в бра­ узере. В неш ний вид при лож ения по ум олчанию показан на рис. 6.7. E x p r e s s - M o z illa F ir e fo x W < Express 1+ !"» localhost 3000 4 v e | E xpress Welcome to Express Рис. 6.7. Приложение Express по умолчанию О знаком ивш ись со сгенерированной заготовкой, м ож но переходить к построению реального прилож ения Express. П рилож ение будет вы полнять ф ункции простейш е­ го чата, в котором пользователи смогут публиковать сообщ ения. П ри построении при лож ений такого рода опы тны е E xpress-разработчики обычно начинаю т с п л а ­ ни рования A P I и, соответственно, необходим ы х м арш рутов и ресурсов. Планирование приложения Shoutbox Требования к прилож ению : 1. Р еги страц и я учетны х записей пользователей, процедуры входа и выхода. 2. В озм ож ность публикац ии сообщ ений (записей). 3. С траничны й просм отр записей. 4. П ростой R E S T A P I с поддерж кой аутентиф икации. Д л я этого необходим о ор ган и зо вать х р анени е дан ны х и об раб отку д ан н ы х при аутентиф икации. Также следует предусмотреть проверку данных, введенных поль­ зователем . Н еобходим ы е м арш руты вы глядят так: О м арш руты API; О GET / a p i / e n t r i e s : получение списка записей; О GET / a p i / e n t r i e s / p a g e : получение одной страницы записей; О POST / a p i / e n t r y : создание новой записи; О м арш руты пользовательского интерф ейса; 6.2. Express 157 О GET /p o s t: ф орм а для новой записи; О POST /p o s t: пуб ликац ия новой записи; О GET / r e g i s t e r : вы вод ф орм ы регистрации; О POST / r e g i s t e r : создание новой учетной записи; О GET /lo g in : вы вод ф орм ы входа; О POST /lo g in : вход в прилож ение; О GET /lo g o u t: вы ход из прилож ения. Такая структура типи чн а д ля б ольш ин ства веб-прилож ений. Возмож но, вы см о­ ж ете использовать прим ер этой главы как образец д ля построения ваш их будущ их прилож ений. В листинге 6.6 м ож но зам етить несколько вы зовов a p p .s e t: a p p .s e t('v ie w s ', p a th .jo in (_dirname, 'v ie w s')); app.set('v iew en g in e', 'e j s ') ; Так в ы п о л н я ется н астр о й к а ко н ф и гу р ац и и п р и л о ж ен и я Express. В следую щ ем разделе ко н ф и гурац ия Express рассм атривается более подробно. 6.2.2. Настройка конфигурации Express и приложения Требования вашего при лож ения зависят от условий, в которы х оно вы полняется. Н апример, во врем я разработки мож ет вы водиться более подробная ж урнальная инф орм ация, а в условиях реальной эксплуатации — сокращ енны й вариант ж у р ­ нала. Кроме настройки ф ункциональности д ля конкретного окруж ения вы можете определить настройки уровня прилож ения, чтобы передать Express информацию об используемом ядре шаблонов и о том, где хранятся шаблоны. Express также позволяет определять нестандартные параметры конф игурации в форме пар «ключ-значение». ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ Д л я настройки перем енны х окруж ения в системах U N IX используется сл е­ дую щ ая команда: $ NODE_ENV=production node app В системе W indow s код вы глядит так: $ se t NODE_ENV=production $ node app Д л я работы с перем енны м и окруж ения в при лож ении используется объект process.env. 158 Глава 6. Connect и Express В Express реализована м и н и м ал ьн ая система работы с окруж ением . О на состоит из нескольких методов, использую щ их значение перем енной окруж ения NODE_ENV: О a p p .s e t( ) ; О a p p .g e t( ) ; О a p p .e n a b le (); О a p p .d is a b le ( ) ; О a p p .e n a b le d (); О a p p .d is a b le d ( ) . В этом разделе будет показано, как использовать систему кон ф и гурац ии для н а ­ стройки поведения Express и как пользоваться этой системой для ваш их собственных целей в ходе разработки. Р азберем ся, что ж е поним ается под конфигурацией на базе окружения. Х отя п ере­ м енная окруж ения NODE_ENV появилась в Express, многие другие ф рейм ворки N ode п р и н ял и ее как м еханизм оповещ ения при лож ений N ode о том, в каких условиях они работаю т (по ум олчанию реж им разработки). М ето д a p p . c o n f i g u r e ( ) п о л у ч а ет н е о б я за т е л ь н ы е стр о к и , п р е д с т а в л я ю щ и е перем енн ы е о круж ения, и ф ункцию . Е сли п ерем енн ая окруж ен и я соответствует переданной строке, нем едленно ак ти ви зи р у ется ф у н кц и я обратного вы зова; если зад ан а то лько ф у н кц и я, она вы зы вается д ля всех перем енны х окруж ения. И м ена перем енны х окруж ения вы бираю тся соверш енно произвольно. Н априм ер, можно определить перем енн ы е developm ent, s ta g e , t e s t и p ro d u c tio n (и л и сокращ енно prod): i f (a p p .g e t('e n v ') === 'development') { app.use(express.errorH andler()); } E xpress исп ользует систем у ко н ф и гу р ац и и в своей внутренн ей реали зац и и , что позволяет вам настроить поведение Express, но система кон ф и гурац ии такж е д о ­ ступна д ля вашего личного использования. Express такж е предоставляет логические разновидности a p p .s e t( ) и app. g e t (). Н а ­ пример, вы зов a p p .e n a b le ( s e ttin g ) эквивалентен a p p . s e t ( s e t t i n g , t r u e ) , а app. e n a b le d ( s e ttin g ) м ож ет исп ользоваться д ля проверки установки ф лага (парны е методы д ля f a l s e — a p p .d is a b le ( s e t t i n g ) и a p p .d is a b le d ( s e ttin g ) ) . Еще один полезны й параметр, часто используемы й при разработке A PI с Express, — js o n s p a c e s. Е сли д обавить его в ф ай л app.js, р азм етк а JS O N будет вы водиться в более удобочитаем ом формате: a p p .s e t('js o n sp aces', 2); 6.2. Express 159 Теперь вы знаете, как использовать систем у кон ф и гурац ии д ля ваш их целей, и мы мож ем перейти к визуализац ии представлений в Express. 6.2.3. Визуализация представлений В прилож ении этой главы будут использоваться шаблоны EJS, хотя, как упоминалось ранее, исп ользоваться м ож ет практически лю бое ядро ш аблонов, известное в со­ обществе Node. Если вы не знаком ы с EJS, не беспокойтесь. EJS имеет много общего с другими язы кам и шаблонов, встречающ имися в других платформах веб-разработки (PH P, JSP, ERB). О сновы EJS будут представлены в этой главе, но EJS с несколькими другими ядрам и ш аблонов будет более подробно рассматриваться в главе 7. В изуализация представлений, будь то визуализац ия целой страницы H TM L, ф раг­ м ента H T M L и ли кан ала RSS, кри ти чн а практи чески д ля каж дого прилож ения. К онц епци я проста: вы передаете данны е представлению , эти данны е п реобразу­ ю тся — обычно в ф орм ат H T M L для веб-прилож ений. Вероятно, вы уж е знаком ы с понятием представления (view ), потому что многие ф рейм ворки представляю т аналогичную ф ункциональность; на рис. 6.8 показано, как представления ф о р м и ­ рую т новое представление данных. { name: 'Tobi', species: 'ferret', age: 2 } <hl>Tobi</hl> <p>Tobi is a 2 year old ferret.</p> Рис. 6.8. Шаблон HTML + данные = представление данных в формате HTML Ш аблон, генерирую щ ий ш аблон на рис. 6.8, вы глядит так: <h1><%= name %></h1> <p><%= name %> is a 2 year old <%= species %>.</p> Express предоставляет два способа визуализац ии представлений: на уровне при ло­ ж ен ия вы зовом a p p .re n d e r() и в ответе вы зовом r e s .r e n d e r ( ) , которы й использует первы й во внутренней реализации. В этой главе используется только r e s . re n d e r(). Загл ян у в в ф айл ./routes/index.js, вы увидите, что в нем определяется ф ункц ия, в ы ­ зы ваю щ ая r e s . r e n d e r ( 'i n d e x ') д ля ви зу ал и зац и и ш аблона ./views/index.ejs, как показано в следую щ ем ф рагм енте (и з listing6_8): r o u t e r . g e t ( '/ ', (req, re s, next) => { re s .re n d e r('in d e x ', { t i t l e : 'Express' }); }); 160 Глава 6. Connect и Express Прежде чем рассм атривать r e s .r e n d e r ( ) более подробно, посмотрим, как настроить систем у представлений. Настройка системы представлений Н астр о й ка систем ы представлени й Express в ы п ол н яется достаточно просто. Н о несм отря на то, что e x p r e s s ( l ) генерирует кон ф и гурац ию за вас, будет полезно знать, что ж е происходит «за кулисам и», — эта ин ф орм аци я пригодится вам для внесения изм енений. М ы ограничим ся трем я областями: О настройка поиска представлений; О настройка яд р а ш аблонов по умолчанию ; О вклю чен и е к эш и р о в ан и я п р ед ставл ен и й д л я сокращ ен и я ф ай л ового в в о д а / вывода. Н ачнем с парам етра view s. Изменение каталога поиска В следую щ ем ф рагм енте показан парам етр views, сгенерированны й Express: a p p .s e t('v ie w s ', _dirname + '/v ie w s '); О н задает путь, которы й будет использоваться Express при поиске представлений. М ы реком ендуем вклю чать в путь п е р е м е н н у ю __dirnam e, чтобы при лож ение не зависело от того, что текущ ий рабочий каталог яв л яе тс я корневы м каталогом п р и ­ лож ения. __DIRNAME __d irn a m e (с д в у м я п о д ч е р к и в а н и я м и ) — гл о б ал ьн ая п ер ем ен н ая N ode, определяю щ ая каталог, в котором находится ф айл, вы полняем ы й в н астоя­ щ ее время. Ч асто в процессе разработки этот каталог совпадает с текущ им рабочим каталогом, но при реальной эксплуатации прилож ения исполняемый ф айл м ож ет быть запущ ен из другого каталога. П ер е м е н н ая __ dirnam e обе­ спечивает согласование путей меж ду разны м и рабочим и средами. С ледую щ ий парам етр кон ф и гурац ии — view engine. Ядро шаблонов по умолчанию К огда при лож ение бы ло сгенерировано e x p r e s s ( l) , парам етру view en g in e было присвоено значение e js , потому что ядро ш аблонов EJS было вы брано параметром ком андной строки -e. Э та настройка позволяет использовать index вместо in d ex . 6.2. Express 161 e js . В противном случае Express требует указать расш ирение д ля того, чтобы опре­ делить используем ое ядр о ш аблонов. П очем у Express вообщ е учиты вает расш и рения? О ни позволяю т использовать н е­ сколько ядер ш аблонов в одном при лож ении Express с сохранением ясности A PI д ля стандартны х сценариев исп ользован ия (впрочем , в больш инстве прилож ений используется только одно ядро ш аблонов). П редполож им, вы обнаруж или, что каналы RSS прощ е програм м ирую тся с другим ядром ш аблонов, или вы переходите с одного яд р а на другое. П о ум олчанию в п р и ­ лож ени и используется Pug, а д ля м арш рута /f e e d — EJS, на что в следую щ ем коде указы вает расш ирение .e js : app.set('v iew en g in e', 'p u g '); a p p .g e t ( '/ ', function(){ re s .re n d e r('in d e x '); }); a p p .g e t( '/f e e d ', function(){ r e s .r e n d e r ( 'r s s .e js ') ; }); СИНХРОНИЗАЦИЯ PACKAGE.JSON Помните, что каждое дополнительное ядро шаблонов, которое вы хотите использовать в приложении, должно быть добавлено в объект зависимостей файла package.json. Такие пакеты должны устанавливаться командой npm i n s t a l l --s a v e имя_пакета и удаляться из node_modules и package.json командой npm u n i n s t a l l - -sav e имя_пакета. Такой подход упрощает эксперименты с разными ядрами шаблонов, когда вы еще только подбираете ядро для своего проекта. Кэширование представлений П арам етр view cache вклю чается по ум олчанию в среде эксплуатац ии прилож ения и предотвращ ает вы полнение дискового вво д а/вы во д а при последую щ их вы зовах re n d e r ( ) . С одерж им ое ш аблона сохраняется в памяти, что приводит к зн ач и тел ь­ ному повы ш ению бы стродействия. П обочны й эф ф ект заклю чается в том, что вы не смож ете редактировать ф айлы ш аблонов без перезапуска сервера; именно по этой причине данная настройка отклю чена в процессе разработки. Вероятно, в условиях реальной эксплуатац ии ее следует держ ать вклю ченной. К ак показано на рис. 6.9, при отклю ченном кэш ировании представлений ш аблон читается с диска при каж дом запросе. И м енно этот реж им позволяет вносить и з ­ м енени я в ш аблон без перезапуска при лож ения. П ри вклю ченном кэш ировании обращ ение к диску происходит только один раз на шаблон. И так, мы показали, как механизм кэш ирования представлений способствует повы ­ ш ению бы стродействия прилож ения (кром е среды разработки). Теперь посмотрим, как Express находит представления д ля визуализации. 162 Глава 6. Connect и Express Запрос Кэширование отключено Запрос Кэширование включено Рис. 6.9. Кэширование представлений Поиск представлений П роцесс поиска представлений проходит по тому же принципу, что и д ля ф ункции N ode r e q u i r e ( ) . П ри вы зове r e s .r e n d e r ( ) или a p p .re n d e r( ) Express сначала п ро­ веряет, сущ ествует ли ф айл по абсолю тном у пути. З атем Express проводит поиск относительно каталога views. Н аконец, Express проверяет ф айл index. Н а рис. 6.10 этот процесс изображ ен в виде блок-схемы. П оскольку e j s назначается ядром по ум олчанию , при вы зове re n d e r расш ирение .ejs будет опущ ено, и ф айл ш аблона будет разреш ен правильно. П о мере эволю ции при лож ения вам понадобятся новые представления, а иногда несколько представлений для одного ресурса. И спользование поиска представлений 6.2. Express 163 Рис. 6.10. Процесс поиска представлений Express упрощ ает организацию: например, вы можете использовать подкаталоги, связанные с ресурсами, и создавать представления в этих подкаталогах. Д обавление подкаталогов позволяет устранить избы точны е части имен (например, edit-entry.ejs и show-entry.ejs). Тогда Express добавляет расш ирение я д р а ш аблонов и связы вает r e s . r e n d e r ( 'e n t r i e s / e d i t ') с ./views/entries/edit.ejs. E xpress проверяет, сущ ествует ли ф ай л с им енем index в подкаталогах каталога п ред ставл ен и й . П р и с в а и в а н и е ф ай л ам им ен ресурсов в м н ож ествен н ом числе (наприм ер, entries) обы чно подразум евает список ресурсов. Э то означает, что вы мож ете использовать вы зов r e s . r e n d e r ( 'e n t r i e s ') для вы полнения визуализации ф айла views/entries/index.ejs. Методы передачи данных представлениям Вы уж е знаете, как передавать локальны е переменны е непосредственно вызовам r e s .r e n d e r ( ) , но д л я этой цели такж е м ож но использовать и другие механизмы . Н априм ер, a p p .lo c a ls д ля перем енны х уровня п ри лож ения и r e s . l o c a l s для л о ­ кальны х перем енны х уровня запроса, которы е обычно задаю тся промеж уточны м и 164 Глава 6. Connect и Express ком п о н ен там и перед итоговы м м етодом о бработки м арш рутов, где происходит ви зу ал и зац и я представлений. Зн ач ен и я , переданны е непосредственно r e s .r e n d e r ( ) , обладаю т более вы соким приоритетом , чем значения, заданны е в r e s . l o c a l s и a p p .lo c a ls (рис. 6.11). Рис. 6.11. Значения, напрямую передаваемые функции render, имеют более высокий приоритет при визуализации шаблона П о ум олчанию E xpress откры вает п р ед ставлени ям доступ только к одной п ер е­ м енной у р о в н я п р и л о ж ен и я s e t t i n g s ; она п ред ставл яет собой объект со всеми значениям и, заданны м и вы зовами a p p .s e t( ) . Например, при использовании вызова a p p . s e t ( ' t i t l e ' , 'My A p p lic a tio n ') ш аблону предоставляется значение s e t t i n g s . t i t l e , как показано в следую щ ем ф рагм енте EJS: <html> <head> <title><%= s e t t i n g s .t i t l e %></title> </head> <body> 6.2. Express 165 <h1><%= s e t t i n g s .t i t l e %></h1> <p>Welcome to <%= s e t t i n g s .t i t l e %>.</p> </body> Во внутренней р еализаци и Express доступ к объекту предоставляется следую щ им кодом JavaS crip t: a p p .lo c a ls.se ttin g s = a p p .settin g s; Вот и все! После знакомства с визуализацией представлений и передачей им данных мы переходим к определению м арш рутов и написанию обработчиков марш рутов, которы е вы полняю т визуализац ию представлений для прилож ения. Такж е в этом разделе будут созданы м одели базы данны х д ля долгосрочного хранени я и н ф о р ­ мации. 6.2.4. Знакомство с маршрутизацией в Express Главная задача марш рутов Express — связать схемы U R L c логикой ответа. О днако марш руты такж е могут связать схему U R L с пром еж уточны м и компонентами. Это позволяет вам использовать пром еж уточны е ком поненты д ля назн ачени я м ного­ кратно используем ой ф ункц иональн ости некоторы м марш рутам. В этом разделе рассм атриваю тся следую щ ие темы: О проверка введенны х данны х с использованием пром еж уточны х компонентов, специ ф и чески х д ля марш рута; О реал и зац и я проверки данны х, специф ической для марш рута; О реал и зац и я страничной работы с данными. Рассм отрим некоторы е способы использования специализированны х промеж уточ­ ны х компонентов, связанны х с конкретны м марш рутом. Проверка данных, введенных пользователем Ч тобы у вас бы ли дан ны е д л я проверки, мы н аконец-то реал и зуем возм ож ность п у б л и к ац и и зап и сей в наш ем п р и ло ж ен и и . Д л я этого необходи м о р еш и ть ряд задач: О создать м одель данных; О добавить м арш руты , связанны е с записью; О создать ф орм ы д ля ввода; О добави ть л о ги к у со зд ан и я зап и сей с и сп о льзован и ем данны х, введен ны х на форме. Н ачнем с создания м одели записи. 166 Глава 6. Connect и Express Создание модели записи П реж де чем д в и гаться дальш е, необходим о у стан ови ть в проекте м одул ь N ode redis. М одуль устан авли вается ком андой npm i n s t a l l - -sav e re d is . Е сли у вас еще не установлен м одуль Redis, посетите сайт http://redis.io/ и просм отрите и н струк­ ции по его установке; если вы работаете в m acO S, модуль легко устанавливается из H om ebrew (http://brew.sh/), а в W indow s используется пакет Redis C hocolatey ( https://chocolatey.org/ ). М ы в о сп о л ьзу ем ся Redis, чтобы нем ного упрости ть задачу: ф у н кц и он ал ьн ость R edis и ES6 упрощ ает создан ие облегченны х м оделей без слож н ой библи отеки баз дан ны х. Е сл и вы не ищ ете л егк и х путей, и с п о л ьзу й те д ругую б и б л и о тек у баз дан ны х (за и н ф о р м ац и ей об и сп о льзо ван и и баз дан ны х в N ode обращ айтесь к главе 8). П осмотрим, как создать облегченную м одель д ля хранени я записей чата. Создайте ф айл models/entry.js для определения модели записи. Добавьте в ф айл код из листин­ га 6.7. М одель запи си будет представлять собой простой класс ES6 для сохранения данны х в списке Redis. Листинг 6.7. Модель для записей const red is = r e q u ir e ( 'r e d is ') ; const db = re d is .c re a te C lie n t(); class Entry { constructor(obj) { for ( le t key in obj) { •<-------th is[k ey ] = obj[key]; -<------} } Создает экземпляр клиента Redis. Перебирает ключи в переданном объекте. Объединяет значения. save(cb) { const entryJSON = JS O N .stringify(this); -<— db.lpush( -<-------- Сохраняет строку JSON в списке Redis. 'e n t r i e s ', entryJSON, (e rr) => { i f (e rr) return c b (err); cb(); } ); } Преобразует сохраненные данные записей в строку JSON. } module.exports = Entry; П осле создания базовой модели следует добавить ф ункцию getRange (л и сти н г 6.8). Э та ф у н кц и я предназначена д ля вы борки записей. 6.2. Express 167 Листинг 6.8. Логика выборки диапазона записей class Entry { Функция Redis Irange используется s ta tic getRange(from, to , cb) { для выборки записей. d b .lr a n g e ( 'e n tr ie s ', from, to , (e rr, items) => { i f (e rr) return cb (err); l e t e n trie s = []; item s.forEach((item ) => { entries.push(JSO N .parse(item )); ■< Декодирует записи, ранее }); сохраненные в формате JSON. cb(null, e n trie s ); }); } } П осле со зд ан и я м одели м ож н о добави ть м арш руты д л я со зд ан и я и п ол уч ен и я списка записей. Создание формы для ввода записей П рилож ение вы водит список записей, но пока не умеет добавлять их. Э та возм ож ­ ность будет добавлен а сейчас, н ач и н ая с вкл ю чен и я следую щ их строк в раздел r o u tin g ф ай л а app.js: a p p .g e t( '/p o s t', en tries.fo rm ); a p p .p o s t( '/p o s t', en tries.su b m it); Затем добавьте следую щ ий м арш рут в ф айл routes/entries.js. Л оги ка м арш рута з а ­ полняет ш аблон, содерж ащ ий форму: exports.form = (req, res) => { r e s .r e n d e r ('p o s t', { t i t l e : 'P o st' }); }; Затем ш аблон EJS в листинге 6.9 создает ш аблон для формы и сохраняет его в views/ post.ejs. Листинг 6.9. Форма для ввода сообщения <!DOCTYPE html> <html> <head> <title><%= t i t l e %></title> <link re l= 's ty le s h e e t' h re f = '/s ty le s h e e ts /s ty le .c s s ' /> </head> <body> <% include menu %> <h1><%= t i t l e %></h1> <p>Fill in the form below to add a new post.</p> fo rm a c tio n = '/p o s t' method='post'> <p> <input ty p e = 'te x t' n a m e = 'e n try [title]' p laceh o ld er= 'T itle' /> Текст заголовка записи. 168 Глава 6. Connect и Express </p> <p> <textarea name='entry[body]' placeholder='Body'></textarea> </p> <p> <input type='subm it' value='P ost' /> </p> </form> </body> </html> Текст тела записи. В ф орм е использую тся такие имена, как e n t r y [ t i t l e ] , поэтому потребуется рас­ ш иренны й разбор тела сообщ ения. Ч тобы сменить парсер тела сообщ ения, откройте ф айл app.js и перейдите к строке app.use(bodyParser.urlencoded({ extended: fa ls e })); П риведите ее к следую щ ему виду, чтобы использовать расш иренны й разбор тела сообщ ения: app.use(bodyParser.urlencoded({ extended: tru e })); Р азобравш и сь с отображ ением ф орм ы , перейдем к созданию записей по отп рав­ ленны м данны м формы. Реализация создания записи Ч тобы добавить возм ож ность создания записей по отправленны м данны м формы, добавьте логику из листинга 6.10 в ф айл routes/entries.js. Листинг 6.10. Добавление записи по отправленным данным формы Берется из exports.subm it = (req, re s, next) => { name=”entry[...]” в форме. const data = req.body.entry; ■<— const user = re s .lo c a ls .u s e r; Промежуточный const username = user ? user.name : null; компонент для загрузки const entry = new Entry({ пользователей будет username: username, добавлен в листинге 6.28. t i t l e : d a t a .t i t l e , body: data.body }); e n try .sa v e ((e rr) => { i f (e rr) return n e x t(e rr); r e s . r e d i r e c t ( '/ ') ; }); }; Теперь при обращ ени и к м арш руту / p o s t в п р и лож ен и и вы см ож ете добавлять записи. П роблем а обязательного входа в прилож ение перед созданием сообщ ений будет реш ена в листинге 6.21. 6.2. Express 169 Разобравш ись с созданием контента, перейдем к следую щ ей задаче — построению списка записей. Добавление списка записей Создайте ф айл routes/entries.js. Добавьте код из листинга 6.11 для вклю чения модели запи си и экспортирования ф ун кц и и д ля вы вода списка записей. Листинг 6.11. Вывод списка записей const Entry = re q u ire ('../m o d e ls /e n try '); e x p o r ts .lis t = (req, re s, next) => { Entry.getRange(0, -1, ( e rr, e n trie s) => { < -------i f (e rr) return n e x t(e rr); r e s .r e n d e r ( 'e n tr ie s ', { -<-------Строит ответ HTTP. t i t l e : 'E n tr ie s ', e n trie s : e n trie s , }); }); }; Получает записи. П осле определения л огики м арш рутов необходимо добавить ш аблон EJS д ля их отображ ения. С оздайте в каталоге views ф айл с именем entries.ejs и поместите в него код EJS, представленны й в листинге 6.12. Листинг 6.12. Представление entries.ejs <!DOCTYPE html> <html> <head> <title><%= t i t l e %></title> <link re l= 's ty le s h e e t' h re f = '/s ty le s h e e ts /s ty le .c s s ' /> </head> <body> <% include menu %> <% en tries.fo rE ach ((en try ) => { %> <div class= 'en try '> <h3><%= e n t r y .ti t l e %></h3> <p><%= entry.body %></p> <p>Posted by <%= entry.username %></p> </div> <% }) %> </body> </html> Перед запуском прилож ения вы полните команду touch views/menu. e js для создания временного ф айла, которы й будет содерж ать меню на более поздней стадии. Когда п редставления и марш руты будут готовы, необходимо сообщ ить прилож ению , где искать марш руты. 170 Глава 6. Connect и Express Добавление маршрутов для работы с записями П реж де чем добавлять в прилож ение м арш руты, относящ иеся к работе с за п и ся ­ ми, необходимо внести изм ен ен ия в app.js. С начала добавьте следую щ ую команду re q u ir e в начало ф ай л а app.js: const e n trie s = r e q u ir e ( './r o u te s /e n tr ie s ') ; Затем такж е в ф айле app.js приведите строку с текстом a p p . g e t ( '/ ' к следую щ ему виду, чтобы для лю бы х запросов к пути / возвращ ался список записей: a p p .g e t ( '/ ', e n t r i e s .l i s t ) ; Теперь при запуске п р и лож ения на главной странице вы водится список записей. Разобравш ись с созданием и вы водом списка записей, перейдем к использованию пром еж уточны х ком понентов, п р и вязан н ы х к конкретны м м арш рутам , д ля п ро­ верки данны х форм. Использование специализированных промежуточных компонентов П редполож им , вы хотите, чтобы текстовое поле на ф орм е ввода было об язател ь­ ны м для заполнения. П ервое, что приходит в голову, — добавить проверку прям о в обратны й вы зов м арш рута, как в следую щ ем ф рагменте. О днако такое реш ение не идеально, потому что логика проверки тесно связы вается с конкретной формой. Во м ногих случаях ло ги ка проверки м ож ет быть абстрагирована в компонентах, пригодны х д ля повторного использования; такое реш ение упростит и ускорит р аз­ работку, а код станет более декларативны м : exports.subm it = (req, re s, next) => { le t data = req.body.entry; i f ( ! d a t a .t i tl e ) { r e s .e r r o r ( 'T itle is r e q u ire d .'); r e s .r e d ir e c t( 'b a c k ') ; return; } i f ( d a ta .title .le n g th < 4) { r e s .e r r o r ( 'T itle must be longer than 4 c h a ra c te rs .'); r e s .r e d ir e c t( 'b a c k ') ; return; } М арш руты E xpress м огут получать пром еж уточны е ком поненты , при м ен яем ы е только при сопоставлении этого марш рута, перед заверш аю щ им обратным вызовом маршрута. С ами обратные вы зовы марш рутов во всех промежуточных компонентах работаю т одинаково — даж е в тех, которы е мы собираемся создать для проверки данных! 6.2. Express 171 Н ачнем с простого, но недостаточно гибкого способа реализаци и проверки данны х в ф орм е специ али зированн ы х пром еж уточны х компонентов. Проверка данных формы с использованием специализированных промежуточных компонентов П ервы й вариант — написание нескольких простых, но специализированны х пром е­ ж уточны х ком понентов д ля вы полнен ия проверки данных. Р асш ирение марш рута POST / p o s t с таким и ком понентам и м ож ет вы глядеть так: a p p .p o s t( '/p o s t', requireE ntryT itle, requireEntryTitleLengthAbove(4), entries.subm it ); О братите внимание: это определение м арш рута, которое обычно получает в аргу­ ментах только путь и логику м арш рутизации, имеет два дополнительны х аргумента с пром еж уточны м и ком понентам и проверки данных. Д ва примера компонентов в листинге 6.13 показывают, как абстрагируется исходная логика проверки данных. Тем не менее такие компоненты все равно нельзя назвать м одульны м и; они работаю т только д ля одного поля e n t r y [ t i t l e ] . Листинг 6.13. Две несовершенные попытки создания промежуточных компонентов проверки данных function requireE n try T itle(req , re s, next) { const t i t l e = re q .b o d y .e n try .title ; if (title ) { next(); } else { r e s .e r r o r ( 'T itle is r e q u ire d .'); r e s .r e d ir e c t( 'b a c k ') ; } } function requireEntryTitleLengthAbove(len) { return (req, re s, next) => { const t i t l e = re q .b o d y .e n try .title ; i f ( title .le n g th > len) { n ext(); } else { r e s .e r r o r ( 'T itle must be longer than $ { le n } .'); r e s .r e d ir e c t( 'b a c k ') ; } }; } Б олее уни версальн ое реш ение — абстрагировани е логи ки проверки с передачей им ени целевого поля. П осмотрим, как это делается. 172 Глава 6. Connect и Express Построение гибких промежуточных компонентов проверки данных Р еш ение с передачей им ени поля продем онстрировано в следую щ ем фрагменте. О но позволяет повторно использовать логику проверки данных и уменьш ить объем кода, которы й вам потребуется написать: a p p .p o s t( '/p o s t', v a lid a te .r e q u ir e d ( 'e n tr y [ title ] ') , v a lid a te .le n g th A b o v e ('e n try [title ]', 4), en tries.su b m it); Зам ени те строку a p p . p o s t ( '/ p o s t ', e n t r i e s .s u b m it) ; в разделе ro u tin g ф айла app. этим ф рагм ентом . С тоит зам етить, что сообщ ество Express разработало много аналогичны х библиотек для всеобщ его использования; тем не менее очень п ол ез­ но поним ать, как работаю т пром еж уточны е ком поненты проверки данны х и как реализовать их самостоятельно. js С оздайте ф айл с именем ./middleware/validate.js и вклю чите в него код из л и ст и н ­ га 6.14. В ф айл validate.js мы экспортируем несколько пром еж уточны х ком п онен­ тов — в данном случае v a lid a te .r e q u ir e d ( ) и v a lid a te . lengthA bove(). П одробности р еал и зац и и не важ ны ; суть данного при м ера закл ю чается в том, что небольш ие дополнительны е усилия значительно расш иряю т возможность использования кода в разны х частях прилож ения. Листинг 6.14. Реализация промежуточных компонентов проверки данных function p a rse F ie ld (fie ld ) { < -------Разбирает синтаксис entry[name]. return fie ld .s p l i t ( / \ [ |\ ] / ) . f i l t e r ( ( s ) => s); } function getF ield (req , fie ld ) { < -------Ищет свойство на основании результатов parseField(). le t val = req.body; field.forE ach((prop) => { val = val[prop]; }); return val; } exports.required = (fie ld ) => { fie ld = p a rse F ie ld (fie ld ); < -------Разбирает поле. return (req, re s, next) => { i f (g etF ield(req , f ie ld ) ) { <-------- При каждом запросе проверяет, содержит ли поле значение. nex t(); < -------Если содержит, происходит переход кследующему промежуточному компоненту. } else { r e s .e r r o r ( '$ { f ie ld .jo in ( ' ')} is re q u ire d '); <---- Если не содержит, выдается ошибка. r e s .r e d ir e c t( 'b a c k ') ; } }; }; exports.lengthAbove = ( f ie ld , len) => { fie ld = p a rse F ie ld (fie ld ); 6.2. Express 173 return (req, re s, next) => { i f (g etF ield (req , fie ld ).le n g th > len) { n ext(); } else { const fie ld s = f i e l d .j o i n ( ' ') ; re s .e rro r('$ { fie ld s } must have more than ${len} c h a ra c te rs'); r e s .r e d ir e c t( 'b a c k ') ; } }; }; Ч тоб ы п р о м еж у точ н ы й ко м п о н ен т стал до ступ н ы м д л я п р и л о ж ен и я, добавьте следую щ ую строку в начало ф айла app.js: const v alid ate = re q u ire ('./m id d le w a re /v a lid a te '); Е сли вы теперь опробуете прилож ение, то увидите, что проверка данны х успеш но действует. A P I п роверки дан ны х м ож но сделать ещ е более д инам ичны м , но мы оставим вам эту задачу д ля сам остоятельной работы. 6.2.5. Аутентификация пользователей В этом разд ел е мы создадим систем у а у тен ти ф и кац и и д ля п р и л о ж ен и я с нуля. П роцесс будет состоять из следую щ их шагов: О реал и зац и я логики д ля хранени я данны х и аутен ти ф и каци и зарегистрирован­ ны х пользователей; О добавление ф ункц иональн ости регистрации учетны х записей; О реал и зац и я входа в прилож ение; О создание и исп ользован ие пром еж уточного ком понента д ля загрузки п ол ьзо­ вателей. Д л я реализации учетны х записей пользователей такж е будет использоваться Redis. Теперь посмотрим, как создать модель пользователя для упрощ ения работы с Redis в коде Node. Сохранение и загрузка записей пользователей В этом разделе мы реализуем загрузку, сохранение и аутентиф икацию пользовате­ лей. Здесь будут реш ены следую щ ие задачи: О определение зависим остей при лож ения с использованием ф айла package.json; О создание м одели пользователя; О добавление л огики загрузки и сохранения данны х пользователей с и сп ользо­ ванием Redis; 174 Глава 6. Connect и Express О защ ита паролей с использованием bcrypt; О добавление логики аутен ти ф и каци и д ля попы ток входа. b c ry p t — ф у н кц и я х еш и рования с затравкой (salt), доступная в виде стороннего модуля, спроектированного специально для хеш ирования паролей. bcrypt особенно хорош о подходит д ля паролей из-за аргум ента со счетчиком итераций, зам едл яю ­ щего попы тки подбора. П реж де чем следовать дальш е, добавьте b c ry p t в проект shoutbox: npm in s ta l l --save redis bcrypt Создание модели пользователя Теперь нуж но создать м одель пользователя. С оздайте ф айл с именем user.js в к а ­ талоге models/. В листинге 6.15 приведена м одель пользователя. В этом коде вклю чаю тся зав и си ­ м ости redis и b crypt, после чего вы зов r e d i s . c r e a t e C l i e n t ( ) откры вает п одклю ­ чение Redis. Ф у н к ц и я User получает объект и объединяет свойства этого объекта со своими. Н апример, new U ser({ name: 't o b i ' }) создает объект и задает свойству name этого объекта значение Tobi. Листинг 6.15. Начало работы над созданием модели пользователя const red is = r e q u ir e ( 'r e d is ') ; const bcrypt = re q u ire ('b c ry p t'); const db = re d is .c re a te C lie n t(); < -------Создает долгосрочное подключение Redis. class User { constructor(obj) { for ( le t key in obj) { < -------Перебирает свойства переданного объекта. th is[k ey ] = obj[key]; •<-------Задает каждое свойство в текущем классе. } } } module.exports = User; < -------Экспортирует класс User. Н а д ан ны й м ом ент м одель п о л ьзо вател я — не более чем заглуш ка. Н еобходим о д обавить м етоды д л я со зд ан и я и о б н о вл ен и я зап и сей с п о л ьзовател ьски м и д ан ­ ными. Сохранение данных пользователя в Redis С ледую щ и й блок ф у н кц и о н ал ьн о сти , которы й нам понадобится, — сохранение данны х пользователя в Redis. М етод save из листинга 6.16 проверяет, сущ ествует ли идентиф икатор пользователя, и если да — вы зы вает метод update, индексируя идентиф икатор пользователя по им ени и зап ол н яя хеш Redis свойствам и объекта. В противном случае пользователь, не им ею щ ий идентиф икатора, считается новым 6.2. Express 175 пользователем ; зн ач ен и е u s e r : i d s у велич ивается, чтобы п ользователь получил уни кальны й идентиф икатор, а пароль хеш ируется перед сохранением в R edis тем ж е методом update. Д обавьте код из листинга 6.16 в ф айл models/user.js. Листинг 6.16. Обновление записей пользователей class User { / / ... save(cb) { i f ( th is .id ) { •<-------Если идентификатор определен, то пользователь уже существует. th is.u p d ate(cb ); } else { d b .in c r ( 'u s e r :id s ', (e rr, id) => { < -------Создает уникальный идентификатор. i f (e rr) return cb (err); t h i s .id = id; •<-------Задает идентификатор для сохранения. this.hashPassw ord((err) => { ■<-------- Хеширует пароль. i f (e rr) return c b (err); th is.u p d a te (c b ); < -----Сохраняет свойства пользователей. }); }); } } update(cb) { const id = t h i s .id ; d b .se t('u se r:id :$ { th is.n a m e } ', id , (e rr) => { — Индексирует пользователей по имени. i f (e rr) return cb (err); d b .h m set('u ser:$ { id }', th is , (e rr) => { -<Использует Redis для хранения свойств c b (err); текущего класса. }); }); } Защита паролей П ри создании пользователя свойству .p a s s при сваи вается пароль пользователя. З атем л огика сохранения пользователей зам еняет значение свойства . pass хешем, сгенерированны м с использованием пароля. К хеш у при м ен яется затравка (salt). П рим енение затравки на уровне п ользовате­ лей помогает защ ититься от атак на базе радуж ны х таблиц: затравка играет роль закры того клю ча для м еханизм а хеш ирования. Вы мож ете воспользоваться bcrypt д ля генерирования 12-сим вольной затравки д ля хеш а методом g e n S a lt() . АТАКИ НА БАЗЕ РАДУЖНЫХ ТАБЛИЦ Атаки на базе радужных таблиц пытаются взломать хешированные пароли по зара­ нее вычисленным таблицам. С этой темой можно ознакомиться в Википедии: https:// ru.wikipedia.org/wiki/Радужная_таблица. 176 Глава 6. Connect и Express П осле того как затравка будет сгенерирована, вы зы вается ф ун кц и я b c r y p t.h a s h (), которая хеш ирует свойство .p a s s и затравку. П олученное значение hash заменяет свойство .p a s s , после чего вы зов .u p d a te ( ) сохраняет его в Redis; это делается д ля того, чтобы в базе данны х пароли не сохранялись в текстовом виде — только в виде хеша. Л истин г 6.17, которы й следует добавить в models/user.js, определяет функцию , кото­ рая создает хеш с затравкой и сохраняет его в свойстве .p a ss объекта пользователя. Листинг 6.17. Добавление шифрования bcrypt в модель пользователя class User { // ... hashPassword(cb) { bcrypt.genSalt(12, (e rr, s a lt) => { -<-------Генерирует 12-символьнуюзатравку. i f (e rr) return cb (err); t h i s . s a l t = s a lt; < -------Задает затравкудля сохранения. b cry p t.h a sh (th is.p a ss, s a lt, (e rr, hash) => { < -------Генерирует хеш. i f (e rr) return c b (err); th is .p a s s = hash; < -------Присваивает хешдля сохранения update(). cb(); }); }); } } Вот и всё! Тестирование логики сохранения пользователя Ч то б ы п р о тести р о в ать м ех ан и зм с о х р ан ен и я п о л ьзо вател ей , зап у сти те сервер Redis ком андой r e d i s - s e r v e r в ком андной строке. Д обавьте код из листинга 6.18 в конец ф айла models/user.js. П осле этого вы полните ком анду node m o d e ls /u s e r .js в ком андной строке, чтобы создать тестового пользователя. Листинг 6.18. Тестирование модели пользователя const User = re q u ire ('./m o d e ls /u s e r'); const user = new User({ name: 'Example', pass: 't e s t ' }); ■<---- Создает нового пользователя. u se r.sa v e ((err) => { < -------Сохраняет пользователя. i f (e rr) c o n so le .e rro r(e rr); co n so le.lo g ('u ser id %d', u se r.id ); }); В ывод долж ен сообщ ать о том, что пользователь был успеш но создан: например, u s e r id 1. П осле тестирован ия м одели пользователя удалите код листинга 6.18 из ф ай л а models/user.js. В программе redis-cli, входящ ей в поставку Redis, мож но ввести команду HGETALL д ля вы борки всех клю чей и значений хеш а (л и сти н г 6.19). 6.2. Express 177 Листинг 6.19. Вывод информации в интерфейсе командной строки Redis $ r e d is - c li < -------- Запускает интерфейс командной строки Redis. redis> get u ser:id s -<-------Находит идентификатор последнего созданного пользователя. "1" redis> h g e ta ll user:1 -<-------Читает данные элемента. 1) "name" -<-------Свойства элемента. 2) "tobi" 3) "pass" 4 ) "$2a$12$BAOWThTAkN]Y7Uht0UdBku4 5) "age " 6) "2" 7) "id" g) "4 " 9 ) "s a lt" 1 0 ) "$2a$12$BAOWThTAkNjY7Uht0UdBku" redis> q u it < -------Завершает интерфейс командной строки Redis. П осле определения логики сохранения пользователя необходимо добавить логику загрузки инф орм ации. ДРУГИЕ КОМАНДЫ ПРОГРАММЫ REDIS-CLI За дополнительной информацией о командах Redis обращайтесь к справочнику команд Redis по адресу http://redis.io/com m ands. Чтение данных пользователя Когда пользователь пы тается вы полнить вход в веб-прилож ение, он обычно вводит им я и пароль в форму; введенны е данны е отправляю тся прилож ению д ля аутен­ тиф икац ии. П осле того как ф орм а входа будет отправлена, необходимо каким -то образом произвести вы борку данны х пользователя по имени. Э та ло ги ка о п р ед ел яется в л и сти н ге 6.20 в м етоде U ser.getB yN am e(). Ф у н к ц и я сначала определяет идентиф икатор методом U s e r .g e tI d ( ) , а затем передает н ай ­ денны й идентиф икатор методу U ser. g e t ( ) . Этот метод получает данны е хеш а этого пользователя от Redis. Д обавьте следую щ ие методы в ф айл models/user.js. Листинг 6.20. Получение данных пользователя от Redis class User { / / ... s ta tic getByName(name, cb) { User.getId(name, ( e rr, id) => { < -------- Определяет идентификатор пользователя по имени. i f (e rr) return cb (err); U ser.g et(id , cb); < -------Получает данные пользователя по идентификатору. }); } s ta tic getId(name, cb) { d b .g et('u ser:id :$ {n am e}', cb); < -------- Получает идентификатор индексированием по имени. 178 Глава 6. Connect и Express } s ta tic g e t(id , cb) { d b .h g e ta ll('u s e r:$ { id } ', (e rr, user) => { < ----- Получает данные в виде простого объекта. i f (e rr) return cb (err); c b (n u ll, new U ser(user)); < -------Преобразует простой объект в новый объект User. }); } } Если вам потребуется получить данные пользователя, используйте код следующего вида: User.getByName('tobi', (e rr, user) => { console.log(user); }); П осле п о л у ч ен и я хеш ированного п ар о л я м ож но переходить к аутен ти ф и кац и и пользователя. Аутентификация входа пользователя П оследний компонент, необходим ы й для аутен ти ф и каци и пользователя, — метод, определенны й в листинге 6.21. О н использует определенны е ранее ф ун кц и и для получения данны х пользователя. Добавьте логику из листинга в ф айл models/user.js. Листинг 6.21. Аутентификация пользователя s ta tic authenticate(nam e, pass, cb) { User.getByName(name, (e rr, user) => { -<-------Проводит поиск пользователя по имени. i f (e rr) return cb (err); i f (!u s e r.id ) return cb(); -<-------Пользователь не существует. bcrypt.hash(pass, u s e r .s a lt, ( e rr, hash) => { < -------- Хеширует введенный пароль. i f (e rr) return cb (err); i f (hash == user.pass) return cb (n u ll, user); < -------Обнаружено совпадение. cb(); < -------Неверный пароль. }); }); } Л огика аутентификации начинается с вы борки данных пользователя по имени. Если пользователь не найден, нем едленно активи зируется ф ун кц и я обратного вызова. В противном случае сохраненная затравка и введенны й пароль хеш ирую тся для получения результата, которы й долж ен совпадать с храним ы м хеш ем u s e r .p a s s . Е сли вы чи слен ны й хеш не совпадает с хранимы м, значит, пользователь ввел н е­ верны е данные. П ри поиске по несущ ествую щ ем у клю чу Redis возвращ ает пустой хеш; вот почему вместо !u s e r используется ! u s e r .id . И так, аутен ти ф и каци я заработала; теперь необходимо позаботиться о том, чтобы пользователи м огли регистрироваться в прилож ении. 6.2. Express 179 6.2.6. Регистрация новых пользователей Ч тобы пользователь мог создать учетную запись, а затем вы полнить вход, п р и л о ­ ж ение долж но реализовать ф ункциональность регистрации и входа. Д л я р еализаци и регистрации необходимо реш ить следую щ ие задачи: О связать м арш руты регистрации и входа с путям и URL; О добавить логику д ля отображ ения ф орм ы регистрации; О добавить логику д ля хранения пользовательских данных, отправленны х с ф о р ­ мой. Ф орм а изображ ена на рис. 6.12. Э та ф орм а отображ ается при посещ ении м арш рута / r e g i s t e r в браузере. П озднее мы создадим аналогичную ф орм у для вы полнен ия входа. Register Fill in the form below to sign up! [ Usemam* I Password ( SignUp I Рис. 6.12. Форма регистрации пользователя Добавление маршрутов регистрации Ч тобы ф орм а регистрации появилась на экране, необходимо создать марш рут для визуал и зац и и ф орм ы и вернуть ее браузеру для вывода. В листинге 6.22 показано, как следует изм енить ф айл app.js. С истем а м одулей Node используется для им п ортирования модуля, определяю щ его поведение м арш рута из каталога марш рутов, а методы H T T P и пути U R L связы ваю тся с ф ун кц и ям и м арш рутизац ии. Так ф ор м и р у ется некое подобие контроллера. К ак вы увидите, м арш руты регистрации сущ ествую т как д ля м етода GET, так и для PO ST. Листинг 6.22. Добавление маршрутов регистрации const re g is te r = r e q u ir e ( './r o u te s /r e g is te r ') ; < -------- Включает логику маршрутизации. a p p .g e t( '/r e g is te r ', re g iste r.fo rm ); ■<-------a p p .p o s t( '/r e g is te r ', re g iste r.su b m it); Добавляет маршруты. 180 Глава 6. Connect и Express Ч тобы определить логику марш рута, создайте в каталоге routes пустой ф айл с им е­ нем register.js. Н ачн ите определение поведения м арш рута регистрации с эк спорти­ ровани я следую щ ей ф ун кц и и из routes/register.js — м арш рута, которы й вы полняет визуализац ию ш аблона регистрации: exports.form = (req, res) => { r e s .r e n d e r ( 'r e g is te r ', { t i t l e : 'R e g iste r' }); }; Д л я определения H T M L -разметки формы регистрации марш рут использует шаблон EJS, которы й будет создан на следую щ ем шаге. Создание формы регистрации Ч тобы определить H T M L -разм етку ф орм ы регистрации, создайте в каталоге views ф айл с именем register.ejs. Разм етка H T M L /E JS для определения формы приведена в листинге 6.23. Листинг 6.23. Шаблон представления с формой регистрации <!DOCTYPE html> <html> <head> <title><%= t i t l e %></title> <link r e l= 's ty le s h e e t' h r e f = '/s ty le s h e e ts /s ty le .c s s ' /> </head> <body> <% include menu %> < -------Навигационные ссылки будут добавлены позднее. <h1><%= t i t l e %></h1> <p>Fill in the form below to sign up!</p> <% include messages %> ■<-------Вывод сообщений будет добавлен позднее. <form a c tio n = '/re g is te r' method='post'> <p> <input ty p e = 'te x t' name='user[name]' placeholder='Username' /> -<-----</p > Пользователь должен ввести <p> имя пользователя. <input type='password' name='user[pass]' placeholder='Password' /> -<-------Пользователь должен ввести пароль. </p> <p> <input type='subm it' value='Sign Up' /> </p> </form> </body> </html> О братите вним ание на директиву in c lu d e messages, которая вклю чает в себя другой ш аблон: m essages.ejs. Этот ш аблон, которы й будет определен на следую щ ем шаге, используется д ля взаим одействия с пользователем. 6.2. Express 181 Организация обратной связи с пользователем Во врем я регистрац ии пользователей, как и во м ногих других ч астях типичного прилож ения, возникает необходимость в передаче обратной связи пользователю . Н апример, пользователь м ож ет попы таться зарегистрироваться с именем, которое уж е занято кем -то другим. В этом случае необходимо предлож ить пользователю вы брать другое имя. В п р и л о ж ен и и д л я вы во д а ош ибок будет и сп о л ьзо ваться ш аблон m essages.ejs. М ногие ш аблоны наш его при лож ения будут вклю чать messages.ejs. Чтобы создать шаблон сообщений, создайте в каталоге views файл с именем messages. ejs и пом естите в него код из приведенного ниж е фрагмента. Л оги ка ш аблона п ро­ веряет, задано ли значение переменной l o c a l s . messages. Если оно задано, то шаблон перебирает ее содерж им ое и вы водит объекты сообщ ений. У каж дого объекта со­ общ ения им еется свойство ty p e (позволяю щ ее при необходимости использовать сообщ ения для оповещ ений, не явл яю щ и х ся ош ибкам и) и свойство s t r i n g (текст сообщ ения). Л о ги ка при лож ения ставит ош ибку в очередь д ля вывода, добавляя ее в массив r e s .lo c a ls .m e s s a g e s . П осле того как сообщ ения будут вы ведены, в ы ­ зы вается метод removeMessages д ля очистки очереди сообщений: <% i f (locals.m essages) { %> <% messages.forEach((message) => { %> <p class='<%= message.type %>'><%= m essage.string %></p> <% }) %> <% removeMessages() %> <% } %> Н а рис. 6.13 изображ ена ф орм а регистрации при вы воде сообщ ения об ошибке. Register Fill In the form below to sign up! Username already taken! I Password I I a»nup I Рис. 6.13. Вывод сообщений об ошибках на форме Д обавление сообщ ений в r e s .lo c a ls .m e s s a g e s создает простой м еханизм взаи м о­ действия с пользователем. О днако r e s . lo c a ls не сохраняется меж ду перенаправле­ ниям и, поэтом у необходимо продлить срок их сущ ествования, использовав сеансы д ля их сохранения м еж ду запросами. 182 Глава 6. Connect и Express Сохранение временных сообщений в сеансах В веб-прилож ениях прим еняется стандартны й паттерн проектирования P R G ( P o s t/ R e d ire c t/G e t). В этом паттерне пользователь запраш ивает форму, данны е формы отп р авл яю тся в виде запроса H T T P PO ST, а пользователь перен ап равляется на другую веб-страницу. Куда им енно перенап равляется пользователь — зависи т от результата проверки данны х прилож ением . Если данны е ф орм ы признаю тся д ей ­ ствительны м и, то пользователь перенаправляется на новую веб-страницу. Паттерн P R G преж де всего используется д ля предотвращ ения повторной отправки форм. В Express при перенаправлении пользователя содержимое r e s .lo c a ls сбрасывается. Е сли вы сохраняете сообщ ения д ля пользователя в r e s .l o c a l s , сообщ ения будут потеряны до того, как они появятся на экране. Проблему можно обойти сохранением сообщ ений в сеансовой переменной. П осле этого сообщ ения могут быть выведены на итоговой странице перенаправления. Ч тобы адаптировать возм ож ность постановки сообщ ений в очередь к сеансовой переменной, необходимо добавить в при лож ение модуль. С оздайте ф айл с именем ./middleware/messages.js и добавьте в него следую щ ий код: const express = re q u ire ('e x p re s s '); function message(req) { return (msg, type) => { type = type || 'in f o '; le t sess = req.session; sess.messages = sess.messages || []; sess.messages.push({ type: type, strin g : msg }); }; }; Ф ун кц и я r e s . message предоставляет возможность добавления в сеансовую перемен­ ную сообщ ений от лю бы х запросов Express. О бъект ex p ress .re sp o n se представляет собой прототип, которы й используется Express д ля объектов ответов. Д обавление свойств в объект означает, что они станут доступны м и д ля всех пром еж уточны х ком понентов и м арш рутов. В предш ествую щ ем ф рагм енте e x p r e s s .re s p o n s e п р и ­ сваивается переменной с именем res, чтобы упростить добавление свойств к объекту и сделать код более понятны м. Д л я р еализаци и этой возм ож ности потребуется поддерж ка сеансов. Д л я этого мы воспользуем ся E xpress-совм естимы м модулем: оф ици альн о поддерж иваем ы м п а ­ кетом express-session. Установите его командой npm i n s t a l l - -save e x p re s s -s e s s io n , а затем добавьте в app.js: const session = re q u ire ('e x p re ss-se ssio n '); app.use(session({ se c re t: 's e c r e t', resave: fa ls e , saveU ninitialized: true })); 6.2. Express 183 П ром еж у то чн ы й ко м п о н ен т лучш е р азм ести ть после в ставк и п ром еж уточного ком понента д ля работы с cookie (при близи тельн о около строки 26). Ч тобы еще больш е упростить добавление сообщ ений, используйте код из следую ­ щего ф рагмента. Ф у н к ц и я r e s . e r r o r позволяет легко добавить в очередь сообщ е­ ний сообщ ение типа e r r o r . И спользуйте ф ункцию res.m essage, которая ранее была определена в модуле: re s .e rro r = msg => this.message(msg, 'e r r o r ') ; О стается сделать последний шаг: предоставить ш аблонам доступ к этим сообщ е­ ниям д ля вы вода на экран. Если этого не сделать, придется передавать массив re q . s e s s io n .m e s s a g e s каж дом у вы зову r e s . r e n d e r ( ) в п ри лож ении ; конечно, такое реш ение вряд ли м ож но назвать идеальным. Д л я р еш ен и я проблем ы мы создадим пром еж уточны й ком понент, которы й для каж дого запроса будет заполнять массив re s .lo c a ls .m e s s a g e s данны м и из массива r e s .s e s s io n .m e s s a g e s , ф актически откр ы вая доступ к сообщ ениям д ля каж дого визуализируем ого ш аблона. П ока что ф айл ./lib/m essages.js расш и ряет прототип ответа, но ничего не экспортирует. Д обавление в ф айл следую щ его ф рагм ента эк с­ портирует необходим ы й вам пром еж уточны й компонент: module.exports = (req, re s, next) => { res.message = message(req); re s .e rro r = (msg) => { return res.message(msg, 'e r r o r ') ; }; res.locals.m essages = req.session.m essages | | []; res.locals.removeM essages = () => { req.session.m essages = []; }; next(); }; С н ач ал а о п р ед ел яется пер ем ен н ая ш аблона m essages д ля хран ен и я сообщ ений сеанса; это массив, которы й м ож ет сущ ествовать, а мож ет и не сущ ествовать после вы полнен ия предыдущ его запроса (н е забы вайте, что эти сообщ ения сохраняю тся меж ду сеансами). Затем нуж но придумать, как удалять сообщ ения из сеанса; в про­ тивном случае они будут неограниченно накапливаться. Остается лиш ь интегрировать этот новы й механизм ф ункцией re q u ire () в ф айл app. js. Этот компонент долж ен м онтироваться после ком понента сеанса, поскольку для него долж но быть определено свойство re q .s e s s io n . О братите внимание: поскольку этот пром еж уточны й ком понент спроектирован так, что он не получает параметры и не возвращ ает вторую ф ункцию , м ож но использовать вы зов a p p .u se(m essag e s) вместо a p p .u s e (m e s s a g e s ()). Д л я надеж ности в пром еж уточны х ком понентах от сторонних разработчиков лучш е использовать app.use(m essages()) вне зависимости от того, получает ком понент парам етры и ли нет: 184 Глава 6. Connect и Express const re g is te r = r e q u ir e ( './r o u te s /r e g is te r ') ; const messages = require('./m iddlew are/m essages'); app.use(express.m ethodOverride()); app.use(express.cookieP arser()); app.use(session({ se c re t: 's e c r e t', resave: fa ls e , saveU ninitialized: true })); app.use(messages); Теперь к перем енной m essages и ф ун кц и и rem oveM essages() м ож но обратиться из любого представления, поэтом у ф айл messages.ejs при вклю чении в лю бой ш аблон долж ен работать правильно. Разобравш ись с вы водом на экран ф орм ы регистрации и м еханизм ом передачи об­ ратной связи пользователю , перейдем к обработке регистрационной инф орм ации, отправленной формой. Регистрация пользователя М ы долж ны создать м арш рутную ф ункцию д ля обработки H T T P -запросов P O S T к /register. Э та ф у н кц и я будет назы ваться subm it. После отправки данны х форм ы промеж уточный компонент b o d y P a rse r() заполняет отправленны м и данны м и свойство req.body. В ф орм е регистрации используется объектны й синтаксис user[nam e], которы й после разбора преобразуется в свойство re q .b o d y .u se r.n a m e . А налогичны м образом д ля поля ввода пароля используется свойство re q .b o d y .u s e r .p a s s . Осталось добавить небольшой фрагмент кода к марш руту отправки, который должен обеспечивать проверку данных (например, проверку того, что им я пользователя еще не занято), и сохранить нового пользователя, как продемонстрировано в листинге 6.24. П осле заверш ения регистрации значение u s e r .id сохраняется в сеансе пользовате­ ля; позднее вы мож ете проверить его и убедиться в том, что пользователь прош ел аутентиф икацию . Е сли проверка проходит неудачно, сообщ ение предоставляется ш аблонам в виде перем енной m essages через массив r e s .lo c a ls .m e s s a g e s , а п о л ь­ зователь перенап равляется обратно к ф орм е регистрации. Ч тобы реализовать эту ф ункциональность, добавьте код из листинга 6.24 в ф айл routes/register.js. Листинг 6.24. Создание пользователя по отправленным данным const User = re q u ire ('../m o d e ls /u s e r'); exports.subm it = (req, re s, next) => { 6.2. Express }; 185 const data = req.body.user; User.getByName(data.name, (e rr, user) => { -<---- Проверяет имя пользователя на уникальность. i f (e rr) return n e x t(e rr); < -------- Передает ошибки подключения кбазе данных идругие ошибки. / / red is использует значение по умолчанию i f (u se r.id ) { •<-------Имя пользователя уже занято. res.error('U sernam e already ta k e n !'); r e s .r e d ir e c t( 'b a c k ') ; } else { user = new User({ < -------Создает пользователя на основе данных POST. name: data.name, pass: data.pass }); u se r.sa v e ((err) => { < -------Сохраняет нового пользователя. i f (e rr) return n e x t(e rr); req .sessio n .u id = u se r.id ; < -------Сохраняет uid для аутентификации. r e s . r e d i r e c t ( '/ ') ; < -------Перенаправляет на страницу со списком. }); } }); Теперь вы смож ете запустить прилож ение, перейти по м арш руту /register и зареги ­ стрировать пользователя. Следую щ ее, что потребуется, — м еханизм возвращ ения зарегистрированны х пользователей д ля аутен ти ф и каци и через ф орм у /lo g in . 6.2.7. Вход для зарегистрированных пользователей Д обавить функциональность входа еще проще, чем функциональность регистрации, поскольку больш ая часть нуж ной логики уж е реализована в определенном ранее универсальном методе аутентиф икации U s e r .a u th e n tic a te () . В этом разделе в наше при лож ение будут добавлены: О логика м арш рута д ля вы вода на экран ф орм ы входа; О логика аутен ти ф и каци и отправленны х из ф орм ы пользовательских данных. Ф орм а входа вы глядит так, как показано на рис. 6.14. Login Fill in the form below to sign in! ( Username I Password Рис. 6.14. Форма входа 186 Глава 6. Connect и Express Д л я начала изм еним ф айл app.js с вклю чением м арш рутов входа и назначением м арш рутны х путей: const login = r e q u ir e ( './r o u te s /lo g in ') ; a p p .g e t( '/lo g in ', login.form ); a p p .p o s t( '/lo g in ', login.subm it); a p p .g e t('/lo g o u t', lo g in .lo g o u t); Затем добавляется ф ункциональность для отображ ения ф орм ы входа на экране. Вывод на экран формы входа Р еал и зац и я ф орм ы входа начинается с создания ф айла д ля хранения м арш рутов входа и вы хода из прилож ения: routes/login.js. М арш рутная логика вы вода на экран ф орм ы входа практи чески не отли чается от логики вы вода ф орм ы регистрации; различаю тся только им я ш аблона и заголовок страницы: exports.form = (req, res) => { re s .r e n d e r ('lo g in ', { t i t l e : 'Login' }); }; Ф о р м а вх о д а E J S -ш аблона, к о т о р а я о п р ед ел ен а в ф а й л е ./view s/login.ejs (л и с ­ тинг 6.25), такж е очень похож а на ф орм у из ф айла register.ejs. Различаю тся только текст ин струкций и марш рут, по котором у отправляю тся данные. Листинг 6.25. Шаблон представления для формы входа <!DOCTYPE html> <html> <head> <title><%= t i t l e %></title> <link r e l= 's ty le s h e e t' h r e f = '/s ty le s h e e ts /s ty le .c s s ' /> </head> <body> <% include menu %> <h1><%= t i t l e %></h1> <p>Fill in the form below to sign in!</p> <% include messages %> <form a c tio n = '/lo g in ' method='post'> Пользователь должен ввести <p> имя пользователя. <input ty p e = 'te x t' name='user[name]' placeholder='Username' /> < ---</p> <p> <input type='password' name='user[pass]' placeholder='Password' /> < -------Пользователь должен ввести пароль. </p> <p> <input type='subm it' value='Login' /> 6.2. Express 187 </p> </form> </body> </html> П осле того как вы добавили м арш рут и ш аблон д ля вы вода ф орм ы входа на экран, в при лож ение нуж но добавить логику обработки попы ток входа. Аутентификация попыток входа Д л я о б р аб о т к и п о п ы то к в х о д а в с и с т е м у вам н у ж н о д о б а в и т ь в п р и л о ж ен и е марш рутную логику, которая проверяет им я пользователя и пароль, отправленные из ф орм ы . Е сли и м я п о л ьзо вател я и пароль верны , и д ен ти ф и к атор п о л ьзо вате­ л я п р и сваи вается сеансовой перем енной, после чего пользователь п ерен ап р авля­ ется на дом аш ню ю страницу. Д обавьте эту л оги ку (л и ст и н г 6.26) в ф ай л routes/ login.js. Листинг 6.26. Маршрут для обработки попыток входа const User = re q u ire ('../m o d e ls /u s e r'); exports.subm it = (req, re s, next) => { const data = req.body.user; U ser.authenticate(data.nam e, d ata.p ass, ( e rr, user) => { < ----- Проверяет учетные данные. i f (e rr) return n e x t(e rr); < ----- Делегирует обработку ошибок. i f (user) { < ----- Обрабатывает пользователя с действительными учетными данными. re q .sessio n .u id = u se r.id ; < ----- Сохраняет идентификатор пользователя для аутентификации. r e s . r e d i r e c t ( '/ ') ; < ----- Перенаправляет ксписку. } else { re s .e rro r('S o rry ! invalid c re d e n tia ls. ') ; < ----- Предоставляет сообщение об ошибке. r e s .r e d ir e c t( 'b a c k ') ; < ----- Перенаправляет обратно кформе входа. } }); }; Е сли пользователь проходит аутен ти ф и каци ю м етодом U s e r .a u th e n tic a te ( ) , то значение свойства r e q .s e s s i o n .u i d при сваи вается по тому ж е принципу, как и в случае с P O S T -м арш рутом / r e g i s t e r : это значение сохраняется в сеансе и может прим еняться в дальнейш ем для вы борки объекта User или других данных, связанных с пользователем . Е сли соответствие не обнаруж ивается, устан авли вается признак ош ибки, а ф орм а снова вы водится на экран. Н екоторы е п ользователи предпочитаю т яв н о заверш ать работу с прилож ением , поэтом у нуж но создать соответствую щ ую ссы лку в при лож ении . В ф айле app.js м арш рут создается следую щ им образом: const login = r e q u ir e ( './r o u te s /lo g in ') ; a p p .g e t('/lo g o u t', lo g in .lo g o u t); Глава 6. Connect и Express 188 З атем в ф айле ./routes/login.js следую щ ая ф у н к ц и я удали т сеанс, обнаруж енны й пром еж уточны м ком понентом s e s s io n ( ) , что приведет к установке сеанса д ля п о ­ следую щ их запросов: exports.logout = (req, res) => { re q .se ssio n .d e stro y ((e rr) => { i f (e rr) throw e rr; r e s . r e d i r e c t ( '/ ') ; }) }; П осле создания страниц регистрации и входа нуж но добавить меню, чтобы п о л ь­ зователи м огли на них попасть. Э тим мы и займемся. Создание меню для аутентифицированных и анонимных пользователей В этом разделе мы создадим меню как д ля анонимны х, так и д ля аутен ти ф и ц и ро­ ванн ы х пользователей. Ч ерез это меню пользователь смож ет входить в систем у регистрироваться, отправлять записи из формы и выходить из системы. Н а рис. 6.15 представлено меню д ля аноним ного пользователя. R О M ozilla Fircfox h t l p : / /1 2 7 .0 .0 .1 :3 0 0 0 / | + [___________________________________________________________________________________________ 0.0.13ООО q l_*j lib-] Рис. 6.15. Меню входа и регистрации обеспечивает доступ к созданным нами формам К огда п о л ь зо в а т е л ь п р о х о д и т а у тен ти ф и к а ц и ю , на эк р ан е п о я в л я е т с я другое м еню с им енем п о л ь зо в а те л я и д в у м я ссы лкам и: ссы л кой на стран и ц у д л я о т ­ правки сообщ ений в чат и ссы лкой д л я вы хода из системы. Это меню показано на рис. 6.16. M ozilla Fircfox О О О I <♦ (•* )$ h ttp ://127.0.0.1:3000/ 127.0.0.1 3000 HL+l ' С Coogle < 0 [ A | | C - | rick - post logout Рис. 6.16. Меню для пользователя, прошедшего аутентификацию К аж ды й создан ны й вам и E JS -ш аблон, п ред ставляю щ и й стран ицу при лож ения, содерж ит код <% in c lu d e menu %>, располож енны й после тега <body>. Э тот код под ­ клю чает ш аблон ./views/menu.ejs (л и сти н г 6.27). 6.2. Express 189 Листинг 6.27. Шаблон меню для анонимных пользователей и пользователей, прошедших аутентификацию <% i f (lo c a ls.u se r) { %> <div id='menu'> < ----- Менюдля пользователей, выполнивших вход. <span class='name'><%= user.name %></span> <a href='/post'> post</a> <a href='/logout'>logout</a> </div> <% } else { %> <div id='menu'> < ----- Менюдля анонимных пользователей. <a href= '/login'> login< /a> <a h re f= '/re g iste r'> re g iste r< /a > </div> <% } %> П редп олагается, что, раз п ерем енн ая u s e r д оступн а д л я ш аблона, пользователь прош ел аутентиф икацию , поскольку в противном случае эта перем енная не была бы определена. Е сли перем енная присутствует, им я пользователя мож но вывести на экран вместе со ссы лкам и д ля ввода данны х и выхода из прилож ения. Если же сайт посещ ает аноним ны й пользователь, вы водятся ссы лки входа и регистрации. В озни кает резонн ы й вопрос — откуда берется л о кал ьн ая перем енн ая u se r, если мы ее еще не создали? В следую щ ем разделе будет написан код, реализую щ ий для каж дого запроса загрузку данны х пользователя, вош едш его в систему, и о ткры ва­ ю щ ий ш аблонам доступ к этим данным. 6.2.8. Промежуточный компонент для загрузки пользовательских данных О дна из типичны х задач веб-прилож ений — загрузка из базы данны х ин ф орм ации о пользователе, которая обычно предоставляется в виде Jav aS crip t-объекта. Наличие подобны х данны х облегчает взаим одействие с пользователем. В при лож ении этой главы для каж дого запроса будут загруж аться пользовательские данны е с исп оль­ зованием промеж уточного компонента. С ценарий разм ещ ается в ф айле ./lib/middleware/user.js, то есть модель User долж на находиться в папке более высокого уровня (./lib). Сначала экспортируется ф ункция промежуточного компонента, затем сеанс проверяется на предмет наличия иденти­ ф икатора пользователя. Е сли идентиф икатор присутствует, значит, пользователь прош ел аутентиф икацию , поэтом у при лож ение м ож ет безопасно вы полнить вы ­ борку данны х из Redis. П оскольку N ode я в л яется однопоточной платф орм ой, локальное хранилищ е п ро­ граммных потоков здесь отсутствует. В случае с H T T P -сервером переменные запроса и ответа яв л яю тся единственны м и доступны м и контекстны м и объектами. В ы со­ коуровневы е ф рейм ворки м огут пользоваться средствам и N ode и предоставлять 190 Глава 6. Connect и Express до п о л н и тел ьн ы е объекты д л я х р ан ен и я дан ны х аутен ти ф и ц и р о ван н ы х п о л ь зо ­ вателей, однако в Express бы ло при нято реш ение придерж и ваться исходны х объ­ ектов, предлагаем ы х платф орм ой N ode. В результате контекстны е данны е обычно х р ан ятся в объекте запроса, как показано в ли сти н ге 6.28, где пользовательские данны е сохраняю тся в свойстве re q .u s e r; последую щ ие программные пром еж уточ­ ны е ком поненты и м арш руты м огут получать доступ к пользовательским данны м через это свойство. В озм ож но, вас интересует, д ля чего нуж но при сваи ван и е r e s . l o c a l s . u s e r . r e s . l o c a l s — объект уро вн я запроса, которы й Express предоставляет д ля получения доступа к данны м из ш аблонов, что очень напоминает a p p .lo c a ls . Это тож е некая ф ункц ия, которая м ож ет при м ен яться д ля объединения сущ ествую щ их объектов. Листинг 6.28. Промежуточный компонент для загрузки данных пользователя, выполнившего вход .. . odels/user . , . ,,); const. User = require(,, ../m module.exports = (req, re s, next) => { const uid = req .se ssio n .u id ; ■<­ i f (!uid) return next(); U ser.get(uid, (e rr, user) => { i f (e rr) return n e x t(e rr); req.user = re s .lo c a ls .u s e r = user; next(); }); }; Получает идентификатор пользователя, выполнившего вход, из сеансовыхданных. Получаетданные пользователя, выполнившего вход, от Redis. Предоставляет доступ кданным пользователя объекту запроса. Ч тобы воспользоваться новы м пром еж уточны м компонентом, сначала удалите из ф ай л а app.js все строки с текстом «user». Затем , как обычно, вклю чите этот модуль вы зовом r e q u ir e ( ) и передайте его м етоду a p p .u s e ( ). В наш ем при лож ении п ере­ менная u ser используется перед марш рутизатором, поэтому свойство re q .u s e r будет доступно только для м арш рутов и промеж уточны х компонентов, следую щ их в коде после u se r. Если вы используете пром еж уточны й компонент, которы й загруж ает данны е (как в наш ем случае), ком понент e x p r e s s .s t a t i c следует разм естить перед ним, иначе каж ды й раз при предоставлении статического ф айла будет происходить лиш нее обращ ение к базе данны х д ля вы борки пользовательских данных. В листинге 6.29 показано, как вклю чить этот ком понент в ф айл app.js. Листинг 6.29. Включение компонента, загружающего пользовательские данные const user = re q u ire ('./m id d le w a re /u se r'); ap p .u se(ex p ress.sessio n ()); a p p .u se (e x p re ss.sta tic (_dirname + '/p u b l i c ') ) ; app.use(user); < -------- Добавляет компонент в приложение. app.use(messages); app.use(app.router); 6.2. Express 191 Е сли вы снова запустите прилож ение и откроете в браузере страницу / l o g i n или / r e g i s t e r , на экране появится меню. Чтобы применить к меню стилевое оформление, добавьте в ф айл public/stylesheets/style.css разм етку CSS из листинга 6.30. Листинг 6.30. Разметка CSS, добавляемая в файл style.css для оформления меню #menu { position: absolute; top: 15px; rig h t: 20px; fo n t-s iz e : 12px; color: #888; } #menu .nam e:after { content: ' - '; } #menu a { tex t-d eco ratio n : none; m argin-left: 5px; color: black; } Теперь, когда меню готово, попробуйте зарегистрироваться в качестве пользовате­ ля. П осле этого на экране долж но п оявиться меню д ля пользователя, прош едш его аутентиф икацию , в котором есть ссы лка Post. В следующем разделе вы узнаете, как создать открытый R EST A PI для прилож ения. 6.2.9. Создание открытого REST API В этом разделе д ля при лож ения будет реализован R E S T -совм естимы й откры ты й A PI, чтобы сторонние при лож ения могли обращ аться к данны м и добавлять новые запи си. А р х итектурны й стиль R E S T обеспечивает возм ож н ость ч тен и я и и зм е ­ нен ия данны х пр и ло ж ен и я с исп ользован ием «глаголов» и «сущ ествительны х», представленны х м етодам и H T T P и U R L -адресами соответственно. З ап рос R EST обычно возвращ ает данны е в формате, удобном д ля м аш инной обработки, н ап ри ­ мер J S O N и ли XM L. Р еал и зац и я A P I состоит из следую щ их этапов: О про екти р о ван и е A P I, с пом ощ ью которого пол ьзовател и см огут отображ ать, вы водить список, удалять и публиковать записи; О добавление базовой аутентиф икации; О реал и зац и я м арш рутизации; О предоставление ответов в ф орм ате JS O N и XML. Д л я аутен ти ф и кац и и и сер ти ф и кац и и запросов A P I могут при м ен яться разны е методы , но р еал и зац и я более слож ны х р еш ен ий вы ходит за рам ки тем ы книги. 192 Глава 6. Connect и Express Ч тобы продем онстрировать, как а у тен ти ф и кац и я ин тегри руется в прилож ение, мы воспользуем ся пакетом basic-auth. Проектирование API П реж де чем браться за реализацию , ж елательно спланировать необходимые м арш ­ руты. В наш ем при лож ении R E S T -совм естимы й A PI будет снабж аться преф иксом / a p i , но это всего лиш ь реш ение из области проектирования, которое вы мож ете изм енить — например, использовать поддомен вида http://api.myapplication.com. С ледую щ ий фрагмент демонстрирует, почему ф ункции обратного вы зова стоит вы ­ делить в отдельны е м одули N ode (вм есто определения их во встроенном ф ормате с вы зовам и a p p . метод()). П ростой список м арш рутов дает четкое представление о том, что вы и ваш а группа р еал и зо вал и и где находятся р еал и зац и и обратны х вызовов: a p p .g e t( '/a p i/u s e r /:id ', a p i.u se r); a p p .g e t( '/a p i/e n tr ie s /:p a g e ? ', a p i.e n trie s ); a p p .p o s t( '/a p i/e n tr y ', api.add); Добавление базовой аутентификации К ак упоминалось ранее, существует много возможных подходов к защ ите A PI и огра­ ничений, вы ходящ их за рам ки темы книги. В этом разделе мы продемонстрируем процесс на прим ере базовой аутентиф икации. П роцесс будет абстрагироваться пром еж уточны м ком понентом a p i.a u th , потому что р еал и зац и я будет находиться в м одуле ./routes/api.js (вскоре мы создадим этот м о ду л ь). М етод a p p .u s e ( ) м ож ет получать путь, которы й в E xpress н азы вается точкой монтирования. С этой точкой м о н ти р о в ан и я д ля лю бы х путей, н ач и н аю ­ щ ихся с / a p i , и лю бой ком анды H T T P, будет ак ти ви зи р о ваться пром еж уточны й компонент. С трока a p p . u s e ( '/ a p i ', a p i .a u t h ) , как показано в следую щ ем ф рагменте, долж на располагаться до промежуточного компонента, загружаю щего данные пользователя. Это нужно для того, чтобы позднее вы м огли изменить компонент загрузки данных, чтобы загруж ать данны е пользователей, прош едш их аутентиф икацию : const api = r e q u ir e ( './r o u te s /a p i') ; a p p .u s e ( '/a p i', ap i.a u th ); app.use(user); Д л я вы полнения базовой аутентиф икации установите модуль basic-auth командой npm i n s t a l l - -sav e b a s ic - a u th . Затем создайте ф айл ./routes/api.js и вклю чите как Express, так и м одель пользователя, как показано в следую щ ем фрагменте. Пакет 6.2. Express 193 b a sic -a u th п о л у ч ает ф у н к ц и ю д л я в ы п о л н е н и я ау т е н ти ф и к а ц и и с си гн атурой (имя_пользователя, пароль, обратный_вызов). М етод U s e r .a u th e n tic a te идеально подходит для этой цели: const auth = re q u ire ('b a s ic -a u th '); const express = re q u ire ('e x p re s s '); const User = re q u ire ('../m o d e ls /u s e r'); exports.auth = (req, re s, next) => { const { name, pass } = auth(req); User.authenticate(nam e, pass, (e rr, user) => { i f (user) req.remoteUser = user; n e x t(e rr); }); }; А утентиф икация готова к работе. П ерейдем к р еализаци и м арш рутов API. Реализация маршрутизации П ервы й марш рут, которы й мы реализуем , — GET / a p i / u s e r / : i d . Л оги ка этого м арш ­ рута долж на сначала получить пользователя по идентиф икатору id и ответить кодом 404 N o t Found, если пользователь не существует. Если пользователь существует, то данны е пользователя будут переданы r e s .s e n d ( ) д ля сериализации, а прилож ение ответит представлением этих данны х в ф орм ате JS O N . Д обавьте логику из следу­ ющего ф рагм ента в ф айл routes/api.js: exports.user = (req, re s, next) => { U ser.get(req.param s.id, (e rr, user) => { i f (e rr) return n e x t(e rr); i f (!u s e r.id ) return res.sendStatus(404); re s.jso n (u se r); }); }; Затем добавьте следую щ ий путь м арш рута в ф айл app.js: a p p .g e t( '/a p i/u s e r /:id ', a p i.u se r); Все готово к тестированию . Тестирование выборки данных пользователя Запустите прилож ение и протестируйте его в программ е ком андной строки cU RL. Тестирование R E S T -аутен ти ф и каци и при лож ения продем онстрировано в следую ­ щем ф рагменте. Учетные данны е передаю тся в U R L tobi:ferret, по которы м cU R L строит поле заголовка A uthorization: $ curl h ttp ://to b i:fe rre t@ 1 2 7 .0 .0 .1 :3 0 0 0 /a p i/u se r/1 -v < 194 Глава 6. Connect и Express В листинге 6.31 представлен результат успеш ного тестирования. Д л я вы полнения аналогичного теста необходимо знать идентификатор пользователя. Если значение 1 не работает, а п ользователь зарегистрирован, попробуйте исп ользовать redis-cli и ком анду GET u s e r :id s . Листинг 6.31. Тестовый вывод * * * * > > > About to connect() to lo cal port 80 (#0) Trying 1 2 7 .0 .0 .1 ... connected Connected to lo cal (127.0.0.1) port 80 (#0) Server auth using Basic with user 'to b i' GET /a p i/u s e r/1 HTTP/1.1 < -------Отправленные заголовки HTTP. A uthorization: Basic Zm9vYmFyYmF6Cg== User-Agent: c u rl/7 .2 1 .4 (universal-apple-darw in11.0) lib c u rl/7 .2 1 .4 OpenSSL/0.9.8r z lib /1 .2 .5 > Host: local > Accept: */* > < HTTP/1.1 200 OK < -------Полученные заголовки HTTP. < X-Powered-By: Express < Content-Type: applicatio n /Jso n ; charset=utf-8 < -------Полученные данные JSON. < Content-Length: 150 < Connection: keep-alive < {"id":"1","nam e":"tobi"} Удаление конфиденциальной информации пользователя К ак видно из ответа JS O N , в ответе передается как пароль пользователя, так и з а ­ травка. Ч тобы изм енить ситуацию , реализуйте метод .toJSON() в объекте User из ф ай л а models/user.js: class User { // ... toJSON() { return { id: th is .i d , name: this.name }; } Если метод .toJSON сущ ествует в объекте, он будет использоваться вы зовам и JSON. s t r i n g i f y для получения ф орм ата JS O N . Е сли бы приведенны й выш е запрос cU R L был вы дан повторно, на этот раз бы ли бы получены только свойства id и name: { "id": "1", "name": "tobi" } Н а следую щ ем ш аге в A P I необходимо добавить возм ож ность создания записей. 6.2. Express 195 Добавление записей Процессы добавления записи с форм ы H T M L и через A PI практически идентичны, поэтом у в данном случае стоит снова воспользоваться уж е реализованной логикой м арш рута e n t r ie s .s u b m it ( ) . Впрочем, при добавлении записей в логике м арш рута храни тся им я пользователя, а запись добавляется к прочей информ ации. По этой причине необходимо м оди ф и­ цировать ком понент загрузки данны х пользователя д ля заполнен ия инф орм ации, загруженной промежуточным компонентом b a sic-au th . Промежуточный компонент b a sic -a u th возвращ ает эту информацию , которую можно присвоить req.rem oteU ser. С оответствую щ ая проверка в ком поненте загрузки данны х пользователя в ы п ол ­ няется достаточно прям олинейно; изм ените определение m o d u le .e x p o rts в ф айле middleware/user.js, чтобы ком понент загрузки пользователей работал с API: module.exports = (req, re s, next) => { i f (req.remoteUser) { re s .lo c a ls .u s e r = req.remoteUser; } const uid = req .sessio n .u id ; i f (!uid) return next(); U ser.get(uid, (e rr, user) => { i f (e rr) return n e x t(e rr); req.u ser = re s .lo c a ls .u s e r = user; next(); }); }; П осле этого вы смож ете добавлять запи си через A PI. Впрочем, необходим о внести еще одно изм ен ен ие — ответ, совм ести м ы й с A PI, вм есто п ер ен ап р авлен и я на домаш ню ю стран ицу при лож ен и я. Д л я добавления этой ф ункц иональн ости приведите вы зов e n tr y .s a v e в ф айле routes/entries.js к сле­ дую щ ему виду: e n try .sa v e (e rr => { i f (e rr) return n e x t(e rr); i f (req.remoteUser) { res.jso n ({ message: 'Entry added.' }); } else { r e s .r e d i r e c t ( '/ ') ; } }); Н аконец, чтобы активировать A PI добавления записей в ваш ем прилож ении, д о ­ бавьте содерж им ое следую щ его ф рагм ента в раздел r o u tin g ф ай л а app.js: a p p .p o s t( '/a p i/e n tr y ', en tries.su b m it); 196 Глава 6. Connect и Express С ледую щ ая команда cU R L позволяет протестировать добавление записи через API. В этом случае данны е заголовка и тела сообщ ения передаю тся с им енам и полей, которы е использую тся в ф орм е H TM L: $ curl -X POST -d "e n try [title ]= 'H o ho ho'&entry[body]='Santa loves you'" h ttp ://to b i:ferret@ 1 2 7 .0 .0 .1 :3 0 0 0 /ap i/en try После способности создания записей необходимо добавить возможность получения данны х записей. Добавление поддержки списка записей С ледую щ им будет реализован м арш рут A P I GET / a p i / e n t r i e s / : p a g e ? . Р еализаци я м арш рута почти не отличается от м арш рута вы вода в ф айле ./routes/entries.js. Также потребуется промежуточны й компонент страничного вывода — page() в следующих ф рагментах. Р еал и зац и я p ag e() будет добавлена ниже. Так как м арш рутная логика будет работать с записям и, в начало ф айла routes/api.js следует вклю чить м одель Entry: const Entry = re q u ire ('../m o d e ls /e n try '); Затем добавьте следую щ ий ф рагм ент в app.js: const Entry = re q u ire ('./m o d e ls /e n try '); a p p .g e t( '/a p i/e n tr ie s /:p a g e ? ', page(Entry.count), a p i.e n trie s ); П осле чего добавьте м арш рутную логику из следую щ его ф рагм ента в ф айл routes/ api.js. Р азл и ч и я меж ду этой логикой и аналогичной логикой из routes/entries.js от­ раж аю т тот факт, что вместо визуализац ии ш аблона строится разм етка JSO N : e x p o rts.e n trie s = (req, re s, next) => { const page = req.page; Entry.getRange(page.from, page.to, (e rr, e n trie s) => { i f (e rr) return n e x t(e rr); re s .js o n (e n trie s ); }); }; Реализация промежуточного компонента страничного вывода Д л я разбиения на страницы будет использоваться строка запроса ?page=N (где N — текущ ая страница). Добавьте ф ункцию (листинг 6.32) промежуточного компонента в ф айл ./middleware/page.js. Листинг 6.32. Промежуточный компонент для разбиения на страницы module.exports = (cb, perpage) => { perpage = perpage || 10; ■<-------По умолчанию 10 записей на страницу. return (req, re s, next) => { < -------Возвращает функцию промежуточного компонента. le t page = Math.max( 6.2. Express 197 parseInt(req.param s.page || '1 ', 10) 1 ) - 1; < -------Разбирает параметр page какдесятичное целое число. c b ((e rr, to ta l) => { -<-------- Активизирует переданнуюфункцию. i f (e rr) return n e x t(e rr); < -------- Делегирует обработку ошибок. req.page = re s.lo c a ls.p a g e = { -<-------Сохраняет свойства page для будущих обращений. number: page, perpage: perpage, from: page * perpage, to : page * perpage + perpage - 1, t o ta l: t o t a l, count: M a th .c e il(to ta l / perpage) }; n ex t(); < -------Передает управление следующему промежуточного компоненту. }); }; } К ом понент получает значение, присвоенное ?page=N (наприм ер, ?page=1). Затем он получает общее количество результатов и предоставляет объект page с заранее вы численны м и значениям и лю бы м представлениям , которы е позднее могут в и зу ­ ализироваться. Э ти зн ачения вы чи сляю тся вне ш аблона, чтобы ш аблон был более четким и содерж ал м еньш е логики. Тестирование маршрута entries С ледую щ ая ком анда cU R L запраш ивает данны е записей из API: $ curl h ttp ://to b i:fe rre t@ 1 2 7 .0 .0 .1 :3 0 0 0 /a p i/e n trie s Э та ком анда cU R L долж на вы вести данны е J S O N следую щ его вида: [ { }, { "username": "rick", " t i t l e " : "Cats c a n 't read minds", "body": "I think you're wrong about the cat th in g ." "username": "mike", " t i t l e " : "I think my cat can read my mind", "body": "I think cat can hear my thoughts." }, Разобравш и сь с базовой р еализаци ей A PI, перейдем к тому, как A P I м ож ет под ­ держ ивать м нож ественны е ф орм аты ответов. 6.2.10. Согласование контента Согласование контента позволяет клиенту указать форматы, которые он готов п ри­ нять и которы е он считает предпочтительны м и. В этом разделе мы предоставим 198 Глава 6. Connect и Express контент A P I в ф орм атах JS O N и XM L, чтобы пользователи A P I м огли реш ить, что им нужно. H T T P поддерж ивает механизм согласования контента через поле заголовка Accept. Н априм ер, клиент, предпочитаю щ ий H T M L , но готовы й при нять простой текст, мож ет задать следую щ ий заголовок запроса: Accept: te x t/p la in ; q=0.5, text/htm l Зн ач ен и е qvalue (q= 0.5 в данном при м ере) указывает, что, хотя ф орм ат te x t/h tm l указан на втором месте, он на 50% предпочтительнее ф орм ата t e x t / p l a i n . Express разбирает эту информацию и предоставляет нормализованны й массив req .accepted: [{ value: 'te x t/h tm l', q u ality : 1 }, { value: 't e x t / p l a i n ', q u a lity : 0.5 }] Express такж е предоставляет метод r e s .f o r m a t( ) , которы й получает массив типов M IM E и обратны х вызовов. Express определяет, какую инф орм ацию хочет п о л у ­ чить клиент и какую инф орм ацию вы готовы предоставить, после чего вы зы вает соответствую щ ую ф ункцию обратного вызова. Реализация согласования контента Р еали зац и я согласования контента для м арш рута GET / a p i / e n t r i e s из ф айла routes/ м ож ет вы глядеть при близи тельн о так, как показано в листинге 6.33. JS O N поддерж ивается так же, как и прежде, — записи сериализую тся в ф орм ат JS O N вы ­ зовом r e s .s e n d ( ) . О братны й вы зов X M L перебирает запи си и записы вает данны е в сокет в процессе перебора. Учтите, что явно задавать значение C ontent-T ype не нуж но; r e s .f o r m a t( ) присваивает нуж ное значение автоматически. api.js Листинг 6.33. Реализация согласования контента e x p o rts.e n trie s = (req, re s, next) => { const page = req.page; Entry.getRange(page.from, page.to, (e rr, e n trie s) => { <.----- Получает данные записей. i f (e rr) return n e x t(e rr); res.form at({ -<-------Отвечает по-разному в зависимости от значения заголовка Accept. 'a p p lic a tio n /js o n ': () => { Ответ JSON. re s.se n d (e n trie s); }, 'a p p lic a tio n /x m l': () => { < -------Ответ XML. re s .w rite ('< e n trie s > \n '); en tries.fo rE ach ((en try ) => { r e s . w r i t e ( ''' <entry> < title > $ { e n try .title } < /title > <body>${entry.body}</body> <username>${entry.username}</username> </entry> ); 6.2. Express 199 }); re s .e n d ('< /e n trie s > '); } }) }); }; Е сли вы задали обратны й вы зов д ля ф орм ата ответа по умолчанию , он будет вы ­ полнен в том случае, если п о л ьзо в ател ь не зап р о си л ф орм ат, которы й вы явн о предоставляете. М етод r e s .f o r m a t ( ) такж е получает имя, которое соответствует заданном у типу M IM E . Н априм ер, вм есто a p p l i c a t i o n / j s o n и a p p lic a tio n /x m l м огут и сп о льзо ­ ваться js o n и xml: res.form at({ j s °n: () => { re s.se n d (e n trie s); }, xml: () => { re s .w rite ('< e n trie s > \n '); en tries.fo rE ach ((en try ) => { re s.w rite ( <entry> < title > $ { e n try .title } < /title > <body>${entry.body}</body> <username>${entry.username}</username> </entry> ); }); re s .e n d ('< /e n trie s > '); }) } Ответ в формате XML П ож алуй, написание специ али зированн ой логики в м арш руте для вы дачи ответа в ф орм ате X M L — не самое элегантное реш ение. П осм отрим , как воспользоваться систем ой представлений д ля реш ен ия этой проблемы. С оздайте ш аблон с им енем ./views/entries/xml.ejs со следую щ им кодом EJS, п ере­ бираю щ им запи си д ля генерирования тегов <entry> (л и сти н г 6.34). Листинг 6.34. Использование шаблона EJS для генерирования разметки XML <entries> <% en tries.fo rE ach (en try => { %> < -------Перебирает все записи. <entry> <title><%= e n t r y .t i t l e %></title> <.-------- Выводит поля. 200 Глава 6. Connect и Express <body><%= entry.body %></body> <username><%= entry.username %></username> </entry> <% }) %> </entries> О братны й вы зов X M L теперь м ож но зам енить одним вызовом r e s .r e n d e r ( ) с пере­ дачей м ассива e n tr ie s : }) xml: () => { re s .re n d e r('e n trie s /x m l', { e n trie s : e n trie s }); } Теперь все готово для тестирования X M L -версии API. Введите следующую команду в ком андной строке, чтобы просм отреть вы вод в ф орм ате XML: curl - i -H 'Accept: application/xm l' h ttp ://to b i:fe rre t@ 1 2 7 .0 .0 .1 :3 0 0 0 /a p i/e n trie s 6.3. Заключение О C o n n ect — ф рейм ворк H TTP, которы й позволяет строить цепочки пром еж уточ­ ны х ком понентов до и после обработки запросов. О П ром еж уточны е ком поненты C o n n ect представляю т собой ф ункции, которые получаю т объекты запроса и ответа N ode, а такж е ф ункцию , которая вызывает следую щ ий компонент, и необязательны й объект ош ибки. О В еб-прилож ения Express такж е строятся из пром еж уточны х компонентов. О В Express можно строить R E ST API, используя команды H T T P для определения марш рутов. О М арш руты Express могут отвечать данны м и JS O N , H T M L и ли в других ф о р ­ матах данных. О Express содержит простой A PI ш аблонизации с поддерж кой многих разны х ядер. 7 Шаблонизация веб-приложений В главах 3 и 6 рассм атр и вал и сь основы и сп о льзо в ан и я ш аблонов д л я создан ия п р ед став л ен и й в п р и л о ж е н и я х E xpress. В этой главе, полностью посвящ ен н ой ш аблонам, мы рассм отрим три поп улярн ы х ш аблонизатора, а такж е узнаем, как ш аблоны помогают «очистить» код веб-прилож ений за счет отделения программной л огики от разм етки визуализации. Е сли вы знаком ы с ш аблонизацией и паттерном M VC (M odel-V iew -C ontroller — М одель-П редставление-К онтроллер), мож ете сразу переходить к разделу 7.2, с ко­ торого начинается подробное рассм отрение ш аблонизаторов, вклю чая EJS, H ogan и Pug. Если ж е ш аблони зация вам в новинку, продолж айте читать — в следую щ их нескольких разделах излож ены концептуальны е основы работы с ш аблонами. 7.1. Поддержка чистоты кода путем шаблонизации С паттерном M VC м ож но разрабаты вать традиционны е п ри лож ения как на базе N ode, так и практи чески лю бой другой веб-технологии. О дна из клю чевы х к о н ­ цепций M VC заклю чается в разделен ии логики, данны х и представления. В M V C п р и л о ж е н и я х п о л ь зо в а т е л ь о б ы чн о за п р а ш и в а е т н у ж н ы й р ес у р с на сервере, затем контроллер (c o n tro lle r) запраш ивает данны е п ри лож ен и я у модели (m odel) и передает их данны е представлению (view ), которое осущ ествляет окончательное ф орм атирован ие данны х д ля конечного пользователя. M V C -представления часто реал и зу ю тся с пом ощ ью одного из я зы к о в ш аб л он и зац и и . Е сли в п ри лож ен и и и сп о льзу ется ш аб лон и зац и я, п р ед ставлени е передает шаблонизатору (tem p la te engine) значения, возвращ енны е моделью , и указы вает ф айл ш аблона, опред еля­ ю щ ий способ отображ ения этих значений. Н а рис. 7.1 показано, как логика ш аблонизации вписы вается в общую архитектуру M V C -прилож ения. Ф ай л ы ш аблонов обычно содерж ат заполни тели для значений Рис. 7.1. Последовательность операций при работе МѴС-приложения и его взаимодействие с уровнем шаблона 7.1. Поддержка чистоты кода путем шаблонизации 203 прилож ений, а такж е ф рагм енты H T M L -, CSS- и иногда клиентского J a v a S c rip tкода, п р ед н азн ач ен н о го д л я р е а л и за ц и и д и н ам и ч еского п о в ед ен и я (в ы во д а на экран сто р о н н и х видж етов вроде кн о п к и Like в F aceb o o k ) и ли д л я вкл ю чен и я специального реж и м а работы ин терф ейса (наприм ер, скры тия или показа частей страницы ). А так как ф айлы ш аблонов больш е связаны с уровнем представления, чем с програм м ной логикой, разработчикам кли ен тских и серверны х прилож ений эти ф айлы помогаю т организовать разделение труда. В этом разделе мы займ ем ся в и зу ал и зац и ей разм етки H T M L , вы полняем ой как с помощ ью ш аблонов, так и без них, чтобы вы смогли почувствовать разницу. О д ­ нако сначала давайте разберем практический прим ер исп ользован ия ш аблонов. 7.1.1. Шаблонизация в действии В качестве простого пр и м ер а п р и м ен ен и я ш аблонов мы рассм отри м проблем у элегантного H T M L -вы вода из простого п р и л о ж ен и я д л я блога. К аж д ая запись блога вклю чает в себя заголовок, дату создания и текст. В окне браузера блог будет вы глядеть так, как показано на рис. 7.2. I am getting old, but thankfully I'm not in ja il! Movies are pretty good Гѵе been watching a lot o f movies lately. It's relaxing, except when they have clowns in them. Рис. 7.2. Пример вывода приложения блога в браузере Запи си блога читаю тся из текстового файла, отформатированного так, как фрагмент из ф айла entries.txt (листинг 7.1). С им волы — показываю т, где заканчивается одна запись и начинается другая. Листинг 7.1. Текстовый файл с записями в блоге t i t l e : I t 's my birthday! date: January 12, 2016 I am g ettin g old, but thankfully I'm not in J a il! t i t l e : Movies are p re tty good date: January 2, 2016 I'v e been watching a lo t of movies la te ly . I t 's relaxing, except when they have clowns in them. 204 Глава 7. Шаблонизация веб-приложений К од пр и ло ж ен и я в ф айле blog.js начинается с вклю чени я необходим ы х модулей и ч тен и я записей блога, как показано в листинге 7.2. Листинг 7.2. Логика разбора записей блога из текстового файла const f s = r e q u i r e ( 'f s ') ; const h ttp = r e q u ir e ( 'h ttp ') ; function g etE n tries() { -<-----Функция для чтения и разбора текста записи. Читает данные const e n trie s = []; записи из файла. le t entriesRaw = f s .re a d F ile S y n c ('./e n trie s .tx t 'u tf 8 ') ; Разбирает текст на отдельные записи блога. entriesRaw = e n trie s R a w .s p lit('— ') ; -<-------entriesRaw.map((entryRaw) => { const entry = {}; const lin e s = e n try R a w .s p lit('\n '); Разбирает текст записи на строки. lin es.m ap ((lin e) => { < -------Разбирает строки на свойства entry i f (lin e .in d e x O f ( 'title : ') === 0) { e n t r y .t i tl e = li n e .r e p l a c e ( 't it l e : '') ; else i f (lin e.in d ex O f('d ate: ') === 0) { en try .d ate = lin e .re p la c e ('d a te : ', ' ' ) ; else { entry.body = entry.body || ' ' ; entry.body += lin e; }); en trie s.p u sh (e n try ); }); return e n trie s ; } const e n trie s = g e tE n trie s(); co n so le .lo g (e n trie s); С ледую щ и й код, д о б ав л ен н ы й в при ло ж ен и е, о п р ед ел яет сервер H T T P . Когда сервер получает зап рос H T T P, он возвращ ает страницу, содерж ащ ую все запи си блога. Э та стран ица ви зу ал и зи р у ется ф ун кц и ей blogPage, которую мы определим чуть позже: const server = h ttp .c re a te S e rv e r((re q , res) => { const output = blogPage(entries); res.writeHead(200, {'Content-Type': 'te x t/h tm l'} ); res.end(output); }); se rv e r.liste n (8 0 0 0 ); Теперь нуж но определить ф ункцию blogPage, которая преобразует запи си блога в H T M L -страницу, отправляемую браузеру пользователя. Д л я этого мы используем два подхода: О в и зу ал и зац и я H T M L -кода без ш аблона; О в и зу ал и зац и я H T M L -кода с помощ ью ш аблона. С начала рассм отрим визуализац ию H T M L -кода без шаблона. 7.1. Поддержка чистоты кода путем шаблонизации 205 7.1.2. Визуализация HTML без шаблона П рилож ение д ля блога могло бы непосредственно вы водить H T M L -код на экран, но см еш ивание H T M L -разм етки с програм м ной логикой п ри лож ения привело бы к хаосу. В листинге 7.3 ф у н кц и я blogPage дем онстрирует вы вод на экран записей блога без помощ и ш аблона. Листинг 7.3 Шаблонизатор отделяет подробности визуализации от программной логики function blogPage(entries) { le t output = ' <html> <head> <style type="text/css"> .e n tr y _ title { font-w eight: bold; } .entry_date { fo n t-s ty le : i t a l i c ; } .entry_body { margin-bottom: 1em; } </style> </head> <body> ; entries.m ap(entry => { output += ' < -------Разметка HTMLслишком сильно смешивается с программной логикой. <div class= "en try _ title"> $ {en try .title}< /d iv > <div class="entry_date">${entry.date}</div> <div class="entry_body">${entry.body}</div> } )/ output += '</body></html>'; return output; } О б р ати те в н и м ан и е: весь кон тен т, о т н о с я щ и й с я к о ф о р м л ен и ю вы вода, C S S о п р е д е л ен и я и H T M L -р азм етк а зн а ч и те л ьн о у вел и ч и ваю т ч и сл о стр о к в коде прилож ения. Визуализация HTML с помощью шаблона В и зуали зац и я разм етки H T M L с прим енением ш аблона позволяет убрать из п р и ­ кладной л огики H T M L -разметку, что делает код п ри лож ения сущ ественно чище. Д л я тестирования примеров, приведенны х в этом разделе, нуж но установить в папку прилож ения модуль для работы с EJS (E m bedded Jav aS crip t). Д л я этого в командой строке введите следую щ ую команду: npm in s t a l l ejs С ледую щ ий ф рагм ент загруж ает ш аблон из ф айла, а затем определяет новую вер­ сию ф ун кц и и blogPage, которая теперь использует ш аблонизатор EJS (о нем мы поговорим в разделе 7.2): 206 Глава 7. Шаблонизация веб-приложений const f s = r e q u i r e ( 'f s ') ; const e js = r e q u ir e ( 'e js ') ; const template = fs.re a d F ile S y n c ('./te m p la te ss/b lo g _ p a g e .e js', 'u t f 8 ') ; function blogPage(entries) { const values = { e n trie s }; return ejs.ren d er(tem p late, values); } П олны й листинг находится в архиве кода, прилагаем ом к книге, в каталоге ch07templates/listing7_4/. Ф а й л E JS -ш аблона содерж ит H T M L -разм етку (отделенную от п р и кл ад н о й л о ги ки ), а такж е зап о л н и тел и , которы е указы ваю т, где долж ны располагаться переданны е ш аблонизатору данные. В листинге 7.4 приведен ф айл E J S -ш аблона, п р и м ен я ем ы й д л я вы вода зап и сей блога на экран, вклю чаю щ и й H T M L -разм етку и заполнители. Листинг 7.4. Шаблон EJS для вывода записей блога <html> <head> <style type="text/css"> .e n tr y _ title { font-w eight: bold; } .entry_date { fo n t-s ty le : i t a l i c ; } .entry_body { margin-bottom: 1em; } </style> </head> <body> <% entries.m ap(entry => { %> < -------Заполнитель, перебирающий записи блога <div class="entry_title"><%= e n tr y .ti t l e %></div> ■<Заполнители для фрагментов <div class="entry_date"><%= en try .d ate %></div> данных в каждой записи. <div class="entry_body"><%= entry.body %></div> <% }); %> </body> </html> В модулях, разработанны х сообщ еством Node, такж е реализованы ш аблонизаторы, причем достаточно разнообразны е. Е сли вы полагаете, что H T M L - и (и л и ) C S S р азм етк е не х ватает элегантности, так как H T M L требует закр ы ваю щ и х тегов, а CSS — откры ваю щ их и закры ваю щ их скобок, обратите вним ание на ш аб лони за­ торы. О ни позволяю т ф айлам ш аблонов использовать специальны е язы к и (такие, как язы к Pug, о котором мы поговорим в этой главе позж е), предлагаю щ ие способы сокращ енной запи си H T M L и (и л и ) CSS. Х отя ш аб лони заторы м огут сделать ш аблоны более «чисты м и», возм ож но, вам не захочется трати ть врем я на изучени е альтерн ати вны х способов зап и си H T M L и CSS. В конце концов, о кон чательное реш ен ие зави си т от ваш их л и ч н ы х п р ед ­ почтений. В оставш ейся части главы мы поговорим о том, как встроить ш аблонизацию в ваши N o d e-прилож ения, на прим ере трех популярны х ш аблонизаторов: 7.2. Шаблонизация с EJS 207 О ш аблонизатор EJS (E m b ed d ed Jav aS crip t); О м ин им алистский ш аблонизатор Hogan; О ш аблонизатор Pug. К аж ды й из этих ш аблонизаторов предлагает альтернативны й способ запи си р а з­ м етки H T M L . Н ачнем с EJS. 7.2. Шаблонизация с EJS EJS (E m bedded JavaScript, https://github.com/visionmedia/ejs) предлагает достаточно прям олинейны й подход к ш аблонизации, которы й будет близок разработчикам, ис­ пользую щ им ш аблоны в других язы ках, таких как J S P (Jav a Server Pages), Sm arty (P H P ), ERB (E m bedded R uby) и подобных. Теги EJS внедряю тся в разметку H T M L кода в качестве заполни телей д ля данны х. К роме того, EJS позволяет вы полнять в ш аблонах простейш ую логику Ja v a S c rip t д ля таких операций, как условное вет­ вление и итерация, как это делается в PH P. В этом разделе вы узнаете: О как создавать E JS -ш аблоны; О как и сп о льзо вать E JS -ф ильтры д ля поддерж ки обы чной ф ун кц и он ал ьн ости представлений, такой как м анип улирование текстом, сортировка и итерация; О как интегрировать EJS с п р и лож ениям и Node; О как использовать EJS д ля кли ен тских прилож ений. А теперь давайте глубж е погрузим ся в м ир ш аблонов EJS. 7.2.1. Создание шаблона В м и р е ш аб л о н и зац и и данны е, отсы лаем ы е ш аб л о н и зато р у д л я ви зуал и зац и и , иногда назы ваю т контекстом (co n tex t). Вот пример использования EJS в N ode для визуал и зац и и простого ш аблона с использованием контекста: const e js = r e q u ir e ( 'e js ') ; const template = '<%= message %>'; const context = { message: 'Hello tem plate!' }; co nso le.lo g (ejs.ren d er(tem p late, context)); О б рати те в н и м ан и е на и сп о л ьзо в ан и е l o c a l s во втором аргум енте, п ер ед авае­ мом ren d er. В торой аргум ент м ож ет вклю чать парам етры визуализации, а такж е кон тек стн ы е данны е. П ар ам етр l o c a l s гарантирует, что отдельн ы е ф рагм енты кон текстн ы х дан ны х не будут и н тер п р ети р о ваться как E JS -парам етры . О днако 208 Глава 7. Шаблонизация веб-приложений в больш инстве случаев в качестве второго парам етра мож но передавать сам к о н ­ текст, как в следую щ ем вы зове render: co n so le.lo g (ejs.ren d er(tem p late, context)); Е сли вы передаете кон текст E JS -ш аблону неп осредствен но во втором аргум енте вы зова render, убедитесь, что при и м енован ии кон текстн ы х зн ач ен и й не и с п о л ь­ зу ю тся следую щ и е клю чевы е слова: cache, c l i e n t , c lo s e , com pileD ebug, debug, file n a m e , open и л и sco p e. О н и за р е зе р в и р о в ан ы с целью и зм е н ен и я н астроек ш аблонизатора. Экранирование символов В проц ессе в и зу а л и зац и и E JS экранирует (escap es) все сп ец и альн ы е сим волы в кон текстн ы х значениях, зам ен яя их кодам и H T M L -сущ ностей. Это позволяет предотвратить атаки м еж сайтового вы п олн ен и я сценариев (C ro ss-S ite Scripting, X SS), в ходе которы х зл о у м ы ш л ен н и к и пы таю тся в качестве дан ны х отправить вредоносны й код J a v a S c rip t в надежде, что этот код будет вы полнен при выводе данны х на экран в браузере другого пользователя. С ледую щ ий ф рагм ент д ем он ­ стрирует, как работает м еханизм экранировани я сим волов в EJS: var e js = r e q u ir e ( 'e js ') ; var template = '<%= message %>'; var context = {message: "<script>alert('X SS a tta c k !');< /s c rip t> " } ; co n so le.lo g (ejs.ren d er(tem p late, context)); В результате вы полнения этого кода на экране появится следую щ ий текст: & lt;script& gt;alert('X S S a tta c k !');& lt;/s c rip t& g t; Е сли вы д оверяете исп ользуем ы м в ш аблоне данны м и не хотите экранировать контекстны е значения в EJS, используйте в теге ш аблона сим волы <%- вместо <%=, как в следую щ ем примере: const e js = r e q u ir e ( 'e js ') ; const template = '<%- message %>'; const context = { message: "< scrip t> alert('T ru sted J a v a S c rip t!');< /sc rip t> " }; co n so le.lo g (ejs.ren d er(tem p late, context)); Е сли вам не нравятся символы , используем ы е в E JS -ш аблоне д ля специф икации тегов, вы мож ете легко их изменить: const e js = r e q u ir e ( 'e js ') ; e js .d e lim ite r = '$ ' const template = '<$= message $>'; const context = { message: 'Hello tem plate!' }; co n so le.lo g (ejs.ren d er(tem p late, context)); 7.2. Шаблонизация с EJS 209 И так, вы ознако м и л и сь с основам и EJS; давай те посм отрим , как EJS позволяет упростить управление представлением данных. 7.2.2. Интеграция шаблонов EJS в приложение Х ранить ш аблоны в ф ай л ах вм есте с кодом пр и лож ен и я неудобно, вдобавок это приводит к загром ож дению кода. М ы воспользуем ся A P I ф айловой системы N ode д ля загрузки ш аблонов из отдельны х файлов. П ерейдите в рабочую папку и создайте ф айл app.js с кодом из листинга 7.5. Листинг 7.5. Хранение шаблонного кода в файлах const e js = r e q u ir e ( 'e js ') ; const fs = r e q u i r e ( 'f s ') ; const h ttp = r e q u ir e ( 'h ttp ') ; const filename = './te m p la te s /s tu d e n ts .e js '; < ---- Обозначает местонахождение файла шаблона. const students = [ ■<-------- Данные для передачи шаблонизатора. { name: 'Rick LaRue', age: 23 }, { name: 'Sarah C athands', age: 25 }, { name: 'Bob Dobbs', age: 37 } ]; const server = h ttp .c re a te S e rv e r((re q , res) => { ■<-------- Создает сервер HTTP. i f (re q .u rl === ' / ' ) { fs.read F ile(filen am e, (e rr, data) => { ■<-------- Читает шаблон из файла. const template = d a ta .to S trin g (); const context = { students: students }; const output = ejs.ren d er(tem p late, context); <.----- Выполняет визуализацию шаблона. res.setH ead er('C o n ten t-ty p e', 'te x t/h tm l') ; res.end (o u tp u t); -<-------- Отправляет ответ HTTP. }); } else { res.statusC ode = 404; res.end('N ot found'); } }); serv e r.liste n (8 0 0 0 ); Затем создайте дочерний каталог с им енем templates. В этом каталоге будут х р а­ ни ться ш аблоны. С оздайте ф айл students.ejs в каталоге templates. В клю чите в ф айл tem plates/students.ejs код из листинга 7.6. Листинг 7.6. Шаблон EJS для построения массива <% i f (stu d en ts.len g th ) { %> <ul> <% students.forE ach((student) => { %> <li><%= student.name %> (<%= student.age %>)</li> <% }) %> </ul> <% } %> 210 Глава 7. Шаблонизация веб-приложений Кэширование шаблонов EJS EJS поддерж ивает кэш и р о ван и е ф у н к ц и й ш аблона в пам яти. Э то означает, что после однократного разбора ф айла ш аблона созданная в результате ф ункц ия сохра­ няется в пам яти. В изуали зация кэш ированного ш аблона осущ ествляется быстрее, поскольку этап разбора пропускается. Е сл и вы зан и м аетесь р азр аб о тк о й в еб -п р и л о ж ен и я в N ode и хотите, чтобы и з ­ м енени я, внесенн ы е в ф ай л ы ш аблона, нем едлен но о траж ал и сь в при лож ении , кэш ирование следует отклю чить. Если ж е вы уж е развернули прилож ение в р або­ чей среде, кэш ирование позволяет быстро и просто получить ощ утим ы й вы игры ш в производительности. У словное вклю чение кэш и рования осущ ествляется через перем енную окруж ения NODE_ENV. Ч тобы проверить, как работает кэш ирование, изм ените вызов E JS-ф ункц ии render из преды дущ его примера: const cache = process.env.NODE_ENV === 'p ro d u ctio n '; const output = e js.re n d e r( tem plate, { students, cache, filename } ); О братите вним ание на то, что в параметре filenam e не обязательно указы вать файл. В данном случае мож ет при м ен яться уникальное значение, определяю щ ее, какой ш аблон вы визуализируете. Теперь, когда вы знаете, как интегрировать ш аблоны EJS в N o d e-прилож ение, д а ­ вайте рассм отрим другую возм ож ность прим енения EJS в браузерах. 7.2.3. Использование EJS в клиентских приложениях Ч тобы воспользоваться EJS на стороне клиента, сначала нуж но загрузить ш аблонизатор EJS в рабочий каталог с помощ ью следую щ их команд: cd /your/w orking/directory curl -O h ttp s ://ra w .g ith u b u s e rc o n te n t.c o m /tj/e js /m a ste r/lib /e js.js П осле загр у зки ф ай л а ejs.js EJS м ож но исп ользовать в кли ен тском коде. В л и с ­ тинге 7.7 представлено простое клиентское прилож ение с ш аблоном EJS. Сохранив код в ф айле с именем index.html, вы смож ете откры ть его в браузере и просмотреть результаты. Листинг 7.7. Добавление шаблонизации на стороне клиента средствами EJS <html> <head> <title>EJS exam ple</title> < script src= "ejs.js"> < /scrip t> 7.3. Использование языка Mustache с шаблонизатором Hogan 211 < script < -------Включает библиотеку jQueryдля операций с DOM. src="h ttp ://a ja x .g o o g le a p is.c o m /a ja x /lib s/jq u ery /1 .8 /jq u e ry .js"> </script> </head> <body> <div id="output"></div> -<-------Заполнитель для результатов визуализации шаблона. <script> const template = "<%= message %>"; ■<----- Шаблон, используемый для визуализации контента. const context = { message: 'Hello tem plate!' }; ■<-------- Данные для шаблона. $(document).ready(() => { -<-------- Ожидает загрузки страницы в браузере. $ ('# o u tp u t').h tm l( ejs.ren d er(tem p late, context) -< і Выполняет визуализацию шаблона ); в div с идентификатором «output». }); < /script> </body> </html> Теперь вы знаете, как использовать в N ode п олноф ункци ональны й ш аблонизатор. П риш ло врем я познаком иться с ш аблонизатором H ogan, в котором ф ун кц и он ал ь­ ность, доступная в коде ш аблона, нам еренно ограничена. 7.3. Использование языка Mustache с шаблонизатором Hogan Ш аблонизатор hogan.js (https://github.com /twitter/hoganjs) был создан в компании T w itter д ля собственны х потребностей в работе с ш аблонами. H ogan п ред ставл я­ ет собой р еализаци ю поп улярн ого стандарта я зы к а ш аблонов M u stach e (h ttp :// mustache.github.com/), разработанного К рисом В анстратом (C h ris W a n stra th ) для проекта G itH ub. В M ustache выбран минималистский подход к ш аблонизации. В отличие от EJS стан­ дарт M ustache сознательно не вклю чает ни условную логику, ни встроенные средства фильтрации контента (кроме экранирования контента для предотвращения XSS-атак). Разработчики M ustache постарались сделать код ш аблона максимально простым. В этом разделе мы узнаем: О как создать и реализовать в при лож ении ш аблоны M ustache; О какие теги ш аблонов определены стандартом M ustache; О как организовать ш аблоны с помощ ью «компонентов»; О как вы полнить точную настройку H ogan с помощ ью нестандартны х ограничи­ телей и других параметров. И так, давайте взглянем на альтернативны й подход к ш аблонизации, которы й пред­ лагает H ogan. 212 Глава 7. Шаблонизация веб-приложений 7.3.1. Создание шаблона Ч то бы исп о льзо вать H o g an в п р и ло ж ен и и и л и вы п ол н и ть лю бой из прим еров, пр ед л агаем ы х в этом разделе, нуж но устан ови ть H ogan в катал ог п р и л о ж ен и я (ch07-tem plates/hogan-snippet). Д л я этого в ком ан дной строке введи те следую щ ую команду: npm i --save hogan.js Д алее представлен простейш ий прим ер исп ользован ия H ogan в при лож ении N ode д л я ви зу ал и зац и и простого ш аблона на основе контекста. П ри его вы полнен ии вы водится текст «H ello tem plate!». const hogan = re q u ire ('h o g a n .js '); const templateSource = '{{message}}'; const context = { message: 'Hello tem plate!' }; const template = hogan.compile(templateSource); co n sole.log(tem plate.render(context)); Теперь, когда вы знаете, как с помощ ью H ogan обрабаты вать ш аблоны M ustache, перейдем к рассм отрению тегов, предлагаем ы х стандартом M ustache. 7.3.2. Теги Mustache К онцептуально теги M ustache не отличаю тся от тегов EJS. Теги M ustache р езер ­ вирую т м есто д л я зн ач ен и й перем енны х, а такж е указы ваю т на необходим ость перебора, что п о зво л яет р асш и р и ть ф у н кц и о н ал ьн о сть M u sta ch e и ли добавить в ш аблон ком м ентарий. Вывод простых значений Ч тобы вы вести контекстное значение в ш аблоне M ustache, заклю чите им я зн ач е­ ни я в двойны е ф игурны е скобки. П одобные скобки среди M ustache-разработчиков п о л у ч и л и ж аргонн ое н азван ие « m u staches» (д осл овн о — усы ). Если, наприм ер, нуж но вы вести значение д л я контекстного элем ента name, воспользуйтесь тегом H ogan {{name}}. П одобно больш инству других ш аблонизаторов, H ogan по ум олчанию экранирует контент, чтобы предотвратить возм ож ны е X SS-атаки. Д л я вы вода неэкранированного значения в H ogan м ож но либо добавить третью ф игурную скобку, либо пред­ варить контекстны й элемент сим волом &. Так, в предыдущ ем прим ере д ля вывода неэкранированного контекстного зн ач ен и я будет и сп ользоваться тег {{{name}}} и ли {{&name}}. Д л я д обавления в ш аблон M ustache ком м ентария при м ен яется ф орм ат вида {{! This is a comment }} 7.3. Использование языка Mustache с шаблонизатором Hogan 213 Секции — перебор нескольких значений Х отя H ogan не позволяет вклю чать логику в ш аблоны, этот ш аблонизатор предла­ гает элегантны й механизм перебора нескольких значений в контекстном элементе с помощ ью секций (sectio n s). Н априм ер, следую щ ий контекст содерж ит элемент с массивом значений: var context = { students: [ { name: 'Jane Narwhal', age: 21 }, { name: 'Rick LaRue', age: 26 } ] }; Д опустим , вы реш или создать ш аблон, которы й вы водит на экран данны е каждого студента в отдельном H T M L -абзаце: <p>Name: Jane Narwhal, Age: 21 years old</p> <p>Name: Rick LaRue, Age: 26 years old</p> С ш аблонам и H ogan эта задача реш ается тривиально: {{#students}} <p>Name: {{name}}, Age: {{age}} years old</p> {{/students}} Инвертированные секции — HTML по умолчанию при отсутствии значений Что произойдет, если элемент stu d e n ts в контекстных данных не является массивом? Если, например, это один объект, то ш аблон просто вы ведет его. О днако секции не отображ аю тся, если значение соответствую щ его элем ента не определено, лож но или представляет собой пустой массив. Ч тобы ш аблон вы водил на экран сообщ ение об отсутствии зн ач ен и я в какой-то секции, H ogan пред о ставл яет то, что в тер м и н ол оги и M u sta ch e н азы вается и н ­ вертированными секциями (in v e rte d sections). С ледую щ ий код после вклю чения в описанны й ранее ш аблон с данны м и студентов выведет на экран сообщ ение о том, что в контексте отсутствую т данны е о студенте: {{Astudents}} <p>No students found.</p> {{/students}} Лямбда-секция — включение в секции нестандартной функциональности Ч тоб ы р а зр а б о т ч и к и м о гл и р а с ш и р я ть ф у н к ц и о н ал ьн о сть M u stach e, стан дарт допускает определен ие тегов секций, обрабаты ваю щ их кон тен т ш аблона путем 214 Глава 7. Шаблонизация веб-приложений вы зова ф ун кц и й , а не перебором элем ентов м ассива. Такие секц ии назы ваю тся лямбда-секциями (sectio n lam bda). В л и сти н ге 7.8 при веден при м ер того, как с пом ощ ью л я м б д а-сек ц и и д обавить поддерж ку парсера M arkdow n в м еханизм визуализац ии ш аблона. В этом примере используется модуль github-flavored-m arkdow n, которы й нуж но установить, введя в ком андной строке команду: npm in s t a l l github-flavored-markdown --dev Е сли вы используете архив исходного кода книги, вы полните команду npm i n s t a l l из каталога ch07-templates/listing7_8. В л и сти н ге 7.8 к о н с тр у к ц и я **Name** в ш аб л он е в и зу а л и з и р у е т с я в разм етк у <strong>N am e</strong> при передаче через парсер M arkdow n, которы й вы зы вается кодом лям бда-секции. Листинг 7.8. Использование лямбда-секции в Hogan const hogan = re q u ire ('h o g a n .js '); const md = require('github-flavored-m arkdow n'); const templateSource = ' {{#markdown}}**Name**: {{name}}{{/markdown}} < -------- const context = { name: 'Rick LaRue', markdown: () => te x t => m d.parse(text) }; const template = hogan.compile(templateSource); co n sole.log(tem plate.render(context)); Включает парсер Markdown. Шаблон Mustache также содержит форматирование Markdown. Контекст шаблона включает лямбда-секциюдля разбора разметки Markdown в шаблоне. С помощ ью лям бда-секций вы смож ете легко реализовать в своих ш аблонах такие вещи, как кэш ирование и преобразования. Компоненты — использование шаблонов в других шаблонах П ри н ап и сан и и ш аблонов м ож но избеж ать неж елательного п овторен и я одного и того ж е кода в нескольких ш аблонах. О дин из способов добиться этого — создать компоненты (p artials). К ом поненты — это ш аблоны, которы е в качестве строитель­ ны х блоков могут вклю чаться в другие ш аблоны. Ещ е один вариант прим енения ком понентов — разбиение слож ны х ш аблонов на более простые. Н априм ер, в листинге 7.9 с помощ ью ком понента код ш аблона, прим еняем ы й для вы вода на экран данны х о студентах, вы деляется из главного шаблона. Листинг 7.9. Использование компонентов в Hogan const hogan = re q u ire ('h o g a n .js '); const studentTemplate = ' < -------<p> Name: {{name}}, Шаблонный код, используемый для компонента. 7.4. Шаблоны Pug 215 Age: {{age}} years old </p> ; const mainTemplate = ' < -------Код основного шаблона. {{#students}} {{>student}} {{/students}} ; const context = { students: [{ name: 'Jane Narwhal', age: 21 }, { name: 'Rick LaRue', age: 26 }] Компиляция основного }; ичастичного шаблона. const template = hogan.compile(mainTemplate); •<— const p a r tia l = hogan.compile(studentTemplate); const html = tem plate.render(context, {student: p a r tia l }); ■<Визуализация основного console.log(htm l); и частичного шаблона. 7.3.3. Тонкая настройка Hogan И сп о л ьзо вать H o g an очень просто — д остаточно и зучить набор тегов, и м ож но приступать к работе. Возможно, в процессе работы вам потребуется изменить всего несколько параметров. Е сли вам не н р авятся ф игурны е скобки в стиле M ustache, достаточно переопреде­ лить используем ы й разделитель путем передачи методу com pile нуж ного разд ел и ­ теля в качестве параметра. В следующем примере показано, как в H ogan выполнить ком п иляцию с разделителем в стиле EJS: hogan.com pile(text, { d elim iters: '<% %>' }); Кроме M ustache, существуют и другие язы ки шаблонов. Одним из проектов, который старается по возм ож ности избавить разработчика от рутины H TM L, явл яется Pug. 7.4. Шаблоны Pug Ш аблони затор P u g (http://pugjs.org), ранее известны й под названием Jad e, предо­ ставляет альтернативный механизм определения H TM L. Ключевое различие между P ug и больш инством других систем ш аблонизации заклю чается в том, что в Pug при м ен яю тся содерж ательны е пробельны е сим волы (w hitespace). П ри создании ш аблонов P ug задаю тся отступы, определяю щие уровень влож енности тегов HTM L. К ром е того, теги H T M L не обязательно явно закры вать, что позволяет исклю чить проблему случайной преж девременной вставки закры ваю щ их тегов или их полного 216 Глава 7. Шаблонизация веб-приложений отсутствия. Благодаря отступам ш аблоны становятся визуально более свободными, что облегчает их сопровож дение. Ч тобы вы лучш е поняли, как это работает, посмотрим, как представляется следу­ ю щ ий ф рагм ент H TM L: <html> <head> <title>W elcome</title> </head> <body> <div id="main" class="content"> <strong>"Hello world!"</strong> </div> </body> </html> Д л я моделирования этой разм етки H T M L м ож но задействовать такой ш аблон Pug: html head t i t l e Welcome body div.content#main strong "Hello world!" В Pug, как и в EJS, допускается внедрение кода Ja v a S c rip t как на стороне сервера, так и на стороне клиента. P u g предлагает такж е дополнительны е механизмы , т а ­ кие как наследование ш аблонов и прим еси (m ixins). Б л агодаря прим есям можно определять просты е м ногократно и сп ользуем ы е м ин и-ш аблоны , п ред н азн ач ен ­ ны е д ля представления в разм етке H T M L наиболее востребованны х визуальны х элементов, таких как списки и поля. П рим еси (m ixins) напоминаю т прим еняем ы е в H ogan.js ком п оненты , у п о м и н ав ш и еся в преды дущ ем разделе. Н асл ед ован и е ш аблонов упрощ ает организацию ш аблонов Pug, необходимы х д ля визуализац ии одной страницы H T M L в нескольких ф айлах. Д алее мы подробно рассм отрим эти механизмы . Ч тобы установить P u g в папку N ode-прилож ения, в ком андной строке введите следую щ ую команду: npm in s ta l l pug --save В этом разделе мы узнаем: О основны е принципы Pug: как задаю тся им ена классов, атрибуты и блочное рас­ ш ирение; О к а к д о бави ть пр о гр ам м н у ю л о ги к у в ш аб лон ы P u g с пом ощ ью встроен н ы х клю чевы х слов; О как организовать ш аблоны с прим енением наследования, блоков и примесей. Д л я начала рассм отрим основы исп ользован ия и синтаксиса Pug. 7.4. Шаблоны Pug 217 7.4.1. Основные сведения о Pug В P u g исп о льзу ю тся те ж е им ена тегов, что и в H T M L , но в P u g м ож но не п р и ­ м е н я т ь о ткр ы в аю щ и е и за к р ы в аю щ и е си м в о л ы < и >, а у р о вен ь в л о ж ен н о сти тегов м ож ет вы р аж аться отступам и. Тег м ож ет вклю чать в себя один и ли больш е C S S -классов, связан н ы х с ним посредством и н стр у к ц и и .< classnam e> . Э лемент d iv, к котором у при м ен ены классы c o n te n t и s id e b a r, м ож ет быть представлен следую щ им образом: div .co n ten t.sid eb ar И д ен ти ф и к ато р ы CSS назн ачаю тся тегам с пом ощ ью п р еф и к са #<ID>. Д л я н а ­ зн ач ен и я ид ен ти ф и катора CSS f e a tu r e d _ c o n te n t в преды дущ ем прим ере мож ет использоваться следую щ ая кон струкция Pug: div.content.sidebar#featured_content Сокращенная запись тега div П оскольку тег d iv очень часто используется в разм етке H T M L , в P ug предусм о­ трена сокращ енная ф орм а его записи. С ледую щ ий прим ер кода генерирует ту же разм етку H T M L , что и предыдущ ий: .content.sidebar#featured_content Теперь, когда вы научились записы вать теги H T M L и соответствую щ ие им классы CSS и идентиф икаторы , перейдем к назначению атрибутов тегов H TM L. Запись атрибутов тегов А трибуты тегов записы ваю тся в круглы х скобках и разделяю тся запяты м и. Ч т о ­ бы определить гиперссылку, которая откроется в другой вкладке, воспользуйтесь следую щ ей записью Pug: a (h re f= 'h ttp ://n o d e js .o r g ', ta rg e t= '_ b la n k ') П оскольку перечисление атрибутов тегов в P ug может привести к появлению д ли н­ ны х строк, ш аблонизатор предлагает более гибкий подход. С ледую щ ая запись Pug вполне корректн а и эквивалентна предыдущ ей: a (h re f= 'h ttp ://n o d e js.o rg ' ta rg e t= '_ b la n k ') М ожно такж е указы вать атрибуты, которые не требую т значений. С ледую щ ий п ри­ мер кода P u g дем онстрирует определение ф орм ы H T M L , которая вклю чает в себя элемент s e l e c t вместе с предварительно вы бранны м параметром: strong Select your fa v o rite food: form 218 Глава 7. Шаблонизация веб-приложений select option(value='C heese') Cheese option(value='T ofu', selected) Tofu Запись контента тегов В преды дущ ем ф рагм енте кода вы уж е вид ел и при м еры кон тен та тегов: S e le c t your f a v o r i t e food после тега stro n g , Cheese после первого тега o p tio n и Tofu после второго тега o p tio n . Это стандартный, но не единственный способ определения контента тегов Pug. Хотя подобны й стиль прекрасно подходит для записи небольш их ф рагм ентов контента, если контента окаж ется много, это м ож ет привести к появлению ш аблонов P u g со слиш ком дли нны м и строками. К ак видно из следую щ его примера, д ля предотвра­ щ ения подобных проблем при записи контента в P ug можно использовать символ | : tex tarea | This is some d efau lt te x t | th a t the user should be | provided with. Е сли тег H T M L (такой, как s t y l e и ли s c r i p t ) приним ает только текст (то есть вл о ж е н и е эл ем ен то в H T M L не д о п у с к а е т с я), си м вол ы | м ож н о оп устить, как в следую щ ем примере: sty le h1 { fo n t-siz e : 6em; color: #9DFF0C; } Н аличие двух разны х способов вы раж ени я длинного и короткого контента тегов упрощ ает создание элегантны х ш аблонов Pug. К роме того, в P ug поддерж ивается альтернативны й м еханизм опи сан ия влож енности, которы й назы вается блочным расширением (block expansion). Упорядочение кода путем блочного расширения В P u g влож енны е теги обычно вы деляю тся отступами, но иногда это мож ет п р и ­ вести к слиш ком больш ом у количеству пробельны х символов. Н апример, в этом ш аблоне P u g с помощ ью отступов определяется простой список ссылок: ul li a (h re f= 'h ttp ://n o d e js .o r g /') Node.js homepage li a (h re f= 'h ttp ://n p m js .o rg /') NPM homepage li a (h re f= 'h ttp ://n o d e b its .o r g /') Nodebits blog 7.4. Шаблоны Pug 219 P ug позволяет записать этот прим ер в более ком пактной ф орм е с использованием блочного расш ирения. В случае блочного расш и рения д ля иллю страци и вл ож ен ­ ности после тега указы вается двоеточие. Следую щ ий код генерирует ту же разметку, что и преды дущ ий, но вместо семи строк кода остается только четыре: ul l i : a (h re f= 'h ttp ://n o d e js .o r g /') Node.js homepage l i : a (h re f= 'h ttp ://n p m js .o rg /') NPM homepage l i : a (h re f= 'h ttp ://n o d e b its .o r g /') Nodebits blog Теперь, когда мы узнали, как в P u g представляется разм етка, давайте выясним, как интегрировать ш аблон P u g в веб-прилож ение. Включение данных в шаблоны Pug Д анны е передаю тся ш аблонам P u g так же, как и ш аблонам EJS. С начала ш аблон ком п и л и р у ется в ф ункцию , ко торая затем вы зы вается вм есте с кон текстом для визуал и зац и и вы вода H T M L . Пример: const pug = re q u ire ('p u g '); const template = 'stro n g #{message}'; const context = { message: 'Hello tem plate!' }; const fn = pug.compile(template); conso le.lo g (fn (co n tex t)); В этом прим ере кода кон струкция #{message} в ш аблоне определяет заполнитель, которы й зам ен яется контекстны м значением. С помощ ью контекстны х значений можно такж е предоставлять значения для атри­ бутов. Н априм ер, следую щ ий ш аблон ви зуали зи рует разм етку <a h r e f = " h t t p : / / google.com "></a>: const pug = re q u ire ('p u g '); const template = 'a (h re f = u r l ) '; const context = { u rl: ' http://google.com ' }; const fn = pug.compile(template); conso le.lo g (fn (co n tex t)); Теперь, когда мы узнали, как в P u g представляется разм етка H T M L и как данны е п р и л о ж е н и й вкл ю ч аю тся в ш аб л о н ы P ug, п р и ш л о врем я узн ать, к ак встроить в ш аблон P u g програм м ную логику. 7.4.2. Программная логика в шаблонах Pug К огда вы передаете ш аблонам P u g д ан ны е п р и ло ж ен и я, вам требуется логика, чтобы как-то обрабаты вать эти данные. P u g позволяет непосредственно внедрять в ш аблоны строки кода Ja v a S c rip t — им енно с помощ ью Ja v a S c rip t вы определяете програм м ную логику в своих ш аблонах. Ч ащ е всего использую тся ин струкции if , 220 Глава 7. Шаблонизация веб-приложений ци клы f o r и объ явл ен и я var. Н о преж де чем углубиться в детали, давайте рассм о­ трим прим ер ш аблона, визуализирую щ его список контактов. Э тот прим ер хорош о иллю стрирует то, как логика P u g мож ет при м ен яться в прилож ении: h3.contacts-header My Contacts i f contacts.len g th each contact in contacts - var fullName = contact.firstN am e + ' ' + contact.lastName .contact-box p fullName i f co n tact.isE d itab le p: a (h re f= '/e d it/+ c o n ta c t.id ) Edit Record p case c o n ta c t.sta tu s when 'A ctive' strong User is activ e in the system when 'In a c tiv e ' em User is inactive when 'Pending' | User has a pending in v ita tio n else p You curren tly do not have any contacts С начала рассм отрим различны е способы обработки вы водим ы х данны х ш аблонизатором P u g при внедрении кода Jav aS crip t. Использование JavaScript-кода в шаблонах Pug Е сли поставить перед строкой кода J a v a S c rip t преф икс -, код Ja v a S c rip t будет вы ­ полнен без возвращ ен ия какого-либо зн ач ен и я в вы водим ы х ш аблоном данных. Если же использовать преф икс =, возвращ аемое этим кодом значение попадет в вы ­ водим ы е данные, причем оно будет экранировано д ля предотвращ ения X SS-атак. Если ж е код, генерируем ы й из Jav aS crip t, экранировать не нужно, м ож но снабдить его преф иксом !=. П еречень доступны х преф иксов приведен в табл. 7.1. Таблица 7.1. Префиксы, используемые для встраивания JavaScript в Pug Префикс Вывод = Выходные данные экранируются (для не заслуживающих доверия или непредсказуемых значений, для защиты от XSS-атак) != Выходные данные не экранируются (для заслуживающих доверия или предсказуемых значений) - Выходные данные отсутствуют В P u g вклю чено несколько наиболее востребованн ы х условны х и итерати вны х инструкций, которы е м ожно записы вать без префиксов: i f , e ls e , case, when, d e fa u lt, u n t i l , w h ile, each и u n le ss. 7.4. Шаблоны Pug 221 К ром е того, в P u g м ож н о о п р ед ел ять перем енны е. В следую щ ем при м ере кода показаны два эквивалентны х способа при сваи вания значений перем енны м в Pug: - count = 0 count = 0 К ак и упом януты е ранее ин струкции с преф иксом -, ин струкции без преф иксов не порож даю т никаких данных. Перебор объектов и массивов В P ug значения, переданны е в контексте, доступны д ля кода Jav aS crip t. В следую ­ щ ем прим ере кода мы считы ваем ш аблон P u g из ф айла, а затем передаем ш аблону контекст с парой сообщ ений, которы е мы собираемся показы вать в массиве: const pug = re q u ire ('p u g '); const fs = r e q u i r e ( 'f s ') ; const template = fs.re a d F ile S y n c ('./te m p la te .p u g '); const context = { messages: [ 'You have logged in s u c c e s s fu lly .', 'Welcome back!' ]}; const fn = pug.compile(template); conso le.lo g (fn (co n tex t)); Ш аблон P u g содерж ит следую щ ий код: - messages.forEach(message => { p= message - }) Ф и н ал ьн ая разм етка H T M L долж на вы глядеть следую щ им образом: <p>You have logged in successfully.</p><p>Welcome back!</p> В P ug такж е поддерж ивается отличная от Ja v a S crip t ф орм а итераций, реализуем ая и н стр у к ц и ей each. С ее пом ощ ью м ож н о легко переб и рать эл ем енты м ассивов и свойства объектов. Вот как вы глядит эквивалент преды дущ его примера, в котором используется и н ­ струкци я each: each message in messages p= message Н емного изм енив код, м ож но вы полнить перебор свойств объекта: each value, key in post div strong #{key} p value 222 Глава 7. Шаблонизация веб-приложений Условная визуализация в шаблоне И ногда ш аблон долж ен реш ить, как им енно долж ны вы водиться те или ины е д ан ­ ные, в зависим ости от значения этих данных. В следую щ ем примере кода показано условие, в котором прим ерно в половине случаев тег s c r i p t вы водится в ф ормате H TM L: - n = Math.round(Math.random() * 1) + 1 - i f (n == 1) { sc rip t alert('Y o u w in !'); - } Условные вы раж ени я P u g такж е м огут запи сы ваться в более чистой альтернатив­ ной форме: - n = Math.round(Math.random() * 1) + 1 i f n == 1 sc rip t alert('Y o u w in !'); Если в условиях использую тся отрицан ия (наприм ер, i f (n != 1)), мож но восполь­ зоваться клю чевы м словом u n le ss: - n = Math.round(Math.random() * 1) + 1 unless n == 1 sc rip t alert('Y o u w in !'); Инструкция case В P u g такж е поддерж ивается условная кон струкция case, напом инаю щ ая sw itch: она позволяет вы брать в ш аблоне нуж ны й вариант вы водим ы х данны х д ля одного из нескольких возм ож ны х сценариев. С л ед у ю щ и й п р и м ер ш аб л о н а д ем о н стр и р у ет и сп о л ьзо в ан и е и н стр у к ц и и c a se д л я вы вода результатов пои ска в блоге тр ем я разн ы м и способами. Е сли в р езу л ь­ тате пои ска ничего не найдено, вы во д и тся соответствую щ ая и н ф орм аци я. Если най дено еди н ствен н ое сообщ ение, оно п о к азы вается полностью . Е сли найдено несколько сообщ ений, то и н стр у к ц и я each перебирает сообщ ения с вы водом их заголовков: case re su lts.le n g th when 0 p No re s u lts found. when 1 p= re su lts[0 ].c o n te n t default each re s u lt in re s u lts p= r e s u l t . t i t l e 7.4. Шаблоны Pug 223 7.4.3. Организация шаблонов Pug П осле определения ш аблонов их нуж но как-то организовать. Как и в случае с п р и ­ кладной логикой, нуж но стрем иться к ум еньш ению разм еров ф айлов ш аблонов. П ри этом предполагается, что один ф айл ш аблона концептуально долж ен соответ­ ствовать одному структурном у элем енту прилож ения, наприм ер странице, врезке или контенту сообщ ения в блоге. В этом разделе мы рассм отрим несколько механизмов, с помощ ью которы х разны е ф айлы ш аблонов д ля визуализац ии контента объединяю тся вместе: О структурировани е ш аблонов путем наследования; О реал и зац и я м акетов путем добавления блока в начало и в конец ш аблона; О вклю чение ш аблона; О м ногократное и сп о льзо ван и е п рограм м ной л оги ки ш аблона с помощ ью п р и ­ месей. Н ачнем с наследования ш аблонов в Pug. Структурирование нескольких шаблонов путем наследования Н ас л ед о в а н и е ш аб л о н о в — один из м ех ан и зм о в с т р у к т у р и р о в а н и я . В р ам к ах этой кон ц еп ц и и ш аблоны рассм атр и ваю тся по анал оги и с классам и в парадигм е объектно-ориентирован ного програм м ировани я. О дин ш аблон м ож ет расш ирять д р у го й ш аблон, к о то р ы й в свою о черед ь м о ж ет р а с ш и р я ть ещ е один ш аблон. К оличество у ровн ей н асл едо ван и я огр ан и ч и вается лиш ь соображ ен и ям и ц е л е­ сообразности. В качестве простого прим ера давайте вы ясним , как путем наследования ш аблонов создать базовую H T M L -обертку д ля контента страницы. В рабочем каталоге соз­ дайте папку template, в которой будет находиться P u g -ф ай л примера. Д л я ш аблона страницы создается ф айл layout.pug со следую щ ей разметкой: html head block t i t l e body block content Ш аб лон layout.pug содерж ит м и н и м ал ьн о е определен ие стран ицы H T M L в виде двух блоков (b lo ck s). С пом ощ ью блоков п ри н асл едован и и ш аблонов зад ается м есто, в ко то р о м будет н а х о д и т ь с я ко н тен т, п р е д о с т а в л я е м ы й п р о и зв о д н ы м ш аблоном . В ф ай л е layout.pug д ля этого служ ат два блока: блок t i t l e позволяет прои зводн ом у ш аблону задать заголовок, а блок c o n te n t — то, что долж но в ы в о ­ д и ться на странице. 224 Глава 7. Шаблонизация веб-приложений Затем в папке ш аблона, находящ ейся в рабочем каталоге, создайте ф айл page.pug. Э тот ф айл ш аблона заполняет блоки t i t l e и co n ten t: extends layout block t i t l e t i t l e Messages block content each message in messages p= message Н аконец, добавьте програм м ную логику из листинга 7.10 (это м оди ф и цирован ная версия преды дущ его прим ера данного раздела), которая вы водит результаты п р и ­ м енени я ш аблона и показы вает наследование в действии. Листинг 7.10. Наследование шаблонов в действии const pug = re q u ire ('p u g '); const f s = r e q u i r e ( 'f s ') ; const tem plateFile = './te m p la te s/p a g e .p u g '; const iterTem plate = fs.readF ileS ync(tem plateF ile); const context = { messages: [ 'You have logged in s u c c e s s fu lly .', 'Welcome back!' ]}; const iterFn = pug.compile( iterTem plate, { filename: tem plateFile } ); c o n so le.lo g (iterF n (co n tex t)); А теперь давайте рассм отрим другое прим енение м еханизм а наследован ия ш абло­ нов: вставку блоков в начало и в конец шаблона. Реализация макетов путем вставки блоков в начало и в конец шаблона В преды дущ ем при м ере блоки, определен ны е в ф ай л е layout.pug, не содерж али контента, что делало задачу вклю чения контента в ш аблон page.pug элем ентарной. Если же блок в унаследованном ш аблоне содержит контент, этот контент мож ет не зам ен яться производны м и ш аблонами, а наращ иваться путем добавления блоков в начало и в конец ш аблона. Это позволит вам определять самый обы чны й контент и добавлять его в ш аблон без зам ены сущ ествую щ его контента. С ледую щ ий шаблон, layout.pug, содержит дополнительны й блок s c r ip ts с контентом (тегом s c r i p t , которы й предназначен для загрузки библиотеки jQ uery): html head - const baseUrl = "h ttp ://a ja x .g o o g le a p is.c o m /a ja x /lib s/jq u ery u i/1 .8 /" block t i t l e block sty le 7.4. Шаблоны Pug 225 block s c rip ts body block content Если вы хотите, чтобы ш аблон page.pug дополнительно загруж ал библиотеку jQuery, мож ете воспользоваться ш аблоном из листинга 7.11. Листинг 7.11. Использование механизма добавления блока в конец шаблона для загрузки дополнительного JavaScript-файла extends layout ■<-------- Расширяет шаблон layout. block t i t l e t i t l e Messages block sty le < -------Определяет блок style. lin k (re l= " sty le sh e e t", href=baseU rl+"them es/flick/jquery-ui. block s c rip ts <.-------- Определяет блок scripts. scrip t(src= b aseU rl+ "jq u ery -u i.js") block content - count = 0 each message in messages - count = count + 1 s c rip t $(() => { $("#message_#{count}").dialog({ height: 140, modal: tru e }); }); != '<div id="message_' + count + '"> ' + message + '< /div>' Н аследование — не единственны й путь объединения нескольких ш аблонов. Также мож но воспользоваться ком андой P u g in c lu d e . Включение шаблонов Ещ е одним инструм ентом организации ш аблонов яв л яетс я ком анда P u g in clu d e . Э та ком анда встраивает в ш аблон контент другого ш аблона. Если в ш аблон layout. pug из преды дущ его прим ера добавить строку in c lu d e f o o te r , получится следую ­ щ ий шаблон: html head block t i t l e block sty le block s c rip ts s c rip t(src = '//a ja x .g o o g le a p is .c o m /a ja x /lib s/jq u e ry /1 .8 /jq u e ry .js') body block content include foo ter Э тот ш аблон вклю чает контент ш аблона footer.pug в разметку, визуализируем ую ш аблоном layout.pug, как показано на рис. 7.3. 226 Глава 7. Шаблонизация веб-приложений Рис. 7.3. Механизм include в Pug предоставляет простой способ включения контента одного шаблона в другой шаблон во время визуализации Э тот м еханизм м ож ет при м ен яться, наприм ер, д ля д обавления в ф айл layout.pug ин ф орм аци и о веб-сайте или элем ентов дизайна. Указав расш ирение, мож но такж е вклю чать в ш аблон ф айлы , не относящ иеся к Pug, например: include tw itter_w idget.htm l Примеси и многократное использование программной логики шаблона Х отя для использования готовых фрагментов кода может прим еняться команда Pug in c lu d e , с ее помощ ью вряд ли удастся создать библиотеку м ногократно исп оль­ зуем ой ф ункциональности, которая м огла бы использоваться как страницам и, так и при лож ениям и. Д л я реш ения этой задачи в P ug служ ит ком анда mixin, которая позволяет определять примеси — м ногократно используем ы е ф рагм енты кода Pug. П рим еси P u g нап ом и наю т ф у н к ц и и Ja v a S c rip t. К ак и ф ун кц и я, прим есь мож ет получать аргументы, которы е учиты ваю тся при генерировании кода Pug. П редполож им , например, что наш е при лож ение обрабаты вает следую щ ую стр у к­ туру данных: const students = [ { name: 'Rick LaRue', age: 23 }, { name: 'Sarah Cathands', age: 25 }, { name: 'Bob Dobbs', age: 37 } ]; Чтобы определить способ вывода списка H TM L, получаемого из значений заданного свойства каж дого объекта, м ож но задействовать следую щ ую примесь: mixin list_ o b ject_ p ro p erty (o b jects, property) ul each object in objects li= object[property] 7.5. Заключение 227 З ат ем м ож н о в о сп о л ьзо в аться при м есью д л я вы вода дан н ы х следую щ ей с т р о ­ кой Pug: mixin list_ o b ject_ p ro p erty (stu d en ts, 'name') П о л ь зу я с ь н асл едо ван и ем ш аблонов, и н с тр у к ц и я м и in c lu d e и п ри м есям и , вы м ож ете без проблем м ногократно исп ользовать визуализирую щ ую разм етку без чрезм ерного «разбухания» ф айлов шаблона. 7.5. Заключение О Ш аблони заторы помогаю т упорядочить прикладную логику и средства в и зу ­ ализации. О В N ode поддерж иваю тся некоторы е популярны е ш аблонизаторы , вклю чая EJS, H ogan.js и Pug. О EJS поддерж ивает просты е управляю щ ие кон струкции и эк ранировани е при незащ ищ енной интерполяции. О H ogan.js — простой ш аб лони затор без у п р авл яю щ и х кон струкций , но с п о д ­ держ кой стандарта M ustache. О P ug — более сложный язы к шаблонов, которы й может выводить разметку H T M L без исп ользован ия угловы х скобок. О Р абота P u g зависи т от пробельны х сим волов при внедрении тегов. 8 Хранение данных в приложениях N ode.js о р и ен ти р у ется на н евер о ятн о ш и р о ки й спектр разр аб о тч и ко в со столь ж е разнообразны м и потребностям и. Н и одна база данны х или технология хран е­ н и я данны х не способна удовлетворить больш инство сценариев использования, об служ иваем ы х N ode. В этой главе предоставлен ш ироки й обзор возм ож ностей хранения данны х наряду с некоторы ми важ ны ми вы сокоуровневы м и концепциями и терминологией. 8.1. Реляционные базы данных Н а протяж ени и почти всего врем ени сущ ествования веб-технологий реляционны е базы данны х зани м али ведущ ее место в области хранени я данных. Э та тема уже рассм атривается во м ногих других книгах и образовательны х программах, поэтому мы не будем тратить много врем ени на ее рассмотрение. Р еляц и он н ы е базы данных, базирую щ иеся на м атем атических кон цепц иях р е л я ­ ц и онной алгебры и теори и м нож еств, известны с 1970-х годов. Схема (schem a) определяет ф орм ат различны х типов данны х и отнош ения, сущ ествую щ ие меж ду этим и типам и. Н априм ер, при построении социальной сети мож но создать типы данны х User и P o st и определить отнош ения «один ко многим» меж ду User и Post. Далее на язы ке SQ L (S tru ctu red Q uery Language) формулирую тся запросы к данным типа «П олучить все сообщ ения, принадлеж ащ ие пользователю с идентиф икатором 123», или на SQL: SELECT * FROM p o st WHERE user_id=123. 8.2. PostgreSQL M yS Q L и P o stg reS Q L (P o stg re s) остаю тся самы ми поп улярн ы м и реляционн ы м и б азам и дан н ы х д л я п р и л о ж ен и й N ode. Р а зл и ч и я м еж ду р ел я ц и о н н ы м и базам и данны х в основном эстетические, поэтом у этот раздел в равной степени относится 8.2. PostgreSQL 229 и к другим реляционн ы м базам данны х — например, M yS Q L в Node. Н о сначала разберем ся, как установить P ostgres на м аш ине разработки. 8.2.1. Установка и настройка С начала нуж но установить P ostgres в ваш ей системе. П ростой команды npm i n s t a l l д ля этого недостаточно. И нструкции по установке зависят от платформ ы . В macOS установка сводится к простой последовательности команд: brew update brew in s t a ll postgres Если в ваш ей системе поддерж ка Postgres уже установлена, вы мож ете столкнуться с проблем ам и при попы тке обновления. В ы полните ин струкции д ля своей п л ат­ ф ормы, чтобы произвести м играцию сущ ествую щ их баз данных, либо полностью сотрите каталог базы данных: # WARNING: w ill delete e x istin g postgres configuration & data rm - r f /u sr/lo c a l/v a r/p o s tg re s Затем и н ици али зируйте и запустите Postgres: in itd b -D /u s r/lo c a l/v a r/p o s tg re s pg_ctl -D /u s r/lo c a l/v a r/p o s tg re s - l lo g file s ta r t Э ти ком анды запускаю т дем она Postgres. Д ем он долж ен запускаться каж ды й раз, когда вы перезагруж аете компьютер. Возможно, вам строит настроить автоматиче­ скую загрузку демона Postgres при запуске; во м ногих сетевых руководствах можно найти описание этого процесса д ля ваш ей операционной системы. Во многих систем ах сем ейства L inux им еется пакет д ля установки Postgres. Д ля системы W indow s следует загрузить програм м у установки на сайте postgresql.org (www.postgresql.org/download/windows/). В составе P o stg re s у стан ав л и в аю тся н екоторы е адм и н и стр ати вн ы е програм м ы командной строки. О знакомьтесь с ними; необходимую инф орм ацию мож но найти в электронной докум ентации. 8.2.2. Создание базы данных П осле того как демон P ostg res заработает, необходимо создать базу данных. Эту п роцедуру достаточно вы полни ть всего один раз. П рощ е всего воспользоваться програм м ой created b из реж и м а ком андной строки. С ледую щ ая ком анда создает базу данны х с им енем a r t i c l e s : createdb a r tic le s 230 Глава 8. Хранение данных в приложениях Е сли операция заверш ается успеш но, ком анда ничего не выводит. Если база д ан ­ ны х с указанны м именем уж е сущ ествует, ком анда ничего не делает и сообщ ает об ошибке. К ак прави ло, п р и л о ж ен и я в лю бой м ом ент врем ен и подклю чены только к одной базе данны х, х отя м ож н о н астр о и ть сразу н еск ольк о баз дан н ы х в зави си м ости от реж и м а, в котором работает база данны х. Во м ногих п р и л о ж ен и я х р а зл и ч а ­ ю тся по к р а й н е й м ере д ва р еж и м а: р еж и м р азр аб о т к и и р еж и м р е ал ь н о й э к с ­ плуатации . Ч тобы удали ть все данны е из сущ ествую щ ей базы данны х, вы полни те команду dropdb с терминала, передав ей им я базы данны х в аргументе: dropdb a r tic le s Ч тобы снова использовать базу данных, необходимо вы полнить команду created b . 8.2.3. Подключение к Postgres из Node С ам ы й поп улярн ы й пакет д ля взаим одействия с P ostgres из N ode назы вается pg. Его м ож но установить при помощ и npm: npm in s ta l l pg --save К огда сервер P ostgres заработает, база данны х будет создана, а пакет pg устан ов­ лен, вы смож ете переходить к использованию базы данны х из Node. П реж де чем вводить какие-либо ком анды к серверу, необходимо создать подклю чение к нему, как показано в листинге 8.1. Листинг 8.1. Подключение к базе данных Параметры конфигурации const pg = re q u ire ('p g '); подключения. const db = new pg.C lient({ database: 'a r t i c l e s ' }); -<— db.con n ect((err, c lie n t) => { i f (e rr) throw e rr; console.log('Connected to d atab ase', db.database); db.end(); -<-------Закрывает подключение кбазе данных, позволяя процессу node завершиться. }); П одробную докум ентацию по p g .C lie n t и другим методам мож но найти на викистранице пакета pg на G itH u b : https://github.com/brianc/node-postgres/wiki. 8.2.4. Определение таблиц Ч тобы храни ть дан ны е в P o stg reS Q L , сначала необходим о определить таблицы и ф орм ат данных, которы е в них будут храниться. П рим ер такого рода приведен в листинге 8.2 (ch08-databases/listing8_3 в архиве исходного кода книги). 8.2. PostgreSQL 231 Листинг 8.2. Определение схемы db.query(' CREATE TABLE IF NOT EXISTS snippets ( id SERIAL, PRIMARY KEY(id), body te x t ); ' , (e rr, re s u lt) => { i f (e rr) throw e rr; console.log('C reated ta b le "sn ip p e ts" '); db.end(); }); 8.2.5. Вставка данных П осле того как таблиц а будет определена, в нее мож но вставить данны е запросам и INSERT (л и сти н г 8.3). Если значение id не указано, то P o stg reS Q L вы берет его за вас. Ч тобы узнать, какой идентиф икатор был вы бран д ля конкретной записи, п ри­ соедините условие RETURNING id к запросу; идентиф икатор будет выведен в строках результата, переданного ф ун кц и и обратного вызова. Листинг 8.3. Вставка данных const body = 'h e llo w orld'; db.query(' INSERT INTO snippets (body) VALUES ( '${body}' ) RETURNING id ' , (e rr, re s u lt) => { i f (e rr) throw e rr; const id = re su lt.ro w s[0 ].id ; co n so le .lo g ('In se rte d row with id %s', id ); db.query(' INSERT INTO snippets (body) VALUES ( '${body}' ) RETURNING id ' , () => { i f (e rr) throw e rr; const id = re su lt.ro w s[0 ].id ; co n so le .lo g ('In se rte d row with id %s', id ); }); }); 8.2.6. Обновление данных П осле того как данны е будут вставлены , их можно будет обновить запросом UPDATE (листинг 8.4). К оличество записей, задействованны х в обновлении, будет доступно в свойстве rowCount результата запроса. П олны й пример для этого листинга содер­ ж и тся в каталоге ch08-databases/listing8_4. 232 Глава 8. Хранение данных в приложениях Листинг 8.4. Обновление данных const id = 1; const body = 'g re e tin g s, w orld'; db.query(' UPDATE snippets SET (body) = ( '${body}' ) WHERE id=${id}; ' , ( e rr, re su lt) => { i f (e rr) throw e rr; console.log('U pdated %s ro w s.', result.rowCount); }); 8.2.7. Запросы на выборку данных О дна из самы х зам ечательны х особенностей реляционн ы х баз данны х — возм ож ­ ность вы полнения сложных произвольны х запросов к данным. Запросы вы полняю т­ ся ком андой SELECT, а простейш ий прим ер такого рода представлен в листинге 8.5. Листинг 8.5. Запрос данных db.query(' SELECT * FROM snippets ORDER BY id ' , ( e rr, re su lt) => { i f (e rr) throw e rr; co n so le.lo g (resu lt.ro w s); }); 8.3. Knex М ногие разработчики предпочитаю т работать с ком андам и SQ L в своих п ри лож е­ н и ях не напрямую , а через абстрактную надстройку. Это ж елание вполне понятно: кон катенаци я строк в ком анды SQ L м ож ет быть громоздким процессом, которы й услож няет понимание и сопровож дение запросов. Сказанное особенно справедливо по отнош ению к я зы к у Ja v a S c rip t, в котором не было син такси са представления м ногострочны х строк до появл ен и я в ES2015 ш аблонны х литералов (см. h ttp s:// developer.mozilla.org/en/docs/Web/favaScript/Reference/Template_literals). Н а рис. 8.1 показана статистика K nex с количеством загрузок, доказы ваю щ их популярность. 16 dependencies 278 dependents ^ version 0 11 7 updated 19 days ago Рис. 8.1. Статистика использования Knex K nex — пакет N ode, реализую щ ий облегченную абстракцию д ля SQL, известную как построитель запросов. П остроитель запросов ф орм ирует строки S Q L через 8.3. Knex 233 декл ар ати вн ы й A P I, которы й им еет м ного общего с генерируем ы м и ком андам и SQL. K nex A P I интуитивен и предсказуем: knex({ c lie n t: 'mysql' }) .s e le c t() .fro m ('u se rs') .where({ id: '123' }) .toSQL(); Э тот вы зов создает парам етризованны й запрос SQ L на диалекте M ySQL: se le c t * from 'u s e r s ' where 'i d ' = ? 8.3.1. jQuery для баз данных Х отя стандарты ANSI и ISO SQ L появились еще в середине 1980-х годов, больш ин­ ство баз данны х продолжает использовать собственные диалекты SQL. PostgreSQ L я вл яется зам етны м исклю чением: эта база данны х мож ет похвастать соблю дением стандарта SQL:2008. П остроитель запросов способен норм ализи ровать различия меж ду диалектам и SQL, предоставляя едины й ун и ф и ц и рован н ы й ин терф ейс для генер и р о ван и я S Q L в р азн ы х технологиях. Такой подход обладает очевидны м и преим ущ ествам и для групп, регулярн о переклю чаю щ ихся меж ду разны м и техн о­ логиям и баз данных. В настоящ ее врем я Knex.js поддерж ивает следую щ ие базы данных: О PostgreS Q L ; О M SSQ L; О M ySQ L; О M ariaD B ; О SQ Lite3; О Oracle. В табл. 8.1 сравниваю тся способы генерирования ком анды INSERT в зависим ости от вы бранной базы данных. Таблица 8.1. Сравнение команд SQL, сгенерированных Knex, для разных баз данных База данных SQL PostgreSQL, SQLite и Oracle insert into "users" ("name", "age") values (?, ?) MySQL и MariaDB insert into 'users' ('n a m e ', 'a g e ') values (?, ?) Microsoft SQL Server insert into [users] ([name], [age]) values (?, ?) K nex поддерж ивает обещания (prom ises) и обратны е вы зовы в стиле Node. 234 Глава 8. Хранение данных в приложениях 8.3.2. Подключение и выполнение запросов в Knex В отличие от м ногих других построителей запросов, K nex такж е мож ет п одклю ­ чаться и вы полнять запросы к вы бранном у драйверу базы данны х за вас. d b ( 'a r t i c l e s ') . s e l e c t ( 't i t l e ') .where({ t i t l e : 'Today's News' }) .th e n (a rtic le s => { c o n so le .lo g (a rtic le s); }); П о ум олчанию запросы K nex возвращ аю т обещ ания, но они такж е поддерж иваю т соглаш ения обратного вы зова N ode с использованием .asC a llb ac k : d b ( 'a r t i c l e s ') . s e l e c t ( 't i t l e ') .where({ t i t l e : 'Today's News' }) .asC allb ack ((err, a r tic le s ) => { i f (e rr) throw e rr; c o n so le .lo g (a rtic le s); }); В главе 3 мы взаимодействовали с базой данных SQLite непосредственно при помощи пакета sqlite3. Этот A PI можно переписать с использованием Knex. Прежде чем запу­ скать этот пример, сначала проверьте из npm, что пакеты knex и sqlite3 установлены: npm in s ta l l knex@~0.12.0 sqlite3@~3.1.0 --save В листинге 8.6 sqlite исп ользуется д ля р еал и зац и и простой м одели A r tic le . С о ­ храните ф айл под именем db.js; он будет исп ользоваться в листинге 8.7 д ля в за и ­ м одействия с базой данных. Листинг 8.6. Использование Knex для подключения и выдачи запросов к sqlite3 const knex = re q u ire ('k n e x '); const db = knex({ c lie n t: 's q l i t e 3 ', connection: { filename: 't l d r . s q l i t e ' }, useNullAsDefault: true }); Выбор этого режима по умолчанию лучше работает при смене подсистемы баз данных. module.exports = () => { return d b .sch em a.createT ab leIfN o tE x ists('articles', ta b le => { ta b le .in c re m e n ts ('id ').p rim a ry (); •<Определяет первичный ключ с именем t a b l e . s t r i n g ( 't i t l e ') ; «id», значение которого автоматически t a b le .te x t( 'c o n te n t') ; увеличивается при вставке. }); }; 8.3. Knex 235 m odule.exports.A rticle = { a ll ( ) { return d b ( 'a r t i c l e s ') .o r d e r B y ( 'ti t l e ') ; }, fin d (id ) { return d b ('a rtic le s ').w h e re ({ id } ) . f i r s t ( ) ; }, create(d ata) { return d b ( 'a r tic le s ') .in s e r t( d a ta ) ; }, d ele te (id ) { return d b ('a rtic le s ').d e l().w h e re ({ id }); } }; Листинг 8.7. Взаимодействие с API на базе Knex d b ().th e n (() => { d b .A rtic le .c rea te ({ t i t l e : 'my a r t i c l e ', content: 'a r t i c l e content' } ). th en (() => { d b .A r tic le .a ll( ) .th e n ( a r tic le s => { c o n so le .lo g (a rtic le s); p ro c e ss.e x it(); }); }); }) .c a tc h (e rr => { throw err }); S Q L ite требует м ин им альной настройки: вам не нуж но загруж ать демон сервера или создавать базы данны х за пределам и прилож ения. SQ L ite записы вает все д ан ­ ные в один ф айл. В ы полнив преды дущ ий код, вы увидите, что в текущ ем каталоге п о яви л ся ф айл articles.sqlite. Ч тобы уничтож ить базу данны х SQ L ite, достаточно удалить всего один файл: rm a r t i c l e s .s q l i te SQ L ite такж е поддерж ивает реж им работы в памяти, при котором запись на диск вообщ е не осущ ествляется. Э тот реж им обычно используется д ля ускорен ия вы ­ п ол н ен и я авто м атизи рованны х тестов. Д л я н астройки реж и м а работы в пам яти и сп ользу ется специ альное и м я ф ай л а :memory:. П ри откры тии н ескольких под ­ клю чений к ф ай л у :memory: каж дое подклю чение получает собственную и зо л и р о ­ ванную базу данных: const db = knex({ c lie n t: 's q l i t e 3 ', connection: { filename: ':memory:' }, useNullAsDefault: true }); 236 Глава 8. Хранение данных в приложениях 8.3.3. Переход на другую базу данных Благодаря использованию Knex, листинги 8.6 и 8.7 позволяю т легко переклю чаться с sqlite3 на P ostgreS Q L . Д л я взаим одействия K nex с сервером P o stg reS Q L необ­ ходим а у стан овка и запуск пакета pg. Установите пакет pg в папку ли сти н га 8.7 (ch08-databases/listing8_7 в коде кн иги) и не забудьте создать соответствую щ ую базу данны х програм м ой ком андной строки P o stg reS Q L crea ted b : npm in s ta l l pg --save createdb a r tic le s Все изм енения кода, необходимые д ля использования новой базы данных, вносятся в кон ф и гурац ии Knex; в остальном A P I и схема исп ользован ия идентичны: const db = knex({ c lie n t: 'p g ', connection: { database: 'a r t i c l e s ' } }) С тоит заметить, что в реальной ситуации вам такж е придется вы полнить миграцию всех сущ ествую щ их данных. 8.3.4. Остерегайтесь ненадежных абстракций П остроители запросов способны норм ализовать синтаксис SQL, но не справятся с норм ализацией поведения. Н екоторы е возмож ности поддерж иваю тся только кон­ кретны м и базам и данных, и некоторы е базы данны х могут п роявлять соверш енно иное поведение при идентичны х запросах. Н апример, ниж е приведены два способа определения первичного клю ча с использованием Knex: О t a b l e . i n c r e m e n t s ( 'i d ') . p r i m a r y ( ) ; О t a b l e . i n t e g e r ( 'i d ') . p r i m a r y ( ) ; О ба варианта работаю т в S Q L ite3 так, как ож идается, но второй вариант вы зывает ош ибку в P o stg reS Q L при вставке новой записи: "null value in column "id" v io la te s n o t-n u ll constraint" Зн ачениям , вставленны м в S Q L ite с неопределенны м первичны м клю чом, будет присвоен автом атически увел и ч и ваем ы й и д ен ти ф и катор — незави си м о от того, было ли явно настроено автоматическое увеличение для столбца первичного ключа. С другой стороны, P o stg reS Q L требует, чтобы столбцы с автоматическим у в ел и ­ чением о пред елялись явн о. М еж ду базам и дан ны х сущ ествует много подобны х поведенческих различий, и некоторы е р азл и ч и я могут оставаться незам етны ми, 8.4. MySQL и PostgreSQL 237 не при водя к видим ы м ош ибкам. Е сли вы реш или перейти на другую базу данных, обязательно проведите тщ ательное тестирование. 8.4. MySQL и PostgreSQL К ак M ySQ L, так и P o stg reS Q L — мощ ные, проверенны е врем енем базы данных, и во м ногих проектах выбор одного или другого варианта практически ни на что не влияет. М ногие различия, которые не играют роли, пока не возникнет необходимость в масш табировании проекта, существуют на границе интерфейса, предоставляемого разработчику прилож ений, или ниж е нее. П одробны й сравнительны й анализ р еляционн ы х баз данны х в основном выходит за рам ки книги, поскольку д ан н ая тем а достаточно слож на. Н екоторы е важ ны е разл и ч и я перечислены ниже: О PostgreS Q L поддерж ивает более вы разительны е типы данных, вклю чая массивы, JS O N и типы, определяем ы е пользователем. О P o stg reS Q L поддерж ивает встроенны й м еханизм полнотекстового поиска. О P o stg reS Q L в полной м ере поддерж ивает стандарт ANSI SQL:2008. О П оддерж ка репл и кац и и в P o stg reS Q L не настолько мощ на и не так проверена на практике, как в M ySQ L. О M yS Q L старш е и обладает более м ногочисленны м сообщ еством. Д л я M ySQ L сущ ествует больш е совм естимы х инструм ентов и ресурсов. О Сообщ ество M yS Q L в больш ей степени ф рагм ентировано из-за нетривиальны х р азл и ч и й м еж ду в етв ям и (н ап р и м ер , M ariaD B и W ebS caleS Q L от Facebook, Google, T w itter и т. д.). О Ядро хранения данных M ySQL с подключаемыми модулями создает больше проблем с пониманием, администрированием и настройкой. Тем не менее его можно рассма­ тривать как полезную возможность для более точной настройки быстродействия. M yS Q L и P o stg re S Q L п р о я в л я ю т р азн ы е х ар ак тер и сти к и б ы строд ей стви я при м асш таби ровании в зависи м ости от типа рабочей нагрузки. О собенности ваш ей нагрузки м огут проявиться только в процессе стан овлен ия проекта. Н а м ногих и н терн ет-ресурсах представлены гораздо более глубокие сравнени я реляционн ы х баз данных: О w w w .digitalocean.com /com m unity/tutorials/sqlite-vs-m ysql-vs-postgresql-a- comparison-of-relational-database-management-systems; О https://blog.udemy.com/mysql-vs-postgresql/; О https://eng.uber.com/mysql-migration/. 238 Глава 8. Хранение данных в приложениях И сходн ы й вы бор базы дан ны х вряд ли зн ачительно повл и яет на успех проекта, поэтому не стоит слиш ком сильно беспокоиться по поводу этого реш ения. Позднее вы сможете переклю читься на другую базу данных, но, скорее всего, возмож ностей Postgres будет достаточно для лю бых потребностей в ф ункциональности и м асш та­ бируемости. О днако если вы вы бираете м еж ду нескольким и базам и данных, вам стоит позн аком иться с идеей гарантий ACID. 8.5. Гарантии ACID С о к р ащ ен и е «A C ID » оп и сы вает набор ж ел ател ьн ы х свойств, которы м и д о л ж ­ ны обладать т р ан закц и и баз данных: атом арность (A to m ic ity ), согласованность (C o n siste n c y ), и золи рован н ость (Iso la tio n ) и устойчивость (D u ra b ility ). Точные определения этих терм инов м огут различаться. К ак правило, чем ж естче система гарантирует свойства A CID , тем значительнее потери по быстродействию . К ласси­ ф и кац и я A C ID — распространенны й способ, которы м разработчик м ож ет быстро опи сать до сто и н ства и недостатки кон кретного реш ен и я (нап ри м ер, присущ ие систем ам N oSQ L ). 8.5.1. Атомарность Атомарная транзакц ия не мож ет быть вы полнена частично: либо вся операция за ­ верш ается, либо база данны х остается без изм енений. Н апример, если транзакц ия заклю чается в удален ии всех ком м ентариев некоторого пользователя, то либо все ком м ентарии будут удалены, либо не будет удален ни один. С итуация, при которой часть ком м ентариев удаляется, а другая часть остается, невозмож на. П р и н ц и п ато м а р н о с ти д о л ж ен д ей с тв о в а т ь д аж е в случае си стем н о й ош иб ки и л и сбоя п и тан и я. В д ан ном случае « атом арн ость» следует п он им ать как « н е ­ делим ость». 8.5.2. Согласованность П р и зав ер ш ен и и усп еш н о й т р а н за к ц и и долж н ы собл ю даться все огран и ч ен и я согласованности (целостности данны х), определенны е в системе. П рим еры таких ограничений — уникальность первичны х клю чей, соответствие данны х некоторой схеме и т. д. Т ранзакции, которы е приводят к некорректном у состоянию данных, обы чно приводят к откату транзакций, хотя незначительны е проблем ы могут р а з­ реш аться автом атически (н ап ри м ер, приведени е дан н ы х к п рави льн ой ф орм е). Э тот вид со гласован ности не следует путать с согласован ностью ( C ) в теорем е CAP, которая относится к единству представления данны х для всех пользователей распределенного хранилищ а. 8.6. NoSQL 239 8.5.3. Изоляция Изолированные тр анзакц ии долж ны приводить к одинаковому результату н езави ­ симо от их параллельного и ли последовательного прим енения. Уровень изоляции, предоставляем ы й системой, напрям ую влияет на ее способность вы полнять парал­ лельны е операции. Н аи вн ая схема и зо л яц и и основана на прим енении глобальной блокировки ; вся база данны х блокируется на врем я транзакции, в результате чего все транзакции ф актически вы полняю тся последовательно. Такая схема предостав­ л яет сильны е гарантии изоляци и, но она патологически неэф ф ективна: тр ан зак­ ции, работаю щ ие с полностью несвязанны м и наборам и данных, блокирую тся без необходим ости (напри м ер, добавление пользователем ком м ентария в идеале не долж но мешать обновлению п роф иля другим пользователем). Н а практике системы предоставляю т разны е уровни изо л яц и и с прим енением схем блокировки с разной избирательностью (н а уровне таблиц, строк или полей). Б олее слож ны е системы могут даж е оптим истически пы таться проводить все транзакц ии с м иним альной б локировкой, а затем заново п о вто р ять тр ан зак ц и и со сни ж ением детал и зац и и в случае вы явл ен и я конф ликтов. 8.5.4. Устойчивость Устойчивость транзакции определяет степень, в которой ее эф ф ект гарантированно сохранится даж е при перезапуске, сбое питания, системны х ош ибках и даж е сбоях об орудовани я. Н априм ер, при лож ение, использую щ ее S Q L ite в реж и м е работы в памяти, не обладает устойчивостью транзакций; все данны е теряю тся при выходе из процесса. С другой стороны, S Q L ite в реж им е запи си данны х на диск будет об­ ладать хорош ей транзакц ионн ой устойчивостью , потому что данны е сохраняю тся даж е после перезапуска маш ины. К азалось бы, прощ е некуда: зап и ш и те д ан ны е на д иск — вуаля, у вас п о я в л я ю т ­ ся устойчи вы е тр ан закц и и . Н о д и сковы й в в о д /в ы во д я в л я е т с я одной из самых м едленн ы х операций, вы п о л н яем ы х ваш им прилож ением , и м ож ет бы стро стать серьезн ы м « у зк и м м естом » ваш его п р и л о ж е н и я даж е п ри у м ерен н ы х ур о вн ях м асш таби рован и я. Н еко то р ы е базы дан ны х поддерж иваю т разн ы е степени к о м ­ пром и сса по устойчи вости , которы е м огут обеспечивать при ем лем ое б ы стродей­ ствие системы . 8.6. NoSQL Х ранилищ а данных, которые не вписываю тся в реляционную модель, объединяются под термином NoSQL. Так как в наш и дни некоторы е базы данны х N oSQ L поддер­ ж иваю т SQ L, смы сл терм ина N oSQ L ближ е к определению «нереляционны й» или придум анном у задним числом сокращ ению «N ot O nly SQ L» («не только SQ L»). 240 Глава 8. Хранение данных в приложениях П одмнож ество парадигм и прим еры баз данных, которые мож но отнести к NoSQL: О пары « к л ю ч -зн а ч е н и е » /к о р т е ж и — D y n am o D B , L evelD B , R edis, etcd , R iak, A erospike, Berkeley DB; О граф овы е базы данны х — N eo4J, O rien tD B ; О документные базы данных — CouchD B, M ongoDB, Elastic (formerly Elasticsearch); О столбцовы е базы данны х — C assandra, HBase; О базы данны х врем енны х рядов — G raphite, InfluxD B , R R D tool; О м ного п ар ад и гм ен н ы е базы дан н ы х — C ouchbase (д о к у м ен тн ая база данны х, храни ли щ е пар «клю ч-значение», распределенны й кэш ). З а более подробны м списком баз дан ны х N oS Q L обращ айтесь по адресу h ttp :// nosql-database.org/. К онцепции N oSQ L бывает трудно усвоить, если вы работали только с реляционными базами данных, потому что применение N oSQ L часто противоречит установивш ейся практике: отсутствие определенных схем. Дублирование данных. Слабое соблюдение ограничений. С истем ы N oSQ L берут на себя обязанности, обычно закрепляем ы е за базам и данны х, и вы водят их в область ответственности прилож ения. Н а первы й взгляд все это вы глядит сомнительно. О бы чно небольш ое количество схем доступа создает основную нагрузку на базу данны х — например, запросы , генерирую щ ие начальны й экран прилож ения, для которого необходим о получить несколько объектов предм етной области. С т ан ­ дартны м прием ом повы ш ения производительности ч тен и я в реляционн ы х базах данны х яв л яется нормализация — запросы обрабаты ваю тся и при водятся к форме, сокращ аю щ ей количество операций чтения, необходимых для потребления данных клиентом. Д анны е N oS Q L чащ е д ен орм али зирую тся по ум олчанию , и весь этап м од ел и ро­ вани я предм етной области м ож ет быть полностью опущен. Такой подход предот­ вращ ает чрезм ерное техническое услож нение м одели данны х, позволяет быстрее отрабаты вать изм ен ен ия и в целом приводит к более простой и производительной архитектуре. 8.7. Распределенные базы данных П ри лож ен и е м ож ет м асш таби роваться вертикально (за счет повы ш ения п р о и з­ во д и тел ьн о сти м аш и н ) и л и го р и зо н тал ьн о (за счет д об авл ен и я новы х м аш и н). В е р т и к а л ь н о е м а с ш та б и р о в а н и е обы чно прощ е р еа л и зу ет ся , но в о зм о ж н о сти о б о р у д о в а н и я о гр ан и ч и в аю т в о зм о ж н о с ти м а с ш т а б и р о в а н и я о д н ой м аш и ны . К ром е того, вертикальное м асш таби рование такж е бы стро дорож ает. Н апротив, 8.8. MongoDB 241 горизон тальное м асш таби рование обладает нам ного больш им потенц иалом для роста при д о бавл ен и и новы х процессов и новы х м аш ин. З а все это при ходи тся платить слож ностью у п равлени я больш им количеством «подвиж ны х частей». Все растущ ие системы со временем достигаю т точки, в которой приходится проводить горизонтальное м асш табирование. Распределенны е базы данны х с самого начала проектирую тся с расчетом на гори ­ зонтальное масш табирование. Х ранение данны х на нескольких м аш инах повыш ает устойчивость данных за счет устранения «единых точек сбоя». М ногие реляционные системы в некоторой степени способны вы полнять горизонтальное м асш таби ро­ вание в ф орм е сегментации, р епликации « гл авны й/под чиненн ы й» и ли « гл а в н ы й / главны й», хотя даж е с этим и ф у н кц и ям и реляционн ы е системы не рассчитаны на масш табирование более чем для нескольких сотен узлов. Например, верхний предел кластера M yS Q L составляет 255 узлов. Р аспределенны е базы данных, напротив, могут м асш табироваться на ты сячи узлов по самой архитектуре. 8.8. MongoDB M ongoD B — докум ентно-ориентированная распределенная база данных, исклю чи­ тельно популярная среди разработчиков Node. О на стоит на первом месте в модном технологическом стеке M EAN (M ongoD B , Express, Angular, N ode) и часто становится одной из первы х баз данных, с которой сталкиваю тся лю ди в начале работы с Node. Н а рис. 8.2 показано, насколько популярен модуль m ongodb в npm. 1,893 dependents updated 6 hours ago Рис. 8.2. Статистика использования MongoDB M ongoD B при влекает более чем справедливую долю кри ти ки ; несм отря на это, M ongoD B остается надеж ны м хр ан и ли щ ем дан ны х д л я м ногих разработчиков. П оддерж ка M ongoD B развернута во м ногих видны х ком паниях, вклю чая Adobe, LinkedIn и eBay, и даже используется в компоненте Больш ого адронного коллайдера в Е вропейском центре ядерн ы х исследований (C E R N ). В базе данны х M ongoD B документы хранятся в бессхемных коллекциях. Документ не обязан строиться по заранее определенной схеме, а докум енты одной ко л л е к ­ ц ии не о бязан ы совм естно и сп о льзо вать одну схему. Таким образом, M ongoD B н аделяется значительной гибкостью , хотя на прилож ение л ож и тся брем я по под ­ держ анию прогнозируем ой структуры докум ента (гаран ти рован н ая согласован ­ ность — «C» в A C ID ). 242 Глава 8. Хранение данных в приложениях 8.8.1. Установка и настройка П акет M ongoD B долж ен бы ть у стан овлен в ваш ей систем е. П роцесс устан овки отличается в зависи м ости от платф орм ы . В m acO S установка сводится к простой команде: brew in s t a l l mongodb С ервер M ongoD B запускается исполняем ы м ф айлом mongod: mongod --config /usr/local/etc/m ongod.conf С ам ы м популярны м драйвером M ongoD B явл яется оф ици альн ы й пакет mongodb, созданны й К ристианом А мором К валхейм ом (C h ristia n A m or Kvalheim): npm in s ta l l mongodb@A2.1.0 --save П ользователям W indow s следует учитывать, что для установки драйвера необходим ф айл msbuild.exe, устанавливаем ы й M icrosoft V isual Studio. 8.8.2. Подключение к MongoDB П осле установки пакета m ongodb и запуска сервера m ongod вы смож ете подклю ­ читься в качестве кли ен та из Node: Листинг 8.8. Подключение к MongoDB const { MongoClient } = require('m ongodb'); M ongoC lient.connect('m ongodb://localhost:27017/articles') .then(db => { co n so le.lo g ('C lien t read y '); d b .clo se(); }, co n so le.erro r); О бработчику успеш ного заверш ен ия передается экзем пляр кли ен та базы данных, из которого вы полняю тся все ком анды базы данных. Больш инство взаимодействий с базой данных осущ ествляется через A PI коллекций: О c o l l e c t i o n . i n s e r t ( d o c ) — вставка одного или нескольких документов; О c o l le c ti o n .f i n d ( q u e r y ) — поиск докум ентов, соответствую щ их запросу; О c o lle c tio n .re m o v e (q u e ry ) — удаление документов, соответствую щ их запросу; О c o l l e c t i o n .d r o p ( ) — удаление всей коллекции; О c o lle c tio n .u p d a te (q u e ry ) — обновление документов, соответствую щ их запросу; О c o lle c tio n .c o u n t( q u e r y ) — подсчет документов, соответствую щ их запросу. 8.8. MongoDB 243 Такие операции, как поиск, вставка и удаление, обычно сущ ествую т в нескольких разнови дн остях — в зависи м ости от того, работаете л и вы с одним или м ногим и значениям и. П римеры: О c o lle c tio n .in s e r tO n e ( d o c ) — вставка одного документа; О c o lle c tio n .in s e r tM a n y ( [d o c 1 , doc2 ]) — вставка нескольких документов; О c o lle c tio n .f in d O n e (q u e r y ) — поиск одного докум ента, соответствую щ его з а ­ просу; О co llectio n .u p d ateM an y (q u ery ) — обновление всех документов, соответствующ их запросу. 8.8.3. Вставка документов c o l l e c t i o n . i n s e r t O n e в став л я ет один объект в ко л л екц и ю как д окум ент (л и с ­ тинг 8.9). О бработчику успеш ного заверш ен ия передается объект с метаданными, относящ и м ися к состоянию операции. Листинг 8.9. Вставка документа const a r tic le = { t i t l e : 'I lik e cake', content: ' I t is quite good.' }; d b .c o lle c tio n ( 'a r tic le s ') .in se rtO n e (a rticle ) .th e n (re s u lt => { c o n s o le .lo g (re su lt.in se rte d Id ); <c o n s o le .lo g (a rtic le ._ id ); B •<— }); Если у документа нет свойства _id, создается новый идентификатор. Сгенерированное значение хранится в insertid. Исходный объект, определяющий документ, изменяется: в него добавляется поле _id. Вызов insertM any работает аналогично, если не считать того, что он получает массив из нескольких докум ентов. О твет in sertM any будет содерж ать массив in s e r te d I d s в порядке передачи докум ентов вместо одного значения in s e r te d I d . 8.8.4. Получение информации Методы, читаю щ ие документы из коллекции (такие, как fin d , update и remove), полу­ чаю т аргумент, используем ы й для идентиф икации документов. П ростейш ая форма аргум ента запроса представляет собой объект, которы й исп ользуется M ongoD B д ля подбора докум ентов с такой ж е структурой и тем и ж е значениям и. Н апример, следую щ ий ф рагм ент находит все статьи с заголовком «I like cake»: d b .c o lle c tio n ( 'a r tic le s ') .find({ t i t l e : 'I lik e cake' }) 244 Глава 8. Хранение данных в приложениях .to A rra y ().th e n (re su lts => { c o n so le .lo g (re su lts); < -------}); Массив документов, соответствующих запросу. Запр о сы использую тся для поиска объектов по их уни кальном у значению _id: collection.findO ne({ _id: somelD }) И л и для поиска с прим енением оператора запроса: d b .c o lle c tio n ( 'a r tic le s ') .f in d ( { title : { $regex: /cake$/I }) Заголовок заканчивается словом ‘cake’ (без учета регистра символов). В язы ке запросов M ongoD B поддерживаются разные операторы запросов. Например: О $eq — равно заданном у значению ; О $neq — не равно заданном у значению ; О $ in — присутствует в массиве; О $nin — не присутствует в массиве; О $ l t , $ l t e , $gt, $ g te — б ольш е/м еньш е или равно; О $near — геопространственное значение расположено вблизи заданных координат; О $not, $and, $or, $nor — логические операторы. Э ти значения могут объединяться для поиска практически по любым условиям. Так создается в вы сш ей степени понятны й, слож ны й и вы разительны й я зы к запросов. З а д ополнительной инф орм аци ей о запросах и операторах запросов обращ айтесь по адресу https://docs.m ongodb.com/manual/reference/operator/query/. В листинге 8.10 приведен прим ер р еал и зац и и приведенного выш е A P I A r t i c l e s с M ongoD B ; при этом в н еш н и й и н тер ф ей с остается п р ак ти ч ески идентичны м . С охраните ф айл под именем db.js (ф ай л listing8_10/db.js в исходном коде книги). Листинг 8.10. Реализация API Article с MongoDB const { MongoClient, ObjectID } = require('m ongodb'); l e t db; module.exports = () => { return MongoClient .connect('m ongodb ://lo calh o st:2 7 0 1 7 /articles') .th e n ((c lie n t) => { db = c lie n t; }); }; m odule.exports.A rticle = { a ll() { return d b .c o lle c tio n ( 'a r tic le s 2 ') .f in d ( ) .s o r t( { t i t l e : 1 }).toA rray(); 8.8. MongoDB 245 }, find(_id) { Добавляет поддержку передачи _id i f (typeof _id !== 'o b je c t') _id = ObjectID(_id); -<—I в формате String иObjectID. return d b .c o lle c tio n ('a rtic le s 2 ').fin d O n e ({ _id }); }, create(d ata) { return d b .c o lle c tio n ('a rtic le s 2 ').in s e rtO n e (d a ta , { w: 1 }); }, d elete(_ id ) { i f (typeof _id !== 'o b je c t') _id = ObjectID(_id); return d b .c o lle c tio n ('a rtic le s 2 ').d e le te O n e ({ _id }, { w: 1 }); } }; С ледую щ ий ф рагм ент показы вает, как исп ользовать л и сти н г 8.10 (listin g 8_1 0 / index.js в прим ере кода): const db = r e q u ir e ( './d b ') ; d b ().th e n (() => { d b .A rtic le .c rea te ({ t i t l e : 'An a r t i c l e ! ' } ).th e n (() => { d b .A r tic le .a ll( ) .th e n ( a r tic le s => { c o n so le .lo g (a rtic le s); p ro c e ss.e x it(); }); }); }); В этом ф рагменте обещ ание из листинга 8.10 используется для подклю чения к базе данны х, после чего объект создается методом c r e a te класса A r tic le . П осле этого ф рагм ент загруж ает все статьи и вы водит их на консоль. 8.8.5. Идентификаторы MongoDB И дентиф икаторы M ongoD B кодирую тся в формате BSO N (B inary JS O N ). Свойство _ id докум ента представляет собой объект J a v a S c rip t O b jec t, и н капсулирую щ и й значение O bjectID в ф орм ате BSON. M ongoD B использует B SO N для внутреннего представления докум ентов и как ф орм ат передачи данных. Ф орм ат B SO N более ком пактен, чем JS O N , и бы стрее разбирается; это означает ускорен ие операций с базой данны х с м еньш им и затратам и ресурсов. Зн ач ен и е B SO N O bjectID — не простая последовательность байтов; в нем зако д и ­ рована ин ф орм аци я о том, где и как был сгенерирован идентиф икатор. Н апример, первы е четы ре байта O bjectID содерж ат временную метку, и вам не придется вклю ­ чать свойство c re a te d A t в свои документы: const id = new ObjectID(61bd7f57bf1532835dd6174b); id.getTimestamp(); •<-------getTimestamp возвращает JavaScript / Date: 2016-07-08T14:49:05.000Z. 246 Глава 8. Хранение данных в приложениях З а дополнительной информ ацией о формате ObjectlD обращайтесь по адресу h ttp s:// docs.mongodb.com/manual/reference/method/ObjectId/. М ож ет показаться, что объекты O bjectID похож и на строки из-за особенностей их вы вода на терминал; тем не менее они явл яю тся объектами. О бъекты O bjectID под­ вержены классической проблеме сравнения объектов: полностью эквивалентны е на первы й взгляд значения считаю тся неэквивалентны м и, потому что они относятся к разны м объектам. В следую щ ем ф рагм енте одно зн ач ен и е и зв л ек ается дваж ды . З атем при помощ и встр о ен н о го м о д у л я N o d e a s s e rt м ы п ы таем ся п р о в ер и ть на эк в и в а л е н т н о с ть объекты и их и дентиф икаторы , но в обоих случаях проверка дает отрицательны й результат: const A rticles = d b .c o lle c tio n ( 'a r tic le s ') ; A rtic le s .f in d ( ) .th e n (a r tic le s => { const a r t i c l e l = a r tic le s [ 0 ] ; return A rticles .findOne({_id: a rtic le 1 ._ id } ) .th e n (a rtic le 2 => { a s s e rt.e q u a l(a rtic le 2 ._ id , a rtic le 1 ._ id ); }); }); Д л я эти х п р о вер о к вы д аю тся со о бщ ен и я об ош ибке, которы е сн ач ала каж утся н еп онятн ы м и, так как ф акти чески е зн ач ен и я на первы й взгляд совпадаю т с ож и ­ даемы ми: operator: equal expected: 577f6b45549a3b991e1c3c18 actu al: 577f6b45549a3b991e1c3c18 operator: equal expected: { _id: 577f6b45549a3b991e1c3c18, t i t l e : 'attractive-m oney' . . . } actu al: { _id: 577f6b45549a3b991e1c3c18, t i t l e : 'attractive-m oney' . . . } Д л я п р ав и л ьн о й п р о вер к и экв и в ал ен тн о сти следует и сп ользовать м етод e q u a l объектов O bjectID , доступны й для всех _id. Также мож но вы полнить приведение типа ид ентиф икаторов и сравнить их как строки или ж е воспользоваться методом deepE quals — вроде того, которы й доступен во встроенном м одуле N ode assert: a r tic le 1 ._ id .e q u a ls (a rtic le 2 ._ id ); S trin g (a rtic le 1 ._ id ) === S trin g (a rtic le 2 ._ id ); asse rt.d e e p E q u al(a rtic le 1 ._ id , a r tic le 2 ._ id ) ; I Выдает исключение, если усл°вие -<— ' не выполняется. И д ен ти ф и к а то р ы , перед ан н ы е д р ай в ер у N ode m ongodb, долж н ы п р ед ставл ять собой O bjectID в ф орм ате BSO N . С трока преобразуется в O bjectID при помощ и конструктора ObjectID: 8.8. MongoDB 247 const { ObjectID } = require('m ongodb'); const stringID = '577f6b45549a3b991e1c3c18'; const bsonID = new ObjectID(stringID); Там, где это возможно, следует поддерживать формат BSON; затраты на марш ализацию в строковую ф орму и обратно снижаю т вы игрыш по быстродействию, которого M ongoD B стрем и тся достичь за счет работы с кл и ен тск и м и и д ентиф икаторам и в ф орм ате BSON. З а дополнительной инф орм ацией о ф ормате B SO N обращ айтесь по адресу http://bsonspec.org/. 8.8.6. Реплицированные наборы Распределенная ф ункциональность M ongoD B в основном выходит за рам ки книги, но в этом разделе будут кратко представлены основы работы с репл и ц и рован н ы ­ м и наборами. М ногие процессы m ongod могут вы п олн яться как у зл ы /у ч астн и ки реплицированного набора (rep lica set). Р еп лиц ированн ы й набор состоит из одного первичного у зл а и м нож ества вторичны х узлов. К аж дом у узлу реплицированного набора долж ен бы ть назначен у ни кальны й порт и каталог д ля хранени я данных. Э кзем п л я р ы не м огут совм естно и сп о льзо в ать порты и л и каталоги, а каталоги долж ны сущ ествовать до запуска. В л и сти н ге 8.11 д л я каж дого у ч астн и к а со зд ается у н и к ал ьн ы й каталог, а у ч аст ­ н и к и зап у ск аю тся на портах с посл ед о вател ьн о у в ел и ч и в аю щ и м и ся ном ерам и, н ач и н ая с 27017. В озм ож но, вам стоит в ы п о л н я ть каж дую ком ан ду m ongod в н о ­ вой в к л ад к е тер м и н ал а без в ы п о л н ен и я в ф оновом реж и м е (б ез заверш аю щ его си м во л а &). Листинг 8.11. Запуск реплицированного набора mkdir -p ./mongodata/db0 ./mongodata/db1 ./mongodata/db2 p k ill mongod < -------Гарантирует отсутствие других выполняемыхэкземпляров mongod. sleep 3 < -------- Дает существующим экземплярам время на завершение. mongod --p o rt 27017 --dbpath ./rs0 -d ata/d b 0 --re p lS et rs0 & mongod --p o rt 27018 --dbpath ./rs0 -d ata/d b 1 --re p lS et rs0 & mongod --p o rt 27019 --dbpath ./rs0 -d ata/d b 2 --re p lS et rs0 & П о сл е то го к а к р е п л и ц и р о в а н н ы й н а б о р за р аб о т ае т , M o n g o D B н ео б х о д и м о п ро в ести н ек о то р у ю и н и ц и а л и за ц и ю . Н ео б х о д и м о п о д к л ю ч и ться к п о р ту э к ­ з е м п л я р а , к о т о р ы й ст ан е т п е р в ы м первичным у зл о м (2 7 0 1 7 по у м о л ч ан и ю ), и вы зв ать м етод r s . i n i t i a t e ( ) , как показан о в л и сти н ге 8.12. З а т ем необходим о д обавить каж ды й эк зем п л яр как уч астн и ка реп л и ц и рован н ого набора. О братите в н и м ан и е на н ео б х о ди м о сть п еред ачи им ен и хоста той м аш и н ы , к к оторой вы подклю чаетесь. 248 Глава 8. Хранение данных в приложениях Листинг 8.12. Инициализация реплицированного набора mongo mongo mongo mongo --ev al --ev al --ev al --ev al " r s .in it i a te ( ) " "rs.ad d (''h o stn am e':2 7 0 1 7 ')" "rs.ad d (''h o stn am e':2 7 0 1 8 ')" "rs.ad d (''h o stn am e':2 7 0 1 9 ')" < ----- 1 Команда UNIXhostname выводит | имя хоста текущей машины. К лиенты M ongoD B долж ны располагать инф орм ацией обо всех возм ож ны х участ­ никах реплицированного набора при подклю чении, хотя не все участники долж ны быть подклю чены в текущ ий момент. П осле подклю чения вы мож ете использовать кли ен та M ongoD B , как обычно. В листинге 8.13 показано, как создать р еп л и ц и ро­ ванны й набор с трем я участникам и. Листинг 8.13. Подключение к реплицированному набору const os = r e q u ir e ( 'o s '); const { MongoClient } = require('m ongodb'); const hostname = os.hostname(); const members = [ '${hostname}:27018', '${hostname}:27017', '${hostname}:27019' ]; M ongoC lient.connect('m ongodb://${m em bers.join(',')}/test?replSet=rs0') .then(db => { db.ad m in ().rep lS etG etS tatu s().th en (statu s => { / / / c o n so le.lo g (statu s); d b .clo se(); }); }); /// Е сли на лю бом из узл о в m ongod прои зойдет сбой, систем а п родолж ит работать (п р и усло ви и вы п о л н ен и я по кр ай н ей м ере двух эк зем п л яр о в). Если сбой п р о ­ изой дет на первичном узле, то втори ч н ы й узел будет автом атически повы ш ен до первичного. 8.8.7. Уровень записи M ongoD B предоставляет в распоряжение разработчика средства точного управления ком пром иссам и по бы стродействию и безопасности, доступны е д ля разны х частей ваш его прилож ения. Ч тобы использовать M ongoD B без неп риятн ы х сю рпризов, важ но поним ать концепции уровней запи си и ч тен и я — особенно с увеличением количества узлов в реплицированном наборе. В этом разделе рассматривается только кон цепц ия значим ости записи, так как она яв л я ет ся более важной. Уровень записи (w rite concern ) определяет количество экзем пляров m ongod, в к о ­ торы е долж на быть успеш но вы полнена запись, чтобы вся операция бы ла признана 8.8. MongoDB 249 успеш ной. Если значение не задано явно, по ум олчанию уровень запи си равен 1; он гарантирует, что данны е бы ли записаны по край ней мере в один узел. М ожет оказаться, что это значение не обеспечивает необходимого уровня защ иты к р и ти ­ ческих данных; если узел отклю чится до того, как данны е будут реплицированы на других узлах, данны е могут быть потеряны . М ож но (и даж е ж ел ател ьн о ) устан о ви ть нулевой уровень запи си, при котором п ри лож ение вообщ е не ож идает никакого ответа: d b .c o lle c tio n ('d a ta ').in s e rtO n e (d a ta , { w: 0 }); Н улевой уровень запи си обеспечивает наивы сш ее бы стродействие при м ин им аль­ ны х гарантиях устойчивости. О бы чно он при м ен яется только для врем енны х или некритичны х данны х (наприм ер, д ля запи си ж урналов или кэш и рования). Е сл и вы п одклю чены к р е п л и ц и р о в ан н о м у набору, м ож н о у стан ови ть уровень зап и си вы ш е 1. Р е п л и к а ц и я по больш ем у коли честву узлов сниж ает вероятность потери д ан н ы х за счет п о я в л е н и я д о п о л н и те л ьн о й зад ер ж к и п ри вы п о л н ен и и операций: d b .c o lle c tio n ('d a ta ').in s e rtO n e (d a ta , { w: 2 }); d b .c o lle c tio n ('d a ta ').in s e rtO n e (d a ta , { w: 5 }); У ровень зап и си ж ел ательн о м асш таби р о вать при и зм ен ен и и к ол и чества узлов в кластере. Э та задача м ож ет реш аться динам ически M ongoD B , если вы брать для уровн я запи си значение m a jo rity . О но гарантирует, что данны е будут записаны более чем в 50% доступны х узлов: d b .c o lle c tio n ('d a ta ').in s e rtO n e (d a ta , { w: 'm ajo rity ' }); У ровень за п и с и по у м олчани ю , р ав н ы й 1, м ож ет не обесп ечи вать адекватн ого уровня защ иты д ля критических данных. Д анны е могут быть потеряны , если узел отклю чится перед р епликацией на других узлах. П ри н азн ачени и у р о вн я запи си вы ш е 1 вы полнен ие прод ол ж и тся только после проверки того, что данны е сущ ествую т на нескольких экзем плярах m ongod. В ы ­ полнение нескольких экзем пляров на одной м аш ине повыш ает уровень защ иты, но не помогает в случае общ есистемны х сбоев вроде нехватки дискового пространства или ОЗУ. Ч тобы защ ититься от маш инны х сбоев, следует запустить экзем пляры на н ескольких м аш инах и следить за тем, чтобы операции запи си распространились по этим узлам; однако вы полнение запи си при этом зам едлится, и этот реж им не защ итит от сбоев на уровне центров обработки данны х. Распределен ие узлов по нескольким центрам обработки данны х обеспечивает защ иту в случае выхода цен­ тров из строя, но обеспечение репликации этих данны х меж ду центрами обработки данны х серьезно повлияет на быстродействие. 250 Глава 8. Хранение данных в приложениях К ак обычно, чем больш е защ иты вы добавляете, тем медленнее и сложнее становит­ ся ваш а система. Э та проблем а не я в л яется специф ической только д ля M ongoD B; она присущ а всем хранилищ ам данны х без исклю чения. И деального реш ен ия не сущ ествует, и вам придется вы брать при ем лем ы й уровень ри ска д ля различны х частей ваш его прилож ения. З а д о п о л н и тел ьн о й и н ф о р м ац и ей о том, как работает р еп л и к ац и я в M ongoD B , обращ айтесь к следую щ им ресурсам: О https://docs.m ongodb.com /m anual/faq/replica-sets/; О https://docs.m ongodb.com /m anual/faq/concurrency/. 8.9. Хранилища «ключ-значение» К аж д ая запись в х рани ли щ е «клю ч-значение» состоит из одного клю ча и од н о­ го значения. Во м ногих систем ах « клю ч-зн ачени е» зн ач ен и е м ож ет относиться к лю бом у типу данных, иметь любую длину или структуру. С точки зрен и я базы данны х значения явл яю тся непрозрачны м и и атомарными: базе данны х неизвестен тип данных, и к этим данны м м ож но обращ аться только как к единому целом у без разбиения на части. С равните с хранением значений в реляционн ы х базах данных: дан ны е х р ан я тся в таблицах, которы е содерж ат строки данны х, разбиты е на з а ­ ранее определенны е столбцы. В храни ли щ ах «клю ч-значение» ответственность за управление ф орм атом данны х возлагается на прилож ение. Х ранилищ а «клю ч-значение» часто встречаю тся в частях прилож ения, критичны х по бы стродействию . В идеале зн ачения располагаю тся в такую структуру, чтобы д ля вы полнен ия задачи требовалось м ин им альное количество операций чтения. Х ранилищ а «клю ч-значение» обладают более простой функциональностью запроса, чем другие типы баз данных. В идеале слож ны е запросы долж ны обрабаты ваться заранее; в противном случае они долж ны вы полняться в прилож ении, а не в базе данных. Это ограничение долж но обеспечивать понятны е и предсказуем ы е хар ак ­ теристи ки бы стродействия. Самые популярны е хранилищ а «клю ч-значение» — такие, как Redis и Memcached, — часто исп ользую тся д л я врем енного х р ан ен и я дан ны х (п р и выходе из процесса данны е теряю тся). О тказ от записи на диск — один из лучш их способов повы ш ения быстродействия. Это решение может стать приемлемым для функций, позволяю щ их восстановить или потерять данные без особых проблем (например, для кэш ирования и пользовательских сеансов). О б ы ч н о с ч и тается , что х р а н и л и щ а « к л ю ч -зн ач е н и е » не м огут и с п о л ь з о в а т ь ­ ся д л я перви чн о го х р а н е н и я данны х, но это не всегда так. М ноги е х р ан и л и щ а «клю ч-зн ачени е» обеспечиваю т такую ж е устойчивость, как и «настоящ и е» базы данны х. 8.10. Redis 251 8.10. Redis Redis — популярное хранилищ е структур данны х в памяти. И хотя многие разработ­ ч и ки считаю т Redis хранилищ ем «клю ч-значение», клю чи и зн ачения составляю т лиш ь небольшое подмножество ф ункций Redis среди разнообразных полезны х базо­ вых структур данных. Н а рис. 8.3 приведена статистика использования redis в npm. II_ П I ®ІІ_ П Н 3 dependencies version 2 6.2 2,260 dependents ^ updated 2^ nonths ago Рис. 8.3. Статистика использования Redis К числу структур данных, встроенны х в Redis, относятся: О строки; О хеши; О списки; О множ ества; О сортированны е множ ества. Redis такж е вклю чает в себя м ного других полезны х возмож ностей: О растровые данные — прям ы е м ан и п у л яц и и с битам и в значениях; О геопространственные индексы — хранение геопространственны х данны х с р а ­ диальны м и запросам и; О каналы — м еханизм поставки данны х «п уб ли кац и я/п од п и ска»; О срок жизни (TTL) — значения могут настраиваться со сроком жизни, по истечении которого они автом атически уничтож аю тся; О вытеснение LRU — (нео б язател ьн о е) вы теснение давно не использовавш ихся значений д ля м аксим ально эф ф ективного исп ользован ия памяти; О HyperLogLog — вы сокопроизводительная аппроксим ация для вы числения м ощ ­ ности множ ества при небольш их затратах пам яти (без необходимости хранения каж дого элем ента); О репликация, кластеризация и сегментация — горизонтальное масш табирование и устойчивость данных; О сценарии Lua — расш ирение R edis нестандартны м и командами. В этом разделе при водятся списки ком анд Redis. Н е стоит относиться к ним как к справочникам ; они даю т лиш ь некоторое представление о том, что мож но делать 252 Глава 8. Хранение данных в приложениях в Redis. R edis — невероятно м ощ ны й и гибкий инструмент; за подробностям и об­ ращ айтесь по адресу http://redis.io/commands. 8.10.1. Установка и настройка Redis может устанавливаться при помощ и системного менеджера пакетов. В macOS R edis легко устан авли вается в H om ebrew : brew i n s t a ll redis Д л я запуска сервера используется исп олн яем ы й ф айл redis-server: re d is-se rv e r /u s r /lo c a l/e tc /re d is .c o n f П о ум олчанию сервер вы полняет прослуш ивание на порту 6379. 8.10.2. Выполнение инициализации Э кзем п ляр кли ен та R edis создается ф ункц ией c r e a te C lie n t из npm -пакета redis: const red is = r e q u ir e ( 'r e d is ') ; const db = red is.createC lien t(6 3 7 9 , '1 2 7 .0 .0 .1 '); В ар гу м ен тах ф у н к ц и и п еред аю тся порт и хост. Н о если вы зап у ск аете сервер R edis на порту по ум олчанию локальной маш ины , ни каки х аргументов передавать вообщ е не придется: const db = re d is.c re a te C lie n t(); Э кзем пляр клиента Redis расш иряет EventEm itter, поэтому вы можете присоединять слуш ателей для различны х статусны х собы тий Redis, как показано в листинге 8.14. Вы мож ете сразу начинать вводить команды д ля клиента; введенны е команды бу­ ф еризую тся до готовности подклю чения. Листинг 8.14. Подключение к Redis и прослушивание статусных событий const red is = r e q u ir e ( 'r e d is ') ; const db = re d is.c re a te C lie n t(); db .o n ('co n n ect', () => console.log('R edis c lie n t connected to s e r v e r .') ) ; d b .o n ('re a d y ', () => console.log('R edis server is re a d y .')); d b .o n ( 'e r r o r ', e rr => co n so le.erro r('R ed is e r r o r ', e rr ) ) ; О бработчик e r r o r срабаты вает при возни кн овен ии проблемы с подклю чением или клиентом . Если при срабаты вании собы тия e r r o r обработчик не присоединен, про­ цесс п р и л о ж ен и я вы дает ош ибку и аварийн о заверш ается; это свойство присущ е всем объектам E ventE m itter в Node. Если при подклю чении происходит сбой, а об­ работч и к e r r o r определен, то кли ен т R edis п ы тается восстан овить подклю чение. 8.10. Redis 253 8.10.3. Работа с парами «ключ-значение» Redis мож ет использоваться в качестве обобщ енного храни ли щ а «клю ч-значение» д л я строк и п р о и зв о л ьн ы х д во и чн ы х данны х. Д л я ч тен и я и зап и си пар «клю чзначение» использую тся методы s e t и g e t соответственно: d b .s e t( 'c o lo r ', 'r e d ', e rr => { i f (e rr) throw e rr; }); d b .g e t( 'c o lo r ', (e rr, value) => { i f (e rr) throw e rr; co n so le .lo g ('G o t:', value); }); Если указать сущ ествую щ ий ключ, значение будет заменено. Если вы попытаетесь прочитать значение с несущ ествую щ им клю чом, значение будет равно n u ll; такое обращ ение не считается ош ибкой. С ледую щ ие ком анды могут исп ользоваться д ля чтен и я и изм ен ен ия значений: О append; О decr; О decrby; О get; О g e tra n g e ; О g e ts e t; О in c r; О in crb y ; О in c r b y f lo a t; О mget; О mset; О msetnx; О p setex ; О s e t; О s e te x ; О setn x ; О se tra n g e ; О s trle n . 254 Глава 8. Хранение данных в приложениях 8.10.4. Работа с ключами Ч тобы проверить, сущ ествует ли клю ч, исп ользуй те метод e x i s t s . О н работает с лю бы м типом данных: d b .e x is ts ( 'u s e r s ', (e rr, doesExist) => { i f (e rr) throw e rr; co n so le.lo g ('u sers e x i s t s : ', doesExist); }); К ром е e x i s t s , для работы с клю чам и м огут использоваться следую щ ие команды (н езави си м о от типа зн ач ен и я — ком анды работаю т со строкам и, м нож ествам и, спискам и и т. д.): О d e l; О e x is ts ; О rename; О renamenx; О s o r t; О scan; О type. 8.10.5. Кодирование и типы данных С ервер Redis хранит клю чи и зн ачения в виде двоичны х объектов; это представле­ ние не зависи т от способа кодирован ия значений, передаваем ы х клиенту. Л ю бая допустим ая строка Ja v a S c rip t (U C S 2 /U T F 1 6 ) мож ет исп ользоваться как д ей стви ­ тельны й клю ч или значение: d b .s e t( 'g r e e tin g ', ' # # ' , re d is .p r in t) ; d b .g e t('g re e tin g ', re d is .p r in t) ; d b .s e t( 'ic o n ', '? ' , re d is .p r in t) ; d b .g e t( 'ic o n ', re d is .p r in t) ; П о ум олчанию клю чи и значения преобразую тся в строки при записи. Н апример, если задать клю ч в числовом виде, при попы тке получения того ж е клю ча вы п олу­ чите строку: d b .s e t( 'c o lo r s ', 1, (e rr) => { i f (e rr) throw e rr; }); d b .g e t( 'c o lo r s ', (e rr, value) => { i f (e rr) throw e rr; console.log('G ot: %s as %s', value, typeof value); }); < ----- Значение будет иметь тип string. 8.10. Redis 255 К лиент R edis незам етно преобразует числа, логические значения и даты в строки; кром е того, он спокойно приним ает объекты буферов. П опы тка задать в качестве зн ачения лю бой другой тип Ja v a S c rip t (наприм ер, O bject, A rray, RegExp) приводит к выдаче предупреж дения, к котором у стоит прислуш аться: d b .s e t( 'u s e r s ', {}, re d is .p r in t) ; Deprecated: The SET command contains a argument of type Object. This is converted to "[object O bject]" by using .to S trin g () now and w ill return an e rro r from v.3.0 on. Please handle th is in your code to make sure everything works as you intended i t to . В будущем это будет рассм атриваться как ошибка. П рилож ение долж но проследить за тем, чтобы клиенту R edis передавались допустим ы е типы. Массивы с одним и несколькими значениями П ри попы тке при сваи вания м ассива значений клиент вы дает загадочную ош ибку «R eplyE rror: E R R syn tax error»: d b .s e t( 'u s e r s ', [ 'A lic e ', 'B ob'], re d is .p r in t) ; О днако д ля массива, содерж ащ его только одно значение, ош ибки не будет: d b .s e t( 'u s e r ', [ 'A lic e '] , re d is .p r in t) ; d b .g e t( 'u s e r ', re d is .p r in t) ; Такие ош ибки нередко про явл яю тся только в усл ови ях реальной эксплуатации. О ни остаю тся незам еченны м и, если тестовы й пакет генерирует только м ассивы с одним значением, типичные для усеченных тестовых данных. Будьте внимательны! Буферы с двоичными данными Redis позволяет хранить произвольны е байтовы е данные; ф актически это означает возможность сохранения лю бых типов данных. Клиент Node поддерж ивает эту воз­ м ож ность за счет специальной обработки типа N ode B uffer. П ри передаче буфера клиенту Redis в качестве клю ча или значения байты передаю тся в неизменном виде серверу Redis. Таким образом пред отвращ ается случайное повреж дение данны х и сниж ение бы стродействия из-за лиш них преобразований м еж ду строкам и и бу­ ферами. Н апример, если вы захотите записать данны е с диска и ли из сети прямо в Redis, непосредственная запись буферов в Redis вы полняется более эф ф ективно, чем с предварительны м преобразованием их в строки. В Redis недавно бы ли добавлены команды д ля м анип уляц ий с отдельны ми битами строковы х значений, которы е могут пригодиться при работе с буферами: О b itc o u n t; О b itfie ld ; 256 Глава 8. Хранение данных в приложениях О b ito p ; О s e tb it; О b itp o s . БУФЕРЫ Б у ф е р ы — данны е, ко то р ы е во звр ащ аю тся по ум олчан и ю б азовы м и A P I N ode д ля работы с ф айлам и и сетям и. О ни представляю т собой кон тей не­ ры д ля см еж ны х блоков двоичны х данных. П оддерж ка буферов появилась в N ode еще до того, как в Ja v a S c rip t появились встроенны е типы д ля работы с двоичны м и данны м и (U int8A rray, F loat32A rray и т. д). В наш и дни буферы реализую тся в N ode специализированны м субклассом U int8A rray. A P I для работы с буф ерам и доступен в N ode глобально; чтобы пользоваться им, вам не придется что-то делать дополнительно. См. h ttp s ://g ith u b .c o m /n o d e js /n o d e /b lo b /m a s te r/lib /b u ffe r.js . 8.10.6. Работа с хешами Хеш представляет собой коллекцию пар «клю ч-значение». Команда hmset получает клю ч и объект, представляю щ ий набор пары «клю ч-значение» в хеше. Д л я получе­ ни я пар «клю ч-значение» в виде объекта используется команда hmget (листинг 8.15). Листинг 8.15. Хранение данных в элементах хешей Redis db.hm set('cam ping', { -<-------sh e lte r: '2-person t e n t ', cooking: 'campstove' }, re d is .p r in t) ; Задает пары «ключ-значение», входящие в хеш. db.hget('cam ping', 'cooking', (e rr, value) => { <.----- Получает значение для «camping.cooking». i f (e rr) throw e rr; console.log('W ill be cooking w ith :', value); }); db.hkeys('cam ping', (e rr, keys) => { < -------- Получает ключихеша в виде массива. i f (e rr) throw e rr; keys.forEach(key => co n so le.lo g (' ${key}')); }); Н е д опускается хранени е влож енн ы х объектов в хеш е R edis — поддерж ивается только один уровень клю чей и значений. Д л я работы с хеш ам и использую тся следую щ ие команды: О hdel ; О h e x is ts ; 8.10. Redis 257 О hget; О h g e ta ll; О hin crb y ; О h in c r b y f lo a t; О hkeys; О hlen; О hmget; О hmset; О h se t; О hsetnx; О h s tr le n ; О h v a ls ; О hscan. 8.10.7. Работа со списками Список представляет собой упорядоченную коллекцию строковых значений. Список м ож ет содерж ать несколько коп ий одного значения. Н а концептуальном уровне списки бли зки к массивам. Ч ащ е всего списки использую тся за их способность м о­ делировать поведение структур данны х стека (L IF O : «последним приш ел, первым вы ш ел») или очереди (F IF O : «первы м приш ел, первы м вы ш ел»). С ледую щ ий ф рагм ент дем онстрирует сохранение и вы борку значений из списка. К ом анда lp u sh добавляет значение в список. К ом анда lra n g e извлекает диапазон значений по начальному и конечному индексу. Аргумент -1 в следующем фрагменте обозначает последний элем ент списка, поэтом у в этом варианте и сп ользован и я lra n g e извлекаю тся все элем енты списка: c lie n t.lp u s h ( 'ta s k s ', 'P ain t the bikeshed r e d . ', re d is .p rin t); c lie n t.lp u s h ( 'ta s k s ', 'P ain t the bikeshed g re e n .', re d is .p rin t); c lie n t.lr a n g e ( 'ta s k s ', 0, -1, ( e rr, items) => { i f (e rr) throw e rr; item s.forEach(item => co n so le.lo g (' $ {item }')); }); С пи ски не содерж ат встроенны х средств проверки при сутствия значения в списке или определения индекса конкретного значения в списке. Вы можете вручную пере­ брать элем енты списка д ля получения нуж ной инф орм ации, но это крайне н е эф ­ ф ективное решение, которого следует избегать. Если вам нуж на функциональность такого рода, лучш е вы брать другую структуру данны х (наприм ер, м нож ество) — а возможно, даж е использовать ее в дополнение к списку. Д убли рован ие данны х 258 Глава 8. Хранение данных в приложениях меж ду нескольким и структурам и часто ж елательно для исп ользован ия различны х характеристик бы стродействия. Д л я работы со спискам и предназначены следую щ ие команды: О b lp o p ; О brpop ; О lin d e x ; О lin s e rt; О lle n ; О lpop; О lpush; О lpushx; О lra n g e ; О lrem; О ls e t; О ltrim ; О rpop; О rpush; О rpushx. 8.10.8. Работа со множествами М ножество представляет собой неупорядоченную коллекцию уникальных значений. П роверка принадлеж ности, а такж е операции д обавления и удален ия элементов из м нож ества вы полняю тся за врем я O (1 ); это означает, что множ ество я вл яется вы сокопроизводительной структурой, подходящ ей д ля м ногих задач: db.sadd('adm ins', 'A lic e ', re d is .p r in t) ; db.sadd('adm ins', 'B ob', re d is .p r in t) ; db.sadd('adm ins', 'A lic e ', re d is .p r in t) ; db.smembers('admins', (e rr, members) => { i f (e rr) throw e rr; console.log(members); }); Д л я работы с м нож ествам и Redis предназначены следую щ ие команды: О sadd; О scard ; О s d iff; 8.10. Redis 259 О s d iffs to re ; О s in te r; О s in te rs to re ; О sismember ; О smembers ; О spop; О srandmember ; О srem; О sunion; О s u n io n s to re ; О sscan. 8.10.9. Реализация паттерна «публикация/подписка» на базе каналов R edis вы ходит за р ам к и т р ад и ц и о н н о й р о л и х р ан и л и щ а данны х, п р ед оставл яя в распоряж ени е разработчика каналы (chan n els). Этот м еханизм передачи данны х реал и зу ет ф ун кц и он альн ость п у б л и к ац и и /п о д п и ск и ; его кон ц еп туальн ая схема изображ ена на рис. 8.4. К аналы хорош о подходят д ля при лож ений реального вре­ м ени — таких, как чаты и игры. Рис. 8.4. Каналы Redis предоставляют простое решение для стандартного сценария передачи данных К л и ен т R edis м ож ет по д п и сы ваться на кан алы и л и п уб л и к овать в них сообщ е­ ния. С ообщ ение, опубликованное в канале, будет доставлено всем подписчикам. П убликатору не обязательно располагать ин ф орм аци ей о подписчиках, как и под ­ писчикам — о публикаторах. И м енно логическая и зол яц и я меж ду подписчикам и и публикаторам и делает этот паттерн столь м ощ ны м и элегантным. В листинге 8.16 приведен прим ер исп ользован ия ф ункциональности п у б л и к ац и и / подписки в Redis д ля р еализаци и чат-сервера T C P /IP . 260 Глава 8. Хранение данных в приложениях Листинг 8.16. Реализация простого чат-сервера на базе функциональности публикации/подписки в Redis Определяет логику настройки для каждого пользователя, подключающегося кчат-серверу. const net = r e q u ir e ( 'n e t') ; const red is = r e q u ir e ( 'r e d is ') ; Создает клиента подписки const server = net.createS erver(socket => { ■<— для каждого пользователя. const subscriber = re d is .c re a te C lie n t(); su b sc rib e r.su b sc rib e ('m a in '); < -------- Подписывается на канал. su b scrib er. o n (' message ' , (channe1, message) => { -<---- Когда сообщение принимается socket.w rite (' Channel ${channel}: ${message}' ) ; из канала, оно выводится для }); пользователя. const publisher = re d is .c re a te C lie n t(); ■< Создает клиента публикации s o c k e t.o n ('d a ta ', data => { I для каждого пользователя. p u b lish er.p u b lish ('m ain ', d ata); ■<Публикует сообщение, }); введенное пользователем. so c k e t.o n ('e n d ', () => { su b scrib er.u n su b scrib e('m ain '); su b scrib er.en d (tru e); pu b lish er.en d (tru e); }); }); •<- Если пользователь отключается, то подключение клиента завершается. se rv e r.liste n (3 0 0 0 ); 8.10.10. Повышение быстродействия Redis N p m -пакет hiredis связы вает J a v a S c rip t с парсером п ротокол а из оф и ц и альн о й библиотеки C H iredis. H iredis м ож ет значительно повы сить бы стродействие п р и ­ лож ений Redis на базе Node, особенно если вы используете операции sunion, s in te r , lra n g e и zrange c больш им и базам и данных. Ч тобы использовать hiredis, просто установите пакет вместе с пакетом redis в вашем прилож ении; пакет N ode redis обнаруж ит его и автом атически использует его при следую щ ем запуске: npm in s ta l l h ired is --save У hiredis есть свои недостатки. Так как пакет ком п и л и руется из кода C, при по­ строении hiredis для некоторы х платф орм вы мож ете столкнуться с ослож нениям и и ли ограничениям и. К ак и со всеми встроенны м и дополнениям и, возможно, вам придется заново построить hiredis ком андой npm r e b u ild после об новления Node. 8.11. Встроенные базы данных В строенная база данны х не требует установки или адм и нистрировани я внеш них серверов. О на работает прям о внутри процесса прилож ения. В заим одействие со 8.12. LevelDB 261 встроен ной базой дан ны х обы чно в ы п о л н яется через прям ы е вы зовы процедур в ваш ем прилож ении, а не через каналы IP C или по сети. Во м ногих ситуациях прилож ение долж но быть автономны м, поэтому встроенная база данны х становится единственно возм ож ны м выходом (например, для м оби ль­ ны х или настольны х при лож ений ). В строенны е базы данны х такж е могут исп оль­ зоваться с веб-серверами; они часто обеспечиваю т реализацию таких ф ункций, как сеансы пользователей или кэш ирование, а иногда даж е использую тся в качестве первичного храни ли щ а данных. Н еко то р ы е встроен ны е базы данны х, часто исп ользуем ы е в п ри лож ен и ях N ode и Electron: О SQ Lite; О LevelDB; О RocksD B; О Aerospike; О EJD B; О NeD B; О LokiJS; О Lowdb. N eD B , LokiJS и Low db написаны на «чистом» Ja v a S c rip t и поэтом у естественным образом встраиваю тся в прилож ения N o d e/E lectro n . М ногие встроенны е базы д ан ­ ны х представляю т собой простые хранилищ а пар «клю ч-значение» или документов, хотя встроенное реляционное хранилищ е S Q L ite яв л яется заметны м исклю чением. 8.12. LevelDB LevelDB — встр аи ваем о е х р ан и л и щ е « клю ч-зн ачени е», р азработан н ое в начале 2011 года ком п анией G oogle и и зн ачально предназначенное д ля исп ользован и я в качестве вспом огательного храни ли щ а р еализаци и IndexedD B в Chrom e. А рхи­ тектура LevelD B строится на кон цепц иях базы данны х B igtable ком п ании Google. LevelD B м ож но сравнить с таким и базам и данных, как Berkeley DB, Tokyo/K yoto C ab in et и A erospike, но в контексте этой книги LevelD B мож но рассм атривать как встроенную версию Redis с миним альны м набором функций. К ак и многие встроен­ ные базы данных, LevelDB не я вл яется м ногопоточной системой и не поддерживает использование нескольким и экзем плярам и общего ф айлового хранилищ а, поэтому эта систем а не будет работать в распределен ной среде без прилож ения-обертки. LevelD B хранит произвольны е байтовы е массивы, отсортированны е в лексикогра­ ф ическом порядке по ключу. З н ач е н и я сж им аю тся с использованием алгоритма 262 Глава 8. Хранение данных в приложениях S nappy ко м п ан и и Google. Д ан н ы е всегда сохран яю тся на диске; общ ая емкость данны х не ограничивается объемом оперативной пам яти на компью тере (в отличие от систем, хранящ их данны е в пам яти, таких как Redis). L evelD B п о д д ер ж и в ает н еб о л ьш о й н абор п о н ятн ы х операторов: G et, P u t, D el и Batch. LevelDB также умеет сохранять «снимки» текущ его состояния базы данных и создавать двусторонние итераторы д ля перебора данны х в прям ом и обратном направлении. П ри создании итератора неявно создается снимок данных; данные, которые видны итератору, не могут изм еняться последую щ ими операциями записи. LevelD B заклады вает ф ундам ент д ля других баз данных. К оличество засл у ж и ва­ ю щ их вни м ан ия ответвлений LevelD B о бъясн яется простотой системы LevelDB: О R ocksD B (Facebook); О H yperL evelD B (H y p erd ex ); О R iak (B asho); О leveldb-m cpe (M ojang, создатели M inecraft); О b itc o in /le v e ld b (д л я проекта b itcoind). З а дополнительной информацией о LevelDB обращайтесь по адресу http://leveldb.org/. 8.12.1. LevelUP и LevelDOWN П оддерж ка L evelD B в N ode пред о ставл яется пакетам и L evelU P и LevelD O W N , которы е написал один из основателей N ode, активн ы й разработчик из А встралии Род Вэгг (R o d Vagg). L evelD O W N — простая, м и н и м ал ьн ая прослойка C + + для LevelD B в Node; вряд ли вам придется взаимодействовать с ней напрямую . LevelU P и н кап су л и р у ет L evelD O W N A P I в более удобном и идиом атичном ин терф ейсе Node, до бавл яя поддерж ку кодирован ия клю чей и значений, JS O N , буф еризации записи до откры тия базы данны х и инкапсуляции интерф ейса итераторов LevelDB в потоках Node. П опулярность L evelU P в npm продем онстрирована на рис. 8.5. Рис. 8.5. Статистика использования LevelUP в npm 8.12.2. Установка Главное достоинство исп ользован ия LevelD B в п ри лож ениях N ode — встроенная природа базы данных: все необходим ое устан авли вается исклю чительно из npm. 8.12. LevelDB 263 Вам не придется устанавливать дополнительное П О ; просто введите следую щую команду, и все будет готово к использованию LevelDB: npm in s t a l l lev el --save П акет level п р ед ставляет собой простую вспом огательную обертку д л я пакетов L evelU P и LevelD O W N , предоставляю щ ую A P I L evelU P д ля исп ользован ия вн у ­ тренней подсистемы LevelD ow n. Д окум ентаци я A P I LevelUP, предоставляем ого пакетом level, находится в R eadm e-ф айле LevelUP: О www.npmjs.com/package/levelup; О www.npmjs.com/package/leveldown. 8.12.3. Обзор API О сновны е методы кли ен та LevelD B д ля хранени я и чтени я значений: О d b .p u t(k e y , v a lu e , c a llb a c k ) — сохраняет значение с заданны м ключом; О d b .g e t( k e y , c a llb a c k ) — читает значение с заданны м ключом; О d b .d e l( k e y , c a llb a c k ) — удаляет значение с заданны м ключом; О d b . b a t c h ( ) .w r i t e ( ) — вы полняет пакетны е операции; О d b .c re a te K e y S tre a m (o p tio n s) — создает поток клю чей из базы данных; О d b .c re a te V a lu e S tre a m (o p tio n s ) — создает поток значений из базы данных. 8.12.4. Инициализация П ри и н и ц и ал и зац и и level необходим о предостави ть путь к каталогу, в котором будут храни ться данные, как показано ниже; если каталог не существует, то он бу­ дет создан. В сообщ естве сущ ествует неф орм альное правило: присваивать таком у каталогу расш ирение .db (наприм ер, ./app.db). Листинг 8.17. Инициализация базы данных level const level = r e q u ir e ( 'le v e l') ; const db = le v e l( './a p p .d b ', { valueEncoding: 'jso n ' }); П осле вы зова l e v e l ( ) возвращ аем ы й экзем пляр L evelU P нем едленно готов к си н ­ хронном у получению команд. Команды, введенны е перед откры тием хранилищ а LevelD B, буф еризую тся до его откры тия. 264 Глава 8. Хранение данных в приложениях 8.12.5. Кодирование ключей и значений Так как LevelD B может использовать произвольны е данные любого типа как в клю ­ чах, так и в значениях, за сериализацию и десериализацию вы зы ваю щ их данны х отвечает прилож ение. LevelU p можно настроить для автоматического кодирования клю чей и значений следую щ их типов данных: О u tf8 ; О jso n ; О b in a ry ; О id; О hex; О a s c ii ; О base64; О ucs2; О u tf1 6 le . П о ум олчанию клю чи и зн ач ен и я кодирую тся в строках U T F -8. В листинге 8.17 клю чи остаю тся строкам и U TF-8, а значения кодирую тся/декодирую тся в формате JSO N . К одирование JS O N позволяет хранить и читать структурированные значения (наприм ер, объекты или м ассивы ) по аналогии с храни ли щ ам и докум ентов (т а к и ­ ми, как M ongoD B ). Н о учтите, что в отличие от реальны х храни ли щ документов в базовой версии LevelD B не существует возмож ности обратиться к клю чам внутри значений; значения непрозрачны . П ользователи такж е могут определять собствен­ ны е кодировки — например, д ля поддерж ки другого структурированного ф орм ата (наприм ер, M essagePack). 8.12.6. Чтение и запись пар «ключ-значение» Б азо в ы й A P I прост: вы зов p u t( k e y , v a lu e ) и сп ользуется д л я зап и си значения, g e t(k e y ) — д ля ч тен и я и d e l(k e y ) — д ля уд ал ен и я зн ачения (л и сти н г 8.18). Код в листинге 8.18 следует присоединить к коду из листинга 8.17; полны й прим ер со­ д ерж ится в ф айле ch08-data-bases/listing8_18/index.js в архиве исходного кода книги. Листинг 8.18. Чтение и запись значений const key = 'u s e r '; const value = { name: 'A lice' }; db.put(key, value, e rr => { 8.12. LevelDB 265 i f (e rr) throw e rr; db.get(key, (e rr, re s u lt) => { i f (e rr) throw e rr; co n so le.lo g ('g o t v a lu e :', re s u lt); db.del(key, (e rr) => { i f (e rr) throw e rr; conso le.lo g ('v alu e was d e le te d '); }); }); }); Е сли сохранить знач ен и е с уж е сущ ествую щ им клю чом, старое знач ен и е будет перезапи сано. П о п ы тка ч тен и я зн а ч е н и я с несущ ествую щ и м клю чом приведет к ош ибке. О бъект ош ибки будет относиться к конкретном у типу N otFoundError то специальны м свойством e rr.n o tF o u n d , по котором у его мож но отличить от других типов ош ибок. Н а первы й взгляд это необычно, но поскольку в LevelD B нет встро­ енного метода для проверки сущ ествования, L evelU P необходимо как-то отличать несущ ествую щ ие значения от неопределенны х. В отличие от g et, попы тка вызвать d e l с несущ ествую щ им клю чом не приводит к ошибке. Листинг 8.19. Получение значений с несуществующими ключами d b .g e t('th is -k e y -d o e s -n o t-e x is t', i f (e rr && !err.notFound) throw i f (e rr && err.notFound) return console.log('V alue was fo u n d :', }); (e rr, value) => { e rr; console.log('V alue was not fo u n d .'); value); Все операции чтени я и записи данны х получаю т необязательны й аргумент o p tio n s д ля переопределения парам етров кодирован ия текущ ей операции (л и сти н г 8.20). Листинг 8.20. Переопределение параметров кодирования для конкретных операций const options = { keyEncoding: 'b in a ry ', valueEncoding: 'hex' }; db.put(new Uint8Array([1, 2, 3 ]), '0xFF0099', options, (err) => { i f (e rr) throw e rr; db.get(new Uint8Array([1, 2, 3 ]), options, ( e rr, value) => { i f (e rr) throw e rr; console.log(value); }); }); 8.12.7. Заменяемые подсистемы базы данных П олож ительны й побочны й эф ф ект разделения L evelU P /L evelD O W N заклю чается в том, что L evelU P не ограничивается использованием LevelDB в качестве внутрен­ ней подсистемы базы данны х. Л ю бая база данны х, которая мож ет быть упакована 266 Глава 8. Хранение данных в приложениях в M em D ow n A PI, мож ет исп ользоваться в качестве подсистемы хранения данных для LevelUP. Это позволяет использовать один A PI для взаимодействия со многими храни ли щ ам и данных. Н екоторы е прим еры альтернативны х подсистем баз данных: О M ySQ L; О Redis; О M ongoD B ; О ф айлы JS O N ; О электронны е таблицы Google; О AWS D ynam oD B ; О табличное храни ли щ е W indow s Azure; О веб-хранилищ е браузера (In d ex ed D B /lo calS to rag e). В озмож ность простой зам ены среды хранения данны х и даж е написания собствен­ ной подсистемы баз данны х означает, что вы можете использовать один последова­ тельны й набор A P I баз данны х и инструм ентов во м ногих ситуациях и условиях. О дин A P I баз данны х на все случаи! Среди альтернативных подсистем баз данных часто используется система memdown, ко то р ая х рани т зн а ч е н и я исклю чи тел ьн о в пам яти, а не на диске (по аналогии с S Q L ite в реж им е работы в пам яти). Д анная возм ож ность особенно полезна в т е ­ стовой среде для сни ж ения затрат на организацию тестирован ия и переналадку. Ч тобы вы полни ть л и сти н г 8.21, убедитесь в том, что у вас устан овлен ы пакеты L evelU P и m emdown: npm in s ta l l --save levelup memdown Листинг 8.21. Использование memdown с LevelUP const lev el = re q u ire ('le v e lu p ') const memdown = require('memdown') const db = l e v e l ( './ l e v e l - a r t i c l e s .d b ', {-<----keyEncoding: 'js o n ', valueEncoding: 'js o n ', db: memdown < Единственное реальное отличие }); передача memdown в параметре db. п г г Для memdown «путем» может быть любая строка, так какдиск не используется. В этом примере можно было использовать тот же пакет level, использовавш ийся ранее, потому что он представляет собой обертку для LevelUP. Н о если вы не используете пакет LevelD O W N на базе LevelDB, входящ ий в поставку level, вы можете просто ис­ пользовать LevelUP и избежать двоичной зависимости от LevelDB через LevelDOWN. 8.12. LevelDB 267 8.12.8. Модульная база данных Бы стродей стви е и м ин им ализм L evelD B наш ли отклик у м ногих разработчиков N ode и породили движ ение м одульны х баз данны х в сообщ естве Node. К онцепция заклю чалась в том, чтобы разработчик мог точно выбрать, какие возмож ности нуж ­ ны в его прилож ении, и адаптировать базу данны х под ваш кон кретны й сценарий использования. 898 results for ‘leveldb’ level-js maxogden leveldown/TeveMb library for browsers using indexedDB ★ 10 V22.4 level, leveldb level-http2 aaaristo Access a Icvddb instance via HTTP2 ★ 0 vO.O.4 leveldb. http2, http leveldown-mobile obastemur A Node.js LevclOB binding (or Android. iOS and Windows UWP. primary backend for LevdUP ★ 0 vl.1.1 ^ leveldb. level Рис. 8.6. Примеры сторонних пакетов LevelDB в npm Н есколько примеров м одульной ф ункциональности LevelDB, доступной в пакетах npm: О атомарны е обновления; О автом атическое увеличение ключей; О геопространственны е запросы; О оперативно обновляем ы е потоки; О вы теснение LRU; О зад ан и я о тображ ен ия/свертки; О р еп л и к ац и я «главны й/главны й»; О р еп л и к ац и я «главны й/подчиненн ы й»; 268 Глава 8. Хранение данных в приложениях О запросы SQL; О вторичны е индексы; О триггеры; О управление версиям и данных. В вики L evelU P ведется достаточно подробная сводка экосистемы LevelDB: h ttp s:// github.com /Level/levelup/w iki/M odules. Вы такж е м ож ете пои скать ин ф орм аци ю о leveldb в npm; на м ом ент написания книги там было 898 пакетов. Н а рис. 8.6 п о ­ казан а ин ф орм аци я о поп улярн ости LevelD B в npm. 8.13. Затратные операции сериализации и десериализации В ажно помнить, что встроенны е операции JS O N обходятся дорого и вы полняю тся в блокирую щ ем режиме; ваш процесс не мож ет делать ничего другого в то время, пока прилож ение преобразует данны е в JS O N и обратно. Это относится к больш ин­ ству других ф орм атов сериализации. С ериализац ия часто становится важ нейш им «узким местом» веб-сервера. Л у ч ш и й способ сократить ее вли ян и е — вы полнять ее как м ож но реж е и свести к м иним ум у объем обрабаты ваем ы х данных. В озможно, вам удастся добиться некоторого повы ш ения скорости за счет перехода на другой ф орм ат сериализаци и (наприм ер, M essagePack или P ro to co l Buffers), но рассм атривать альтернативны е ф орм аты стоит только после того, как вы вы ж мете всю возм ож ную эконом ию за счет со кр ащ ен и я разм еров передаваем ы х данны х и устран ения избы точны х ш агов сери али зац и и /д есери али зац и и . В строенны е ф у н кц и и J S O N .s trin g ify и JSO N .parse подверглись тщ ательной о п ­ ти м и зац и и , но п ри о бработке м егабайтов д ан н ы х они начинаю т пасовать. Д л я дем онстрации в листинге 8.22 вы полняется сериализаци я и десери али зация п р и ­ близительно 10 М байт данных. Листинг 8.22. Хронометражное тестирование сериализации const bytes = r e q u ir e ('p re tty -b y te s '); const obj = {}; fo r ( le t i = 0; i < 200000; i++) { o b j[i] = { [Math.random()]: Math.random() }; } c o n s o le .tim e ('s e ria lis e '); const jsonS tring = JSO N .stringify(obj); c o n so le .tim e E n d ('se ria lise '); c o n so le .lo g ('S e ria lise d S iz e ', bytes(B uffer.byteL ength(jsonS tring))); 8.14. Хранение данных в браузере 269 c o n s o le .tim e ('d e s e ria lis e '); const obj2 = JSON.parse(jsonString); co n so le.tim eE n d ('d eserialise'); В 2015 году на ко м п ью тер е In te l C o re i7 M acB ook P ro 3,1 ГГц с N o d e 6.2.2 на сери ал и зац и ю п р и б л и зи тел ьн о 10 М байт дан н ы х требовалось п ри бл и зи тел ьн о 140 мс, а на д есер и ал и зац и ю — 335 мс. Н а веб-сервере так ая нагрузка бы ла бы катастроф ической, потом у что эти операции вы полняю тся в блокирую щ ем р еж и ­ ме, и вы п о л н яю тся последовательно. Такой сервер см ож ет об рабаты вать около ни ч то ж н ы х сем и зап р о со в в секун ду при с е р и а л и за ц и и и около трех запросов в секунду при д есери али зации . 8.14. Хранение данных в браузере А синхронная м одель програм м ирования, исп ользуем ая в N ode, хорош о работает во м ногих практи чески х сценариях, потом у что у б ольш ин ства веб-прилож ений самы м значительны м «узким местом» я в л яе т с я ввод /вы вод . Главное, что можно сделать для одновременного сокращ ения нагрузки на сервер и повы ш ения качества взаим одействий с пользователем, — организация хранения данны х на стороне к л и ­ ента. Д оволен будет тот пользователь, которому не приходится ожидать заверш ения ци клической передачи по сети д л я получения результатов. Х ранение данны х на стороне клиента также улучш ает доступность прилож ения: ваше прилож ение может сохранить по крайней мере частичную работоспособность, пока пользователь или ваш сервис не активен. 8.14.1. Веб-хранилище: localStorage и sessionStorage Веб-хранилище определяет простое х ран и ли щ е «клю ч-значение» и обладает о т ­ личной поддерж кой среди настольны х и м обильны х браузеров. П ри и сп ользова­ нии веб-хранилищ а домен м ож ет сохранить небольш ой объем данны х в браузере и прочитать их позднее — даж е после о б новления сайта, зак р ы ти я вкл ад ки или заверш ен ия браузера. В еб-хранилищ е — первы й ш аг на пути хранения данны х на стороне клиента. Главной сильной стороной этой технологии яв л яется ее простота. Сущ ествует два A P I веб-хранилищ а: localStorage и sessionStorage. sessionStorage реализует A PI, идентичны й A P I localStorage, хотя и несколько отличается в поведе­ нии. Как и в случае с localStorage, данные, хранящ иеся в sessionStorage, сохраняются меж ду перезагрузкам и страниц, но в отличие от localStorage срок дей стви я всех данны х sessionStorage истекает при заверш ении сеанса страницы (п р и закры тии вкладки или браузера). Данные sessionStorage недоступны из разных окон браузеров. A PI веб-хранилищ а бы ли разработаны для преодоления ограничений cookie в брау­ зерах. Если говорить конкретнее, cookie плохо подходят для обмена данны ми между 270 Глава 8. Хранение данных в приложениях нескольким и активны м и вкладкам и одного домена. Если пользователь вы полняет некоторы е дей стви я м еж ду вкладкам и, хранилищ е sessionStorage мож ет использо­ ваться для обмена данны м и состояния м еж ду вкладкам и без исп ользован ия сети. Cookie также плохо подходят для долгосрочного хранения данных, которые должны «пережить» границы сеансов, вкладок и окон (например, созданны х пользователем докум ентов или электронной почты ). Д л я таких сценариев было спроектировано хранилищ е localStorage. О бъем данных, которые могут храниться в веб-хранилищ е, ограничивается в зависим ости от конкретного браузера. Д л я м обильны х браузеров этот порог составляет всего 5 М байт. Сводка API A P I localStorage предоставляет следую щ ие методы д ля работы с клю чам и и зн а ­ чениям и: О lo c a lS to r a g e .s e tI te m ( k e y , v a lu e ) — сохраняет значение с заданны м ключом; О lo c a lS to r a g e .g e tlte m ( k e y ) — получает значение д ля заданного ключа; О lo c a lS to ra g e .re m o v e lte m (k e y ) — удаляет значение д ля заданного ключа; О l o c a l S t o r a g e .c l e a r ( ) — удаляет все клю чи и значения; О lo c a lS to r a g e .k e y (in d e x ) — получает значение с заданны м индексом; О lo c a lS to r a g e .le n g th — возвращ ает общее количество клю чей в localStorage. 8.14.2. Чтение и запись значений К ак клю чи, так и значения долж ны быть строками. Если передать значение, кото­ рое не я в л яется строкой, оно будет автом атически преобразовано в строку. Такое преобразование не генерирует строки JS O N ; это обычное наивное преобразование с использованием .to S tr in g . О бъекты в конечном итоге сериализую тся в строку [ o b je c t O b je c t]. Ч тобы разм естить в веб-хранилищ е более слож ны е типы данных, при лож ение долж но само сериализовать зн ачения в строки и обратно. В л и сти н ­ ге 8.23 показано, как хранить J S O N в localStorage. Листинг 8.23. Хранение JSON в веб-хранилище const examplePreferences = { temperature: 'C elcius' }; / / Сериализация при записи lo c a lS to ra g e .se tItem ('p re fere n c e s', JSON.stringify(examplePreferences)); / / Десериализация при чтении const preferences = JSO N .parse(localS torage.getItem ('preferences')); console.log('Loaded p re fe re n c e s:', preferences); 8.14. Хранение данных в браузере 271 О б ращ ени е к данны м в еб-х р ан и л и щ а осу щ ествл яется д остаточно быстро, хотя и оно так ж е в ы п о л н я е т с я си н х р о н н о . В еб -х р ан и л и щ е б л о к и р у ет U I -п оток на врем я в ы п о л н ен и я ч тен и я и запи си. Д л я м алы х н агрузок эти затраты останутся незам етны м и, но вы долж ны п озаб оти ться о том, чтобы избеж ать л и ш н и х о п ер а­ ц ий ч тен и я и ли зап и си (особен но при б ольш их объемах дан ны х). К сож алению , веб-хран илищ е такж е недоступно д ля веб-работников (w eb w orkers), поэтому все о п ераци и ч тен и я и зап и си д олж ны в ы п о л н яться в одном U I -потоке. П одробны й анализ в л и я н и я на бы стродействи е р азл и ч н ы х техн ологи й х ран ен и я на стороне клиента приведен в посте Н олана Л оусона (N olan Lawson), автора PouchD B : h ttp :// nolanlawson.com/2015/09/29/indexeddb-websql-localstorage-what-blocks-the-dom/. A P I веб-хранилищ а не предоставляю т встроенны х средств д ля вы полнен ия запро­ сов, вы борки клю чей по диапазонам или поиска по значениям . Р азработчик огра­ ничивается возмож ностью обращ ения с перебором клю чей. Ч тобы провести поиск, вам придется создавать и поддерж ивать собственные индексы; или, если ваш набор данны х достаточно мал, м ожно полностью перебрать его элементы. В листинге 8.24 перебираю тся все клю чи localStorage. Листинг 8.24. Перебор всего набора данных в localStorage function getAllKeys() { return O bject.keys(localStorage); } function getAllKeysAndValues() { return getAllKeys() .reduce((obj, s tr ) => { o b j[s tr] = lo calS to rag e.g etItem (str); return obj; }, {}); } / / Получение всех значений const allV alues = getAllKeys().map(key => localStorage.getItem (key)); / / В виде объекта console.log(getAllKeysAndValues()); К ак и у бо льш и н ства х р ан и ли щ «клю ч-зн ачени е», у клю чей сущ ествует только одно п р о стр ан ств о им ен. Н ап р и м ер , если в базе д ан н ы х х р а н я т с я сообщ ен и я и ком м ентарии, невозм ож но создать два разны х хран и ли щ а д л я сообщ ений и для комментариев. Впрочем, достаточно легко реализовать собственные «пространства им ен», снабди в каж ды й клю ч п р еф и к со м д л я об о зн ач ен и я п р о стр ан ств а имен (л и ст и н г 8.25). Листинг 8.25. Ключи, реализующие пространства имен lo c a lS to ra g e .se tIte m ('/p o sts/$ { p o st.id } ', post); localStorage.setItem ('/com m ents/${com m ent.id}', comment); 272 Глава 8. Хранение данных в приложениях Чтобы получить все элементы из пространства имен, отфильтруйте набор элементов при пом ощ и ф ун кц и и getA llK eys (л и сти н г 8.26). Листинг 8.26. Получение всех элементов в пространстве имен function getNamespaceItems(namespace) { return g etA llK e y s().filte r(k e y => key.startsW ith(namespace)); } console.log(getNamespaceItems('/exampleNamespace')); У ч ти те, ч т о эт о т ф р а г м е н т п е р е б и р а е т в се к л ю ч и и з lo c a lS to ra g e ; п о м н и т е о в о зм о ж н о м с н и ж е н и и б ы с т р о д е й с т в и я п р и п ер еб о р е б о л ьш о го к о л и ч ес т в а элем ентов. И з-за синхронности A P I localStorage существуют некоторые ограничения на то, где и как этот A P I м ож ет использоваться. Н апример, localStorage мож ет использовать­ ся д ля м ем оизаци и результатов лю бой ф ункц ии, которая получает и возвращ ает данные, сериализуем ы е в ф орм ат JS O N (л и сти н г 8.27). Листинг 8.27. Использование localStorage для мемоизации / / При последующих вызовах с тем же аргументом / / будет извлекаться сохраненный результат. function memoizedExpensiveOperation(data) { const key = '/m em oized/${JSO N .stringify(data)}'; const memoizedResult = localStorage.getItem (key); i f (memoizedResult != n u ll) return memoizedResult; / / Затратные операции const re s u lt = expensiveWork(data); / / Сохранение результатов в localStorage, / / чтобы их не приходилось вычислять заново localStorage.setItem (key, re s u lt); return re s u lt; } Учтите, что операци я долж на бы ть достаточно м едленной, чтобы преимущ ества м ем оизаци и перевесили затраты на процесс сер и ал и зац и и /д есер и ал и зац и и (н а ­ пример, криптограф ически й алгоритм ). С оответственно, localStorage лучш е всего работает в том случае, если м ем оизаци я эконом ит время, расходуем ое на передачу данны х по сети. Технология веб-хранилищ имеет свои ограничения, но д ля правильно вы бранны х задач она становится мощ ны м и просты м инструментом. Также заслуж иваю т в н и ­ м ан и я некоторы е другие темы, относящ и еся к хранению данны х браузером: О IndexedD B ; О Service w orkers; О O ffline-first. 8.14. Хранение данных в браузере 273 8.14.3. localForage О сновны е недостатки веб-хранилищ а — синхронны й A P I с блокировкой и огра­ н и ченн ая емкость пам яти в некоторы х браузерах. К ром е веб-хран илищ а многие современные браузеры поддерживают W ebSQ L и (и л и ) IndexedD B. О ба хранилищ а данны х работаю т в неблокирую щ ем реж им е и позволяю т хранить гораздо больш е данны х, чем технологии веб-хранилищ а. О днако использовать любую из этих баз данны х напрямую , как это делалось с A PI веб-хранилищ а, не рекомендуется. Технология W ebSQ L считается устаревш ей, а ее наследник IndexD B имеет неудобны й и гром оздкий A PI, не говоря уж е об и сп рав­ лениях, необходимых для поддерж ки в браузерах. Ч тобы удобно и надежно хранить данны е в браузере без блокировки, мы реком ендуем использовать нестандартны й и н стр у м ен т д л я « н о р м а л и за ц и и » и н тер ф ей са. О д н и м из т ак и х и н стр у м ен то в н орм ал и зац и и я в л я е т с я б и бли отека localF orage от M ozilla (http://m ozilla.github. io/localForage/). Обзор API И нтерф ейс localForage довольно близко воспроизводит интерф ейс веб-хранилищ а, хотя и в асинхронной (не блокирую щ ей) форме: О lo c a lf o r a g e .s e tI te m ( k e y , v a lu e , c a llb a c k ) — сохраняет значение с заданны м ключом; О l o c a l f o r a g e . g e t I t e m ( k e y j c a l l b a c k ) — п о л у ч ает зн а ч е н и е д л я зад ан н о го клю ча; О lo c a lf o r a g e .r e m o v e I te m ( k e y j c a l lb a c k ) — у д ал яет зн ач ен и е д л я зад ан ного ключа; О lo c a l f o r a g e .c l e a r ( c a l lb a c k ) — удаляет все клю чи и значения; О lo c a lf o r a g e .k e y ( in d e x j c a llb a c k ) — получает зн ачение с зад ан ны м индексом; О lo c a l f o r a g e .l e n g t h ( c a l lb a c k ) — возвращ ает количество клю чей в localForage. A P I lo c a lF o ra g e так ж е в кл ю чает п о л езн ы е д о п о л н ен и я, не им ею щ и е аналогов в веб-хранилищ е: О lo c a lf o r a g e .k e y s ( c a llb a c k ) — удаляет все клю чи и значения; О l o c a l f o r a g e . i t e r a t e ( i t e r a t o r , c a llb a c k ) — перебирает клю чи и значения. 8.14.4. Чтение и запись A P I localForage поддерж ивает как обещ ания (prom ises), так и схему обратного вы ­ зова N ode с объектом ош ибки на первом месте. 274 Глава 8. Хранение данных в приложениях Листинг 8.28. Сравнение получения данных в localStorage и localForage const value = localStorage.getItem (key); console.log(value); lo calfo rag e. getItem(key) .then(value => console.log(value)); I localStorage: блокировка, -<------ 1 синхронное выполнение. localForage: без блокировки, асинхронное выполнение с использованием обещаний. localforage.getItem (key, (e rr, value) => { < console.log(value); }); localForage: без блокировки, асинхронный вызов с использованием обратных вызовов в стиле Node. Во внутренн ей р еал и зац и и localF orage исп ользует л учш ий м еханизм хранения, д о ст у п н ы й в те к у щ е й среде б р ау зер а. Е сл и п о д д ер ж к а In d e x e d D B д оступ н а, localForage использует ее. В противном случае будет сделана поп ы тка п ереклю ­ читься на W ebSQ L и даж е использовать веб-хранилищ е при необходимости. Вы м ож ете н астрои ть порядок, в котором будут опробованы м еханизм ы хранения, и даж е исклю чить некоторы г е гварианты: Никогда не возвращается к использованию localStorage. / / Не будет использовать localStorage я localforage.setDriver([localforage.INDEXEDDB, localforage.WEBSQL]); В отличие от localStorage, localForage не ограничивается хранением одних лиш ь строк. П оддерж ивается больш инство прим итивов Ja v a S c rip t (таких, как м ассивы и объекты ), а такж е двоичны е типы данных: TypedArray, A rra y B u ffe r и Blob. У чти­ те, что IndexedD B — единственная подсистема баз данны х, изначально способная хранить двоичны е данные: при использовании W ebSQ L и localStorage появляю тся затраты , связанны е с преобразованием данных: Prom ise.all([ lo calforage.setItem ('num ber', 3), lo c a lfo ra g e .s e tIte m ('o b je c t', { key: 'v alu e' }), lo c a lfo ra g e.s etIte m ('ty p e d arra y ', new Uint32Array([1,2,3])) ]); П овторение A P I веб-хран илищ а делает работу с localForage более интуитивной, а такж е исклю чает многие недостатки и проблемы совместимости при организации хранени я данны х в браузере. 8.15. Виртуальное хранение Виртуальное хранение (h o ste d sto rag e) — другая тактика, которая позволяет и зб е­ ж ать необходим ости сам остоятельной организации хранени я на стороне сервера. Сервисы виртуальной инфраструктуры — например, предоставляемые AWS (Amazon W eb Services) — часто рассм атриваю тся исклю чительно как средство о п ти м и за­ ции бы стродействи я и м асш табирования, но ум ное исп ользован ие виртуальны х 8.15. Виртуальное хранение 275 сервисов с ранней стадии работы мож ет сэконом ить м ного времени, которое было бы потрачено на неудачную и излиш ню ю реализацию инф раструктуры . М н о ги е (е с л и не в с е ) базы д ан н ы х , п е р е ч и с л ен н ы е в это й главе, п ред л агаю т возм ож н ость виртуального хранени я. В ир ту ал ьн ы е сервисы позволяю т быстро опробовать инструм ентарий и даж е развернуть общ едоступны е при лож ен и я без хлопот, связанны х с настройкой сам остоятельного хостинга базы данных. Тем не м енее р азв ер н у ть собственную б азу д ан н ы х стан о ви тся все прощ е. М ноги е о б ­ лачн ы е сервисы предоставляю т готовы е образы серверов со всем необходимы м програм м ны м обеспечением и конф и гурац ией д ля запуска м аш ины с вы бранной вам и базой данных. 8.15.1. S3 A m azon S im ple S to rag e Service (S 3 ) — серви с удал ен н ого р азм ещ ен и я ф ай лов, предоставляем ы й как часть популярного ком плекса сервисов AWS. S3 представ­ ляет собой эф ф екти вн ы й по затратам м еханизм хранения и разм ещ ен ия ф айлов, доступн ы х по сети. П о сути это ф ай л о в ая систем а в облаке. С исп ользован ием R E S T -совм естим ы х вы зовов H T T P ф айлы м огут загруж аться в гнезда (b u ck ets) наряду с 2 К байт метаданны х. Д алее к содерж им ом у гнезд м ож но обращ аться м е­ тодом H T T P G E T или через протокол B itT orrent. Д л я гнезд и их содерж имого м ож но настроить разны е уровни разреш ений, вкл ю ­ ч ая доступ, кон троли руем ы й по времени. Такж е м ож но задать срок ж и зни д ля со­ держ им ого гнезд; по истечении этого срока содерж имое становится недоступны м и удаляется из гнезда. Д анны е S3 легко преобразую тся в сеть доставки контента (C D N , C o n te n t D elivery N etw o rk ). AWS предоставляет сервис C lo u d F ro n t CDN, которы й м ож но легко связать с ваш им и ф айлам и д ля обращ ения к ним с низкой задерж кой из лю бой точки мира. Н е все данны е необходимо (и л и хотя бы ж елательн о) хранить в базе данных. Есть ли в ваш их данны х компоненты , которы е могут рассм атриваться как ф айлы ? П о ­ сле того как вы сгенерировали результаты слож ны х вы числений д ля пользователя, возмож но, вам стоит направить эти результаты в S3, а затем навсегда устраниться от обеспечения доступа к ним. Распространенны й и наиболее очевидный вариант использования S3 — размещ ение ресурсов, отправленны х пользователем (наприм ер, граф ики). Ресурсы хранятся во временном каталоге на маш ине прилож ения, обрабатываю тся таким инструментом, как ImageM agick, для сокращ ения размера файла, и в дальнейш ем отправляю тся в S3 для хостинга. Этот процесс м ожно еще сильнее упростить, отправляя данные прямо в S3, где они инициирую т дальнейш ую обработку. К лиентские при лож ения такж е м огут о тправлять данны е прям о в S3. Н екоторы е сервисы , ориен ти рован ны е на разработчиков, даже могут не предоставлять собственное хранилищ е; пользователь 276 Глава 8. Хранение данных в приложениях долж ен предоставить м аркеры доступа, чтобы прилож ение пользовалось собствен­ ны м и гнездам и S3. S3 не ограничивается хранением графики С ер ви с S3 м ож ет и с п о л ьзо в атьс я д л я х р ан ен и я лю бы х ти п ов ф ай л о в в лю бом ф орм ате разм ером до 5 терабайт. S3 лучш е всего подходит д ля больш их объемов данны х, которы е и зм ен яю тся о тн о си тел ьн о редко, к которы м об ращ аю тся как к едином у целому. Х ранени е данны х в S3 обходит все слож ности с н астрой кой и сопровож дением сервера д ля разм ещ ен ия и хранени я ф айлов. Этот вариант отлично подходит для сценариев, в которы х относительно редко записы ваю тся больш ие блоки данных, к которы м происходят атом арны е обращ ения (то есть обращ ения как к единому целом у), с больш им количеством чтени й из м ногих потенциальны х мест. 8.16. Какую базу данных выбрать? В этой главе рассм отрены л и ш ь некоторы е из баз данны х, часто исп ользуем ы х в п р и ло ж ен и ях N ode. Успеш ные п р и ло ж ен и я могут строиться на основе лю бых из этих баз данных. В одном при лож ении не всегда находится идеальное реш ение хранени я данных; панацеи не сущ ествует. У каж дой базы данны х есть свои плюсы и минусы , и разработчик долж ен оценить, какие стороны лучш е подходят для т е ­ кущ его состояния проекта. Ч асто наиболее уместны м реш ением оказы вается ком ­ бинаци я технологий. Вместо того чтобы спраш ивать: «К акую базу данны х следует использовать?», стоит задаться вопросом: «Н асколько далеко мож но зайти вообще без использования базы данных?» Какую часть проекта можно построить с м иним у­ мом долгосрочных реш ений? Ч асто реш ения лучш е отлож ить на будущее; вы всегда смож ете при нять лучш ее реш ение позднее, когда у вас будет больш е инф орм ации. 8.17. Заключение О В N ode могут использоваться как реляционны е базы данных, так и базы данных N oSQ L. О П ростой м одуль N ode pg отлично подходит д ля работы с язы ком SQL. О М одуль K nex позволяет работать с нескольким и базам и данны х в Node. О A C ID — набор характеристик тран закц и й баз данных, обеспечиваю щ ий безо­ пасность данных. О M ongoD B — база данны х N oSQ L, использую щ ая Jav aS crip t. 8.17. Заключение 277 О R edis — храни ли щ е структур данных, которое мож ет исп ользоваться как база данны х и кэш. О LevelD B — быстрое хранилищ е пар «клю ч-значение» ком пании Google, ассоци­ ирую щ ее строки со значениям и. О LevelD B я в л я е т с я м одульной базой данных. О Технологии веб-хранилищ а, вклю чая localForage и localStorage, могут исп оль­ зоваться д ля хранени я данны х в браузере. О Такие сервисы, как Am azon S3, могут использоваться для сохранения данны х у провайдеров облачны х сервисов. Тестирование приложений Node П о мере расш ирения ф ункц иональн ости при лож ения возрастает риск появления ош ибок. Б ез тести р о в ан и я при ло ж ен и е счи тается незаверш енны м , а поскольку ручное тестирован ие утом ительн о и чревато новы м и ош ибками, обусл овл ен н ы ­ ми человеческим ф актором , у разработчиков все более поп улярн ы м становится автом ати зи рован н ое тестирование. В этом случае вм есто того, чтобы проверять ф ункциональность при лож ения вручную, разработчик пиш ет логику автом ати зи ­ рованного тестирован ия кода. Е сли вы ранее не сталкивались с концепцией автоматизированного тестирования, во о б р ази те себе робота, к о то р ы й вм есто вас в ы п о л н я ет всю рути н н у ю работу, пр ед о ставл яя вам возм ож ность зан я ться более ин тересны м и вещ ами. Тогда при внесении изм ен ен ий в код робот автом атически проверяет, не вкрались л и туда ош ибки. Д аж е если вы еще не закон чи ли разработку при лож ения или ж е только начали создавать свое первое N o d e-прилож ение, вам полезно знать, как р еал и зо ­ вать автом атизированное тестирование, поскольку тогда вы смож ете писать тесты в процессе разработки прилож ения. В этой главе рассм атриваю тся два типа автом атизированного тестирования: м о ­ дульное и приемочное. Модульное тестирование направлено на непосредственную проверку логики прилож ения, обычно на уровне ф ун кц и и или метода; м одульное тестирование применимо ко всем типам прилож ений. В плане методологии модуль­ ное тестирован ие м ож ет бы ть разделено на две основны е категории: разработка через тестирование (T est-D riven Developm ent, T D D ) и разработка через реализацию п о вед ения (B e h a v io r-D riv e n D evelopm ent, B D D ). С п ракти ческой точки зрени я тесты в стиле T D D и B D D почти во всем одинаковы , несм отря на стилистические разл и ч и я (которы е могут быть важ ны в зависимости от того, кому придется читать ваш и тесты ). М еж ду тестам и в стиле T D D и B D D сущ ествую т и некоторы е другие различия, но их описание вы ходит за рам ки темы книги. Приемочное тестирование п редставляет собой до п о л н и тел ьн ы й уровень тестирован ия, п ри м ен яем ы й п р е ­ имущ ественно при отладке веб-приложений. Этот вид тестирования подразумевает 9.1. Модульное тестирование 279 сценарное управлени е браузером и проверку ф ункц иональн ости веб-прилож ений с его помощью. В этой главе представлены готовы е реш ения, иллю стрирую щ ие модульное и п р и ­ ем очн ое тести р о в ан и е. В р ам к ах м о д у л ьн о го т ест и р о в а н и я м ы п о зн ак о м и м ся с N od e-модулем assert и со средам и M ocha, Vows и should.js, а такж е Chai. В рамках приемочного рассм атривается среда Selenium . И нструм енты тестирован ия вместе с соответствую щ им и м етодологиям и и подходами представлены на рис. 9.1. Рис. 9.1. Обзор тестовых фреймворков Н ачнем с м одульного тестирования. 9.1. Модульное тестирование М одульное тестирование — это такая разновидность автоматического тестирования, при которой вы пиш ете логику д ля тестирован ия отдельны х частей прилож ения. Б л агодаря тестам вы см ож ете более критично оценить архитектуру при лож ения и избежать ловуш ек на ранних этапах разработки. Тесты также придадут уверенность в том, что последние изм ен ен ия не при вели к появлению ош ибок. Х отя написание м одульны х тестов требует определенного времени, вы смож ете сэконом ить время в дальнейш ем , поскольку вам не придется вручную проверять прилож ение после внесения изменений. М одульное тестирование мож ет быть довольно сложным, причем асинхронная л о­ гика все еще больше усложняет. А синхронные модульные тесты могут вы полняться 280 Глава 9. Тестирование приложений Node п ар ал л ель н о , п оэтом у при р азр аб о тк е тестов следует п р о я в л я ть осторож ность и следить, чтобы тесты не в л и я л и друг на друга. Н апри м ер, если тесты создаю т врем енны е ф айлы на диске, будьте вним ательны при удалении этих ф айлов после заверш ения каждого теста, чтобы не удалить рабочие ф айлы другого теста, который еще не заверш ился. С учетом подобны х особенностей, м ногие среды модульного тестирования предусматриваю т средства управления порядком вы полнения тестов. В этом разделе мы рассмотрим: О встроенный в Node модуль assert — неплохой вариант д ля пользователей, осва­ иваю щ их автом атизированное тестирование в стиле TD D ; О Mocha — относительно новы й тестовы й ф рейм ворк, которы й мож ет и сп ользо­ ваться для тестирован ия в стиле T D D или BDD; О Vows — ш ироко используем ы й ф рейм ворк тестирован ия в стиле B D D ; О shouldjs — модуль, которы й строится на основе м одуля N ode assert д ля прове­ ден ия тестирован ия в стиле B D D . В следую щ ем разделе продем онстрировано тестирование бизнес-логики с исп оль­ зованием м одуля assert, входящ его в поставку Node. 9.1.1. Модуль assert В больш инстве случаев м одульное тестирование при лож ений N ode вы полняется с пом ощ ью встроен ного м о д у л я assert. П ри этом п ровод и тся п роверк а некоего условия, и, если условие не вы полняется, генерируется ош ибка. М одуль assert и с­ пользуется м ногим и сторонним и ф рейм воркам и тестирования. Впрочем, даж е без ф рейм ворка он м ож ет быть весьма полезен д ля тестирования. Простой пример П редполож им , у вас им еется простое п р и лож ение д ля веден ия спи ска за п л а н и ­ рованн ы х дел. Это при лож ение нуж но протестировать и убедиться в том, что все работает как положено. В листинге 9.1 определяется модуль, содержащий основную ф ункциональность при­ лож ения. Л огика модуля поддерж ивает создание, извлечение и удаление элементов списка заплани рованны х дел. М одуль такж е содерж ит простой метод doAsync, что п о зво л яет проследить за тестирован ием асинхрон ны х методов. С охрани те этот ф айл под именем todo.js. Листинг 9.1. Модель списка запланированных дел class Todo { constructor() { th is.to d o s = []; < -------- Определяет базуданных запланированныхдел. 9.1. Модульное тестирование 281 } add(item) { < -------- Добавляет элемент списка. i f (!item ) throw new Error('Todo.prototype.add requires an ite m '); th is.to d o s.p u sh (item ); } d eleteA ll() { -<-------- Удаляет все элементы из списка. th is .to d o s = []; } get length() { < -------Определяет количество элементов списка. retu rn th is .to d o s.le n g th ; } doAsync(cb) { < -------- Выполняет обратный вызов через 2 секунды. setTimeout(cb, 2000, tru e ); } } module.exports = Todo; -<-------- Экспортирует функциюTodo. Теперь можно воспользоваться модулем Node assert для тестирования кода. Введите в ф айле test.js код из листинга 9.2 д ля загрузки необходимых модулей, создайте но­ вы й список дел и присвойте значение переменной, в которой храни тся количество заверш енны х тестов. Листинг 9.2. Настройка требуемых модулей const a s s e rt = r e q u ir e ( 'a s s e r t') ; const Todo = r e q u ir e ( './to d o ') ; const todo = new Todo(); le t testsCompleted = 0; Тестирование значения переменной с помощью утверждения equal Теперь можно добавить тест, проверяющ ий функциональность удаления элементов из списка запланированны х дел. Добавьте функцию из листинга 9.3 в конец ф айла test.js. Листинг 9.3. Тест, проверяющий, что элементы не остаются в списке после удаления Убеждается в том, что function deleteT est() { данные были добавлены todo.add('D elete Me'); < -------- Добавляет данные для проверки удаления. правильно. a sse rt.e q u a l(to d o .len g th , 1, '1 item should e x i s t ') ; •< to d o .d e le te A ll(); <.-------- Удаляет все записи. a sse rt.e q u a l(to d o .len g th , 0, 'No items should e x i s t ') ; -<Убеждается в том, testsCompleted++; <.-------- Отмечает, что тест прошел успешно. что запись была } удалена. Тест добавляет элемент todo, а затем удаляет его. Если логика прилож ения работает правильно, в конце теста в списке не долж но остаться ни одного запланированного дела — а следовательно, значение to d o .le n g th долж но быть равно 0. Если ж е во з­ никнут проблемы, генерируется исключение. Если же значение, возвращ аемое todo. le n g th , отлично от 0, в результате проверки утверж ден ия на консоль вы водится 282 Глава 9. Тестирование приложений Node трассировка стека с сообщ ением об ош ибке «No item s should exist». После успеш ной проверки значение перем енной te stsC o m p le ted увеличивается, показы вая, что тест пройден успеш но. Выявление возможных проблем с помощью утверждения notEaqual В качестве следую щего ш ага добавьте в ф айл test.js код из листинга 9.4. С помощью этого кода проверяю тся ф ункциональность прилож ения, реализую щ ая добавление элем ентов в список заплани рованны х дел. Листинг 9.4. Тестирование добавления элементов в список запланированных дел function addTest() { to d o .d e le te A ll(); -<-------- Удаляет все существующие элементы. todo.add('A dded'); -<-------- Добавляет элемент. assert.notE qual(todo.getC ount(), 0, '1 item should e x i s t ') ; -<---- . a a ,■ ч t ■ * I Убеждается в том, что testsCompleted++; -<-------- Отмечает, что тест прошел успешно. } I элементы существуют. К ак видите, м одуль assert такж е поддерж ивает утверж ден ия notE qual. Э тот тип утверж дений п ри м ен яется в тех случаях, когда код п ри лож ения генерирует опре­ деленное значение, свидетельствую щ ее о наличии проблем в логике прилож ения. В листинге 9.4 демонстрируется использование утверждения notEqual. Все элементы спи ска удаляю тся, элем ент добавляется, затем л огика при лож ен и я получает все элементы. Если количество элементов равно 0, значит, утверждение несостоятельно и в результате генерируется исклю чение. Использование дополнительной функциональности: strictEqual, notStrictEqual, deepEqual, notDeepEqual П ом им о утверж дений eq u a l и notE qual, м одуль assert предлагает строгие (stric t) версии утверж дений, s t r i c tE q u a l и n o tS tr ic tE q u a l. В этих утверж ден иях исп оль­ зуется оператор строгого равенства (===) вместо более «свободной» версии (==). Д л я сравнения объектов при м ен яю тся утверж ден ия deepEqual и notDeepEqual из м одуля assert. Слово «deep» (глубокий), используем ое в н азван иях утверж дений, означает, что вы полняется рекурсивное сравнение двух объектов, то есть ср авн и ­ ваю тся свойства д вух объектов, а если свойства сами я в л я ю т ся объектам и, они тож е сравниваю тся. Проверка асинхронных значений на истинность с помощью утверждения ok П риш ло врем я протестировать метод doAsync в прилож ении списка зап лани рован­ ных дел (листинг 9.5). П оскольку тест явл яется асинхронным, передается ф ункция обратного вы зова (cb), которая сигнализирует исполнителю о заверш ении теста. В данном случае вы не м ож ете полож иться на то, что ф ун кц и я вернет результат, 9.1. Модульное тестирование 283 как в случае с синхронны м и тестами. Д л я проверки значения doAsync при м ен яет­ ся утверж дение ok — оно позволяет легко удостовериться в том, что это значение равно tru e . Листинг 9.5. Тестирование передачи значения true при обратном вызове doAsync function doAsyncTest(cb) { todo.doAsync(value => { -<-------- Обратный вызов срабатывает через 2 секунды. a ssert.o k (v alu e, 'Callback should be passed tr u e ') ; Проверяет значение testsCompleted++; < -------- Отмечает, что тест прошел успешно. на истинность. cb(); < -------- Инициирует обратные вызовы при завершении. }); } Тестирование корректности механизма генерирования ошибок С пом ощ ью м о ду л я assert м ож но такж е проверить корректн ость генерировани я сообщ ений об ош ибках, как показано в листинге 9.6. Во втором аргументе вы зова throw s передается регулярное вы раж ение, которое ищ ет в сообщ ении об ош ибке текст «requires». Листинг 9.6. Тестирование генерирования ошибки при отсутствии параметра function throwsTest(cb) { assert.throw s(todo.add, /r e q u ir e s /) ; < -------- todo.add вызывается без аргументов. testsCompleted++; < -------- Отмечает, что тест прошел успешно. } Добавление логики запуска тестов П осле определения тестов м ож но добавить в ф айл логику запуска каж дого теста. Код, приведенны й в листинге 9.7, запускает каж ды й тест, а затем вы водит к ол и че­ ство запущ енны х и заверш енны х тестов. Листинг 9.7. Выполнение тестов и оповещение о завершении тестов deleteT est(); addTest(); throw sTest(); doAsyncTest(() => { console.log('Completed ${testsCompleted} t e s t s ') ; }); < -------- Оповещает о завершении. Д л я запуска тестов м ож но воспользоваться следую щ ей командой: $ node c h a p te r0 9 -te s tin g /listin g _ 0 9 _ 1 -7 /te st.js Е сли тесты пройдут, сценари й сообщ ит о кол и честве п рой денн ы х тестов. Э тот сценарий достаточно «интеллектуален», чтобы отслеж ивать врем я запуска и з а ­ верш ения тестов во избеж ание проблем, связанны х с некорректны м вы полнением отдельны х тестов. Н апример, при прохож дении теста програм м а мож ет не достиг­ нуть утверж дения. 284 Глава 9. Тестирование приложений Node Ч тобы использовать встроенную в N ode ф ункциональность, каж ды й вариант теста долж ен содерж ать больш ое коли чество ш аблонного кода д л я п одготовки теста (нап р и м ер , уд али ть все эл ем ен ты ) и получить трассу его вы п ол н ен и я (счетчи к completed). Рутина отвлекает внимание разработчика от главной задачи — написания м одульн ы х тестов, поэтом у лучш е воспользоваться специальны м ф реймворком, которы й вы полнит за вас всю тяж елую работу, чтобы вы м огли заняться собственно тестированием бизнес-логики. Д авайте посмотрим, как упростить свою задачу с по­ мощью фреймворка модульного тестирования M ocha от независимого разработчика. 9.1.2. Mocha Mocha — сам ы й поп улярн ы й ф рейм ворк тестирован ия — прост в освоении. Х отя по ум олчанию этот ф рейм ворк использует стиль B D D , с его помощ ью такж е м о ж ­ но тестировать п р и лож ения в стиле T D D . M ocha вклю чает в себя ш ирокий набор средств, позволяю щ их, в частности, обнаруж ивать утечки глобальны х переменных и тестирование на стороне клиента прилож ений. ВЫЯВЛЕНИЕ УТЕЧЕК ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ Н еобходим ость в глобальны х переменных, которы е доступны во всем п р и ­ лож ении, возникает нечасто, к том у ж е согласно передовой практи ке п р о ­ грам м ирования их количество нуж но стрем иться м иним изировать. О днако в ES5 очень просто случайно создать глобальную переменную, просто забыв поставить клю чевое слово var при объявлени и переменной. M ocha помогает обнаруживать случайные утечки глобальных переменных, генерируя ошибку, когда при тестировании обнаруж ивает ком анду создания глобальной п ере­ менной. Ч то б ы о ткл ю ч и ть реж и м о б н ар у ж ен и я у течек гл о б ал ьн ы х перем енн ы х, вклю чите в ком андную строку m ocha параметр --ignored-leaks. Если ж е вы хотите указать допустим ое число и сп ользуем ы х глобальны х переменны х, перечислите их через запятую после парам етра ком андной строки —globals. П о ум олчанию л огика тестов M ocha определяется B D D -ф ун кц и ям и d e s c rib e , i t , b e fo re , a f t e r , beforeE ach и a fte rE a c h . В качестве альтернативы м ож но и сп ользо­ вать T D D -интерф ейс M ocha, в котором d e s c r ib e зам ен яется на s u ite , i t на t e s t , b e fo re на se tu p и a f t e r на teardow n. В наш ем прим ере будет использоваться B D D ин терф ейс по умолчанию . Тестирование Node-приложений с помощью Mocha Д авайте создадим небольш ой проект m em db — ком пактную базу данны х в памяти, а затем с пом ощ ью M ocha протести руем эту базу данны х. С начала д л я проекта нуж но создать ф айлы и папки: 9.1. Модульное тестирование $ $ $ $ $ $ 285 mkdir -p memdb/test cd memdb touch index.js touch test/memdb.js npm i n i t -y npm in s t a ll --save-dev mocha О ткр о й те ф ай л package.json и добавьте свойство s c r i p t s , определяю щ ее способ вы полнен ия тестов: "sc rip ts " : { " te s t" : "mocha" }, Тесты расп о л агаю тся в каталоге t e s t . П о ум олчани ю M ocha и сп ользует B D D ин тер ф ей с. В л и сти н ге 9.8 показано, как он вы гл яд и т (chapter09-testing/m em db в исходном коде книги). Листинг 9.8. Базовая структура тестов Mocha const memdb = r e q u i r e ( '. . ') ; describe('memdb', () => { d escrib e('.sav eS y n c(d o c)', () => { it('s h o u ld save the document', () => { }); }); }); M ocha поддерживает такж е интерф ейсы в стиле TD D , q u n it и e x p o rts, которые под­ робно описаны на веб-сайте проекта (https://mochajs.org/). Чтобы проиллюстрировать концепцию использования различны х интерфейсов, приведем интерф ейс ex p o rts: module.exports = { 'memdb': { '.saveSync(doc)': { 'should save the document': () => { } } } } Все эти интерф ейсы предлагают одинаковую функциональность, но мы ограничимся B D D -интерф ейсом и напиш ем первы й тест, код которого приведен в листинге 9.9. П ом естите его в ф айл test/m em db.js. В этом тесте д ля проверки утверж ден ий и с ­ п ользуется м одуль N ode assert. Листинг 9.9. Описание функциональности memdb .save const memdb = r e q u i r e ( '. . ') ; const a s s e rt = r e q u ir e ( 'a s s e r t') ; describe('memdb', () => { -<-------- Описывает функциональность memdb. d escrib e('.sav eS y n c(d o c)', () => { < -------- Описывает функциональность метода .save(). 286 Глава 9. Тестирование приложений Node it('s h o u ld save the document', () => { < -------- Описывает ожидание. const pet = { name: 'Tobi' }; memdb.saveSync(pet); const r e t = memdb.first({ name: 'Tobi' }); a s s e r t( r e t == p et); -<-------- Проверяет, что питомец был найден. }); }); }); Ч тобы вы полнить тесты, достаточно вы полнить команду npm test. По умолчанию ф рейм ворк ищ ет вы полняем ы е ф айлы Ja v a S c rip t в папке ./test. О днако поскольку метод .s a v e ( ) не реализован, единственны й определенны й к настоящ ем у времени тест провалится (рис. 9.2). w a v d e d Q d e v : "/P ro je c ts /m e m d b wavded@ -/Projects/memdb» mocha x 1 of 1 test failed: 1) memdb .save(doc) should save the document: TypeError: Object #<0bject> has no method 'save' at Context.<anonymous> (/home/wavded/Projects/memdb/test/memdb.js:8:13) at Test.Runnable.run (/usr/local/lib/nodejnodules/mocha/lib/runnable.js:184:32) at Runner.runTest (/usr/local/lib/node_modules/mocha/lib/runner.js:300:10) at Runner.runTests.next (/usr/local/lib/node_modules/mocha/lib/runner.js:346:12) at next (/usr/local/lib/node_modules/mocha/lib/runner.js:228:14) at Runner.hooks (/usr/local/lib/node_modules/mocha/lib/runner.js:237:7) at next (/usr/local/lib/node_modules/mocha/lib/runner.js: 185:23) at Runner.hook (/usr/local/1ib/node_modules/mocha/lib/runner.js:205:5) at process.startup.processNextTick.process._tickCallback (node.js:244:9) wavdedg . ~/Projects/tnemdb» Рис. 9.2. Проваленный тест Mocha А теперь обеспечим успеш ное прохож дение теста. Д обавьте код из листинга 9.10 в ф айл index.js. Листинг 9.10. Добавление функциональности сохранения const db = []; exports.saveSync = (doc) => { db.push(doc); <.-------- Добавляет документ в массив базыданных. }; Выбирает документы, соответствующие e x p o r ts .f ir s t = (obj) => { каждому свойству в obj. return d b .filte r((d o c ) => { -<— for ( le t key in obj) { i f (doc[key] != obj[key]) { Совпадение отсутствует; возвращает return fa ls e ; false и не выбирает документ. } } return tru e ; < -------Все совпало; документ выбирается. } ) .s h i f t ( ) ; < -------- Только первый документ или null. }; 9.1. Модульное тестирование 287 С нова запустите тесты ком андой npm. Результат долж ен вы глядеть прим ерно так, как показано на рис. 9.3. Рис. 9.3. Успешный тест в Mocha Определение логики инициализации и завершения с использованием перехватчиков Mocha В варианте теста из листинга 9.10 предполагается, что m e m d b .first() работает п ра­ вильно, поэтом у м ож но добавить еще несколько вариантов тестов. В измененном ф айле теста, код которого приведен в листинге 9.11, используется новая для M ocha концепция перехватчиков (hooks). Н апример, B D D -интерфейс предоставляет пере­ хватчики b e fo re E a ch (), a f te r E a c h ( ) , b e f o r e ( ) и a f t e r ( ) , приним аю щ ие обратные вы зовы для определения логики ин иц и али зац и и и заверш ения. Листинг 9.11. Добавление перехватчика beforeEach const memdb = r e q u i r e ( '. . ') ; Очищает базуданных перед каждым const a s s e rt = r e q u ir e ( 'a s s e r t') ; тестовым сценарием, чтобытесты describe('memdb', () => { не имели состояния. beforeEach(() => { memdb.clear(); }); describe('synchronous .saveSync(doc)', () => { it('s h o u ld save the document', () => { const pet = { name: 'Tobi' }; memdb.saveSync(pet); const r e t = memdb.first({ name: 'Tobi' }); a s s e r t( r e t == pet); }); }); d e s c r i b e ( '.f i r s t ( o b j ) ', () => { it('s h o u ld return the f i r s t matching do c', () => { -<■ Первое ожидание const to b i = { name: 'Tobi' }; для .first(). const loki = { name: 'Loki' }; memdb.saveSync(tobi); < -------- Сохраняет два документа. memdb.saveSync(loki); l e t r e t = memdb.first({ name: 'Tobi' }); ■< Проверяет правильность a s s e r t( r e t == to b i); возвращения каждого r e t = memdb.first({ name: 'Loki' }); документа. a s s e r t( r e t == lo k i); }); 288 Глава 9. Тестирование приложений Node it('s h o u ld return null when no doc m atches', () => { < ­ const r e t = memdb.first({ name: 'Manny' }); a s s e r t( r e t == n u ll); }); }); }); Второе ожидание для .first(). В идеальном случае тестовы е сценарии вообщ е не долж ны иметь состояния. Ч т о ­ бы добиться этого с базой данны х m em db, нуж но просто удалить все документы, реализовав метод . c l e a r ( ) в ф айле index.js: ex p o rts.c le a r = () => { db.length = 0; }; Зап усти в M ocha снова, вы увидите, что все три теста пройдены. Тестирование асинхронной логики Д о сих пор мы еще не рассм атривали тестирование асинхронной логики в M ocha. Ч тобы посмотреть, как это делается, внесите небольш ое изменение в одну из ф ун к­ ций, которые бы ли ранее определены в ф айле index.js. Если изменить ф ункцию save так, как показано ниже, м ож но передать ф ункцию обратного вызова, которая будет вы полняться после небольш ой задерж ки (им итирую щ ей вы полнение асинхронной операции): exports.save = (doc, cb) => { db.push(doc); i f (cb) { setTimeout(() => { c b (); }, 1000); } }; Тестовые сценарии M ocha определяю тся как асинхронны е просты м добавлением аргум ента в ф ункц ию , задаю щ ую л о ги к у тести р о ван и я. Э тот аргум ент обычно назы вается done. Л и сти н г 9.12 показы вает, как написать тест д л я асинхронного метода save. Листинг 9.12. Тестирование асинхронной логики describe('asyncronous .sav e(d o c)', () => { it('s h o u ld save the document', (done) => { const pet = { name: 'Tobi' }; Акшшвдет офгтнш memdb.save(pet, () => { < -------- Сохраняет документ. вызов с первымдокументом. const r e t = memdb.first({ name: 'Tobi' }); -<----a s s e r t( r e t == p et); -<-------- Проверяет, что документ сохранен правильно. done(); <.-------Сообщает Mocha об окончании тестового сценария. }); }); }); 9.1. Модульное тестирование 289 А налогичное прави ло п р и м ен яется ко всем перехватчикам . Н апри м ер, п ерехват­ ч и к b e fo re E a c h (), пред н азн ач ен н ы й д ля очистки базы данны х, мог бы добавить об ратн ы й вы зов, и тогда ф р ей м в о р к M ocha будет ож идать его зав ер ш ен и я для прод о л ж ен и я работы . Е сли d o n e() вы зы вается с объектом ош ибки в первом аргу­ менте, M ocha сообщ ит об ош ибке и пом етит перехватчи к и ли тестовы й сценарий как непрош едш ий: beforeEach((done) => { memdb.clear(done); }); З а д о п о л н и т е л ь н о й и н ф о р м а ц и е й о M o ch a о б р ащ ай тесь к эл ек т р о н н о й д о к у ­ м ентации по адресу http://mochajs.org. Ф р ей м ворк M ocha такж е мож ет работать с Ja v a S c rip t на стороне клиента. НЕПАРАЛЛЕЛЬНОЕ ТЕСТИРОВАНИЕ В MOCHA В M ocha тесты вы полняю тся не параллельно, а последовательно; хотя этот реж им упрощ ает нап исание тестов, вы полнен ие групп тестов происходит медленнее. Тем не менее M ocha не допустит, чтобы тест продолж ался слишком долго. По ум олчанию M ocha разреш ает каж дом у тесту вы полняться не более 2000 миллисекунд, после чего он считается проваленным. Если вам требуются более дли тельн ы е тесты, запустите M ocha с парам етром ком андной строки --tim eo u t и укаж ите больш ее врем я тестирования. В больш инстве случаев последовательны й реж им вы полнен ия тестов п р и ­ емлем. Е сли вам этот способ не подходит, воспользуйтесь другим и ф реймворкам и тестирования, работаю щ им и в параллельном режиме, — таким и как ф рейм ворк Vows, рассм атриваем ы й в следую щ ем разделе. 9.1.3. Vows Тесты, создаваем ы е с помощ ью ф рейм ворка модульного тестирования Vows, полу­ чаю тся более структурированны м и, чем с другим и ф рейм воркам и, поэтому Vowsтесты прощ е читать и сопровож дать. Vows и с п о л ьзу ет со бственн ую тер м и н о л о ги ю в сти л е B D D , в р ам к ах которой о п р е д е л я е т с я стр у к ту р а тестов. В Vows гр у п п а тестов со держ и т один и л и б о ­ л ее пакетов (b a tc h e s ). П акет тесто в м ож н о тр а к т о в а ть как гр у п п у св я зан н ы х контекстов ( c o n te x ts ), и л и к о н ц е п т у а л ьн ы х об ластей , к о то р ы е вы хо тел и бы п ротести ровать. П акеты и кон тексты зап у скаю тся параллельно. К онтекст мож ет содерж ать раздел (to p ic ), одно или больш е обязательств (vow s) и (и л и ) один или больш е св язан н ы х кон текстов (вл о ж ен н ы е кон тексты такж е м огут вы п ол н яться 290 Глава 9. Тестирование приложений Node п ар ал л ель н о ). Р азд ел — это л о ги ка тести р о ван и я, с вя зан н ая с контекстом . О б я ­ зательство п р ед ставл яет собой тест результата раздела. С труктура Vows-тестов п р ед ставлена на рис. 9.4. Рис. 9.4. В Vows для структурирования тестов используются пакеты, контексты, разделы и обязательства Ф р ей м во р к Vows, как и M ocha, ориентирован на автом атизированное тестирова­ ние прилож ений. Р азн и ц а в основном касается оф орм лен ия и параллелизм а: для Vows-тестов требую тся особые структура и терминология. В этом разделе мы п о ­ знаком им ся с прим ером теста для при лож ения и вы ясним, как использовать Vows д ля одноврем енного вы полнен ия нескольких тестов. Д обавьте ф рейм ворк Vows в проект, установив его с использованием npm: mkdir -p vow s-todo/test cd vows-todo touch to d o .js touch te s t /t o d o - t e s t .js npm i n i t -y npm in s ta l l --save-dev -g vows Vows следует добавить в свойство t e s t ф ай л а package.json, чтобы д ля проведения тестов было достаточно ввести ком анду npm t e s t : "s c rip ts " : { " te s t" : "vows t e s t /* .j s " }, 9.1. Модульное тестирование 291 Тестирование логики приложения с помощью Vows Ч тобы запустить процесс тестирован ия в Vows, нуж но вы полнить либо сценарий, содерж ащ и й л о ги ку теста, либо у ти л и ту ком ан дной строки vows. В следую щ ем прим ере с помощ ью автономного тестового сценария (которы й запускается так же, как и лю бой другой сценарий N o d e) проводится один из тестов основной логики при лож ения для списка заплани рованны х дел. В листинге 9.13 создается пакет тестов. Внутри пакета можно определить контекст; вн утри ко н текста оп р ед ел яю тся раздел и о бязательство. О б ратите вн и м ан и е на и сп о льзо ван и е обратного вы зова д л я обработки аси н хрон н ой л о ги ки в разделе. Е сли раздел не я в л я е т с я асинхронны м , то вм есто передачи зн ач ен и я через обрат­ н ы й вы зов осу щ ествл яется возврат этого значения. С охрани те ф ай л под им енем test/todo-test.js. Листинг 9.13. Использование Vows для тестирования const vows = req u ire('v o w s'); const a s s e rt = r e q u ir e ( 'a s s e r t') ; const Todo = r e q u i r e ( './ ../ t o d o ') ; vow s.describe('Todo').addBatch({ < -------Пакет 'when adding an ite m ': { < -------Контекст to p ic: () => { < -------Раздел const todo = new Todo(); todo.add('Feed my c a t') ; return todo; }, ' i t should e x is t in my to d o s': (e r, todo) => { •<-------a sse rt.e q u a l(to d o .len g th , 1); } } }).export(module); Обязательство Е сли все бы ло сделано правильно, тест м ож но будет запустить ком андой npm t e s t . Е сли вы устан овили Vows глобально ком андой npm i -g vows, все тесты из папки test вы полняю тся следую щ ей командой: $ vows te s t/* З а дополнительной инф орм ацией о Vows обращ айтесь к электронной документации проекта (http://vowsjs.org/) — рис. 9.5. Vows предоставляет всеобъемлю щ ее реш ение для тестирования, причем вы можете заменять функциональность библиотеки тестирования за счет использования другой библиотеки проверки тестовы х утверж дений. Д опустим, вам нрави тся M ocha, но не нравится библиотека утверж дений Node. В следую щем разделе рассматривается C hai — библиотека тестовы х утверж дений, которая м ож ет использоваться вместо м одуля N ode assert. Глава 9. Тестирование приложений Node 292 «- С ; D vowsJs.org Write som e vowe, oxocute them: 1lows Asynchronous behaviour driven development for Node. There are tw o reasons why we might w ant asynchronous testing. The first, and obvious reason is that node js is asynchronous, and therefore our tests should be The second reason is to make tests which target I/O run much faster, by running them concurrently. Get the report, mako sure you kept your w / M u l t be to w fte d to an ( v M U t t t w A topic not «Bitting on error ✓ should pot» m il i f the test І» ( V K ttn g / thould past the result оО чп Л и A topic w itt in g on error / iM u lA t 't ro il* an exception i f the test A context with о netted context ✓ hot access to the environment • con makt coffee A netted context / should hove access to the parent topics A netted context with no topics / should past the porent topics дояп / ОС • 7 honored • 1 pending (Ф .Ш t) Рис. 9.5. Vows сочетает полнофункциональное BDD-тестирование с макросами и управлением логикой выполнения 9.1.4. Chai Chai (http://chaijs.com /) — поп улярн ая библиотека тестовы х утверж дений, вкл ю ­ чаю щ ая в себя три интерф ейса: should, e x p ec t и a s s e r t . И нтерф ей с a s s e r t , пред­ ставленны й в листинге 9.14, внеш не напоминает встроенны й модуль тестовы х у т ­ верж дений Node, но вклю чает полезны е средства для сравнения объектов, массивов и их свойств. Н апример, м етод typeO f мож ет использоваться д ля сравнения типов, а p ro p e rty проверяет, обладает ли объект нуж ны м свойством. Листинг 9.14. Интерфейс assert в Chai const const const const chai = r e q u ir e ( 'c h a i') ; a s se rt = c h a i.a s s e rt; -<-------- Выбирает интерфейс assert. foo = 'b a r '; tea = { fla v o rs: [ 'c h a i ', 'e a r l g re y ', 'pg t i p s '] }; assert.typeO f(foo, 's t r i n g ') ; asse rt.e q u a l(fo o , 'b a r ') ; assert.len g th O f(fo o , 3); a ss e rt.p ro p e rty (te a , 'f la v o r s ') ; a sse rt.le n g th O f(te a .fla v o rs, 3); Впрочем, главной причиной для использования C hai все же яв л яю тся интерф ейсы should и ex p ect. О ни предоставляю т динам ичны й A PI, напоминаю щ ий сп ец и али ­ зирован ны е библиотеки в стиле B D D . Вот как вы глядит ин терф ейс expect: const chai = r e q u ir e ( 'c h a i') ; const expect = chai.expect; const foo = 'b a r '; 9.1. Модульное тестирование 293 e x p e c t( f o o ) .to .b e .a ( 's tr in g ') ; e x p e c t(fo o ).to .e q u a l('b a r'); Такой A P I вы глядит как предложение на английском язы ке — декларативны й стиль не такой компактный, но проще читается. В интерфейсе should все наоборот: объекты декорирую тся д ополнительны м и свойствам и, так что вам не придется заклю чать проверку в вы зов метода, как в случае expect: const chai = r e q u ire ( 'c h a i') ; chai.should(); const foo = 'b a r '; fo o .s h o u ld .b e .a ('s trin g '); fo o .sh o u ld .e q u a l('b a r'); В ы бор исп ользуем ого и н тер ф ей са зави си т от проекта. Е сли вы начинаете с н а ­ п и с а н и я т есто в , к о т о р ы е и с п о л ь з у ю т с я д л я д о к у м е н т и р о в а н и я п р о ек та , д е ­ т а л и зи р о в а н н ы е и н те р ф е й с ы e x p e c t и s h o u ld п од ой д ут хорош о. Б л ю сти тел и ч и стоты Ja v a S c rip t предпочитаю т e x p e c t, потом у что этот вари ан т не изм ен яет прототипы , но р азр або тч и кам с опы том R uby м огут бы ть более зн ак ом ы такие A P I, как should. Главным преимущ еством Chai является ш ирокий ассортимент плагинов. К их числу относятся такие расш ирения, как chai-as-prom ised (http://chaijs.com/plugins/chaias-promised/), упрощ аю щ ее написание тестового кода с использованием обещ аний, и chai-stats (http://chaijs.com/plugins/chai-stats/) — библиотеки для сравнения чисел в соответствии со статистическим и методами. О братите внимание: C hai я вл яется библиотекой тестовых утверждений, которая может использоваться наряду с такими систем ам и вы полнения тестов, как M ocha. Еще одна библиотека тестовых утверждений B D D — Should.js. В следующем разделе мы рассмотрим библиотеку Should.js и покажем, как писать тесты с использованием этой библиотеки. 9.1.5. Библиотека should.js Библиотека should.js — это библиотека тестовых утверждений, которая сделает ваши тесты более понятны м и, так как вы смож ете вы раж ать утверж ден ия в стиле BDD. П оскольку она предназначена для использования вместе с другим и ф рейм воркам и тестирования, вам не придется отказываться от своего любимого фреймворка. В этом разделе мы разберем ся, как с помощ ью Should.js писать утверж дения, и напиш ем тест для собственного модуля. Б и блиотеку Should.js неслож но использовать с другим и средами тестирования, по­ скольку она просто дополняет O b je c t.p r o to ty p e единственны м свойством should. В результате становится возм ож ны м создание таких вы разительны х утверждений, как u s e r .r o le .s h o u ld .e q u a l( " a d m in " ) или u s e r s .s h o u ld .in c lu d e ( " r ic k " ) . 294 Глава 9. Тестирование приложений Node Д опустим , вы пиш ете в N ode калькулятор, запускаем ы й из ком андной строки; он пом ож ет вам понять, кто сколько долж ен заплатить чаевых, когда вы с друзьям и реш ите пропустить по стаканчику. Естественно, вам придется написать тесты для п р о вер к и л о ги к и вы чи сл ен и й , причем они долж н ы бы ть п о н ятн ы даж е ваш им д рузьям -гум анитариям , чтобы они не реш или, что вы пытаетесь их надуть. Ч тобы создать п ри лож ен и е-калькулятор, введите следую щ ие команды, которы е позволят вы брать папку д ля при лож ения и библиотеки тестирован ия Should.js: mkdir -p t i p s / t e s t cd tip s touch in d ex .js touch t e s t / t i p s . j s Теперь м ож но установить библиотеку Should.js следую щ им и командами: npm i n i t -y npm in s ta l l --save-dev should Затем отредактируйте содерж имое ф айла index.js, которы й будет содержать логику, определяю щ ую базовую ф ункциональность прилож ения. Точнее, л огика кал ьк у ­ л я ц и и чаевы х будет содерж ать четы ре вспом огательны е функции: О addPercentageT oE ach — увеличивает каж дое число в м ассиве на заданную п ро­ центную величину; О sum — вы числяет сумму всех элем ентов массива; О p ercen tF o rm at — ф орм атирует вы водим ую процентную величину; О d o lla rF o rm a t — ф орм атирует вы водим ую величину в долларах. Д обавьте описанную логику, вклю чив в ф айл index.js код из листинга 9.15. Листинг 9.15. Логика вычисления чаевых exports.addPercentageToEach = (p ric e s, percentage) => { •< Прибавляет процентную return p rices.m ap ((to tal) => { величину кэлементам to ta l = p a rse F lo a t(to ta l); массива. return to ta l + ( to ta l * percentage); }); }; exports.sum = (prices) => { -<-------- Вычисляет сумму элементов массива. return prices.reduce((currentSum , currentValue) => { return parseFloat(currentSum) + parseFloat(currentV alue); }); }; exports.percentFormat = (percentage) => { ■<------ Форматирует процентную величинудля вывода. return parseFloat(percentage) * 100 + '%'; }; exports.dollarForm at = (number) => { < -------- Форматирует денежную сумму вдолларахдля вывода. return '$${parseFloat(num ber).toFixed(2)}'; }; 9.1. Модульное тестирование 295 Затем отредактируйте тестовы й сценарий в ф айле test/tips.js в соответствии с л и ­ стингом 9.16. Э тот сц ен ар и й загр у ж ает м о ду л ь с л о ги ко й в ы ч и сл ен и я чаевы х, определяет величину налога, процент чаевы х и тестируем ы е пози ции счета, а т а к ­ ж е дополнительно тестирует добавление процента д ля каж дого элем ента м ассива и общую сумму счета. Листинг 9.16. Логика тестирования кода вычисления чаевых const const const const const tip s = r e q u i r e ( '. . ') ; < -------- Использует модуль с логикой вычисления чаевых. should = re q u ire ('s h o u ld '); tax = 0.12; < -------Определяет ставку налога и процент чаевых. t i p = 0.15; prices = [10, 20]; < -------- Определяет тестируемые позиции счета. const pricesWithTipAndTax = tips.addPercentageToEach(prices, tip + tax ); pricesW ithTipAndTax[0].should.equal(12.7); < -------- Тестирует добавление налога и чаевых. pricesW ithTipAndTax[1].should.equal(25.4); const totalAmount = tips.sum(pricesW ithTipAndTax).toFixed(2); totalA m ount.should.equal('38.10'); < -------- Тестирует суммирование позиций счета. const totalAmountAsCurrency = tips.dollarForm at(totalA m ount); totalAm ountAsCurrency.should.equal('$38.10'); const tipAsPercent = tip s.p ercen tF o rm at(tip ); tipA sPercent.should.equal('15% '); Запустите сценарий с помощ ью следую щ ей команды. Если все сделано правильно, сценарий ничего не долж ен вы водить на экран, поскольку мы не делали никаких утверж дений, и ваш и друзья л иш ний раз удостоверятся в ваш ей честности: $ node t e s t / t i p s . j s Ч тобы упр о сти ть запуск, добавьте ком ан ду в свойство t e s t из р азд ел а s c r i p t s в ф айле package.json: "sc rip ts " : { " te s t" : "node t e s t / t i p s . j s " } Б и бли отека Should.js поддерж ивает м ногие типы тестовы х утверж дений — от у т ­ верждений, использую щ их регулярны е вы раж ения, до утверждений, проверяю щ их свой ства объектов. В результате обесп ечи вается исчерпы ваю щ ее тести рован и е дан н ы х и объектов, ген ери руем ы х при лож ен и ем . Н а стран и ц е проекта G itH u b ( http://github.com /shouldjs/should.js) есть п о л н а я д о ку м ен тац и я, опи сы ваю щ ая ф ункциональность библиотеки Should.js. П омимо библиотек тестовы х утверж дений, для управления вы полнением тестиру­ емого кода часто при м ен яю тся шпионы, заглушки и макеты. В следую щ ем разделе показано, как это делается при использовании Sinon.js. 296 Глава 9. Тестирование приложений Node 9.1.6. Шпионы и заглушки в Sinon.JS П о сл ед н и й и н стр у м ен т в ваш ем и н с тр у м е н тар и и тест и р о в ан и я — б и б л и о тек а м акетов и загл у ш ек. М ы пиш ем м о ду л ьн ы е тесты д л я и зо л я ц и и ч астей т е ст и ­ руем ой систем ы , но и н огда д о б и ться этой цели бы вает трудно. П редставьте, что вы тестируете код д л я м асш таб и р о ван и я и зображ ени й. З ап и сы в ать д ан ны е в р е ­ ал ьн ы е гр аф и ч ески е ф ай л ы не хочется, но как п и сать тесты ? В коде не долж но бы ть сп ец и альн ы х ветвей, обх о дящ и х ся без зап и си в ф ай л овую систему, потом у что в тако м случае вы ф ак ти ч еск и не будете тести ровать свой код. В таки х с л у ­ ч ая х необходим о о п р ед ел ять заглушки (s tu b s ) д л я ф у н к ц и о н ал ьн о сти ф ай л овой систем ы . П р ак ти к а н ап и сан и я загл у ш ек такж е пом огает р еал и зо вать при н ц и п T D D , п отом у что вы м ож ете о п р ед ел ять загл у ш к и д л я зави си м остей , которы е ещ е не готовы. В этом р азд ел е вы у знаете, к ак и сп о л ьзо в ать S inon.JS (http ://sin o n js.o rg /) для нап и сан и я тестовы х ш пионов, заглуш ек и м акетов. П реж де чем браться за дело, создайте новы й проект и установите Sinon: mkdir sinon-js-exam ples cd sinon-js-exam ples npm i n i t -y mkdir te s t npm i --save-dev sinon Затем создайте ф айл с тестовы м и данны ми. В наш ем примере будет использовать­ ся простая база данны х «клю ч-значение» в ф орм ате JS O N . Н аш а цель — создать заглуш ку д ля A P I ф айловой системы, чтобы избеж ать создания реальны х ф айлов в ф айловой системе. Это позволит нам протестировать код базы данны х — в отличие от кода ф айловы х операций (л и сти н г 9.17). Листинг 9.17. Класс Database const f s = r e q u i r e ( 'f s ') ; class Database { constructor(filenam e) { th is.filen am e = filename; th is .d a ta = {}; } save(cb) { fs .w rite F ile (th is .file n a m e , JS O N .strin g ify (th is.d ata), cb); } in sert(k ey , value) { th is.d a ta [k e y ] = value; } } module.exports = Database; 9.1. Модульное тестирование 297 С охрани те ф айл под им енем db.js. А теперь попробуем протестировать его с и с­ пользованием ш пионов Sinon. Шпионы И н огд а п ри тести р о в ан и и б ы вает д остаточн о знать, что м етод просто бы л в ы ­ зван. Ш пионы (sp ies) идеально подходят д л я этой цели. A P I п озвол яет зам енить м етод объектом , к ко то р о м у м о ж н о п р и м ен я ть тестовы е у твер ж д ен и я . Ч тобы с м о д е л и р о в а т ь в ы зо в f s . w r i t e F i l e в db.js, в о с п о л ь з у й т е с ь за м е н о й м ето д а в Sinon — ш пионом: sinon .sp y (fs, 'w r ite F ile ') ; П ри заверш ении теста исходны й метод восстан авли вается методом r e s to re : fs .w r ite F ile .r e s to r e ( ); В тесто во й б и б л и о тек е (так о й , как M o ch a) эти вы зовы разм ещ аю тся в блоках beforeE ach и a fte rE a c h . В листинге 9.18 приведен полны й пример использования ш пионов. С охраните ф айл под именем spies.js. Листинг 9.18. Использование шпионов const const const const sinon = re q u ire ('s in o n '); Database = r e q u ir e ( './d b ') ; fs = r e q u i r e ( 'f s ') ; database = new D a tab ase('./sam p le.jso n '); const fsW riteFileSpy = sin o n .sp y (fs, 'w r ite F ile ') ; const saveDone = sinon.spy(); d a ta b a se .in se rt('n a m e ', 'C harles D ickens'); database.save(saveDone); sinon.assert.calledO nce(fsW riteFileSpy); f s .w r ite F ile .r e s to r e ( ); < -------- -<■ •<-------- (1) Заменяет исходный метод fs. (2) Проверяет, что writeFile вызывается только один раз. (3) Восстанавливает исходный метод. П осле назн ачени я ш пиона ( 1) вы полняется тестируем ы й код. Затем кон струкция sin o n .assert проверяет, что нуж ны й м етод бы л вы зван (2 ), после чего восстан ав­ л и вается исходны й м етод (3 ). В этом тесте восстан овлен и е не я в л я е т с я строго обязательны м , но на практике реком ендуется всегда восстанавливать измененны е методы . Заглушки В других ситуациях требуется управлять логикой вы полнения. Н априм ер, можно и н и ц и и р о в ать вы по л н ен и е ош ибочной ветви, чтобы протести ровать обработку ош ибок в коде. П реды дущ ий пример можно переписать с использованием заглуш ки 298 Глава 9. Тестирование приложений Node (s tu b ) вм есто ш пиона, чтобы м етод w r i t e F i l e в ы п ол н и л свой обратны й вызов. О братите внимание: при этом следует избегать вы зова исходного метода, а вместо этого заставить тестируем ы й код вы полнить переданную ф ункцию обратного вы ­ зова. В листинге 9.19 продем онстрировано и сп ользован ие заглуш ек д ля зам ены ф ункций. С охраните ф айл под именем stub.js. Листинг 9.19. Использование заглушек const const const const sinon = re q u ire ('s in o n '); Database = r e q u ir e ( './d b ') ; f s = r e q u i r e ( 'f s ') ; database = new D a tab ase('./sam p le.jso n '); const stub = sin o n .stu b (fs, 'w r ite F ile ', ( f i l e , data, cb) => { cb(); }); const saveDone = sinon.spy(); Заменяет writeFile собственной функцией. d a ta b a se .in se rt('n a m e ', 'C harles D ickens'); database.save(saveDone); sin o n .assert.called O n ce(stu b ); •<sinon.assert.calledO nce(saveD one); fs .w r ite F ile .r e s to r e ( ); Проверяет, что метод writeFile был вызван. Проверяет, что метод обратного вызова database, save был выполнен. С очетание ш пионов и заглуш ек идеально подходит д ля тестирован ия кода Node, в котором интенсивно использую тся ф ункции, заданны е пользователем, обратные вы зовы и обещ ания. О т рассм о тр ен и я инструм ентов, п ред назначенны х д л я м о ­ дульного тестирования, мы перейдем к соверш енно другом у стилю тестирования: функциональному тестированию . 9.2. Функциональное тестирование В б о льш и н стве п р о екто в в еб -р азр аб о тк и ф ункциональные тесты основаны на управл ен и и браузером и проверке разли ч н ы х преобразований D O M д ля списка требований, относящ ихся к конкретном у пользователю . Представьте, что вы стро­ ите систему уп равлени я контентом. Ф ун кц и он альн ы й тест для ф ункц ии отправки граф и ки в библиотеку долж ен переслать изображ ение, проверить, что добавление прош ло успеш но, и убедиться в том, что оно было добавлено в соответствую щ ий список. Выбор средств для реализации функционального тестирования в Node чрезвычайно широк. О днако на высоком уровне их можно разделить на две крупные группы: тер­ минальные (headless) и браузерные тесты. Терминальные тесты обычно использую т некий аналог Phantom JS для ф орм ирования браузерной среды, ориентированной на 9.2. Функциональное тестирование 299 терм инальную среду; при этом более просты е реш ения использую т такие б ибли о­ теки, как Cheerio и JS D O M . Браузерны е тесты использую т средства автоматизации б раузера — такие, как S elenium ( www.seleniumhq.org ), — чтобы вы м огли писать сценарии, управляю щ ие реальны м браузером. О ба подхода использую т одни и те ж е тестовые средства Node, так что вы сможете использовать M ocha, Jasm ine и даже C ucum ber для управлени я взаим одействием Selenium с ваш им прилож ением. П ри ­ мер среды тестирован ия показан на рис. 9.6. Л S. • Mocha Тестовые сценарии Уровень браузера Ваше приложение • Chai * Средства проверки утверждений DOM • Selenium • Firefox • Приложения, выполняемые с NODE_ENV=TecT • Тестовая база данных У Рис. 9.6. Тестирование средствами автоматизации браузера В этом разделе рассм атриваю тся средства ф ункционального тестирован ия в Node, к оторы е п о зв о л я т вам сф о р м и р о в ать тестовую среду на осн ован и и ваш и х сп е­ ц и ф и чески х требований. 9.2.1. Selenium Selenium — популярная библиотека автоматизации браузера на базе Java. И спользо­ вав драйвер д ля конкретного язы ка, вы смож ете подклю читься к серверу Selenium и провести тесты для реального браузера. В этом разделе вы научитесь пользоваться W eb d riv erlO (http://w ebdriver.io/), драйвером Selenium д ля Node. Работать с Selenium сложнее, чем с просты м и тестовы м и библиотекам и N ode, п о ­ тому что вам придется установить Ja v a и загрузить JA R -ф айл Selenium . Загрузите Ja v a д ля своей операционной системы, зайдите на сайт загрузки Selenium (h ttp :// docs.seleniumhq.org/download/) и загрузите JA R -ф айл. П осле этого сервер Selenium мож но будет запустить ком андой следую щ его вида: Java - ja r selen ium -server-standalone-2.53.0.jar Глава 9. Тестирование приложений Node 300 В ваш ем случае точн ая версия Selenium м ож ет быть другой. В озможно, вам такж е придется указать путь к двоичном у ф айлу браузера. Н апример, в W indow s 10 с бра­ узером Firefox, заданны м в свойстве browserName, полны й путь д ля Firefox может быть указан следую щ им образом: java - ja r -D w ebdriver.firefox.driver= "C :\path\to\firefox.exe" seleniumse rv e r-sta n d a lo n e-3 .0 .1 .ja r Точны й путь зависи т от конкретного способа установки Firefox на ваш ей машине. З а дополнительной ин ф орм аци ей о драйвере Firefox обращ айтесь к докум ентации Selenium H Q (https://github.com/SeleniumHQ/selenium/wiki/FirefoxDriver). Драйверы д ля C hrom e и M icrosoft Edge настраиваю тся аналогичны м образом. Теперь создайте новы й проект N ode и установите W ebdriverIO : mkdir -p selenium /test/specs cd selenium npm i n i t -y npm in s ta l l --save-dev webdriverio npm in s ta l l --save express В комплект поставки W ebdriverIO вклю чен удобный генератор конфигурационны х ф айлов. Ч тобы запустить его, вы полните ком анду wdio config: ./node_modules/.bin/wdio config О тветьте на вопросы и подтвердите значения по умолчанию . Н а рис. 9.7 показан прим ер сеанса. WDIO Configuration Helper ? ? ? ? ? ? ? ? ? Where do you want to execute your tests? On my local machine Which framework do you want to use? mocha Shall I install the framework adapter for you? Yes Where are your test specs located? ./test/specs/**/*.js Which reporter do you want to use? Do you want to add a service to your test setup? Level of logging verbosity: verbose In which directory should screenshots gets saved if a command fails? ./errorShots/ What is the base url? http://localhost:4000 Installing wdio packages: pkg: wdio-mocha-framework Packages installed successfully, creating configuration file... Configuration file was created successfully I To run your tests, execute: $ wdio wdio.conf.js Рис. 9.7. Настройка тестов Selenium в wdio 9.2. Функциональное тестирование 301 Д ополни те ф айл package.json ком андой wdio, чтобы тесты м ож но было запускать ком андой npm t e s t : "sc rip ts " : { " te s t" : "wdio w dio.conf.js" }, Теперь добавьте ч то -н и бу д ь в тест. Б азо во го сервера E xpress будет достаточно. П рим ер в листинге 9.20 м ож ет использоваться в последую щ их листингах д ля т е ­ стирования. С охраните код из листинга в ф айле index.js (ф ай л c09-testing/selenium/ index.js в архиве кода книги). Листинг 9.20. Пример проекта Express const express = re q u ire ('e x p re s s '); const app = express(); const port = process.env.PORT | | 4000; a p p .g e t ( '/ ', (req, res) => { re s.se n d (' <html> <head> <title>My to-do l i s t < / t i tl e > </head> <body> <h1>Welcome to my awesome to-do list</h1> </body> </html> ') ; }); a p p .lis te n (p o rt, () => { console.log('Running on p o r t', p o rt); }); К преимущ ествам W ebdriverIO можно отнести простой, динамичны й A PI для нап и­ сания тестов Selenium. Синтаксис логичен и понятен — вы даже можете использовать селекторы CSS при написании тестов. В листинге 9.21 (ф ай л test/specs/todo-test.js в архиве кода) приведен простой тест, которы й создает кли ен та W ebdriverIO , а з а ­ тем проверяет заголовок на странице. Листинг 9.21. Тест WebdriverIO const a s s e rt = r e q u ir e ( 'a s s e r t') ; const webdriverio = re q u ire ('w e b d riv e rio '); d escrib e('to d o t e s t s ', () => { le t c lie n t; before(() => { c lie n t = w ebdriverio.rem ote(); -<-------return c l i e n t .i n i t ( ) ; }); it( 'to d o l i s t t e s t ', () => { (1) Создает клиента Webdriverio. 302 Глава 9. Тестирование приложений Node return c lie n t . u r l ( '/ ' ) < -------- (2) Получает домашнюю страницу. .g e tT itle () < -------- Получает заголовок. .th e n ( title => a s s e r t.e q u a l( title , 'My to-do l i s t ' ) ) ; }); }); Проверяет заголовок. П осле подклю чения W ebdriverIO (1 ) вы сможете использовать экзем пляр клиента д ля получения страниц от п р и лож ения (2 ). П осле этого вы смож ете запросить т е ­ кущ ее состояние докум ента в браузере — в примере используется метод g e t T i t l e д ля получения элем ента t i t l e из заголовка документа. Е сли вы хотите запросить элем енты CSS из документа, используйте метод .ele m en ts (http://w ebdriver.io/api/ protocol/elements.html). Сущ ествую т разные методы для м анипуляций с документом, ф орм ам и и даж е cookie. Этот тест, похож ий на другие тесты M ocha этой главы, заставляет реальны й браузер обратиться к веб-прилож ению N ode. Ч тобы вы полнить тест, запустите сервер на порту 4000: PORT=4000 node in d ex .js Затем введите ком анду npm t e s t . Вы увидите, как откроется Firefox, а в командной строке вы полняю тся тесты. Если вы предпочитаете Chrom e, откройте ф айл wdio. conf.js и изм ените свойство browserName. НЕТРИВИАЛЬНОЕ ТЕСТИРОВАНИЕ С ИСПОЛЬЗОВАНИЕМ SELENIUM Е сли вы используете W ebd riv erIO и Selenium для тестирован ия более слож ­ ного веб-прилож ения, использую щ его R eact и ли A ngular и т. д., вам стоит п о б л и ж е п о зн а к о м и тьс я со всп о м о гател ьн ы м и м етодам и. Н еко то р ы е из м етодов приостанавливаю т тест до того момента, когда станут доступны м и некоторы е элементы ; эта возм ож ность отлично подходит д ля прилож ений R eact, которы е асинхронно строят документ, м ногократно обновляя его до появл ен и я удаленны х данных. З а дополнительной информацией обращайетсь к описанию методов w aitFor* — таких, как w aitForV isible (h ttp ://w e b d riv e r.io /a p i/u tility /w a itF o rV isib le .h tm l). 9.3. Ошибки при прохождении тестов К о гд а в ы р а б о т а е т е н ад с е р ь е з н ы м п р о е к то м , н е и зб е ж н о н а с т у п и т м ом ен т, когда тесты пер естан у т проходить. N ode п р ед о ставл яет р я д и н струм ен тов для п о л у ч ен и я более п одробн ой и н ф о р м ац и и о сбой ны х тестах. В этом р азд ел е вы у зн а е те , к а к р а с ш и р и т ь в ы х о д н у ю и н ф о р м ац и ю , г ен е р и р у е м у ю п р и о тл ад к е сбой ны х тестов. 9.3. Ошибки при прохождении тестов 303 Е сли тест не прош ел, преж де всего следует сгенерировать более подробны й о т ­ л адоч н ы й вы вод. В следую щ ем разд ел е показано, как сделать это при пом ощ и перем енной NODE_DEBUG. 9.3.1. Получение более подробных журналов Е сли тест не прош ел, полезно получить и н ф орм ац и ю о том, что при этом п р о и с­ ходило в программе. В N ode сущ ествует два способа получения такой информации: один предназначен для внутренней ф ункциональности Node, другой — для модулей npm . Д л я о тладки б азовы х м одулей N ode и сп ользуется перем ен н ая NODE_DEBUG. Переменная NODE_DEBUG Представьте, что в прилож ении им еется вы зов ф ункц ии ф айловой системы с боль­ ш им уровнем влож енности, для которого вы забы ли использовать обратны й вызов. Н априм ер, д ля следую щ его прим ера будет вы дано исклю чение: const fs = r e q u i r e ( 'f s ') ; function deeplyNested() { f s .r e a d F i l e ( '/ ') ; } deeplyNested(); В трассировке стека вы водится ограниченная ин ф орм аци я об исклю чении. В част­ ности, в нее не вклю чается полная ин ф орм аци я о точке вызова, в которой прои зо­ ш ло исклю чение: fs .js :6 0 throw e rr; / / Forgot a callback but d o n 't know where? Use NODE_DEBUG=fs Л Error: EISDIR: ille g a l operation on a d irecto ry , read a t Error (native) Без содерж ательны х ком м ентариев м ногие программисты , столкнувш ись с такой ош ибкой, начинаю т винить Node. Но, как видно из ком м ентария, значение NODE_ DEBUG=fs позволяет получить более подробную инф орм ацию о модуле fs. Запустите сценарий следую щ ей командой: NODE_DEBUG=fs node node-debug-example.js Теперь вы получите более подробную трассировку, упрощ аю щ ую процесс отладки: fs .js :5 3 throw backtrace; Л 304 Error: at at at at at at at at at at Глава 9. Тестирование приложений Node EISDIR: ille g a l operation on a d irecto ry , read rethrow (fs.js :4 8 :2 1 ) maybeCallback (fs.js:6 6 :4 2 ) O b ject.fs.read F ile (fs.js:2 2 7 :1 8 ) deeplyNested (node-debug-example.js:4:6) Object.<anonymous> (node-debug-example.js:7:1) Module._compile (module.js:435:26) O bject.M odule._extensions..js (module.js:442:10) Module.load (module.js:356:32) Function.Module._load (module.js:311:12) Function.Module.runMain (module.js:467:10) И з трасси ровки видно, что проблем а кроется в наш ем ф айле — в ф ун кц и и из стро­ ки 4, которая была изначально вы звана из строки 7. Такая инф орм ация существенно упрощ ает отладку любого кода, использующ его базовые модули, причем этот способ вклю чает в себя не только ф айловую систему, но и сетевые библиотеки — такие, как N o d e-м одули кли ен та и сервера HTTP. Переменная DEBUG О бщ едоступная альтернатива д ля NODE_DEBUG — перем енная DEBUG. М ногие пакеты в npm проверяю т переменную окруж ения DEBUG. О на им итирует стиль параметров, используем ы й NODE_DEBUG, поэтому вы можете указать список модулей или просмот­ реть их все в реж им е DEBUG= '* '. Н а рис. 9.8 показан проект из главы 4 в реж име DEBUG='*'. *♦ tldr git:(master) X DEBUG***" npa start > tldrfl.0.0 start /Users/alex/Docuaents/Code/nodeinaction/ch04*what*is-a-node-web-app/tldr > node index.js express:application set "x-powered-by" to true +0es express:application set "etag" to 'weak' +3ns express:application set "etag fn" to [Function: wetag] *2ms express:application set ”env" to 'development* ♦las express:application set “query parser" to 'extended' ♦Oas express:application set "query parser fn" to [Function: parseExtendedQueryString] +0as express:application set "subdoaain offset" to 2 +0as express:application set "trust proxy" to false +0as express:application set "trust proxy fn” to [Function: trustNone] *i«s express:application booting in development node *0as express:application set "view" to [Function: View] ♦Oas express:application set "views" to '/Users/alex/Docuaents/Code/nodeinaction/ch04*what*is-a-node*web-app/tldr/views' +0as express:application set "jsonp callback naae" to 'callback' +0as express:router use / query «429as Рис. 9.8. Запуск приложения Express в режиме DEBUG='*' Е сли вы хотите встроить ф ункциональность NODE_DEBUG в свои собственны е п ро­ екты, используйте встроенны й метод u til.d e b u g lo g : const debuglog = re q u ire ('u til').d e b u g lo g ('e x a m p le '); debuglog('You can only see these messages by s e ttin g NODE_DEBUG=example!'); Ч тобы создать нестандартны е средства отладочного вывода, настроенны е с п ере­ м ен н о й DEBUG, необходи м о и сп о л ьзо в ать пакет deb u g из npm ( www.npmjs.com / 9.3. Ошибки при прохождении тестов 305 package/debug ). В ы можете создать столько вариантов отладочного вывода, скол ь­ ко потребуется. Представьте, что вы строите веб-прилож ение M V C . В ы можете создать отдельны е п о д си стем ы ведения ж ур н ал а д ля моделей, представлений и контроллеров. Затем, когда тесты не пройдут, вы см ож ете указать ж урналы , н еобходи м ы е для отладки ко н кр е тн о й части п рилож ения. Л и с т и н г 9.22 (ф айл ch09-testing/debug-example/index.js в архиве кода) дем он стр и рует использование м од ул я debug. Листинг 9.22. Использование пакета debug const debugViews = require('debug')('debug-exam ple:view s'); const debugModels = require('debug')('debug-exam ple:m odels'); debugViews('Example view message'); debugModels('Example model message'); Ч то б ы запустить этот пример и просмотреть отладочны й ж урнал представлений, присвойте DEBUG значение debug-exam ple:views: DEBUG=debug-example:views node in d ex .js П оследняя интересная возмож ность отладочны х ж урналов — вы можете снабдить раздел отладочной инф орм ации преф иксом -, чтобы и склю чить его из журналов: DEBUG='* -debug-example:views' node in d ex .js И склю чение определенных модулей означает, что вы по-прежнему можете исполь­ зовать реж им *, но убрать из вывода ненуж ны е или сл и ш ком длинны е разделы. 9.3.2. Получение расширенной трассировки стека Е с л и вы используете асинхронны е операции (а и х использует каждый, кто когдалибо писал код с а си н хр о н н ы м и обратн ы м и вы зовам и и ли обещ аниям и), недо­ статочно подробная трассировка стека может создать проблемы. В таки х случаях вам м огут помочь пакеты npm. Н апример, при асинхронном вы полнении обратных вызовов N ode не сохраняет стек вызовов, из которого была поставлена в очередь операция. Ч то б ы убедиться в этом, создайте два файла: один, с именем async.js, определяет асинхронную ф ункцию , а другой, с именем index.js, включает async.js. С ле д ую щ и й фрагмент хранится в файле aync.js (файл ch09-testing/debug-stacktraces/ async.js в архиве кода): module.exports = () => { setTimeout(() => { throw new E rror(); }) }; 306 Глава 9. Тестирование приложений Node А в ф айле index.js достаточно вклю чить async.js: r e q u ir e ( './a s y n c .js ') ( ) ; Е сли теперь запустить index.js ком андой node in d e x .js , вы получите краткую тр ас­ сировку стека, в которой не у казан а точка вы зова ош ибочной ф ун кц и и — только точка, в которой бы ло сгенерировано исклю чение: throw new E rror(); Л Error at null._onTimeout (async.js:3:11) at Timer.listOnTimeout (tim ers.js:9 2 :1 5 ) Чтобы получить отчет с более подробной информацией, установите пакет trace (www. npmjs.com/package/trace) и запустите его ком андой node - r t r a c e in d e x .js . Ф лаг - r приказы вает N ode вклю чить м одуль trace, преж де чем загруж ать что-либо еще. У трассировки стека есть и другая проблема: она мож ет быть чрезм ерно подроб­ ной — наприм ер, если трасси ровка вклю чает в себя слиш ком м ного внутренней инф орм ации Node. Д ля очистки трассировки стека используется пакет clarify (www. npm js.co m /p ack ag e/clarify ). Его такж е м ож но запустить с ф лагом -r: $ node -r c la rify in d ex .js throw new E rror(); Л Error at null._onTimeout (async.js:3:11) П акет clarify особенно полезен, если вы хотите вклю чить трасси ровку стека в сиг­ нальны е сообщ ения об ош ибках веб-прилож ений, пересы лаемы е по электронной почте. Е сли вы вы полняете в N ode код, предназначенны й д ля браузеров (наприм ер, как часть изом орф ны х веб-п рилож ени й), то д ля улучш ени я трассировки стека можно воспользоваться пакетом source-m ap-support (www.npmjs.com/package/source-mapsupport). П акет м ож ет запускаться с ф лагом -r, но он такж е работает с некоторы ми тестовы м и ф рейм воркам и: $ node -r source-m ap-support/register in d ex .js $ mocha --re q u ire source-m ap-support/register index.js К огда вы в следую щ ий раз будете мучиться с трассировкой стека, сгенерированной д ля асинхронного кода, воспользуйтесь таким и инструм ентам и, как trace и clarify. С их помощ ью вы сможете в полной мере использовать всю информацию , которую вам предоставляю т V 8 и Node. 9.4. Заключение 307 9.4. Заключение О Д л я написания м одульны х тестов требуется система исполнения тестов — такая, как M ocha. О N ode содерж ит встроенную библиотеку тестовы х утверж ден ий assert. О Также сущ ествую т другие библиотеки тестовы х утверж дений — например, C hai и Should.js. О Если вы хотите обойти вы полнение некоторого кода (наприм ер, сетевых зап ро­ сов), используйте Sinon.js. О Sinon.js такж е позволяет следить за вы полнением кода и контролировать вы ­ полнение некоторы х ф у н кц и й и методов. О Selenium используется для нап исания браузерны х тестов, основанны х на сце­ нарном управлени и реальны м и браузерами. Развертывание и обеспечение доступности приложений Node Р азраб отка веб-п рилож ени я — одно дело, а запуск его в эксплуатацию — совсем другое. Д л я каж дой веб-технологии сущ ествую т приемы, направленны е на п овы ­ ш ение надежности и производительности, и Node не явл яется исклю чением из этого правила. В этой главе мы расскаж ем, как выбрать подходящ ую среду разработки д ля ваш его п р и лож ения и как обеспечить доступность прилож ения. В следую щ ем разд ел е опи сан ы р азл и ч н ы е типы сред, в которы х м ож ет р а зв е р ­ ты в аться при лож ение. З атем мы зай м ем ся обеспечением вы сокой д оступн ости прилож ений. 10.1. Хостинг Node-приложений В веб-прилож ениях, которы е разрабаты ваю тся в этой книге, используется сервер H T T P на базе N ode. Браузер м ож ет взаим одействовать с прилож ением без вы де­ ленного сервера H T T P (такого, как A pache или N ginx). Впрочем, вы мож ете р а з­ местить сервер (наприм ер, N ginx) перед своим прилож ением , так что прилож ения N ode часто могут разм ещ аться в лю бой среде, где можно было запустить веб-сервер. О б лач н ы е провайдеры , вкл ю чая H ero k u и A m azon, такж е п оддерж иваю т N ode. К ак сл едствие, сущ ествует три н ад еж н ы х и м асш таб и р у ем ы х способа зап у ск а прилож ений: О п латф орм а как сервис — прилож ение запускается на платф орм е Amazon, Azure и ли H eroku; О сервер и ли ви ртуальная м аш ина — прилож ение запускается на сервере U N IX и ли W indow s в облаке, на м аш инах ком пании, предоставляю щ их услуги п р и ­ ватного хостинга, или на внутренних м ощ ностях ваш ей компании; О контейнер — прилож ение и любые другие ассоциированные сервисы запускаются с использованием програм м ного контейнера (наприм ер, D ocker). 10.1. Хостинг Node-приложений 309 В ы брать н у ж н ы й в ар и ан т бы вает тяж ел о — особенно если учесть, что сначала опробовать их не всегда просто. В арианты не при вязан ы к кон кретны м п ровай де­ рам: наприм ер, A m azon и A zure м огут предоставить все стратегии разверты ван ия. В этом разд ел е опи сан ы тр еб о в ан и я к каж дом у варианту, их достои н ства и н е­ д остатки. К счастью , д л я каж дого вар и ан та д оступ н ы б есп латны е и ли эконом р еш ен и я, п оэтом у все вар и ан ты д о сту п н ы как р азр аб о тч и к ам -л ю б и тел я м , так и проф ессионалам . 10.1.1. Платформа как сервис В схеме «платф орм а как сервис» (PaaS, P latfo rm as Service) при лож ение обычно готови тся к разверты ванию : вы долж ны зар еги стри роваться д ля и сп ользован ия сервиса, создать новое прилож ение, а затем добавить удаленны й репози тори й G it в проект. П ри отправке в этот удаленны й репози тори й происходит разверты вание прилож ения. По умолчанию прилож ение вы полняется в одном контейнере (точное определение контейнера зависи т от провайдера), а в случае сбоя сервис пы тается перезапустить прилож ение. Вы получаете ограниченны й доступ к журналам, а так­ ж е веб-интерф ейс и ин терф ейс ком андной строки д ля уп равлени я прилож ением. Чтобы провести масш табирование, следует запустить дополнительны е экзем пляры прилож ения, что требует дополнительны х затрат. В табл. 10.1 представлена сводка характеристик типи чн ы х предлож ений PaaS. Таблица 10.1. Характеристики PaaS Простота использования Высокая Возможности Развертывание посредством отправки в GiT, простое горизонтальное масштабирование Инфраструктура Абстрагированная/черный ящик Пригодность для коммерче­ ского использования Хорошая: приложения обычно изолированы по сети Стоимость* Низкий трафик: $$, популярный сайт: $$$$ Провайдеры Heroku, Azure, AWS Elastic Beanstalk * $: дешево, $$$$$: дорого У провай деров PaaS есть свои п редпочти тельны е базы данны х. Д л я H ero k u это PostgreSQ L , а для Azure — SQ L D atabase. П одробности подклю чения к базе данных определяю тся перем енны м и окруж ения, поэтому вы мож ете подклю чаться без д о ­ бавления учетны х данны х в исходный код проекта. Вариант PaaS отлично подходит д ля разработчиков-лю бителей, потому что он обходится недорого (а иногда даж е бесплатно) д ля небольш их проектов с незначительны м траф иком. 310 Глава 10. Развертывание и обеспечение доступности приложений Node Н екоторые провайдеры проще в использовании, чем другие: платформа H eroku в выс­ ш ей степени проста для программистов, знаком ы х с Git, даже если они не обладают навыками системного администрирования или DevOps. Системы PaaS обычно знают, как следует запускать проекты, созданные такими популярны ми инструментами, как Node, Rails и D jango, так что все происходит почти автоматически. Пример: Node в Heroku за 10 минут В этом разделе мы развернем прилож ение в H eroku. И спользовав настройки H eroku по умолчанию , мы развернем прилож ение в одном облегченном контейнере Linux. Д л я р азверты ван ия базового при лож ения N ode в H eroku потребую тся следую щ ие предварительны е условия: О разверты ваем ое прилож ение; О учетная запись H eroku: https://signup.heroku.com/; О H ero k u CLI: https://devcenter.heroku.com/articles/heroku-cli. К огда все эти элем енты будут готовы, войдите в H eroku в ком андной строке: heroku login H eroku предлагает ввести адрес электронной почты и пароль Heroku. Затем создайте простое при лож ение Express: mkdir heroku-example npm i -g express-generator express npm i В ы п о л н и те ко м ан ду npm s t a r t , введи те адрес http://localhost:3000 и убедитесь в том, что все работает правильно. Н а следую щ ем шаге создайте репози тори й G it и при лож ение Heroku: g it i n i t g it add . g it commit -m 'I n i t i a l commit' heroku create g it push heroku master К оманда вы водит случайно сгенерированны й U R L -адрес вашего прилож ения и ре­ позиторий Git. К аж ды й раз, когда вы хотите развернуть прилож ение, заф иксируйте изм ен ен ия в G it и вы полните ком анду g i t push heroku m aster. Вы мож ете изменить U R L и им я при лож ения ком андой heroku rename. Теперь откройте U R L herokuapp.com с предыдущего шага, чтобы увидеть приложение Express.Чтобы просмотреть ж урналы прилож ения, вы полните команду heroku logs, а для получения доступа к ком андном у интерпретатору в контейнере прилож ения вы полните ком анду heroku run bash. 10.1. Хостинг Node-приложений 311 H eroku предоставляет простой и быстрый механизм запуска прилож ений Node. О б­ ратите внимание: никакая настройка, относящ аяся к Node, не потребуется — H eroku запускает базовы е при лож ения N ode без дополнительны х изм енений кон ф и гура­ ции. Впрочем, в некоторы х случаях требуется более вы сокая степень кон троля над средой вы полнения, так что в следую щем разделе рассм атривается второй вариант: использование серверов для хостинга при лож ений Node. 10.1.2. Серверы И сп о л ьзо ван и е собственного сервера обладает рядом преим ущ еств перед PaaS. Вместо того чтобы беспокоиться, где будет работать база данных, вы мож ете уста­ новить P ostgreS Q L , M yS Q L и ли даж е Redis на том ж е сервере. У становить можно все, что угодно: нестандартное программное обеспечение ведения журнала, серверы H T TP, п рослой ки кэш и р о ван и я — все зависи т от вас. В табл. 10.2 представлена сводка характеристик реш ений с собственны м сервером. Таблица 10.2. Характеристики решений с собственным сервером Простота использования Низкая Возможности Полный контроль над всем стеком, запуск собствен­ ной базы данных и уровня кэширования Инфраструктура Открыта для разработчика (или системного админи­ стратора/DevOps) Пригодность для коммерче­ ского использования Хорошая, если у вас имеется персонал, способный за­ ниматься сопровождением сервера Стоимость Малая виртуальная машина: $, большой сервер: $$$$$ Провайдеры Azure, Amazon, компании, предоставляющие сервис хостинга Сущ ествует несколько вариантов сопровож дения сервера. Вы можете получить не­ дорогую виртуальную м аш ину у такой компании, как Linode или D igital Ocean; это будет полноценны й сервер, которы й вы сможете настроить по своему усмотрению, но он будет совместно использовать ресурсы одного комплекта оборудования с дру­ гим и виртуальны м и м аш инам и. Также м ож но купить собственное оборудование или взять сервер в аренду. Н екоторы е ком пании-хостеры предоставляю т сервис у п р ав л я ем о го хостинга, о к а зы в а я п о д д ер ж ку в со п р о во ж д ен и и оп ерац и он н ой системы сервера. Н еобходим о реш ить, какую операционную систему вы собираетесь использовать. D ebian сущ ествует в неско льки х р азн ови дн остях, N ode такж е хорош о работает с W indow s и Solaris, поэтом у вы бор оказы вается более слож ны м, чем каж ется на первы й взгляд. 312 Глава 10. Развертывание и обеспечение доступности приложений Node Д ругое критическое реш ение — организация доступа к ваш ему прилож ению : т р а ­ ф и к м ож ет перенап равляться с портов 80 и 443 ваш ему прилож ению , но вы такж е м ож ете р азм ести ть N ginx перед п р и л о ж ен и ем д л я п р о к си -о б р аб о тки запросов и (во зм о ж н о ) предоставления статических файлов. П ерем ещ ени е кода из р еп о зи то р и я на сервер такж е м ож ет осущ ествл яться р а з­ ны м и способами. Вы м ож ете вручную скопировать ф ай л ы при помощ и scp, sftp и ли rsync, или ж е воспользоваться C hef д ля уп равлени я нескольким и серверами и управлени я версиям и. Н екоторы е специалисты создаю т G it-перехватчик в стиле H eroku, которы й автом атически обновляет прилож ение на сервере в зависимости от отправки данны х в определенны е ветви Git. В ажно понимать, что управление собственны м сервером — достаточно непростое дело. Н астройка требует значительны х усилий, а на сервере приходится устанавли­ вать новейшие исправления ош ибок О С и обновления безопасности. Разработчикалю бителя это м ож ет отпугнуть — однако в процессе настройки вы узнаете много нового, а возмож но, проявите интерес к области D evOps. Д л я зап у ска п р и ло ж ен и й N ode на ви р ту ал ьн ой м аш и не и ли полном сервере не нуж но ничего особенного. Е сли вы хотите озн аком и ться с некоторы м и п р и ем а­ ми, исп ользуем ы м и д ля запуска п р и лож ений N ode на сервере и обеспечения их работы в течение длительного времени, переходите к разделу 10.2. В противном случае продолж айте читать — в следую щ ем разделе приводится дополнительная и н ф орм аци я о N ode и Docker. 10.1.3. Контейнеры И сп о л ьзо ван и е програм м ны х кон тей неров п редставляет собой некий м еханизм виртуализаци и О С, которы й автоматизирует процесс разверты ван ия прилож ений. С ам ы й известны й из таких проектов — D ocker — расп ростран яется с откры ты м кодом, но такж е содерж ит ком м ерческие ф ункции, упрощ аю щ ие разверты ван ие при лож ений д ля реальной эксплуатации. В табл. 10.3 представлена сводка х ар ак ­ теристи к реш ений с контейнером. Таблица 10.3. Характеристики решений с контейнерами Простота использования Средняя Возможности Полный контроль над всем стеком, запуск собственной базы данных и уровня кэширования, возможность пере­ хода на других провайдеров и локальные машины Инфраструктура Открыта для разработчика (или системного администра­ тора/DevOps) Пригодность для коммер­ ческого использования Превосходная: развертывание на управляемом хосте, хосте Docker или собственном центре обработки данных 10.1. Хостинг Node-приложений 313 Стоимость $$$ Провайдеры Azure, Amazon, Docker Cloud, Google Cloud Platform (сKubernetes), компании, предоставляющие сервис хо­ стинга для управления контейнерами Docker D ocker п о зволяет определять п р и ло ж ен и я в ф орм ате образов (im ages). Если вы построили типичную систему управления контентом (C M S ), которая имеет м икро­ сервис для обработки графики, основной сервис для хранения данны х прилож ений, и базу данны х, ее м ож но развернуть с четы рьм я разны м и образам и Docker: О образ 1 — м икросервис м асш таби рования граф ики, отправляем ой CM S; О образ 2 — PostgreSQ L ; О образ 3 — основное веб-прилож ение C M S с адм инистративны м интерф ейсом; О образ 4 — откры ты й ин терф ейс веб-прилож ения. Так как D ocker распространяется с откры ты м кодом, вы не ограничиваетесь одним провайдером д ля развер ты ван и я при лож ений на базе D ocker. Также д ля р азв ер ­ ты ван и я образов м ож но воспользоваться Am azon E lastic Beanstalk, D ocker C loud и даж е M icrosoft Azure. Am azon такж е предоставляет сервисы ECS (E C 2 C ontainer Service) и AWS C odeC om m it для облачны х репозиториев G it, которы е м огут быть развернуты в E lastic B eanstalk по аналогии с H eroku. Самы й замечательны й ф акт при использовании контейнеров заклю чается в том, что после контейнеризации прилож ения вы мож ете вызвать его новый экзем пляр всего одной командой. Е сли у вас появи л ся новы й компью тер, вы просто обращ аетесь к репозиторию прилож ения, устанавливаете D ocker локально, а затем вы полняете сценарий для запуска прилож ения. Так как прилож ение имеет четко определенную процедуру разверты ван ия, вам и ваш им коллегам будет прощ е понять, как ваше п ри лож ение долж но вы полняться за пределам и локальной среды разработки. Пример: запуск приложений Node с Docker П ример: https://nodejs.org/en/docs/guides/nodejs-docker-webapp/. Чтобы запустить прилож ение Node в контейнере Docker, сначала следует выполнить ряд подготовительны х действий. 1. Установите D ocker: https://docs.docker.com/engine/installation/. 2. С оздайте при лож ение N ode. З а и н ф орм аци ей о том, как быстро создать п р и ­ лож ение Express, обращ айтесь к разделу 10.1.1. 3. Д обавьте в проект новы й ф айл с именем Dockerfile. Ф ай л Dockerfile сообщ ает Docker, как построить образ вашего прилож ения, как уста­ новить прилож ение и запустить его. М ы используем оф ици альн ы й D o ck er-образ 314 Глава 10. Развертывание и обеспечение доступности приложений Node Node (https://hub.docker.com/_ynode/), вклю чив в Dockerfile команду FROMnode:boron и затем вы полнив ком анды npm i n s t a l l и npm s t a r t с и н струкциям и R U N и CM D . С ледую щ ий ф рагм ент содерж ит полны й ф айл Dockerfile д ля просты х прилож ений Node: FROM node:argon RUN mkdir -p /u sr/src /a p p WORKDIR /u sr/src /a p p COPY package.json /u sr/s rc /a p p / RUN npm in s ta ll COPY . /u sr/src /a p p EXPOSE 3000 CMD ["npm", " s ta rt" ] После того как вы создали ф айл Dockerfile, вы полните команду docker b u ild (https:// docs.docker.com/engine/reference/commandline/build/) в терм инале, чтобы создать образ п ри лож ения. Д л я построения достаточно указать только каталог, так что, если вы находитесь в каталоге при лож ения Express, введите ком анду docker b u ild . К ом анда строит образ и отправляет его демону Docker. В ы полните команду docker images, чтобы просмотреть список изображ ений. П олу­ чите идентиф икатор изображ ения, а затем запустите прилож ение командой docker run -p 8080:3000 -d <image ID>. М ы п р и вязал и внутренний порт (3000) к 8080 на локальном хосте, поэтому для обращ ения к прилож ению в браузере вводится адрес http://localhost:8080. 10.2. Основы развертывания П редп олож им , вы со зд ал и веб-п рилож ени е, которы м хотите похвастаться, или ком м ерческое при лож ение, которое нуж но протести ровать перед вводом в эк с ­ плуатацию . Вероятно, вы начнете с простого разверты ван ия прилож ения, а затем проделаете определенную работу, чтобы максимизировать время доступности и про­ изводительность прилож ения. В этом разделе мы проведем простое временное раз­ вертывание из репозитория Git, а также выясним, как с помощью программы Forever сохранять п р и лож ение работоспособны м и работаю щ им . В ременно развернутое при лож ение не сохраняется после перезагрузки сервера, но зато быстро создается. 10.2.1. Развертывание из репозитория Git В этом разделе мы познаком им ся с базовы м разверты ванием из репози тори я Git, чтобы вы поняли, из каких основны х этапов оно состоит. О бы чно разверты вание осущ ествляется за четы ре этапа. 10.2. Основы развертывания 315 1. П одклю чение к серверу по протоколу SSH. 2. У становка на сервере п л атф о р м ы N o d e и при необходи м ости ин струм ентов кон троля версий (наприм ер, G it и ли Subversion). 3. Загр у зка из репозитория контроля версий на сервер ф айлов прилож ения, вклю ­ ч ая N o d e-сценарии, изображ ени я и таблицы стилей CSS. 4. З ап у ск прилож ения. Вот прим ер кода при лож ения, которое запускается после загрузки ф ай л ов п р и ­ лож ен и я с помощ ью програм м ы Git: g it clone https://github.com /M arak/hellonode.git cd hellonode node se rv e r.js К ак и PH P, N ode не м ож ет вы полняться в ф оновом реж име. П оэтому базовое р а з­ верты вание, про которое мы упом янули, требует откры тия S S H -соединения. Как только подклю чение SS H будет закры то, вы полнен ие п р и л о ж ен и я заверш ится. К счастью, поддерж ивать вы полнение при лож ения очень легко, если прибегнуть к пом ощ и одного простого инструмента. АВТОМАТИЗИРОВАННОЕ РАЗВЕРТЫВАНИЕ Существует много способов автоматизации развертывания N ode-приложений. О дин из способов заклю чается в исп о льзо ван и и таки х инструм ентов, как F leet ( h ttp s://g ith u b .c o m /su b sta c k /fle e t), с помощ ью которого можно развер­ ты вать при лож ения на одном и ли нескольких серверах с помощ ью команды g it push. Б олее тради ц и о н н ы й подход заклю чается в п ри м ен ении и н стр у ­ м ента C apistrano, как описано в статье «D eploying node.js applications w ith C apistrano» Э вана Тайлера (E v an Tahler), опубликованной в блоге Bricolage ( h ttp s://b lo g .ev an tah ler.co m /d ep lo y in g -n o d e-is-ap p licatio n s-w ith -cap istran o af675cdaa7c6#.8r9v0kz3l). 10.2.2. Поддержание работы Node-приложения Предположим, вы создали персональный блог с помощью специального прилож ения д ля разработки блогов G host (https://ghost.org/) и собираетесь развернуть его. При этом нуж но гарантировать продолж ение вы полнен ия п ри лож ения даж е в случае разры ва подклю чения SSH. Ч ащ е всего д л я р е ш е н и я задач подобного рода п р и м ен я ет ся созд ан н ы й N o d e сообщ еством инструм ент N o d ejitsu F orever (https://github.com/nodejitsu/forever). О н позволяет сохранить N ode-прилож ение работаю щ им даже после разры ва соеди­ н ения SSH и дополнительно обеспечивает его перезапуск после сбоя. Н а рис. 10.1 концептуально показано, как работает програм ма Forever. 316 О Глава 10. Развертывание и обеспечение доступности приложений Node О Forever запускает ваше серверное приложение и отслеживает потенциальные сбои. В случае сбоя Forever выполняет определенное действие и повторно запускает приложение. t і Сбой приложения Рис. 10.1. Forever обеспечивает выполнение приложения даже в случае сбоя Д л я вы полнен ия глобальной установки Forever используется ком анда sudo. КОМАНДА SUDO Зачастую в процессе глобальной установки модуля npm (с флагом -g) команду npm нужно предварять командой sudo (www.sudo.ws). В результате утилита npm запустится с привилегиями суперпользователя. При первом выполнении команды sudo понадобится ввести пароль. Только тогда команда, указанная после пароля, будет выполнена. Ч тобы установить Forever, вы полните следую щ ую команду: npm in s t a l l -g forever После заверш ения установки воспользуйтесь программой Forever для запуска блога и поддерж ания его в работоспособном состоянии: forever s ta r t se rv e r.js Е сли нуж но остановить работу блога, введите ком анду Forever stop: forever stop s e rv e r.js Д л я получения списка прилож ений, которы ми можно управлять с помощью Forever, воспользуйтесь ком андой l i s t : forever l i s t 10.3. Максимизация времени доступности и производительности приложений 317 Ещ е одна полезная способность Forever заклю чается в том, что этот инструмент способен перезагрузить при лож ение в случае изм ен ен и я какого-либо исходного ф айла. Это избавит вас от необходим ости каж ды й раз перезагруж ать его вручную после добавления какого-либо нового м еханизм а и ли исправления ош ибки. Д л я запуска Forever в этом реж им е воспользуйтесь ф лагом -w: forever -w s ta r t s e rv e r.js Х отя F o rev er я в л я е т с я чр езвы ч ай н о полезны м д ля развер ты ван и я при лож ен и й инструм ентом , иногда приходится обращ аться к ины м реш ениям с более ш и роки ­ м и возм ож ностям и. В следую щ ем разделе мы п озн аком им ся с корпоративны м и инструм ентам и м они тори нга вы полняю щ и хся прилож ений, а такж е узнаем, как повы сить производительность прилож ений. 10.3. Максимизация времени доступности и производительности приложений Когда прилож ение N ode готово к публикации, нуж но убедиться в том, что оно зап у­ скается и останавливается при запуске и остановке сервера, а такж е автоматически п ерезапускается после сбоев сервера. Д овольно просто забы ть о необходимости остановить при лож ение перед перезагрузкой сервера или о необходимости п ере­ запустить прилож ение после перезагрузки сервера. Т акж е не сл ед у ет за б ы в ат ь о н ео б х о д и м ы х ш агах по м а к с и м и за ц и и п р о и зв о ­ дительн ости при лож ения. Н априм ер, если при лож ение вы п ол н яется на сервере с четы рехъядерны м процессором, не стоит загруж ать только одно ядро. Если при исп о льзо ван и и одного я д р а резко возрастет тр аф и к веб-п ри лож ен и я, вы ч и сл и ­ тельны х возм ож ностей одного ядр а мож ет не хватить д ля обслуж ивани я траф ика, в результате ваш е при лож ение не смож ет стабильно отвечать на запросы. Старайтесь не использовать Node с целью хостинга статических ф айлов для крупно­ масш табны х сайтов. П латф орм а N ode «заточена» под интерактивны е прилож ения, такие как веб-п рилож ени я и протоколы T C P /IP , поэтом у не м ож ет предоставлять статические ф айлы так ж е эф ф ективно, как специально предназначенное для этого программное обеспечение. Д л я предоставления статических ф айлов использую тся такие реш ения, как систем а N ginx (http://nginx.org/en/), которая оптим изирована для реш ения подобных задач. Также можно вы грузить все статические ф айлы в сеть доставки контента (C D N ), такую как Am azon S3 (http://aws.amazon.com/s3/), а з а ­ тем ссы латься на эти ф айлы из прилож ения. В этом разд ел е вы найдете несколько р еком енд аци й, касаю щ и хся доступн ости и производительности: 318 Глава 10. Развертывание и обеспечение доступности приложений Node О использование программы U p sta rt для сохранения доступности и работоспособ­ ности п р и лож ения при перезапусках и сбоях; О и сп о льзо ван и е кластерного A P I-и н тер ф ей са в N ode д л я м ногоядерн ы х п р о ­ цессоров; О предоставление статических ф айлов при лож ения N ode с помощ ью Nginx. Н ачнем мы с рассм отрения очень м ощ ного и удобного инструм ента U p start, пред­ назначенного для поддерж ания доступности прилож ений. 10.3.1. Поддержание доступности приложения с Upstart Д опустим , вы в восторге от своего п р и ло ж ен и я и собираетесь вы пустить его на рынок. Вы хотите иметь ж елезную гарантию того, что при перезапуске сервера вы потом не забудете перезапустить прилож ение. К роме того, нуж но принять меры к тому, чтобы в случае сбоя прилож ения оно не только автоматически перезапуска­ лось, но и ин ф орм аци я о сбое бы ла записана, а вы бы ли оповещ ены об этом, чтобы диагностировать причину сбоя. П роект U p sta rt (http://upstart.ubuntu.com) предлагает элегантны й способ уп равле­ ни я запуском и остановкой любого L inux-прилож ения, вклю чая прилож ение Node. П оддерж ку програм м ы U p s ta rt обеспечиваю т, в частности, соврем енны е версии п л атф о р м U b u n tu и C e n tO S . А льтерн ати вное реш ен ие д л я m acO S основано на создании ф айлов launchd (n o d e-lau n ch d в npm позволяет это сделать), а в системе W indow s для этого используется подсистема W indow s Services, поддерж ка которой обеспечивается пакетом node-w indow s в npm. Ч то б ы у с т ан о в и т ь U p s ta r t на п л а т ф о р м е U b u n tu , в о с п о л ь зу й те сь следую щ ей командой: sudo ap t-g et in s ta l l u p start Д л я установки U p s ta rt на платф орм е C en tO S используйте команду sudo yum in s t a ll u p start П осле установки U p sta rt для каж дого из ваш их при лож ений нуж но добавить к о н ­ ф игурацион ны й ф айл програм м ы U p start. У казанны е ф айлы создаю тся в каталоге /etc/init и имею т н азван и я вида имя_приложения.(ХюХ. К онф игурационны е ф айлы не обязательно долж ны пом ечаться как исполняем ы е. С помощ ью следую щ ей ком анды создается пустой кон ф и гурац ионн ы й ф айл п ро­ граммы U p sta rt для учебного прилож ения, рассм атриваем ого в этой главе: sudo touch /e tc /in it/h e llo n o d e .c o n f 10.3. Максимизация времени доступности и производительности приложений 319 Д алее добавьте в кон ф игурационны й ф айл код из листинга 10.1. Этот код запустит прилож ение после запуска сервера и остановит его после остановки сервера. То, что долж но запускаться програм м ой U p sta rt, находится в разделе exec. Листинг 10.1. Типичный конфигурационный файл Upstart Определяет имя автора приложения. Запускает приложение в начале работы системы после того, как станет доступной файловая система исеть. author "Robert DeGrimston" description "hellonode" < -----Задает имя приложения или описание. setuid "nonrootuser" -<— — Запускает приложение от имени пользователя nonrootuser. s ta r t on (lo cal-filesy stem s and net-device-up IFACE=eth0) stop on shutdown < -------- Останавливает приложение при завершении работы системы. respawn < -------- Перезапускает приложение при сбое. console log < -------- Записывает stdin иstderr в/var/log/upstart/yourapp.log. env NODE_ENV=production < -------exec /usr/bin/node /p a th /to /s e r v e r .js -—— Задает значения Задает команду выполнения приложения. переменных окружения, необходимыхдля работы приложения. С этим конф игурационны м ф айлом ваш процесс будет работать после перезапуска сервера и даже после его неожиданного сбоя. Весь вывод, генерируемый прилож ени­ ем, будет нап равляться в ф айл ж урн ала /var/log/upstart/hellonode.log, причем U p sta rt будет управлять своеврем енной архивацией и обновлением журнала. П осле создания конф игурационного ф айла програм мы U p sta rt запустите п р и л о ­ ж ение следую щ ей командой: sudo service hellonode В случае успеш ного запуска появится сообщение: hellonode sta rt/ru n n in g , process 6770 U p sta rt м ож но настраивать в очень ш ироких пределах. О писание всех доступны х парам етров м ож но най ти в электронной докум ентации (http://upstart.ubuntu.com/ cookbook/). Upstart и Respawn Если указан параметр respawn, U p sta rt в случае сбоя по умолчанию будет постоянно пы таться перезагрузить прилож ение, пока количество попы ток п ерезагрузки не достигнет 10 в течение 5 секунд. Ч тобы изм енить это ограничение, воспользуйтесь парам етром respawn l i m i t КОЛИЧЕСТВО ИНТЕРВАЛ. Здесь КОЛИЧЕСТВО — это 320 Глава 10. Развертывание и обеспечение доступности приложений Node количество попы ток внутри заданного (в секундах) ИНТЕРВАЛА времени. Н ап р и ­ мер, с помощ ью следую щ их ком анд м ож но задать 20 попы ток в течение 5 секунд: respawn respawn lim it 20 5 Е с л и п р и л о ж е н и е п ы т а е тс я п е р е за г р у зи т ь с я 10 раз на п р о т я ж е н и и 5 секун д (р еж и м по у м о л чан и ю ), это м ож ет о зн ач ать н ал и ч и е ош ибок в коде и л и к о н ф и ­ гу р ац и и , сп о со б н ы х п р и в ести к п о л н о й н ев о зм о ж н о с ти за п у с к а п р и л о ж ен и я . Ч т о б ы с о х р а н и т ь р есу р сы , к о то р ы е м о гу т п о т р е б о в а т ь с я д р у ги м п роц ессам , U p s ta r t п р ек р ащ ает п о п ы тк и п е р е за п у с к а п р и л о ж е н и я п осл е д о ст и ж е н и я з а ­ д анного о граничения. П оэтом у при проверке работоспособности п р и ло ж ен и я реком ендуется не п о л а ­ гаться на U p sta rt, а н ай ти возм ож н ость о п еративно уведом л ять разработч и ков о п р о б л е м ах по эл е к т р о н н о й п о ч те и л и с пом ощ ью д р у ги х средств бы строго обм ена ин ф орм аци ей. Такая проверка м ож ет сводиться просто к посещ ению веб-сайта и ож иданию п р а­ вильного ответа. П ри этом м ож но использовать собственны е методы и ли специ­ альны е инструменты, такие как M onit (http://mmonit.com/monit/) или Zabbix (www. zabbix.com/). Теперь, когда мы узнали, как сделать так, чтобы прилож ение продолж ало работать независим о от сбоев и перезагрузок сервера, логично направить уси ли я на следую ­ щ ий аспект — быстродействие. И в этом нам пом ож ет кластерны й A PI платф орм ы Node. 10.3.2. Кластерный API Н есм отря на то что у больш инства соврем енны х процессоров несколько ядер, п ро­ цесс N ode использует только одно из них. Если вы разм ещ аете N o d e-прилож ение на сервере и хотите м аксим и зировать загрузку сервера, мож но вручную запустить несколько экзем пляров при лож ения на разны х T C P /I P -портах, а затем с помощью системы балан си ровки загрузки распределить веб-траф ик по этим экзем плярам . Тем не менее такое реш ение потребует значительны х усилий. Ч тобы упростить использование нескольких ядер одним прилож ением, в N ode был вклю чен кластерны й A PI. С его помощ ью прилож ение мож ет легко запустить н е­ сколько рабочих процессов, которы е одноврем енно будут вы полнять одни и те же дей стви я на разны х ядрах и использовать один и тот ж е T C P / I P -порт. Н а рис. 10.2 показано, как на сервере с ч еты рехъядерн ы м процессором орган и зуется работа п ри лож ен и я с прим енением кластерного A PI. К од из листинга 10.2 автом атически порож дает главны й процесс и дополнительно рабочие процессы (р або тн и ки ) д ля каж дого ядра процессора. 10.3. Максимизация времени доступности и производительности приложений 321 Рис. 10.2. Для выполнения на четырехъядерном процессоре главный процесс порождает трех работников Листинг 10.2 Демонстрация кластерного API платформы Node const c lu ste r = r e q u ir e ( 'c lu s te r ') ; const h ttp = r e q u ir e ( 'h ttp ') ; const numCPUs = re q u ire ('o s ').c p u s ().le n g th ; < -------- Определяет количество ядер процессора. i f (clu ster.isM aster) { for ( le t i = 0; i < numCPUs; i++) { -<-------- Создает ответвление для каждого ядра. c lu s te r.fo rk (); } c lu s te r .o n ( 'e x it', (worker, code, signal) => { console.log('W orker %s d ie d .', w orker.process.pid); }); } else { h ttp .S e rv e r((re q , res) => { -<----- Определяет работу, которая должна выполняться каждым работником. res.writeHead(200); re s .e n d ( 'I am a worker running in process: ' + process.pid); } ).liste n (8 0 0 0 ); } П о с к о л ь к у г л а в н ы й п р о ц есс и р а б о т н и к и в ы п о л н я ю т с я в р а зн ы х си стем н ы х п роцессах (это необходим о д ля того, чтобы они вы п ол н ял и сь на р азн ы х я драх), они не м огут о б м ен и ваться дан н ы м и со сто я н и я через гл обальны е перем енны е. Тем не м енее к л астер н ы й A P I п р ед о став л я ет м астеру и р аб о тн и кам м еханизм п ередачи данны х. В листинге 10.3 приведен прим ер обмена сообщ ениям и м еж ду главны м п роц ес­ сом и работникам и. С четчик всех запросов храни тся у мастера, и когда какой-то работник сообщ ает о заверш ении обработки запроса, эта и н ф орм аци я передается каж дом у работнику. Листинг 10.3. Демонстрация кластерного API платформы Node const c lu ste r = r e q u ir e ( 'c lu s te r ') ; const h ttp = r e q u ir e ( 'h ttp ') ; const numCPUs = re q u ire ('o s ').c p u s ().le n g th ; const workers = {}; le t requests = 0; 322 Глава 10. Развертывание и обеспечение доступности приложений Node i f (c lu ster.isM aster) { fo r ( le t i = 0; i < numCPUs; i++) { w orkers[i] = c lu s te r.fo rk (); ( ( i) => { w o rkers[i].on('m essage', (message) => { < -------- Прослушивает сообщения от работника. i f (message.cmd == 'increm entRequestTotal') { requests++; < -------- Увеличивает счетчик запросов. for (var j = 0; j < numCPUs; j++) { w orkers[j].send({ < -------- Передает новый счетчик запросов каждому работнику. cmd: 'updateOfRequestTotal', requests: requests }); } } }); } ) ( i) ; ■<-------Использует замыкание для сохранения значения счетчика. } c l u s te r .o n ( 'e x it', (worker, code, signal) => { console.log('W orker %s d ie d .', w orker.process.pid); }); } else { process.on('m essage', (message) => { < -------- Прослушивает сообщения от главного процесса. i f (message.cmd === 'updateOfRequestTotal') { requests = m essage.requests; -<-------- Обновляет счетчик запросов по данным сообщения. } }); h ttp .S e rv e r((re q , res) => { Сообщает главному процессу res.writeHead(200); о необходимости увеличения res.end('W orker ${process.pid}: ${requests} r e q u e s ts .'); счетчика запросов. process.send({ cmd: 'increm entRequestTotal' }); } ).liste n (8 0 0 0 ); } С пом ощ ью к л астер н о го A P I -и н те р ф е й с а п л а т ф о р м ы N ode очень просто с о з­ давать п р и л о ж ен и я , способны е и сп о л ьзо в ать в о зм о ж н о сти соврем енного о б о ­ рудования. 10.3.3. Хостинг статических файлов и представительство П латф орм а N ode в первую очередь предназначена д ля предоставления д и нам иче­ ского веб-контента, поэтом у она не столь эф ф ективна, когда речь идет о статиче­ ском контенте, таком как изображ ения, таблицы C S S -стилей и ли кли ен тский код Ja v a S c rip t. П р едо ставл ен и е стати ч ески х ф ай л ов по п ротокол у H T T P — весьм а специф ическая задача, для реш ения которой были оптимизированы специфические програм м ны е проекты , которы е разрабаты вались много лет. К счастью, веб-сервер с откры ты м исходны м кодом, N ginx (http://nginx.org/en/), оптим изированны й для предоставления статических файлов, м ож но легко устано­ вить вместе с Node именно с целью предоставления статических файлов. В типичной 10.3. Максимизация времени доступности и производительности приложений 323 кон ф и гурац ии N g in x /N o d e сервер N ginx сначала обрабатывает каж ды й веб-запрос и возвращ ает N ode те запросы , которы е не относятся к статическим ф айлам. С оот­ ветствую щ ая ко н ф и гурац ия представлена на рис. 10.3. Рис. 10.3. Nginx-сервер можно использовать как посредника, который быстро возвращает статические ресурсы веб-клиентам Реализует эту конф игурацию код из листинга 10.4, которы й пом ещ ается в раздел h ttp конф игурационного ф айла сервера Nginx. К онф игураци онны й ф айл обычно храни тся в каталоге /etc L in u x -сервера и назы вается /etc/nginx/nginx.conf. Листинг 10.4. Конфигурационный файл для использования сервера Nginx в качестве посредника для Node.js и предоставления статических файлов http { upstream my_node_app { server 127.0.0.1:8000; ■<-------- IP-адрес и порт приложения Node. } server { lis te n 80; < -------Порт, на котором посредник принимает запросы. server_name localhost domain.com; access_log /var/log/nginx/my_node_app.log; location ~ / s t a t i c / { < ----- Обрабатывает запросы файлов для URL-путей, начинающихся с /static/. root /home/node/my_node_app; i f ( ! - f $request_filename) { return 404; } } location / { -<-------- Определяет URL-путь, по которому будет отвечать посредник. proxy_pass http://my_node_app; proxy_redirect o ff; Глава 10. Развертывание и обеспечение доступности приложений Node 324 proxy_set_header proxy_set_header proxy_set_header proxy_set_header X-Real-IP $remote_addr; X-Forwarded-For $proxy_add_x_forwarded_for; Host $http_host; X-NginX-Proxy tru e ; } } } И спользовани е N ginx д ля предоставления статических веб-ресурсов гарантирует, что N ode будет делать только то, что лучш е всего умеет. 10.4. Заключение О Д л я хостинга п р и лож ений N ode м огут исп ользоваться провайдеры PaaS, в ы ­ деленны е серверы, виртуальны е приватны е серверы и облачны й хостинг. О Д л я быстрого разверты вания прилож ений Node на серверах Linux использую тся програм м ы Forever и U p start. О Д л я повы ш ения бы стродействи я п р и лож ений м одуль N ode clu ster позволяет порож дать дополнительны е процессы. III За пределами веб-разработки Миллионы людей пользуются приложениями, построенными на базе Node. Если вы когда-либо использовали Slack или Visual Studio Node, значит, вы уже работали с приложениями Node. В этой части рассматривается Electron и модули для написания программ командной строки на базе Node. Если вы когда-либо хотели создать приложение, которое работало бы в Linux, macOS или Windows, — теперь у вас есть такая возможность. Написание приложений командной строки П рограммы командной строки Node используются во множестве областей, от средств автом атизац ии проектов (таких, как G ulp или Yeoman) до парсеров X M L и JSO N . Если вас когда-либо интересовало, как строить программы командной строки на базе Node, вы найдете в этой главе все необходимое для начала работы. Вы узнаете, как програм м ы N ode получаю т аргум енты ком андной строки и как реализуется в в о д / вы вод с исп ользован ием каналов. М ы такж е п ри вели полезны е советы, которые помогут вам пользоваться реж им ом ком андной строки более эф ф ективно. Х отя написать программу ком андной строки с N ode несложно, очень важно соблю ­ дать некоторы е соглаш ения, при няты е в сообщ естве. В этой главе описаны многие из этих соглаш ений, поэтом у другие пользователи смогут работать с ваш им и п ро­ грам м ам и без изли ш н ей докум ентации. 11.1. Соглашения и философия В ажной частью разработки программ командной строки явл яется понимание согла­ шений, встречаю щ ихся в проверенны х временем программах. В качестве реального прим ера возьм ем Babel: Usage: babel [options] < file s ...> Options: -h, -f, stdin [ ... -q, -V, - - help - - filename [filename] output usage information filename to use when reading from ] - - quiet --version Don't log anything output the version number З д есь стоит обратить вн и м ан и е на несколько ф актов. П ервое — и сп ользован и е клю чей -h и - - h e lp д ля вы вода справочной инф орм ации: ф лаг — используется во 11.1. Соглашения и философия 327 многих программах. Второй флаг, - f , предназначен для имени файла; эта мнемоника легко запоминается. М ногие флаги основаны на мнемонических сокращениях. Также часто встречается ф лаг -q д ля скры того (q u ie t) вывода, как и ф лаг -v д ля вывода версии программы. Все эти ф лаги долж ны поддерж иваться ваш ими прилож ениями. Впрочем, пользовательский ин терф ейс — не просто соглаш ение. Д еф и с и двойной д еф ис ( - - ) входят в специф икацию ин терф ейса O pen G roup U tility C on v en tio n s1. В докум енте даж е указано, как они долж ны использоваться: О реком ендация 4 — все клю чи долж ны начинаться с преф икса - ; О реком ендация 10 — первы й аргумент - -, не являю щ и й ся преф иксом ключа, дол­ ж ен интерпретироваться как при знак конца списка клю чей. Все последую щ ие аргум енты долж ны рассм атриваться как операнды, даж е если они начинаю тся с сим вола -. Д руго й асп ект п р о е к ти р о в а н и я п р и л о ж ен и й ком ан дн ой строки — ф и л о со ф и я. О на уходит корн ям и к создателям U N IX , которы е хотели создавать «компактные, мощ ны е инструм енты » д ля исп ользован ия в простом текстовом интерф ейсе. Ф и л о со ф и я U N IX такова: писать программы, которы е делаю т что-то одно — и делаю т это хорошо. П исать программы, которы е работаю т в сочетании друг с другом. П исать программы д ля работы с текстовы м и потоками, потому что это уни версальн ы й интерф ейс. Д уг Макилрой2 В этой главе представлен ш ирокий обзор средств командной строки и общепринятых соглаш ений UN IX , чтобы вы м огли разрабаты вать инструменты командной строки, которы м и м огут п о л ьзо ваться другие лю ди. Т акж е будут при ведены некоторы е реком ендации, относящ иеся к системе W indow s, но в основном ваш и программы N ode по ум олчанию будут кросс-платф орм енны м и. ПОЛЕЗНАЯ ИНФОРМАЦИЯ О КОМАНДНОЙ СТРОКЕ: ВЫВОД СПРАВКИ Е сли у вас возни кн ут проблем ы с исп ользован ием ком андной строки, п о ­ пробуйте ввести ком анду m an < ком анд а> . Э та ком анда загруж ает страницу с докум ентацией команды. Если вы не пом ните название команды, введите команду apropos < усл ови е> для проведения поиска по базе данны х систем ны х команд. 1 «The Open Group Base Specifications Issue 7», http://pubs.opengroup.org/onlinepubs/ 9699919799/basedefs/V1_chap11.html. 2 «Basics of the Unix Philosophy», www.catb.org/~esr/writings/taoup/html/ch01s06.html. Глава 11. Написание приложений командной строки 328 11.2. Знакомство с parse-json Д л я програм м истов J a v a S c rip t одно из самых полезны х при лож ений читает р а з­ метку JS O N и сообщает, яв л яется ли эта разм етка действительной. В этой главе мы воссоздадим эту программу. С начала определимся, как долж на выглядеть командная строка такого прилож ения. В следую щ ем ф рагм енте приведен прим ер запуска такой программы: node p a rse -jso n .js -f my.json П реж де всего необходимо понять, как извлечь аргументы программ ы - f m y.json из ком андной строки. В ходны е данны е долж ны чи таться из потока s td in . Н иж е рассказано, как реш аю тся эти задачи. 11.3. Аргументы командной строки М ноги е — х отя и не все — програм м ы ком ан дной строки получаю т аргументы. В N ode сущ ествует встроенны й м еханизм для обработки аргументов, но сторонние м одули npm предоставляю т дополнительны е возможности. Э ти возм ож ности п ри­ годятся д ля р еализаци и некоторы х распространенны х соглаш ений. 11.3.1. Разбор аргументов командной строки Д л я работы с аргументами ком андной строки можно использовать массив p ro ce ss. argv. Элементы массива содержат строки, передаваемые командному интерпретато­ ру при вы полнении команды . Таким образом, разбив ком анду на части, вы можете определить, какая подстрока будет храниться в каж дом элементе массива. Элемент p ro c e s s .a r g v [ 0 ] содерж ит подстроку node, элем ент p ro c e s s .a rg v [ 1 ] — подстроку p a r s e - j s o n .j s , элем ент [2] — подстроку -f, и т. д. Ч и та тел я м , р або тавш и м с п р и л о ж е н и я м и ком ан д н ой строки, уж е встреч али сь аргум енты с п реф и к сам и - и ли - - . Э ти преф и ксы определяю т специ альны е со ­ глаш ения д ля передачи значений прилож ениям : - - обозначает полное им я ключа, а преф икс - - односим вольное сокращ ение. Х орош им прим ером служ ит и сп о лн я­ ем ы й ф айл ком андной строки npm с клю чам и -h и - -h elp. СОГЛАШЕНИЯ АРГУМЕНТОВ Такж е на практике ш ироко при м ен яю тся следую щ ие соглаш ения по поводу аргументов: • - - v e r s io n — вы вод версии прилож ения; • -у или - -y es — д ля пропущ енны х клю чей использую тся зн ачения по умолчанию . 11.3. Аргументы командной строки 329 А льтерн ати вны е им ена аргум ентов (таки е, как -h и - -h e lp ) затрудн яю т разбор строки при поддерж ке нескольких клю чей. К счастью, д ля разбора аргументов су ­ щ ествует специальны й модуль, которы й назы вается yargs. С ледую щ ий ф рагмент показы вает, как yargs работает в простейш ем случае. О т вас потребуется совсем немного: вклю чите y args вызовом re q u ire и обратитесь к свойству argv для анализа аргументов, переданны х сценарию: const argv = re q u ire ('y a rg s ').a rg v ; console.log({ f : arg v .f }); Н а рис. 11.1 показано, чем встроенная версия аргументов ком андной строки N ode отличается от объекта, генерируем ого модулем yargs. Массив Node process.argv Объект yargs argv Рис. 11.1. Массив Node argv и объект yargs argv Х о тя о бъект с и н ф о р м а ц и е й пар ам етр о в полезен, он не п р ед о ставл я ет особой структуры для проверки аргументов и генерирования сообщ ений об использовании. В следую щ ем разделе показано, как вы вести опи сан ия аргументов и проверить их значения. 11.3.2. Проверка аргументов М одуль yargs вклю чает в себя методы д ля проверки аргументов. Л и сти н г 11.1 д е­ м онстрирует использование yargs для разбора аргум ента -f, которы й понадобится ваш ему парсеру JS O N , а также использую тся методы d e sc rib e и nargs для контроля за ож идаем ы м ф орм атом аргументов. Листинг 11.1. Использование yargs для разбора аргументов командной строки const readFile = r e q u ir e ( 'f s ') .r e a d F ile ; const yargs = re q u ire ('y a rg s '); const argv = yargs .dem and('f') < -------- Требует передачи ключа -f для выполнения. 330 Глава 11. Написание приложений командной строки . n a r g s ( 'f ', 1) -<-------- Сообщает yargs, что за -f должен следовать один аргумент. .d e s c r ib e ( 'f ', 'JSON f i l e to p arse') .argv; const f i l e = arg v .f; re a d F ile (file , ( e rr, dataBuffer) => { const value = JSO N .parse(dataB uffer.toString()); console.log(JSO N .stringify(value)); }); И спользовать yargs удобнее, чем работать с массивом p ro c e ss.a rg v , — преж де всего из-за возм ож ности установления правил. В листинге 11.1 метод demand требует обя­ зательной передачи аргумента, после чего метод nargs объявляет, что единственным аргум ентом будет ф айл JS O N . Ч тобы программ ой было прощ е пользоваться, yargs такж е м ож но передать текст с описанием. По соглаш ению текст с описанием вы ­ водится при вы зове програм м ы с клю чом -h и ли - - h e lp . Вы мож ете добавить эти клю чи с помощ ью yargs, как в следую щ ем фрагменте: yargs // ... .u sag e('p arse-jso n [o p tio n s]') .h e lp ( 'h ') . a l i a s ( 'h ', 'h e lp ') // ... Теперь парсер JS O N м ож ет получить аргум ент с именем ф айла и обработать файл. Впрочем, работа с ф айлам и в этом проекте еще не заверш ена, потому что программа такж е долж на уметь получать данны е из s td in . В следую щ ем разделе вы узнаете, как реализуется это стандартное соглаш ение UNIX. ПОЛЕЗНАЯ ИНФОРМАЦИЯ О КОМАНДНОЙ СТРОКЕ: ИСТОРИЯ Командны й интерпретатор хранит ж урнал команд, которые вводились ранее. В ведите ком ан ду history, чтобы просм отреть историю команд; часто этот клю ч сокращ ается до h. 11.3.3. Передача stdin в виде файла Если в качестве аргумента ф айла передается деф ис ( - f -), это означает, что данные долж ны быть получены из s td in . Это еще одно распространенное соглаш ение к о ­ мандной строки. И спользуйте пакет mississippi, чтобы легко реализовать эту возмож ­ ность. П ри этом перед вы зовом JSON.parse необходимо вы полнить конкатенацию всех данных, передаваем ы х прилож ению , потому что метод ож идает получить для разбора полную строку JS O N . С м одулем m ississippi прим ер вы глядит так, как п о ­ казано в листинге 11.2. 11.4. Использование программ командной строки с npm 331 Листинг 11.2. Чтение файла из stdin # !/u sr/b in /en v node const concat = re q u ire ('m is s is s ip p i').c o n c a t; const readFile = r e q u ir e ( 'f s ') .r e a d F ile ; const yargs = re q u ire ('y a rg s '); const argv = yargs .u sag e('p arse-jso n [o p tio n s]') .h e lp ( 'h ') . a l i a s ( 'h ', 'h e lp ') .dem and('f') / / Ключ -f необходим для выполнения .n a r g s ( 'f ', 1) / / Сообщает yargs, что после -f следует 1 аргумент .d e s c r ib e ( 'f ', 'JSON f i l e to p arse') .argv; const f i l e = arg v .f; function p a rse (str) { const value = JSON.parse(str); console.log(JSO N .stringify(value)); } i f ( f i le === ' - ' ) { pro cess.std in .p ip e(co n cat(p arse)); } else { re a d F ile (file , (e rr, dataBuffer) => { i f (e rr) { throw e rr; } else { p arse(d ataB u ffer.to S trin g ()); } }); } Э тот код загруж ает м одуль m ississippi и назначает псевдоним co n c at. П осле эт о ­ го c o n c a t и сп о л ьзу ется с потоком s t d in . Так как m ississippi получает ф ункц ию с полны м набором данны х, м ы м ож ем и сп о льзовать исходную ф ун кц и ю p a rs e из л и сти н га 11.1. Э то прои сходит только в том случае, если вм есто им ени ф айла у казан сим вол -. 11.4. Использование программ командной строки с npm Любое приложение, которое вы хотите сделать доступным для других пользователей, легко устанавливается через npm. Ч тобы передать npm информ ацию о прилож ении ком андной строки, прощ е всего воспользоваться полем bin в ф айле package.json. Это поле заставляет npm установить и сп олн яем ы й ф айл, доступны й д ля лю бых сценариев в текущ ем проекте. П оле bin такж е приказы вает npm установить исп ол­ няем ы й ф айл глобально при использовании команды npm i n s t a l l - -g lo b a l. Д анная возм ож ность полезна не только д ля разработчиков N ode, но и для всех остальных, кто захочет использовать ваш и сценарии. 332 Глава 11. Написание приложений командной строки Э тот ф рагм ент и строка #! /u s r / b i n / e n v node в листинге 11.2 — все, что необходимо д ля прим ера с парсером J S O N этой главы: "name": "parse-json", "bin": { "parse-json": "index.js" }, П ри установке пакета командой npm i n s t a l l -g lo b a l команда p a rse -jso n становится доступной на общесистемном уровне. Чтобы опробовать ее, откройте окно терминала (и л и окно ком андной строки в W indow s) и введите ком анду p a rs e -js o n . О братите внимание: этот способ работает даж е в W indow s, потому что npm автоматически устанавливает обертку, обеспечиваю щ ую ее прозрачную работу в W indow s. 11.5. Связывание сценариев с каналами П рограм м а parse-json проста — она получает текст и проверяет его на правильность формата. А если у вас имею тся другие инструменты командной строки, с которыми долж на использоваться эта програм м а? Представьте, что у вас имеется программа, ко то р ая м ож ет вклю чать в себя цветовое вы деление си н такси са в ф ай л ы JS O N . Б ы л о бы зам ечательно, если бы разм етку JS O N м ож но было сначала разобрать, а уж е потом прим енять цветовое вы деление. В этом разделе вы узнаете о каналах, которы е м огут делать это — и многое другое. П рограм м а parse-json и другие програм м ы будут и сп ользоваться д ля о р ган и за­ ции необы чной последовательности операций с применением каналов. Командные интерпретаторы W indow s и U N IX различаю тся, но основные моменты (к счастью) остаю тся неизм енны м и. Н екоторы е р азл и ч и я п роявл яю тся при отладке, но они не долж ны повлиять на ваш у работу при написании прилож ений командной строки. 11.5.1. Передача данных parse-json Д л я связы вания при лож ений командной строки чащ е всего прим еняется механизм каналов (pipes). К аналы берут поток s td o u t п ри лож ения и присоединяю т его к п о ­ току s td in другого процесса. К аналы яв л яю тся центральны м элементом м еж п ро­ цессны х ком м ун икац ий: ор ган и зац и и взаи м од ей стви я м еж ду програм м ам и. Вы мож ете обратиться к содерж им ом у s t d i n в N ode, исп ользуя обозначение p ro c e ss. s td in , потому что этот поток доступен для чтения. Следую щ ий ф рагмент разбирает данны е JS O N , поступаю щ ие из s td in : echo "[1 ,2 ,3 ]" | parse-json -f О братите вни м ан ие на сим вол | . О н сообщ ает ком андном у интерпретатору, что команда echo '{ } ' долж на направить свой вывод в поток s td in программы parse-json. 11.5. Связывание сценариев с каналами 333 ПОЛЕЗНАЯ ИНФОРМАЦИЯ О КОМАНДНОЙ СТРОКЕ: КОМБИНАЦИИ КЛАВИШ Д л я вы полнен ия поиска в истории ком анд м ож но связать команду history с grep: histo ry | grep node Еще удобнее для обращ ения к предыдущ им командам использовать клавиш и со стрелкам и | и | на клавиатуре. П ользователи пользую тся этим и клавиш а­ ми постоянно — но есть еще лучш ий способ! К ом бинация клавиш C trl+ R , вы полняю щ ая рекурси вны й поиск по истории команд, позволяет находить д линны е ком анды по частичном у совпадению текста. Ещ е несколько полезны х ком бинаций: C trl+ S вы полняет поиск в прям ом направлении, а C trl+ G отм еняет поиск. Такж е предусм отрены ком бинации д ля эф ф ективного редакти рования текста: C trl+ W удаляет слова, A L T + F /B перем ещ ается вперед и ли назад на одно слово, а C trl+ A /E перем ещ ается в начало и ли в конец строки. 11.5.2. Ошибки и коды завершения В текущ ем вар и ан те п рограм м а ничего не вы водит. Н о если ей бы ли переданы некорректны е данные, как узнать, что она успеш но заверш илась, если вы даж е не знаете, что она долж на вы водить? П равильны й ответ — код заверш ения. Вы м о ­ ж ете проверить код заверш ен ия последней вы полненной команды, но учтите, что ком анды echo и node рассм атриваю тся как единое целое из-за каналов. В системе W indow s код заверш ен ия проверяется следую щ им образом: echo %errorlevel% В U N IX д ля проверки используется следую щ ая команда: echo $? Е сли ком анда вы полнена успеш но, код заверш ен ия равен 0. Таким образом, если сценарию бы ли переданы некорректны е данные JS O N , он заверш ается с ненулевым кодом: parse-json -f in v a lid .jso n П ри в ы п о л н е н и и этой ком ан ды п р и л о ж е н и е зав е р ш ае т ся с н ен у л евы м кодом и вы водит сообщ ение с опи сан ием причины . Е сли ош ибка бы ла ин ициирована, но осталась необработанной , N o d e авто м ати ч ески заверш ает работу и вы водит сообщ ение об ош ибке. 334 Глава 11. Написание приложений командной строки Потоки ошибок В ывод на консоль м ож ет быть полезен, но еще лучш е сохранить его в ф айле для последую щ его ч тен и я и д ля отладки. К счастью, в командном интерпретаторе для этого достаточно перенаправить поток s td o u t: echo 'you can overw rite f i l e s ! ' > ou t.lo g echo 'you can even append to f i l e s ! ' >> ou t.log К азалось бы, при попытке вы полнить эти команды с некорректной разм еткой JS O N вы вод parse-json с сообщ ением об ош ибке стоит сохранить в файле: parse-json -f in v a lid .jso n >out.log О днако никаких ош ибок в файле нет. И это поведение становится вполне понятным, если понять р азл и ч и я м еж ду s t d e r r и std o u t: О содерж им ое потока s td o u t исп ользуется другим и п ри лож ен и ям и ком андной строки; О содерж им ое s t d e r r используется разработчиками. N ode вы водит данны е в s t d e r r при вы зове c o n s o le .e r r o r или генерировании ош иб­ ки. В этом отнош ении N ode отличается от команды echo, которая вы водит данные в s td o u t (как и c o n s o le .lo g ). Вероятно, с этой инф орм ацией вы захотите направить s t d e r r в ф айл (вм есто s td o u t) . К счастью, это делается очень просто. С потокам и s td in , s td o u t и s t d e r r связы ваю тся ч и сл а от 0 до 2; s t d e r r соответ­ ствует ч исло 2. П еренап р авл ен и е осущ ествл яется кон струк ц и ей 2 > o u t.lo g ; она сообщ ает ком андном у ин терп ретатору номер перенаправляем ого потока и ф айл д ля сохранения вывода: parse-json -f in v a lid .jso n 2> o u t.lo g П еренаправление вы вода — то, что делаю т каналы , но только с процессами вместо ф айлов. В озьмем следую щ ий фрагмент: node -e "con so le.lo g (n u ll)" | parse-json М ы вы водим n u ll и передаем parse-json. Зн ач ен и е n u ll не будет выведено на к о н ­ соль, потому что оно передается только следую щ ей команде. П редполож им, нечто подобное делается с c o n s o le .e r r o r : node -e "c o n so le .e rro r(n u ll)" | parse-json Вы увидите ошибку, потом у что н и како й текст не передается parse-json д л я п о ­ требления. Зн ач ен и е n u ll, направленное в s t d e r r , п оявится на консоли. Д анны е долж ны н ап равляться в s td o u t, а не в s td e r r . Н а рис. 11.2 показано, как каналы и ном ерны е вы ходны е потоки могут и сп ользо­ ваться для связы ван и я програм м и н ап равлени я вы вода в отдельны е файлы. 11.5. Связывание сценариев с каналами 335 У N ode такж е им еется A P I для работы с каналами. О н основан на потоках Node, что позволяет использовать его с чем угодно, что реализует классы потоков Node. И спользовани е каналов в N ode рассм атривается в следую щ ем разделе. Рис. 11.2. Объединение каналов и потоков вывода ПОЛЕЗНАЯ ИНФОРМАЦИЯ О КОМАНДНОЙ СТРОКЕ: ОЧИСТКА СТРОКИ Н екоторы е ком анды получаю тся довольно длинны м и; что делать, если вы хотите удалить длинную ком анду без вы полнен ия? П олезная ком бинация клавиш C trl+ U удаляет текущ ую строку. П ри наж атии клавиш C trl+ Y строка появится обратно, так что вы м ож ете использовать эти ком бинации клавиш при коп ировани и и вставке. 11.5.3. Использование каналов в Node В этом разделе рассм атривается работа с каналами через N ode A PI. Д л я этого будет написан короткий сценарий, которы й вы водит время вы полнен ия программ ы без преры вания конвейерной передачи. П р о гр ам м а м ож ет о т сл еж и в ать со сто ян и е к а н ал а без п р ер ы в ан и я его работы ; д ля этого она ож идает закр ы ти я s t d in и последую щ его нап равлени я результатов в s td o u t. Так как програм м ы N ode заверш аю т работу, когда у них не остается ввода для потребления, при заверш ении програм м ы м ож ет вы водиться сообщ ение. С о ­ храните следую щ ий прим ер в ф айле time.js, чтобы опробовать его: p ro c e ss.std in .p ip e (p ro c ess.std o u t); const s ta r t = Date.now(); p ro c e s s .o n ('e x it', () => { const timeTaken = Date.now() - s ta r t; console.error('T im e (s): ${timeTaken / 1000}'); }); 336 Глава 11. Написание приложений командной строки И з-за повторного связы вания с std o u t можно разместить time.js между сцепленными командами, и это не помеш ает их работе! И parse-json, и tim e.js могут использовать­ ся с каналам и. Н апример, следую щ ая цепочка команд сообщает, сколько времени трати тся на разбор JS O N и отправку данных: parse-json -f te s t.js o n | node tim e .js И так, вы получили представление о том, как выводить данны е и как получать ввод от д руги х при лож ений ; теперь м ож но п ри ступ ать к построению более слож ны х процессов. Н о сначала стоит поговорить о последовательности вы полнен ия при объединении процессов каналами. ПОЛЕЗНАЯ ИНФОРМАЦИЯ О КОМАНДНОЙ СТРОКЕ: ЗАВЕРШЕНИЕ К роме истории команд, м ногие ком андны е интерпретаторы могут автом ати­ чески подставлять ком анды и ф айлы при наж атии клавиш и Tab. Н екоторы е даж е позволяю т просм отреть список возм ож ны х подстановок ком бинацией клавиш A lt+?. 11.5.4. Каналы и последовательность выполнения команд П ри связы вании команд каналам и каж дая команда запускается немедленно. Коман­ ды никоим образом не ож идаю т друг друга. Это означает, что данны е не будут д о ­ ж и даться заверш ен ия какой-либо ком анды и програм мы могут потреблять только те данны е, которы е бы ли получены . Так как ком анды ничего не ожидаю т, вы не знаете, как заверш илась преды дущ ая команда. П редставьте, что сообщ ение долж но вы водиться только в том случае, если разбор JS O N прош ел успеш но. Д л я этого нам понадобятся новы е операторы. О ператоры && и | | в ком андном интерпретаторе работаю т прим ерно так же, как и в Ja v aS crip t с числам и. О ператор &&вы полняет следую щ ую команду, если преды дущ ий код за ­ верш ения равен нулю, а | | вы полняет следую щ ую команду, если код заверш ения отличен от нуля. Посмотрим, как создать небольш ой сценарий, который выводит сообщ ение в s td e r r при заверш ении процесса. Зам етим , что ситуация отличается от echo, потому что вывод направляется в s t d e r r — он предназначен для разработчиков, а не для других програм м . Д л я этого необходим о прослуш ивать собы тие e x i t процесса, а затем записать аргументы в s td e r r : p ro c e ss.std in .p ip e (p ro c ess.std o u t); p ro c e s s .o n ('e x it', () => { const args = p ro c e ss.a rg v .slic e (2 ); c o n so le .e rro r(a rg s .jo in (' ') ) ; }); 11.6. Интерпретация реальных сценариев 337 И спользовав оператор &&, м ож но вы звать exit-message.js в том случае, если разбор JS O N прош ел успешно: parse-json -f te s t.js o n && node exit-m essage.js "parsed JSON successfully" О днако exit-message.js не получит вы вода parse-json. О ператор &&долж ен дождаться заверш ен ия parse-json.js, чтобы узнать, нуж но ли вы полнять следую щ ую команду. П ри исп ользован ии && не сущ ествует автоматического перенаправления, д ей ству­ ющего при исп ользован ии каналов. Перенаправление ввода Вы уж е видели, как вы полняется перенаправление вывода, но и ввод мож ет п ере­ н ап равляться аналогичны м образом. Т акая необходимость возникает достаточно редко, но она может быть полезной, если исполняемы й ф айл не получает им я файла в аргументе. Если вы хотите, чтобы ком анда прочитала ф айл в s td in , используйте конструкцию <имя_файла: parse-json -f - < invalid.json О бъединяя обе форм ы перенаправления, вы можете использовать временный ф айл для восстановления вы вода parse-json: parse-json -f te s t.js o n >tmp.out && node exit-m essage.js "parsed JSON successfully" <tmp.out Н ау ч и в ш и сь о б р ащ аться с потокам и , ко д ам и зав е р ш ен и я и п оряд ком команд, вы смож ете писать сценарии с ком андам и N ode для ваш их собственны х пакетов. В следую щ ем разделе показано, как использовать B row serify и U glifyJS с исп оль­ зованием каналов. ПОЛЕЗНАЯ ИНФОРМАЦИЯ О КОМАНДНОЙ СТРОКЕ: ОЧИСТКА ЭКРАНА В некоторы х случаях на терминал могут быть направлены двоичны е данные. С ловно в сцене из «М атрицы », экран заполняется непонятны м и символами. В таких случаях вы мож ете либо наж ать C trl+ L , чтобы обновить экран, либо ввести ком анду reset д ля сброса терминала. 11.6. Интерпретация реальных сценариев Вы готовы к тому, чтобы начать писать собственны е поля s c r i p t в ф айлах package. json. Д л я прим ера посмотрим, как объединить пакеты brow serify и uglifyjs из npm. П рилож ен ие B row serify (http://browserify.org/) получает м одули N ode и объеди­ няет их д ля исп ользован ия в браузере. П рилож ение U glifyJS (https://github.com/ mishoo/UglifyJS2) проводит миниф икацию ф айла JavaScript, чтобы при передаче его б раузеру расходовалось м еньш е ресурсов кан ала связи и времени. Ваш сценарий 338 Глава 11. Написание приложений командной строки будет получать ф айл main.js (из каталога ch11-command-line/snippets/uglify-example в архиве кода), вы полнять его конкатенацию для использования в браузере, а затем м и н иф ици ровать полученны й сценарий: { "devDependencies": { "browserify": "13.3.0", " u g lify -js" : "2.7.5" }, "s c rip ts " : { "build": "browserify -e m ain.js > bundle.js && u g lify js bundle.js > bundle.m in.js'' } } С ц ен ар и й п о стр о ен и я зап у ск ается ком ан дой npm run b u ild . В этом при м ере он создает ф айл bundle.js. Е сли создание bundle.js прош ло успеш но, сценарий создает ф айл bundle.min.js. П ри пом ощ и оператора && вы мож ете гарантировать, что вторая ф аза будет вы полнена только при успеш ном вы полнении первой фазы. И спользовав приемы, представленны е в этой главе, вы сможете создавать и исполь­ зовать п р и лож ения ком андной строки. П омните, что вы всегда м ож ете воспользо­ ваться ком андной строкой д ля объединения сценариев из других язы ко в — если у вас им еется полезная программа ком андной строки, написанная на язы ке Python, R uby и ли H askell, вы см ож ете легко исп ользовать ее в своих програм м ах Node. 11.7. Заключение А ргументы ком андной строки м огут читаться из массива p ro c e ss.a rg v . О Такие модули, как yargs, упрощ аю т разбор и проверку аргументов. О Удобный способ добавления сценариев в проекты Node — определение сценариев npm в ф айле package.json. О Д л я ч тен и я и зап и си дан ны х в програм м ы ком ан дной строки исп ользую тся стандартны е каналы ввода/вы вода. О С тандартны й ввод, вывод и ош ибки могут перенаправляться разны м процессам и файлам. О П рограм м ы вы даю т коды заверш ен ия, по которы м м ож но провери ть усп еш ­ ность их заверш ения. О П рограммы командной строки соблюдают общ епринятые соглаш ения, знакомые другим пользователям . 12 Разработка настольных приложений с использованием Electron П р ед ы д у щ ая гл ав а б ы ла п о св я щ ен а п о стр о ен и ю п рограм м к о м ан д н о й строки в Node. Тем не менее N ode начинает обращ ать на себя вним ание в другой области: настольны х при лож ениях. П рограм м исты все чащ е при влекаю т веб-технологии д л я реш ен и я проблем кроссп латф о р м ен н о й разработки. В этой главе вы у зн а е ­ те, как построить настольное веб-п рилож ени е на базе встроенны х средств, N ode и клиентской веб-технологии. Вы мож ете разработать и запустить это прилож ение в Linux, m acO S и W indow s. Также м одули N ode будут использоваться в модели, не так далеко уш едш ей от разработки веб-п рилож ени й «клиент-сервер». 12.1. Знакомство с Electron Ф рей м во р к E lectron, изначально известны й под названием A tom Shell, позволяет строить настольны е при лож ения на базе веб-технологий. П рограм м ны й и п ол ьзо­ вательский ин терф ейс создаю тся разработчиком на базе H T M L , CSS и JavaS cript, но некоторы е части настольны х програм м предоставляю тся ф рейм ворком . К их числу относятся: О автом атические обновления; О сообщ ения о сбоях; О установка д ля M icrosoft W indow s; О отладка; О платф орм енны е меню и уведом ления. Н а базе E le c tro n б ы ли со зд ан ы н ек о то р ы е и зв естн ы е п р и л о ж е н и я . П ервое из них — A tom , текстовы й редактор G itH u b ; среди более современны х прилож ений засл у ж и в аю т у п о м и н ан и я Slack, п о п у л я р н ы й чат-серви с, и V isual S tu d io C ode ком пании M icrosoft (рис. 12.1). Глава 12. Разработка настольных приложений с использованием Electron 340 • О н> р 4* • p*e*f*on»ine - Visual Studio Cod* Рис. 12.1. Окно приложения и встроенное контекстное меню в Visual Studio Code О п р о б у й те н е к о то р ы е из эти х п р и л о ж е н и й — они даю т п р ед с тав л ен и е о том, ч т о м о ж н о с д е л а т ь с п о м о щ ь ю E le c tr o n . В о о р у ж и в ш и с ь з н а н и я м и N o d e и Jav aS crip t, вы смож ете строить конкурентоспособны е настольны е прилож ения. 12.1.1. Технологический стек Electron П реж де чем браться за E lectron, вам стоит ознаком иться с тем, какое место за н и ­ м ает E lectro n в N ode, H T M L и CSS. П рилож ен ие E lectron содерж ит следую щ ие компоненты: О главны й процесс — сценарий Node, которы й загруж ает прилож ение и открывает доступ к м одулям Node; О процесс визуализации — веб-страница, находящ аяся под управлением Chromium. О днако у реального п р и ло ж ен и я им ею тся и другие зависи м ости. П риведенны й вы ш е список м ож но дополнить следую щ им и ф ункциям и: О вклю чение главного процесса; О подклю чение к платф орм енн ой базе данны х (наприм ер, SQ L ite); О взаим одействие с веб-API; О чтение и запись л окальны х ф айлов (наприм ер, кон ф и гурац ионн ы х ф айлов); О доступ к платф орм енной ф ункц иональн ости (наприм ер, контекстны е меню ); О вклю чение процесса визуализации; 12.1. Знакомство с Electron 341 О реализация современного полнофункционального веб-приложения с использова­ нием выбранной технологии клиентской стороны (например, React или Angular); О исп ользован ие встроенной ф у нкц иональн ости (наприм ер, контекстны е меню и уведом ления); О встроенны е сценарии; О генерирование клиентского кода J a v a S c rip t в вы бранной системе построения (G ru n t, G ulp, сценарии npm ); О подготовка к распространению . Н а рис. 12.2 приведена схема трех основных частей типичного прилож ения Electron. Как видно из схемы, Node используется для запуска главного процесса и взаим одей­ ствия с операционной системой д ля использования сервисны х функций: откры тия ф айлов, чтени я и запи си в базу данных, взаим одействия с веб-служ бам и. И хотя особое внимание уделяется пользовательскому интерф ейсу процесса визуализации, N ode остается важ нейш ей частью архитектуры прилож ения. Сценарий Node.js Главный процесс ^ R K T A P I^ База данных SQLite Браузер Chromium Процесс визуализации Сценарии построения (css Node.js или сценарии командного интерпретатора Рис. 12.2. Основные части типичного приложения Electron 12.1.2. Проектирование интерфейса Познакомивш ись с основными компонентами прилож ений Electron, посмотрим, как строятся современны е интерф ейсы . П рилож ен ия E lectron строятся на базе H TM L, CSS и Jav aS crip t, так что вы не смож ете задействовать платф орм енны е виджеты. Представьте, что вы хотите создать ин терф ейс в стиле M ac. П анель инструментов 342 Глава 12. Разработка настольных приложений с использованием Electron m acO S м ож но им итировать градиентам и CSS. Также средствам и CSS м ож но и с ­ п о л ь зо в ать п л атф о р м ен н ы е ш р и ф ты , п р ед о ставл я ем ы е m acO S и W indow s; вы даж е мож ете вклю чить сглаж ивание, чтобы ваш е прилож ение больш е походило на платф орм енн ы е прилож ения. М ож но снять текстовое вы деление д ля некоторы х U I-ком понентов и реализовать поддерж ку перетаскиван ия мышью. В настоящ ее врем я многие прилож ения E lectron использую т CSS с таким и же цветами, стилями, значкам и и градиентам и, как в m acO S и W indow s. Н еко то р ы е п р и л о ж ен и я прилагаю т особы е у си л и я к и м и тац и и п л атф орм енн ой функциональности; примером такого рода служит почтовое прилож ение N1 (https:// github.com/nylas/N1). Д ругие при лож ения — такие, как Slack (https://slack.com/) — обладают достаточно четко выраж енной индивидуальностью и работают без особых м оди ф и кац и й на всех платформ ах. Когда вы начнете строить собственные прилож ения Electron, вам придется выбирать, какой подход лучш е соответствует ваш ему проекту. Если вы хотите создать п р и ­ лож ение, которое вы глядит так, словно оно использует платф орм енны е виджеты, вам придется создать стили, подходящ ие для каж дой платформ ы . П роектирование всех вариантов пользовательского ин тер ф ей са потребует дополнительного в р е ­ мени. В озможно, этот вариант будет более п ри влекательн ы м д ля пользователей вашего прилож ения, но он такж е м ож ет повы сить затраты на разверты вание новой ф ункциональности. В следую щ ем разделе заготовка пр и ло ж ен и я E lectro n и сп ользуется д ля создания нового прилож ения. Это стандартны й метод построения новы х проектов в Electron. 12.2. Создание приложения Electron Н ачать работу с E lectro n прощ е всего с проекта electro n -q u ick -start, доступного на G itH u b (https://github.com/atom/electron-quick-start). В этом м аленьком р еп о зи ­ то р и и содерж атся зависи м ости, необходим ы е д л я зап уска базового п р и лож ен и я E lectron. Ч тобы использовать этот проект, скопируйте проект из репози тори я и установите его зависи м ости в npm: g it clone h ttp s://g ith u b .co m /ato m /electro n -q u ick -start cd e le c tro n -q u ic k -sta rt npm in s ta ll П осле заверш ен ия всех загрузок вы м ож ете запустить главны й процесс командой npm s t a r t . Вы мож ете спокойно брать этот проект за основу для других прилож ений E lectron; вам соверш енно не обязательно создавать свои проекты с нуля. 12.2. Создание приложения Electron 343 П ри зап у ск е п р и л о ж е н и я п о я в л я е т с я окн о с в е б -ст р а н и ц ей и и н стр у м ен там и C hrom ium D eveloper Tools. Е сли вы — веб-разработчик, исп ользую щ ий C hrom e, р езу л ь тат не сл и ш ко м в п еч атл я ет: п р и л о ж е н и е н ап о м и н ает в е б -с т р а н и ц у без C S S -оф орм лен ия в C hrom e. Н о д ля того, чтобы все это заработало, «за кулисами» происходит много всего. Н а рис. 12.3 показано, как результат вы глядит в macOS. « E lectron • Edit View Window Help Hello Woridl • Hello World! W c arc using node 4.1.1. Chrome 4S.0.24S4.8S. and Elcciron 0.3S.4. E le c tro n Ѵ*И0в0.М.4К>.».«» D*v*top*r Tool* - fi'e.7//User*№lesn)oe\«nefrts/CodeAxxMnactiorVch13/e'«c:ron-Q\jtc*-fttart/;rx*ei.himl Q, О Q V Elements Network <topin m e> * Sources Timeline Profiles Resources Audits Console Д 1 >_ ф □, C ;Preserve log Д /deep/ corfcinetor is e«f>rec«tee. See https://www.cltroaett«ti.s.coa/fe«turet/67Sa4S463a34ll2a to r mere M t e i l t . Рис. 12.3. Проект electron-quick-start в macOS Это автоном ны й пакет п р и лож ения m acOS: он вклю чает в себя версию N ode, от­ личную от той, которая работает в моей системе, использует собственны е команды меню и окно About. К этому м ом енту вы мож ете начать строить собственное веб-прилож ение в index. html с использованием H T M L , Ja v a S c rip t и CSS. Н о вам как N ode-программисту, вероятно, не терпится использовать N ode на практике; давайте сначала посмотрим, как это делается. E lectro n вклю чает в себя модуль rem ote, использую щ ий м еханизм м еж процессны х ком м уникаций (IP C , InterP ro cess C om m unication) меж ду процессом визуализации и главны м процессом Node. М одуль rem ote даж е мож ет предоставить доступ к м о ­ дулям N ode. Ч тобы опробовать его на практике, добавьте в свой проект E lectron ф айл с именем readfile.js и вклю чите в него код из листинга 12.1. Листинг 12.1. Простой модуль Node const fs = r e q u i r e ( 'f s ') ; module.exports = (cb) => { f s .r e a d F ile ( './m a in .js ', { encoding: 'u tf 8 ' }, cb); }; Теперь откройте ф айл index.html и внесите изм енения: добавьте элем ент с ид ен ти ­ ф икатором so u rce и сценарий, которы й загруж ает readfile.js (л и сти н г 12.2). 344 Глава 12. Разработка настольных приложений с использованием Electron Листинг 12.2. Загрузка модулей Node из процесса визуализации <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> < title>H ello W orld!</title> </head> <body> <h1>Hello World!</h1> <pre id="source"></pre> <script> var re a d file = r e q u ir e ( 'r e m o te ') .r e q u ir e ( './r e a d f ile ') ; re a d file (fu n c tio n (e rr, te x t) { c o n s o le .lo g ( 'r e a d f ile :', e r r , te x t); document.getElementById('source').innerHTML = te x t; }); </script> </body> </html> В листинге 12.2 модуль rem ote используется для загрузки ф айла readfile.js и его вы ­ полнен ия в главном процессе. В заим одействие меж ду двумя процессами проходит без проблем, поэтому внешне все мало отличается от использования стандартных мо­ дулей Node. Единственное серьезное различие — использование r e q u ir e ( 'r e m o te ') . re q u ire (file ). 12.3. Построение полнофункционального настольного приложения И так, вы узнали, как создать базовое при лож ение E lectron и как использовать м о ­ дули Node. П ора сделать следую щ ий ш аг и построить полноценное прилож ение. П рилож ение, которое мы создадим, представляет собой инструм ент разработчика для вы дачи и просмотра запросов HTTP. Его можно рассматривать как графический ин терф ейс д ля м одуля req u est (www.npmjs.com/package/request). Х отя при лож ения E lectro n м ож но строить непосредственно из H T M L , JavaS cript, CSS и Node, в этой главе мы воспользуем ся соврем енны м и инструм ентам и р азр а­ ботчика клиентской части; это упростит сопровождение и расш ирение прилож ения. П ри этом будут использоваться следую щ ие средства: О e lectro n -q u ick -start как основа д ля проекта; О модуль req u est для запросов H T T P ; О R eact д ля кода пользовательского интерф ейса; О B abel д ля преобразования соврем енного синтаксиса ES6 в синтаксис ES5, со­ вм естим ы й с браузером; О w ebpack д ля построения клиентского прилож ения. 12.3. Построение полнофункционального настольного приложения 345 Н а рис. 12.4 показано, как будет вы глядеть готовое прилож ение. Рис. 12.4. Electron-приложение HTTP Master Затем вы узнаете, как настроить проект на базе R eact с w ebpack и Babel. 12.3.1. Исходная настройка React и Babel О дна из основны х проблем при построении нового п ри лож ения со слож ной к л и ­ ентской частью — настройка таких библиотек, как R eact и Babel, с системой п о ­ строения, без особых проблем с сопровож дением. Есть много вариантов, вклю чая G runt, G ulp и W ebpack. С итуация дополнительно услож няется тем, что библиотеки и зм ен яю тся со временем, так что книги и учебны е опи сан ия быстро устареваю т. Чтобы избежать проблем со стремительны м развитием средств разработки кл и ен т­ ской части, мы указы ваем точны е версии всех зависим остей, так что вы сможете воспроизвести описания и получить сходны е результаты. Если вы все ж е зап утае­ тесь, воспользуйтесь таким и инструм ентам и, как Yeoman (http://yeoman.io/), для генерирования заготовки прилож ения. Затем измените сгенерированную заготовку и приведите ее в соответствие с прилож ением , описанны м в этой главе. 12.3.2. Установка зависимостей С оздайте новы й проект electro n -q u ick -start. С тоит напомнить, что проект следует клонировать с G itH ub: g it clone h ttp s://g ith u b .co m /ato m /electro n -q u ick -start cd e le c tro n -q u ic k -sta rt npm in s ta ll 346 Глава 12. Разработка настольных приложений с использованием Electron Затем установите react, react-dom и babel-core: npm in s ta l l --save-dev react@0.14.3 react-dom@0.14.3 babel-core@6.3.17 Д ал ее необходи м о у стан о в и ть п л аги н ы B abel. Г лавны й из ни х — b a b e l-p re se tes2015 — избыточен для проекта, ограниченного Chrom ium , но его вклю чение упро­ стит эксперим енты с ф ункц иям и ES2015, которые C hrom ium еще не поддерживает. И спользуй те д ля установки следую щ ие команды: npm in s ta l l --save-dev babel-preset-es2015@6.3.13 npm in s ta l l --save-dev babel-plugin-transform -class-properties@ 6.3.13 П лагин добавляет поддерж ку JS X в Babel: npm in s ta l l --save-dev babel-plugin-transform -react-jsx@ 6.3.13 Затем установите webpack: npm in s ta l l --save-dev webpack@1.12.9 Д л я работы с Babel вам такж е понадобится пакет babel-loader д ля webpack: npm in s ta l l --save-dev babel-loader@6.2.0 К огда зависи м ости будут готовы, добавьте в проект ф айл .babelrc. О н приказы вает B abel использовать плагины ES2015 и React: { "plugins": [ "transform -react-jsx" ], "p resets": ["es2015"] } Н аконец, откройте ф айл package.json и обновите свойство s c r i p t s , вклю чив в него вы зов webpack: "s c rip ts " : { " s ta r t" : "electron m ain.js", "build": "node_modules/.bin/webpack --progress --colors" }, Это позволяет строить приложение командой npm run build. Также доступны плагины w ebpack для горячей загрузки React, но здесь эта возможность не рассматривается. Если вы хотите, чтобы код на стороне клиента автоматически строился при изменении файлов, воспользуйтесь таким и инструментами, как fswatch и nodemon. 12.3.3. Настройка webpack Д л я исп ользован ия w ebpack вам понадобится ф айл webpack.config.js. Д обавьте его в корневой каталог проекта. 12.3. Построение полнофункционального настольного приложения 347 Ф ай л содерж ит код J a v a S c rip t с использованием м одуля C om m onJS в стиле Node: const webpack = require('w ebpack'); module.exports = { se ttin g : 'value' }; В наш ем проекте понадобятся настройки д ля поиска ф айлов R eact (.jsx), загрузки точки входа (/app/index.jsx) и разм ещ ен ия вы вода в месте, в котором пользователь­ ский ин терф ейс E lectro n смож ет его най ти (js/app.js). Ф ай л ы R eact такж е долж ны обрабаты ваться Babel. В результате объединения всех этих требований будет полу­ чен ф айл, приведенны й в листинге 12.3. Листинг 12.3. webpack.config.js const webpack = require('w ebpack'); module.exports = { module: { loaders: [ { t e s t : / \ .j s x ? $ / , loaders: ['b a b e l-lo a d e r'] } ] }, entry: [ './a p p /in d e x .js x ' ], resolve: { extensions: [ ' ' , ' . j s ' , '. j s x '] }, output: { path: _dirname + ' / j s ' , filename: 'a p p .js ' } }; В этом листинге мы приказы ваем w ebpack преобразовы вать ф айлы .jsx (R eact) про­ грам м ой Babel при помощ и свойства m o d u le .lo a d e rs. П рограм м а Babel уж е была настроена д л я обработки ф айлов R eact записью t r a n s f o r m - r e a c t - j s x в .babelrc. З атем свойство e n try используется для определения главной точки входа д ля кода React. Этот способ работает, потому что компоненты React базирую тся на элементах H T M L . П оскольку элем енты H T M L долж ны иметь один родительский узел, одна точка входа м ож ет охваты вать все прилож ение. Свойство r e s o lv e .e x te n s io n s сообщ ает w ebpack, что ф айлы .jsx следует рассм атри­ вать как модули. Е сли использовать ком анду вида im port {C lass} from ' c l a s s ', она проверит ф айлы class.js и class.jsx. Н аконец, свойство o u tp u t сообщает w ebpack, куда следует записать выходной файл. В данном прим ере используется значение js/, но вы м ож ете вы брать лю бой путь, доступны й д ля пользовательского ин терф ейса E lectron. 348 Глава 12. Разработка настольных приложений с использованием Electron П ора наполнить прилож ение R eact реальным содержанием. Н ачнем с главной точки входа и того, как она будет обращ аться к U I -элементам запроса и ответа. 12.4. Приложение React Н а рис. 12.4 п оказан вн еш н и й вид п р и ло ж ен и я. О но содерж ит две группы U Iкомпонентов, которы е м ож но разделить на 7 элементов: О запрос (Request); • URL: String; • метод: String; • заголовки: объект с парам и строк; О ответ (Response); • код статуса H T T P ; • заголовки: объект с парам и строк; • тело: String; • ош ибки: String. Н о в R eact н е л ьзя просто вы вести два гр аф и ч еск и х объекта по соседству: они долж ны содерж аться в одном родителе. П отребуется объект при лож ения верхнего уровня, содерж ащ ий U I -элем енты запроса и ответа. С классам и запроса (R e q u e s t) и ответа (R esponse), которы е будут реализованы позднее, сам класс App долж ен вы глядеть так, как показано в листинге 12.4. Листинг 12.4. Класс App import React from 'r e a c t '; import ReactDOM from 'react-dom '; import Request from './ r e q u e s t '; import Response from './re s p o n s e '; class App extends React.Component { render() { return ( <div className="container"> <Request /> <Response /> </div> ); } } ReactDOM.render(<App /> , document.getElementByld('app')); Сохраните код в файле app/index.jsx. О н сначала загружает классы Request и Response, а за т е м в ы в о д и т их д ан н ы е в эл е м е н т е d iv . В п о сл е д н ей ст р о к е R e a c tD O M 12.4. Приложение React 349 исп ользу ется д л я ви зу ал и зац и и у злов м одели D O M класса App. Тег <App /> и с ­ пользуется д ля обращ ения к классу App. Ч т о б ы это р е ш е н и е р а б о т а л о , н ео б х о д и м о о п р е д е л и т ь к о м п о н е н т ы R e q u e s t и Response. 12.4.1. Определение компонента Request К ласс R equest получает на входе U R L -адрес и метод H TTP, после чего генерирует запрос, которы й отправляется с использованием м одуля N ode request. П остроение ин терф ейса осущ ествляется с использованием JS X , но, в отличие от предыдущ его примера, элем ент не прорисовы вается напрям ую R eactD O M ; это происходит при вклю чении в главны й класс п р и лож ения в ф айле app/index.jsx. В листинге 12.5 (ф ай л app/request.js) содерж ится полны й код класса. М ы удалили ф ункцию редакти рования заголовков д ля сокращ ения объема примера; если вам захочется увидеть прим ер с расш иренной ф ункциональностью , вклю чая р едак ти ­ рование заголовков, обращ айтесь к репозиторию H T T P W izard на G itH u b (https:// github.com/alexyoung/http-wizard). Листинг 12.5. Класс Request import React from 'r e a c t '; import Events from './e v e n ts '; const request = re m o te .re q u ire ('re q u e st'); class Request extends React.Component { constructor(props) { super(props); th i s .s t a t e = { u rl: n u ll, method: 'GET' }; } handleChange = (e) => { const s ta te = {}; state[e.targ et.n am e] = e .ta rg e t.v a lu e ; th is .s e tS ta te ( s ta te ) ; } makeRequest = () => { r e q u e s t( th is .s ta te , ( e rr, re s, body) => { const statusCode = res ? res.statusC ode : 'No response'; const re s u lt = { response: '(${statu sC o d e})', raw: body ? body : ' ' , headers: res ? res.headers : [], e rro r: e rr ? JSO N .stringify(err, n u ll, 2) : '' }; E v e n ts .e m it('re s u lt', re s u lt); new Notification('HTTP response fin ish ed : ${statusCode}') Глава 12. Разработка настольных приложений с использованием Electron 350 }); } render() { return ( <div className="request"> <h1>Request</h1> <div className="request-options"> <div className="form-row"> <label>URL</label> <input name="url" type="url" v a lu e = { th is.sta te .u rl} onChange={this.handleChange} /> </div> <div className="form-row"> <label>Method</label> <input name="method" type="text" value={this.state.m ethod} placeholder="GET, POST, PATCH, PUT, DELETE" onChange={this.handleChange} /> </div> <div className="form-row"> <a className="btn" onClick={this.makeRequest}>Make request</a> </div> </div> </div> ); } } export default Request; Основную часть листинга занимает H T M L -разметка метода render. Сосредоточимся на остальном, прежде чем заним аться построением пользовательского интерфейса. С начала потомок класса N ode E v entE m itter в ф айле app/events.jsx используется для взаим одействия м еж ду этим ком понентом и компонентом response. Следую щ ий ф рагм ент взят из ф айла app/events.jsx: import { EventEmitter } from 'e v e n ts'; const Events = new EventEmitter(); export default Events; О братите вни м ан ие на то, что класс R equest я в л я е т с я потом ком React.Component. О н о п р ед ел яет кон структор, которы й у стан авл и вает состоян и е по ум олчанию ; свойство s t a t e зани м ает особое м есто в R eact и м ож ет зад аваться таким образом только в конструкторе. Во всех остальны х местах необходим о использовать t h i s . s e tS ta te . 12.4. Приложение React 351 Рис. 12.5. Уведомление М етод handleChange задает состояние на основании атрибута name элемента H TM L. Ч тобы понять, как работает этот механизм, обратитесь к элем енту <input> для U RL в методе render: <input name="url" type="url" v a lu e = { th is.sta te .u rl} onChange={this.handleChange} /> Зад ан н ое значение name используется д ля зад ан и я U R L при редактировании. Н а ­ значение s t a t e такж е инициирует вы полнение ren d er, а R eact обновляет атрибут v a lu e изм ененны м состоянием . А теперь посмотрим, как м одуль request будет и с­ пользоваться этим классом. Класс содерж ит клиентский код, которы й вы полняется в веб-представлении, поэто­ му вам понадобится доступ к модулю request для создания запросов HTTP. Electron предоставляет возмож ность загрузки удаленны х модулей без лиш него ш аблонного кода. В начале класса глобальны й объект rem ote вклю чает модуль N ode request: const request = re m o te .re q u ire ('re q u e st'); Д алее в методе makeRequest запрос H T T P вы дается простым вызовом re q u e s t(). А р­ гументы re q u e st бы ли заданы в состоянии класса, поэтому от вас потребуется лиш ь обработать обратны й вызов, которы й отрабатывает при заверш ении запроса. Объем императивного кода очень мал: состояние класса задается на основании результата 352 Глава 12. Разработка настольных приложений с использованием Electron запроса, после чего результат передается для использования компонентом Response. Такж е отображ ается визуальн ое уведом ление; если запрос слиш ком медленный, п о льзователь оповещ ается об этом ви зу ал ьн о с и сп ользован и ем всплы ваю щ его окна операционной системы: new Notification('HTTP response fin ish ed : ${statusCode}') Н а рис. 12.5 показано типичное уведомление. П осм отрим , как ком понент Response отображ ает ответ HTTP. 12.4.2. Определение компонента Response К ом понент Response прослуш ивает собы тия r e s u l t и задает свое состояние в соот­ ветствии с результатам и последнего запроса. Д л я вывода результатов используется таблиц а д ля заголовков и элем енты d iv д ля тела запроса и ош ибок. В листинге 12.6 приведен полны й код компонента Response из ф айла app/response.jsx. Листинг 12.6. Компонент Response import React from 'r e a c t '; import Events from './e v e n ts '; import Headers from './h e a d e r s '; class Response extends React.Component { constructor(props) { super(props); t h is .s ta t e = { re s u lt: {}, tab: 'body' }; } componentWillUnmount() { E v en ts.rem o v eL isten er('resu lt', th is.h a n d le R e su lt.b in d (th is)); } componentDidMount() { E v e n ts.a d d L iste n e r('re su lt', th is.h a n d le R e su lt.b in d (th is)); } handleR esult(result) { th is .s e tS ta te ({ re s u lt: re s u lt }); } handleSelectTab = (e) => { const tab = e .ta rg e t.d a ta s e t.ta b ; th is .s e tS ta te ({ tab: tab }); } render() { const re s u lt = t h i s .s t a te .r e s u l t ; const tabClasses = { 12.4. Приложение React body: t h i s .s t a te .t a b === 'body' ? 'a c tiv e ' : nu ll, e rro rs: t h i s .s t a te .t a b === 'e r r o r s ' ? 'a c tiv e ' : null, }; const rawStyle = t h i s .s t a te .t a b === 'body' ? null : { display: 'none' } const erro rsS ty le = t h is .s t a t e .t a b === 'e r r o r s ' ? null : { display: 'none' }; return ( <div className="response"> <h1>Response <span id="response">{result.response}</span></h1> <div className="content-container"> <div className="content"> <div id="headers"> <table className="headers"> <thead> <tr> <th className="name">Header Name</th> <th className="value">Header Value</th> </tr> </thead> <Headers headers={result.headers} /> </table> </div> <div className="results"> <ul className="nav"> < li className={tabClasses.body}> <a data-tab='body' onClick={this.handleSelectTab}>Body</a> < /li> < li className={tabClasses.errors}> <a d a ta -ta b = 'e rro rs ' href="#" onClick={this.handleSelectTab}>Errors</a> </li> </ul> <div className="raw" id="raw" style={rawStyle}>{result.raw}</div> <div className="raw" id="error" style={errorsS tyle}> {result.error}</div> </div> </div> </div> </div> ); } } export default Response; 353 354 Глава 12. Разработка настольных приложений с использованием Electron К омпонент Response не содерж ит кода, непосредственно относящ егося к обработке ответов H T T P ; он вы водит свое состояние в разны х элементах H T M L . Д л я п ере­ клю чени я вкладок обработчик o n c lic k связы вается с м етодом h an d leS electT ab , которы й переклю чается м еж ду телом запроса и ош ибкам и при помощ и атрибута ( d a ta - ta b ) . К ом пон ент R esponse и сп о льзу ет д ругой ком понент, H eaders, д л я отображ ен и я заголовков ответа H T T P. Р азбиени е ком понента на еще м еньш ие ком поненты — стандартная практи ка в R eact. З н ач ен и я заголовков передаю тся субкомпонентам через атрибуты ; в R eact они назы ваю тся свойствами (props): <Headers headers={result.headers} /> В листинге 12.7 представлен код ком понента Headers из ф айла app/headers.jsx. Листинг 12.7. Компонент Headers import React from 'r e a c t '; class Headers extends React.Component { render() { const headers = th is.p ro p s.h ead ers | | {}; const headerRows = Object.keys(headers).m ap((key, i) => { return ( <tr key={i}> <td className="name">{key}</td> <td className="value">{headers[key]}</td> </tr> ); }); return ( <tbody className="header-body"> {headerRows} </tbody> ); } } export default Headers; О братите вним ание на обращ ение к свойствам в начале м етода re n d e r() в вы ра­ ж ен ии th is .p r o p s .h e a d e r s . 12.4.3. Взаимодействие между компонентами React К лассы R equest и R esponse достаточно хорош о изол и рован ы друг от друга; они сосредоточены на реш ении своих конкретны х задач без того, чтобы напрям ую вы ­ зы вать друг друга. В R eact сущ ествуют другие, более слож ны е средства управления 12.5. Построение и распространение 355 состоянием , но они вы ходят за рам ки темы этой главы. В наш ем прим ере слож ны е м еханизм ы передачи данны х не нуж ны , потому что в нем используется всего два основн ы х ком п онента, п оэтом у д л я передачи д ан н ы х и сп о л ьзу ется кл асс N ode E ventE m itter. Ч тобы использовать E v en tE m itter таким образом, создайте экзем пляр в отдельном ф айле (app/events.jsx), а затем экспортируйте экземпляр: import { EventEmitter } from 'e v e n ts '; const Events = new EventEmitter(); export d efau lt Events; Т еперь к о м п о н е н т ы м о гу т и н и ц и и р о в а т ь с о б ы т и я и п о д к л ю ч ать сл у ш ател ей д ля передачи данны х. К ом понент R eq u ire использует эту возм ож ность в методе makeRequest с результатом запроса H T T P : E v e n ts .e m it('re s u lt', re s u lt); Затем в классе Response мы сохраняем результаты, назн ачая слуш ателя на ранней стадии ж изненного ц и кла компонента: componentWillUnmount() { E v en ts.rem o v eL isten er('resu lt', th is.h a n d le R e su lt.b in d (th is)); } С ростом п р и лож ения сопровож дать этот паттерн становится все сложнее; в ч аст­ ности, возникает проблема с отслеж иванием имен событий. Так как данные хранятся в строковом виде, их легко забы ть и ли записать неправильно. В усоверш ен ство­ ванн ой верси и этого п аттерн а и сп о льзу ется спи сок констант, представляю щ их собой им ена собы тий. Е сли вы снова р асш и рите этот паттерн, чтобы раздели ть обязанности диспетчеризации событий и сохранения данных, у вас получится нечто похож ее на контейнер состоян ия R edux от Facebook (http://redux.js.org/);, именно поэтом у многие програм м исты использую т его д ля проекти рования и построения более крупны х прилож ений. 12.5. Построение и распространение И так, вы построили ф ункц иональн ое настольное прилож ение, и теперь его можно упаковать д ля m acO S, L inux и W indow s. Распространени е при лож ений E lectron состоит из трех этапов: 1. И зм ен и те стандартное о ф орм лен ие п р и ло ж ен и я E lectron, н азн ачив ему им я и значок. 2. У пакуйте при лож ение в файл. 3. С оздайте двоичны й ф айл для каж дой платформы . 356 Глава 12. Разработка настольных приложений с использованием Electron П роект electro n -q u ick -start уж е почти готов к распространению . От вас потребуется лиш ь скопировать свой код в папку E lectro n Contents/Resources/app в m acO S или electron/resources/app в W indow s и Linux. Тем не менее ручное копирование — не лучш ий способ построения распространяе­ мого двоичного ф айла. Другой, более надеж ны й способ основан на использовании пакета electron-packager (www.npmjs.com/package/electron-packager) М акса Огдена (M ax O gden). П акет предоставляет ин терф ейс ком андной строки д ля построения исп олн яем ы х ф айлов д ля W indow s, L inux и macOS. 12.5.1. Построение приложений с использованием Electron Packager Ч тобы установить electron-packager, установите его глобально. После этого вы смо­ ж ете построить лю бой проект, д ля которого потребуется создать двоичны е ф айлы д ля конкретны х платформ: npm in s ta l l electron-packager -g П осле того как установка будет заверш ена, вы сможете запустить пакет из каталога прилож ения. П ри вы зове следует указать путь к прилож ению , им я прилож ения, платформу, архитектуру (32- и ли 64-разрядн ую ) и версию Electron: electron-packager . HttpWizard --version=1.4.5 К ом анда загруж ает E lectro n версии 1.4.5 и генерирует двоичны е ф айлы д ля всех поддерживаемых платформ и архитектур. Это потребует времени (размер Electron — около 40 М байт), но когда все будет сделано, вы получите двоичные файлы, которые будут работать во всех основны х операционны х системах. СКРЫВАЕМ ИНСТРУМЕНТЫ РАЗРАБОТЧИКА П реж де чем р асп р о стр ан ять построенную версию , уд ал и те и ли изм ен ите строку main.js, которая откры вает инструм енты разработчика Chrom ium : mainWindow.webContents.openDevTools(); Такж е м ож но заклю чить эту ком анду в условную конструкцию с проверкой флага, чтобы инструм енты разработчика откры вались только при устан ов­ ленном флаге: i f (process.env.NODE_ENV === 'debug') { mainWindow.webContents.openDevTools(); } 12.5. Построение и распространение 357 12.5.2. Упаковка Ч тобы дополнительно повы сить бы стродействие прилож ения, вы мож ете у п ак о ­ вать ф айлы Ja v a S c rip t (кл и ен тски е и N ode) с использованием архивов A tom Shell A rchives (https://github.com/atom/asar). Э ти архивы , назы ваем ы е файлами asar, похож и на ta r -архивы U N IX . О ни скры ваю т код Jav a S crip t, но не м аскирую т его настолько, чтобы его было невозмож но декодировать. Тем не менее они решают про­ блем у с искаж ением дли нны х имен ф айлов в W indow s, которая м ож ет возникнуть в зависи м остях с очень больш им уровнем влож енности. В E lectron C hrom ium может читать ф айлы a sa r, как и Node, так что вам не придется делать ничего особенного д ля их поддерж ки. К роме того, electron-packager может создавать пакеты asar за вас с клю чом ком андной строки - -a sa r. Н а рис. 12.6 показано, как выглядит приложение, упакованное без использования asar. Q. Search » ѵ Contents II I Frameworks lnfo.pl 1st MacOS Pkglnfo Resources ► ► ► am.lproj H i app t o ar.lproj atom.asar О atom.lens t o bg.Iproj t o br.lproj t o ea.lproj t o oa.lproj t o de.lproj t o de.lproj t o default.app t o el .Iproj t o en_GBJproj t o en. Iproj t o ec_4l0.lproj ■ I esJproj t o et lproj t o fa.lproj ■ I «Iproj ■ I fll.lproj t o fr.lproj В gu. iproj ■ l he. Iproj t o hi.lproj t o hr Iproj ► ■ app ► Ш build ► t o css t t HHpWii.rd-linux-a32 К HtlpWlzsrd-wln32-ls32 • index.html t o js LICENSE.md___________ ► ► ► ► ► М. node_modules ► notes, md ► package.json ► roadfilc.js » README, md ► webpack.config.js ► ► ► ► ► » ► ► ► ► ► + ► ► ь ► ► ► ► 'use s t r i c t ' j const e le c tr o n = r e q u ir e ( 'e le c t r o n ') ; const app = e le c tro n .a p p ; / / Nodule to c o n tr o l a p p lic a tio n l i f e , const Browserwindow = e le c tr o n . B rm se rttin d o w j / / Module to create native browser window, const request = r e q u ir e ( ' re q u e s t*) ; / / Report crashes to our se rve r. electron.crashReporter.star tt); / / Keep a g lo b a l re fe re n ce o f th e window o b je c t, i f you d o n 't, th e window w i l l I I Ka u it M t » li> ( llu main.js 1 KB Created Yesterday, 16:58 Modified Yesterday, 16:58 Last opened Yesterday, 16:58 Add Tags... 1 o f 1 5 selected , 5 .8 6 G 8 available Рис. 12.6. Содержимое типичного пакета приложения Electron О братите внимание: ф айлы Ja v a S c rip t м ож но откры вать д ля просм отра исходного кода. В прилож ении E lectron двоичны й ф орм ат имею т только ф айлы ресурсов (н а ­ пример, гр аф и ка) и двоичны е м одули Node. Ч тобы построить прилож ение с ф айлам и asar, используйте electron-packager с ф л а ­ гом -- a s a r : electron-packager . HttpWizard --version=0.36.0 --asar= true 358 Глава 12. Разработка настольных приложений с использованием Electron Это самый простой способ, потому что electron-packager вы полнит все необходимые ком анды . Ч тобы проделать то ж е вручную , необходим о устан овить asar, а затем запустить програм м у ком андной строки д ля создания пакета: npm in s ta l l -g asar asar pack path-to-your-app/ app.asar П олучив архив asar, загрузите двоичны й ф айл E lectron (https://github.com/atom/ electron/releases) д ля той платф орм ы , которую вы хотите поддерж ивать, и добавьте архив в каталог Resources (см. рис. 12.6). З ап у ск исполняем ого ф айла или пакета п ри лож ен и я долж ен привести к запуску вашего прилож ения. Такж е п р и лож ения E lectro n м огут и зм ен яться посредством редакти рования д во ­ ичны х ф айлов. Так м ож но изм ен ить и м я и зн ач ки при лож ения. Е сли запустить двоичны й ф айл E lectro n без изм енений, на экране появится окно для запуска п ри­ лож ени й E lectron, созданны х на базе репози тори я electron-quick-start. 12.6. Заключение О Ф р ей м во р к E lectro n позволяет создавать настольны е при лож ения с и сп ользо­ ванием Node, Jav aS crip t, H T M L и CSS. О Вы можете генерировать платформенные меню и уведомления без использования C + +, C # и ли O bjective-C . О Е сли у вас им ею тся полезны е м одули Node, вы мож ете использовать их из кода J a v a S c rip t на стороне к л и ен та в п о л ьзо вател ьско м и н терф ей се п р и л о ж ен и я E lectron. О E lectron использует полноф ункциональны й браузер, так что вы мож ете строить пользовательские интерф ейсы с прим енением новейш их технологий Jav aS crip t (таких, как R eact или A ngular). Приложения Приложение А. Установка Node Приложение Б. Автоматизированное извлечение веб-данных Приложение В. Официально поддерживаемые промежуточные компоненты Установка Node В этом при лож ении приведена более подробная ин ф орм аци я об установке Node. js. Е сли вы начали работать с N ode недавно, мы реком ендуем исп ользовать для установки заранее построенны й пакет. Н иж е процесс установки будет рассм отрен д ля всех остальны х операционны х систем. В зависи м ости от тр ебовани й N ode такж е м ож ет устан авли ваться други м и спо­ собами. Е сли вы п р и надлеж ите к ч и сл у опы тны х п ользователей N ode и ли если у вас действую т особые требования в области D evO ps, переходите к обзору других способов установки Node. А.1. Установка Node с использованием программы установки У N ode есть две программы установки и несколько заранее построенны х двоичных пакетов. Если вы работаете в m acO S или W indow s, мож но использовать как двоич­ ны е пакеты, так и програм м ы установки. Д воичны е пакеты содерж ат исполняемы е ф айлы , но у програм м установки есть специальны е мастеры (w izards), которы е по­ могут установить N ode в каталоге системы, которы й будет легко находиться при вы полнен ии в терм инале таких команд, как node и npm. Е сли вы еще не обладаете достаточны м опы том работы с Node, используйте п ро­ грам м у у стан овки . Все верси и м ож но н ай ти на сайте N ode в разд ел е Downloads (https://nodejs.org/en/download/). А.1.1. Программа установки для macOS Д л я m acO S загр у зи те 6 4 -р азр яд н ы й ф ай л .pkg с сайта N ode (https://nodejs.org/ en/download/). И спользуй те либо LTS, либо текущ ую версию. П осле загрузки вы получите ф айл пакета, изображ енны й на рис. А.1. А.1. Установка Node с использованием программы установки 361 Рис. А.1. Установочный файл .pkg П осле того как програм м а устан овки будет загруж ена, сделайте на ней двойной щ елчок; откроется м астер установки (рис. А.2). Рис. А.2. Мастер установки Щ елкните на кнопке Continue и выполните инструкции; со значениями по умолчанию N ode устан авли вается правильно. П осле того как процесс установки будет зав е р ­ шен, вы смож ете откры ть терм инал и ввести команду node д ля запуска терминала N ode R E P L (рис. А.3). В следую щ ем р а зд ел е р а с с м ат р и в а е т с я п р о ц есс у с тан о в к и д л я п о л ьзо вател ей W indow s. 362 Приложение А. Установка Node £ alex — node — node — node — 46*10 [-* ~ node > c o n s o le .lo g ( 'h e llo w o r ld ') h e llo w o rld undefined >1 Рис. А.3. Node REPL А.1.2. Программа установки для Windows Н а странице N ode D ow nloads (https://nodejs.org/en/download/) щ елкните на значке Windows Installer или на ссы лке Windows Installer .msi. Д оступны 32- и 64-разрядны е версии; вероятно, вы вы берете 64-разрядную . П осле того как ф айл будет загружен, сделайте на нем двойной щ елчок, чтобы запустить м астер устан овки (рис. А.4). Рис. А.4. Программа установки Windows .msi П о д твер д и те зн а ч е н и я по ум олчани ю , затем открой те cmd.exe и проверьте, как работает N ode R EPL. Н а рис. А.5 изображ ено окно N ode R E P L в W indow s. M ic r o s o ft Windows [V e rs io n 19.Ѳ .14393] ( с ) 2Ѳ16 M ic ro s o ft C o rp o ra tio n . A l l r ig h t s re se rve d . С:\U s e r s \a le x r >node Рис. А.5. Node REPL в Windows A.2. Другие способы установки Node 363 Е сли вы обычно не устанавливаете програм м ное обеспечение этим способом или не хотите устан авли вать N ode на общ есистем ном уровне, в следую щ ем разделе рассм атриваю тся другие варианты установки Node. A.2. Другие способы установки Node N ode такж е м ож но построить из исходны х кодов, восп ользоваться м енедж ером пакетов операционной системы и ли м енедж ером версий Node. П ри построении из исходного кода вам понадобится рабочая систем а и работоспособны й экзем пляр Python. A.2.1. Установка Node из исходного кода И сходны й код N ode м ож но загрузить на странице загрузки сайта nodejs.org, но он такж е доступен в р еп о зи то р и и G itH u b (https://github.com /nodejs/node). П олное руководство по построению такж е доступно на G itH u b (n o d e/B u ild in g .m d (h ttp s:// github.com/nodejs/node/blob/master/BUILDING.md). Д л я построения N ode вам п о ­ надобится: О L inux — P y th o n 2.6 и л и 2.7, gcc и g+ + 4.8 и вы ш е и ли clang и clang+ + 3.4 или вы ш е. Ч то б ы п о л у ч и т ь все эти п р о гр ам м ы , п рощ е всего у с т ан о в и т ь пакет build-essen tials в дистри бутивах сем ейства D ebian и ли его экви вален т в других д истри бутивах; О m acO S — X code и инструм енты ком андной строки, которы е могут устан авли ­ ваться с Xcode; О W indow s — P yth o n 2.6 или 2.7, Visual C ++ Build Tools, Visual Studio 2015 U pdate 3. Когда инструментарий построения будет готов, выполните ./c o n fig u re и make в опе­ рационны х систем ах сем ейства U N IX . В W indow s вы полните команду .\v c b u ild nosign. A.2.2. Установка Node из менеджера пакетов Если вы работаете в L inux и ли m acO S, возможно, вы предпочтете установить N ode при помощ и м енедж ера пакетов. Это реш ение упрощ ает обновление Node. Н ап р и ­ мер, если вы используете веб-сервер на базе Linux, вы мож ете установить N ode для автоматического получения обновлений безопасности. Н а сайте N ode разм ещ ен обш ирны й список ин струкций по установке для операци­ онны х систем, которы е предоставляю т N ode в ф орм ате пакета (https://nodejs.org/ en/download/package-manager/). 364 Приложение А. Установка Node Н априм ер, в D ebian и систем ах на базе U b u n tu вы мож ете получить N ode из реп о­ зи то р и я N ode-Source. О н имеет собственны й репози тори й на G itH u b с д о п ол н и ­ тельной ин ф орм аци ей (https://github.com/nodesource/distributions). В m acO S д л я у стан о в к и N o d e м ож н о в о с п о л ьзо в а ться м ен ед ж ером H om ebrew (http://brew.sh/). Е сли м енедж ер H om ebrew уж е установлен в ваш ей системе, д о ­ статочно вы полнить ком анду brew i n s t a l l node. П акет Node также доступен в D ocker H ub. Если добавить FROM node:argon в Dockerfile, в образе будет установлена LTS-версия Node. Б Автоматизированное извлечение веб-данных Извлечение веб-данных (w eb scraping), требую щ ее сочетания навы ков серверного и кли ен тского програм м ировани я, ориен ти рован о на при м ен ение програм м ны х средств для анализа веб-страниц и преобразования их в структурированные данные. Представьте, что вам поручено создать новую версию сайта, которы й в настоящ ее врем я п р ед ставляет собой набор старом одны х статически х H T M L -страниц. Вы хотите загрузить страницы , проан али зировать их и извлечь названия, описания, авторов и цены д ля всех книг. Д елать это вручную не хочется, поэтому вы пиш ете д ля вы полнен ия этой работы програм м у Node. Это и есть извлечение веб-данных. N ode отлично подходит д ля этой цели благодаря идеальном у балансу меж ду браузерной технологией и мощ ью сценарны х язы ков общего назначения. В этой главе вы узнаете, как использовать библиотеки разбора H T M L для извлечения полезных данны х по селекторам CSS и даж е вы полнен ия динам ических веб-страниц в п ро­ цессе Node. Б.1. Извлечение веб-данных И звлечением веб-данны х назы вается процесс и звлечения полезной ин ф орм ации с веб-сайтов. О бы чно д ля этого програм м а загруж ает необходимые страницы , р а з­ бирает их, а затем запраш ивает низкоуровневую разметку H T M L с использованием селекторов CSS или X Path. Результаты запросов экспортирую тся в ф айлы CSV или сохраняю тся в базе данных. Н а рис. Б.1 показано, как работает процесс извлечения веб-данны х от начала до конца. И звлечение веб-данных м ож ет противоречить условиям использования некоторых сайтов и з-за ценного кон тен та и ли ограничений по ресурсам. Е сли ты сячи п р о ­ грамм извлечения данны х обращ аю тся к одному сайту, которы й работает на старом и м едленном сервере, сайт м ож ет вы йти из строя. П реж де чем извлекать контент, необходимо убедиться в том, что у вас есть разреш ение на чтение и дублирование 366 Приложение Б. Автоматизированное извлечение веб-данных Загрузка страниц Разбор HTML Г <html> <body> --------- -</body> </html> Л { "html": { "body": <A {■■■} } } V J Рис. Б.1. Последовательность действий по извлечению и сохранению контента контента. Ф орм ально за этой ин ф орм аци ей м ож но обратиться к ф айлу robots.txt сайта ( www.robotstxt.org), но сначала следует связаться с владельцами сайта. А может быть, владельцы сайта сами предлож или вам проиндексировать информацию — на­ пример, в контексте более крупного кон тракта веб-разработки. В этом разделе вы узнаете, как разработчики прим еняю т извлечение данны х на р е­ альны х сайтах. Затем мы рассмотрим необходимые инструменты , обеспечиваю щ ие эф ф ективное прим енение N ode д ля и звлечения веб-данных. Б.1.1. Применение извлечения веб-данных О тл и ч н ы м п р и м ер о м и зв л еч ен и я веб-д ан н ы х сл уж и т в ер т и к а л ьн а я пои сковая систем а O c to p a rt (https://octopart.com/). O c to p a rt (рис. Б .2 ) ин декси рует данны е п р о и зв о д и те л е й и п р о д авц о в эл е к тр о н и к и , ч тоб ы п о л ь зо в ат ел я м бы ло прощ е н ай ти н у ж н ы е ком п оненты . Н ап р и м ер , вы м ож ете п ровести п ои ск резисторов на о сновани и в ел и ч и н ы сопр о ти вл ен и я, д оп уска на ном инал, класса м ощ ности и типа корпуса. Т акие сайты использую т веб-боты д л я загрузки контента, скрейперы д ля анализа контента и извлечения интересны х значений (например, вел и ч и ­ ны допуска), и внутренню ю базу данны х д ля хранения обработанной информ ации. Б.1. Извлечение веб-данных 367 Рис. Б.2. Octopart предоставляет средства поиска электронных компонентов Впрочем, извлечение веб-данны х используется не только д ля поисковы х систем. О но такж е п р и м ен я ется в р азв и в аю щ и х ся о б л астях теори и об раб отки дан ны х и в ж у р н ал и сти ке данны х. С п ец и ал и сты по ж у р н ал и сти ке дан н ы х и сп ользую т базы дан ны х д л я со зд ан и я историй, но п о ско льку огром ны е объемы дан ны х не хранятся в легкодоступны х форматах, они могут использовать такие инструменты, как извлечение веб-данных, д ля автом атизации сбора и обработки данных. Все это дает ж урн али стам возм ож ность представлени я ин ф орм ац и и новы м и способами, с прим енением средств визуализац ии данны х, вклю чая и н ф ограф ику и и н терак­ тивную графику. Б.1.2. Необходимые инструменты Д л я вы полнен ия работы по извлечению веб-данны х вам понадобится пара л егко ­ доступны х инструм ентов: браузер и Node. Б раузер — один из самы х полезны х и н ­ струментов извлечения данных; щ елкнув правой кнопкой м ы ш и и выбрав команду Исследовать элемент (Inspect Element), вы уж е сделали первы е ш аги на пути анализа сайта и преобразования его в необработанны е данные. С ледую щ им ш агом долж ен стать разбор страниц в N ode. В этой главе мы рассм отрим два типа парсеров: О облегченны й и неприхотливы й: cheerio; О поддерж иваю щ ий веб-стандарты и эм улирую щ ий м одель D O M : jsdom . 368 Приложение Б. Автоматизированное извлечение веб-данных О бе библиотеки устан авли ваю тся из npm . Возможно, вам потребуется разбирать ф орм аты данны х с нечеткой структурой, наприм ер даты. М ы кратко рассм отрим м одули Ja v a S c rip t D a te .p a rs e и M o m en tjs. В первом прим ере используется cheerio — простой и удобны й способ разбора ста­ тических веб-страниц. Б.2. Простейшее извлечение веб-данных с использованием cheerio Б иблиотека cheerio (www.npmjs.com/package/cheerio), написанная Ф еликсом Бемом (F elix B hm ), идеально подходит д ля и звлечения веб-данных, потому что она объ­ единяет два клю чевы х аспекта: бы стры й разбор H T M L и A P I в стиле jQ u e ry для запроса и вы полнен ия операций c разм еткой H TM L. П редставьте, что вам нуж но извлечь инф орм ацию о книгах на сайте издательства. У издателя еще нет A P I для получения ин ф орм аци и о книгах, поэтом у вам п р и ­ дется загрузить страницы с сайта и преобразовать их в результат в ф орм ате JSO N , вклю чаю щ ий в себя и м я автора и название книги. Н а рис. Б.3 показано, как работает извлечение веб-данны х в cheerio. В листинге Б.1 приведена небольш ая програм м а извл еч ен и я веб-данны х на базе cheerio. В нее вклю чен ф рагм ент разм етки H TM L, так что вам пока не нуж но бес­ покоиться о загрузке самой страницы. Загрузка страницы Разбор HTML Г <div class="book"> <h2>Catch-22</h2> <h3>Joseph Heller Л { "div": { "h2": {...} — } </div> } V J > node Index.js { title: "Catch-22", author: "Joseph Heller"} Вывод на консоль Запрос с использованием cheerio Рис. Б.3. Извлечение веб-данных с использованием cheerio Б.2. Простейшее извлечение веб-данных с использованием cheerio 369 Листинг Б.1. Извлечение информации о книге const html = ' -<-------- Определяет разметку HTMLдля разбора. <html> <body> <div class="book"> <h2>Catch-22</h2> <h3>Joseph Heller</h3> <p>A s a t ir i c a l indictment of m ilita ry madness.</p> </div> </body> </htm l>'; const cheerio = re q u ire ('c h e e rio '); const $ = cheerio.load(htm l); -<-------- Разбирает весь документ. const book = { t i t l e : $('.book h 2 ') .t e x t( ) , < -------author: $('.book h 3 ') .te x t( ) , d escrip tio n : $('.book p ') .t e x t ( ) }; Извлекает поля с использованием селекторов CSS. console.log(book); Л и сти н г Б.1 использует cheerio д ля разбора ж естко запрограм м ированного д оку­ м ента H T M L с использованием м етода .lo a d ( ) и селекторов CSS. В этом простом прим ере селекторы CSS просты и понятны , но реальная разм етка H T M L намного сложнее. К сожалению , вам неизбеж но придется столкнуться с плохо структури ­ рованн ой разм еткой H T M L , и ваш а кв ал и ф и к ац и я ан ал и ти ка веб-данны х будет определяться ваш им ум ением извлечь нуж ны е значения. А нализ плохой разм етки H T M L проходит в два этапа: сначала вы визуализируете документ, а затем определяете селекторы интересую щ их вас элементов. Д л я опре­ делен ия селекторов используется ф ункциональность cheerio. К счастью , соврем енны е браузеры предлагаю т простое реш ен ие д л я пои ска се ­ лекторов: если ваш браузер содерж ит средства разработчика, обычно вы мож ете щ елкнуть правой кнопкой м ы ш и и вы брать команду Исследовать элемент (Inspect Element). Вы увидите соответствую щ ую разм етку H TM L, а браузер такж е долж ен вы вести представление селектора для этого элемента. П редп олож им , вы пы таетесь и зв л еч ь и н ф о р м ац и ю о кн и ге с н еряш ли во н а п и ­ санного сайта, использую щ его таблицы без классов CSS. Р азм етка H T M L может вы глядеть так: <html> <body> <h1>Alex's Dated Book Website</h1> <table> <tr> <td><a href="/book1">Catch-22</a></td> <td>Joseph Heller</td> 370 Приложение Б. Автоматизированное извлечение веб-данных </tr> </table> </body> </html> Е сли вы откроете этот ф рагм ент в C hrom e и щ елкнете правой кнопкой м ы ш и на названии, вы увидите нечто похож ее на рис. Б.4. • •• С [ 3 іівЛ/иэѳ'а.,авх>Т)ос.т£п;а,'Зосаі‘оЭгіг£зсіізгк'стС5 sc'tclra'sHap. » = Alex's Dated Book Website caica joscjxi HcJic- Добавлено Chrome! Нет в реальной разметке HTML. >- О a. Т - f in is •л Упрощает поиск селекторов Cu-ipu'.eil » « .« м іг .к г у і* t 4, 1 . і .лг«с wesfcit-aiv-U"!! { ' cc .or: -wesfctt-U-tki l» * l < n » n U w i> * <tdv ■j i r * f V t o :* l"< »rr*-?}•:/■> 1 .......................• * « Vmc1*«K.'hl • - □ ------------ Flt*d In Styles Ccn»Se Seirei ‘ -lulstiir R«nc«-inc (5 Y <ггп J"arir>- t -tr^prvf <19 Рис. Б.4. Просмотр HTML в Chrome В белой полосе под H T M L вы водится текст «htm l body tab le tb o d y tr td a» — д о ­ вольно б ли зки й к нуж ном у селектору. Н о это не совсем прави льн о, потом у что в реальной разм етке H T M L нет tbody — этот элемент был вставлен Chrom e. Когда вы используете браузер для визуализации элементов, будьте готовы скорректировать свои результаты на основании настоящ ей используемой разм етки H TM L . Этот при­ мер показывает, что д ля получения н азван ия следует найти ссы лку внутри ячей ки таблицы , а д ля получения соответствую щ его автора взять следую щ ую я ч ей ку Допустим, приведенная выше разметка H T M L хранится в файле messy_html_example. html. Код из листинга Б.2 извлекает название, ссы лку и автора. Листинг Б.2. Обработка разметки HTML const const const const f s = r e q u i r e ( 'f s ') ; html = fs.readFileSync('./m essy_htm l_exam ple.htm l', 'u t f 8 ') ; cheerio = re q u ire ('c h e e rio '); Использует метод cheerio $ = cheerio.load(htm l); first() для получения const book = { t i t l e : $ ( 'ta b le t r td a ') . f i r s t ( ) . t e x t ( ) , -—---href: $ ( 'ta b le t r td a ') . f i r s t ( ) . a t t r ( 'h r e f ') , author: $ ( 'ta b le t r t d ') .e q ( 1 ) .te x t( ) }; Использует метод console.log(book); cheerio eq() для перехода ко второму элементу. Загружает HTML из файла. конкретной ссылки. Использует метод cheerio attr() для получения URL. Б.3. Обработка динамического контента с jsdom 371 Д л я загрузки H T M L используется м одуль fs; это делается д ля того, чтобы вам не приходилось вводить разм етку H T M L в примере. Н а практике источником данных м ож ет служ ить ж и вой веб-сайт, но данны е такж е могут загруж аться из ф ай л а или базы данны х. П осле того как р азб о р д о ку м ен та будет заверш ен, вы зов f i r s t ( ) используется д ля получения первой ячей ки таблицы со ссылкой. Д л я получения U R L -адреса ссы лки используется метод cheerio a t t r ( ) ; он возвращ ает конкретны й атрибут элемента, как и jQ uery. Такж е заслуж ивает вни м ан ия метод e q (); в этом листинге он пропускает первы й элем ент td , потому что им я автора содерж ится во втором элементе. ОПАСНОСТИ РАЗБОРА ВЕБ-ДАННЫХ С помощ ью такого модуля, как cheerio, вы сможете легко и быстро интерпре­ тировать веб-документы . Н о будьте осторож ны с типом контента, которы й вы пы таетесь обрабаты вать. Н апример, с двоичны м и данны м и м ож ет быть выдано исклю чение, так что использование модуля в веб-прилож ении может привести к аварийном у заверш ению процесса Node. М ожет быть рискованно, если програм м а и звл еч ен и я данны х встроена в процесс, об служ иваю щ ий ваш е веб-прилож ение. Л учш е проверить тип контента перед тем, как передавать его парсеру; такж е рассм отрите возм ож ность вы полнен ия программ ы и звлечения веб-данных в отдельны х процессах Node, чтобы снизить последствия лю бы х серьезны х сбоев. О дно из о граничений cheerio закл ю чается в том, что работать м ож но только со статической версией документа; cheerio используется д ля работы с «чисты ми» д о ­ кументам и H T M L , но не с динам ическим и страницами, использую щ ими Jav aS crip t на стороне клиента. В следую щ ем разделе вы узнаете, как использовать jsdom для м одели рован ия среды браузера в своих п ри лож ениях N ode д ля вы полнен ия кода Ja v a S c rip t на стороне клиента. Б.3. Обработка динамического контента с jsdom Б и блиотека jsdom — настоящ ая мечта специалиста по извлечению веб-данных; она загруж ает разм етку H T M L , и н терп ретирует ее в соответствии с моделью D O M типичного браузера и вы полняет кл и ен тски й код Jav a S c rip t. Вы м ож ете указать кли ен тский код Jav aS crip t, которы й необходимо вы полнить; обычно это означает вклю чение jQ uery. Э то означает, что вы м ож ете внедрять jQ u e ry (и л и ваш и соб ­ ственны е сценарии о тладки ) в лю бы е страницы . Н а рис. Б.5 показано, как jsdom сочетает H T M L и Jav aS crip t, чтобы получить доступ к контенту, обычно недоступ­ ному д ля извлечения. Приложение Б. Автоматизированное извлечение веб-данных 372 Загрузка HTML и JavaScript app.js Разбор HTML и выполнение JavaScript <div class="book"> <h2> class="title"> </h2> jquery.js </div> > node index.js { title: "Catch-22", author: "Joseph Heller"} Вывод на консоль Запрос Рис. Б.5. Извлечение веб-данных с использованием jsdom У jsdom также есть свои недостатки. Библиотека не является идеальной моделью бра­ узера, она работает медленнее cheerio, а парсер H TM L работает в жестком режиме, так что на страницах с плохо написанной разметкой могут происходить сбои. Впрочем, не­ которые сайты не имеют смысла без поддержки JavaScript на стороне клиента, поэтому jsdom остается неоценимым инструментом для некоторых задач извлечения веб-данных. О бы чно jsd o m используется через м етод jsdom .env. В листинге Б.3 продем онстри­ ровано применение jsdom для извлечения данных со страницы посредством анализа jQ u e ry и извлечения полезны х значений. Листинг Б.3. Извлечение данных с jsdom const Jsdom = re q u ire ('jsd o m '); const html = ' -<-------- Включает подходящий фрагмент HTML. <div class="book"> <h2>Catch-22</h2> <h3>Joseph Heller</h3> <p>A s a tir ic a l indictment of m ilitary madness.</p> </div> jsdom.env(html, ['./n o d e _ m o d u le s/jq u e ry /d ist/jq u e ry .js'], scrape); -<function sc rap e(err, window) { var $ = window.$; -<-------- Определяет удобный псевдонимдля объекта jQuery. $ ('.b o o k ').e a ch (fu n c tio n () { < -------- Перебирает книги методом jQuery $.each. var $el = $ (th is ); console.log({ t i t l e : $ e l .f in d ( 'h 2 ') .t e x t ( ) , Использует методы author: $ e l.f i n d ( 'h 3 ') .t e x t ( ) , jQueryдля извлечения d escrip tio n : $ e l .f i n d ( 'p ') .t e x t ( ) значений, относящихся }); к книге. }); } Разбирает документ изагружает jQuery. Б.3. Обработка динамического контента с jsdom 373 Ч тобы вы полнить ли сти н г Б.3, необходимо сохранить jQ u e ry локально и устан о­ вить jsd o m 1. И то и другое м ож но сделать в npm; м одули назы ваю тся jsdom (www. npmjs.com/package/jsdom) и jQ u e ry (www.npmjs.com/package/jquery) соответственно. А когда все будет сделано, этот код вы ведет название книги, автора и описание из ф рагм ента H TM L. М етод jsd o m .en v исп ользуется д ля разбора докум ента и внедрения jQ uery. В не­ дрен и е jQ u e ry о с у щ еств л я ется за гр у зк о й из npm , но вы такж е м ож ете указать U R L -адрес jQ u e ry в сети C D N и ли ваш ей ф ай ловой системе; jsdom будет знать, что делать. М етод jsd o m .e n v работает асинхронно; д л я работы ему необходим а ф ун кц и я обратного вызова. Э та ф у н кц и я получает объекты ош ибки и окна; для об­ ращ ен ия к докум енту используется объект окна. В этом прим ере д ля объекта окна определяется псевдоним, чтобы к нему м ож но было легко обращ аться в запи си $. С електор используется с методом jQ u e ry .each для перебора книг. В этом примере книга всего одна, но она демонстрирует, что методы обхода jQ u e ry действительно работают. Д л я обращ ения ко всем значениям из книги такж е использую тся методы обхода jQ uery. Л и сти н г Б.3 похож на прим ер д ля cheerio из листинга Б.1. П рин цип иальное р а з­ личие заклю чается в том, что jQ u e ry разбирается и вы полняется N ode в текущ ем процессе. В листинге Б.1 cheerio исп ользуется д ля предоставления аналогичной ф ункциональности, но cheerio предоставляет собственную прослойку, похож ую на jQ uery. Н а этот раз вы вы полняете код, предназначенны й д ля браузера, так, словно он действительно вы полняется в браузере. Метод jsdom. env полезен только при работе со статическими страницами. Д ля разбора страниц, использующ их клиентский код JavaScript, вместо него следует использовать jsdom .jsdom . Э тот синхронны й метод возвращ ает объект окна, с которы м можно работать другими средствами jsdom . В листинге Б.4 jsdom используется для разбора докум ента с тегом s c r i p t , а метод jsd o m .jQ u e ry ify упрощ ает извлечение данных. Листинг Б.4. Разбор динамической разметки HTML с использование jsdom const jsdom = re q u ire ('jsd o m '); const jqueryPath = './n o d e _ m o d u le s/jq u e ry /d ist/jq u ery .js'; -<-------- Задает путь jQuery. const html = ' <div class="book"> <h2></h2> < -------HTMLбез статических значений. <h3></h3> Сценарий, который динамически <script> вставляет значение. document.querySelector('h2').innerHTML = 'C atch-22'; -<---document.querySelector('h3').innerHTML = 'Joseph H e lle r'; </script> </div> 1 На момент написания книги 'текущей была версия jsdom 6.3.0. 374 Приложение Б. Автоматизированное извлечение веб-данных const doc = Jsdom.Jsdom(html); ■<-------- Создает объект, представляющий документ. const window = doc.defaultView; Jsdom.JQueryify(window, JqueryPath, function() { ■<-------- Вставляет jQuery в документ. var $ = window.$; $ ('.b o o k ').e a ch (fu n c tio n () { var $el = $ (th is ); console.log({ t i t l e : $ e l .f i n d ( 'h 2 ') .t e x t ( ) , ■<-------- Извлекает значения, связанные с книгой. author: $ e l.f in d ( 'h 3 ') .te x t( ) }); }); }); Д л я работы листинга кода Б.4 необходима установка jQ uery, и, если код создается вручную, вы долж ны создать новы й проект командами npm i n i t и npm i n s t a l l - -save jq u e ry jsdom. О н использует простой документ H TM L, в котором интересующ ие нас значения вставляю тся динам ически. В ставка осущ ествляется кли ен тским кодом Jav aS crip t из тега s c r ip t. Н а этот раз вместо jsdom .env используется jsdom.jsdom. О н вы полняется синхронно, потом у что объект докум ента создается в памяти, но ничего не делает до тех пор, пока вы не попытаетесь обратиться к нему с запросом или вы полнить какую-нибудь операцию. Д л я этого вы зов jsd o m .jQ u e ry ify используется для вставки конкретной вер си и jQ u e ry в д окум ент. П о сл е того как б и б л и о тек а jQ u e ry будет загруж ен а и заработает, в ы п о л н яется обратны й вы зов, которы й зап раш и вает у докум ента интересую щ ие вас зн ачения и вы водит их на консоль. Результат вы глядит так: { t i t l e : 'C atch-22', author: 'Joseph H eller' } Это доказывает, что библиотека jsdom вы звала необходимый код JavaS cript на сторо­ не клиента. Теперь представьте, что это реальная веб-страница — и вам станет ясно, почему библиотека jsd o m настолько мощна: она позволяет извлекать данны е даже с сайтов, построенны х с м иним ум ом разм етки H T M L на базе таких динам ических технологий, как A ngular и React. Б.4. Обработка «сырых» данных П осле того как п олезн ы е дан ны е будут н аконец-то п рочитаны со страницы , их необходимо обработать д ля сохранения в базе данны х или д ля экспортирования в таком формате, как CSV. И звлечен ны е данны е либо состоят из н еструктуриро­ ванного обычного текста, либо кодирую тся в микроф орм атах. М икроф орм аты — облегченны е ф орм аты данны х на основе разм етки, и сп ользуе­ мы е д ля таких данны х, как адреса, календари и собы тия, с тегами или клю чевы ми словам и. И н ф о р м ац и ю о вы р аботанн ы х м и кр о ф о р м атах м ож н о н ай ти на сайте microformats.org. П рим ер имени, представленного в м икроформате: <a class="h-card" href="http://example.com">Joseph Heller</a> Б.4. Обработка «сырых» данных 375 М икроф орм аты относительно просто разбираю тся; с cheerio и ли jsdom д ля и зв л е­ ч ен и я им ени Joseph H e lle r достаточно простого вы раж ени я вида $ ( '. h - c a r d ') . t e x t ( ) . П ростой текст потребует больш ей работы. В этом разделе вы увидите, как разобрать данны е и как преобразовать их в ф орм аты , более подходящ ие д ля баз данных. Н а больш инстве веб-страниц м икроф орм аты не использую тся. О дна из областей, в которы х си ту ац и я м ож ет создать проблем ы , но остается потенц иально у п р а в ­ ляем ой, — зн ач ен и я данны х. Д анны е м огут встречаться во м ногих ф орм атах, но в пределах одного сайта они обычно остаю тся последовательны м и. П осле того как ф орм ат будет определен, вы смож ете разобрать и отф орм атировать данные. В J a v a S c rip t им еется встроен ны й парсер данных: при вы полнен ии ком анды new D a te ('2 0 1 6 01 0 1 ') возвращ ается новы й экзем пляр Date, соответствую щ ий 1 я н ­ варя 2016 года. П оддерж иваем ы е входны е ф орм аты определяю тся методом Date. p a r s e , к о то р ы й б ази р у е тс я на стан д ар тах R F C 2822 (h ttp ://to o ls.ie tf.o rg /h tm l/ rfc2822#page-14) и л и IS O 8601 (w w w .w 3.org/TR/N O TE-datetim e). Т акж е могут сработать другие ф орм аты ; нередко стоит опробовать их с исходны м и данны м и и посмотреть, что получится. Другое реш ение основано на поиске значений в исходных данных с использованием регулярного вы раж ения, с последую щ им использованием конструктора Date для создания новы х объектов Date. К онструктор имеет следую щ ую сигнатуру: new Date(year, m o n th [,d ay [,h o u r[,m in u tes[,seco n d s[,m illis]]]]]); В озм ож ностей разб ора данны х в J a v a S c rip t обычно хватает д ля разны х случаев, но п ри р еф о р м ати р о в ан и и д ан н ы х его недостаточно. П ревосходн ы м реш ением проблемы может стать M om ent.js (http://momentjs.com) — библиотека разбора, про­ верки и ф орм атирован ия дат. M om ent.js обладает динам ичны м A PI, что позволяет использовать сцепленны е вы зовы следую щ его вида: moment().format("MMM Do YY"); / / Sep 7th 15 Это удобно д ля преобразования извлеченны х данны х в C SV -ф айлы , хорош о рабо­ таю щ ие во м ногих програм м ах (таких, как M icrosoft Excel). Представьте, что у вас им еется веб-страница с книгам и, вклю чаю щ ая в себя название и дату публикации. З н а ч е н и я нуж но сохранить в базе данны х, но ваш а база дан ны х требует, чтобы данны е бы ли отф орм атированы по схеме ГГГГ-М М -ДД. Л и сти н г Б.5 показывает, как использовать M om ent c cheerio д ля этой цели. Листинг Б.5. Разбор данных и генерирование CSV 'use s t r i c t '; const cheerio = re q u ire ('c h e e rio '); const fs = r e q u i r e ( 'f s ') ; const html = fs .re a d F ile S y n c ('./in p u t.h tm l'); < -------- Загружает входной файл. const moment = require('m om ent'); -<-------- Включает moment. 376 Приложение Б. Автоматизированное извлечение веб-данных const $ = cheerio.load(htm l); const books = $ ('.b o o k ') .m ap((i, e l) => { ■<-------Связывает каждуюкнигу с автором, названием идатой публикации. return { author: $ ( e l ) .f i n d ( 'h 2 ') .t e x t ( ) , t i t l e : $ ( e l) .f in d ( 'h 3 ') .t e x t ( ) , published: $ ( e l) .f in d ( 'h 4 ') .te x t( ) }; }) .ge t ( ) ; c o n s o le .lo g ( 'title , author, sourceDate, dbD ate'); books.forEach((book) => { le t date = moment(new D ate(book.published)); console.log( '%s, %s, %s, %s', book.author, b o o k .title , book.published, date.format('YYYY-MM-DD') ); }); -<-------- -<-------- Заголовки файла CSV. Разбирает дату. Д л я вы полнения листинга Б.5 необходимо установить cheerio, M om ent и ин ф орм а­ цию книг. П рограм м а получает разм етку H T M L (и з input.html) и вы водит данные CSV. В разм етке H T M L даты долж ны храни ться в элем ентах h4: <div> <div class="book"> <h2>Catch-22</h2> <h3>Joseph Heller</h3> <h4>11 November 1961</h4> </div> <div class="book"> <h2>A Handful of Dust</h2> <h3>Evelyn Waugh</h3> <h4>1934</h4> </div> </div> П осле того как програм м а загр у зи т входной ф айл, она загруж ает M om ent, а з а ­ тем связы вает каж дую книгу с просты м объектом Ja v a S c rip t при помощ и методов cheerio .map и .g e t. М етод .map перебирает книги, а обратны й вы зов извлекает каж ды й интересую щ ий вас элем ент при помощ и м етода обхода селекторов .fin d . Д л я получения итогового текста в виде м ассива используется м етод .g e t. Л и сти н г Б.5 вы водит данны е в ф орм ате C SV методом c o n s o le .lo g . С начала в ы ­ водится заголовок, а затем каж дая строка в цикле, перебираю щ ем все книги. Д аты преобразую тся в формат, совм естим ы й с M ySQ L; дата сначала разбирается вы ра­ ж ением new Date, а затем ф орм атируется средствами M om ent. Б.5. Заключение 377 О своивш ись с разбором и ф орм атированием дат, вы сможете прим енить аналогич­ ные методы к другим ф орм атам данных. Н апример, денеж ны е суммы и расстояния мож но извлекать с прим енением регулярн ы х вы раж ений, а затем ф орм атировать с прим енением более общ их библиотек числового ф орм атирован ия — таких, как N um eral (www.npmjs.com/package/numeral). Б.5. Заключение О И звлечен ие веб-данны х представляет собой автом атизированное преобразова­ ние веб-страниц (ин огда плохо структурированн ы х) в ф орматы , удобны е для ком пью терной обработки — CSV, базы данны х и т. д. О И звлечен ие веб-данны х при м ен яется вертикальны м и поисковы м и системами и в области ж урн али сти ки данных. О Если вы собираетесь извлечь данны е с сайта, сначала получите разреш ение. Д ля этого м ож но проверить содерж им ое ф ай л а robots.txt и связаться с владельцем сайта. О О сн о в н ы е и н стр у м ен ты — п ар сер ы стати ч еск о й р а зм е тк и H T M L (c h e e rio ) и парсеры, способные вы полнять Ja v a S c rip t (jsdom ); такж е пригодятся средства разработки браузера д ля нахож дения подходящ его селектора CSS д ля и н тере­ сую щ их вас элементов. О И ногда сам и данны е плохо отф орм атированы , и вам приходится разбирать т а ­ кие величины , как даты или денеж ны е суммы, чтобы их м ож но было сохранить в базе данных. Официально поддерживаемые промежуточные компоненты C o n n ect представляет собой м иним альную обертку д ля встроенны х м одулей к л и ­ ента и сервера H T T P в N ode. А вторы и участники проекта C o n n ect такж е предо­ ставляю т оф ици альн о поддерж иваем ы е пром еж уточны е ком поненты (m iddlew are co m p o n en ts) д ля р еал и зац и и н и зкоуровн евы х ф ункций, и сп ользуем ы х многими веб-ф рейм воркам и , вклю чая таки е операции, как обработка cookie, разбор тела, сеансы, базовая ау тен ти ф и кац и я и м еж сайтовая ф альси ф и кац и я запросов (CSRF, C ross-Site R equest Forgery). В этом при лож ении описаны все оф ици альн о поддер­ ж иваем ы е модули, чтобы вы м огли использовать их д ля построения облегченны х веб-п рилож ени й без более крупного ф рейм ворка. В.1. Разбор cookie, тел запросов и строк информационных запросов Я дро N ode не предоставляет м одулей, реализую щ их вы сокоуровневую ф у н кц и о ­ нальность веб-прилож ений, такую как синтакси ческий разбор cookie, б уф ер и за­ ц и я тел обы чны х запросов и ли си н так си ческ и й разбор строк и н ф орм аци онны х запросов, — все эти возм ож ности реализованы в м одулях C onnect. В этом разделе рассм атриваю тся четы ре м одуля д ля разбора данны х запросов: О cookie-parser — разбор cookie из веб-браузеров и сохранение результатов в re q . cookies; О qs — разбор строки U R L запроса в re q .q u e ry ; О b o d yP arser — использование и разбор тела запроса в req.body. Н ачнем с м одуля cookie-parser. О н упрощ ает получение данных, храним ы х б рау­ зером посетителя сайта, чтобы вы м огли прочитать статус авторизации, настройки веб-сайтов и т. д. В.1. Разбор cookie, тел запросов и строк информационных запросов 379 В.1.1. cookie-parser: разбор HTTP-cookie М одуль cookie-parser поддерж ивает разбор обы чны х cookie, подписанны х cookie и специ альны х cookie ф орм ата JS O N ( www.npmjs.com/package/cookie-parser ). По ум олчанию использую тся обы чны е неподписанны е cookie, при этом заполняется значениям и объект req .c o o k ie s. Если же вам нуж на поддерж ка подписанных cookie, затрудн яю щ ая м ан и п у л яц и и с cookie, то при создании экзем пляра c o o k ie -p a rs e r следует передать секретную строку. НАСТРОЙКА COOKIE НА СЕРВЕРЕ Модуль cookie-parser() не предоставляет никаких вспомогательных функций для на­ стройки исходящих cookie. Для этого нужно использовать функцию res.setHeader() с Set-Cookie в качестве заголовка. Среда Connect исправляет заданную по умолчанию функцию Node res.setHeader() таким образом, чтобы использовать заголовки Set-Cookie специального вида. В результате функция работает так, как надо. Обычные cookie Чтобы прочитать cookie, необходимо загрузить модуль, добавить его в стек промеж у­ точного П О, а затем прочитать cookie в запросе. В листинге В.1 продемонстрирован каж ды й из этих шагов. Листинг В.1. Чтение cookie, отправленных в запросе (1) Загружает промежуточный const connect = re q u ire ('c o n n e c t'); компонент cookie-parser. const cookieParser = re q u ire ('c o o k ie -p a rs e r'); •<---connect() .use(cookieParser()) < -------- (2) Добавляет его в стек промежуточных компонентов приложения. .u se((req , re s, next) => { res.end(JS O N .stringify(req.cookies)); < -------- (3) Отвечает строковой версией cookie. }) .listen (3 0 0 0 ); В этом ф рагм енте загруж ается пром еж уточны й ком понент (1 ). П омните, что для этого необходим о устан о ви ть ком п онент ком ан дой npm i n s t a l l c o o k ie - p a r s e r. З атем экзем пляр c o o k ie - p a r s e r добавляется в стек пром еж уточны х компонентов этого при лож ения (2 ). О стается сделать последний ш аг — вернуть cookie браузеру в строке (3 ), чтобы вы м огли убедиться в прави льн ости работы. П ри запуске этого прим ера необходимо назначить cookie с запросом. О ткры в адрес http://localhost:3000 в браузере, скорее всего, вы ничего не увидите; возвращ ается пустой объект ({}). Д л я назн ачени я cookie м ож но воспользоваться cURL: curl h ttp ://lo c a lh o st:3 0 0 0 / -H "Cookie: foo=bar, bar=baz" 380 Приложение В. Официально поддерживаемые промежуточные компоненты Подписанные cookie П одписанны е cookie лучш е всего использовать при передаче важ ны х данных, п о ­ скольку их целостность м ож но проверить д ля предотвращ ения атак через посред­ ника (m an-in-the-m iddle attack). Если проверка будет пройдена, подписанные cookie пом ещ аю тся в объект re q .s ig n e d C o o k ie s . Д ва отдельны х объекта использую тся д ля п рояснен ия нам ерений разработчика. Если бы подписанны е и неподписанны е cookie хранились в одном объекте, обы чны е cookie м ож но было бы изм енить таким образом, чтобы им итировать подписанны е cookie. С о д е р ж и м о е п о д п и с а н н о г о c o o k ie м о ж е т в ы г л я д е т ь п р и м е р н о так : t o b i . DDm3AcVxE9oneYnbmpqxoy[...]1; слева от точки (.) находится значение cookie, а сп ра­ ва — хеш-код, сгенерированны й сервером по алгоритму SH A -256 в ф орм ате H M AC (H a sh -b a se d M essage A u th e n tic a tio n C ode). Е сли C o n n e c t пред п ри м ет поп ы тку убрать подпись cookie-ф айла, ничего не получится, поскольку окаж ется изм ен ен ­ ны м значение или H M A C -код. П редполож им , например, что в ваш ем р аспоряж ени и им еется подписанны й объ­ ект cookie с клю чом name и значением luna. Вызов c o o k ie P a rse r закодирует cookie таким образом, что получится значение s:luna.PQLM0wNvqOQEObZX[...]. Х еш -часть проверяется д ля каж дого запроса, и, если cookie отправляется в неизм енном виде, к данны м м ож но обратиться в виде req .sig n edC ookies.nam e: $ curl h ttp ://lo c a lh o st:3 0 0 0 / -H "Cookie: name=s:luna.PQLM0wNvqOQEObZXU[...]" {} { name: 'lu n a ' } GET / 200 4ms Е сли и зм ен ить зн ачение cookie, как в следую щ ей ком анде c u rl, cookie name будет доступно в зап и си re q .c o o k ie s .n a m e , потом у что оно не прош ло проверку. П ри этом оно м ож ет при годиться д ля отладки и ли сп еци ф и чески х целей прилож ения: $ curl h ttp ://lo c a lh o st:3 0 0 0 / -H "Cookie: name=manny.PQLM0wNvqOQEOb[...]" { name: 'manny.PQLM0wNvqOQEOb[...]' } {} GET / 200 1ms В первом аргументе c o o k ie P a rse r передается секретная строка, исп ользуем ая для п од пи сы ван ия cookie. В листинге В.2 исп ользуется строка «tobi is a cool ferret». Листинг В.2. Разбор подписанных cookie const connect = re q u ire ('c o n n e c t'); const cookieParser = re q u ire ('c o o k ie -p a rs e r'); const secret = 'to b i is a cool f e r r e t '; 1 Значение сокращено для эко номии места. В.1. Разбор cookie, тел запросов и строк информационных запросов 381 const connect = re q u ire ('c o n n e c t'); const cookieParser = re q u ire ('c o o k ie -p a rs e r'); const secret = 'to b i is a cool f e r r e t '; (1) Подписанные cookie автоматически connect ( ) добавляются в объект запроса. .use(cookieP arser(secret)) <.---.u se((req , res) => { c o n so le.lo g ('C o o k ies:', req.cookies); console.log('Signed c o o k ie s:', req.signedCookies); (2) Обращается r e s .e n d ( 'h e llo \n '); к подписанным cookie } ).liste n (3 0 0 0 ); из объекта запроса. В этом п р и м ер е п о д п и сан н ы е cookie р а зб и р а ю т ся авто м ати ч ески , п отом у что пром еж уточном у ком поненту c o o k ie P a rse r был передан аргумент s e c r e t (1 ). О б ­ ращ ение к значениям происходит через объект re q u e s t (2 ). М одуль cookie-parser такж е о ткры вает доступ к ф у н кц и о н ал ьн о сти разб ора cookie, п ред оставляем ой через методы signedC ookie и signedC ookies. П реж де чем двигаться дальш е, посмотрим, как использовать этот пример. Как и в случае с листингом В.1, д ля отправки cookie можно воспользоваться командой c u rl с клю чом -H. Н о чтобы объект cookie можно было считать подписанным, он должен быть закодирован определенны м образом. М одуль Node crypto используется для отмены подписи cookie методом signedCookie. Если вы хотите подписать cookie для тестирования листинга В.2, установите cookiesignatu re и подпиш ите строку с той ж е секретной строкой: const signature = re q u ire ('c o o k ie -sig n a tu re '); const message = 'lu n a '; const secret = 'to b i is a cool f e r r e t '; console.log(signature.sign(m essage, s e c re t); Теперь в случае изм ен ен ия подписи и ли сообщ ения сервер узнает об этом. Кроме подписанны х cookie модуль поддерж ивает cookie в кодировке JS O N . В следующем разделе показано, как работает этот вариант. Cookie в формате JSON С пециальны е cookie в ф орм ате JS O N снабж аю тся преф иксом j : , которы й р асп оз­ нается C o n n ect как сериализуем ы й ф орм ат JS O N . Cookie в ф орм ате JS O N могут быть как подписанны ми, так и неподписанны м и. Т аки е ф р е й м в о р к и , к а к E x p ress, м о гу т и с п о л ь з о в а т ь это т ф о р м ат д л я с о зд а ­ ни я и н ту и ти вн о понятного и н тер ф ей са д л я работы с cookie — вм есто того, чтобы вы п о л н ять сериализаци ю вручную и разб и р ать зн ач ен и я cookie в ф орм ате JS O N . С л ед у ю щ и й п р и м ер д ем о н стр и р у ет, как C o n n e c t р а зб и р а ет cookie в ф о рм ате JS O N : 382 Приложение В. Официально поддерживаемые промежуточные компоненты $ curl h ttp ://lo c a lh o st:3 0 0 0 / -H 'Cookie: foo=bar, bar=j:{"foo":"bar"}' { foo: 'b a r ', bar: { foo: 'b a r' } } {} GET / 200 1ms К ак упоминалось ранее, JSO N -cookie также могут быть подписанными, как показано в следую щ ем примере: $ curl h ttp ://lo c a lh o st:3 0 0 0 / -H "Cookie: cart=j:{\"items\":[1]}.sD5p6xFFBO/4ketA1OP43bcjS3Y" {} { c a rt: { items: [ 1 ] } } GET / 200 1ms Настройка исходящих cookie К ак уж е отм ечалось, м одуль cookie-parser не предлагает ни какой ф у н к ц и о н ал ь­ ности д ля запи си исходящ их заголовков д ля H T T P -клиента с помощ ью заголовка S e t-C o o k ie . В C o n n e c t предусм отрена я в н а я поддерж ка нескольки х заголовков S et-C ookie, обеспечиваем ая ф ункц ией r e s .s e t- H e a d e r ( ) . П редполож им , что нуж но задать д ля cookie с именем foo строковое значение bar. В C o n n ect эту операцию м ож но вы полнить с помощ ью единственной строки кода, в которой в ы зы в ается ф у н к ц и я r e s .s e t H e a d e r ( ) . М ож но такж е у стан авли вать зн ач ен и я разли ч н ы х парам етров, им ею щ их отнош ение к cookie (напри м ер, срок д ей стви я), как показано во втором вы зове s e t-H e a d e r(): var connect = re q u ire ('c o n n e c t'); connect() .u se((req , res) => { res.setH eader('S et-C ookie', 'fo o = b ar'); res.setH eader('S et-C ookie', 'to b i= fe rre t; Expires=Tue, 08 Jun 2021 10:18:14 GMT' ); res.en d (); }) .listen (3 0 0 0 ); М ож но проверить заголовки, передаваем ы е сервером в ответ на запрос H TTP. Д ля вы полнен ия этой операции используется ф лаг --h e ad ком анды c u rl. К ак видите, заголовки S et-C o o k ie устан авли ваю тся требуем ы м образом: $ curl h ttp ://lo c a lh o st:3 0 0 0 / --head HTTP/1.1 200 OK Set-Cookie: foo=bar Set-Cookie: to b i= fe rre t; Expires=Tue, 08 Jun 2021 10:18:14 GMT Connection: keep-alive Вот и все, что относится к способам передачи cookie с ответом HTTP. В cookie можно хранить произвольны е текстовые данные, но чащ е всего на стороне сеанса хранится В.1. Разбор cookie, тел запросов и строк информационных запросов 383 один объект cookie д ля каж дого сеанса, чтобы сервер мог получить полную и н ф о р ­ мацию о состоянии пользователя. Требуемый м еханизм ин капсулирован в модуле express-session, которы й рассм атривается далее в этом прилож ении. Теперь, когда вы научились работать с cookie, вам, вероятно, не терпится взяться за другие стандартны е методы получения пользовательского ввода. В следую щ их двух разделах рассм атривается разбор строк и тел запросов; вы узнаете, что, хотя C o n n ect работает на достаточно низком уровне, мож но воспрои звести ф у н кц и о ­ нальность более слож ны х веб-ф рейм ворков без написания больш их объемов кода. В.1.2. Разбор строк запросов О дин из способов получения ввода основан на и сп ользован ии парам етров GET. П осле U R L -адреса став и тся во п р о си тел ьн ы й знак, за которы м следует список аргументов, разделенны х сим волам и &: http://localhost:3000/page?nam e=tobi& species=ferret U R L -адреса такого типа могут предоставляться ваш ему прилож ению ф орм ой, н а­ строенной на использование метода GET, или якорн ы м и элем ентам и в ш аблонах прилож ения. В ероятно, вы уж е видели, как этот способ передачи данны х п ри м е­ няется д ля разбивки вы вода на страницы . О бъект запроса, передаваем ы й каж дом у пром еж уточном у ком п оненту в п р и л о ­ ж ен и ях C onnect, вклю чает в себя свойство u r l, но в данном случае вас интересует п осл ед н яя часть U R L: только сим волы , следую щ ие за вопроси тельны м знаком. В поставку N ode входит м одуль разбора U RL, так что ф орм ально д ля получения нуж ной строки мож но воспользоваться методом u r l.p a r s e . Н о C onnect такж е п ри­ ходится разбирать URL, и разобранная версия сохраняется во внутреннем свойстве. Д л я разбора строк запросов рекомендуется использовать модуль qs (www.npmjs.com/ package/qs). Этот модуль оф ициально не поддерж ивается Connect; альтернативные реш ен и я доступны через npm . Ч тобы использовать модуль qs и другие похож ие модули, вы зовите его метод .p a r s e ( ) из своего промеж уточного компонента. Простой пример использования В листинге В.3 метод q s .p a r s e используется д ля создания объекта, хранящ егося в свойстве re q .q u e ry , д ля исп ользован ия последую щ им и пром еж уточны м и ко м ­ понентами. Листинг В.3. Разбор строк запросов const connect = re q u ire ('c o n n e c t'); const qs = r e q u ir e ( 'q s '); connect() .u se((req , re s, next) => { 384 Приложение В. Официально поддерживаемые промежуточные компоненты console.log(req._parsedU rl.query); req.query = qs.parse(req._parsedU rl.query); < --- (1) Использует qs для разбора строки запроса. next(); }) .u se((req , res) => { console.log('query s t r i n g : ', req.query); r e s .e n d ( '\n ') ; }) .listen (3 0 0 0 ); < -------- Выводит разобранную строку. В этом примере нестандартны й промежуточны й компонент используется для полу­ чен и я разобранного U R L -адреса, его разбора с использованием q s .p a r s e (1) и его последую щ его вы вода в следую щ ем компоненте. П редполож им , вы проектируете прилож ение д ля построения м узы кальн ой ф о н о ­ теки. В при лож ение м ож но вклю чить поисковую систему и использовать строку запроса д ля ф о р м ирования парам етров поиска: /songSearch?artist=Bob%20Marley&track=Jammin. В этом прим ере будет получен объект re s .q u e ry следую щ его вида: { a r t i s t : 'Bob M arley', track : 'Jammin' } М етод q s .p a r s e поддерж ивает влож енны е массивы , так что слож ны е строки з а ­ просов, такие как ?im ages[]= foo.png& im ages[]= bar.png, будут создавать объекты следую щ его вида: { images: [ 'fo o .p n g ', 'b ar.p n g ' ] } Если в запросе H T T P параметры строки запроса не заданы (например, /song-Search), re q .q u e ry по ум олчанию содерж ит пустой объект: {} Ф р ей м во р ки более вы сокого уровня, такие как Express, обычно содерж ат встроен­ ные средства разбора строк запросов, потому что эта задача очень часто встречается в веб-разработке. Еще одна стандартная ф у н кц и я веб-ф рейм ворков — разбор тел запросов д ля получения данных, отправленны х формами. С ледую щ ий раздел объ­ ясняет, как разбирать тела запросов, работать с ф ормами и отправленными ф айлами и как убедиться в безопасности таких запросов. В.1.3. body-parser: разбор тел запросов Б ольш ин ство веб-п рилож ени й получает и обрабаты вает пользовательский ввод. Д анны е могут поступать от ф орм или даж е от других програм м (в случае R E STсовм естимы х A P I). Запросы и ответы H T T P назы ваю тся сообщениями HTTP. Ф о р ­ мат сообщ ения состоит из списка заголовков и тела сообщ ения. В веб-прилож ениях Node тело обычно представляет собой поток данных и может кодироваться разными В.1. Разбор cookie, тел запросов и строк информационных запросов 385 способами: запрос P O S T от ф орм ы обычно кодируется в ф орм ате a p p lic a tio n /x www-form-urlencoded, а R E S T -совм естим ы й запрос JS O N мож ет быть закодирован в ф орм ате a p p lic a tio n /js o n . Это означает, что прилож ениям C onnect необходимы промеж уточны е компоненты, способные декодировать потоки данны х в кодировке формы, JS O N или даже в ф ор­ мате сж аты х данны х с прим енением gzip и ли deflate. В этом разделе мы покажем, как реш аю тся следую щ ие задачи: О обработка ввода от форм; О разбор запросов JS O N ; О проверка тел запросов на основании контента и размера; О получение отправленны х файлов. Формы П редполож им , нуж но п р и н ять регистрац ионн ую и н ф орм аци ю д л я ваш его п р и ­ л о ж ен и я от ф орм ы . Д л я реш ен и я этой задачи достаточно пом естить ком понент body-parser ( www.npmjs.com/package/body-parser ) перед лю бы м другим пром еж у­ точны м компонентом, которы й будет обращ аться к объекту req.body. Н а рис. В.1 показано, как это происходит. Пользователь отправляет форму First name Last name Email Password bodyParser S Распаковка сжатых данных Разбор значений формы в req.body Передача управления другим компонентам Рис. В.1. Обработка формы модулем body-parser 386 Приложение В. Официально поддерживаемые промежуточные компоненты Л и сти н г В.4 дем онстрирует использование м одуля body-parser с P O S T -запросам и H T T P от форм. Листинг В.4. Разбор запросов форм const connect = re q u ire ('c o n n e c t'); const bodyParser = re q u ire ('b o d y -p a rse r'); (1) Добавляет body-parser в стек промежуточных компонентов. connect() .use(bodyParser.urlencoded({ extended: fa ls e })) .u se((req , re s, next) => { res.setH eader('C ontent-T ype', 't e x t /p l a i n ') ; res.end('You sent: ' + JSON.stringify(req.body) + '\ n ') ; (2) Возвращает тело }) запроса в виде строки. .listen (3 0 0 0 ); Ч то бы и сп о льзо вать этот прим ер, необходим о устан ови ть м одуль b o d y -p a rse r1, а затем вы дать простой запрос H T T P с телом в U R L -кодировке. С ам ы й простой способ — использование c u r l с клю чом -d: curl -d name=tobi h ttp ://lo c a lh o st:3 0 0 0 В результате сервер долж ен вы вести текст You s e n t: { "n a m e":"to b i"} . Ч тобы этот способ работал, необходимо добавить body-parser в стек промеж уточны х компонен­ тов (1), а затем разобранное тело в req.body преобразуется в строку (2 ) для удобства вывода. П арсер u rlen co d ed получает строку в кодировке U T F -8 и автоматически распаковы вает тела запросов, закодированны е gzip и ли deflate. В этом прим ере парсеру тела передаю тся парам етры ex ten d ed : f a l s e . Когда этот парам етр равен tr u e , парсер использует другую библиотеку д ля разбора ф орм ата строки. Это позволяет использовать более слож ны е влож енны е J S O N -подобные объекты в формах. Д ругие парам етры представлены в следую щ ем разделе, п освя­ щ енном проверке запросов. Проверка запросов К аж ды й парсер, поставляем ы й с м одулем body-parser, поддерж ивает два реж има проверки запросов: l i m i t и v e r if y . Реж им l i m i t позволяет блокировать запросы с разм ером больш е некоторого порога: по умолчанию этот разм ер равен 100 Кбайт, однако его м ож но увеличить, если вы захотите получать ф орм ы больш его размера. Если вы создаете блог, систему управлени я контентом или что-нибудь в этом роде, где пользователь теоретически м ож ет вводить действительны е, но очень длинны е данные, такая возм ож ность будет очень полезной. Реж им v e r if y позволяет использовать ф ункцию для проверки запросов. Например, она пригодится, если вы хотите получить низкоуровневое тело запроса и убедиться 1 Мы использовали версию 1.11.0. В.1. Разбор cookie, тел запросов и строк информационных запросов 387 в том, что оно имеет прави льн ы й формат. С каж ем, вы мож ете воспользоваться ею и убедиться в том, что методы A PI, получаю щ ие XM L, всегда начинали с п р ави л ь­ ного заголовка XM L. В листинге В.5 продем онстрировано исп ользован ие обоих параметров. Листинг В.5. Проверка запросов форм const connect = re q u ire ('c o n n e c t'); const bodyParser = re q u ire ('b o d y -p a rse r'); function verifyR equest(req, re s, buf, encoding) { i f (!b u f.to S trin g ().m a tc h (/Aname=/)) { throw new Error('Bad fo rm at'); -<-------- (1) Вьщает ошибку в случае неправильного формата. } } connect() .use(bodyParser.urlencoded({ extended: fa ls e , lim it: 10, < -------- (2) Задает лимит запроса. v erify : verifyRequest -<-------- (3) Добавляет функцию проверки. })) .use(fu n ctio n (req , re s, next) { res.setH eader('C ontent-T ype', 't e x t/ p la i n ') ; res.end('You sent: ' + JSON.stringify(req.body) + '\ n ') ; }) .listen (3 0 0 0 ); О б р ати те вни м ан ие: объект E r r o r и н и ц и и р у ет ся клю чевы м словом th ro w (1 ). М одуль b ody-parser настраивается д ля перехвата этих ош ибок перед обработкой запроса, поэтом у ош ибка будет возвращ ена C onnect. П осле того как ф ун кц и я п ро­ верки запроса будет создана, вы долж ны передать ее пром еж уточном у компоненту body-parser посредством исп ользован ия реж и м а v e r if y (3). Л и м и т разм ера тела задается в байтах; здесь он достаточно м ал — всего 10 байт (2 ). Ч тобы увидеть, что происходит при слиш ком больш ом запросе, используйте ком анду c u r l с больш им значением name. К роме того, если вы хотите узнать, что произойдет при вы даче ош ибки проверки, используйте c u r l для отправки другого зн ачения вместо name. Для чего нужно ограничение? А теперь посмотрим, как злоум ы ш ленник может вывести из строя уязвим ы й сервер. С начала создадим небольш ое прилож ение C o n n ect с именем server.js. Это п р и л о ­ ж ение делает очень мало: оно разбирает тело запроса с помощ ью промеж уточного ком понента b o d y P a rse r(): const connect = re q u ire ('c o n n e c t'); const bodyParser = re q u ire ('b o d y -p a rse r'); connect() 388 Приложение В. Официально поддерживаемые промежуточные компоненты .use(bodyParser.json({ lim it: 99999999, extended: fa lse })) .u se((req , re s, next) => { res.end('O K \n'); }) .listen (3 0 0 0 ); А теперь создайте ф айл dos.js, представленны й в следую щ ем листинге. К ак видите, хакер м ож ет в о сп о л ьзо ваться H T T P -кл и ен том в N ode д л я атаки приведенного выш е при лож ения C onnect. Д л я организации подобной атаки достаточно записать несколько м егабайтов данны х JS O N . const h ttp = r e q u ir e ( 'h ttp ') ; l e t req = h ttp .req u est({ method: 'POST', Сообщает серверу о том, что port: 3000, выотправляете данные JSON. headers: { 'Content-Type': 'a p p lic a tio n /jso n ' } }); r e q .w r i t e ( '[ ') ; < -------- Начинает отправку большого объекта массива. l e t n = 300000; while (n-- ) { re q .w r ite ( '" fo o " ,') ; -<-------- Массив содержит 300 000 строк «foo». } r e q .w r ite ( '" b a r " ] ') ; req.end(); Зап у сти те сервер и вы полните сценарий атаки: $ node s e rv e r.js & $ node d o s.js П онаблю дав за процессом node в to p (1 ), вы увидите, что во врем я вы полнен ия dos. js он начинает использовать больш е ресурсов процессора и оперативной памяти. Конечно, это плохо, но зато теперь вы понимаете, почему пром еж уточны е ком п о­ ненты разбора тела поддерж иваю т реж им lim it. Разбор данных JSON К аж дому разработчику, которы й создает веб-прилож ения с Node, приходится часто иметь дело с JS O N . П арсер JS O N м одуля b o dy-parser поддерж ивает ряд полезны х параметров, которы е вы видели в преды дущ их примерах. Л и сти н г В.6 показывает, как происходит разбор разм етки JS O N и обработка полученны х значений. Листинг В.6. Проверка запросов форм const connect = re q u ire ('c o n n e c t'); const bodyParser = re q u ire ('b o d y -p a rse r'); connect() .use(bodyParser.json()) < -------- (1) Добавляет парсер JSON. .u se((req , re s, next) => { В.1. Разбор cookie, тел запросов и строк информационных запросов 389 res.setH eader('C ontent-T ype', 'a p p lic a tio n /js o n '); res.end('Name: ${req.body.name}\n'); -<-------- (2) Получает значение из объекта body. }) .listen (3 0 0 0 ); П осле того как парсер JS O N будет загруж ен (1 ), ваш и обработчики запросов могут интерпретировать значение r e q . body как объект Ja v a S c rip t вместо строки. Д анны й прим ер предполагает, что был отправлен объект JS O N со свойством name, и в о з­ вращ ает в ответе значение (2 ). Это означает, что у запроса заголовок C ontent-Type долж ен содерж ать a p p l i c a tio n /js o n , а вы долж ны отправить действительную р аз­ м етку JS O N . По ум олчанию пром еж уточны й ком понент jso n использует строгий режим разбора, но вы можете смягчить требования к кодировке, присвоив параметру значение f a l s e . НАЗНАЧЕНИЕ ЗАГОЛОВКА CONTENT-TYPE ДЛЯ JSON О дин из параметров, о котором вам необходимо знать, — type. О н позволяет изм ен ить тип разбираем ого кон тен та на JS O N . В следую щ ем прим ере и с ­ пользуется значение по ум олчанию application/json. Н о в некоторы х случаях прилож ению приходится взаим одействовать с клиентам и H TTP, которы е не отправляю т этот заголовок; будьте вним ательны . Следую щ ий запрос c u r l м ож ет использоваться для отправки данны х прилож ению , котор о е о тп р ав л яет объект J S O N со свойством usernam e, к о тором у при своен о значение to b i: curl -d '{"nam e":"tobi"}' -H "Content-Type: ap p lication/json" h ttp ://lo c a lh o st:3 0 0 0 Name: to b i Разбор многосекционных данных <form> М одуль b ody-parser не поддерж ивает м ногосекционны е (m u ltip a rt) тела запросов. Д л я по д дер ж ки о тп р ав ки ф ай л о в необходим о об раб аты вать м н огосекц и он н ы е сообщ ения, поэтом у д ля таких операций, как отправка аватара пользователя, п о ­ надобится м ногосекционная поддержка. О ф и ц и альн о поддерж иваем ого м ногосекционного парсера д ля C o n n ect не сущ е­ ствует, но н еко то р ы е п о п у л я р н ы е р еш ен и я кач ествен н о соп ровож даю тся. Д ва прим ера — busboy (www.npmjs.com/package/busboy) и m u ltip arty (www.npmjs.com/ package/multiparty). У обоих модулей им ею тся соответствую щ ие м одули Connect: connect-b u sb o y и connect-m ultiparty. Это происходит из-за того, что сами м ного­ секционны е парсеры зависят от низкоуровневы х H T T P -модулей Node, что п озво­ ляет использовать их с разны м и ф рейм воркам и, не ограничиваясь исклю чительно Connect. 390 Приложение В. Официально поддерживаемые промежуточные компоненты Л истин г В.7 использует m u ltip arty и вы водит инф ормацию об отправленном файле на консоль. Листинг В.7. Обработка отправленных файлов const connect = re q u ire ('c o n n e c t'); const m ultipart = re q u ire ('c o n n e c t-m u ltip a rty '); connect() .u se (m u ltip a rt()) <-------- (1) Добавляет промежуточный компонент multiparty. .u se((req , re s, next) => { c o n s o le .lo g (re q .file s ); < -------- (2) Выводит информацию об отправленных файлах. res.end('U pload re c e iv e d \n '); }) .listen (3 0 0 0 ); Этот короткий пример добавляет промеж уточный компонент m ultiparty (1), а затем вы водит полученны е ф айлы (2 ). Ф ай л ы будут загруж ены в каталог для временных ф айлов, поэтом у вам придется воспользоваться м одулем fs д ля удален ия файлов, когда прилож ение закончит работать с ними. Ч тобы использовать этот прим ер, убедитесь в том, что у вас устан овлен модуль c o n n e c t-m u ltip arty 1. Затем запустите сервер и отправьте ему ф айл ком андой c u rl с клю чом -F: curl -F file=@ index.js h ttp ://lo c a lh o st:3 0 0 0 И м я ф айла разм ещ ается после сим вола @и снабж ается преф иксом — именем поля. И м я поля используется в ф айле r e q . f i l e s , чтобы вы м огли отличить один загр у ­ ж енны й ф айл от другого. П римерны й вывод прилож ения показан ниже. Как видите, значение r e q .f i l e s . f i l e . path будет доступно д ля вашего прилож ения, и вы сможете переим еновать ф айл на диске, передать данны е д ля обработки, отправить их в сеть доставки контента или вы полнить лю бы е другие операции, нуж ны е ваш ему прилож ению : { fieldName: 'f i l e ' , originalFilenam e: 'in d e x .js ', path: '/var/folders/d0/_jqj3lf96g37s5w rf79v_g4c0000gn/T/60201-p4pohc.js', headers: { 'c o n te n t-d is p o s itio n ': 'form -data; name="file"; filenam e= "index.js"', 'c o n te n t-ty p e ': 'a p p lic a tio n /o c te t-stre a m ' }, В озможно, вас заинтересует, как организовать сж атие ответов. Н иж е рассказано о том, как пром еж уточны й ком понент сж атия помож ет снизить нагрузку на канал и создать впечатление повы ш енной скорости отклика прилож ения. 1 Для тестирования в этом примере использовалась версия 1.2.5. В.1. Разбор cookie, тел запросов и строк информационных запросов 391 В.1.4. Сжатие ответов Вероятно, в преды дущ ем разделе вы обратили вним ание на то, что парсеры могут расп аковы вать запросы , исп ользую щ ие gzip и л и deflate. В поставку N ode входит базовы й м одуль сж ати я данны х, которы й н азы вается zlib; он и сп ользуется для р еал и зац и и м етодов как сж атия, так и р аспаковки. П ром еж уточн ы й ком понент сж ати я (www.npmjs.com/package/compression) м ож ет и сп ользоваться д л я сж атия исх о д ящ и х ответов; это означает, что данны е, о тп р ав л яем ы е ваш им сервером , м огут сж им аться. Сервис Google PageSpeed Insights рекомендует использовать сжатие gzip1; а если вы присм отритесь к запросам, вы даваем ы м ваш им браузером, в средствах разработ­ чика, вы увидите, что многие сайты возвращ аю т сж аты е ответы. С ж атие повыш ает нагрузку на процессор, но такие ф орм аты , как обы чны й текст и H T M L , хорош о сж им аю тся; сж атие м ож ет улучш ить бы стродействи е ваш его сайта и сократить нагрузку на канал. DEFLATE ИЛИ GZIP? Н аличие двух вариантов сж атия м ож ет вы звать недоумение. К акой из двух вариантов лучш е, и вообщ е зачем нуж ны два варианта? В соответствии со стандартам и (R F C 1950 и R F C 2616), оба варианта использую т один ал го­ ритм, но различаю тся по способу обработки заголовка и контрольной суммы. К сож алению , некоторы е браузеры некорректно обрабаты ваю т deflate, п о ­ этом у в общ ем случае лучш е и сп о льзо вать gzip. П ри разборе ж ел ательн о поддерж ивать оба варианта, но если вы сж имаете вы вод своего сервера — используйте gzip д ля больш ей надежности. М одуль сж атия обнаруж ивает приним аем ы е кодировки по полю заголовка A cceptEncoding. Е сли поле отсутствует, и сп о л ьзу ется то ж д ествен н ая ко д и р о вк а (это означает, что ответ не и зм ен яется). Е сли ж е поле содерж ит g z ip и (и л и ) d e f la te , ответ сж имается. Простой пример использования О бы чно ком понент сж атия следует разм ещ ать вы соко в стеке C onnect, потому что он служ ит оберткой д ля методов r e s . w r i t e ( ) и r e s .e n d ( ) . В следую щ ем прим ере продем онстрировано сж атие контента: const connect = re q u ire ('c o n n e c t'); const compression = require('com pression'); 1 См. https://developers.google.com/speed/docs/insights/EnableCompression. 392 Приложение В. Официально поддерживаемые промежуточные компоненты connect() .use(compression({ threshold: 0 })) .u se((req , res) => { res.setH eader('C ontent-T ype', 't e x t /p l a i n ') ; res.en d ('T h is response is com pressed!\n'); }) .listen (3 0 0 0 ); Д л я запуска этого при м ера необходим о устан овить м одуль com pression из npm. З атем зап у сти те сервер и п оп робуй те вы дать запрос c u r l, в котором заголовку A ccept-E ncoding при сваивается значение gzip: $ curl h ttp ://lo c a lh o st:3 0 0 0 - i -H "Accept-Encoding: gzip" Аргумент - i приказы вает cU R L вывести заголовки, чтобы вы могли убедиться в том, что C ontent-E ncoding присвоено значение gzip. Вывод вы глядит невразумительно, потом у что сж аты е данны е не состоят из стандартны х символов. П опробуйте об­ работать их gunzip с клю чом - i , чтобы просм отреть вывод: $ curl h ttp ://lo c a lh o st:3 0 0 0 -H "Accept-Encoding: gzip" | gunzip Э то м о щ н ы й и о т н о с и т е л ь н о н е с л о ж н ы й в р е а л и за ц и и м ех а н и зм , но не с т о ­ ит по л агать , что в ам в сегд а следу ет с ж и м ать все, что о т п р а в л я е т ваш сервер. Д л я отклю чен и я сж ати я м ож но во сп о л ьзо ваться нестандартной ф ункцией ф ильтрац ии. Использование нестандартной функции фильтрации П о ум олчанию com pression вклю чает типы M IM E t e x t / * , * /js o n и * / j a v a s c r i p t в стандартную ф ункцию f i l t e r , чтобы избеж ать сж атия этих типов данных: e x p o r ts .f ilte r = fu nction(req, res){ const type = res.getH eader('C ontent-Type') | | ' ' ; return ty p e .m a tc h (/js o n |te x t|ja v a s c rip t/); }; Ч тобы изм енить это поведение, м ож но передать f i l t e r в объекте параметров, как показано в следую щ ем ф рагм енте кода, которы й сж имает только обы чны й текст: function f ilt e r ( r e q ) { const type = req.getH eader('C ontent-Type') | | ' ' ; return 0 === ty p e .in d e x O f('te x t/p la in '); } connect() .use(compression({ f i l t e r : f i l t e r })); Задание уровней сжатия и расходования памяти П р и вязк и N o d e-м одуля zlib поддерж иваю т парам етры бы стродействия и сжатия, которы е такж е м огут передаваться ф ун кц и и com pression. В.2. Реализация базовых функций веб-приложения 393 В следую щ ем прим ере кода перем енной l e v e l при сваивается значение 3, что соот­ ветствует невы сокой степени сж атия при вы соком бы стродействии, а перем енной memLevel — значение 8, обеспечиваю щ ее ускоренное сжатие за счет больш его расхо­ дования памяти. Эти значения полностью зависят от разрабатываемого прилож ения и доступных ресурсов (за дополнительны ми сведениями обратитесь к документации к модулю N ode zlib): connect() .use(compression({ le v e l: 3, memLevel: 8 })); А теперь обратимся к промежуточным компонентам, обеспечивающим такие базовые потребности веб-прилож ений, как ведение ж урн ала и сеансовы е данные. В.2. Реализация базовых функций веб-приложения C onn ect старается реализовать и предоставить встроенные пром еж уточны е ком по­ ненты д ля больш инства потребностей стандартны х веб-прилож ений, чтобы их не приходилось реализовы вать снова и снова каж дом у разработчику. Такие ф ункц ии веб-прилож ений, как ведение ж урнала, сеансы и виртуальны й хостинг, предостав­ ляю тся C o n n ect во встроенном виде. В этом р азд ел е рассм атр и ваю тся п ять пол езн ы х п ром еж уточ н ы х ком понентов, которы е с больш ой вероятностью понадобятся вам в прилож ениях: О m organ — предоставляет гибкую систему протоколи рован ия запросов; О serve-favicon — обслуж ивает запросы /favicon.ico, не требуя никаких специальных мер с ваш ей стороны; О m etho d -o v errid e — прозрачная зам ена req.m ethod д ля клиентов с ограниченной ф ункциональностью ; О v h o st — создание нескольких сайтов на одном сервере (ви ртуальн ы й хостинг); О express-session — управлени е сеансовы м и данны м и. Д о настоящ его м ом ента вы сам и создавали нестандартны е пром еж уточны е к о м ­ пон енты в ед ен и я ж урн ала, но р азр аб о тч и ки , о тветствен н ы е за сопровож дени е C onnect, предостави ли гибкое реш ение, которое н азы вается m organ. Д л я начала рассм отрим его. В.2.1. morgan: ведение журнала запросов М о д у л ь m o rg a n ( w w w .npm js.com /package/m organ) — ги б к и й п р о м еж у т о ч н ы й ком понент д ля п ротоколи рован ия запросов с возм ож ностью настройки ф ормата 394 Приложение В. Официально поддерживаемые промежуточные компоненты вывода. Он такж е поддерж ивает ключи для б уф ер и зац и и вывода, чтобы сократить количество операций запи си на диск, и для назначения ж урнального потока, если данны е вместо консоли долж ны вы водиться в другой прием ник (наприм ер, файл или сокет). Простейший вариант использования Ч тобы использовать m organ в своем прилож ении, вы зовите его как ф ункцию для п олучения п ром еж уточной ф ункции, как показано в листинге В.8. Листинг В.8. Использование модуля morgan для ведения журнала const connect = re q u ire ('c o n n e c t'); const morgan = require('m organ'); connect() .use(morgan('combined')) -<-------- (1) Режим combined используется для каждого запроса. .u se((req , res) => { res.setH eader('C ontent-T ype', 'a p p lic a tio n /js o n '); res.en d ('L o g g in g \n '); -<-------- (2) Отвечает на запрос сообщением. }) .listen (3 0 0 0 ); Ч тобы использовать этот пример, н еобходи м о установить м одуль m organ из npm 1. М одуль добавляется в начало стека пром еж уточны х ком понентов ( 1 ) , после чего выводится простой текстовый ответ (2 ). При использовании аргумента combined (1 ) это при л ож ени е C on n ect будет выводить данны е в ф орм ате ж урналов A pache. Это гибкий формат, которы й м ож ет разбираться м ногим и при лож ени ям и ком андной строки, что позволит вам обработать свои журналы аналитическими приложениями, генерирую щ им и п олезн ую статистику. П ри выдаче запросов от разны х клиентов (таких, как c u r l, wget и бр аузер ) вы увиди те в ж урн але строку, и д ен ти ф и ц и р ую ­ щ ую агента. Ф орм ат ж урнала combined определяется следую щ им образом: :remote-addr - :remote-user [:d a te [ c lf ]] ":method :u rl HTTP/:http-version" :sta tu s :res[co n ten t-length] " :re fe rre r" ":user-agent" К аж ды й из ф рагм ентов, предваряем ы й двоеточи ем , представляет со б о й маркер (to k en ), которы й в ж урнальной запи си зам еняется реальны ми значениям и из за ­ проса HTTP. Например, в результате простого запроса c u r l( 1 ) будет сгенерирована прим ерно такая строка журнала: 127.0.0.1 - - [Thu, 05 Feb 2015 04:27:07 GMT] "GET / HTTP/1.1" 200 - "-" "curl/7.37.1" 1 Мы использовали версию 1.5.1. В.2. Реализация базовых функций веб-приложения 395 Настройка форматов журнала Вы такж е мож ете создавать собственны е ф орм аты ж урнала. Д л я этого нуж но пере­ дать специальную строку маркеров. Н апример, следую щ ий ф орм ат будет создавать запи си вида GET /u s e r s 15 ms: connect() .use(morgan(':method :u rl :response-time ms')) .use(hello) .listen (3 0 0 0 ); По умолчанию доступны следую щ ие м аркеры (обратите внимание, что в названиях заголовков регистр сим волов не важен): О :re q [h e a d e r] exam ple: :re q [A c c ep t]; О :r e s [ h e a d e r] exam ple: :re s [C o n te n t-L e n g th ]; О :h ttp - v e r s io n ; О :re sp o n se -tim e ; О :rem o te-ad d r; О :d a te ; О :method; О :u r l; О :re fe rre r; О :u s e r-a g e n t; О : s t a tu s . Вы такж е м ож ете определять нестандартны е маркеры. Д л я этого достаточно у к а ­ зать название м аркера и ф ункцию обратного вы зова для ф ун кц и и c o n n e c t. lo g g e r. to k e n . Н апри м ер, д л я р еги страц и и каж дой строки запроса м ож но использовать следую щ ий код: var u rl = r e q u ir e ( 'u r l') ; m org an.token('query-string', fu nction(req, res){ return u rl.p a rse (re q .u rl).q u e ry ; }); К ром е ф орм ата по ум олчанию модуль m organ такж е вклю чает другие предопреде­ ленны е ф орм аты (такие, как s h o r t и t i n y д ля вы вода сокращ енной инф орм ации). Еще один предопределенный формат, dev, позволяет выводить краткие сведения для разработчиков веб-прилож ений. Этот ф орм ат мож ет потребоваться в тех случаях, когда, например, нуж но вы яснить, находится ли пользователь на веб-сайте, и вам не хотелось бы возиться с подробностям и запросов H TTP. В этом ф орм ате такж е 396 Приложение В. Официально поддерживаемые промежуточные компоненты использую тся цвета, соответствую щ ие коду состоян ия ответа. Н апример, ответам с кодами состояния 2xx соответствует зелены й цвет, с кодами 3xx — синий, с кодами 4xx — ж елты й и с кодам и 5xx — красны й. П одобная цветовая схема сущ ественно облегчает разработку. Ч тобы задействовать предопределенны й формат, просто передайте его название lo g g e r(): connect() .use(m organ('dev')) .u se (h e llo ); .listen (3 0 0 0 ); Теперь, когда вы знаете, как ф орм атировать данные, выводимые ж урналом, давайте поговорим о его параметрах. Режимы вывода журнала: stream и immediate К ак уж е упом иналось, с помощ ью парам етров поведение м одуля m organ можно изменить. О дин из эти х парам етров, stre a m , п о зв о л яет передать эк зем п л я р N o d e -объекта Stream, которы й будет и сп ользоваться вместо s td o u t д ля вы вода данных. С пом о­ щью этого парам етра м ож но перенаправить вы вод в собственны й ф айл ж урнала независим о от вы вода сервера. П ри этом используется экзем пляр объекта Stream, созданны й на основе fs .c re a te W rite S tre a m . П ри прим енении этих парам етров в общем случае такж е реком ендуется вклю чить свойство form at. В следую щем примере кода задействованы нестандартный формат и ж урналы , находящ иеся в папке /var/log/myapp.log. А благодаря ф лагу присоедине­ ни я append ф айл не будет усекаться при перезагрузке прилож ения: const f s = r e q u i r e ( 'f s ') ; const morgan = require('m organ'); const log = fs.createW riteS tream ('/v ar/lo g /m yapp.log', { fla g s: 'a ' }) connect() .use(morgan({ format: ':method : u r l ', stream: log })) .u s e ( '/ e r r o r ', erro r) .use(hello) .listen (3 0 0 0 ); Д ругой полезны й параметр, im m ediate, обеспечивает вы вод строки ж урнала, если запрос получен в первы й раз (вм есто ож и дани я ответа). Этот парам етр реком ен­ дуется использовать в том случае, если сервер держ ит запросы откры ты м и на п ро­ тяж ен и и длительного врем ени и вы хотите узнать, когда начинается соединение. К ром е того, этот парам етр м ож но задействовать д ля отладки критически важ ны х разделов прилож ения. С казанное означает, что в этом реж им е нельзя прим енять такие маркеры , как : s t a t u s и :re s p o n s e -tim e , поскольку они связаны с ответом. В.2. Реализация базовых функций веб-приложения 397 Ч тобы перейти в данны й реж им , при свойте зн ач ен ие tr u e парам етру im m ediate, как показано в следую щ ем прим ере кода: const app = connect() .use(connect.logger({ immediate: tru e })) .u s e ( '/ e r r o r ', error) .u se (h e llo ); В от и все, что касается ведения ж урнала! А теперь р ассм отрим пром еж уточны й ком понент для предоставления значка сайта. В.2.2. serve-favicon: значки адресной строки и закладки М аленький значок сайта (fa v ic o n ) отображ ается в адресн ой строке и на ярлычках вкладок браузера. Чтобы вывести такой значок, браузер запраш ивает ф айл /favicon. ico. О бы чно лучш е предоставлять файлы значков как м ож но раньше, чтобы осталь­ ны е части п р и л ож ен и я п оп р осту игнор и ровали их. М одул ь serv e-fa v ico n (www. npmjs.com /package/serve-favicon) по ум олч ан и ю п редоставляет зн ачок C onn ect. Ч тобы сменить значок, передайте соответствую щ ие аргументы. Значок сайта п о ­ казан на рис. В.2. Рис. В.2. Значок сайта Применение О бы чно пром еж уточны й ком понент serve-favicon разм ещ ается в сам ой верхней части стека, п оэтом у запросы значков сайта игнорирую тся всем и п оследую щ им и к ом понентам и ведения журнала. Значок кэш ируется в памяти для уск орен и я п о ­ сл едую щ и х ответов. В следую щ ем прим ере кода показан ком понент serve-favicon, отправляю щ ий файл .ico путем передачи в качестве единственного аргумента пути к файлу: const connect = re q u ire ('c o n n e c t'); const favicon = re q u ire ('s e rv e -fa v ic o n '); connect() .use(favicon(_dirname + '/f a v ic o n .ic o ')) .u se((req , res) => { res.en d ('H ello W orld!\n'); }); 398 Приложение В. Официально поддерживаемые промежуточные компоненты Д л я тестирован ия вам понадобится ф айл с именем favicon.ico. Д ополнительно с по­ мощ ью аргум ента maxAge м ож но передать значение, определяю щ ее врем я кэш и ро­ вани я значка сайта в пам яти. В следую щ ем разделе рассм атривается еще один небольш ой, но весьма полезны й пром еж уточны й ком понент — m ethod-override. О н позволяет им итировать метод запроса H T T P в том случае, если ф ункциональность кли ен та ограничена. В.2.3. method-override — имитация методов HTTP И ногда бы вает полезно использовать ком анды H TTP, вы ходящ ие за рам ки стан­ д ар тн ы х м етодов G E T и P O S T . П редставьте, что вы создаете си стем у веден и я блогов и хотите предоставить пользователям возмож ность создания, обновления и уд ал ен и я статей. Вместо G E T и P O S T более естественно использовать запись DELETE /статья. К сожалению , не все браузеры поддерж иваю т метод D E L E T E . С тандартное обходное реш ение — разреш ить серверу получить инф орм ацию о том, какой метод H T T P следует использовать, по параметрам запроса, значениям ф о р ­ мы, а иногда даж е заголовкам H TTP. О дин из способов основан на вклю чении тега <input type=hidden> со значением, соответствующ им нуж ному имени метода. Сервер проверяет значение и им итирует метод, соответствую щ ий запросу. Э тот прием поддерж ивается больш инством веб-ф рейм ворков; в C o n n ect д ля его реализаци и реком ендуется использовать модуль m ethod-override (www.npmjs.com/ package/method-override). Применение П о ум олчанию полю H T M L п ри сваи вается и м я _method, но вы мож ете передать собственное значение м етоду m ethodO verride, как показано в следую щ ем примере: connect() const connect = re q u ire ('c o n n e c t'); const methodOverride = req u ire('m eth o d -o v erride'); connect() .use(methodOverride('_method__') ) .listen(3000) Ч тобы показать, как реализуется m eth o d O v errid e(), посмотрим, как создать к р о ­ ш ечное при ло ж ен и е д л я об н о вл ен и я и н ф о рм ац и и п ользователей. П рилож ен ие состоит из одной ф орм ы , ко торая отвечает одним сообщ ением об успехе, когда ф орм а отправляется браузером и обрабаты вается сервером (рис. В.3). П рилож ение обновляет данные о пользователе с помощью двух разных промежуточ­ ных компонентов. В ф ункции update ф ункц ия n e x t() вы зы вается в том случае, если метод запроса отличен от PU T. К ак упом иналось ранее, больш инство браузеров не поддерж иваю т атрибут method="put" ф ормы, поэтому прилож ение из листинга В.9 работает некорректно. В.2. Реализация базовых функций веб-приложения і в ^ , в п о «тоы с хV 399 . © * 0« І/ 6 кг О ^ (ирлк*) ч «- С Q tocil/ •Сз 6 <! 0 \ Updated п ш to le k i Рис. В.3. Использование method-override для имитации запроса PUT, чтобы обновить форму Листинг В.9. Некорректно работающее приложение, обновляющее сведения о пользователе const connect = re q u ire ('c o n n e c t'); const morgan = require('m organ'); const bodyParser = re q u ire ('b o d y -p a rse r'); function e d it(re q , re s, next) { (1) Форма отправляет запрос i f ('GET' != req.method) return next(); PUTвместо GETили POST. res.setH eader('C ontent-T ype', 'te x t/h tm l') ; res.w rite('< form method="put">'); re s.w rite ('< in p u t type="text" name="user[name]" value="Tobi" / > ') ; re s.w rite ('< in p u t type="submit" value="Update" /> ') ; re s.w rite ('< /fo rm > '); res.en d (); } (2) Проверяет, был ли запрос отправлен методом PUT. function update(req, re s, next) { i f ('PUT' != req.method) return next(); res.end('U pdated name to ' + req.body.user.name); } connect() .use(morgan('combined')) .use(bodyParser.urlencoded({ extended: fa ls e })) .u se(ed it) .use(update) .listen (3 0 0 0 ); Ф орм а из этого прим ера отправляет серверу запрос P U T (1 ). Ф орм а долж на отпра­ вить данные ф ункции update, но только в том случае, если при отправке использован 400 Приложение В. Официально поддерживаемые промежуточные компоненты метод P U T (2 ). О пробуйте этот прим ер с разны м и браузерам и и клиентам и H T T P; д ля отправки запроса P U T из c u r l используется клю ч -X. Д л я р еш ен и я проблем ы с п од держ кой м етодов браузером д о б ав л я е тс я м одуль m ethod-override. В листинге В.10 на ф орм у д обавляется д ополнительны й элемент с именем _method, а метод m ethodO verride() добавляется после метода b odyP arser(), потом у что он обращ ается к req .b o d y д ля получения доступа к данны м формы. Листинг В.10. Использование method-overrider для поддержки метода HTTP PUT const const const const connect = re q u ire ('c o n n e c t'); morgan = require('m organ'); bodyParser = re q u ire ('b o d y -p a rse r'); methodOverride = req u ire('m eth o d -o v erride'); function e d it(re q , re s, next) { Отправляет подстав i f ('GET' != req.method) return n ex t(); с методом HTTP в виде res.setH eader('C ontent-T ype', 'te x t/h tm l') ; переменной формы _method. res.w rite('< form method="post">'); re s.w rite ('< in p u t type="hidden" name="_method" value="put" /> ') ; re s.w rite ('< in p u t type="text" name="user[name]" value="Tobi" / > ') ; re s.w rite ('< in p u t type="submit" value="Update" /> ') ; re s.w rite ('< /fo rm > '); res.en d (); } function update(req, re s, next) { i f ('PUT' != req.method) return next(); res.end('U pdated name to ${req.body.user.nam e}');; } connect() .use(m organ('dev')) .use(bodyParser.urlencoded({ extended: fa ls e })) .use(methodOverride('_method')) •<— Использует промежуточный .u se(ed it) компонент methodOverride для .use(update) отслеживания переменной формы. .listen (3 0 0 0 ); Зап у сти в этот прим ер, вы увидите, что теперь у вас п оявилась возм ож ность о т­ правлять запросы P U T практически из любого браузера. Получение доступа к исходному значению req.method М етод m etho d O v errid e() зам еняет исходное значение свойства req.m ethod, однако C o n n ect копирует исходны й метод, и вы всегда м ож ете получить доступ к нему через свойство re q .o rig in a lM e th o d . Таким образом, преды дущ ая ф орм а м огла бы вы водить значения так: console.log(req.m ethod); / / "PUT" console.log(req.originalM ethod); / / "POST" В.2. Реализация базовых функций веб-приложения 401 Ч тобы избеж ать вклю чения лиш них перем енны х ф ормы, такж е поддерж иваю тся заголовки H TTP. Разны е ф ирм ы -разработчи ки использую т разны е заголовки, так что вы мож ете создавать серверы, которы е поддерж иваю т несколько разны х имен полей заголовков. Это будет полезно в том случае, если вы хотите поддерж ивать кли ен тские инструм енты и библиотеки, рассчитанны е на кон кретны й заголовок. В следую щ ем прим ере поддерж иваю тся три им ени поля заголовка: app.use(methodOverride('X-HTTP-Method')) -<-------- Microsoft app.use(methodOverride('X-HTTP-Method-Override')) -<-------- Google/GData app.use(methodOverride('X-Method-Override')) < -------- IBM М арш рути зация на основании заголовков — достаточно стандартная задача. Х о­ рош им прим ером служ ит поддерж ка виртуальны х хостов. В озможно, вы видели серверы Apache, которы е использую т эту возм ож ность при разм ещ ении н есколь­ ких сайтов на м еньш ем количестве I P -адресов. A pache и N ginx могут определить, к каком у им енно сайту относится обращ ение, на основании заголовка H ost. C o n n ect тож е предоставляет такую возм ож ность, причем реали зуется она проще, чем м ож но было бы предполож ить. В следую щ ем разделе рассм атриваю тся в и р ­ туальны е хосты и модуль vhost. В.2.4. vhost: виртуальный хостинг М одуль vhost (виртуальны й хост) (www.npmjs.com/package/vhost) представляет собой простое облегченное средство м арш рутизации запросов на основании заголовка H ost запроса. Эта задача обычно реш ается реверсны м прокси-сервером, которы й затем направляет запрос веб-серверу, вы полняю щ емуся на том же локальном компьютере, но на другом порту. Компонент v h o st вы полняет эту операцию в том же самом N odeпроцессе путем передачи управления H T T P -серверу, связанному с экземпляром vhost. Применение К ак и д ля б ольш ин ства п ром еж уточны х ком понентов, д л я и сп о л ьзо ван и я к о м ­ понента v h o st достаточно единственной строки кода. Э тот ком понент приним ает два аргумента. П ервы й аргум ент представляет собой им я хоста, которому долж ен соответствовать экзем пляр v h o st. В торой аргумент — это экзем пляр h ttp .S e r v e r , которы й будет использоваться в том случае, если H T T P -запрос делается с соответ­ ствующим именем хоста. П оскольку все прилож ения C onnect являю тся субклассами класса h t t p .S e r v e r , экзем пляр при лож ения такж е подойдет: const connect = re q u ire ('c o n n e c t'); const server = connect(); const vhost = re q u ire ('v h o s t'); const app = r e q u ir e ( './s ite s /e x p r e s s js .d e v ') ; se rv e r.u se (v h o st('e x p re ssjs.d e v ', app)); serv e r.liste n (3 0 0 0 ); 402 Приложение В. Официально поддерживаемые промежуточные компоненты Ч тобы воспользоваться преды дущ им модулем ./sites/expressjs.dev, назначьте H T T P сервер свойству m o d u le .e x p o rts, как показано в следую щ ем примере: const h ttp = r e q u ir e ( 'h ttp ') module.exports = h ttp .c re a te S e rv e r((re q , res) => { re s.e n d ('h e llo from expressjs.com \n'); }); Использование нескольких экземпляров vhost() П одобно другим пром еж уточны м ком понентам, ком понент v h o st мож но исп оль­ зовать м ногократно в при лож ении д ля связы ван и я нескольких хостов с соответ­ ствую щ им и прилож ениям и: const app = r e q u ir e ( './s ite s /e x p r e s s js .d e v ') ; se rv e r.u se (v h o st('e x p re ssjs.d e v ', app)); const app = r e q u ir e ( './s ite s /le a r n b o o s t.d e v ') ; se rv e r.u se (v h o st('le a rn b o o st.d e v ', app)); Вместо ручной настройки промежуточного компонента vh o st можно сгенерировать список хостов в ф айловой системе. С оответствую щ ая м етодика продем онстриро­ вана в следую щ ем примере, в котором метод f s .r e a d d ir S y n c ( ) возвращ ает массив записей каталога: const connect = re q u ire ('c o n n e c t') const f s = r e q u i r e ( 'f s ') ; cons app = connect() const s ite s = fs .re a d d irS y n c ('s o u rc e /s ite s '); s ite s .fo rE a c h ((s ite ) => { co n so le.lo g (' . . . %s', s ite ) ; ap p .u se(v h o st(site, r e q u i r e ( '. / s i t e s / ' + s i te ) ) ) ; }); ap p.listen(3000); Главное преимущ ество ком понента v h o st перед реверсны м прокси-сервером — его простота. Он позволяет управлять всеми прилож ениями как одним целым. Подобная м етодика идеально подходит для обслуж ивания нескольких небольш их сайтов или сайтов, содерж ащ их преимущ ественно статический контент. Впрочем, есть и недо­ статок: если на одном сайте произойдет сбой, все остальны е сайты тож е вы йдут из строя, поскольку все они работаю т в рам ках одного процесса. А теп ерь м ы р ассм отри м ком п он ен т у п р а в л ен и я сеансам и — один из наи более ф ун д ам ен тал ьн ы х пром еж уточны х ком понентов, п ред ставляем ы х C onnect. Это ком понент упр авл ен и я сеансами, которы й назы вается express-session. В.2.5. express-session: управление сеансами М еханизм р еализаци и сеансов в веб-п рилож ени ях зависи т от требований. Н а п р и ­ мер, одним из важ н ы х реш ений стан овится подсистем а хран ен и я данны х. О дни В.2. Реализация базовых функций веб-приложения 403 при лож ения пользую тся таким и вы сокопроизводительны м и базами данных, как Redis; д ля других предпочтительна простота, и они использую т ту же базу данных, что и главное прилож ение. М одуль express-session (www.npmjs.com/package/expresssession) предоставляет A PI, которы й м ож ет расш и ряться д ля разны х баз данных. Это м ощ ны й и легко расш и ряем ы й м одуль, поэтому у него сущ ествует множ ество расш ирений, поддерж иваем ы х сообщ еством. В этом разделе вы узнаете, как п о л ь­ зоваться версией с хранением данны х в пам яти и Redis. А для начала мы поговорим о настройке пром еж уточны х компонентов и доступных параметрах. Применение В листинге В.11 представлен прим ер небольш ого прилож ения, ведущ его подсчет просм отров страницы . И н ф о р м ац и я храни тся в сеансовы х данны х пользователя. По ум олчанию cookie при сваивается им я c o n n e c t.s id со значением h ttp O n ly (это означает, что кли ен тские сценарии не м огут получить доступ к этому значению ). Д анны е самого сеанса х ранятся в пам яти на сервере. В листинге В.11 продем он­ стрировано базовое использование express-session в C o n n e ct1. Листинг В.11. Использование сеансов в Connect const connect = re q u ire ('c o n n e c t'); const session = re q u ire ('e x p re ss-se ssio n '); connect() .use(session({ se c re t: 'example s e c r e t', < ----- (1) Базовые параметры, необходимые для использования сеансов. resave: f a ls e , saveU ninitialized: true })) .u se((req , res) => { req.session.view s = req.session.view s | | 0; req.session.view s++; < -------- Создает сеансовую переменнуюviews иувеличивает ее. res.end('V iew s:' + req.session.view s); < -------- Возвращает значение браузеру. }) .listen (3 0 0 0 ); Э тот короткий прим ер создает сеансы, а затем оперирует с сеансовой переменной с именем views. С начала сеансовы й пром еж уточны й компонент инициализируется с необходимыми параметрами: s e c re t, resave и sa v e U n in itia liz e d (1). О бязательный параметр s e c r e t определяет, подписан ли объект cookie, используем ы й для иденти­ ф икац ии сеанса. П араметр resav e обеспечивает принудительное сохранение сеанса при каж дом запросе, даж е если он не изм енился. Это необходимо д ля некоторы х подсистем хранения сеансовых данных, поэтому перед вклю чением лучш е уточнить 1 Для тестирования использовалась версия 1.10.2. 404 Приложение В. Официально поддерживаемые промежуточные компоненты нуж ное значение по докум ентации. П оследний параметр, s a v e U n in itia liz e d , и н и ­ циирует создание сеанса даж е при отсутствии значений д ля сохранения. Сбросьте этот флаг, если хотите вы полнять правило, требующее подтверж дения пользователя перед сохранением cookie. Установка времени истечения сеанса П редполож им , что нуж но установить врем я истечения сеанса в 24 часа, разреш ить передачу cookie сеанса только при исп ользован ии протокола H T T P S и задать им я cookie. Д л я у п равлени я сроком ж и зни сеанса мож но задать свойства e x p ire s или maxAge объекта cookie: const hour = 3600000 req .sessio n .co o k ie.ex p ires = new Date(Date.now() + hour * 24); req.session.cookie.maxAge = hour * 24; П ри исп ользован ии C o n n ect часто устан авли вается значение перем енной maxAge, определяю щ ей количество м иллисекунд, прош едш их от заданного времени. С п о ­ мощ ью этой перем енной вы раж ение будущ их дат зачастую запи сы вается в и н ту ­ итивно более понятном виде: new Date(Date.now() + maxAge) Теперь, после знаком ства с настройкой сеансов, мы рассмотрим методы и свойства, доступны е при работе с данны м и сеанса. Работа с данными сеанса A P I управлени я данны м и в express-session очень прост. О сновной принцип закл ю ­ ч ается в том, что лю бы е свойства, присвоенны е объекту r e q .s e s s io n , сохраняю тся после вы полнен ия запроса. Затем эти свойства загруж аю тся при поступлении п о ­ следую щих запросов от того же пользователя (браузера). Например, для сохранения ин ф орм аци и о корзине покупок достаточно задать объект свойству c a rt: re q .se ssio n .c a rt = { items: [1,2,3] }; К огда вы обращ аетесь к свойству r e q . s e s s i o n .c a r t в последую щ их запросах, вам будет доступен массив .ite m s. А поскольку это обы чны й объект Jav aS crip t, в п о ­ следую щ их запросах м ож но вы зы вать м етоды вл ож ен н ы х объектов, причем он сохраняется им енно так, как ожидается: re q .se ssio n .c a rt.ite m s.p u sh (4 ); С ледует иметь в виду, что, если этот объект сеанса меж ду запросами сериализуется в ф орм ате JS O N , на объект r e q .s e s s io n наклады ваю тся те же ограничения, что и на ф орм ат JS O N : циклические свойства недопустимы, не могут прим еняться объекты fu n c tio n , объекты Date сериализую тся некорректно и т. п. У читы вайте эти ограни­ чен и я при использовании объекта сеанса. В.2. Реализация базовых функций веб-приложения 405 В C onnect данные сеанса сохраняю тся автоматически, но во внутренней реализации при этом вы зы вается метод S e s s io n # s a v e ( [ c a llb a c k ] ) , которы й такж е доступен в виде откры того A P I-интерф ейса. Ч тобы предотвратить атаки ф и ксац ии сеанса, д л я а у т ен ти ф и к а ц и и п о л ь зо в ател ей ч асто п р и м ен я ю тся два п о л езн ы х метода: S e s s io n # d e s tro y () и S e s s io n # re g e n e ra te (). П ри построении при лож ений на базе Express эти методы использую тся д ля аутентиф икации. А теперь рассм отрим операции с сеансовы м и cookie. Манипулирование сеансовыми cookie C o n n e c t п о зв о л яет зад ать глобальны е парам етры cookie д л я сеансов, но м ож но такж е м анип улировать конкретны м и cookie с помощ ью объекта S essio n # c o o k ie, которы й по ум олчанию устанавливает глобальны е параметры. П реж де чем начать н астр аи вать свойства, давай те расш и ри м преды дущ ее п р и ­ л ож ени е д ля у п р ав л ен и я сеансам и, чтобы м ож но было просм атривать свойства сеансовы х cookie путем запи си каж дого свойства в отдельны е теги <p> в H T M L разм етке ответа: res.w rite('<p>view s: ' + sess.view s + '< /p > '); res.w rite('< p > ex p ires in: ' + (sess.cookie.maxAge / 1000) + 's< /p > '); res.w rite('<p>httpO nly: ' + sess.cookie.httpO nly + '< /p > '); res.w rite('< p> path: ' + sess.cookie.path + '< /p > '); res.write('<p>dom ain: ' + sess.cookie.domain + '< /p > '); res.w rite('< p> secure: ' + sess.cookie.secure + '< /p > '); В C onnect разреш ается изм енять программным способом все свойства cookie в каж ­ дом сеансе, вклю чая e x p ir e s , h ttp O n ly , se c u re , p ath и domain. Н априм ер, можно задать продолж ительность активного сеанса в 5 секунд: req .sessio n .co o k ie.ex p ires = new Date(Date.now() + 5000); В качестве альтернативы д ля зад ан и я дли тельн ости активного сеанса м ож но и с ­ пользовать более интуитивно понятны й A P I-интерф ейс с методом доступа .maxAge, которы й позволяет получить и установить значение в миллисекундах относительно текущ его времени. С ледую щ ий код заверш ает активн ы й сеанс через 5 секунд: req.session.cookie.maxAge = 5000; О стальны е свойства (такие, как domain, path и s e c u re ) ограничиваю т область види­ мости (scope) cookie, р аспространяя ее только на домен, путь и ли защ ищ енны е со­ единения, в то время как httpO nly предотвращ ает доступ к данным cookie со стороны кли ен тских сценариев. Э ти свойства могут обрабаты ваться аналогичны м образом: req.sessio n .co o k ie.p ath = '/adm in'; req.session.cookie.httpO nly = fa ls e ; 406 Приложение В. Официально поддерживаемые промежуточные компоненты Д о сих пор д ля хранения данны х сеанса использовалось заданное по ум олчанию храни ли щ е в пам яти. Д авайте разберем ся, как подклю чать альтернативны е х р а ­ ни ли щ а данных. Хранилища данных сеанса В преды дущ их прим ерах мы использовали встроенны й объект c o n n e c t.s e s s io n . MemoryStore — это простое хранилищ е данны х в памяти, которое идеально подходит для тестирования прилож ений, поскольку не требует учитывать другие зависимости. О днако д ля разработки и эксплуатац ии при лож ения нуж но иметь надеж ную м ас­ ш табируемую базу данных, обеспечивающую хранение данны х сеанса; в противном случае вы будете терять сеансовы е данны е при перезапуске сервера. Н есм отря на то что практически лю бая база данны х мож ет служ ить хранилищ ем д ан н ы х сеанса, д л я часто м ен яю щ и х ся д ан н ы х лучш е всего подходят х р а н и л и ­ щ а вида «клю ч-значение», которы м присущ е м алое врем я отклика. С ообщ ество C o n n ect создало несколько храни ли щ данны х сеанса на основе таких баз данных, как C ouchD B , M ongoD B , Redis, M em cached, P o stgreS Q L и пр. В наш ем случае мы используем базу данны х Redis, д ля работы с которой исп оль­ зу ется м одуль co n n ect-red is ( https://www.npmjs.com/package/connect-redis). Б аза данны х Redis представляет собой хорош ее резервное хранилищ е, поскольку под ­ д ерж ивает п о л и ти к у и стечен и я срока д ей стви я клю чей, обеспечивает высокую производительность и проста в установке. Ч тобы убедиться в том, что база данны х R edis установлена в ваш ей системе, вы ­ полните ком анду r e d is - s e r v e r : $ red is-serv er [11790] 16 Oct [11790] 16 Oct [11790] 16 Oct connections [11790] 16 Oct 16:11:54 * Server s ta rte d , Redis version 2.0.4 16:11:54 * DB loaded from disk: 0 seconds 16:11:54 * The server is now ready to accept on port 6379 16:11:55 - DB 0: 522 keys (0 v o la tile ) in 1536 s lo ts HT. Теперь нужно установить модуль connect-redis, добавив его в файл package.json и вы ­ полнив ком анду npm i n s t a l l . М ож но такж е непосредственно вы полнить команду npm i n s t a l l - -sav e c o n n e c t- r e d is 1. М одуль connect-redis экспортирует функцию , которая д олж на быть передана перем енной connect. Листинг В.12. Использование Redis для хранения сеансовых данных const const const const connect = re q u ire ('c o n n e c t'); session = re q u ire ('e x p re ss-se ssio n '); RedisStore = re q u ire ('c o n n e c t-re d is')(se ssio n );-< favicon = re q u ire ('s e rv e -fa v ic o n '); 1 При написании книги использовалась версия 2.2.0. Передает RedisStore экземпляр 1 express-session. В.3. Безопасность веб-приложе ний 407 const options = { host: 'lo c a lh o s t' }; connect() .use(favicon(_dirname + '/f a v ic o n .ic o ')) .use(session({ sto re : new R edisStore(options), < ­ Настраивает сеанс рекомендуемыми se c re t: 'keyboard c a t ', значениями по умолчанию и RedisStore. resave: f a ls e , saveU ninitialized: tru e })) .u se((req , res) => { req.session.view s = req.session.view s || Изменяет сеансовые значения req.session.views++; обычным способом. res.end('V iew s: ' + req.session.view s); }) .listen (3 0 0 0 ); В этом примере создается хранилищ е сеансовой информации, использую щ ее Redis. П ередача co n n ect-red is ссы лки на перем енную c o n n e ct позволи т вы полни ть н а ­ следование из прототипа c o n n e c t.s e s s io n .S to r e .p r o to ty p e . Это важно, поскольку в N ode один процесс мож ет одноврем енно использовать несколько версий модуля. П ередавая конкретную версию C onnect, м ож но гарантировать, что connect-redis задействует правильную копию. Экземпляр класса R edisStore передается se s s io n () как значение sto re, а конструктору R ed isS to re могут передаваться произвольны е параметры, которые вы собираетесь использовать, — например, преф икс для сеансов. После того как оба ш ага будут вы ­ полнены, вы сможете обращ аться к сеансовым переменным так же, как к MemoryStore. В этом примере стоит обратить вним ание на одну подробность: мы вклю чаем про­ меж уточны й компонент значка сайта, чтобы предотвратить повторное увеличение сеансовой перем енной; в противном случае значение view s будет увеличиваться на 2 при каж дом запросе, так как браузер запраш ивает страницу и /favicon.ico. Р аздел получился довольно длинны м , но на этом описание пром еж уточны х ко м ­ понентов д ля основны х ф ункц ий веб-прилож ений заверш ается. В следую щ ем р аз­ деле рассм атриваю тся встроенны е пром еж уточны е компоненты , обеспечиваю щ ие безопасность веб-прилож ений. Э та область чрезвы чайно важ на д ля прилож ений, которы е долж ны защ ищ ать свои данные. В.3. Безопасность веб-приложений К ак уж е н ео д н о к р атн о отм ечалось, A P I -и н те р ф е й с я д р а N ode пред н ам ерен н о реализован на низком уровне. Это означает, что он не поддерж ивает встроенны е м ех ан и зм ы за щ и ты и л и о п ти м а л ь н ы е м е то д и к и в том, ч то ка са етс я со зд ан и я 408 Приложение В. Официально поддерживаемые промежуточные компоненты веб-прилож ений. К счастью, эти средства безопасности реализую тся пром еж уточ­ ны м и ком понентам и C onnect. В этом разделе рассм атриваю тся три модуля, относящ ихся к безопасности, которые м ож но установить из npm: О basic-auth — п о д д ер ж и в ает б азо ву ю H T T P -ау те н ти ф и к а ц и ю за щ и щ ен н ы х данных; О csurf — обеспечивает защ иту против атак ф альсиф икации меж сайтовых запросов (C ro ss-S ite R equest Forgery, C SR F); О errorHandler — помощ ь в отладке на этапе разработки. Н ачнем с ком понента basic-auth, реализую щ его базовую H T T P -аутентиф икацию . В.3.1. basic-auth: базовая HTTP-аутентификация В главе 4 мы со зд ал и п р и м и ти в н ы й п р о м еж уточны й ком понент, реали зую щ и й базовую аутентиф икацию . О казы вается, сущ ествую т несколько модулей C onnect, предназначенных для вы полнения той же операции. Как упоминалось ранее, базовая аутентиф икация — это простейш ая H T T P -аутентиф икация, и применять ее следует осторожно, поскольку учетны е данны е пользователя м огут быть легко перехваче­ ны (если при передаче данны х не используется протокол H T T P S ). У читы вая это, м ож но сказать, что базовая H T T P -аутен ти ф и каци я м ож ет быть полезной только д ля р еализаци и бы строй и не слиш ком надеж ной системы аутен ти ф и каци и в н е­ больш их или персональны х прилож ениях. Если прилож ение использует м одуль basic-auth, при первой попы тке подклю чения пользователя к прилож ению веб-браузер запроси т учетны е данные, как показано на рис. В.4. T he serv er lo ca l:8 0 re q u ire s a u sern am e and pa ssw ord. T he serv er says: A u th o riz a tio n R equ ired. U ser Nam e: Passw ord: ( C a n ce l ) ( Log In ) Рис. В.4. Запрос учетных данных при базовой аутентификации Применение М одуль basic-auth (www.npmjs.com/package/basic-auth) позволяет получить учетные данны е из поля заголовка H T T P A u th o riz a tio n . В листинге В.13 показано, как и с­ пользовать его с ваш ей собственной ф ункц ией проверки пароля. В.3. Безопасность веб-приложений 409 Листинг В.13. Использование модуля basic-auth const auth = re q u ire ('b a s ic -a u th '); const connect = re q u ire ('c o n n e c t'); function passw ordV alid(credentials) { return cred en tials && credentials.nam e === ' t j ' && c re d e n tia ls.p ass === 't o b i '; } Проверяет пароль для жестко запрограммированного имени пользователя. Получает разобранные учетные данные. connect() .u se((req , re s, next) => { const cre d e n tia ls = auth(req); i f (passw ordV alid(credentials)) { n ext(); } else { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="example"' }); res.en d (); } }) .u se((req , res) => { res.en d ('T h is is the secret a re a \n '); }) .listen (3 0 0 0 ); Возвращает заголовок WWW-Authenticate, если пароль указан неправильно. Впротивном случае next() передает управление в «защищеннуюобласть». М од у ль b a s ic -a u th о б есп ечи вает то л ь ко ч асть п роц есса а у т ен ти ф и к а ц и и , с в я ­ зан н у ю с р азб о р о м п о л я за го л о в к а A u th o riz a tio n . Вы д олж н ы сам остоятельн о п р о в е р и т ь п а р о л ь, о б р а т и в ш и с ь с в ы зо в о м к п р о м е ж у т о ч н о м у к о м п о н ен т у ; м о д у л ь b a s ic -a u th о т п р а в л я е т н у ж н ы е за г о л о в к и в том случае, есл и п о п ы тк а ау тен ти ф и кац и и завер ш и л ась неудачей. В этом при м ере в случае удачной ау тен ­ т и ф и к а ц и и в ы зы в ается n e x t (), чтобы у п р ав л ен и е бы ло передано в защ ищ ен ны е ч асти пр и ло ж ен и я. Пример с cURL Теперь попробуйте выдать запрос H T T P к серверу при помощ и c u rl; вы увидите, что пользователь не авторизован: $ curl h ttp ://lo c a lh o st:3 0 0 0 - i HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="Authorization Required" Connection: keep-alive Transfer-Encoding: chunked Unauthorized П ри вы даче того ж е запроса с учетны м и данны м и базовой аутен ти ф и каци и H T T P (обратите вним ание на начало U R L ) доступ будет успеш но получен: 410 Приложение В. Официально поддерживаемые промежуточные компоненты $ curl --user t j : t o b i h ttp ://lo c a lh o st:3 0 0 0 - i HTTP/1.1 200 OK Date: Sun, 16 Oct 2011 22:42:06 GMT Cache-Control: public, max-age=0 Last-Modified: Sun, 16 Oct 2011 22:41:02 GMT ETag: "13-1318804862000" Content-Type: te x t/p la in ; charset=UTF-8 Accept-Ranges: bytes Content-Length: 13 Connection: keep-alive I'm a secret П р одол ж ая тем у безоп асн ости , рассм отри м м одуль csurf, предн азн ачен н ы й для защ иты от атак ф альсиф икации м еж сайтовы х запросов. В.3.2. csurf: защита от атак CSRF П ри атаке ф ал ьси ф и к ац и и м еж сай тов ы х за п р о со в (C r o ss-S ite R eq u est Forgery, C S R F ) ф актически эксп луати руется дов ер и е к сайту со стороны браузера. В х оде такой атаки аутен ти ф и ц и рован н ы й пользователь ваш его п р и л ож ен и я посещ ает д р угой сайт, создан н ы й или в злом анн ы й ор ган и затор ом атаки. П о сл е этого от и м ен и пользователя вы полняю тся п одлож н ы е запросы , а он даж е не подозревает об этом. Э ту атаку лучш е пояснить на примере. П редполож им , что в вашем п рилож ении за ­ прос DELETE /a c c o u n t вызывает удален и е учетной зап и си пользователя (только во время подклю чения пользователя к серверу). Также предполож им, что пользователь п осети л ф орум , уязви м ы й для C S R F -атаки. О рганизатор атаки м ож ет передать сценарий, которы й генерирует зап р ос DELETE /a c c o u n t и удаляет учетную запись пользователя. К онечно, такая ситуация крайне нежелательна; избеж ать ее м ож но с пом ощ ью м одуля csurf. М одул ь csurf ( https://w w w .npm js.com /package/csurf) ген ер и рует 24-си м вольн ы й уникальны й идентификатор, так называемый маркер аутентичности (a u th en ticity to k e n ), и присваивает его сеан су пользователя в в и де r e q . s e s s i o n . _ c s r f . Затем этот маркер м ож ет быть включен в вводимы е в ф орм у данны е в качестве скрытого значения под названием _ c s r f , и C S R F -ком понент м ож ет проверить маркер при подписании. Э тот п роцесс повторяется для каж дого взаим одействия. Применение Ч тобы убеди ться в том, что м одуль csurf м ож ет получить доступ к r e q .b o d y ._ c s r f (скры тое значение вводимы х данны х) и r e q .s e s s io n ._ c s r f , следует поместить csurf п осл е м одулей body-parser и express-session, как показано в листинге В .1 4 1: 1 При тестировании примера использовалась версия 1.6.6. В.3. Безопасность веб-приложений 411 Листинг В.14. Защита от атак CSRF const bodyParser = re q u ire ('b o d y -p a rse r'); const connect = re q u ire ('c o n n e c t'); const csurf = r e q u ir e ( 'c s u r f ') ; const session = re q u ire ('e x p re ss-se ssio n '); const sesionOptions = { resave: fa ls e , saveU ninitialized: fa ls e , sec re t: '1234' }; connect() .use(bodyParser.urlencoded({ extended: fa ls e })) .use(session(sesionO ptions)) .u se (c su rf()) < -------- Загружает промежуточный компонент csurf после парсера и обработчика сеансов. .u se((req , re s, next) => { i f ( ' / ' != re q .u rl) return n ex t(); -<-------- Отображает формудля маршрута /. const token = req.csrfToken(); Получает текущий маркер CSRF при помощи const html = ' этого метода, добавленного csurf. fo rm method="post" action="/save"> <input type="text" name="_csrf" value="${token}"> <button type="submit">Submit</button> </form>'; res.setH eader('C ontent-T ype', 'te x t/h tm l') ; res.end(htm l); }) .u se((req , res) => { -<-------- Эта функция выполняется после запроса POSTс правильным маркером. const html = ' <p>Body: ${req.body._csrf}</p> <p>Session se c re t: ${req.session.csrfSecret}</p> ; res.end(htm l); }) .u s e ((e rr, req, re s, next) => { < -------- Обработчик ошибокдля неправильного маркера. c o n so le .e rro r(e rr); res.end('D id you get the c srf token wrong?'); }) .listen (3 0 0 0 ); Ч тобы и сп о льзо вать csurf, необходим о сн ач ала загрузи ть пром еж уточны е к о м ­ пон ен ты b o d y -p a rse r и session. З а т е м этот п р и м ер ото б р аж ает форму, которая вклю чает в себя текстовое поле для м аркера CSRF. С этим м аркером все запросы с определенны м типом метода будут проверяться на основании секретной строки в сеансе. Д л я получения текущ его м аркера используется re q .c srfT o k e n — метод, добавленны й csurf. Запросы с недействительны м и м аркерам и будут автоматически помечаться csurf, поэтому мы вклю чили обработчик для правильного маркера и об­ работчик ош ибки. В этом прим ере используется текстовое поле, чтобы вы видели, что происходит при попы тке изм ен ен ия маркера. П рим ер показы вает, что csurf автом атически акти ви зи руется д ля определенны х ти п ов запр о со в. Э ти ти п ы о п р ед ел яю тся пар ам етром ig n o re M e th o d s, которы й 412 Приложение В. Официально поддерживаемые промежуточные компоненты мож ет передаваться csurf. П о ум олчанию H T T P -методы GET, H E A D и O P T IO N S игнорирую тся, но при необходим ости м ож но добавить и другие. Д р у го й аспект веб -р азр аб о тки — о бесп ечени е доступ а к подробн ы м ж урн алам и д етали зи рован н ы м отчетам об ош ибках в среде разработки и в рабочей среде. Рассм отрим модуль errorhandler, предназначенны й д ля реш ения этих задач. В.3.3. errorhandler: — обработка ошибок при разработке М одуль erro rh an d ler (www.npmjs.com/package/errorhandler) идеально подходит для разработки, предлагая подробны е ответы с ин ф орм аци ей об ош ибках в ф орм атах H T M L , JS O N и простого текста на базе поля заголовка A ccept. О н предназначен только для использования в ходе разработки и не долж ен я в л яться частью рабочей конф игурации. Применение О бы чно этот компонент долж ен располагаться последним, поскольку предназначен д ля перехвата всех ошибок: connect() .u se((req , re s, next) => { setTimeout(function () { next(new Error('som ething b ro k e !')); }, 500); }) .u se (e rro rh an d ler()); Получение HTML-ответа об ошибке Е сли просм отреть в браузере лю бую страницу, д ля которой был вы полнен п о к а­ зан н ы й здесь код, вы увидите C o n n e c t-страницу ош ибки, подобную показанной на рис. В.5. Н а ней вы водится сообщ ение об ош ибке, состояние ответа и полная трасси ровка стека. Получение ответа об ошибке в формате простого текста П редполож им , что мы тестируем A PI, построенны й на базе C onnect. Ответы, гене­ рируем ы е в виде больш их ф рагм ентов H T M L -разметки, далеки от идеала, поэтому по ум олчанию e r r o r H a n d le r ( ) отвечает в ф орм ате t e x t / p l a i n , которы й хорош о подходит д ля H T T P -клиентов ком андной строки (таких, как c u r l( 1 ) ) . С ледую щ ий вы вод дем онстрирует эту концепцию: $ curl localhost:3000 -H "Accept: te x t/p la in " Error: something broke! at O bject.handle (/U se rs/tj/P ro je c ts/n o d e -in -actio n /so u rce /connect-m iddlew are-errorH andler.js:12:10) at next (/U se rs/tj/P ro je c ts/c o n n e c t/lib /p ro to .js:1 7 9 :1 5 ) В.3. Безопасность веб-приложений <+Error tomcthng broke' «• с б 413 x \ -■ ш > kt 6 9 \ Connect 500 Error: som ething broke! at Objecthandle ^ЦмдЛуРгсіесІаЛкхІе-йѵасЬогѵЧоигс^соппесЬпЁагіІчкие errortlan^er.|«:12:10| at next at Ob/ect logger |as handte] msers'tl'Protects/con^ect/iitVmtdaieware.'logger is: 155:5) at next (AJMnAt/Pro|ect3/connect/l<b/proio.|s:179:15) at Function, handle HJseraAl/Proiects/connecLllb/proto is :192:3) at Server app (<Us«rs/t)'Pro|ects/tQnnect/M>/connect.|s:33:3l) at Server.errvt (events.|s:e7:17) at HI IHf*anif .ontrxxKT»ng (http.j»:1134:12) at HTTPParaer.onHeadersComptete (http js 108:31) at Socket ondata (*tp.|s: 1029:27) ___________ A Рис. В.5. Разметка HTML для errorhandler в окне браузера a t O bject.logger [as handle] (/U se rs/tj/P ro je c ts/c o n n e ct /lib/m iddlew are/logger.js:155:5) a t next (/U se rs/tj/P ro je c ts/c o n n e c t/lib /p ro to .js:1 7 9 :1 5 ) a t Function.handle (/U s e rs /tj/P ro je c ts/c o n n e c t/lib /p ro to .js:1 9 2 :3 ) a t Server.app (/U se rs/tj/P ro je c ts/c o n n e c t/lib /co n n e c t.js:5 3 :3 1 ) a t Server.em it (events.js:6 7 :1 7 ) a t HTTPParser.onIncoming (http.js:1 1 3 4 :1 2 ) a t HTTPParser.onHeadersComplete (h ttp .js:1 0 8 :3 1 ) a t Socket.ondata (h ttp .js:1 0 2 9 :2 2 ) Получение JSON-ответа об ошибке Е сли вы о тп р авл яете H T T P -запрос, которы й вклю чает в себя загол овок H T T P A ccept: a p p l i c a tio n /js o n , то получите следую щ ий ответ в ф орм ате JSO N : $ curl h ttp ://lo c a lh o st:3 0 0 0 -H "Accept: ap p lication/json" {"erro r":{"stack ":"E rro r: something broke!\n at O bject.handle (/U se rs/tj/P ro je c ts/n o d e -in -a ctio n /source/connect-m iddlew are-errorH andler.js:12:10)\n a t next (/U se rs /tj/P ro je c ts/c o n n e c t/lib /p ro to .js:1 7 9 :1 5 )\n a t O bject.logger [as handle] (/U s e rs/tj/P ro je c ts /connect/lib/m iddlew are/logger.js:155:5)\n a t next (/U se rs /tj/P ro je c ts/c o n n e c t/lib /p ro to .js:1 7 9 :1 5 )\n a t Function.handle (/U s e rs /tj/P ro je c ts /c o n n e c t/lib / p ro to .js:1 9 2 :3 )\n at Server.app (/U se rs/tj/P ro je c ts/c o n n e c t/lib /c o n n e c t.js:5 3 :3 1 )\n a t Server.em it (ev en ts.js:6 7 :1 7 )\n a t HTTPParser.onIncoming (h ttp .js:1 1 3 4 :1 2 )\n a t HTTPParser.onHeadersComplete (h ttp .js:1 0 8 :3 1 )\n a t Socket.ondata (http.js:1029:22)","m essage":"som ething broke!"}} 414 Приложение В. Официально поддерживаемые промежуточные компоненты М ы добавили дополнительное ф орм атирован ие в ответе JS O N , чтобы упростить его чтение на странице, но, когда C o n n ect отправляет ответ JS O N , он уплотняется с помощ ью ф ун кц и и J S O N .s trin g ify (). Ч увствуете ли вы теперь себя проф ессионалом в вопросах безопасности C onnect? Вероятно, нет, тем не менее вы знаете уж е достаточно, чтобы защ итить свои п р и ­ лож ени я хотя бы на начальном уровне. А теперь мы рассмотрим функцию , исполь­ зуемую во всех веб-прилож ениях: предоставление статических файлов. В.4. Предоставление статических файлов П редоставление статических ф айлов — еще одна ф ункция, которая не реализована в ядре Node, но используется во многих веб-прилож ениях. К счастью, с некоторы ми просты м и м одулям и нуж ная ф ункциональность реализована в C onnect. В этом р азд ел е м ы р ассм о тр и м ещ е два о ф и ц и ал ьн о п о д держ и ваем ы х м одул я C o n n ect — на этот раз предоставляю щ их ф айлы из ф айловой системы. О бы чно эти ф у н кц и и предоставляю тся серверами H T T P (таким и, как A pache и N ginx), но при небольш их у си л и ях вы м ож ете добавить их в свои проекты C onnect: О serve-static — предоставляет ф айлы из ф айловой системы с заданны м корневы м каталогом; О serve-index — предоставляет аккуратно отформатированное содержимое каталога при запросе каталога. Сначала давайте рассмотрим, каким образом можно предоставлять статические файлы с помощ ью единственной строки кода, в которой используется модуль serve-static. В.4.1. serve-static — автоматическое предоставление статических файлов браузеру М одуль se rv e -sta tic (www.npmjs.com/package/serve-static) реал и зует вы со ко п р о ­ изводительны й, гибкий и м ногоф ункц иональн ы й статический ф айл овы й сервер с поддерж кой механизмов кэш ирования HTTP, запросов Range и т. п. М одуль такж е обеспечивает проверку безопасности д ля опасны х путей, по ум олчанию запрещ ает доступ к скры ты м ф айлам (начинаю щ им ся с ) ) . и отклоняет вредоносные нулевые байты. В сущ ности, serve-static яв л яется очень надеж ны м компонентом предостав­ л ен и я статических ф айлов, совм естимы м с различны м и H T T P -клиентами. Применение П редполож им , что прилож ение следует типичном у сценарию обслуж ивани я ста­ тических ресурсов, находящ ихся в каталоге ./public. Зад ача реш ается с помощ ью единственной строки кода: a p p .u s e (c o n n e c t.s ta tic ('p u b lic ')); В.4. Предоставление статических файлов 415 В этой к о н ф и гу р ац и и м о ду л ь se rv e -sta tic п р о в ер я ет обы чны е ф ай л ы , которы е н ах о д я тс я в п ап ке ./public/, вы б р ан н о й по U R L -адресу зап роса. Е сл и ф ай л с у ­ щ ествует, зн ачение поля C o n ten t-T y p e ответа будет по ум олчани ю опред ел яться расш и рением ф айла, и прои сходит пересы лка данны х. Е сли зап рош ен н ы й путь не пред ставл яет ф айл, акти в и зи р у ется ф у н к ц и я обратного вы зова n e x t( ) , к о т о ­ р ая р еал и зу ет переход к следую щ ем у (есл и таковой и м еется) пром еж уточном у компоненту. Чтобы протестировать прилож ение, создайте ф айл ./public/foo.js с вызовом co n so le. l o g ( 't o b i ') и сгенерируйте запрос д ля сервера. Д л я этого исп ользуйте команду c u r l( 1 ) с ф лагом - i , задаю щ им печать H T T P -заголовков. Вы увидите, что поля H T T P -заголовка, связанны е с кэшем, получаю т требуемые значения, поле ContentType отраж ает заданное расш ирение .js и контент передается успешно: $ curl h ttp ://lo c a lh o s t/f o o .js - i HTTP/1.1 200 OK Date: Thu, 06 Oct 2011 03:06:33 GMT Cache-Control: public, max-age=0 Last-Modified: Thu, 06 Oct 2011 03:05:51 GMT ETag: "21-1317870351000" Content-Type: a p p lic a tio n /ja v a sc rip t Accept-Ranges: bytes Content-Length: 21 Connection: keep-alive c o n s o le .lo g ('to b i'); П оскольку путь в запросе используется в исходном виде, ф айлы в папках предо­ ставляю тся так, как и следовало ожидать. Н апример, при запросах к серверу GET / j a v a s c r i p t s / j q u e r y . j s и GET / s t y l e s h e e t s / a p p . c s s сервер будет обслуж ивать, соответственно, ф айлы ./public/javascripts/jquery.js и ./public/stylesheets/app.css. Использование модуля serve-static с монтированием И ногда название при лож ения предваряется путем вида /public, /assets, /static и т. д. Реализованная в C onnect концепция м онтирования упрощ ает представление стати­ ческих ф айлов, находящ ихся в разны х папках. Д л я этого просто см онтируйте п р и ­ лож ение в нуж ном месте. К ак упом иналось в главе 5, промеж уточны е компоненты «не знаю т», где они см онтированы , поскольку преф икс пути удаляется. Н априм ер, запрос GET / a p p / f i l e s / j s / j q u e r y . j s с модулем serve-static, см онтиро­ ванны м в точке /app/files, передается пром еж уточном у ком поненту в виде GET / j s / jquery. Такое решение хорошо работает с функциональностью префиксов, поскольку точка /a p p /file s перестает быть частью процесса разреш ен ия файла: a p p .u s e ( '/a p p /f ile s ', c o n n e c t.s ta tic ('p u b lic ') ) ; И сходны й запрос GET / f o o . j s в данном случае не сработает, поскольку промеж уточ­ ны й ком понент не вы зы вается до тех пор, пока присутствует точка монтирования, 416 Приложение В. Официально поддерживаемые промежуточные компоненты зато с п р еф и к сн о й версией зап роса GET / a p p / f i l e s / f o o . j s ф ай л будет передан успешно: $ curl h ttp ://lo c a lh o s t/f o o .js Cannot get /f o o .js $ curl h ttp ://lo c a lh o s t/a p p /f ile s /f o o .js c o n s o le .lo g ('to b i'); Абсолютный и относительный пути И м ейте в виду, что путь, переданны й модулю serve-static, задается относительно текущ его рабочего каталога. Это означает, что передача значения " p u b lic " в качестве пути разреш ается, по сути, в виде p ro c e ss.c w d () + "p u b lic " . Тем не м енее при зад ан и и базового каталога иногда требуется исп ользовать аб ­ солю тны й путь. Д л я этого служ ит п е р е м е н н а я __dirnam e, которая исп ользуется следую щ им образом: a p p .u s e ( '/a p p /f ile s ', c o n n e c t.sta tic (_dirname + '/p u b lic ') ) ; Предоставление файла index.html при запросе каталога Еще одна полезная возможность модуля serve-static — возможность предоставления ф айлов index.html. Если запраш ивается каталог, в котором находится ф айл index. html, будет предоставлен этот файл. В о зм о ж н о сть п р е д о с та в л е н и я с тати ч еск и х ф ай л о в п о л езн а д л я ресурсов в е б ­ при лож ений (C SS, Jav aS crip t, граф и ка и т. д.). Н о что, если вы хотите разреш ить пользователям загруж ать списки произвольны х ф айлов из списка каталогов? Н а помощ ь приходит модуль serve-index. В.4.2. serve-index: генерирование списков содержимого каталогов К омпактны й модуль serve-index (www.npmjs.com/package/serve-index) предоставляет пользователям возм ож ность просм отра удаленны х ф айлов. Н а рис. В.6 изображ ен интерф ейс этого ком понента с полем для ввода кри тери я поиска, значкам и файлов и активн ы х цепочечны х ссы лок (b readcrum bs). Применение Э тот ком понент спроектирован для работы с модулем serve-static, которы й предо­ ставляет ф айлы ; serve-index просто предоставляет списки содерж имого. К о н ф и ­ гурация сводится к следую щ ему фрагменту, в котором запрос GET / предоставляет каталог ./public: const connect = re q u ire ('c o n n e c t'); const serv eS tatic = r e q u ir e ( 's e r v e - s ta tic ') ; В.4. Предоставление статических файлов О О О <- listing directory /Images С jO 417 x^__j local 3000/im ag e s ф Л f Search / images action_ button- admm-gapps- assignment-points- attendance-back.png attendance-modal, png attendance-msg- attendance-print- attendance-print- attendance-remove- attendance-remove- attendance-rol lover, png attendance-search- attendance-select, png autocomplete- back_am>w.png bar-black-b»g.png bg-texture, png bg.grid.png borrowed.png btn. remove-hover, png btn.remove.png button- button-arrow-down, png button-fulIscreen- button-fullscreen.png button-paginate-left- button-pagmate- button-paginate-nght- button-paginate- button-settings.png buttons_raf.png calendar-arrows.png caiendar-event- calendar-google- caiendar-sync.png Рис. В.6. Предоставление списка содержимого каталога с использованием промежуточного компонента Connect directory() const serveIndex = re q u ire ('se rv e -in d e x '); connect() .u se(serv eIn d ex ('p u b lic')) .u s e (s e rv e S ta tic ('p u b lic ')) .listen (3 0 0 0 ); Использование directory() с монтированием П ри м онти р о ван и и м ож но задать преф икс, п озволяю щ ий нап рави ть оба ком п о­ нента, serve-static и serve-index, по лю бому пути на ваш е усм отрение — например, как в следую щ ем прим ере кода, в котором используется преф икс GET / f i l e s . Здесь параметр ico n s задает реж им показа значков, а параметр hidden вклю чает для обоих компонентов реж им показа и предоставления скры ты х файлов: connect() . u s e ( '/ f i l e s ', serv e In d e x ('p u b lic ', { icons: tru e , hidden: tru e })) . u s e ( '/ f i l e s ', s e rv e S ta tic ('p u b lic ', { hidden: tru e })) .listen (3 0 0 0 ); Теперь вы мож ете легко перем ещ аться по ф айлам и папкам. Глоссарий Глава 1 JSON (Jav aS crip t O bject N o ta tio n ) — облегченны й ф орм ат передачи данных, удоб­ ны й д ля чтени я и запи си и основанны й на подм нож естве JavaS cript. libuv — многоплатформенная библиотека асинхронного ввода/вы вода; используется в N ode, а такж е в других библиотеках и язы ках (наприм ер, Ju lia). npm — м енедж ер пакетов Node, позволяет устанавливать пакеты Node из больш ого центрального репози тори я и у п равлени я зависи м остям и в проектах Node. Promise — объект Prom ise, используется в стандартизированном A P I E C M A S cript 2015 д л я п р ед ставлени я значений , которы е м огут быть доступны прям о сейчас, в будущ ем или никогда. REPL (re a d -ev al-p rin t loop) — ин тер ф ей с ком ан дной строки, п озвол яю щ и й в ы ­ полнить код и просм отреть результаты. Абстрактный интерфейс — программное описание API, не включающее реализацию. Х орош ий прим ер абстрактного ин терф ейса в N ode.js — A P I Stream s. Асинхронный — код, которы й не обязательно вы полняется в порядке следования. В N ode.js этот терм ин используется д ля обозначения A PI, получаю щ их ф ункции обратного вызова, которы е будут вы полнены в некоторы й мом ент будущего. Н а ­ прим ер, f s . r e a d F i l e получает ф ункцию обратного вы зова, которая получает со­ держ им ое ф айла после заверш ен ия его чтения. Базовые модули — библиотеки, встроенны е в Node. Деструктуризация — в E C M A S crip t 2015 бы ла введена концепция д еструктуриза­ ции, которая позволяет разбивать объекты и массивы на переменны е и константы. Н априм ер, c o n st { name } = { n am e:'A lex ' } создает константу с именем name и значением alex . Глоссарий 419 Неблокирующий ввод/вывод — блокирую щ ие операции ввода/вы вода прерываю т вы полнение програм м ы до заверш ен ия операции. В N ode используется неб л оки ­ рую щ ий вво д /вы во д , при котором чтение данны х из сети и ли ф айлового ресурса не приостанавливает вы полнение программы. Оставшиеся параметры — синтаксис оставш ихся параметров в E C M A S cript 2015 позволяет представить неизвестное количество аргументов ф ункции в виде массива. Ч тобы задать им ена двух аргументов, а остальны е поместить в массив, используйте запись fu n c tio n ( a , b, . . . r e s t ) . С интаксис такж е м ож ет и сп ользоваться при д е­ структуризаци и д ля коп ировани я объектов: c o n st newObject = { ...o ld O b je c t }. Семантическое управление версиями — схема определения совместимости библио­ теки с тремя числами: основным, дополнительны м и оперативным номерами версии. В ерсия записы вается в виде 1.0.2 (основная: 1, дополнительная: 0, оперативная: 2). П рилож ение, зависящ ее от версии 1.0.2, долж но быть совместимо с версией 1.1.1, но не с версией 2.0.0. Событие — строка, обеспечиваю щ ая вы зов ф ункции. Э та ф ун кц и я назы вается слу­ шателем события. Событие с заданным именем отправляется генератором (em itter). Б азовы м классом д ля создания генераторов в N ode яв л яе т ся класс E ventE m itter. Стандарт ECMAScript — спец и ф и кац и я я зы к а сценариев E C M A S cript бы ла стан ­ д артизирована ассоциацией Ecm a In tern atio n al. С ущ ествует несколько стандартов EC M A Script; книга ориентирована на стандарт E C M A Script 2015 (E C M A S cript 6th E dition). Создатели реализации Jav aS crip t использую т стандарт ECM A Script, чтобы обеспечить совм естимость их интерпретатора с кодом Jav aS crip t, написанны м для других реализаций. Стрелочная функция — ф у н кц и я, за п и сан н ая в сокращ енном син такси се. П ри передаче ф ункц ий в аргум ентах других ф ун кц и й вместо запи си f u n c tio n ( ) {} и с­ пользуется запись () => {}. Если ф ункц ия получает только один аргумент, круглые скобки м ож но опустить. Цикл событий — цикл собы тий N ode, ож идает появл ен и я внеш них собы тий и п ре­ образует их в активизацию ф ун кц и й обратного вызова. В других системах исп оль­ зую тся аналогичны е механизм ы (диспетчеры сообщ ений, исполнительны е циклы ) для быстрой м арш рутизации собы тий по соответствую щ им обработчикам событий. Глава 2 CommonJS — спец и ф и кац и я м одуля д ля определения значений, которы е долж ны экспортироваться из текущ его ф айла Ja v a S c rip t. См. модуль. package .json — ф айл, определяю щ ий имя, автора, лицензию и зависи м ости п ро­ екта N ode. У каж дой програм м ы N ode и библиотеки, созданной вами, долж ен быть ф айл package.json. 420 Глоссарий Свойство — объекты Jav aS crip t представляю т собой коллекции клю чей и значений; клю чи и значения назы ваю тся свойствам и объекта. Вложенный обратный вызов — обратный вы зов внутри обратного вызова; при пере­ даче ф ункц ии обратного вы зова другой ф ункц ии иногда бывает нуж но определить другую ф ункцию обратного вы зова внутри первой. Глобальная область видимости — под областью видим ости (scope) поним аю тся ч асти програм м ы , ко то р ы е м огут о б р ащ аться к значению . Таким образом , под глобальной областью видим ости поним ается значение, доступное в лю бой точке программы. Замыкание — ф у н кц и и Ja v a S c rip t получаю т доступ к переменным, определенны м в их внеш ней области видимости. Если ф ункц ия B определяется внутри ф ункции A, ф у н кц и я B получит доступ ко всем значениям A. Модуль — м одули N ode представляю т собой отдельны е ф айлы , содерж ащ ие код Jav aS crip t. Зн ач ен и я (как правило, ф ункции и константы ) могут экспортироваться д ля исп ользован ия в других файлах. Программная логика (и л и л огика упр авл ен и я п отоком ) — порядок вы полнен ия команд. Так как среда Node является асинхронной, программная логика представляет интерес, так как в Ja v a S c rip t сущ ествует много способов уп равлени я программной логикой, вклю чая обратны е вызовы, обещ ания, базовы е прим итивы циклов и и те­ раторы . В N ode под программной логикой поним ается способ группировки серий асинхронны х задач. Система управления контентом (CMS) — веб-п рилож ени е д ля редакти рования текста и графики, которы е долж ны отображ аться на общ едоступном сайте. Состояние — совокупность значений всех перем енны х в програм ме на заданны й мом ент времени. Трассировка стека — список програм м ны х инструкций, вы полненны х до мом ента возни кн овен ия ош ибки. Функция обратного вызова — ф ункц ия, которая передается другой ф ун кц и и для вы зова в будущем. Глава 3 cURL — про гр ам м а ком ан дной стр о ки и п рограм м н ая б и бл и отек а д л я вы дачи запросов H TTP. Ч асто используется как средство отладки д ля бы строй проверки реакц ии веб-серверов на запросы. MIME (M u ltip u rp o se In te rn e t M ail E x te n sio n s) — ин терн ет-стан дарт д л я в к л ю ­ чен и я нетекстовы х данны х в сообщ ения электронной почты и многосекционны е Глоссарий 421 тела сообщ ений. Это позволяет почтовы м клиентам отображ ать разм етку H TM L, изображ ени я и текст из других кодировок, кроме A SC II. ORM (o b ject-relatio n al m apping) — библиотека, связы ваю щ ая структуры данных, удобны е д ля програм м иста (напри м ер, объекты Ja v a S c rip t), со структурам и баз данны х (наприм ер, таблиц ам и и внеш ним и клю чам и). REST (R ep resen tatio n al S tate T ransfer), REST-совместимые API — веб-A PI без со­ стояния, использую щ ий набор заранее определенны х операций в H TTP. О перации б азирую тся на ком андах H T T P ; самые распространенны е команды — GET, POST, PU T и DELETE. Клиентский пакет — предварительно обработанны й код Ja v a S c rip t из нескольких исходны х ф айлов, которы й обычно подвергается м и н и ф и кац и и и сжатию, а затем отправляется клиентам. Кодирование данных формы — при отправке веб-серверу запроса H T T P PO ST, вклю чая простую отправку ф ормы, содерж им ое ф орм ы кодируется в теле запроса. Сам ы й распространенны й формат, application/x-w w w -form -urlencoded, аналогичен кодированию U R L -адресов: небезопасны е A S C II-символы зам еняю тся ком б и н а­ ц и ям и со знаком процента. Маршрут — ф рагм ент U R L -адреса и ком анда H TTP, которы е долж ны быть обра­ ботаны обработчиком марш рута. Модель базы данных — м одель данных, удобная для программиста, которая уп ро­ щ ает вы полнение операций с таблицам и баз данны х или докум ентами на «родном» я зы к е базы данных. Обработчик маршрута — определяем ая пользователем ф ункц ия обратного вызова, которая вы полняется при передаче запроса H T T P к веб-прилож ению . О бработчик м арш рута обы чно генерирует контент (возм ож но, загруж ая его из базы данны х) и ли вносит и зм ен ен и я в базу данны х, а затем генерирует ответ по ш аблону или в определенном ф орм ате (наприм ер, JS O N ). Статический файл — ф айл, предоставляем ы й веб-сервером без дополнительной обработки. О бы чно граф ика, ф айлы CSS и ф айлы с кли ен тским кодом Ja v aS c rip t представляю т собой статические файлы . Шаблон — текстовы й формат, которы й мож ет вклю чать в себя встроенны е данные или код Ja v a S c rip t и используется д ля генерировани я разм етки H TM L. Глава 4 webpack loader — вы полняет преобразование и ли транспиляци ю исходного кода. Канал — передача вы ходны х данны х на вход другой программы. В U N IX каналы меж ду процессами обозначаю тся символом вертикальной черты ( | ); в N ode потоки данны х соединяю тся посредством цепочечного вы зова методов. 422 Глоссарий Карта исходного кода — ф айл, при помощ и которого средства отладки браузера могут связать строку транспилированного ф айла с исходны м кодом. Плагин webpack — изм еняет поведение самого процесса построения (вм есто вы ­ ходны х ф айлов). Поток — эф ф екти вн ы й м еханизм ввода и (и л и ) вывода, которы й м ож ет содерж ать текстовые или двоичные данные. Node поддерживает потоки для чтения, потоки для записи, а такж е другие типы потоков, и эти потоки м ож но связы вать друг с другом при пом ощ и каналов. Система построения — набор ин стр у м ен тов и к о н ф и гу р ац и о н н ы х ф ай л ов д ля генерирования кода Jav aS crip t, эф ф екти вн о вы полняем ого в браузере. Статический анализатор — программа, проверяю щ ая правильность ф орм ата и с­ ходного ф айла. С татические анализаторы м огут использоваться д ля соблю дения требований конкретного стиля програм м ировани я в проекте, д ля чего исходный код проверяется по набору статических правил. Транспилятор — т р а н с п и л я т о р ы J a v a S c r ip t п р ео б р азу ю т од н у р азн о в и д н о ст ь E C M A S cript в другую. Ч ащ е всего транспиляторы использую тся д ля преобразова­ ни я современного кода ES2015 в старый код E C M A Script 5, которы й может работать в большем количестве браузеров. Другой пример — TypeScript — представляет собой надм нож ество Jav aS crip t, транспилируем ое в ES5 или ES2015. Цепочечный вызов методов — вы зов м етода д ля возвращ аем ого зн ачения ранее вы званного метода. Глава 5 GET, параметры — параметры U R L -адреса, следую щ ие за вопросительны м знаком и разделенны е сим волом &. HTTP, команда — метод H T T P (G ET, PO ST, PUT, PATCH, D E L E T E ), представля­ ю щ ий действие, которое долж но быть вы полнено с удаленны м ресурсом. MVC (M o d e l-V ie w -C o n tro lle r) — п аттерн п р о ек ти р о в а н и я д л я р азб и ен и я п р о ­ грам м ной систем ы на ком поненты : м одель (m o d el) уп р ав л я ет дан ны м и и л о ги ­ кой, п р ед ставлени е (v iew ) преобразует дан ны е в п ол ьзо вател ьски й ин терф ейс, а ко н тр о л л ер (c o n tro lle r) п р еоб разует в заи м о д ей стви я в оп ерац и и м одели или представлени я. Адаптер базы данных — некоторы е библиотеки баз данны х пиш утся в обобщ ен­ ном виде и могут расш и ряться специализированны м и адаптерами, реализую щ им и ф ункциональность д ля нуж ной базы данных. Логическая изоляция — возм ож ность простого изм ен ен ия ф ункц ии, класса или м одуля в проекте или их повторного исп ользован ия в другом проекте. Глоссарий 423 Полностековый фреймворк — фреймворк, вклю чаю щ ий в себя средства для работы как с клиентским , так и с серверны м кодом. О бы чно это означает наличие б и б л и ­ отек д ля работы с запросам и H TTP, м арш рутизац ии запросов, создания моделей баз данны х и взаим одействия с кодом, вы полняем ы м в браузере. Изоморфные приложения — при лож ения Jav aS crip t, способны е вы полняться как на стороне клиента, так и на стороне сервера, с использованием одного кода. Промежуточные компоненты — ф ункции, которы е могут вы зы ваться последова­ тельно д ля изм ен ен ия запроса и ответа H TTP. Реляционная база данных — структура базы данных, основанная на отнош ениях меж ду храним ы м и сущ ностями. Одностраничное веб-приложение — приложение, которое предоставляется браузеру однократно и не требует перезагрузки всей страницы. Если приложению потребуется по какой-то причине изм енить U R L -адрес в браузере, при помощ и H T M L H istory A P I создается и л л ю зи я изм ен ен ия U R L и загрузки новой страницы с сервера. Веб-фреймворк — набор библиотек д ля разработки веб-п рилож ени я с возм ож н о­ стью расш и рения с использованием плагинов и ли пром еж уточны х компонентов. Глава 6 bcrypt — ф ункция хеш ирования паролей. Ф ун кци я связывает данные произвольно­ го объема со строкой ф иксированного размера, которая может безопасно храниться в базе данны х (чтобы избеж ать хранения пароля в текстовом виде). База данных Redis — база данны х в пам яти, которая такж е вы полняет ф ункц ии кэш а и брокера сообщений. М ожет использоваться для хранения сеансовых данных и обработки p u sh -сообщ ений в веб-прилож ениях. Затравка — случайны е данные, добавляем ы е к входным данны м хеш -ф ункции для услож нен ия словарны х атак. Объект ответа — объект, определяю щ ий реакцию вашего сервера на запрос HTTP. Вклю чает в себя тело ответа (обы чно это веб-страни ца) и заголовки. Однопоточность — выполняемая программа (процесс) может состоять из параллельно выполняемых программных потоков. В модели JavaScript используется один программ­ ный поток, но предусмотрена возможность сохранения переключения контекста и вы ­ полнения другого кода при возникновении событий. События в браузере представляют собой взаимодействия (например, нажатие кнопки пользователем); в Node они обычно относятся к вводу/вы воду (например, сетевые операции или чтение данных с диска). Препроцессор CSS — программа, преобразую щ ая надмнож ество CSS в разм етку CSS, ко то р ая м ож ет и н тер п р ети р о ваться браузером . Я зы к и таблиц стилей Sass 424 Глоссарий и LESS вклю чаю т в себя препроцессоры CSS; в этих я зы к ах добавляю тся такие возм ож ности, как переменные, влож ение и примеси. Согласование контента — часть стандарта H TTP, относящ аяся к предоставлению разны х версий докум ента с одинаковы м U R I. П ользовательские агенты (браузеры ) могут запраш ивать альтернативны е ф орм аты данных, если они поддерж иваю тся сервером. Стороннее промежуточное ПО — пром еж уточны е компоненты , не расп ростран я­ емые авторам и исходной веб-библиотеки или ф рейм ворка. Хеш Redis —связь строкового поля со значением, используем ая для представления объектов в базе данны х Redis. Язык шаблонов — облегченны й я зы к разм етки, которы й преобразуется в H T M L и расш и ряется возм ож ностям и, упрощ аю щ им и внедрение значений из кода, п ере­ бор содерж имого м ассивов и объектов. Глава 7 XSS (м еж сайтовы е сценарны е) атаки — если веб-прилож ение получает входные данны е от ф орм или парам етров U R L и эти зн ачения отображ аю тся в ш аблонах, п о я в л я е тс я во зм о ж н о сть в н ед р ен и я вредон осного кода. З н а ч е н и я необходим о сначала экранировать д ля предотвращ ения атак этого вида. Значимые пробельные символы — в Ja v a S c rip t ф игурны е скобки, символы «точка с запятой» и сим волы новой строки использую тся д ля разд елен ия команд. Если потребуется создать новы й лексический блок, используется ф ункц ия или у п равл я­ ю щ ая команда. В я зы ках со значим ы м и пробельны м и сим волам и (наприм ер, P ug) строки кода группирую тся по количеству пробелов, используем ы х д ля ф орм и ро­ вани я отступа в каж дой строке. Лексическая область видимости — возм ож ность обращ ения к перем енной опре­ деляется ее областью видимости. В Ja v a S c rip t добавление ф ун кц и и вводит новый уровень области видим ости. Л ю бы е переменные, определенны е в ф ункции, стано­ вятся видим ы м и д ля всех ф ункций, определяем ы х внутри этой ф ункции. Лямбда-секция — л ям б д а-ф у н к ц и ей н азы вается ан он и м н ая ф ун кц и я. В H ogan лям бда-секцией назы вается способ связы ван и я ф ун кц и и с тегом в шаблоне. Примесь — обычно так назы вается класс, содерж ащ ий методы д ля использования в других классах. В Sass при м еси представляю т собой группы об ъявлен и й CSS, которы е м огут повторно использоваться в разны х местах; в P ug прим еси предна­ значаю тся д ля определения ш аблонны х ф рагм ентов, пригодны х д ля повторного использования. Частичный шаблон — небольшой шаблон, пригодный для повторного использования. Глоссарий 425 Глава 8 ACID — набор характеристик, которы м и долж на обладать база данны х. О перации с такой базой данных долж ны быть атомарными (операция либо заверш ается успеш ­ но, либо терпит полную неудачу, и база данны х остается в неизм енном состоянии), согласованны м и (данны е могут изм ен яться только разреш енны м образом), и зол и ­ рованны м и (с гарантиям и параллельного вы полнения) и устойчивы ми (внесенны е изм ен ен ия остаю тся даж е в случае сбоя и ли перезагрузки системы). BSON — двоичны й формат, используем ы й M ongoD B д ля представления объектов. О бъект состоит из упорядоченного набора элементов; элементы состоят из имени поля, ти п а и зн ач ен и я. К м н о ж еству типов, поддерж иваем ы х BSO N , относятся строки, целы е числа, даты и код Jav aS crip t. NoSQL — база данных, не основанная на табличных отнош ениях между сущностями (в отличие от р еляционн ы х баз данны х). Веб-работник — механизм, обеспечиваю щий возможность вы полнения браузерного кода Ja v a S c rip t в ф оновы х потоках. Демон — программа, вы полняем ая в ф оновом реж име (обы чно запускается авто­ м атически при загрузке системы ). Документо-ориентированная база данных — база данны х, содерж ащ ая ч ас ти ч ­ но стр уктурированн ы е дан ны е и не им ею щ ая предопределен ной схемы (ин огда в ф орм ате JS O N или X M L ). К этой категории относятся базы данны х M ongoD B и C ouchD B . Мемоизация — прием оптим изации, основанны й на сохранении результата вы зова ф ункции, чтобы ее не приходилось вы зы вать заново. Ненадежная абстракция — поп ы тка со кр ы ти я слож ности, которая раскры вает слиш ком много подробностей реализации. Первичный ключ — столбец базы данных, используем ы й д ля однозначной и д ен ­ ти ф и кац и и каж дой строки. Построитель запросов — програм м ны й интерф ейс, более удобны й для програм ­ мистов по сравнению с написанием запросов SQ L вручную. Публикация-подписка — паттерн, реализую щ ий возможность рассы лки сообщ ений нескольким получателям . Распределенная база данных — база данных, хранящ аяся на нескольких компью те­ рах — возм ож но (хотя и не обязательно), располож енны х в разны х географ ических местах. Реляционная алгебра — теоретическая основа для м одели рован ия храним ы х д ан ­ ны х и запросов, которы е могут при м ен яться к этим данным. 426 Глоссарий Реплицированный набор — группа процессов M ongoDB, обеспечивающих хранение одного набора данных. Схема базы данных — ф орм альное определение данны х и отнош ений меж ду эл е­ м ентам и базы данных; по сути, архитектура базы данных. Транзакция базы данных — одна или несколько операций с базам и данных, объ­ единенны х в группу и вы полняем ы х в соответствии со свойствам и ACID. Глава 9 BDD (р а зр а б о т к а ч ерез р еал и зац и ю п о в е д ен и я ) — р асш и р ен и е T D D с други м сти л ем A P I, к ото р ы й у д ел я е т о сн овн ое вн и м ан и е тому, где в п роц ессе п р о и с ­ ходит тести р о ван и е, что д о лж н о и л и не д олж н о т ест и р о в ат ь ся и как о й объем те ст и р о в а н и я д о лж ен п р о в о д и ться за один заход. B D D так ж е стар ается более н а гл я д н о п р о д ем о н стр и р о в ать , п о ч ем у не п р о х о д я т тесты и как н азн ач аю тся им ена тестовы х м одулей. TDD (разработка через тестирован ие) — написание тестов перед написанием т е ­ стирования кода. typeof — оператор Ja v a S c rip t, возвращ аю щ ий строку д ля заданного объекта или значения. Макеты — объекты и л и зн ач ен и я, которы е вы гл яд я т как р еал ьн ы е аналоги, но обычно обладают поведением, достаточным только для вы полнения тестов. Вместо того чтобы обращ аться к реальны м ф айлам или сетевым ресурсам в тестах (таки е операции м огут быть м едленны м и или опасны ми при деструктивном поведении), макеты могут использоваться д ля безопасной им и таци и их поведения. Модульное тестирование — м етодология изолированного тестирования отдельных частей м одуля (наприм ер, ф ун кц и й и ли методов класса) и проверки результатов по тестовы м утверж ден иям в м алы х тестовы х сценари ях (м одулях). Система исполнения тестов — программа, управляю щ ая нагрузочны м тестирова­ нием, их вы полнением и сбором результатов д ля вы вода (наприм ер, M ocha). Тестовое утверждение — проверка результата вы раж ени я на соответствие ож и да­ ниям . Таким вы раж ением м ож ет быть простая логическая конструкция, условие равенства и вообщ е практи чески все, что угодно. В N ode тестовы е утверж ден ия вы даю т исклю чения, если условие не вы полняется. С истем а исп олн ен и я тестов мож ет перехваты вать и накапливать эти исклю чен ия д ля построения отчетов по тестированию . Функциональное тестирование — тести р о ван и е ф рагм ен та ф ун кц и он ал ьн ости в системе. В контексте веб-разработки это подразум евает полностековое тести ро­ вание: браузер и сервер тестирую тся одновременно. Глоссарий 427 Глава 10 Amazon EC2 (A m azon E lastic C om pute C lo u d ) — сервис ви ртуализаци и Amazon. CDN (сеть доставки кон тен та) — распределенны е серверы, поставляю щ ие стати ­ ческий контент. D o ck e r, о б р а з — образ ф айловой системы, которы й используется D ocker д ля соз­ д ан ия контейнера. Elastic Beanstalk — сервис Am azon д ля сценарного управлени я разверты ванием на других сервисах Am azon (наприм ер, EC 2). SSH (S e c u re S hell) — и н тер ф ей с ком ан дной строки (и л и X 11) д ля вы п ол н ен и я команд на удаленном компью тере с поддерж кой ш иф рования. М ожет формировать рабочий процесс веб-р азр аб о тчи ка при исходной настройке нового сервера или пр и м ен яться к серверам д л я в ы п о л н ен и я операци й сопровож дени я и ли ком анд отладки. sudo — програм м а д ля запуска програм м с п ри вилегиям и другого пользователя. О б ы чн о и с п о л ьзу ет с я д л я в ы п о л н е н и я ком анд, н уж д аю щ и хся в сп ец и альн ы х привилегиях (наприм ер, редакти рования систем ны х конф игурационны х ф айлов). Контейнер — разновидность технологии виртуализации. Контейнеры представляю т собой экзем пляры операционной системы с изоли рован ны м и пользовательским и пространствами, работаю щ ие на базе ведущ ей операционной системы. Контейнеры предоставляю т д о полнительны е средства у п р авл ен и я исп ользован ием ресурсов и средства безопасности, они бы стро создаю тся и уничтож аю тся. Ротация журнала — периодически вы полняем ая команда, которая переименовывает ф айлы ж урналов с вклю чением даты и (н е обязательно) сж имает их для эконом ии пространства. Глава 11 Open Group — комитет, публикую щ ий Single U N IX Specification — семейство стан­ дартов, позволяю щ ее создателям операци онны х систем исп ользовать товарны й зн ак U N IX . stderr — поток д ля вы вода сообщ ений об ош ибках вы полняем ой программы. stdin — входной поток вы полняем ой программы. stdout — вы ходной поток д ля сообщ ений, вы водим ы х программой. Ключи командной строки — ф лаги, передаваемы е в ком андной строке д ля вкл ю ­ чен и я и ли отклю чения некоторы х ф ункц ий прилож ения. 428 Глоссарий Командный интерпретатор — п о л ьзо вател ьски й и н терф ей с ком ан дной строки, предназначенны й д ля ввода ком анд и просм отра результатов. Ф орм ирует своего рода «обертку» для операционной системы. Межпроцессные коммуникации — м еханизм , п ред оставл яем ы й оп ераци онной систем ой д ля обмена данны м и меж ду программами. О дним из примеров являю тся каналы , использую щ ие вы ходны е данны е одной программы как входны е данны е другой программы, но даж е ф айлы могут рассм атриваться как разновидность м еж ­ процессны х ком м уникаций. Перенаправление — сохранение вы вода одной программы и передача его на вход другой програм м ы или ф айла. Статус завершения — значение, возвращ аем ое програм м ой при заверш ении. Н е ­ нулевы е зн ачения яв л яю тся признаком ош ибки. Алекс Янг, Брэдли Мек, Майк Кантелон Node.js в действии 2-е издание Перевел с английского Е. М атвеев Заведующая редакцией Ведущий редактор Литературный редактор Художественный редактор Корректоры Верстка Ю. Сергиенко Н. Римицан И. Карпова С. Заматевская С. Беляева, И. Тимофеева Л. Егорова И зготовлено в России. И зготовитель: ООО «П итер Пресс». М есто нахождения и ф актический адрес: 192102, Россия, город С анкт-П етербург улица А ндреевская, дом 3, литер А , помещ ение 7Н. Тел.: +78127037373. Д ата изготовления: 12.2017. Н аименование: книж ная продукция. С рок годности: не ограничен. Н алоговая льгота — общ ероссийский классификатор продукции О К 034-2014, 58.11.12.000 — К ниги печатные проф ессиональны е, технические и научные. П одписано в печать 11.12.17. Формат 7 0 х 100/16. Бумага офсетная. Усл. п. л. 34,830. Тираж 1000. Заказ 0000. О тпечатано в ОАО «П ервая О бразцовая типограф ия». Ф илиал «Ч еховский П ечатны й Двор». 142300, М осковская область, г. Чехов, ул. П олиграф истов, 1. Сайт: w w w .chpk.ru. E-m ail: m arketing@ chpk.ru Факс: 8(496) 726-54-10, телеф он: (495) 988-63-87 п зал тЕ л ьск п п а о м ИЗДАТЕЛЬСКИЙ ДОМ «ПИТЕР» предлагает профессиональную, популярную и детскую развивающую литературу Заказать книги оптом можно в наших представительствах РОССИЯ Санкт-Петербург: м. «Выборгская», Б. Сампсониевский пр., д. 29а тел./факс: (812) 703-73-83, 703-73-72; e-mail: sales@piter.com Москва: м. «Электрозаводская», Семеновская наб., д. 2/1, стр. 1, б этаж тел./факс: (495) 234-38-15; e-mail: sales@msk.piter.com Воронеж: тел.: 8 951 861-72-70; e-mail: hitsenko@piter.com Екатеринбург: ул. Толедова, д. 43а; тел./факс: (343) 378-98-41, 378-98-42; e-mail: office@ekat.piter.com; skype: ekat.manager2 Нижний Новгород: тел.: 8 930 712-75-13; e-mail: yashny@yandex.ru; skype: yashnyl Ростов-на-Дону: ул. Ульяновская, д. 26 тел./факс: (863) 269-91-22, 269-91-30; e-mail: piter-ug@rostov.piter.com Самара: ул. Молодогвардейская, д. 33а, офис 223 тел./факс: (846) 277-89-79, 277-89-66; e-mail: pitvolga@mail.ru, pitvolga@samara-ttk.ru БЕЛАРУСЬ Минск: ул. Розы Люксембург, д. 163; тел./факс: +37 517 208-80-01, 208-81-25; e-mail: og@minsk.piter.com ( Издательский дом «Литер» приглашает к сотрудничеству авторов: \ тел./факс: (812) 703-73-72, (495) 234-38-15; e-mail: ivanovaa@piter.com Подробная информация здесь: http://www.piter.com/page/avtoru \ _____________________ ______________________) Издательский дом «Питер» приглашает к сотрудничеству зарубежных торговых партнеров или посредников, имеющих выход на зарубежный рынок: тел./факс: (812) 703-73-73; e-mail: sales@piter.com Заказ книг для вузов и библиотек: тел./факс: (812) 703-73-73, доб. 6243; e-mail: uchebnik@piter.com Заказ книг по почте: на сайте www.piter.com; тел.: (812) 703-73-74, доб. 6216; e-mail: books@piter.com Вопросы по продаже электронных книг: тел.: (812) 703-73-74, доб. 6217; e-mail: Kuznetsov@piter.com КНИГА-ПОЧТОЙ ЗАКАЗАТЬ КНИГИ ИЗДАТЕЛЬСКОГО ДОМА «ПИТЕР» МОЖНО ЛЮБЫМ УДОБНЫМ ДЛЯ ВАС СПОСОБОМ: • на нашем сайте: www.piter.com • по электронной почте: books@piter.com • по телефону: (812) 703-73-74 ВЫ МОЖЕТЕ ВЫБРАТЬ ЛЮБОЙ УДОБНЫЙ ДЛЯ ВАС СПОСОБ ОПЛАТЫ: ^ Наложенным платежом с оплатой при получении в ближайшем почтовом отделении. С помощью банковской карты. Во время заказа вы будете перенаправлены на защищ енный сервер нашего оператора, где сможете ввести свои данные для оплаты. @ Электронными деньгами. М ы принимаем к оплате Яндекс.Деньги, Webmoney и Kiwi-кошелек. В лю бом банке, распечатав квитанцию, которая формируется автоматически после совершения вами заказа. ВЫ МОЖЕТЕ ВЫБРАТЬ ЛЮБОЙ УДОБНЫЙ ДЛЯ ВАС СПОСОБ ДОСТАВКИ: • Посылки отправляются через «Почту России». Отработанная система позволяет нам организовывать доставку ваших покупок максимально быстро. Дату отправления вашей покупки и дату доставки вам сообщат по e-mail. • Вы можете оформить курьерскую доставку своего заказа (более подробную информацию можно получить на нашем сайте www.piter.com). • М ож но оф ормить доставку заказа через почтоматы (адреса почтоматов можно узнать на нашем сайте www.piter.com). ПРИ ОФОРМЛЕНИИ ЗАКАЗА УКАЖИТЕ: • фамилию, имя, отчество, телефон, e-mail; • почтовый индекс, регион, район, населенный пункт, улицу, дом, корпус, квартиру; • название книги, автора, количество заказываемы х экземпляров. БЕСПЛАТНАЯ ДОСТАВКА: • курьером по М оскве и Санкт-Петербургу при заказе на сум м у от 2000 руб. • почтой России при предварительной оплате заказа на сум м у от 2000 руб. ПЗПА ТЕПЬСКПП П О М ВАША УНИКАЛЬНАЯ КНИГА Хотите издать свою книгу? Она станет идеальным подарком для партнеров и друзей, отличным инструментом для продвижения вашего бренда, презентом для памятных событий! Мы сможем осуществить ваши любые, даже самые смелые и сложные, идеи и проекты. МЫ ПРЕДЛАГАЕМ: • • • • • издать вашу книгу издание книги для использования в маркетинговых активностях книги как корпоративные подарки рекламу в книгах издание корпоративной библиотеки Почему надо выбрать именно нас: Издательству «Питер» более 20 лет. Наш опыт - гарантия высокого качества. Мы предлагаем: услуги по обработке и доработке вашего текста • современный дизайн от профессионалов • высокий уровень полиграфического исполнения • продажу вашей книги во всех книжных магазинах страны • Обеспечим продвижение вашей книги: • рекламой в профильных С М И и местах продаж • рецензиями в ведущих книжных изданиях • интернет-поддержкой рекламной кампании Мы имеем собственную сеть дистрибуции по всей России, а также на Украине и в Беларуси. Сотрудничаем с крупнейшими книжными магазинами. Издательство «Питер» является постоянным участником многих конференций и семинаров, которые предоставляют широкую возможность реализации книг. Мы обязательно проследим, чтобы ваша книга постоянно имелась в наличии в магазинах и была выложена на самых видных местах. Обеспечим индивидуальный подход к каждому клиенту, эксклюзивный дизайн, любой тираж. Кроме того, предлагаем вам выпустить электронную книгу. Мы разместим ее в крупнейших интернет-магазинах. Книга будет сверстана в формате ePub или PDF - самых популярных и надежных форматах на сегодняшний день. Свяжитесь с нами прямо сейчас: Санкт-Петербург - Анна Титова, (812) 703-73-73, titova@piter.com Москва - Сергей Клебанов, (495) 234-38-15, klebanov@piter.com