19. Логирование, модули debug и winston

Наша следующая тема логирование или иначе говоря отладочный вывод. Когда проект маленький, то вполне достаточно console.log для того, чтобы что-то вывести. Однако проект имеет свойство расти. Например, тот же server.js естественным образом разделяется на сервер и обработчик запроса — request. Со временем появляется работа с пользователем, база данных и так далее. Каждый файл может захотеть по ходу своего выполнения, что-то вывести. И этот вывод для нас очень важен, поскольку показывает, что происходит. Особенно, если что-то происходить не так. В текущем коде, везде используется console.log для вывода

Это означает, что перейдя по браузерному url, я получу однообразную кашу из всех записей, что делает скрипт.

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

Для этого используются специализированные модули. Рекордсмен по простоте, это модуль DEBUG.

Модуль debug

Давайте поставим его в наш проект. Для этого вспоминаем главу — 7. Введение в NPM — менеджер пакетов для Node.JS и в консоле, из директории проекта, вводим команду

получаем

screenshot_19_01

как видим NPM создал, как в общем то и ожидалось, директорию «node_modules» в корне нашего проекта

screenshot_19_02

куда и поставил новый модуль, «debug». Давайте теперь подключим его, добавив такую строку

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

Вот так, в данном случае, пусть это буде ‘server’. Теперь вместо console.log мы пишем debug

Аналогичную операцию я произвожу и с файлом request.js, только на этот раз идентификатором будет не ‘server’ a ‘server:request’. Обратим внимание на двоеточие в  ‘server:request’, это нам еще будет важно

Итак, запускаю, сейчас не из WebStorm а через терминал

screenshot_19_03

И мы видим, что ничего не выводит, потому что, чтоб  выводило, мне нужно указать, что выводить. Для возврата в исходный режим нажимаем сочетание клавиш «Ctrl + C». И пробуем еще раз, но теперь для передачи информации о том, что выводить, нужно создать переменную окружения с названием «DEBUG» и дать ей значение, для начала, «server»

screenshot_19_04

На эту тему, для пользователей windows, есть интересная статья —

Команда SET — работа с переменными среды Windows

Что произошло? Мы создали переменную которой дали значение, запустили сервер и видим что в консоль дебагом выведено сообщение  которое помечено идентификатором «server».

 

У нас ведь есть еще один файл, который тоже должен выводить, нужную нам, информацию в консоль. Но в фале request.js другой идентификатор, а именно — ‘server:request’, давайте добавим его в нашу переменную. Вводим в консоле

Давайте чуть отвлечемся и проговорим вот такой момент, который сейчас важен. Переменная «DEBUG» имеет значение, это мы уже поняли. Это значение это строка. И если до последней команды в консоле, эта строка была равна «DEBUG=server», то после она стала равной  «DEBUG=server:request,server». Как видим последней командой мы дописали значение в начало этой строки.

От сюда делаем два вывода —

  • во первых принципиально важно не забыть поставить запятую после выражения которое мы хотим добавить к нашей строке, потому что именно по запятым парсится значение переменной DEBUG, и отделяя запятыми мы перечисляем идентификаторы которыми помечены интересующие нас логи, прямо как точка с запятой — «;» в переменной «PATH».
  • во вторых если у нас вдруг появится файл «user.js» и в нем мы подключим модуль «debug» которому укажем идентификатор, скажем, «user», то нам нужно будет опять проделать такую операцию

    если мы хотим выводить логи из этого файла.

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

Просто set и имя переменной, вот такая многогранная команда «set».

И так возвращаемся к нашему серверу. Мы добавили значение в переменную DEBUG

screenshot_19_06

теперь посмотрим какое значение имеет переменная DEBUG

screenshot_19_07

Судя по значению должно выводить логи из обоих файлов, проверим

screenshot_19_08

запустили сервер и он сразу вывел лог помеченный идентификатором ‘server’. Теперь перезагрузим открытую в браузере страницу по адресу — «http://127.0.0.1:1337/echo?message=TEST«, смотрим в консоль

вот и второй файл и логи в нем отработали.

К стати если мы уже решили выводить абсолютно все логи, то можно значение переменной  DEBUG установить равной «*». Давайте установим, убедимся чему равно и запустим сервер. И не забываем, чтоб прервать работу сервера из консоли, достаточно нажать сочетание клавиш «Ctrl + C». Итак смотрим

screenshot_19_10

Теперь перезагрузим открытую в браузере страницу по адресу — «http://127.0.0.1:1337/echo?message=TEST» и смотрим в консоль

Все работает по прежнему.

Модуль winston

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

может быть средней важности при отладке. Эта информация

может быть неважной. Эта информация может быть очень важной

поскольку, это ошибка — url не найден.

При помощи DEBUG задать важность, каким то образом нельзя. Кроме того DEBUG все пишет в стандартный поток вывода, а мы можем захотеть писать в файл или базу данных. Если такая потребность возникла или планируется, что она возникнет, тогда имеет смысл взглянуть на более навороченный модуль для логирования, который называется «Winston».

Ставим его

screenshot_19_12

и заменяем в коде require(‘debug’) на require(‘winston’), который возвращает объект «log».

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

  • log.debug — Маленькой важности
  • log.info — Сообщение средней важности.
  • log.error — это ошибка.

Все ошибки считаются очень важными, кроме того логгер настроен так, что по умолчанию он выводит сообщения только уровня info и более важные. Сейчас мы это увидим, запускаю в WebStorm и в браузере перехожу на соответствующий url — «http://127.0.0.1:1337/echo?message=TEST«.

screenshot_19_13

Как и говорилось ранее log.debug вообще не выводится, выводятся только сообщения уровня info и более важные, такие как error.

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

 

Просто сделать обертку над winston, которая будет находиться в отдельном модуле и добавлять интересующею нас функциональность. Назовем этот модуль «log», он будет принимать текущий объект модуля  и возвращать по сути тот же winston, но по разному настроенный, в зависимости от того какой именно модуль мы ему передаем, для каких то будем логировать так, для каких то можно логировать по другому. Вот пример такого модуля

Он экспортирует функцию, которая принимает модуль для которого нужно сделать логирование и возвращает winston настроенный соответственно его пути

В данном случае настройка заключается в том, что мы смотрим на что заканчивается путь и если это «/request.js» то возвращаем winston настроенный одним способом

а если это что-то другое то другим

Для настройки winston здесь используется концепция транспортов. Транспорт это нечто, что умеет передавать информацию, в данном случае информацию из логов. Например есть встроенный в winston транспорт «Console»

Который можно настраивать вот так, указать timestamp, расцветить — colorize, level — использовать его только для сообщений уровня info и выше.

Второй транспорт здесь это файл

который записывает логи, как указано, в файл с названием ‘debug.log’ —  filename: ‘debug.log’ и будет включаться для сообщений уровня debug и выше — level: ‘debug’. То есть фактически для всех. Таким образом, если путь оканчивается на /request.js, то мы возвращаем winston

который будет записывать в консоль сообщения info или выше и в файл вообще все.

ну, а для других путей, мы будем возвращать winston вообще без транспортов.

соответствующие вызовы log, с одной стороны не будут вызывать ошибку, а с другой стороны такая запись никуда не пойдет.

Проверяю это, запустив сервер и перейдя в браузере по адресу — «http://127.0.0.1:1337/echo?message=log-me-please» и мы видим

screenshot_19_14

что действительно информация info, попала в консоль, а так же появился файл debug.log, в котором есть все, все сообщения.

Итак мы с вами рассмотрели отладку,

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

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

NODE_DEBUG

Следующие средство отладки которое мы сейчас изучим называется NODE_DEBUG=»cluster fs http module net timer tls». NODE_DEBUG это переменная окружения которая используется внутри Node.JS. Есть ряд встроенных модулей которые, если эта переменная стоит, могут показывать, что происходит внутри них. Таким образом NODE_DEBUG, это средство для глубокой отладки. Его используют в тех случаях, когда наши возможности по отладке исчерпаны и нам ничего не остается, кроме как заглянуть внутрь самой ноды и посмотреть, что же там делается внутри.

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

на этот раз разделяем, значения присваиваемые переменной NODE_DEBUG, пробелами. И запускаем наш server.js. Для MacOs это будет выглядеть вот так

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

Смотрите, я только запустил, не успел никуда зайти, а уже начала поступать информация модуль NET вывел, что мы теперь слушаем все интерфейсы и порт 1337. А теперь перейду по адресу «http://127.0.0.1:1337/echo?message=TEST»

screenshot_19_16

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

Итак, у нас есть три инструмента логирования, которых хватит как для отладки небольшого проекта, так для полноценного логгинг-решения, ну и на конец, чтобы заглянуть внутрь Node.JS.

 

18. Отладка скриптов под Node.JS

Отладчик node debug

Следующая наша тема касается отладки под Node.JS. Первым делом мы рассмотрим самый простой, встроенный отладчик, который называется «node debug».

Выглядит это так: допустим у нас есть скрипт

например server.js который упорно глючит и мы хотели бы остановит его выполнение на некотором моменте, чтобы  посмотреть переменные и посмотреть что происходит. Для этого мы в этом месте ставим «debugger»

и запускаем скрипт в режиме отладки —

никаких модулей для этого ставить не надо

screenshot_18_01

Запустил. Изначально скрипт находится в состоянии паузы, видите ничего не происходит. В режиме отладки есть некоторые команды, например «help» —

screenshot_18_02

«help» нам вывел их список, мне сейчас нужна команда «cont». «cont» продолжит выполнение —

screenshot_18_03

Видите, он продолжил и даже вывел в console — «Server is running».

Отлично, что ж, давайте откроем браузер и перейдем по этой ссылке — http://127.0.0.1:1337/

Опа, как только я зашел, произошло событие ‘request’, запустился обработчик и выполнение остановилось, как раз на команде «debugger».

screenshot_18_04

теперь я могу перейти в консоль «repl» — режим выполнения команд и например, выяснить, что такое «urlParsed»

screenshot_18_05

например запустить что то — «res.end(«Uahahaha!»)

screenshot_18_06

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

screenshot_18_07

Дождалась!

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

Отладка под браузером Chrome

Для этого нам понадобится утилита которая называется «node inspector». Поставим ее глобально

screenshot_18_08

приготовления завершены и теперь несколько слов о том, что сейчас будет происходить. У Node.JS, при запуске, есть специальный параметр «—debug» после которого можно дать файл скрипта для запуска

когда Node.JS запускается с этим параметром, то он не только запускает server.js, но Node.JS так же начинает слушать происходящее на этом порту — «5858». К этому порту может подключиться другая программа и давать команды Node.JS, относящиеся к отладке. Например приостановить выполнение, возобновить выполнение,  получить текущее значение какой то переменной и так далее. Эти команды выдаются в соответствии со специальным протоколом, который описан в документации к «V8» — https://github.com/v8/v8/wiki/Debugging%20Protocol. Например, вы можете посмотреть примеры команд в доке, вот такая команда

означает продолжить выполнение, или вот такая команда

говорит Node.JS вычислить 1+2. Мы конечно же эти команды вручную набирать не будем, хотя могли бы, а используем утилиту node-inspector которая как раз будет посылать эти команды, а нам предоставлять красивый веб интерфейс.

Итак, оставлю Node.JS здесь запущенным

screenshot_18_09

и открою новое окно консоли, в котором запущу node-inspector.

screenshot_18_10

node-inspector — это веб сервер к которому можно подсоединиться под этим урлом — http://127.0.0.1:8080/?port=5858, который он дает и работать с отладчиком. То есть я буду слать команды веб серверу node-inspector, а он будет транслировать их как раз ноде, которая слушает протокол отладки, используя язык отладки «V8».

Итак я зашел в node-inspector

screenshot_18_11

Этот дизайн похож на встроенный инструмент разработки, хромовский, но на самом деле это не они. Они похожи, поскольку вот эти стили HTML взяты из того же движка, соответственно интерфейс очень похож, но на самом деле это всего лишь веб страница, которую нам отдает node-inspector. Теперь я зайду в отдельном окошке на страницу http://127.0.0.1:1337/echo?message=TEST, вернувшись в интерфейс отладчика мы увидим

screenshot_18_12

Произошло следующее — когда я перешел по этому адресу, то сработала функция обработчик request — это та функция которая передается вторым параметром методу «server.on()»

Если бы Node.JS был запущен без флагов отладки, то команда debugger была бы проигнорирована, но в нашем случае она сработала, V8 приостановил выполнение JavaScript и послал node-inspector, который подключен к ноде по этому порту — «5858», об этом информацию. Node-inspector ее получил и при помощи протокола «WebSocket», с которым мы познакомимся далее, переслал ее в браузер, браузерному JavaScript. Здесь уже отреагировал на это веб интерфейс и показал мне, что вот тут остановка

screenshot_18_13

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

screenshot_18_14 screenshot_18_15

При этом node-inspector берет на себя трансляцию моих действий в команды для отладчика V8. Могу даже использовать консоль, посмотреть «urlParsed»

screenshot_18_16

Все есть. Или если я хочу ответить на запрос, то я могу сделать так же как делал со встроенным отладчиком, а именно вызвать метод в текущем контексте — «res.end(«Uahahaha! Test passed»);»

screenshot_18_17

screenshot_18_18

Когда я наотлаживался, я могу play сделать

screenshot_18_19

придется нажать дважды, потому что favicon будет запрашиваться.

screenshot_18_20

при следующем запросе

screenshot_18_21

опять управление перейдет сюда

screenshot_18_22

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

screenshot_18_23

Все очень удобно, этим рекомендуется пользоваться. Единственное рекомендуется иметь ввиду, что мы работаем с цепочкой соединений. Мы зайдя в данный url к node-inspector — «http://127.0.0.1:8080/?port=5858», присоединяемся из клиентского, браузерного JavaScript к серверу node-inspector, серверный node-inspector присоединяется к Node.JS и бывает так, что какое то из этих соединений рвется, соответственно отладка перестает работать, в этом случае можно перезайти на страницу отладки, если это не помогло, рестартовать node-inspector, если это не помогло, то рестартовать по цепочке все — ноду, node-inspector и перезайти на страницу в браузере и тогда точно все будет хорошо.

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

сейчас я запущу этот сервер — раз

screenshot_18_09

два

screenshot_18_10

три

screenshot_18_11

Сколько действий.

Теперь, если я перейду по адресу сервера — «http://127.0.0.1:1337/», то никакой отладки не произойдет, потому что JavaScript просто упадет. Для того чтобы отлаживать JavaScript в этом случае, можно зайти в отладчик и здесь кликнуть то же самое, что мы кликаем в отладчике в браузере

screenshot_18_24

то есть сделать эту пимпочку фиолетовой. Теперь перехожу на эту страницу — «http://127.0.0.1:1337/» и отладчик останавливается на ошибке

screenshot_18_25

 

Давайте взглянем на еще один пример отладки, на этот раз консольной утилитой.

В файле pow.js есть вычисление степени при помощи такой вот рекурсивной функции.

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

screenshot_18_26

Но выводит 32. А два в третьей это, как известно, восемь. В чем же дело? Чтобы посмотреть, что происходит, попробуем запустить отладчик.

screenshot_18_27

Теперь если я попробую запустить node-inspector, то он запустится, но на самом деле ничего отлаживаться не будет. Почему? Так как мы понимаем происходящее, то можем это легко объяснить. Ведь нода запустилась, открыла доступ по порту «5858», а потом выполнила скрипт и все. Нода закончила свою работу, она уже все вывела и все и никакой node-inspector теперь к ней не подключится, ну просто потому что она не запущена. Порт 5858 закрыт. Что делать? То что нам на самом деле нужно — это другой флаг «debug-brk», он запускает скрипт сразу входя в состояние останова.

screenshot_18_28

Вот я запустил, оно подняло отладчик и теперь ждет пока кто нибудь к нему подключится и даст команду продолжить выполнение. Теперь я для верности перезапущу node-inspector, который запущен в отдельном окне.

screenshot_18_30

И войдем в инструменты разработки через Chrome. Открываю соответствующий url

screenshot_18_29

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

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

screenshot_18_31

Эта кнопка или клавиша F11 переведет управление на следующую команду. Нажимаю

screenshot_18_32

Хм, куда же  мы попали? Здесь немножко тонкий момент, дело в том, что когда я попытаюсь подключить объект «console», то Node.JS его require, мы видим это на изображении, это встроенный объект по умолчанию не подключается, а тут вот нужно его подключить. Процесс подключения console нас не очень интересует, по этому я нажму другую кнопку, вот эту

screenshot_18_33

которая означает, что процесс выполнения нужно продолжить, но остановиться как только произойдет выход из этой функции. На изображении мы видим, что после того как я нажал на эту кнопку node-inspector нам показал, что console подключен и следующий вызов обратится к pow(). Жмем F11 или кнопку step to next function

screenshot_18_34

вот мы находимся внутри функции pow(), и теперь повторные нажатия будут переводить управление внутрь вложенных вызовов нажму несколько раз

screenshot_18_35

видите «Call Stack» растет. «Call Stack» это последовательность сложенных вызовов функции которые привели к текущему положению дел. Соответственно в данном случае из списка Call Stack нас интересует pow.js, это единственный наш файл, остальное показаны встроенные всякие модули. Если мы выделим какой либо из вызовов pow.js, из списка Call Stack, мы увидим, что различаются он локальными переменными.

screenshot_18_36

Дальнейшие шаги по отладке очевидны, по этому мы переходим дальше, а именно к отладке под IDE.

Отладка под IDE

В качестве IDE будет использоваться WebStorm и для того, чтоб запустить отладку нам вполне подойдет уже готовая конфигурация,

screenshot_18_37

Единственное нужно будет, чтоб запускалась именно нода, а не supervisor!
Чтобы запустить отладку жму сюда

screenshot_18_38

И смотрю в консоле, что у меня получилось. При запуске в режиме отладки, WebStorm добавляет к Node соответствующий параметр «—debug-brk» и указывает ему значение, это значение это  порт на котором нода должна ожидать подключение отладчика, по умолчанию  — 5858. Ранее к этому порту подключался node-inspector, но сейчас к этому порту подключен сам WebStorm. Соответственно он реализует необходимый интерфейс и сейчас если я зайду в браузере на этот url — «http://127.0.0.1:1337/echo?message=TEST» и перейдем в окно WebStorm, то увидим

screenshot_18_39

То произойдет остановка, и здесь, в окне Variables, я могу ходить, анализировать переменные смотреть urlParsed и так далее.

Резюме по способам отладки для Node.JS

Первый способ, это запуск node debug script. При этом нода тут же приостанавливает выполнение скрипта и переходит в специальный режим консольной отладки. В котором можно получит список команд, через «help», управлять выполнением скрипта, переходить в режим консоли через «repl». Это работает, но уж больно просто. Хочется какой то более удобный интерфейс, по этому в тех случаях когда это возможно используется отладка под браузером Chrome, через node-inspector либо под IDE. Для того чтобы такая отладка стала возможной нужно запустить ноду со специальным флагом «—debug» либо «—debug-brk», последний при этом, переведет скрипт в состояние остановки тут же. Если нода запущена с этими флагами, то она дает доступ к встроенному механизму отладки V8, соответственно V8 начинает слушать этот порт, по умолчанию 5858,  отладчик к нему подключается и может слать команды которые управляют выполнением, получать текущие переменные и так далее. Для отладки под браузером Chrome в качестве отладчика можно использовать node-inspector, который с одной стороны подключается к порту ноде и умеет говорить с ней, с другой стороны он показывает страничку в Chome и принимает через нее команды, вместо Chrome могут быть другие браузеры, достаточно современные. Ну а относительно IDE я могу сказать то что смотря какая у вас IDE, как в ней все сделано, это может быть удобно, может не удобно, лично для меня наиболее безглючным и надежным способом, как правило, является отладка через Chrome, но для нее нужно запускать node-inspector.

 

17. Разработка, отладка, supervisor

Всем привет, на этом занятии наша основная цель улучшить нашу разработку на Node.JS. Первое средство которое мы для этого изучим, называется «supervisor». Доку по нему можем посмотреть здесь.

До этого момента, мы разработали «echo server»

Как вы помните в процессе разработки была некоторая сложность, можно сказать неудобство. Любые изменения в коде, требовали перезапуска сервера, это легко объяснимо, ведь Node.JS считывает файлы только один раз. Считав файл он получает соответствующий объект модуля и в дальнейшем использует его. А что делать в процессе разработки? Есть конечно вариант очистить внутренний кеш Node.JS, но наиболее часто используют другой подход, а именно специальный модуль, который отслеживает все файлы в директории и как только в ней что то меняется, перезапускает Node.JS. Один из таких модулей называется «supervisor». Этот модуль содержит в себе консольную утилиту, которая должна быть доступна по системному пути по этому я ставлю его глобально

screenshot_17_01

Поставилось, и теперь я запускаю вместо

«supervisor»

screenshot_17_02

Запустил, что это мне даст? Давайте посмотрим, сейчас я изменю файл, добавив в него, например, пустую строку и сохраню то что получилось.

screenshot_17_03

Мы видим — «crashing child» и старт новый. И так он будет перезапускатся при каждом изменении. А supervisor все время следит за файлами и как только он видит, что изменились файлы с расширениями, по умолчанию — «node» или «js», то он перезапускает ноду. Пожалуй стоит подредактировать конфигурацию запуска server.js в Webstorm с использованием  supervisor. Как попасть в окно настроек смотрим здесь. Запускаться у нас будет боле не node

screenshot_17_04

а supervisor.

Сейчас, пользователям Windows, повышенное внимание!

Наша задача это поменять в указанной строке путь так, чтоб он указывал к файлу «supervisor.cmd», который находится по пути — «C:\Users\<USER_NAME>\AppData\Roaming\npm\supervisor.cmd». <USER_NAME> — это соответственно имя вашего пользователя Windows. Но наш WebStorm не видит скрытые директории, а директория «AppData» — скрытая. поэтому, вам придется вручную ввести название этой папки или путя в целом. Смотрим

screenshot_17_05 screenshot_17_07 screenshot_17_08 screenshot_17_09 screenshot_17_10 screenshot_17_11

Далее Apply и OK. Запускаю

screenshot_17_12

Так, вроде работает. Ну ка добавлю строку, сохраню файл

screenshot_17_13

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

Какие подводные камни есть у supervisor? Они тоже присутствуют. Первый подводный камень о котором необходимо знать, это новые файла. Если я создам новый файл в директории проекта, например — «file.js», то никакой реакции от supervisor мы не дождемся. Если я сделаю изменения в новом файле — реакции ноль. Это потому что supervisor  наблюдает за каждым файлом индивидуально и он просто не знает о том что новый файл появился, но если я после того как изменил этот файл, внесу его через require в server.js то как раз изменения в server.js он отследит и все перезапустит ну а после запуска будет следить и за новым файлом тоже. Таким образом, если добавление нового файла влечет за собой изменения существующих, а как правило так оно и есть, то у нас все хорошо. Но в тех редких случаях когда не влечет, придется перезапускать supervisor вручную, автоматически новые файлы он не обнаружит.

Следующий подводный камень супервизора заключается в том, что если мы поставили в директорию проекта много модулей и у нас есть большая директория node_modules, а каждый модуль ставит свои зависимости, то общее количество файлов в поддиректории может стать очень большим и supervisor начнет есть очень много процессорного времени, особенно это заметно на ноутбуках, которые при этом начинают гораздо сильнее греться, вот чтобы такого не было, желательно, либо supervisor запускать с параметром «ignore» и указывать что нужно игнорировать node_modules, для примера попробуем проигнорировать наш file.js

screenshot_17_14

либо переместить эту node_modules, жирную, куда то на верх, потому что, действительно, зачем за ней смотреть, это внешние модули, они изменяются не так уж часто.

Итак, далее для разработки мы будем использовать supervisor.

16. Документация к модулю http

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

screenshot_16_01

Сейчас модуль HTTP совмещает в себе два функционала. Первый функционал, это функционал сервера — http.createServer([requestListener]) создает новый объект класса http.Server. Если передан обработчик в http.createServer([requestListener]), то он ставится на Event: ‘request’. Второй функционал это http.createClient([port], [host]). Если мы найдем его в доке, мы увидим

screenshot_16_02

говорит используй http.request(). Его общий смысл такой, что он предназначен для создания http запросов. При помощи этого метода Node.JS может обратится к другому сайту или серверу. Сам по себе класс http.Server используется мало, создается объект, вешается обработчик на событие Event: ‘request’, запрос пришел, и при помощи server.listen(port[, hostname][, backlog][, callback]) серверу назначается определенный порт, хост или если у вас UNIX система, можно назначить UNIX socket — server.listen(path[, callback]). Далее при получении запроса происходит событие request и контроль приходит к функции обработчику — Event: ‘request’. Она получает два аргумента, первый request  — объект запроса, входящие данные. Второе response — объект ответа. Давайте начнем с объекта запроса. Но прежде обратите внимание в документации есть некоторые непонятки, возможно в будущем при переписовании модуля они будут устранены, Class: http.ClientRequest который здесь описан

screenshot_16_03

это вовсе не объект запроса о котором сейчас идет речь, Class: http.ClientRequest это объект запроса если мы используем «http.Client», если мы при помощи Node.JS генерируем этот запрос, а если мы это функционал не используем, то Class: http.ClientRequest в общем не причем.
Нас интересует Class: http.IncomingMessage

screenshot_16_04

Именно Class: http.IncomingMessage это первый параметр функции обработчика запроса и он получает message.url, message.method, message.headers и некоторые другие перечисленные в спецификации свойства. Хотелось бы обратить ваше внимание на то, что в спецификации  Class: http.IncomingMessage находятся не все свойства объекта запроса, потому что Class: http.IncomingMessage наследует другому классу Class: stream.Readable, который представляет собой так называемый поток и эти потоки мы в ближайшем будущем обсудим. По этому и события Event: ‘close и Event: ‘aborted не единственные, есть еще, мы перейдем к ним позже.

Обычный цикл работы сервера состоит из того, что мы узнаем, что именно к нам пришло

screenshot_16_04

читаем там url и другие свойства, а дальше отвечаем, осуществляя запись в объект Class: http.ServerResponse

screenshot_16_05

вот, второй параметр обработчика запросов «request». Если есть желание мы можем поставить свои заголовки, это делается либо вызовом response.writeHead(statusCode[, statusMessage][, headers]) который отправляет их в ответ тут же, либо неявным образом — response.setHeader(name, value), response.statusCode, неявный означает в данном случае, что тут же они отправлены не будут, а подождут первой записи. Впрочем во многих случаях никакие особые заголовки не требуются, можно сразу написать ответ, если это, скажем, веб страница, то ответ можно записать вызовом response.end([data][, encoding][, callback]) и в первом параметре указать текст страницы. Также для записи в ответ есть метод response.write(chunk[, encoding][, callback]), в отличии от response.end([data][, encoding][, callback]) он не закрывает соединение, поэтому response.write(chunk[, encoding][, callback]) используют в тех случаях когда мы хотим много, много раз что то писать в ответ постепенно, например потому что появляются все новые и новые данные, но и в этом случае для того чтобы окончить ответ используют response.end([data][, encoding][, callback]). Соединение будет висеть до того пока response.end([data][, encoding][, callback]) не будет вызван, ну либо пока его клиент не закроет или произойдет какая то ошибка. У объекта есть и другие свойства, события которые здесь не упомянуты потому что эта документация посвящена, собственно, Class: http.ServerResponse, а Class: http.ServerResponse является наследником другого класса который мы в дальнейшем тоже изучим, но на текущий момент мы уже знаем достаточно, чтобы получить запрос и отослать нормально ответ в браузер.

 

15. Эхо-сервер на Node.JS

Эхо сервер

В этой главе, мы создадим эхо сервер. То есть такой сервер, который при запросе на URL/echo с параметром message выдает значение этого параметра

А на все другие запросы, пусть отвечает «страница не найдена». начнем с такого шаблона

Создается сервер и ему дается функция обработчик на request. Первый шаг обработки запроса, это понять, что за запрос к нам к нам пришел. Для этого  мы воспользуемся свойствами «method» и «url» объекта запроса.

запускаю и обращаюсь в браузере к этой странице — http://127.0.0.1:1337/echo?message=Hello
Посмотрите пожалуйста

screenshot_15_01

После того как я перешел в браузере по выше указанному url, программа при помощи console.log() вывела, что метод называется «GET» и было запрошено вот это — «/echo?message=Hello». Обратите внимание, браузер ничего не вывел и что то ожидает, ожидает ответа. Потому что сервер, если не дать явной команды написать что то в ответ, делать ничего не будет. В этом явное отличие Node.js от многих существующих серверов, он делает только то с соединением, что ему скажешь. Если ему не сказать отошли ответ, он ничего не отсылает. Соответственно получается, что соединение есть, функция «request()» отработала, но она ничего не сделала, чтоб ответить на него, поэтому запрос завис. И он будет висеть столько, сколько браузер или сервер считают нужным. Кому первому надоест, тот первый оборвет соединение. Итак, теперь давайте ответим сообщением, которое было прислано в параметре «message». Для этого мне нужно понять, что конкретно в message здесь находится.
Напомню задачу, если url вот такой — «/echo?» и параметр «message» есть, то нужно отослать его значение, а иначе — «страница не найдена». Соответственно, для того чтобы разобрать url, можно конечно воспользоваться регулярными выражениями, но в Node.JS есть для этого специальный модуль который так и называется — «url». Использую его для того, чтобы разобрать переданную строку запроса, соответствующий метод называется «parse()». Смотрим код

и сейчас я запускаю сервер, снова перехожу по адресу -«http://127.0.0.1:1337/echo?message=Hello» и вот, что я вижу в консоле моей IDE

screenshot_15_02

Больше всего нас здесь интересуют параметры: «query» и «pathname». Мы видим, что параметр «message» в «query» есть, но как это определить если «query» это строка? Самый простой способ это добавить еще один аргумент команде «url.parse(req.url, true);» которая, если указано «true»,  разберет эту строку в объект. Соответственно, сделав проверку на наличие всего этого в запросе, если оно есть то выводим значение «message», если нет, тогда нужно ответить — «Страница не найдена», для этого ставится соответствующий код ответа и тело страницы.

Пере запускаю сервер и захожу в браузер

screenshot_15_03

Давайте теперь зайдем по другому url

screenshot_15_04

Если мы на теле страницы, в браузере, нажмем правую кнопку мыши и выберем нижний пункт — «Просмотреть код» и выберем вкладку «Network»

screenshot_15_05

Здесь мы видим, что «Page not found» имеет код ответа «404», а соответственно нормальный url

screenshot_15_06

стандартный код ответа «200». Итак «Эхо сервер» готов и работает. Нам было важно с него начать, потому что эхо сервер является прототипом реального приложения. Реальные приложения тоже получают запросы различного вида и выдают на них ответы.

Работа с заголовками

следующая наша тема это работа с заголовками, я прошу на нее обратить особое внимание, если вы в серверной разработке новичок и раньше разрабатывали преимущественно на клиенте. Когда браузер делает запрос, то вместе с url он отправляет дополнительную информацию, в которой указывает, что это за браузер и детали того, что хочет запросить. Информация эта в специальном формате, называется заголовками, и доступна она так — «req.headers». Например в данном случае, я сейчас запускаю сервер, перехожу на страницу браузера, обновляю, возвращаюсь в WebStorm и вот заголовки которые отправил браузер

screenshot_15_07

Каждый заголовок имеет имя и значение. Обратите внимание, что все заголовки в Node.JS имеют имя в нижнем регистре это особенность именно Node.JS. В данном случае «host» оказывается передается в заголовке — «host: ‘127.0.0.1:1337′»,  «connection: ‘keep-alive'» — означает, что браузер, хотел бы по этому соединению гнать все новые и новые запросы, то о чем мы говорили раньше, connection один, реквестов много. Далее детали того, что это за браузер и что он хотел бы получить. В ответ на такой запрос сервер отвечает конечно же телом страницы и тоже заголовками, в которых в частности находится статус. Как правило статус это «200», такой вот статус по умолчанию

называется он «OK» и означает, что страница сгенерирована нормально. Другой распространенный статус — «404»  и называется  «Not Found», страница не найдена.

Есть и другие статусы, например — «403» — доступ запрещен, или «500» — серверная ошибка.

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

Обновляю браузер, во вкладку «Network» захожу и вижу здесь все эти заголовки.

screenshot_15_08

 

Заголовки которые браузер отправил — «Requests Headers» и заголовки которые получил браузер от сервера — «Response Headers». Нажав «view sourse»

screenshot_15_09

мы получаем более подробную картину. Появились дополнительные заголовки, это более подробный вид. Статус «OK», как мы говорили и прочие заголовки, они здесь более менее стандартные в данном случае. Давайте поставим какие нибудь другие, наиболее важные типы заголовков, которые приходится ставить в ручную, это заголовки которые касаются типов содержимого или кеширования. Например если я делаю веб сервис, то я вполне возможно не хочу, чтобы результаты ответа сервера кешировались. Для этого я поставлю заголовок —

можно сделать его еще понадежней, добавив еще опций —

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

и в браузере посмотрим, что у нас с заголовками

Изменения легко заметить, появился дополнительный заголовок, как раз тот который мы высылали. Есть и много других заголовков, с ними вы можете ознакомиться либо в описании протокола «HTTP» в различных учебниках по кешированию и так далее, а с какими то заголовками мы еще познакомимся в процессе, когда они понадобятся.

А теперь немножко отойдем от общего «http» и вернемся к Node.JS. Для того, чтобы работать с заголовками у ответа «response» есть два метода, принципиально разные. Первый метод продемонстрирован в нашем примере

«statusCode» ставиться и «setHeader» или есть еще метод «removeHeader» — добавляют или, соответственно, удаляют заголовок. При этом сами заголовки будут отправлены на сервер, не сейчас, не когда написался этот Header, а вместе с ближайшей записью каких то данных например вызов «resEnd» отправляет ответ и заголовки тоже. Второй способ управления заголовками называется явный, выглядит он так

тут мы пишем статус — «200», описание статуса — «OK» и объект с заголовками — «{‘Cache-control’: ‘no-cache’}». Он отличается тем, что при этом заголовки пишутся в ответ тут же, не ожидая начала ближайшей записи. Иногда такое тоже бывает нужным, но обычно вполне хватает способов, которыми мы пользовались до этого — «statusCode» и «setHeader».

14. Node.JS как веб-сервер

Всем привет, в этой главе мы познакомимся с Node.JS уже в роли веб сервера. Создадим для этого новое приложение. Сейчас я нахожусь в редакторе «WebStorm», в совершенно пустой директории. Первым делом мы  немного настроим проект, мы это уже проходили, посмотреть можно здесь. Далее я создаю файл «server.js»

screenshot_14_01

и в нем, первым делом, подключаю модуль «http». Об этом модуле, мы еще поговорим подробнее, но на текущий момент нам необходим из него один объект. Это «http.Server()», как следует из его названия, он умеет слушать IP и Порт и отвечать на входящие запросы. Для того, чтобы дать ему IP и Порт используется команда «listen()». Ну а для того, чтобы отвечать на запросы, используются события. «http.Server()» является «EventEmitter» и при входящих запросах, соответствующие событие инициируется, которое называется «request», и его обработчик получает два объекта, первый «req» это входящий запрос, он содержит информацию, которую присылает браузер, включая в частности URL пришедшего запроса, и второй параметр «res» это объект ответа, из первого мы читаем во второй пишем. Конкретно наша функция, тут же заканчивает выполнение запроса, отсылая фразу привет мир. Давайте мы посмотрим как это работает. Для этого я создам конфигурацию, смотрим по шагам

screenshot_14_02 screenshot_14_03 screenshot_14_04 screenshot_14_05

screenshot_14_060

screenshot_14_07 screenshot_14_08

Теперь зайдем по этому адресу в нашем браузере — «server.listen(1337, ‘127.0.0.1’);»

screenshot_14_09

Упс, кодировка слетела, давайте  добавим код, который в <head> добавит правильную кодировку

screenshot_14_10

Смотрим

screenshot_14_11

Замечательно, по виду все работает. Чтоб в этом убедиться создадим переменную «counter» которая будет выводить текущий счетчик запросов.

screenshot_14_12

Для проверки в браузере нужно перезагрузить сервер, потому что Node.JS устроен так, что запустившись и считав файл модуля, в данном случае файл «server.js», Node.JS создает из него объект module и этим объектом в дальнейшем пользуется. Соответственно при изменениях данного файла, Node.JS просто не подбирает эти изменения, потому что файл уже обработан. Соответственно, чтобы заставить Node.JS перечитать файл, самый простой способ запустить сервер еще раз. Но если я сейчас нажму Play

screenshot_14_13

То вот, что мы увидим

screenshot_14_14

Ошибка «EADDRINUSE» означает, что адрес уже используется, потому что node server.js попытался запустится еще раз, не прекратив действие уже запущенной программы. Предыдущий сервер  уже занял этот IP и Порт. Что делать? Например можно взять и подредактрировать конфигурацию добавив «Single instance only», ну и имя дадим нашему серверу, заодно

screenshot_14_15 screenshot_14_16

Это означает, что этот сервер может быть запущен в единственном экземпляре. И тогда повторный play убьет текущий запуск, и перезапустит сервер. Если вы в консоле работаете, то это вообще не проблема, прибиваете текущий Node.JS и стартуете новый. В дальнейшем мы посмотрим как это оптимизировать.

И так, теперь перехожу в браузер и нажимаю рестарт. Счетчик увеличивается, но не на один, а на два. Это все потому что браузер устроен так, что вместе со страничкой он делает еще один запрос, этот запрос делается на url «favicon.ico». В данном случае мы никакую favicon не отдаем, поэтому браузер каждый раз его повторяет, поэтому одно обновление страницы приводит к двум запросом. По крайней мере в текущем контексте в хроме. Что ж, мы сделали наш первый серверный Node.JS. В дальнейшем мы будем работать, чтобы сделать его гораздо мощнее и интереснее.

Перед тем как мы продолжим, не большая ремарка. Если вы посмотрите сам сайт Nide.JS то пример аналогичный этому выглядит там немножко по другому, он выглядит так

Основное отличие здесь в том, что мы используем «new Server()», а на сайте «http.createServer()» — это одно и то же. Ну и далее у нас обработчик запросов ставиться явно, а в примере с сайта он передается аргументом. Передача аргументов, это тоже установка обработчика, просто такой дополнительный синтаксис. Он чуть чуть короче, но я выбрал для примера другой синтаксис потому что он более нагляден и показывает то, что реально происходит.

А теперь, если вы внимательно читали, то у вас не вызовет проблемы ответить на следующий небольшой вопрос: в нашем примере есть обработчик событий ‘request’, скажите пожалуйста, как не глядя в документацию сказать какие еще есть события для сервера и когда они вызываются, в каком порядке, при обработке этого запроса?  Почему я прошу не смотреть документацию? Тут две причины, во первых так интересней, во вторых «Server()», вот этот

screenshot_14_17

на самом деле наследует от «net.Server», а он от «EventEmitter».

http.Server -> net.Server -> EventEmitter 

Соответственно некоторые события сервера описаны в «http.Server», а некоторые в документации к «net.Server» и просто взглянув в книгу, понять точный порядок событий, достаточно сложно.  Подсказка находится вот в этой строке — http.Server -> net.Server -> EventEmitter 
Сервер является «EventEmitter», это означает, что все события генерируются вызовом «server.emit» понятно, что мы сами не вызываем этот метод, в данном случае его вызывает сам Node.JS, а мы просто ставим обработчики. Но ничто нам не мешает, чисто джава скриптовым трюком, переопределить этот метод на наш собственный. Метод «emit» принимает название события и необходимые данные для него, в данном случае это будут «req» и «res». Но нас интересует только название события, которое мы при помощи «console.log()» будем выводить. Ну а далее мы будем передавать вызов исходному методу «emit». Посмотрим что получилось, перезапускаем сервер и тут же видим первое  событие «listening», оно означает, что сервер начал слушать соединение.

screenshot_14_18

Далее захожу в браузер и перехожу по адресу «http://127.0.0.1:1337/». А теперь обратите внимание перезагружаю страницу браузера и мы видим, что «request» в консоле WebStorm увеличивается, причем по две штуки, ну мы знаем почему, а «connection» было одно, другое в общем мало.

screenshot_14_19

Потому что событие «connection» возникает тогда, когда браузер открывает к серверу новое соединение, сетевое, а «request» присылает запрос. Браузер устроен так, что одно сетевое соединение он старается использовать по максимуму, называется это «Keep-Alive», он его сохраняет и по нему гонит новые и новые запросы.

Итак, мы создали простейший веб сервер, познакомились с соответствующим объектом «http.Server» и событиями которые в нем возникают.

13. События, EventEmitter и утечки памяти

Следующий объект который нас интересует, это «EventEmitter» или как его иногда любовно называют, «ee». «EventEmitter» представляет собой основной объект реализующий работу с событиями в Node.JS. Большое количество других встроенных объектов, которые генерируют события в Node.JS, ему наследуют. Для того, чтобы воспользоваться «EventEmitter», достаточно подключить ‘events’, встроенный и взять у него соответствующее свойство.

после чего я могу создать новый объект

и у него есть методы, для работы с событиями. Первый метод это подписка, «on».

Который принимает два аргумента, это имя события — ‘request’ и функцию — обработчик. Я могу указать много подписчиков и все они будут вызваны в том же порядке в котором назначены.

Второй, основной метод это «emit». Он принимает два аргумента, первый это событие которое он должен генерировать, второй это данные которые должен передать.

Эти данные попадают в функцию обработчик. Соответственно, если предположить, что мы пишем веб сервер, то в одном месте кода будет обработчик запросов

Веб сервер — «server», при запросе — ‘request’,  что то с ним делает — «function(request)».

А совсем в другом месте кода, например в обработчике входящих соединений, будет «server.emit()», который генерирует события.

Вот наш код целиком

Давайте его запустим

Screenshot_13_01

Как видите, оба события были обработаны, сначала этим обработчиком

а потом вот этим

Подробное описание различных методов для работы с событиями, вы конечно найдете в документации, а мы с вами остановимся на том, что принципиально отличает работу с событиями в Node.JS от работы с браузерными событиями. И первое отличие мы сразу можем увидеть из разбора нашего примера. Если браузерные обработчики срабатывают в произвольном порядке, то обработчики событий в Node.JS срабатывают точно в том порядке в котором они были назначены. То есть если у меня есть какие то обработчики, то назначая следующий, я точно уверен, он сработает после предыдущих. Еще одно отличие в том, что в браузере я никак не могу получит список обработчиков, которые назначены на определенный элемент, а в Node.JS это сделать легко.

Screenshot_13_02

«emitter.listeners(eventName)» возвращает все обработчики на данное событие, а «emitter.listenerCount(eventName)» возвращает их количество, раньше был «EventEmitter.listenerCount(emitter, eventName)», но как мы видим в доке

Screenshot_13_03

Следующее, пожалуй наиболее важное отличие состоит в том, что в «EventEmitter» специальным образом обрабатываются события с названием ‘error’. Если где либо происходит emit этого события и у него нет обработчика, то «EventEmitter» генерирует исключения. таким образом с виду такой безобидный emit

повалит весь процесс. Исключения генерируются встроенного типа — «TypeError» — это если в таком виде как на примере выше, а если так

Если есть какой то объект в аргументах, который является «instanceof.Error», например «new Error()» то этот объект будет использован в качестве аргумента «throw» — «thow err».

Например если я запущу этот код

и мы все видим

screenshot_13_04

нода упала с исключением, если же есть обработчик, хоть какой нибудь

то все будет нормально, запускаю

screenshot_13_05

работает.

Если в «emit» передать объект, который будет описывать, что именно за ошибка была, то он будет передан в обработчик, там мы можем его разобрать и произвести  какие либо действия, чтобы обработать.

screenshot_13_06

Наконец последняя особенность «EventEmitter» о которой сейчас пойдет речь это встроенное средство для борьбы с утечками памяти. Для разбора посмотрим на вот этот пример

Здесь каждые 200 миллисекунд создается новый объект — «new Request()», и выводится текущее поедание памяти — «console.log(process.memoryUsage().heapUsed);». Объект типа «Request()», в реальной жизни это может быть запрос от клиента, ну а здесь это просто некий объект у которого есть поле — «this.bigData», в котором содержится, что то жирное — «new Array(1e6).join(‘*’)», просто, чтоб было видно сколько памяти на самом деле съедается. Соответственно, если много таких объектов будет в памяти, то есть они тоже будут много. Ну еще у этого объекта есть пара методов,

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

screenshot_13_07

Как легко увидеть, память вырастает и очищается, потом опять вырастает и опять очищается.  Пока у нас все хорошо и это нормальный режим функционирования Node.JS. Ведь в данном случае «var request = new Request();» это локальная переменная данной функции и после окончания работы функции она нигде не сохраняется, этот объект более не нужен и память из под него можно очистить.

Теперь же немножко расширим этот пример. Добавим объект источника данных, который мы назовем «db» и который может посылать какую то информацию, которую «Request()» может в свою очередь пересылать клиенту.

изменение не большое, посмотрим к чему это приведет при запуске кода

screenshot_13_08

ого, какой то warning, память постоянно растет, в чем же дело? Для того, чтобы это понять, немножко глубже познакомимся  с алгоритмом работы «EventEmitter», а именно с тем как работают вообще эти события, что происходит когда я вызываю «db.on()».  Информацию о том, что я поставил где то обработчик, нужно где то запомнить и действительно она запоминается в специальном свойстве объекта «db».  В этом свойстве находятся все обработчики событий, которые назначены и когда происходит вызов «db.emit()», то они из него берут и вызываются. Теперь можно понять, от чего возникла утечка. Ведь не смотря на то, что «var request = new Request();» здесь по идеи больше не нужен, как в прошлом примере, эта функция — обработчик, которую мы передаем «db.on()» в качестве аргумента — «function(info){ self.send(info); }» находится в свойствах объекта «db». И получается так, что каждый «Request()» который создается сохраняет там внутри объекта «db» функцию обработчик «function(info){ self.send(info); }», а она, через замыкание, ссылается на весь объект «Request()» и получается что обработчик «db.on()» привязывает «Request()» к «db». Пока живет «db» будет жить и «Request()». Происходящие можно легко увидеть если добавить «consol.log(db)» и запустить код еще раз

screenshot_13_09

Здесь мы видим объект «db» и вот как раз свойство «events» в котором находятся обработчики, и оно действительно все время увеличивается по размеру. Было сначала маленькое, потом растет функций все больше и каждая функция, через замыкание, тянет за собой весь объект «Request()». А вот и warning

screenshot_13_10

Оказывается у «EventEmitter» есть максимальное число обработчиков, которых можно назначить и оно равно 10. Как только это число превышается, то он вот такое предупреждение выводит, что может быть утечка памяти. Которая в нашем случае как раз и произошла. Что делать? Как вариант, можно, например, взять и после окончания обработки запроса убрать обработчики на событие ‘data’. Для этого код нужно немного переписать, добавить вызов метода «request.end()» в конце.

И вот, что будет при таком вызове.

screenshot_13_11

Теперь все хорошо, никакой утечки памяти не происходит.

Когда такой сценарий наиболее опасен? Он наиболее опасен в тех случаях, если по какой то причине максимальное количество обработчиков отключают, то есть делают — «db.setMaxListners(0);», предполагая, что много кто может подписываться на эти события. Действительно бывают такие источники событий для которых много много подписчиков возможно и нужно отменить этот лимит. Соответственно лимит отменяют, а вот убирать обработчики забывают. И вот это как раз и приводит к тому, что нода растет и растет  в памяти.

Как отследить эти утечки? Это достаточно проблематично, может помочь такой модуль, называется «heapdump», который позволяет делать снимок памяти Node.JS и потом анализировать его в Chrome. Но лучшая защита, это думать, что делаешь, когда привязываешь коротко живущие объекты слушать события долго живущих. И помнить о том, что может понадобится от них отвязаться, чтобы память была очищена.

Итак, «require(‘events’).EventEmitter» один из самых важных и широко используемых объектов в Node.JS. Сам по себе «EventEmitter», на самом деле используется редко, в основном используются наследники этого класса, такие как объект запроса, объект сервера и много, много всего другого, мы с этим в ближайшем будущем уже столкнемся.

  • Для генерации события используется «emit» -> «emit(event, args…) -> on(event, args…)»  ему передается название события и какие то аргументы данные, при этом он вызывает обработчики, назначенные через «on()».
  • Сохраняет порядок обработчиков. Гарантирует, что обработчики будут вызваны в том же порядке.
  • Можно проверить наличие обработчиков. При этом в отличии от браузерных обработчиков всегда можно проверить, есть ли какие то обработчики на определенное событие.
    Получить обработчики — «emitter.listeners(event)»
    Получить количество обработчиков — «EventEmitter.listenerCount(emitter, event)»
    Кроме того сам метод «emit()», если событие было обработано, возвращает «true» если нет, то «false». Правда используется эта фишка достаточно редко.
  • «emit(error) без обработчиков -> throw. Дальше в «EventEmitter» есть специальное событие, называется «error», если на это событие нет обработчиков, то это приводит к тому, что «EventEmitter» сам делает «thow». Казалось бы зачем, но как мы скоро убедимся, это решение очень мудрое и полезное, потому что многие встроенные объекты в Node.JS сообщают о своих ошибках именно так, через «emit(error)» и без такого «throw» их было бы очень легко пропустить, забыть о них и потом долго искать, что же где случилось.
  • Борется с утечками памяти. И наконец последнее, в «EventEmitter» есть встроенные средства, по борьбе с утечками памяти. Сейчас они нам пока не очень нужны, но в дальнейшем. когда мы будем делать проект на Node.JS, они нам еще пригодятся.

 

12. Наследование от ошибок Error

Несколько особняком, в  Node.JS, стоит наследование от встроенного объекта ошибки «Error». На этом примере

я сначала объясню, зачем оно может понадобиться, а потом как его делать правильно.
Здесь код состоит, по сути, из двух функций. Первая «function getPhrase(name)» это самая простейшая функция по интернационализации, которая только может быть. Она берет название фразы «name» и возвращает соответствующий перевод, если он есть. А если нет, то исключение. Вторая «function makePage(url)» получает «url», сейчас она умеет работать только с «index.html», поэтому, для остальных, она кидает ошибку, а если это «index.html» то возвращает такую вот форматированную строку — «return util.format(«%s, %s!», getPhrase(«Hello»), getPrase(«world»));». Давайте посмотрим, как работает такой код, запускаем

Screenshot_12_01

пока работает.
А теперь давайте добавим к этому коду правильную обработку ошибок. Например, если мы получили не корректный URL, несуществующий,

тогда «function makePage(url)» бросит ошибку, «Нет такой страницы». Если представить себе, что это веб сервер, то означает, что мы должны вывести юзеру сообщение «HTTP 404» Страница не найдена. Это один вариант ошибки, одна обработка. А посмотрим, что если фраза неизвестна. Если в каком то месте кода, мы вызвали получение фразы например вот так

Нет такой фразы, это уже совсем другая ошибка. И тогда нужно уже не 404 сделать, а статус 500 и написать уведомление системному администратору, что что-то тут не так, что словарь не полон и его срочно нужно поправить. Это программная ошибка, по этому обрабатывать ее нужно по другому.

К сожалению в текущем коде, даже если я добавлю «try/catch» к функции «makePage», все равно понять где какая ошибка, нельзя. И то и другое, это просто ошибки — класс «Error». С точки зрения объектно ориентированного программирования разумный способ решить эту проблему, это сделать свои объекты ошибки для разных случаев.

Это будет «PhraseError» и «HttpError», в ее конструкторе мы заодно укажем статус.

Что ж, объявим соответствующие классы при помощи «util.inherits»

Сразу же посмотрим на особенности работы над объектом ошибок. Какие нам свойства здесь важны. Первое, конечно же, «message». Для того, чтобы поставить «message» мне нужно сделать это вручную. Это есть особенность такая, работы с ошибками. То есть не вызов стандартного родителя суперкласса — вот так мы обычно конструктор супер класса вызываем

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

Следующие свойство, которое есть у всех встроенных ошибок, это «name». По свойству «name» мы можем точно понять, что за ошибка у нас есть. Оно у всех встроенных ошибках в прототипе у нас есть, по этому и сюда тоже мы запишем.

и

Наконец, последнее свойство, которое нам будет важно, это «stack». О нем мы поговорим чуть позже.

И так, давайте я сейчас запущу этот код. Как видим тут

«makePage(‘index’)» это неизвестный URL, по этому мы должны получить ошибку ‘HttpError’. Такую ошибку мы обрабатываем вот так

То есть просто выводим, без всякой паники. Запускаю

Screenshot_12_02

Увидели то, что должны были увидеть, все работает.

Теперь посмотрим другой вариант, а именно, если URL правильный, но ошибка произошла в программе, то есть видите, тут какая то не понятная фраза «Hell» вместо «Hello»

В данном случае, то есть если ошибка какая то другая, мы должны на нее отреагировать тоже иначе. Паника, кошмар, программная ошибка, срочно всех поднимаем, исправляем. Эта ветка кода будет действовать для всех программных ошибок, в том числе и для встроенных, а не только для «PhraseError». Запускаем

Screenshot_12_03

Вывело действительно «console.error», посмотрим на свойства —

  • Ошибка — «PhraseError», первое правильно
  • сообщение — «Нет такой фразы: Hell», тоже правильно
  • стек — «undefined»

Стек, это можно сказать самое важное. Свойство «stack» должно хранить информацию о том где, в каком файле произошла эта ошибка, что ей предшествовало.

Здесь, в конструкторах

я не произвел никаких действий, специальных которые правильно поставят стек. И в стандартах JavaScript их вообще не предусмотрено. Я просто создаю новый объект.

На самом деле есть разные способы которые все таки позволяют получить стек, но здесь они нам не понадобятся, поскольку в «V8» есть специальная JavaScript команда, которая не входит в стандарт, но позволяет получит стек. Выглядит она так

Эта команда получает текущий стек, то есть последовательность сложенных вызовов, которые привели к текущему месту кода и сохраняет его в «this», то есть в объекте ошибки. Давайте посмотрим на наш код

Запустим

Screenshot_12_04

Вот теперь, оно вывело стек, то есть где это все произошло. Но если посмотреть внимательно, здесь есть небольшой грешок. А именно, ошибка произошла, на самом деле, вот здесь

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

Screenshot_12_05

до «getPhrase», а то что происходило внутри «new PhraseError» нас в принципе не интересует. То есть вот эта строчка стека

Screenshot_12_06

в данном случае лишняя. Чтобы убрать ее, в «captureStackTrace(this)» предусмотрен специальный параметр, второй, необязательный, это функция, до которой будет собираться «stackTrace». То есть если здесь я укажу текущий конструктор

то при этом, тот стек который внутри указанной функции, не будет показан в выводе, не будет собран

Screenshot_12_07

Я перезапустил и лишняя строка теперь вырезана.

И так мы получили унаследованные объекты ошибки с правильными свойствами «message», «name» и с возможностью вывести «stack».

11. Модуль console

А теперь поговорим о следующем встроенном модуле, «console». Как мы знаем уже, он использует «util.format» и «util.inspect» для вывода. Что еще интересного? «console», в отличии от «util», является глобальной переменной, это большая редкость в Node.JS, действительно «console» мы не должны «require()». Она у нас есть везде.

Теперь кратко взглянем на основные методы этого объекта, к счастью их всего два.

console.log(), console.error()

  1. «console.log()» или, что, то же самое, «console.info()».
  2. «console.error()» или, что, то же самое, «console.warn()».

Зачем есть два метода? Давайте я их сейчас запущу

Screenshot_11_01

Запустил. Есть разница? По виду нет. Но на самом деле разница существует и она заключается в том, что «log» выводит стандартный поток вывода, а «error» выводит поток ошибок.  Это системные термины, смысл такой, что у каждой программы у каждого процесса, есть как минимум два потока вывода. Первый считается нормальным выводом, второй  считается ошибками, можно их, например направить в разные файлы.

Screenshot_11_02

То, что выведено в первый поток, пойдет в файл «ok», сейчас там находится «Log», во второй в «err», сейчас там «Error». Посмотрим

Screenshot_11_03

Таким образом, если мы делаем перенаправление вывода и правильно используем «error» и «log», то мы всегда сможем отличить хорошее сообщение от ошибки.

Я использовал команду «type» для просмотра содержимого файлов. Это под Windows. Подробнее про «type» можно посмотреть здесь. В Mac OS используйте команду «cat».

console.trace()

Кроме этих методов, есть еще метод

Который используется достаточно редко, он выводит текущий StackTrace, тоже в поток ошибок, бывает иногда полезно, но редко.

console.debug()

Наконец

Обращаю ваше внимание на этот метод, точнее на его отсутствие. В браузерах он обычно есть, а в Node.JS его нету.

10. Модуль util и наследование

Всем привет, тема ближайших глав — самые часто используемые модули Node.JS. Мы уделим им время именно сейчас, потому, что потом, когда мы будем их использовать, у нас просто не будет возможности отойти в сторону резко, и посмотреть, что как там работает.

util.inspect()

Первым методом, который мы изучим, будет метод «util.inspect()», модуля встроенного в «util». Этот метод позволяет красиво вывести любой объект, даже если у этого объекта, как в этом примере, есть ссылка на самого себя.

запускаем

Screenshot_10_01

и видим, что, действительно красиво обработалось. При этом, если у объекта есть свойство, «inspect», и оно равно функции, то эта функция будет вызвана и именно ее результат, будет возвращен. Например

Видите, я сделал функцию «inspect»

Screenshot_10_02

И вернулось «123». Таким образом поведение метода «util.inspect()», несколько напоминает «toString». И этот метод используется консолью автоматически, если она хочет залоггировать какой то объект

видите, я убрал «util.inspect()», вызываю

Screenshot_10_02

все работает так же. Потому что на самом деле консоль вызывает его внутри. Однако иногда, все же, необходимо обращаться к «util.inspect()» явно, в первую очередь в тех случаях, когда мы хотим вывести что-то не в консоль, а например получить строчное представление объекта для вывода в файл. Кроме того, есть дополнительные параметры «util.inspect()», можно например задать глубину вывода объекта, по умолчанию два, но они используются редко. Подробнее здесь.

util.format()

Следующая команда, это «util.format()». «util.format()» получает строку и дальше, следующие параметры он в нее подставляет

То есть вместо «%s» будет выведена строка «string», вместо «%d» будет выведено число «123», а далее вместо «%j» будет выведен объект в формате jason — «{test: «obj»}». Если я сейчас это запущу, то вот, что я получу

Screenshot_10_03

Обратите внимание, это формат json, это не вывод «util.inspect()». Соответственно если вдруг я вместо числа передал здесь что-то еще

То при выводе я получу «NaN», потому что  автоматически происходит конвертация.

Screenshot_10_04

Метод «util.format()» так же используется в консоли, не явно. Если я сейчас возьму это и перенесу в консоль

То все отлично продолжит работать.

Screenshot_10_05

Подробнее про «util.format()», здесь.

util.inherits()

Следующий, последний метод «util» о котором пойдет речь, это метод «util.inherits()». Чтобы было легче его понять, я позволил себе скачать исходники «Node.JS» и достать из них файл «util.js», это как раз и есть исходник модуля «util». И там, на чистом JavaScript, вот он метод

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

Впрочем использовать его можно и так.

Достаточно создать родительский класс, конструктор, методы в прототипе. Затем, чтобы унаследовать от него, создаем конструктор потомка и вызываем «util.inherits()». Затем добавляем в прототип методы. Получается, что все объекты создаваемые этим конструктором будут наследовать от «Animal», так что если я сейчас запущу этот код, то «rabbit.walk();» сначала вызовет метод родителя, а «rabbit.jump();» вызовет метод потомка.

Screenshot_10_06

Все как обычно при наследовании. Подробнее, опять таки здесь.