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

Мы начинаем публикацию серии материалов, которые представляют собой поэтапный перевод руководства по Node.js для начинающих. А именно, в данном случае «начинающий» — это тот, кто обладает некоторыми познаниями в области браузерного JavaScript. Он слышал о том, что существует серверная платформа, программы для которой тоже пишут на JS, и хотел бы эту платформу освоить. Возможно, вы найдёте здесь что-то полезное для себя и в том случае, если уже знакомы с Node.js.

Кстати, в прошлом году у нас был похожий по масштабам проект, посвящённый bash-скриптам. Тогда мы, после публикации всех запланированных материалов, собрали их в виде PDF-файла. Так же планируется поступить и в этот раз.

Сегодня мы обсудим особенности Node.js, начнём знакомство с экосистемой этой платформы и напишем серверный «Hello World».

[Советуем почитать] Другие части цикла

Часть 1: Общие сведения и начало работы
Часть 2: JavaScript, V8, некоторые приёмы разработки
Часть 3: Хостинг, REPL, работа с консолью, модули
Часть 4: npm, файлы package.json и package-lock.json
Часть 5: npm и npx
Часть 6: цикл событий, стек вызовов, таймеры
Часть 7: асинхронное программирование
Часть 8: Руководство по Node.js, часть 8: протоколы HTTP и WebSocket
Часть 9: Руководство по Node.js, часть 9: работа с файловой системой
Часть 10: Руководство по Node.js, часть 10: стандартные модули, потоки, базы данных, NODE_ENV
Полная PDF-версия руководства по Node.js

Обзор Node.js

Node.js — это опенсорсная кроссплатформенная среда выполнения для JavaScript, которая работает на серверах. С момента выпуска этой платформы в 2009 году она стала чрезвычайно популярной и в наши дни играет весьма важную роль в области веб-разработки. Если считать показателем популярности число звёзд, которые собрал некий проект на GitHub, то Node.js, у которого более 50000 звёзд, это очень и очень популярный проект.

Платформа Node.js построена на базе JavaScript движка V8 от Google, который используется в браузере Google Chrome. Данная платформа, в основном, используется для создания веб-серверов, однако сфера её применения этим не ограничивается.

Рассмотрим основные особенности Node.js.

▍Скорость

Одной из основных привлекательных особенностей Node.js является скорость. JavaScript-код, выполняемый в среде Node.js, может быть в два раза быстрее, чем код, написанный на компилируемых языках, вроде C или Java, и на порядки быстрее интерпретируемых языков наподобие Python или Ruby. Причиной подобного является неблокирующая архитектура платформы, а конкретные результаты зависят от используемых тестов производительности, но, в целом, Node.js — это очень быстрая платформа.

▍Простота

Платформа Node.js проста в освоении и использовании. На самом деле, она прямо-таки очень проста, особенно это заметно в сравнении с некоторыми другими серверными платформами.

▍JavaScript

В среде Node.js выполняется код, написанный на JavaScript. Это означает, что миллионы фронтенд-разработчиков, которые уже пользуются JavaScript в браузере, могут писать и серверный, и клиентский код на одном и том же языке программирования без необходимости изучать совершенно новый инструмент для перехода к серверной разработке.

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

▍Движок V8

В основе Node.js, помимо других решений, лежит опенсорсный JavaScript-движок V8 от Google, применяемый в браузере Google Chrome и в других браузерах. Это означает, что Node.js пользуется наработками тысяч инженеров, которые сделали среду выполнения JavaScript Chrome невероятно быстрой и продолжают работать в направлении совершенствования V8.

▍Асинхронность

В традиционных языках программирования (C, Java, Python, PHP) все инструкции, по умолчанию, являются блокирующими, если только разработчик явным образом не позаботится об асинхронном выполнении кода. В результате если, например, в такой среде, произвести сетевой запрос для загрузки некоего JSON-кода, выполнение потока, из которого сделан запрос, будет приостановлено до тех пор, пока не завершится получение и обработка ответа.

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

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

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

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

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

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

▍Библиотеки

Благодаря простоте и удобству работы с менеджером пакетов для Node.js, который называется npm, экосистема Node.js прямо-таки процветает. Сейчас в реестре npm имеется более полумиллиона опенсорсных пакетов, которые может свободно использовать любой Node.js-разработчик.
Рассмотрев некоторые основные особенности платформы Node.js, опробуем её в действии. Начнём с установки.

Установка Node.js

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

Существует ещё один весьма удобный способ установки Node.js, который заключается в использовании менеджера пакетов, имеющегося в операционной системе. Например, менеджер пакетов macOS, который является фактическим стандартом в этой области, называется Homebrew. Если он в вашей системе есть, вы можете установить Node.js, выполнив эту команду в командной строке:

brew install node

Список менеджеров пакетов для других операционных систем, в том числе — для Linux и Windows, можно найти здесь.

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

Я посоветовал бы начинающим пользоваться официальными установщиками Node.js. Пользователям macOS я порекомендовал бы устанавливать Node.js с помощью Homebrew. Теперь, после того, как вы установили Node.js, пришло время написать «Hello World».

Первое Node.js-приложение

Самым распространённым примером первого приложения для Node.js можно назвать простой веб-сервер. Вот его код:

const http = require('http')
const hostname = '127.0.0.1'
const port = 3000
const server = http.createServer((req, res) => {
  res.statusCode = 200
  res.setHeader('Content-Type', 'text/plain')
  res.end('Hello Worldn')
})
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`)
})

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

node server.js

Для проверки сервера откройте какой-нибудь браузер и введите в адресной строке http://127.0.0.1:3000, то есть — тот адрес сервера, который будет выведен в консоли после его успешного запуска. Если всё работает как надо — на странице будет выведено «Hello World».

Разберём этот пример.

Для начала, обратите внимание на то, что код содержит команду подключения модуля http.

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

Метод createServer() объекта http создаёт новый HTTP-сервер и возвращает его.

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

Когда сервер получает запрос, вызывается событие request, предоставляющее два объекта. Первый — это запрос (req, объект http.IncomingMessage), второй — ответ (res, объект http.ServerResponse). Они представляют собой важнейшие механизмы обработки HTTP-запросов.

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

Второй нужен для формирования и отправки ответа на запрос.

В данном случае ответ на запрос мы формируем следующим образом. Сначала устанавливаем свойство statusCode в значение 200, что указывает на успешное выполнение операции:

res.statusCode = 200

Далее, мы устанавливаем заголовок Content-Type:

res.setHeader('Content-Type', 'text/plain')

После этого мы завершаем подготовку ответа, добавляя его содержимое в качестве аргумента метода end():

res.end('Hello Worldn')

Мы уже говорили о том, что вокруг платформы Node.js сформировалась мощная экосистема. Обсудим теперь некоторые популярные фреймворки и вспомогательные инструменты для Node.js.

Фреймворки и вспомогательные инструменты для Node.js

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

  • Express. Эта библиотека предоставляет разработчику предельно простой, но мощный инструмент для создания веб-серверов. Ключом к успеху Express стал минималистический подход и ориентация на базовые серверные механизмы без попытки навязать некое видение «единственно правильной» серверной архитектуры.
  • Meteor. Это — мощный фулстек-фреймворк, реализующий изоморфный подход к разработке приложений на JavaScript и к использованию кода и на клиенте, и на сервере. Когда-то Meteor представлял собой самостоятельный инструмент, включающий в себя всё, что только может понадобиться разработчику. Теперь он, кроме того, интегрирован с фронтенд-библиотеками, такими, как React, Vue и Angular. Meteor, помимо разработки обычных веб-приложений, можно использовать и в мобильной разработке.
  • Koa. Этот веб-фреймворк создан той же командой, которая занимается работой над Express. При его разработке, в основу которой легли годы опыта работы над Express, внимание уделялось простоте решения и его компактности. Этот проект появился как решение задачи внесения в Express серьёзных изменений, несовместимых с другими механизмами фреймворка, которые могли бы расколоть сообщество.
  • Next.js. Этот фреймворк предназначен для организации серверного рендеринга React-приложений.
  • Micro. Это — весьма компактная библиотека для создания асинхронных HTTP-микросервисов.
  • Socket.io. Это библиотека для разработки сетевых приложений реального времени.

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

Краткая история Node.js

В этом году Node.js исполнилось уже 9 лет. Это, конечно, не так уж и много, если сравнить этот возраст с возрастом JavaScript, которому уже 23 года, или с 25-летним возрастом веба, существующем в таком виде, в котором мы его знаем, если считать от появления браузера Mosaic.

9 лет — это маленький срок для технологии, но сейчас возникает такое ощущение, что платформа Node.js существовала всегда.

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

Теперь поговорим о технологиях, лежащих в основе Node.js и кратко рассмотрим основные события, связанные с этой платформой.

Итак, JavaScript — это язык программирования, который был создан в Netscape как скриптовый язык, предназначенный для управления веб-страницами в браузере Netscape Navigator.

Частью бизнеса Netscape была продажа веб-серверов, которые включали в себя среду, называемую Netscape LiveWire. Она позволяла создавать динамические веб-страницы, используя серверный JavaScript. Как видите, идея использования JS для серверной разработки гораздо старше чем Node.js. Этой идее почти столько же лет, сколько и самому JavaScript, но во времена, о которых идёт речь, популярности серверный JS не снискал.

Одним из ключевых факторов, благодаря которому платформа Node.js стала столь распространённой и популярной, является время её появления. Так, за несколько лет до этого JavaScript начали считать серьёзным языком. Случилось это благодаря приложениям Web 2.0, вроде Google Maps или Gmail, которые продемонстрировали миру возможности современных веб-технологий.

Благодаря конкурентной войне браузеров, которая продолжается и по сей день, серьёзно возросла производительность JavaScript-движков. Команды разработчиков, стоящих за основными браузерами, каждый день работают над повышением производительности их решений, что благотворно влияет на JavaScript в целом. Один из таких движков — это уже упомянутый V8, используемый в браузере Chrome и применяемый в Node.js. Он является одним из результатов стремления разработчиков браузеров к высокой производительности JS-кода.

Конечно же, популярность Node.js основана не только на удачном стечении обстоятельств и на том, что эта платформа появилась в правильное время. Она представила миру инновационный подход к серверной разработке на JavaScript. Рассмотрим основные вехи истории Node.js.

▍2009

  • Появление Node.js
  • Создание первого варианта npm.

▍2010

  • Появление Express.
  • Появление Socket.io.

▍2011

  • Выход npm 1.0.
  • Большие компании, такие, как LinkedIn и Uber, начали пользоваться Node.js.

▍2012

  • Быстрый рост популярности Node.js.

▍2013

  • Появление Ghost, первой крупной платформы для публикаций, использующей Node.js.
  • Выпуск Koa.

▍2014

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

▍2015

  • Основание организации Node.js Foundation.
  • Слияние IO.js и Node.js.
  • В npm появляется возможность работать с приватными модулями.
  • Выход Node.js 4 (надо отметить, что версий 1, 2 и 3 у этой платформы не было).

▍2016

  • Инцидент с пакетом left-pad.
  • Появление Yarn.
  • Выход Node.js 6.

▍2017

  • В npm начинают больше внимания уделять безопасности.
  • Выход Node.js 8
  • Появление поддержки HTTP/2.
  • V8 официально признают в качестве JS-движка, предназначенного не только для Chrome, но и для Node.
  • Еженедельно осуществляется 3 миллиарда загрузок из npm.

▍2018

  • Выход Node.js 10.
  • Поддержка ES-модулей.
  • Экспериментальная поддержка mjs.

Итоги

Сегодня вы ознакомились с платформой Node.js, разобрались с её установкой, написали и испытали первое простое приложение. В следующий раз мы поговорим о том, каким объёмом знаний в области JavaScript нужно обладать для успешной разработки для Node.js, о том, чем различаются браузерный и серверный JS-код, и обсудим некоторые приёмы Node.js-разработки.

Уважаемые читатели! Скажите, запустился ли у вас Hello World для Node.js?

Следующие части руководства:
Часть 1: Общие сведения и начало работы
Часть 2: JavaScript, V8, некоторые приёмы разработки
Часть 3: Хостинг, REPL, работа с консолью, модули
Часть 4: npm, файлы package.json и package-lock.json
Часть 5: npm и npx
Часть 6: цикл событий, стек вызовов, таймеры
Часть 7: асинхронное программирование

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

Продолжать ли перевод цикла статей про Node.js?


85.61%
Категорически да :)
571


11.39%
С удовольствием добавлю в закладки
76

Проголосовали 667 пользователей.

Воздержались 49 пользователей.

Введение в Node.js¶

Это руководство является руководством по началу работы с Node.js, серверной средой выполнения JavaScript.

Обзор¶

Node.js — это среда выполнения для JavaScript, которая работает на сервере.

Node.js с открытым исходным кодом, кросс-платформенный, и с момента своего появления в 2009 году он приобрел огромную популярность и сейчас играет значительную роль на сцене веб-разработки. Если звезды GitHub являются одним из факторов популярности, то наличие 58000+ звезд означает большую популярность.

Node.js запускает движок V8 JavaScript, ядро Google Chrome, вне браузера. Node.js может использовать работу инженеров, которые сделали (и будут продолжать делать) Chrome JavaScript runtime молниеносно быстрым, и это позволяет Node.js воспользоваться огромными улучшениями производительности и компиляцией Just-In-Time, которую выполняет V8. Благодаря этому код JavaScript, выполняемый в Node.js, может стать очень производительным.

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

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

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

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

В Node.js можно без проблем использовать новые стандарты ECMAScript, поскольку вам не нужно ждать, пока все ваши пользователи обновят свои браузеры — вы сами решаете, какую версию ECMAScript использовать, изменяя версию Node.js, и вы также можете включить определенные экспериментальные функции, запустив Node с флагами.

Он содержит огромное количество библиотек.¶

Благодаря своей простой структуре менеджер пакетов node (npm) помог экосистеме Node.js разрастись. Сейчас в реестре npm размещено почти 500 000 пакетов с открытым исходным кодом, которые вы можете свободно использовать.

Пример приложения Node.js¶

Самый распространенный пример Hello World из Node.js — это веб-сервер:

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Hello Worldn');
});

server.listen(port, hostname, () => {
    console.log(
        `Server running at http://${hostname}:${port}/`
    );
});

Чтобы запустить этот фрагмент, сохраните его как файл server.js и запустите node server.js в терминале.

Этот код сначала включает в себя модуль Node.js http.

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

Метод createServer() из http создает новый HTTP-сервер и возвращает его.

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

При получении нового запроса вызывается функция request event, которая предоставляет два объекта: запрос (объект http.IncomingMessage) и ответ (объект http.ServerResponse).

Эти 2 объекта необходимы для обработки HTTP-вызова.

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

Второй используется для возврата данных вызывающей стороне.

В данном случае с:

Мы устанавливаем свойство statusCode в 200, чтобы указать на успешный ответ.

Мы устанавливаем заголовок Content-Type:

res.setHeader('Content-Type', 'text/plain');

…и мы завершаем закрытие ответа, добавляя содержимое в качестве аргумента к end():

res.end('Hello Worldn');

Node.js фреймворки и инструменты¶

Node.js — это низкоуровневая платформа. Чтобы сделать работу разработчиков проще и интереснее, на базе Node.js были созданы тысячи библиотек.

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

  • Express — Один из самых простых и в то же время мощных способов создания веб-сервера. Его минималистский подход и беспристрастное внимание к основным функциям сервера является ключом к его успеху.
  • Meteor — Невероятно мощный фреймворк полного стека, позволяющий использовать изоморфный подход к созданию приложений на JavaScript и разделять код на клиенте и сервере. Когда-то он был готовым инструментом, который предоставлял все, но теперь он интегрируется с такими библиотеками front-end, как React, Vue и Angular. Meteor можно использовать и для создания мобильных приложений.
  • Koa — Созданный той же командой, что и Express, Koa стремится быть еще проще и меньше, опираясь на многолетние знания. Новый проект родился из необходимости создавать несовместимые изменения, не нарушая существующего сообщества.
  • Next.js — Это фреймворк для рендеринга приложений на стороне сервера React.
  • Micro — Это очень легкий сервер для создания асинхронных HTTP микросервисов.
  • Socket.io — Это механизм коммуникации в реальном времени для создания сетевых приложений.

Как установить Node.js¶

Как можно установить Node.js на вашу систему: пакетный менеджер, установщик с официального сайта или nvm¶

Node.js может быть установлен различными способами. В этой заметке освещены наиболее распространенные и удобные из них.

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

Один из очень удобных способов установки Node.js — через менеджер пакетов. В данном случае у каждой операционной системы он свой.

На macOS стандартом де-факто является Homebrew, который — после установки — позволяет установить Node.js очень легко, выполнив эту команду в CLI:

Другие менеджеры пакетов для Linux и Windows перечислены здесь.

nvm — популярный способ запуска Node.js. Он позволяет легко менять версию Node.js, устанавливать новые версии, чтобы попробовать и легко откатиться назад, если, например, что-то сломается.

Это также очень полезно для тестирования вашего кода со старыми версиями Node.js.

Я рекомендую использовать официальную программу установки, если вы только начинаете и еще не используете Homebrew. В противном случае Homebrew — мое любимое решение.

Сколько JavaScript нужно знать, чтобы использовать Node.js?¶

Если вы только начинаете изучать JavaScript, насколько глубоко вам нужно знать язык?

Новичку трудно достичь того момента, когда вы будете достаточно уверены в своих способностях программиста.

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

Я бы рекомендовал вам хорошо усвоить основные концепции JavaScript, прежде чем погружаться в Node.js:

  • Лексическая структура
  • Выражения
  • Типы
  • Переменные
  • Функции
  • this
  • Стрелочные функции
  • Циклы
  • Циклы и область видимости
  • Массивы
  • Шаблонные литералы
  • Точки с запятой
  • Строгий режим
  • ECMAScript 6+

Зная эти понятия, вы уже на пути к тому, чтобы стать опытным разработчиком JavaScript как в браузере, так и в Node.js.

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

  • Асинхронное программирование и колбеки
  • Таймеры
  • Промисы
  • async и await
  • Замыкания
  • Цикл событий

Различия между Node.js и браузером¶

Чем написание JavaScript-приложений в Node.js отличается от программирования для Web в браузере?

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

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

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

У front-end разработчика, который пишет приложения на Node.js, есть огромное преимущество — язык все тот же.

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

Что меняется, так это экосистема.

В браузере большую часть времени вы взаимодействуете с DOM или другими API веб-платформы, такими как Cookies. В Node.js их, конечно, не существует. У вас нет document, window и всех остальных объектов, предоставляемых браузером.

И в браузере у нас нет всех тех приятных API, которые Node.js предоставляет через свои модули, например, функции доступа к файловой системе.

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

Это означает, что вы можете писать все современные ES6—7—8—9 JavaScript, которые поддерживает ваша версия Node.

Поскольку JavaScript развивается так быстро, но браузеры могут быть немного медленными, а пользователи немного медлят с обновлением — иногда в Интернете вы застреваете в использовании старых версий JavaScript/ECMAScript.

Вы можете использовать Babel для преобразования вашего кода в ES5-совместимый перед отправкой его в браузер, но в Node.js вам это не понадобится.

Еще одно отличие заключается в том, что в Node.js используется система модулей CommonJS, в то время как в браузере мы начинаем видеть внедрение стандарта ES Modules.

На практике это означает, что пока что вы используете require() в Node.js и import в браузере.

Движок JavaScript V8¶

V8 — это название движка JavaScript, на котором работает Google Chrome. Именно он принимает наш JavaScript и выполняет его во время просмотра веб-страниц в Chrome.

V8 обеспечивает среду выполнения, в которой выполняется JavaScript. DOM и другие API веб-платформы предоставляются браузером.

Самое интересное, что движок JavaScript не зависит от браузера, в котором он размещен. Эта ключевая особенность обеспечила подъем Node.js. Движок V8 был выбран Node.js еще в 2009 году, и по мере роста популярности Node.js, V8 стал тем движком, на котором сейчас работает невероятное количество кода на стороне сервера, написанного на JavaScript.

Экосистема Node.js огромна, и благодаря ей V8 также используется в приложениях для настольных компьютеров, в таких проектах, как Electron.

Другие движки JS¶

Другие браузеры имеют свой собственный движок JavaScript:

  • Firefox имеет Spidermonkey.
  • Safari имеет JavaScriptCore (также называется Nitro)
  • Edge имеет Chakra

и многие другие.

Все эти движки реализуют стандарт ECMA ES-262, также называемый ECMAScript, стандарт, используемый в JavaScript.

Стремление к производительности¶

V8 написан на C++ и постоянно совершенствуется. Он переносимый и работает на Mac, Windows, Linux и некоторых других системах.

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

V8 постоянно развивается, как и другие движки JavaScript, чтобы ускорить работу Сети и экосистемы Node.js.

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

Компиляция¶

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

Это происходит с 2009 года, когда компилятор SpiderMonkey JavaScript был добавлен в Firefox 3.5, и все последовали этой идее.

JavScript компилируется внутри V8 с помощью компиляции just-in-time (JIT) для ускорения выполнения.

Это может показаться неинтуитивным. Но с момента появления Google Maps в 2004 году JavaScript превратился из языка, который обычно выполнял несколько десятков строк кода, в полноценные приложения с тысячами и сотнями тысяч строк, работающие в браузере.

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

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

Как выйти из программы Node.js¶

Существуют различные способы завершения работы приложения Node.js.

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

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

Основной модуль process предоставляет удобный метод, который позволяет вам программно выйти из программы Node.js: process.exit().

Когда Node.js выполняет эту строку, процесс немедленно завершается.

Это означает, что любой ожидающий обратный вызов, любой сетевой запрос, который все еще отправляется, любой доступ к файловой системе или процессы, пишущие в stdout или stderr — все это будет немедленно безболезненно завершено.

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

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

Подробнее о кодах выхода вы можете прочитать здесь.

Вы также можете установить свойство process.exitCode:

и когда программа впоследствии завершится, Node.js вернет этот код выхода.

Программа изящно завершится, когда вся обработка будет закончена.

Много раз с помощью Node.js мы запускаем серверы, например, этот HTTP-сервер:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.send('Hi!');
});

app.listen(3000, () => console.log('Server ready'));

Эта программа никогда не завершится. Если вы вызовете process.exit(), любой ожидающий или выполняющийся запрос будет прерван. Это нехорошо.

В этом случае вам нужно послать команде сигнал SIGTERM и обработать его с помощью обработчика сигнала процесса:

Примечание: process не требует require, он автоматически доступен.

const express = require('express');

const app = express();

app.get('/', (req, res) => {
    res.send('Hi!');
});

const server = app.listen(3000, () =>
    console.log('Server ready')
);

process.on('SIGTERM', () => {
    server.close(() => {
        console.log('Process terminated');
    });
});

Что такое сигналы? Сигналы — это система обмена данными Portable Operating System Interface (POSIX): уведомление, посылаемое процессу, чтобы оповестить его о произошедшем событии.

SIGKILL — это сигнал, который сообщает процессу о немедленном завершении, и в идеале должен действовать подобно process.exit().

SIGTERM — это сигнал, который сообщает процессу о необходимости плавного завершения. Это сигнал, который посылается менеджерами процессов, такими как upstart или supervisord и многими другими.

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

process.kill(process.pid, 'SIGTERM');

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

Как читать переменные окружения из Node.js¶

Модуль process ядра Node предоставляет свойство env, в котором хранятся все переменные окружения, которые были установлены в момент запуска процесса.

Вот пример, который обращается к переменной окружения NODE_ENV, которая по умолчанию установлена в development.

process.env.NODE_ENV; // "development"

Установив ее в production перед запуском сценария, вы сообщите Node.js, что это производственная среда.

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

Здесь мы задали 2 переменные для API_KEY и API_SECRET

API_KEY=123123 API_SECRET=456456 node app.js

Мы можем получить их в Node.js, выполнив команду

process.env.API_KEY; // "123123"
process.env.API_SECRET; // "456456"

Вы можете записать переменные окружения в файл .env (который следует добавить в .gitignore, чтобы избежать публикации на GitHub), затем

и в начале вашего основного файла Node добавьте

require('dotenv').config();

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

Примечание: Некоторые инструменты, например, Next.js, делают переменные окружения, определенные в .env, автоматически доступными без необходимости использования dotenv.

Где разместить приложение Node.js¶

Приложение Node.js может быть размещено во многих местах, в зависимости от ваших потребностей.

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

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

Самый простой вариант: локальный туннель.¶

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

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

Очень хорошим инструментом для этого, доступным на всех платформах, является ngrok.

Используя его, вы можете просто набрать ngrok PORT и нужный вам PORT будет открыт для интернета. Вы получите домен ngrok.io, но при платной подписке вы можете получить пользовательский URL, а также больше возможностей безопасности (помните, что вы открываете свою машину для публичного Интернета).

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

Развертывание нулевой конфигурации¶

Glitch¶

Glitch — это игровая площадка и способ создавать свои приложения быстрее, чем когда-либо, и видеть их в реальном времени на собственном поддомене glitch.com. В настоящее время вы не можете иметь собственный домен, и есть несколько ограничений, но это действительно здорово для создания прототипов. Он выглядит забавно (и это плюс), и это не отупляющая среда — вы получаете всю мощь Node.js, CDN, безопасное хранение учетных данных, импорт/экспорт GitHub и многое другое.

Предоставляется компанией, стоящей за FogBugz и Trello (и соавторами Stack Overflow).

Я часто использую его в демонстрационных целях.

Codepen¶

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

Serverless¶

Способ публикации приложений и отсутствие сервера для управления — это Serverless. Serverless — это парадигма, в которой вы публикуете свои приложения как функции, а они отвечают на запросы конечной точки сети (также называемой FAAS — Functions As A Service).

К очень популярным решениям относятся:

  • Serverless Framework
  • Стандартная библиотека.

Они оба предоставляют слой абстракции для публикации на AWS Lambda и других FAAS-решений на базе Azure или Google Cloud.

PAAS¶

PAAS расшифровывается как Platform As A Service. Эти платформы убирают многие вещи, о которых вы должны беспокоиться при развертывании вашего приложения.

Zeit Now¶

Zeit — интересный вариант. Вы просто вводите now в терминале, и он берет на себя заботу о развертывании вашего приложения. Есть бесплатная версия с ограничениями, а платная версия более мощная. Вы просто забываете о существовании сервера, вы просто развертываете приложение.

Nanobox¶

Nanobox

Heroku¶

Heroku — удивительная платформа.

Это отличная статья о начале работы с Node.js на Heroku.

Microsoft Azure¶

Azure — это облачное предложение Microsoft.

Посмотрите, как создать веб-приложение Node.js в Azure.

Google Cloud Platform¶

Google Cloud — это удивительная структура для ваших приложений.

У них есть хороший Раздел документации по Node.js.

Виртуальный выделенный сервер¶

В этом разделе вы найдете обычных подозреваемых, упорядоченных от более удобных для пользователя к менее удобным:

  • Digital Ocean
  • Linode
  • Amazon Web Services, в частности, я упоминаю Amazon Elastic Beanstalk, поскольку он немного абстрагирует сложность AWS.

Поскольку они предоставляют пустую Linux-машину, на которой вы можете работать, специального руководства по ним нет.

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

Bare metal¶

Другим решением является получение bare metal сервера, установка дистрибутива Linux, подключение к интернету (или аренда сервера на месяц, как это можно сделать с помощью сервиса Vultr Bare Metal).


Как использовать Node.js REPL¶

REPL расшифровывается как Read-Evaluate-Print-Loop, и это отличный способ быстро изучить возможности Node.js.

Команда node — это команда, которую мы используем для запуска наших скриптов Node.js:

Если мы опускаем имя файла, мы используем его в режиме REPL:

Если вы попробуете сделать это сейчас в терминале, вот что произойдет:

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

Совет: если вы не знаете, как открыть терминал, наберите в Google «How to open terminal on <ваша операционная система>».

REPL ждет, пока мы введем какой-нибудь код JavaScript.

Начните с простого и введите:

> console.log('test')
test
undefined
>

Первое значение, test, — это вывод, который мы сказали консоли вывести, затем мы получаем undefined, которое является возвращаемым значением выполнения console.log().

Теперь мы можем ввести новую строку JavaScript.

Использование вкладки для автозаполнения¶

Самое замечательное в REPL то, что он интерактивен.

По мере написания кода, если вы нажмете клавишу tab, REPL попытается автозаполнить то, что вы написали, чтобы соответствовать переменной, которую вы уже определили, или предопределенной переменной.

Изучение объектов JavaScript¶

Попробуйте ввести имя класса JavaScript, например Number, добавить точку и нажать tab.

REPL выведет все свойства и методы этого класса, к которым вы можете получить доступ:

MgYHCtgjD1rom1yKM43E-qBh7ansJuyglRWr

Исследуйте глобальные объекты¶

Вы можете просмотреть глобальные объекты, к которым у вас есть доступ, набрав global. и нажав tab:

e2qWLuyjYC4DFZjEs2jYWK-NL9AXbpDiSdA7

Специальная переменная _¶

Если после некоторого кода вы напечатаете _, то будет выведен результат последней операции.

Команды с точкой¶

В REPL есть несколько специальных команд, все они начинаются с точки .. К ним относятся

  • .help: показывает справку по точечным командам.
  • .editor: включает режим редактора, позволяющий легко писать многострочный код JavaScript. Как только вы окажетесь в этом режиме, введите ctrl-D, чтобы запустить написанный вами код.
  • .break: при вводе многострочного выражения ввод команды .break прерывает дальнейший ввод. Аналогично нажатию клавиши ctrl-C.
  • .clear: сбрасывает контекст REPL на пустой объект и очищает любое многострочное выражение, вводимое в данный момент.
  • .load: загружает файл JavaScript, относительно текущего рабочего каталога.
  • .save: сохраняет все, что вы ввели в сеансе REPL, в файл (укажите имя файла).
  • .exit: завершает работу (то же самое, что два раза нажать ctrl-C).

REPL знает, когда вы набираете многострочный оператор, без необходимости вызывать .editor.

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

[1, 2, 3].forEach(num => {

и вы нажмете enter, REPL перейдет на новую строку, начинающуюся с 3 точек, указывая, что теперь вы можете продолжить работу над этим блоком.

...   console.log(num)
... })

Если вы напечатаете .break в конце строки, многострочный режим остановится и утверждение не будет выполнено.

Node.js, принимать аргументы из командной строки¶

Как принимать аргументы в программе Node.js, передаваемые из командной строки

Вы можете передавать любое количество аргументов при вызове приложения Node.js с помощью:

Аргументы могут быть отдельными или иметь ключ и значение.

Например:

или

Это меняет способ получения этого значения в коде Node.js.

Для его получения используется объект process, встроенный в Node.js.

Он раскрывает свойство argv, которое представляет собой массив, содержащий все аргументы командной строки.

Первый аргумент — это полный путь команды node.

Второй элемент — полный путь выполняемого файла.

Все дополнительные аргументы присутствуют с третьей позиции и далее.

Вы можете перебирать все аргументы (включая путь к узлу и путь к файлу) с помощью цикла:

process.argv.forEach((val, index) => {
    console.log(`${index}: ${val}`);
});

Вы можете получить только дополнительные аргументы, создав новый массив, исключающий первые 2 параметра:

const args = process.argv.slice(2);

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

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

const args = process.argv.slice(2);
args[0];

В данном случае:

args[0] — это name=flavio, и вам нужно разобрать его. Лучший способ сделать это — использовать minimist library, который помогает работать с аргументами:

const args = require('minimist')(process.argv.slice(2));
args['name']; // flavio

Вывод в командную строку с помощью Node.js¶

Как выводить в консоль командной строки с помощью Node.js, от базового console.log до более сложных сценариев.

Базовый вывод с помощью модуля консоли¶

Node.js предоставляет console модуль, который обеспечивает множество очень полезных способов взаимодействия с командной строкой.

По сути, это то же самое, что и объект console, который можно найти в браузере.

Самым основным и наиболее используемым методом является console.log(), который печатает строку, которую вы передаете в консоль.

Если вы передадите объект, он будет отображен как строка.

В console.log можно передать несколько переменных, например:

const x = 'x';
const y = 'y';
console.log(x, y);

и Node.js выведет обе фразы.

Мы также можем форматировать красивые фразы, передавая переменные и спецификатор формата.

Например:

console.log('My %s has %d years', 'cat', 2);
  • %s форматирует переменную как строку
  • %d или %i форматируют переменную как целое число
  • %f форматирует переменную как число с плавающей точкой
  • %O используется для печати представления объекта

Пример:

console.log('%O', Number);

Очистить консоль¶

console.clear() очищает консоль (поведение может зависеть от используемой консоли).

Подсчет элементов¶

console.count() — удобный метод.

Возьмите этот код:

const x = 1;
const y = 2;
const z = 3;

console.count(
    'The value of x is ' +
        x +
        ' and has been checked .. how many times?'
);

console.count(
    'The value of x is ' +
        x +
        ' and has been checked .. how many times?'
);

console.count(
    'The value of y is ' +
        y +
        ' and has been checked .. how many times?'
);

Происходит это так: count будет считать, сколько раз печатается строка, и выводить рядом с ней счет.

Вы можете просто считать яблоки и апельсины:

const oranges = ['orange', 'orange'];
const apples = ['just one apple'];

oranges.forEach((fruit) => {
    console.count(fruit);
});

apples.forEach((fruit) => {
    console.count(fruit);
});

Печать трассировки стека¶

Бывают случаи, когда полезно распечатать трассировку стека вызовов функции, возможно, чтобы ответить на вопрос: «Как вы достигли этой части кода?».

Вы можете сделать это с помощью console.trace():

const function2 = () => console.trace();
const function1 = () => function2();
function1();

Это выведет трассировку стека. Вот что будет выведено, если я попробую сделать это в Node REPL:

Trace
  at function2 (repl:1:33)
  at function1 (repl:1:25)
  at repl:1:1
  at ContextifyScript.Script.runInThisContext (vm.js:44:33)
  at REPLServer.defaultEval (repl.js:239:29)
  at bound (domain.js:301:14)
  at REPLServer.runBound [as eval] (domain.js:314:12)
  at REPLServer.onLine (repl.js:440:10)
  at emitOne (events.js:120:20)
  at REPLServer.emit (events.js:210:7)

Вычислите затраченное время¶

Вы можете легко подсчитать, сколько времени занимает выполнение функции, используя time() и timeEnd().

const doSomething = () => console.log('test');
const measureDoingSomething = () => {
    console.time('doSomething()');
    // do something, and measure the time it takes
    doSomething();
    console.timeEnd('doSomething()');
};

measureDoingSomething();

stdout и stderr¶

Как мы видели, console.log отлично подходит для печати сообщений в консоли. Это то, что называется стандартным выводом, или stdout.

Ошибка console.error печатается в поток stderr.

Он не появится в консоли, но появится в журнале ошибок.

Цвет вывода¶

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

Пример:

console.log('x1b[33m%sx1b[0m', 'hi!');

Вы можете попробовать сделать это в Node REPL, и он выведет hi! желтым цветом.

Однако это низкоуровневый способ сделать это. Самый простой способ раскрасить вывод консоли — это использовать библиотеку. Chalk является такой библиотекой, и в дополнение к раскрашиванию она также помогает с другими средствами стилизации, например, делает текст жирным, курсивным или подчеркнутым.

Вы устанавливаете ее с помощью npm install chalk, а затем можете использовать ее:

const chalk = require('chalk');
console.log(chalk.yellow('hi!'));

Использовать chalk.yellow гораздо удобнее, чем пытаться запомнить escape-коды, и код становится гораздо более читабельным.

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

Создание индикатора выполнения¶

Progress — это замечательный пакет для создания прогресс-бара в консоли. Установите его с помощью npm install progress.

Этот фрагмент создает 10-шаговый прогресс-бар, и каждые 100 мс завершается один шаг. Когда полоса завершается, мы очищаем интервал:

const ProgressBar = require('progress');

const bar = new ProgressBar(':bar', { total: 10 });
const timer = setInterval(() => {
    bar.tick();
    if (bar.complete) {
        clearInterval(timer);
    }
}, 100);

Принять ввод из командной строки в Node.js¶

Как сделать программу Node.js CLI интерактивной?

Node, начиная с версии 7, предоставляет readline модуль для выполнения именно этого: получения ввода из читаемого потока, такого как поток process.stdin, который во время выполнения программы Node является вводом терминала, по одной строке за раз.

const readline = require('readline').createInterface({
    input: process.stdin,
    output: process.stdout,
});

readline.question(`What's your name?`, (name) => {
    console.log(`Hi ${name}!`);
    readline.close();
});

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

Метод question() показывает первый параметр (вопрос) и ожидает ввода пользователем. Он вызывает функцию обратного вызова после нажатия клавиши Enter.

В этой функции обратного вызова мы закрываем интерфейс readline.

readline предлагает несколько других методов, и я позволю вам ознакомиться с ними в документации по пакету, ссылку на которую я привел выше.

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

Самый простой способ — использовать readline-sync package, который очень похож по API и справляется с этим из коробки.

Более полное и абстрактное решение предоставляет пакет Inquirer.js.

Вы можете установить его с помощью npm install inquirer, а затем воспроизвести приведенный выше код следующим образом:

const inquirer = require('inquirer');

var questions = [
    {
        type: 'input',
        name: 'name',
        message: "What's your name?",
    },
];

inquirer.prompt(questions).then((answers) => {
    console.log(`Hi ${answers['name']}!`);
});

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

Стоит знать все альтернативы, особенно встроенные, предоставляемые Node.js, но если вы планируете вывести CLI-ввод на новый уровень, Inquirer.js — оптимальный выбор.

Выявление функциональности из файла Node.js с помощью экспорта¶

Как использовать API module.exports для передачи данных в другие файлы вашего приложения или в другие приложения.

Node.js имеет встроенную систему модулей.

Файл Node.js может импортировать функциональность, открытую другими файлами Node.js.

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

const library = require('./library');

для импорта функциональности, раскрытой в файле library.js, который находится в папке текущего файла.

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

Любой другой объект или переменная, определенные в файле, по умолчанию являются приватными и не раскрываются внешнему миру.

Именно это позволяет сделать API module.exports, предлагаемый module system.

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

Вы можете сделать это двумя способами.

Первый — присвоить объект свойству module.exports, которое является объектом, предоставляемым из коробки системой модулей, и это заставит ваш файл экспортировать только этот объект:

const car = {
    brand: 'Ford',
    model: 'Fiesta',
};

module.exports = car;

// ...in the other file

const car = require('./car');

Второй способ — добавить экспортируемый объект в качестве свойства exports. Этот способ позволяет экспортировать множество объектов, функций или данных:

const car = {
    brand: 'Ford',
    model: 'Fiesta',
};

exports.car = car;

или непосредственно

exports.car = {
    brand: 'Ford',
    model: 'Fiesta',
};

А в другом файле вы будете использовать его, ссылаясь на свойство вашего импорта:

const items = require('./items');
items.car;

или

const car = require('./items').car;

В чем разница между module.exports и exports?

Первый раскрывает объект, на который он указывает. Второй раскрывает свойства объекта, на который он указывает.

Введение в npm¶

npm означает менеджер пакетов узла.

В январе 2017 года в реестре npm было зарегистрировано более 350 000 пакетов, что делает его самым большим хранилищем кода на одном языке на Земле, и вы можете быть уверены, что здесь есть пакет для (почти!) всего.

Он начинался как способ загрузки и управления зависимостями пакетов Node.js, но с тех пор стал инструментом, используемым и в front-end JavaScript.

Есть много вещей, которые делает npm.

Загрузка¶

npm управляет загрузкой зависимостей вашего проекта.

Установка всех зависимостей¶

Если в проекте есть файл packages.json, то, выполнив команду

установит все, что нужно проекту, в папку node_modules, создав ее, если она еще не существует.

Установка одного пакета¶

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

npm install <package-name>

Часто можно увидеть дополнительные флаги, добавляемые к этой команде:

  • --save устанавливает и добавляет запись в файл package.json dependencies.
  • --save-dev устанавливает и добавляет запись в файл package.json devDependencies.

Разница в основном заключается в том, что devDependencies обычно являются инструментами разработки, например, библиотекой для тестирования, в то время как dependencies поставляются с приложением в продакшене.

Обновление пакетов¶

Обновление пакетов также упрощается, если выполнить

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

Вы также можете указать отдельный пакет для обновления:

npm update <package-name>

Версионирование¶

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

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

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

Указание явной версии библиотеки также помогает держать всех на одной и той же точной версии пакета, так что вся команда работает с одной и той же версией до тех пор, пока файл package.json не будет обновлен.

Во всех этих случаях версионирование очень помогает, и npm следует стандарту семантического версионирования (semver).

Выполняемые задачи¶

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

Например:

{
    "scripts": {
        "start-dev": "node lib/server-development",
        "start": "node lib/server-production"
    }
}

Очень часто эта функция используется для запуска Webpack:

{
    "scripts": {
        "watch": "webpack --watch --progress --colors --config webpack.conf.js",
        "dev": "webpack --progress --colors --config webpack.conf.js",
        "prod": "NODE_ENV=production webpack -p --config webpack.conf.js"
    }
}

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

$ npm watch
$ npm dev
$ npm prod

Куда npm устанавливает пакеты?¶

Когда вы устанавливаете пакет с помощью npm (или yarn), вы можете выполнить 2 типа установки:

  • локальная установка
  • глобальная установка

По умолчанию, когда вы вводите команду npm install, например:

пакет устанавливается в текущее дерево файлов, в подпапку node_modules.

При этом npm также добавляет запись lodash в свойство dependencies файла package.json, находящегося в текущей папке.

Глобальная установка выполняется с помощью флага -g:

Когда это происходит, npm не будет устанавливать пакет в локальную папку, а вместо этого будет использовать глобальное расположение.

Где именно?

Команда npm root -g скажет вам, где именно находится это место на вашей машине.

На macOS или Linux это место может быть /usr/local/lib/node_modules. В Windows это может быть C:UsersYOUAppDataRoamingnpmnode_modules.

Если вы используете nvm для управления версиями Node.js, однако, это расположение будет отличаться.

Я, например, использую nvm и расположение моих пакетов было показано как /Users/flavio/.nvm/versions/node/v8.9.0/lib/node_modules.

Как использовать или выполнить пакет, установленный с помощью npm¶

Как включить и использовать в коде пакет, установленный в папке node_modules.¶

Когда вы устанавливаете с помощью npm пакет в папку node_modules или глобально, как вы используете его в своем коде Node?

Скажем, вы устанавливаете lodash, популярную библиотеку утилит JavaScript, используя

Это позволит установить пакет в локальную папку node_modules.

Чтобы использовать его в своем коде, вам просто нужно импортировать его в свою программу с помощью require:

const _ = require('lodash');

Что если ваш пакет является исполняемым файлом?

В этом случае он поместит исполняемый файл в папку node_modules/.bin/.

Один из простых способов продемонстрировать это — cowsay.

Пакет cowsay предоставляет программу командной строки, которая может быть выполнена, чтобы заставить корову сказать что-нибудь (и других животных тоже).

Когда вы устанавливаете пакет с помощью npm install cowsay, он установит себя и несколько зависимостей в папку node_modules.

Там есть скрытая папка .bin, которая содержит символические ссылки на двоичные файлы cowsay.

Как их выполнить?

Конечно, вы можете набрать ./node_modules/.bin/cowsay, чтобы запустить его, и это работает, но npx, включенный в последние версии npm (начиная с 5.2), является гораздо лучшим вариантом. Вы просто запускаете:

и npx найдет местоположение пакета.

Руководство по пакету.json¶

Файл package.json является ключевым элементом во многих кодовых базах приложений, основанных на экосистеме Node.js.

Если вы работаете с JavaScript или когда-либо взаимодействовали с проектом JavaScript, Node.js или front-end проектом, вы наверняка встречали файл package.json.

Для чего он нужен? Что вы должны знать о нем, и какие интересные вещи вы можете с ним делать?

Файл package.json — это своего рода манифест вашего проекта. Он может делать множество вещей, совершенно не связанных между собой. Например, это центральное хранилище конфигурации для инструментов. В нем также хранятся имена и версии установленных пакетов npm и yarn.

Структура файла¶

Вот пример файла package.json:

Он пустой! Не существует фиксированных требований к тому, что должно быть в файле package.json для приложения. Единственное требование — это соблюдение формата JSON, иначе он не может быть прочитан программами, которые пытаются получить доступ к его свойствам программно.

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

Это еще один package.json:

{
    "name": "test-project"
}

Он определяет свойство name, которое сообщает имя приложения или пакета, содержащегося в той же папке, где находится этот файл.

Вот гораздо более сложный пример, который я извлек из примера приложения Vue.js:

{
    "name": "test-project",
    "version": "1.0.0",
    "description": "A Vue.js project",
    "main": "src/main.js",
    "private": true,
    "scripts": {
        "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
        "start": "npm run dev",
        "unit": "jest --config test/unit/jest.conf.js --coverage",
        "test": "npm run unit",
        "lint": "eslint --ext .js,.vue src test/unit",
        "build": "node build/build.js"
    },
    "dependencies": {
        "vue": "^2.5.2"
    },
    "devDependencies": {
        "autoprefixer": "^7.1.2",
        "babel-core": "^6.22.1",
        "babel-eslint": "^8.2.1",
        "babel-helper-vue-jsx-merge-props": "^2.0.3",
        "babel-jest": "^21.0.2",
        "babel-loader": "^7.1.1",
        "babel-plugin-dynamic-import-node": "^1.2.0",
        "babel-plugin-syntax-jsx": "^6.18.0",
        "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
        "babel-plugin-transform-runtime": "^6.22.0",
        "babel-plugin-transform-vue-jsx": "^3.5.0",
        "babel-preset-env": "^1.3.2",
        "babel-preset-stage-2": "^6.22.0",
        "chalk": "^2.0.1",
        "copy-webpack-plugin": "^4.0.1",
        "css-loader": "^0.28.0",
        "eslint": "^4.15.0",
        "eslint-config-airbnb-base": "^11.3.0",
        "eslint-friendly-formatter": "^3.0.0",
        "eslint-import-resolver-webpack": "^0.8.3",
        "eslint-loader": "^1.7.1",
        "eslint-plugin-import": "^2.7.0",
        "eslint-plugin-vue": "^4.0.0",
        "extract-text-webpack-plugin": "^3.0.0",
        "file-loader": "^1.1.4",
        "friendly-errors-webpack-plugin": "^1.6.1",
        "html-webpack-plugin": "^2.30.1",
        "jest": "^22.0.4",
        "jest-serializer-vue": "^0.3.0",
        "node-notifier": "^5.1.2",
        "optimize-css-assets-webpack-plugin": "^3.2.0",
        "ora": "^1.2.0",
        "portfinder": "^1.0.13",
        "postcss-import": "^11.0.0",
        "postcss-loader": "^2.0.8",
        "postcss-url": "^7.2.1",
        "rimraf": "^2.6.0",
        "semver": "^5.3.0",
        "shelljs": "^0.7.6",
        "uglifyjs-webpack-plugin": "^1.1.1",
        "url-loader": "^0.5.8",
        "vue-jest": "^1.0.2",
        "vue-loader": "^13.3.0",
        "vue-style-loader": "^3.0.1",
        "vue-template-compiler": "^2.5.2",
        "webpack": "^3.6.0",
        "webpack-bundle-analyzer": "^2.9.0",
        "webpack-dev-server": "^2.9.1",
        "webpack-merge": "^4.1.0"
    },
    "engines": {
        "node": ">= 6.0.0",
        "npm": ">= 3.0.0"
    },
    "browserslist": [
        "> 1%",
        "last 2 versions",
        "not ie <= 8"
    ]
}

здесь происходит много вещей:

  • name задает имя приложения/пакета
  • version указывает текущую версию
  • description — краткое описание приложения/пакета
  • main задает точку входа для приложения
  • private, если установлено значение true, предотвращает случайную публикацию приложения/пакета на npm.
  • scripts определяет набор скриптов для node, которые вы можете запускать
  • dependencies задает список пакетов npm, установленных в качестве зависимостей
  • devDependencies задает список пакетов npm, установленных в качестве зависимостей для разработки
  • engines устанавливает, на каких версиях Node работает этот пакет/приложение
  • browserslist используется для указания того, какие браузеры (и их версии) вы хотите поддерживать.

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

Разбивка свойств¶

В этом разделе подробно описаны свойства, которые вы можете использовать. Я ссылаюсь на «пакет», но то же самое относится и к локальным приложениям, которые вы не используете как пакеты.

Большинство из этих свойств используется только на сайте npm website, другие скриптами, которые взаимодействуют с вашим кодом, например npm или другими.

name

Устанавливает имя пакета.

Пример:

Имя должно быть меньше 214 символов, не должно содержать пробелов, может содержать только строчные буквы, дефисы (-) или символы подчеркивания (_).

Это необходимо потому, что когда пакет публикуется на npm, он получает свой собственный URL, основанный на этом свойстве.

Если вы опубликовали этот пакет на GitHub, хорошим значением для этого свойства будет имя репозитория GitHub.

Перечисляет имя автора пакета.

Пример:

Может также использоваться с этим форматом:

{
    "author": {
        "name": "Flavio Copes",
        "email": "[email protected]",
        "url": "https://flaviocopes.com"
    }
}

contributors

Помимо автора, у проекта может быть один или несколько соавторов. Это свойство представляет собой массив, в котором они перечислены.

Пример:

Может также использоваться с этим форматом:

{
    "contributors": [
        {
            "name": "Flavio Copes",
            "email": "[email protected]",
            "url": "https://flaviocopes.com"
        }
    ]
}

bugs

Ссылка на трекер проблем пакета, скорее всего, на страницу проблем GitHub

Пример:

{
    "bugs": "https://github.com/flaviocopes/package/issues"
}

homepage

Устанавливает домашнюю страницу пакета

Пример:

{
    "homepage": "https://flaviocopes.com/package"
}

version

Указывает текущую версию пакета.

Пример:

Это свойство соответствует нотации семантического версионирования (semver) для версий, что означает, что версия всегда выражается 3 числами: x.x.x.

Первое число — это основная версия, второе — второстепенная версия, а третье — версия патча.

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

license

Указывает лицензию пакета.

Пример:

keywords

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

Пример:

"keywords": [
  "email",
  "machine learning",
  "ai"
]

Это поможет людям найти ваш пакет при навигации по похожим пакетам или при просмотре сайта npm.

description

Это свойство содержит краткое описание пакета.

Пример:

"description": "A package to work with strings"

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

repository

Это свойство определяет, где находится репозиторий данного пакета.

Пример:

"repository": "github:flaviocopes/testing",

Обратите внимание на префикс github. Есть и другие популярные сервисы:

"repository": "gitlab:flaviocopes/testing",
"repository": "bitbucket:flaviocopes/testing",

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

"repository": {
  "type": "git",
  "url": "https://github.com/flaviocopes/testing.git"
}

Вы можете использовать различные системы контроля версий:

"repository": {
  "type": "svn",
  "url": "..."
}

main

Устанавливает точку входа для пакета.

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

Пример:

private

если установлено значение true, предотвращает случайную публикацию приложения/пакета на npm.

Пример:

scripts

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

Пример:

"scripts": {
  "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
  "start": "npm run dev",
  "unit": "jest --config test/unit/jest.conf.js --coverage",
  "test": "npm run unit",
  "lint": "eslint --ext .js,.vue src test/unit",
  "build": "node build/build.js"
}

Эти скрипты являются приложениями командной строки. Вы можете запустить их, вызвав npm run XXXX или yarn XXXX, где XXXX — имя команды.

Пример:
npm run dev

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

dependencies

Задает список пакетов npm, установленных в качестве зависимостей.

Когда вы устанавливаете пакет с помощью npm или yarn:

npm install <PACKAGENAME>
yarn add <PACKAGENAME>

этот пакет автоматически вставляется в этот список.

Пример:

"dependencies": {
  "vue": "^2.5.2"
}

devDependencies

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

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

Когда вы устанавливаете пакет с помощью npm или yarn:

npm install --dev <PACKAGENAME>
yarn add --dev <PACKAGENAME>

этот пакет автоматически вставляется в этот список.

Пример:

"devDependencies": {
  "autoprefixer": "^7.1.2",
  "babel-core": "^6.22.1"
}

engines

Устанавливает, на каких версиях Node.js и других команд работает этот пакет/приложение.

Пример:

"engines": {
  "node": ">= 6.0.0",
  "npm": ">= 3.0.0",
  "yarn": "^0.13.0"
}

browserslist

Используется для указания того, какие браузеры (и их версии) вы хотите поддерживать. На него ссылаются Babel, Autoprefixer и другие инструменты, чтобы добавлять только те полифиллы и fallbacks, которые необходимы для браузеров, на которые вы ориентируетесь.

Пример:

"browserslist": [
  "> 1%",
  "last 2 versions",
  "not ie <= 8"
]

Эта конфигурация означает, что вы хотите поддерживать 2 последние основные версии всех браузеров с не менее 1% использования (из статистики CanIUse.com), кроме IE8 и ниже (see more в browserslist).

Свойства, специфичные для команды¶

Файл package.json также может содержать специфическую конфигурацию команд, например, для Babel, ESLint и других.

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

Версии пакетов¶

Вы видели в описании выше такие номера версий, как: ~3.0.0 или ^0.13.0. Что они означают, и какие еще спецификаторы версий вы можете использовать?

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

Учитывая, что при использовании semver (semantic versioning) все версии имеют 3 цифры, первая — основной выпуск, вторая — минорный выпуск и третья — выпуск патча, у вас есть следующие правила:

  • ~: если вы пишете ~0.13.0, вы хотите обновлять только выпуски патчей: 0.13.1 подходит, а 0.14.0 — нет.
  • ^: если вы пишете ^0.13.0, вы хотите обновлять патч и минорные релизы: 0.13.1, 0.14.0 и так далее.
  • *: если вы пишете *, это означает, что вы принимаете все обновления, включая основные обновления версий.
  • >: вы принимаете любую версию выше той, которую вы указали.
  • >=: вы принимаете любую версию, равную или более высокую, чем та, которую вы указали
  • <=: вы принимаете любую версию, равную или ниже указанной вами
  • <: вы принимаете любую версию ниже указанной.

Существуют и другие правила:

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

и вы можете объединить большинство из вышеперечисленных правил в диапазоны, например: 1.0.0 || >=1.1.0 <1.2.0, чтобы использовать либо 1.0.0, либо один релиз, начиная с 1.1.0, но ниже 1.2.0.

Файл package-lock.json¶

Файл package-lock.json автоматически генерируется при установке пакетов node.

В версии 5 npm представил файл package-lock.json.

Что это такое? Вы, вероятно, знаете о файле package.json, который гораздо более распространен и существует гораздо дольше.

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

Это решает очень специфическую проблему, которую package.json оставил нерешенной. В package.json вы можете указать, до каких версий вы хотите обновиться (patch или minor), используя нотацию semver, например:

  • если вы пишете ~0.13.0, вы хотите обновлять только релизы патчей: 0.13.1 подойдет, а 0.14.0 — нет.
  • если вы пишете ^0.13.0, вы хотите обновлять патч и минорные релизы: 0.13.1, 0.14.0 и так далее.
  • если вы пишете 0.13.0, это точная версия, которая будет использоваться всегда.

Вы не фиксируете в Git свою папку node_modules, которая обычно огромна, и когда вы пытаетесь воспроизвести проект на другой машине с помощью команды npm install, если вы указали синтаксис ~ и был выпущен патч-релиз пакета, будет установлен именно он. То же самое для ^ и минорных релизов.

Если вы указываете точные версии, как 0.13.0 в примере, эта проблема вас не коснется.

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

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

В package-lock.json устанавливается текущая версия каждого пакета в камне, и npm будет использовать именно эти версии при выполнении npm install.

Эта концепция не нова, и менеджеры пакетов других языков программирования (например, Composer в PHP) используют подобную систему в течение многих лет.

Файл package-lock.json должен быть зафиксирован в вашем Git-репозитории, чтобы его могли получить другие люди, если проект публичный или у вас есть соавторы, или если вы используете Git в качестве источника для развертывания.

Версии зависимостей будут обновлены в файле package-lock.json, когда вы запустите npm update.

Пример¶

Это пример структуры файла package-lock.json, который мы получаем, когда запускаем npm install cowsay в пустой папке:

{
    "requires": true,
    "lockfileVersion": 1,
    "dependencies": {
        "ansi-regex": {
            "version": "3.0.0",
            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
            "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
        },
        "cowsay": {
            "version": "1.3.1",
            "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.3.1.tgz",
            "integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkMAjufp+0F9eLjzRnOHzVAYeIYFF5po5NjRrgefnRMQ==",
            "requires": {
                "get-stdin": "^5.0.1",
                "optimist": "~0.6.1",
                "string-width": "~2.1.1",
                "strip-eof": "^1.0.0"
            }
        },
        "get-stdin": {
            "version": "5.0.1",
            "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz",
            "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g="
        },
        "is-fullwidth-code-point": {
            "version": "2.0.0",
            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
            "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
        },
        "minimist": {
            "version": "0.0.10",
            "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
            "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
        },
        "optimist": {
            "version": "0.6.1",
            "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
            "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
            "requires": {
                "minimist": "~0.0.1",
                "wordwrap": "~0.0.2"
            }
        },
        "string-width": {
            "version": "2.1.1",
            "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
            "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVa",
            "requires": {
                "is-fullwidth-code-point": "^2.0.0",
                "strip-ansi": "^4.0.0"
            }
        },
        "strip-ansi": {
            "version": "4.0.0",
            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
            "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
            "requires": {
                "ansi-regex": "^3.0.0"
            }
        },
        "strip-eof": {
            "version": "1.0.0",
            "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
            "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
        },
        "wordwrap": {
            "version": "0.0.3",
            "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
            "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
        }
    }
}

Мы установили cowsay, который зависит от:

  • get-stdin
  • optimist
  • string-width
  • strip-eof

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

  • ansi-regex
  • is-fullwidth-code-point
  • minimist
  • wordwrap
  • strip-eof

Они добавляются в файл в алфавитном порядке, и каждый из них имеет поле version, поле resolved, указывающее на местоположение пакета, и строку integrity, которую мы можем использовать для проверки пакета.

Найти установленную версию пакета npm¶

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

Пример:

Вы также можете просто открыть файл package-lock.json, но это требует некоторого визуального сканирования.

npm list -g — то же самое, но для глобально установленных пакетов.

Чтобы получить только пакеты верхнего уровня (в основном те, которые вы сказали npm установить и перечислили в файле package.json), выполните команду npm list --depth=0:

Вы можете получить версию конкретного пакета, указав его имя:

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

Если вы хотите посмотреть, какая последняя версия пакета доступна в репозитории npm, выполните команду npm view [имя_пакета] version:

❯ npm view cowsay version

1.3.1

Установка старой версии пакета npm¶

Установка старой версии пакета npm может быть полезна для решения проблемы совместимости.

Вы можете установить старую версию пакета npm, используя синтаксис @:

npm install <package>@<version>

Пример:

устанавливает версию 1.3.1 (на момент написания статьи).

Установите версию 1.2.0 с помощью:

То же самое можно сделать и с глобальными пакетами:

Вас также может заинтересовать список всех предыдущих версий пакета. Вы можете сделать это с помощью npm view <package> versions:

❯ npm view cowsay versions

[ '1.0.0',
  '1.0.1',
  '1.0.2',
  '1.0.3',
  '1.1.0',
  '1.1.1',
  '1.1.2',
  '1.1.3',
  '1.1.4',
  '1.1.5',
  '1.1.6',
  '1.1.7',
  '1.1.8',
  '1.1.9',
  '1.2.0',
  '1.2.1',
  '1.3.0',
  '1.3.1' ]

Обновление всех зависимостей Node до последней версии¶

Когда вы устанавливаете пакет с помощью npm install <packagename>, последняя доступная версия пакета загружается и помещается в папку node_modules, и соответствующая запись добавляется в файлы package.json и package-lock.json, которые присутствуют в вашей текущей папке.

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

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

Когда вы npm install cowsay, эта запись будет добавлена в файл package.json:

{
    "dependencies": {
        "cowsay": "^1.3.1"
    }
}

а это выдержка из package-lock.json, где я удалил вложенные зависимости для ясности:

{
    "requires": true,
    "lockfileVersion": 1,
    "dependencies": {
        "cowsay": {
            "version": "1.3.1",
            "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.3.1.tgz",
            "integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkMAjufp+0F9eLjzRnOH",
            "requires": {
                "get-stdin": "^5.0.1",
                "optimist": "~0.6.1",
                "string-width": "~2.1.1",
                "strip-eof": "^1.0.0"
            }
        }
    }
}

Теперь эти 2 файла говорят нам, что мы установили версию 1.3.1 cowsay, а наше правило для обновлений — ^1.3.1, что для правил версионности npm (объяснение позже) означает, что npm может обновляться до патчей и минорных релизов: 0.13.1, 0.14.0 и так далее.

Если появляется новый минорный или патч релиз и мы набираем npm update, установленная версия обновляется, а файл package-lock.json старательно заполняется новой версией.

package.json остается неизменным.

Чтобы узнать о новых выпусках пакетов, вы выполняете команду npm outdated.

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

dQXY78UwUHW2iHblpRRLd8YdM4Zvdyf-3ctc

Некоторые из этих обновлений являются основными релизами. Запуск npm update не обновит их версию. Основные релизы никогда не обновляются таким образом, потому что они (по определению) вносят разрушающие изменения, а npm хочет избавить вас от проблем.

Чтобы обновить до новой мажорной версии все пакеты, установите пакет npm-check-updates глобально:

npm install -g npm-check-updates

затем запустите его:

Это обновит все подсказки о версии в файле package.json, в dependencies и devDependencies, чтобы npm мог установить новую основную версию.

Теперь вы готовы к запуску обновления:

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

Семантическое версионирование с помощью npm¶

Semantic Versioning — это соглашение, используемое для придания смысла версиям.

Если и есть что-то хорошее в пакетах Node.js, так это то, что все согласились использовать Semantic Versioning для нумерации версий.

Концепция Semantic Versioning проста: все версии состоят из 3 цифр: x.y.z.

  • первая цифра — основная версия
  • вторая цифра — минорная версия
  • третья цифра — версия патча.

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

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

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

Почему это так важно?

Потому что npm установил некоторые правила, которые мы можем использовать в package.json файле для выбора версий, до которых он может обновить наши пакеты, когда мы запускаем npm update.

Правила используют эти символы:

  • ^
  • ~
  • `>
  • >=
  • <
  • <=
  • =
  • -
  • ||

Давайте рассмотрим эти правила подробнее:

  • ^: если вы пишете ^0.13.0, при запуске npm update он может обновиться до патча и минорных релизов: 0.13.1, 0.14.0 и так далее.
  • ~: если вы напишете ~0.13.0, при запуске npm update он может обновляться до патчей: 0.13.1 — нормально, а 0.14.0 — нет.
  • <: вы принимаете любую версию выше той, которую вы указали.
  • >=: вы принимаете любую версию, равную или более высокую, чем та, которую вы указали
  • <=: вы принимаете любую версию, равную или меньшую указанной вами
  • <: вы принимаете любую версию ниже указанной вами
  • =: вы принимаете именно эту версию
  • -: вы принимаете диапазон версий. Пример: 2.1.0 - 2.6.2.
  • ||: вы объединяете наборы. Пример: < 2.1 || > 2.6.

Вы можете комбинировать некоторые из этих обозначений, например, использовать 1.0.0 || >=1.1.0 <1.2.0, чтобы использовать либо 1.0.0, либо один релиз, начиная с 1.1.0, но ниже 1.2.0.

Существуют и другие правила:

  • нет символа: вы принимаете только ту конкретную версию, которую вы указали (1.2.1)
  • latest: вы хотите использовать последнюю доступную версию.

Удаление пакетов npm локально или глобально¶

Чтобы удалить пакет, который вы ранее установили локально (используя npm install <package-name> в папке node_m`odules), выполните:

npm uninstall <package-name>

из корневой папки проекта (папки, содержащей папку node_modules).

Эта операция также удалит ссылку в package.json file.

Если пакет был зависимостью разработки, перечисленной в devDependencies файла package.json, вы должны использовать флаг -D / --save-dev, чтобы удалить его из файла:

npm uninstall -D <package-name>

Если пакет установлен глобально, необходимо добавить флаг -g / --global:

npm uninstall -g <package-name>

Пример:

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

npm глобальные или локальные пакеты¶

Когда пакет лучше всего устанавливать глобально? И почему?

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

  • локальные пакеты устанавливаются в директорию, где вы запустили npm install <имя пакета>, и помещаются в папкуnode_modules` в этой директории.
  • глобальные пакеты устанавливаются в одно место в вашей системе (где именно — зависит от вашей установки), независимо от того, где вы запустили npm install -g <package-name>.

В вашем коде они оба требуются одинаково:

Когда же следует устанавливать тем или иным способом?

В целом, все пакеты следует устанавливать локально.

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

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

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

Пакет должен быть установлен глобально, если он предоставляет исполняемую команду, которую вы запускаете из оболочки (CLI), и она используется повторно в разных проектах.

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

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

  • npm
  • create-react-app
  • vue-cli
  • grunt-cli
  • mocha
  • react-native-cli
  • gatsby-cli
  • forever
  • nodemon

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

в командной строке.

npm dependencies и devDependencies¶

Когда пакет является зависимостью, а когда — зависимостью разработки?

Когда вы устанавливаете пакет npm с помощью npm install <имя пакета>, вы устанавливаете его как зависимость.

Пакет автоматически перечисляется в файле package.json в списке dependencies (начиная с npm 5: до этого вам нужно было вручную указывать --save).

Когда вы добавляете флаг -D, или --save-dev, вы устанавливаете пакет как зависимость разработки, что добавляет его в список devDependencies.

Зависимости разработки — это пакеты, предназначенные только для разработки, которые не нужны в производстве. Например, пакеты для тестирования, webpack или Babel.

Когда вы переходите в продакшн, если вы набираете npm install и папка содержит файл package.json, они будут установлены, так как npm предполагает, что это развертывание разработки.

Вам нужно установить флаг --production (npm install --production), чтобы избежать установки этих зависимостей разработки.

npx Node Package Runner¶

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

В этом разделе я хочу представить очень мощную команду, которая доступна в npm начиная с версии 5.2, выпущенной в июле 2017 года: npx.

Если вы не хотите устанавливать npm, вы можете установить npx как standalone package.

npx позволяет запускать код, созданный с помощью Node.js и опубликованный через реестр npm.

Легкий запуск локальных команд¶

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

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

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

Выполнение команд без установки¶

Есть еще одна замечательная особенность npm — это возможность запускать команды без предварительной установки.

Это довольно полезно, в основном потому, что:

  1. вам не нужно ничего устанавливать
  2. вы можете запускать разные версии одной и той же команды, используя синтаксис @version.

Типичной демонстрацией использования npx является команда cowsay. Команда cowsay выведет корову, говорящую то, что вы написали в команде. Например:

cowsay "Hello" напечатает

 _______
< Hello >
 -------
           ^__^
           (oo)_______
            (__)       )/
                ||----w |
                ||     ||

Это возможно, если команда cowsay была установлена глобально из npm ранее, в противном случае вы получите ошибку при попытке запустить команду.

npx позволяет вам запустить эту команду npm без ее локальной установки:

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

  • использование инструмента vue CLI для создания новых приложений и их запуска: npx vue create my-vue-app.
  • создание нового приложения React с помощью create-react-app: npx create-react-app my-react-app.

и многое другое.

После загрузки загруженный код будет стерт.

Запустите некоторый код, используя другую версию Node.js {#run-some-code-using-a-different-node-js-version}.¶

Используйте @, чтобы указать версию, и объедините ее с пакетом node npm:

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

Запуск произвольных фрагментов кода непосредственно из URL.¶

npx не ограничивает вас пакетами, опубликованными в реестре npm.

Вы можете запускать код, который находится, например, в гисте GitHub:

npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32

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

Цикл событий¶

Цикл событий — один из самых важных аспектов JavaScript. Этот раздел объясняет внутренние детали того, как JavaScript работает с одним потоком, и как он обрабатывает асинхронные функции.

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

Ваш код JavaScript работает в однопоточном режиме. Одновременно выполняется только одно действие.

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

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

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

Среда управляет несколькими параллельными циклами событий для обработки вызовов API, например. Web Workers также работают в своем собственном цикле событий.

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

Блокирование цикла событий¶

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

Почти все примитивы ввода-вывода в JavaScript являются неблокирующими. Сетевые запросы, операции с файловой системой Node.js и так далее. Блокировка является исключением, и именно поэтому JavaScript так сильно основан на обратных вызовах, а в последнее время на обещаниях и async/await.

Стек вызовов¶

Стек вызовов представляет собой очередь LIFO (Last In, First Out).

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

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

Вам знакома трассировка стека ошибок в отладчике или в консоли браузера?

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

SFxrWa7lVtAfUsjnjoMqgCGdG4bK0jDvi-11

Простое объяснение цикла событий¶

Давайте выберем пример:

const bar = () => console.log('bar');

const baz = () => console.log('baz');

const foo = () => {
    console.log('foo');
    bar();
    baz();
};

foo();

Этот код печатает:

как и ожидалось.

Когда этот код выполняется, сначала вызывается foo(). Внутри foo() мы сначала вызываем bar(), затем baz().

В этот момент стек вызовов выглядит следующим образом:

bFPM-QZwRcB6APbq6sSJpyQMZHWRACvJzAly

Цикл событий на каждой итерации смотрит, есть ли что-то в стеке вызовов, и выполняет это:

T3jPPIkLHGvy0QXBrUz8cb3VM0bVVez-joQ4

пока стек вызовов не опустеет.

Очередь выполнения функций¶

Приведенный выше пример выглядит нормально, в нем нет ничего особенного: JavaScript находит функции для выполнения и выполняет их по порядку.

Давайте посмотрим, как отложить выполнение функции до тех пор, пока стек вызовов не станет пустым.

Смысл использования setTimeout(() => {}), 0) заключается в том, чтобы вызвать функцию, но выполнить ее после того, как все остальные функции в коде будут выполнены.

Возьмем такой пример:

const bar = () => console.log('bar');

const baz = () => console.log('baz');

const foo = () => {
    console.log('foo');
    setTimeout(bar, 0);
    baz();
};

foo();

Этот код печатает, что, возможно, удивительно:

Когда этот код выполняется, сначала вызывается foo(). Внутри foo() мы сначала вызываем setTimeout, передавая bar в качестве аргумента, и инструктируем его выполнить немедленно так быстро, как он может, передавая 0 в качестве таймера. Затем мы вызываем baz().

В этот момент стек вызовов выглядит следующим образом:

iUnlUVBLW8ozpE2ewbJswyp9tOP5OzPUXn0-

Вот порядок выполнения всех функций в нашей программе:

MsT6C2UAZJaEEm6SmU266PO-V4b-DY0wlMqb

Почему это происходит?

Очередь сообщений¶

Когда вызывается setTimeout(), браузер или Node.js запускает таймер. Как только таймер истечет, в данном случае немедленно, поскольку в качестве таймаута мы указали 0, функция обратного вызова будет помещена в очередь сообщений.

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

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

Нам не нужно ждать, пока такие функции, как setTimeout, fetch или другие, выполнят свою работу, потому что они предоставляются браузером и живут в своих собственных потоках. Например, если вы установите таймаут setTimeout на 2 секунды, вам не придется ждать 2 секунды — ожидание происходит в другом месте.

ES6 Очередь заданий¶

В ECMAScript 2015 появилась концепция очереди заданий, которая используется Promises (также представленными в ES6/ES2015). Это способ выполнить результат асинхронной функции как можно быстрее, а не помещать его в конец стека вызовов.

Обещания, которые разрешаются до завершения текущей функции, будут выполнены сразу после текущей функции.

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

Пример:

const bar = () => console.log('bar');

const baz = () => console.log('baz');

const foo = () => {
    console.log('foo');
    setTimeout(bar, 0);
    new Promise((resolve, reject) =>
        resolve('should be right after baz, before bar')
    ).then((resolve) => console.log(resolve));
    baz();
};

foo();

Это печатает:

foo
baz
should be right after foo, before bar
bar

Это большая разница между Promises (и async/await, который построен на promises) и обычными асинхронными функциями через setTimeout() или другие API платформы.

Понимание process.nextTick()¶

По мере того как вы пытаетесь понять цикл событий Node.js, одной из важных его частей является process.nextTick(). Она взаимодействует с циклом событий особым образом.

Каждый раз, когда цикл событий совершает полный цикл, мы называем это тиком.

Когда мы передаем функцию в process.nextTick(), мы инструктируем движок вызывать эту функцию в конце текущей операции, перед началом следующего тика цикла событий:

process.nextTick(() => {
    // do something
});

Цикл событий занят обработкой кода текущей функции.

Когда эта операция заканчивается, движок JavaScript запускает все функции, переданные в вызовы nextTick во время этой операции.

Так мы можем сказать движку JavaScript обрабатывать функцию асинхронно (после текущей функции), но как можно быстрее, а не ставить ее в очередь.

Вызов setTimeout(() => {}, 0) выполнит функцию в следующем тике, гораздо позже, чем при использовании nextTick().

Используйте nextTick(), когда хотите быть уверены, что на следующей итерации цикла событий код уже будет выполнен.

Понимание setImmediate()¶

Когда вы хотите выполнить некоторый фрагмент кода асинхронно, но как можно скорее, один из вариантов — использовать функцию setImmediate(), предоставляемую Node.js:

setImmediate(() => {
    // run something
});

Любая функция, переданная в качестве аргумента setImmediate(), является обратным вызовом, который выполняется в следующей итерации цикла событий.

Чем setImmediate() отличается от setTimeout(() => {}, 0) (передача таймаута 0 мс) и от process.nextTick()?

Функция, переданная в process.nextTick(), будет выполнена на текущей итерации цикла событий, после завершения текущей операции. Это означает, что она всегда будет выполняться перед setTimeout() и setImmediate().

Обратный вызов setTimeout() с задержкой в 0 мс очень похож на setImmediate(). Порядок выполнения будет зависеть от различных факторов, но оба они будут выполняться в следующей итерации цикла событий.

Таймеры¶

При написании кода JavaScript может возникнуть необходимость отложить выполнение функции. Узнайте, как использовать setTimeout() и setInterval() для планирования функций на будущее.

setTimeout()

При написании кода JavaScript может возникнуть необходимость отложить выполнение функции. Этой задачей занимается setTimeout.

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

setTimeout(() => {
    // runs after 2 seconds
}, 2000);

setTimeout(() => {
    // runs after 50 milliseconds
}, 50);

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

const myFunction = (firstParam, secondParam) => {
    // do something
};

// runs after 2 seconds
setTimeout(myFunction, 2000, firstParam, secondParam);

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

const id = setTimeout(() => {
    // should run after 2 seconds
}, 2000);

// I changed my mind
clearTimeout(id);

Нулевая задержка¶

Если вы зададите задержку тайм-аута равной 0, функция обратного вызова будет выполнена как можно быстрее, но после выполнения текущей функции:

setTimeout(() => {
    console.log('after ');
}, 0);

console.log(' before ');

будет выводить до после.

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

Некоторые браузеры (IE и Edge) реализуют метод setImmediate(), который делает такую же точно функциональность, но он не является стандартным и недоступен в других браузерах. Но это стандартная функция в Node.js.

setInterval()

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

setInterval(() => {
    // runs every 2 seconds
}, 2000);

Приведенная выше функция выполняется каждые 2 секунды, пока вы не прикажете ей остановиться, используя clearInterval, передавая ей идентификатор интервала, который вернула setInterval:

const id = setInterval(() => {
    // runs every 2 seconds
}, 2000);

clearInterval(id);

Обычно принято вызывать clearInterval внутри функции обратного вызова setInterval, чтобы позволить ей автоматически определять, следует ли ей запуститься снова или остановиться. Например, этот код запускает что-то, если App.somethingIWait имеет значение arrived:

const interval = setInterval(() => {
    if (App.somethingIWait === 'arrived') {
        clearInterval(interval);
        return;
    }
    // otherwise do things
}, 100);

Рекурсивный setTimeout¶

setInterval запускает функцию каждые n миллисекунд, без какого-либо учета того, когда функция закончила свое выполнение.

Если функция выполняется всегда одинаковое количество времени, то все в порядке:

eyf875I-cxYqAgNDSeh7CeLg4RXdJIgJphEw

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

ge2DPdTuZwHnJIyUH9VSLok1J5WHPOlc1DML

И, возможно, одна долгая казнь накладывается на следующую:

I9kJc6l-BIT850OGlNDJre80RcsLp7N4amvy

Чтобы избежать этого, вы можете запланировать рекурсивный setTimeout, который будет вызван по завершении функции обратного вызова:

const myFunction = () => {
  // do something
  setTimeout(myFunction, 1000)
}

setTimeout(
  myFunction()
}, 1000)

для реализации этого сценария:

B2kod2dFuR5U1uwaaW9SGiC1zX5gIUEaiJ8A

setTimeout и setInterval также доступны в Node.js, через модуль Timers.

Node.js также предоставляет setImmediate(), что эквивалентно использованию setTimeout(() => {}, 0), в основном используется для работы с циклом событий Node.js.

Асинхронное программирование и обратные вызовы¶

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

Асинхронность в языках программирования¶

Компьютеры асинхронны по своей конструкции.

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

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

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

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

Как правило, языки программирования являются синхронными, а некоторые предоставляют возможность управлять асинхронностью, в самом языке или с помощью библиотек. C, Java, C#, PHP, Go, Ruby, Swift, Python — все они по умолчанию синхронны. Некоторые из них управляют асинхронностью с помощью потоков, порождая новый процесс.

JavaScript¶

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

Строки кода выполняются последовательно, одна за другой.

Например:

const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();

Но JavaScript родился внутри браузера. Вначале его основной задачей было реагировать на действия пользователя, такие как onClick, onMouseOver, onChange, onSubmit и так далее. Как он мог это делать при синхронной модели программирования?

Ответ кроется в его окружении. Браузер** предоставляет способ сделать это, предоставляя набор API, которые могут обрабатывать такого рода функциональность.

Совсем недавно Node.js представил неблокирующую среду ввода-вывода, чтобы распространить эту концепцию на доступ к файлам, сетевые вызовы и так далее.

Обратные вызовы¶

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

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

document
    .getElementById('button')
    .addEventListener('click', () => {
        // item clicked
    });

Это так называемый обратный вызов.

Обратный вызов — это простая функция, которая передается в качестве значения другой функции и будет выполнена только тогда, когда произойдет событие. Мы можем сделать это, потому что JavaScript имеет функции первого класса, которые можно присваивать переменным и передавать другим функциям (называемым функциями высшего порядка).

Обычно весь клиентский код оборачивается в слушатель события load на объекте window, который запускает функцию обратного вызова только тогда, когда страница готова:

window.addEventListener('load', () => {
    // window loaded
    //do what you want
});

Обратные вызовы используются повсеместно, а не только в событиях DOM.

Одним из распространенных примеров является использование таймеров:

setTimeout(() => {
    // runs after 2 seconds
}, 2000);

XHR-запросы также принимают обратный вызов, в данном примере назначая свойству функцию, которая будет вызываться при наступлении определенного события (в данном случае изменения состояния запроса):

const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
        xhr.status === 200
            ? console.log(xhr.responseText)
            : console.error('error');
    }
};

xhr.open('GET', 'https://yoursite.com');
xhr.send();

Обработка ошибок в обратных вызовах¶

Как обрабатывать ошибки в обратных вызовах? Одной из очень распространенных стратегий является использование того, что принято в Node.js: первым параметром любой функции обратного вызова является объект ошибки — обратные вызовы с ошибками.

Если ошибки нет, объект является null. Если ошибка есть, он содержит некоторое описание ошибки и другую информацию.

fs.readFile('/file.json', (err, data) => {
    if (err !== null) {
        //handle error
        console.log(err);
        return;
    }

    // no errors, process data
    console.log(data);
});

Проблема с обратными вызовами¶

Обратные вызовы отлично подходят для простых случаев!

Однако каждый обратный вызов добавляет уровень вложенности. Когда у вас много обратных вызовов, код начинает быстро усложняться:

window.addEventListener('load', () => {
    document
        .getElementById('button')
        .addEventListener('click', () => {
            setTimeout(() => {
                items.forEach((item) => {
                    // your code here
                });
            }, 2000);
        });
});

Это всего лишь простой 4-уровневый код, но я видел гораздо больше уровней вложенности, и это не весело.

Как нам решить эту проблему?

Альтернативы обратным вызовам¶

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

  • Обещания (ES6)
  • Async/Await (ES8)

Обещания¶

Обещания — это один из способов работы с асинхронным кодом в JavaScript без написания большого количества обратных вызовов в коде.

Введение в обещания¶

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

Хотя обещания существуют уже много лет, они были стандартизированы и введены в ES2015, а в ES2017 их вытеснили асинхронные функции.

Функции Async используют API promises в качестве своего строительного блока, поэтому их понимание является фундаментальным, даже если в более новом коде вы, скорее всего, будете использовать функции async вместо promises.

Как работают обещания, вкратце¶

Как только обещание было вызвано, оно начинает выполняться в состоянии ожидания. Это означает, что вызывающая функция продолжает выполнение, ожидая, пока обещание выполнит свою собственную обработку и даст вызывающей функции обратную связь.

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

Какие JS API используют обещания?¶

Помимо вашего собственного кода и кода библиотек, обещания используются стандартными современными Web API, такими как:

  • t the Battery API
  • Fetch API
  • Service Workers.

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

Создание обещания¶

Promise API предоставляет конструктор Promise, который вы инициализируете с помощью new Promise():

let done = true;

const isItDoneYet = new Promise((resolve, reject) => {
    if (done) {
        const workDone = 'Here is the thing I built';
        resolve(workDone);
    } else {
        const why = 'Still working on something else';
        reject(why);
    }
});

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

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

Потребление обещания¶

В предыдущем разделе мы представили, как создается обещание.

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

const isItDoneYet = new Promise();
//...

const checkIfItsDone = () => {
    isItDoneYet
        .then((ok) => {
            console.log(ok);
        })
        .catch((err) => {
            console.error(err);
        });
};

Выполнение checkIfItsDone() выполнит обещание isItDoneYet() и будет ждать его разрешения, используя обратный вызов then, а если возникнет ошибка, то она будет обработана в обратном вызове catch.

Цепочка обещаний¶

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

Отличным примером цепочки обещаний является Fetch API, слой поверх API XMLHttpRequest, который мы можем использовать для получения ресурса и составления очереди цепочки обещаний для выполнения при получении ресурса.

Fetch API — это механизм, основанный на обещаниях, и вызов fetch() эквивалентен определению нашего собственного обещания с помощью new Promise().

Пример цепочки обещаний¶

const status = (response) => {
    if (response.status >= 200 && response.status < 300) {
        return Promise.resolve(response);
    }
    return Promise.reject(new Error(response.statusText));
};

const json = (response) => response.json();

fetch('/todos.json')
    .then(status)
    .then(json)
    .then((data) => {
        console.log(
            'Request succeeded with JSON response',
            data
        );
    })
    .catch((error) => {
        console.log('Request failed', error);
    });

В этом примере мы вызываем fetch() для получения списка элементов TODO из файла todos.json, находящегося в корне домена, и создаем цепочку обещаний.

Выполнение fetch() возвращает response, который имеет множество свойств, и в них мы ссылаемся на:

  • status, числовое значение, представляющее код статуса HTTP
  • statusText, сообщение о статусе, которое равно OK, если запрос прошел успешно.

response также имеет метод json(), который возвращает обещание, которое будет разрешено с содержимым тела, обработанным и преобразованным в JSON.

Итак, учитывая эти предпосылки, вот что происходит: первое обещание в цепочке — это определенная нами функция status(), которая проверяет статус ответа и, если это не успешный ответ (между 200 и 299), она отклоняет обещание.

Эта операция приведет к тому, что цепочка обещаний пропустит все перечисленные обещания и перейдет непосредственно к оператору catch() внизу, записывая в журнал текст Request failed вместе с сообщением об ошибке.

В случае успеха вместо этого вызывается определенная нами функция json(). Поскольку предыдущее обещание в случае успеха возвращало объект response, мы получаем его в качестве входных данных для второго обещания.

В данном случае мы возвращаем обработанные данные JSON, поэтому третье обещание получает JSON напрямую:

.then((data) => {
  console.log('Request succeeded with JSON response', data)
})

и мы просто выводим его в консоль.

Обработка ошибок¶

В примере, приведенном в предыдущем разделе, у нас был catch, который добавлялся к цепочке обещаний.

Когда что-то в цепочке обещаний не срабатывает и вызывает ошибку или отклоняет обещание, управление переходит к ближайшему по цепочке оператору catch().

new Promise((resolve, reject) => {
    throw new Error('Error');
}).catch((err) => {
    console.error(err);
});

// or

new Promise((resolve, reject) => {
    reject('Error');
}).catch((err) => {
    console.error(err);
});

Каскадирование ошибок¶

Если внутри catch() возникает ошибка, вы можете добавить второй catch() для ее обработки, и так далее.

new Promise((resolve, reject) => {
    throw new Error('Error');
})
    .catch((err) => {
        throw new Error('Error');
    })
    .catch((err) => {
        console.error(err);
    });

Оркестрирование обещаний¶

Promise.all()

Если вам нужно синхронизировать различные обещания, Promise.all() поможет вам определить список обещаний и выполнить что-то, когда все они будут разрешены.

Пример:

const f1 = fetch('/something.json');
const f2 = fetch('/something2.json');

Promise.all([f1, f2])
    .then((res) => {
        console.log('Array of results', res);
    })
    .catch((err) => {
        console.error(err);
    });

Синтаксис ES2015 destructuring assignment позволяет также делать:

Promise.all([f1, f2]).then(([res1, res2]) => {
    console.log('Results', res1, res2);
});

Конечно, вы не ограничены использованием fetch, любое обещание подходит.

Promise.race()

Promise.race() запускается, когда разрешается первое из переданных вам обещаний, и выполняет присоединенный обратный вызов только один раз, с результатом первого разрешенного обещания.

Пример:

const promiseOne = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'one');
});

const promiseTwo = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'two');
});

Promise.race([promiseOne, promiseTwo]).then((result) => {
    console.log(result); // 'two'
});

Общая ошибка, Uncaught TypeError: undefined is not a promise¶

Если вы получаете в консоли ошибку Uncaught TypeError: undefined is not a promise, убедитесь, что вы используете new Promise(), а не просто Promise().

Асинхронность и ожидание¶

Откройте для себя современный подход к асинхронным функциям в JavaScript.

JavaScript за очень короткое время эволюционировал от обратных вызовов к обещаниям (ES2015), а с ES2017 асинхронный JavaScript стал еще проще благодаря синтаксису async/await.

Асинхронные функции — это комбинация обещаний и генераторов, и, по сути, они являются абстракцией более высокого уровня над обещаниями. Позвольте мне повторить: async/await построены на обещаниях.

Почему были введены async/await?¶

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

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

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

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

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

Как это работает¶

Функция async возвращает обещание, как в этом примере:

const doSomethingAsync = () => {
    return new Promise((resolve) => {
        setTimeout(() => resolve('I did something'), 3000);
    });
};

Когда вы хотите вызвать эту функцию, вы добавляете await, и вызывающий код будет остановлен пока обещание не будет разрешено или отклонено. Одна оговорка: клиентская функция должна быть определена как async.

Вот пример:

const doSomething = async () => {
    console.log(await doSomethingAsync());
};

Быстрый пример¶

Это простой пример использования async/await для асинхронного запуска функции:

const doSomethingAsync = () => {
    return new Promise((resolve) => {
        setTimeout(() => resolve('I did something'), 3000);
    });
};

const doSomething = async () => {
    console.log(await doSomethingAsync());
};

console.log('Before');
doSomething();
console.log('After');

Приведенный выше код выведет в консоль браузера следующее:

Before
After
I did something // after 3s

Обещайте все вещи¶

Добавление ключевого слова async к любой функции означает, что функция будет возвращать обещание.

Даже если она не делает этого явно, внутренне это заставит ее вернуть обещание.

Вот почему этот код действителен:

const aFunction = async () => {
  return 'test'
}

aFunction().then(alert) // This will alert 'test'

и это то же самое, что:

const aFunction = async () => {
    return Promise.resolve('test');
};

aFunction().then(alert); // This will alert 'test'

Код гораздо проще читать.¶

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

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

Например, вот как можно получить ресурс JSON и разобрать его, используя обещания:

const getFirstUserData = () => {
    return fetch('/users.json') // get users list
        .then((response) => response.json()) // parse JSON
        .then((users) => users[0]) // pick first user
        .then((user) => fetch(`/users/${user.name}`)) // get user data
        .then((userResponse) => userResponse.json()); // parse JSON
};

getFirstUserData();

А вот та же функциональность, предоставляемая с помощью await/async:

const getFirstUserData = async () => {
    const response = await fetch('/users.json'); // get users list
    const users = await response.json(); // parse JSON
    const user = users[0]; // pick first user
    const userResponse = await fetch(`/users/${user.name}`); // get user data
    const userData = await userResponse.json(); // parse JSON
    return userData;
};

getFirstUserData();

Несколько последовательных асинхронных функций¶

Функции async можно очень легко объединять в цепочки, а синтаксис гораздо более читабелен, чем у простых обещаний:

const promiseToDoSomething = () => {
    return new Promise((resolve) => {
        setTimeout(() => resolve('I did something'), 10000);
    });
};

const watchOverSomeoneDoingSomething = async () => {
    const something = await promiseToDoSomething();
    return something + ' and I watched';
};

const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
    const something = await watchOverSomeoneDoingSomething();
    return something + ' and I watched as well';
};

watchOverSomeoneWatchingSomeoneDoingSomething().then(
    (res) => {
        console.log(res);
    }
);

Будет печататься:

I did something and I watched and I watched as well

Более простая отладка¶

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

async/await делает это очень легко, потому что для компилятора это просто как синхронный код.

Эмиттер событий Node.js¶

В Node.js можно работать с пользовательскими событиями.

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

С обратной стороны Node.js предлагает нам возможность построить подобную систему, используя events модуль.

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

Вы инициализируете его, используя:

const EventEmitter = require('events');
const eventEmitter = new EventEmitter();

Этот объект раскрывает, среди многих других, методы on и emit.

  • emit используется для запуска события
  • on используется для добавления функции обратного вызова, которая будет выполняться при срабатывании события.

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

eventEmitter.on('start', () => {
    console.log('started');
});

Когда мы запустим:

eventEmitter.emit('start');

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

Примечание: addListener() — это псевдоним для on(), если вы видите, что он используется.

Передача аргументов событию¶

Вы можете передать аргументы обработчику события, передав их в качестве дополнительных аргументов в emit():

eventEmitter.on('start', (number) => {
    console.log(`started ${number}`);
});

eventEmitter.emit('start', 23);

Несколько аргументов:

eventEmitter.on('start', (start, end) => {
    console.log(`started from ${start} to ${end}`);
});

eventEmitter.emit('start', 1, 100);

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

  • once(): добавление одноразового слушателя события
  • removeListener() / off(): удалить слушателя из события
  • removeAllListeners(): удаление всех слушателей для события

Как работают HTTP-запросы¶

Что происходит, когда вы набираете URL в браузере, от начала и до конца?

В этом разделе описывается, как браузеры выполняют запросы страниц по протоколу HTTP/1.1.

Если вы когда-нибудь проходили собеседование, вас могли спросить: «Что происходит, когда вы набираете что-то в поисковой строке Google и нажимаете клавишу Enter?».

Это один из самых популярных вопросов, которые вам задают. Люди просто хотят узнать, можете ли вы объяснить некоторые базовые понятия и имеете ли вы хоть какое-то представление о том, как на самом деле работает интернет.

В этом разделе я проанализирую, что происходит, когда вы вводите URL-адрес в адресную строку браузера и нажимаете клавишу Enter.

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

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

Протокол HTTP¶

Я анализирую только URL-запросы.

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

Я предполагаю, что вы вводите фактический URL.

Когда вы вводите URL и нажимаете enter, браузер сначала строит полный URL.

Если вы просто ввели домен, например flaviocopes.com, браузер по умолчанию добавит к нему HTTP://, по умолчанию используя протокол HTTP.

Вещи относятся к macOS / Linux.¶

К вашему сведению. Windows может делать некоторые вещи немного по-другому.

Фаза поиска DNS¶

Браузер начинает DNS-поиск, чтобы получить IP-адрес сервера.

Доменное имя — это удобное сокращение для нас, людей, но интернет устроен таким образом, что компьютеры могут узнать точное местоположение сервера по его IP-адресу, который представляет собой набор цифр типа 222.324.3.1 (IPv4).

Сначала проверяется локальный кэш DNS, чтобы узнать, не был ли домен недавно разрешен.

Chrome имеет удобный визуализатор кэша DNS, который можно посмотреть по этому URL: chrome://net-internals/#dns (скопируйте и вставьте его в адресную строку браузера Chrome).

Если там ничего не найдено, браузер использует DNS-резольвер, используя системный вызов POSIX gethostbyname для получения информации о хосте.

gethostbyname¶

gethostbyname сначала ищет информацию в локальном файле hosts, который в macOS или Linux находится в /etc/hosts, чтобы проверить, предоставляет ли система информацию локально.

Если это не дает никакой информации о домене, система делает запрос на DNS-сервер.

Адрес DNS-сервера хранится в настройках системы.

Вот 2 популярных DNS-сервера:

  • 8.8.8.8: публичный DNS-сервер Google
  • 1.1.1.1.1: DNS-сервер CloudFlare.

Большинство людей используют DNS-сервер, предоставляемый их интернет-провайдером.

Браузер выполняет DNS-запрос, используя протокол UDP.

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

То, как выполняется запрос UDP, не входит в задачи данного руководства.

DNS-сервер может иметь IP-адрес домена в кэше. Если нет, он обратится к корневому DNS-серверу. Это система (состоящая из 13 реальных серверов, распределенных по всей планете), которая управляет всем интернетом.

DNS-сервер не знает адреса всех и каждого доменного имени на планете.

Он знает, где находятся верхнеуровневые DNS-резолверы.

Домен верхнего уровня — это расширение домена: .com, .it, .pizza и так далее.

Как только корневой DNS-сервер получает запрос, он перенаправляет его на DNS-сервер домена верхнего уровня (TLD).

Допустим, вы ищете flaviocopes.com. DNS-сервер корневого домена возвращает IP-адрес сервера TLD .com.

Теперь наш DNS-резольвер будет кэшировать IP-адрес этого сервера TLD, поэтому ему не придется снова запрашивать его у корневого DNS-сервера.

DNS-сервер ДВУ будет иметь IP-адреса авторитетных серверов имен для домена, который мы ищем.

Каким образом? Когда вы покупаете домен, регистратор домена отправляет соответствующий TDL серверам имен. Когда вы обновляете серверы имен (например, при смене хостинг-провайдера), эта информация автоматически обновляется регистратором домена.

Это DNS-серверы хостинг-провайдера. Их обычно больше одного, чтобы служить в качестве резервных.

Например:

  • ns1.dreamhost.com
  • ns2.dreamhost.com
  • ns3.dreamhost.com

DNS-резольвер начинает с первого и пытается узнать IP-адрес домена (с поддоменом тоже), который вы ищете.

Это окончательный источник истины для IP-адреса.

Теперь, когда у нас есть IP-адрес, мы можем продолжить наше путешествие.

TCP request handshaking¶

Имея IP-адрес сервера, браузер может инициировать TCP-соединение с ним.

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

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

Отправка запроса¶

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

Он состоит из 3 частей:

  • строка запроса
  • заголовок запроса
  • тело запроса

Строка запроса¶

Строка запроса содержит, в одной строке:

  • метод HTTP
  • местоположение ресурса
  • версия протокола

Пример:

Заголовок запроса¶

Заголовок запроса представляет собой набор пар поле: значение, которые задают определенные значения.

Есть 2 обязательных поля, одно из которых Host, а другое Connection, в то время как все остальные поля являются необязательными:

Host: flaviocopes.com
Connection: close

Host указывает доменное имя, на которое мы хотим нацелиться, а Connection всегда устанавливается на close, если только соединение не должно оставаться открытым.

Некоторые из наиболее используемых полей заголовка следующие:

  • Origin
  • Accept
  • Accept-Encoding
  • Cookie
  • Cache-Control
  • Dnt

но существует и множество других.

Заголовочная часть завершается пустой строкой.

Тело запроса¶

Тело запроса необязательно, оно не используется в GET-запросах, но очень часто используется в POST-запросах и иногда в других глаголах, и может содержать данные в формате JSON.

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

Ответ¶

После отправки запроса сервер обрабатывает его и отправляет ответ.

Ответ начинается с кода статуса и сообщения о статусе. Если запрос успешен и возвращает значение 200, то ответ начинается с:

Запрос может вернуть другой код состояния и сообщение, например, одно из следующих:

404 Not Found
403 Forbidden
301 Moved Permanently
500 Internal Server Error
304 Not Modified
401 Unauthorized

Ответ содержит список HTTP-заголовков и тело ответа (которое, поскольку мы делаем запрос в браузере, будет HTML).

Разбор HTML¶

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

  • CSS-файлы
  • изображения
  • фавикон
  • JavaScript-файлы

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

Build an HTTP Server with Node.js¶

Вот веб-сервер HTTP, который мы использовали в качестве приложения Node.js Hello World во введении:

const http = require('http');

const hostname = 'localhost';
const port = 3000;

const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Hello Worldn');
});

server.listen(port, hostname, () => {
    console.log(
        `Server running at http://${hostname}:${port}/`
    );
});

Давайте кратко проанализируем его. Мы включаем http модуль.

Мы используем модуль для создания HTTP-сервера.

Сервер настроен на прослушивание указанного порта, 3000. Когда сервер готов, вызывается функция обратного вызова listen.

Функция обратного вызова, которую мы передаем, будет выполняться при каждом поступающем запросе. При получении нового запроса вызывается функция request event, которая предоставляет два объекта: запрос (объект http.IncomingMessage) и ответ (объект http.ServerResponse).

request предоставляет детали запроса. Через него мы получаем доступ к заголовкам запроса и данным запроса.

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

В данном случае с:

Мы устанавливаем свойство statusCode в 200, чтобы указать на успешный ответ.

Мы также устанавливаем заголовок Content-Type:

res.setHeader('Content-Type', 'text/plain');

и мы завершаем закрытие ответа, добавляя содержимое в качестве аргумента к end():

res.end('Hello Worldn');

Выполнение HTTP-запросов с помощью Node.js¶

Как выполнять HTTP-запросы с помощью Node.js, используя GET, POST, PUT и DELETE.

Я использую термин HTTP, но HTTPS — это то, что должно использоваться везде, поэтому в этих примерах используется HTTPS вместо HTTP.

Выполнение запроса GET¶

const https = require('https');
const options = {
    hostname: 'flaviocopes.com',
    port: 443,
    path: '/todos',
    method: 'GET',
};

const req = https.request(options, (res) => {
    console.log(`statusCode: ${res.statusCode}`);
    res.on('data', (d) => {
        process.stdout.write(d);
    });
});

req.on('error', (error) => {
    console.error(error);
});

req.end();

Выполнить POST-запрос¶

const https = require('https');

const data = JSON.stringify({
    todo: 'Buy the milk',
});

const options = {
    hostname: 'flaviocopes.com',
    port: 443,
    path: '/todos',
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Content-Length': data.length,
    },
};

const req = https.request(options, (res) => {
    console.log(`statusCode: ${res.statusCode}`);
    res.on('data', (d) => {
        process.stdout.write(d);
    });
});

req.on('error', (error) => {
    console.error(error);
});

req.write(data);
req.end();

PUT и DELETE¶

Запросы PUT и DELETE используют тот же формат запроса POST и просто изменяют значение options.method.

HTTP-запросы в Node.js с использованием Axios¶

Axios — это очень популярная библиотека JavaScript, которую можно использовать для выполнения HTTP-запросов, работающая как в браузере, так и на платформе Node.js.

Она поддерживает все современные браузеры, включая поддержку IE8 и выше.

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

Использование Axios имеет довольно много преимуществ перед родным Fetch API:

  • поддерживает старые браузеры (Fetch нуждается в полифилле)
  • есть возможность прервать запрос
  • есть возможность установить таймаут ответа
  • встроенная защита от CSRF
  • поддерживает прогресс загрузки
  • выполняет автоматическое преобразование данных JSON
  • работает в Node.js

Установка¶

Axios можно установить с помощью npm:

или yarn:

или просто включите его в свою страницу с помощью unpkg.com:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

API Axios¶

Вы можете начать HTTP-запрос с объекта axios:

axios({
    url: 'https://dog.ceo/api/breeds/list/all',
    method: 'get',
    data: {
        foo: 'bar',
    },
});

но для удобства вы обычно используете:

  • axios.get()
  • axios.post()

(как в jQuery вы бы использовали $.get() и $.post() вместо $.ajax()).

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

  • axios.delete()
  • axios.put()
  • axios.patch()
  • axios.options()

и метод для получения HTTP-заголовков запроса, отбрасывая тело:

  • axios.head()

GET-запросы¶

Одним из удобных способов использования Axios является использование современного (ES2017) синтаксиса async/await.

Этот пример Node.js запрашивает Dog API для получения списка всех пород собак, используя axios.get(), и подсчитывает их:

const axios = require('axios');

const getBreeds = async () => {
    try {
        return await axios.get(
            'https://dog.ceo/api/breeds/list/all'
        );
    } catch (error) {
        console.error(error);
    }
};

const countBreeds = async () => {
    const breeds = await getBreeds();
    if (breeds.data.message) {
        console.log(
            `Got ${
                Object.entries(breeds.data.message).length
            } breeds`
        );
    }
};

countBreeds();

Если вы не хотите использовать async/await, вы можете использовать синтаксис Promises:

const axios = require('axios');

const getBreeds = () => {
    try {
        return axios.get(
            'https://dog.ceo/api/breeds/list/all'
        );
    } catch (error) {
        console.error(error);
    }
};

const countBreeds = async () => {
    const breeds = getBreeds()
        .then((response) => {
            if (response.data.message) {
                console.log(
                    `Got ${
                        Object.entries(
                            response.data.message
                        ).length
                    } breeds`
                );
            }
        })
        .catch((error) => {
            console.log(error);
        });
};

countBreeds();

Добавление параметров в GET-запросы¶

GET-ответ может содержать параметры в URL, например, так: https://site.com/?foo=bar.

С помощью Axios вы можете сделать это, просто используя этот URL:

axios.get('https://site.com/?foo=bar');

или вы можете использовать свойство params в опциях:

axios.get('https://site.com/', {
    params: {
        foo: 'bar',
    },
});

POST-запросы¶

Выполнение POST-запроса аналогично GET-запросу, но вместо axios.get вы используете axios.post:

axios.post('https://site.com/')

Объект, содержащий параметры POST, является вторым аргументом:

axios.post('https://site.com/', {
    foo: 'bar',
});

Использование WebSockets в Node.js¶

WebSockets — это альтернатива HTTP-коммуникации в веб-приложениях.

Они предлагают долговечный двунаправленный канал связи между клиентом и сервером.

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

Поддержка WebSockets браузерами¶

WebSockets поддерживаются всеми современными браузерами.

Чем WebSockets отличаются от HTTP¶

HTTP — это совсем другой протокол, и у него другой способ общения.

HTTP — это протокол запроса/ответа: сервер возвращает некоторые данные, когда клиент их запрашивает.

С помощью WebSockets:

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

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

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

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

Безопасные WebSockets¶

Всегда используйте безопасный, зашифрованный протокол для WebSockets, wss://.

Протокол ws:// относится к небезопасной версии WebSockets (http:// WebSockets), и его следует избегать по очевидным причинам.

Создайте новое соединение WebSockets¶

const url = 'wss://myserver.com/something';
const connection = new WebSocket(url);

connection — это объект WebSocket.

Когда соединение успешно установлено, происходит событие open.

Прослушайте его, назначив функцию обратного вызова свойству onopen объекта connection:

connection.onopen = () => {
    // ...
};

При возникновении ошибки происходит обратный вызов функции onerror:

connection.onerror = (error) => {
    console.log(`WebSocket error: ${error}`);
};

Отправка данных на сервер с помощью WebSockets¶

Как только соединение открыто, вы можете отправлять данные на сервер.

Это удобно делать внутри функции обратного вызова onopen:

connection.onopen = () => {
    connection.send('hey');
};

Получение данных с сервера с помощью WebSockets¶

Слушайте с функцией обратного вызова onmessage, которая вызывается при получении события message:

connection.onmessage = (e) => {
    console.log(e.data);
};

Реализация сервера WebSockets в Node.js¶

ws — это популярная библиотека WebSockets для Node.js.

Мы будем использовать ее для создания сервера WebSockets. Она также может быть использована для реализации клиента и использования WebSockets для связи между двумя внутренними сервисами.

Легко установить ее с помощью:

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

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
    ws.on('message', (message) => {
        console.log(`Received message => ${message}`);
    });
    ws.send('ho!');
});

Этот код создает новый сервер на порту 8080 (порт по умолчанию для WebSockets) и добавляет функцию обратного вызова при установлении соединения, отправляя ho! клиенту и регистрируя полученные сообщения.

Посмотрите живой пример на Glitch¶

Здесь — живой пример сервера WebSockets.

Здесь — это клиент WebSockets, который взаимодействует с сервером.

Работа с дескрипторами файлов в Node.js¶

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

Дескриптор файла — это то, что возвращается при открытии файла с помощью метода open(), предлагаемого модулем fs:

const fs = require('fs');

fs.open('/Users/flavio/test.txt', 'r', (err, fd) => {
    //fd is our file descriptor
});

Обратите внимание на r, который мы использовали в качестве второго параметра для вызова fs.open().

Этот флаг означает, что мы открываем файл для чтения.

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

  • r+ открыть файл для чтения и записи
  • w+ открыть файл для чтения и записи, позиционируя поток в начало файла. Файл создается, если он не существует
  • a открыть файл для записи, расположив поток в конце файла. Файл создается, если не существует
  • a+ открыть файл для чтения и записи, расположив поток в конце файла. Файл создается, если не существует

Вы также можете открыть файл с помощью метода fs.openSync, который вместо того, чтобы предоставить объект дескриптора файла в обратном вызове, возвращает его:

const fs = require('fs');

try {
    const fd = fs.openSync('/Users/flavio/test.txt', 'r');
} catch (err) {
    console.error(err);
}

Получив дескриптор файла любым выбранным вами способом, вы можете выполнять все операции, требующие его, например, вызывать fs.open() и многие другие операции, взаимодействующие с файловой системой.

Node.js file stats¶

Каждый файл поставляется с набором деталей, которые мы можем проверить с помощью Node.js.

В частности, используя метод stat(), предоставляемый модулем fs.

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

const fs = require('fs');
fs.stat('/Users/flavio/test.txt', (err, stats) => {
    if (err) {
        console.error(err);
        return;
    }
    // we have access to the file stats in `stats`
});

Node.js также предоставляет метод sync, который блокирует поток до тех пор, пока статистика файлов не будет готова:

const fs = require('fs');
try {
    const stats = fs.stat('/Users/flavio/test.txt');
} catch (err) {
    console.error(err);
}

Информация о файле включена в переменную stats. Какую информацию мы можем извлечь с помощью stats?

Много, включая:

  • является ли файл каталогом или файлом, используя stats.isFile() и stats.isDirectory()
  • является ли файл символической ссылкой, используя stats.isSymbolicLink()
  • размер файла в байтах с помощью stats.size.

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

const fs = require('fs');
fs.stat('/Users/flavio/test.txt', (err, stats) => {
    if (err) {
        console.error(err);
        return;
    }

    stats.isFile(); // true
    stats.isDirectory(); // false
    stats.isSymbolicLink(); // false
    stats.size; // 1024000 //= 1MB
});

Node.js Пути к файлам¶

Каждый файл в системе имеет путь.

В Linux и macOS путь может выглядеть следующим образом:

/users/flavio/file.txt.

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

C:usersflaviofile.txt.

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

Вы включаете этот модуль в свои файлы, используя:

const path = require('path');

и вы сможете начать использовать его методы.

Получение информации из пути¶

Получив путь, вы можете извлечь из него информацию, используя эти методы:

  • dirname: получить родительскую папку файла
  • basename: получить часть имени файла
  • extname: получить расширение файла

Пример:

const notes = '/users/flavio/notes.txt';

path.dirname(notes); // /users/flavio
path.basename(notes); // notes.txt
path.extname(notes); // .txt

Вы можете получить имя файла без расширения, указав второй аргумент basename:

path.basename(notes, path.extname(notes)); // notes

Работа с путями¶

Вы можете объединить две или более частей пути с помощью функции path.join():

const name = 'flavio';
path.join('/', 'users', name, 'notes.txt'); // '/users/flavio/notes.txt'

Вы можете получить расчет абсолютного пути относительного пути с помощью path.resolve():

path.resolve('flavio.txt'); // '/Users/flavio/flavio.txt' if run from my home folder

В этом случае Node.js просто добавит /flavio.txt в текущий рабочий каталог. Если вы укажете вторую папку с параметрами, resolve будет использовать первую в качестве основы для второй:

path.resolve('tmp', 'flavio.txt'); // '/Users/flavio/tmp/flavio.txt' if run from my home folder

Если первый параметр начинается со слэша, это означает, что это абсолютный путь:

path.resolve('/etc', 'flavio.txt'); // '/etc/flavio.txt'

path.normalize() — еще одна полезная функция, которая попытается вычислить фактический путь, если он содержит относительные спецификаторы типа . или .., или двойные косые черты:

path.normalize('/users/flavio/..//test.txt'); //  /users/test.txt

Но resolve и normalize не проверяют, существует ли путь. Они просто вычисляют путь на основе полученной информации.

Чтение файлов с помощью Node.js¶

Самый простой способ прочитать файл в Node.js — это использовать метод fs.readFile(), передав ему путь к файлу и функцию обратного вызова, которая будет вызвана с данными файла (и ошибкой):

const fs = require('fs');

fs.readFile('/Users/flavio/test.txt', (err, data) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(data);
});

В качестве альтернативы можно использовать синхронную версию fs.readFileSync():

const fs = require('fs');

try {
    const data = fs.readFileSync(
        '/Users/flavio/test.txt',
        'utf8'
    );
    console.log(data);
} catch (err) {
    console.error(err);
}

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

И fs.readFile(), и fs.readFileSync() считывают полное содержимое файла в память перед возвратом данных.

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

В этом случае лучшим вариантом будет чтение содержимого файла с помощью потоков.

Запись файлов с помощью Node.js¶

Самый простой способ записи в файлы в Node.js — использовать API fs.writeFile().

Пример:

const fs = require('fs');

const content = 'Some content!';

fs.writeFile('/Users/flavio/test.txt', content, (err) => {
    if (err) {
        console.error(err);
        return;
    }
    // file written successfully
});

В качестве альтернативы можно использовать синхронную версию fs.writeFileSync():

const fs = require('fs');

const content = 'Some content!';

try {
    const data = fs.writeFileSync(
        '/Users/flavio/test.txt',
        content
    );
    // file written successfully
} catch (err) {
    console.error(err);
}

По умолчанию этот API заменяет содержимое файла, если он уже существует.

Вы можете изменить значение по умолчанию, указав флаг:

fs.writeFile(
    '/Users/flavio/test.txt',
    content,
    { flag: 'a+' },
    (err) => {}
);

Флаги, которые вы, скорее всего, будете использовать, следующие:

  • r+ открыть файл для чтения и записи
  • w+ открыть файл для чтения и записи, позиционируя поток в начало файла. Файл создается, если он не существует
  • a открыть файл для записи, расположив поток в конце файла. Файл создается, если не существует
  • a+ открыть файл для чтения и записи, расположив поток в конце файла. Файл создается, если не существует

Вы можете узнать больше о flags.

Добавить в файл¶

Удобным методом добавления содержимого в конец файла является fs.appendFile() (и его аналог fs.appendFileSync()):

const content = 'Some content!';

fs.appendFile('file.log', content, (err) => {
    if (err) {
        console.error(err);
        return;
    }
    // done!
});

Использование потоков¶

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

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

Работа с папками в Node.js¶

Основной модуль Node.js fs предоставляет множество удобных методов, которые вы можете использовать для работы с папками.

Проверка существования папки¶

Используйте fs.access(), чтобы проверить, существует ли папка, и может ли Node.js получить к ней доступ со своими разрешениями.

Создание новой папки¶

Используйте fs.mkdir() или fs.mkdirSync() для создания новой папки:

const fs = require('fs');

const folderName = '/Users/flavio/test';

try {
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir);
    }
} catch (err) {
    console.error(err);
}

Чтение содержимого каталога¶

Используйте fs.readdir() или fs.readdirSync для чтения содержимого каталога.

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

const fs = require('fs');
const path = require('path');

const folderPath = '/Users/flavio';

fs.readdirSync(folderPath);

Вы можете получить полный путь:

fs.readdirSync(folderPath).map(fileName => {
  return path.join(folderPath, fileName)
}

Можно также отфильтровать результаты, чтобы вернуть только файлы и исключить папки:

const isFile = (fileName) => {
    return fs.lstatSync(fileName).isFile();
};

fs.readdirSync(folderPath)
    .map((fileName) => {
        return path.join(folderPath, fileName);
    })
    .filter(isFile);

Переименование папки¶

Используйте fs.rename() или fs.renameSync() для переименования папки.

Первый параметр — текущий путь, второй — новый путь:

const fs = require('fs');

fs.rename('/Users/flavio', '/Users/roger', (err) => {
    if (err) {
        console.error(err);
        return;
    }
    // done
});

fs.renameSync() — это синхронная версия:

const fs = require('fs');

try {
    fs.renameSync('/Users/flavio', '/Users/roger');
} catch (err) {
    console.error(err);
}

Удаление папки¶

Используйте fs.rmdir() или fs.rmdirSync() для удаления папки.

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

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

В этом случае метод remove() — это то, что вам нужно.

Установите его с помощью:

npm install fs-extra

и используйте его следующим образом:

const fs = require('fs-extra');

const folder = '/Users/flavio';

fs.remove(folder, (err) => {
    console.error(err);
});

Его также можно использовать с обещаниями:

fs.remove(folder)
    .then(() => {
        // done
    })
    .catch((err) => {
        console.error(err);
    });

или с async/await:

async function removeFolder(folder) {
    try {
        await fs.remove(folder);
        // done
    } catch (err) {
        console.error(err);
    }
}

const folder = '/Users/flavio';
removeFolder(folder);

Модуль Node.js fs¶

Модуль fs предоставляет множество очень полезных функций для доступа и взаимодействия с файловой системой.

Нет необходимости устанавливать его. Будучи частью ядра Node.js, он может быть использован простым запросом:

const fs = require('fs');

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

  • fs.access(): проверить, существует ли файл и может ли Node получить к нему доступ с его разрешениями
  • fs.appendFile(): добавление данных в файл. Если файл не существует, он создается
  • fs.chmod(): изменить разрешения файла, указанного переданным именем. Сопутствующие: fs.lchmod(), fs.fchmod().
  • fs.chown(): изменение владельца и группы файла, указанного переданным именем. Связанные: fs.fchown(), fs.lchown().
  • fs.close(): закрыть дескриптор файла
  • fs.copyFile(): копирование файла
  • fs.createReadStream(): создание потока файлов для чтения
  • fs.createWriteStream(): создание потока файлов на запись
  • fs.link(): создание новой жесткой ссылки на файл
  • fs.mkdir(): создание новой папки
  • fs.mkdtemp(): создание временного каталога
  • fs.open(): установить режим работы с файлом
  • fs.readdir(): чтение содержимого каталога
  • fs.readFile(): чтение содержимого файла. Связанные: fs.read()
  • fs.readlink(): чтение значения символической ссылки
  • fs.realpath(): преобразование относительных указателей пути к файлу (., ..) в полный путь
  • fs.rename(): переименовать файл или папку
  • fs.rmdir(): удалить папку
  • fs.stat(): возвращает статус файла, идентифицированного переданным именем. Связанные: fs.fstat(), fs.lstat().
  • fs.symlink(): создание новой символической ссылки на файл
  • fs.truncate(): усечь до заданной длины файл, идентифицированный переданным именем. Связанные: fs.ftruncate().
  • fs.unlink(): удаление файла или символической ссылки
  • fs.unwatchFile(): прекратить наблюдение за изменениями в файле
  • fs.utimes(): изменение временной метки файла, идентифицированного переданным именем. Связанные: fs.futimes().
  • fs.watchFile(): начать наблюдение за изменениями в файле. Связанные: fs.watch().
  • fs.writeFile(): запись данных в файл. Связанные: fs.write().

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

Например:

  • fs.rename()
  • fs.renameSync()
  • fs.write()
  • fs.writeSync()

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

Примечание: Node 10 включает экспериментальную поддержку для API на основе обещаний.

Для примера рассмотрим метод fs.rename(). Асинхронный API используется с обратным вызовом:

const fs = require('fs');

fs.rename('before.json', 'after.json', (err) => {
    if (err) {
        return console.error(err);
    }
    // done
});

Синхронный API можно использовать подобным образом, с блоком try/catch для обработки ошибок:

const fs = require('fs');

try {
    fs.renameSync('before.json', 'after.json');
    // done
} catch (err) {
    console.error(err);
}

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

Модуль пути Node.js¶

Модуль path предоставляет множество очень полезных функций для доступа и взаимодействия с файловой системой.

Нет необходимости устанавливать его. Будучи частью ядра Node.js, он может быть использован простым запросом:

const path = require('path');

Этот модуль предоставляет path.sep, который предоставляет разделитель сегментов пути ( в Windows, и / в Linux / macOS), и path.delimiter, который предоставляет разделитель путей (; в Windows, и : в Linux / macOS).

Это методы path.

path.basename()

Возвращает последнюю часть пути. Второй параметр может отфильтровать расширение файла:

require('path').basename('/test/something'); // something
require('path').basename('/test/something.txt'); // something.txt
require('path').basename('/test/something.txt', '.txt'); // something

path.dirname()

Возвращает часть пути, относящуюся к каталогу:

require('path').dirname('/test/something'); // /test
require('path').dirname('/test/something/file.txt'); // /test/something

path.extname()

Возвращает расширенную часть пути:

require('path').dirname('/test/something'); // ''
require('path').dirname('/test/something/file.txt'); // '.txt'

path.isAbsolute()

Возвращает true, если это абсолютный путь:

require('path').isAbsolute('/test/something'); // true
require('path').isAbsolute('./test/something'); // false

path.join()

Объединяет две или более частей пути:

const name = 'flavio';
require('path').join('/', 'users', name, 'notes.txt'); // '/users/flavio/notes.txt'

path.normalize()

Пытается вычислить фактический путь, если он содержит относительные спецификаторы, такие как . или .., или двойные косые черты:

require('path').normalize('/users/flavio/..//test.txt'); // /users/test.txt

path.parse()

Разбирает путь к объекту с сегментами, которые его составляют:

  • root: корень
  • dir: путь к папке, начиная с корня
  • base: имя файла + расширение
  • name: имя файла
  • ext: расширение файла

Пример:

require('path').parse('/users/test.txt');

результаты:

{
  root: '/',
  dir: '/users',
  base: 'test.txt',
  ext: '.txt',
  name: 'test'
}

path.relative()

Принимает 2 пути в качестве аргументов. Возвращает относительный путь от первого пути ко второму, основанный на текущем рабочем каталоге.

Пример:

require('path').relative(
    '/Users/flavio',
    '/Users/flavio/test.txt'
); // 'test.txt'
require('path').relative(
    '/Users/flavio',
    '/Users/flavio/something/test.txt'
); // 'something/test.txt'

path.resolve()

Вы можете получить расчет абсолютного пути относительного пути с помощью path.resolve():

path.resolve('flavio.txt'); // '/Users/flavio/flavio.txt' if run from my home folder

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

path.resolve('tmp', 'flavio.txt'); // '/Users/flavio/tmp/flavio.txt' if run from my home folder

Если первый параметр начинается со слэша, это означает, что это абсолютный путь:

path.resolve('/etc', 'flavio.txt'); // '/etc/flavio.txt'

Модуль Node.js os¶

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

const os = require('os');

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

os.EOL указывает последовательность разделителей строк. Этоnв Linux и macOS, иrn` в Windows.

Когда я говорю Linux и macOS, я имею в виду POSIX платформы. Для простоты я исключаю другие менее популярные операционные системы, на которых может работать Node.

os.constants.signals сообщает нам все константы, связанные с обработкой сигналов процесса, таких как SIGHUP, SIGKILL и так далее.

os.constants.errno устанавливает константы для сообщений об ошибках, таких как EADDRINUSE, EOVERFLOW и так далее.

Вы можете прочитать их все здесь.

Давайте теперь посмотрим на основные методы, которые предоставляет os:

  • os.arch()
  • os.cpus()
  • os.endianness()
  • os.freemem()
  • os.homedir()
  • os.hostname()
  • os.loadavg()
  • os.networkInterfaces()
  • os.platform()
  • os.release()
  • os.tmpdir()
  • os.totalmem()
  • os.type()
  • os.uptime()
  • os.userInfo()

os.arch()

Возвращает строку, идентифицирующую базовую архитектуру, например arm, x64, arm64.

os.cpus()

Возвращает информацию о процессорах, доступных в вашей системе.

Пример:

[
    {
        model: 'Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz',
        speed: 2400,
        times: {
            user: 281685380,
            nice: 0,
            sys: 187986530,
            idle: 685833750,
            irq: 0,
        },
    },
    {
        model: 'Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz',
        speed: 2400,
        times: {
            user: 282348700,
            nice: 0,
            sys: 161800480,
            idle: 703509470,
            irq: 0,
        },
    },
];

os.endianness()

Возвращает BE или LE в зависимости от того, был ли Node.js скомпилирован с Big Endian или Little Endian.

os.freemem()

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

os.homedir()

Возвращает путь к домашнему каталогу текущего пользователя.

Пример:

os.hostname()

Возвращает имя хоста.

os.loadavg()

Возвращает расчет, произведенный операционной системой по среднему значению нагрузки.

Возвращает значимое значение только в Linux и macOS.

Пример:

[3.68798828125, 4.00244140625, 11.1181640625];

os.networkInterfaces()

Возвращает сведения о сетевых интерфейсах, доступных в вашей системе.

Пример:

{
  lo0: [
    {
      address: '127.0.0.1',
      netmask: '255.0.0.0',
      family: 'IPv4',
      mac: 'fe:82:00:00:00:00',
      internal: true,
    },
    {
      address: '::1',
      netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
      family: 'IPv6',
      mac: 'fe:82:00:00:00:00',
      scopeid: 0,
      internal: true,
    },
    {
      address: 'fe80::1',
      netmask: 'ffff:ffff:ffff:ffff::',
      family: 'IPv6',
      mac: 'fe:82:00:00:00:00',
      scopeid: 1,
      internal: true,
    },
  ],
  en1: [
    {
      address: 'fe82::9b:8282:d7e6:496e',
      netmask: 'ffff:ffff:ffff:ffff::',
      family: 'IPv6',
      mac: '06:00:00:02:0e:00',
      scopeid: 5,
      internal: false,
    },
    {
      address: '192.168.1.38',
      netmask: '255.255.255.0',
      family: 'IPv4',
      mac: '06:00:00:02:0e:00',
      internal: false,
    },
  ],
  utun0: [
    {
      address: 'fe80::2513:72bc:f405:61d0',
      netmask: 'ffff:ffff:ffff:ffff::',
      family: 'IPv6',
      mac: 'fe:80:00:20:00:00',
      scopeid: 8,
      internal: false,
    },
  ]
}

os.platform()

Возвращает платформу, для которой был скомпилирован Node.js:

  • darwin
  • freebsd
  • linux
  • openbsd
  • win32
  • …more

os.release()

Возвращает строку, определяющую номер выпуска операционной системы.

os.tmpdir()

Возвращает путь к назначенной временной папке.

os.totalmem()

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

os.type()

Идентифицирует операционную систему:

  • Linux
  • Darwin на macOS
  • Windows_NT на Windows

os.uptime()

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

Модуль событий Node.js¶

Модуль events предоставляет нам класс EventEmitter, который является ключевым для работы с событиями в Node.js.

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

const EventEmitter = require('events');
const door = new EventEmitter();

Слушатель событий ест свой собственный собачий корм и использует эти события:

  • newListener, когда добавляется слушатель
  • removeListener, когда слушатель удаляется.

Вот подробное описание наиболее полезных методов:

  • emitter.addListener()
  • emitter.emit()
  • emitter.eventNames()
  • emitter.getMaxListeners()
  • emitter.listenerCount()
  • emitter.listeners()
  • emitter.off()
  • emitter.on()
  • emitter.once()
  • emitter.prependListener()
  • emitter.prependOnceListener()
  • emitter.removeAllListeners()
  • emitter.removeListener()
  • emitter.setMaxListeners()

emitter.addListener()

Псевдоним для emitter.on().

emitter.emit()

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

emitter.eventNames()

Возвращает массив строк, которые представляют события, зарегистрированные на текущем EventListener:

emitter.getMaxListeners()

Получение максимального количества слушателей, которое можно добавить к объекту EventListener. По умолчанию это значение равно 10, но может быть увеличено или уменьшено с помощью setMaxListeners():

emitter.listenerCount()

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

door.listenerCount('open');

emitter.listeners()

Получает массив слушателей события, переданного в качестве параметра:

emitter.off()

Псевдоним для emitter.removeListener(), добавленный в Node 10.

emitter.on()

Добавляет функцию обратного вызова, которая вызывается, когда испускается событие.

Использование:

door.on('open', () => {
    console.log('Door was opened');
});

emitter.once()

Добавляет функцию обратного вызова, которая вызывается, когда событие испускается в первый раз после регистрации этой функции. Этот обратный вызов будет вызван только один раз, больше никогда.

const EventEmitter = require('events');
const ee = new EventEmitter();

ee.once('my-event', () => {
    // call callback function once
});

emitter.prependListener()

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

emitter.prependOnceListener()

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

emitter.removeAllListeners()

Удаляет всех слушателей объекта-эмиттера события, слушающих определенное событие:

door.removeAllListeners('open');

emitter.removeListener()

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

const doSomething = () => {};
door.on('open', doSomething);
door.removeListener('open', doSomething);

emitter.setMaxListeners()

Устанавливает максимальное количество слушателей, которое можно добавить к объекту EventListener. По умолчанию это значение равно 10, но может быть увеличено или уменьшено:

door.setMaxListeners(50);

The Node.js http module¶

Модуль http в Node.js предоставляет полезные функции и классы для построения HTTP-сервера. Это ключевой модуль для сетевой работы Node.js.

Его можно включить, используя:

const http = require('http');

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

Properties¶

http.METHODS

В этом свойстве перечислены все поддерживаемые методы HTTP:

> require('http').METHODS

/* [
  'ACL',
  'BIND',
  'CHECKOUT',
  'CONNECT',
  'COPY',
  'DELETE',
  'GET',
  'HEAD',
  'LINK',
  'LOCK',
  'M-SEARCH',
  'MERGE',
  'MKACTIVITY',
  'MKCALENDAR',
  'MKCOL',
  'MOVE',
  'NOTIFY',
  'OPTIONS',
  'PATCH',
  'POST',
  'PROPFIND',
  'PROPPATCH',
  'PURGE',
  'PUT',
  'REBIND',
  'REPORT',
  'SEARCH',
  'SUBSCRIBE',
  'TRACE',
  'UNBIND',
  'UNLINK',
  'UNLOCK',
  'UNSUBSCRIBE'
] */

http.STATUS_CODES

В этом свойстве перечислены все коды состояния HTTP и их описание:

> require('http').STATUS_CODES

/* {
  100: 'Continue',
  101: 'Switching Protocols',
  102: 'Processing',
  200: 'OK',
  201: 'Created',
  202: 'Accepted',
  203: 'Non-Authoritative Information',
  204: 'No Content',
  205: 'Reset Content',
  206: 'Partial Content',
  207: 'Multi-Status',
  208: 'Already Reported',
  226: 'IM Used',
  300: 'Multiple Choices',
  301: 'Moved Permanently',
  302: 'Found',
  303: 'See Other',
  304: 'Not Modified',
  305: 'Use Proxy',
  307: 'Temporary Redirect',
  308: 'Permanent Redirect',
  400: 'Bad Request',
  401: 'Unauthorized',
  402: 'Payment Required',
  403: 'Forbidden',
  404: 'Not Found',
  405: 'Method Not Allowed',
  406: 'Not Acceptable',
  407: 'Proxy Authentication Required',
  408: 'Request Timeout',
  409: 'Conflict',
  410: 'Gone',
  411: 'Length Required',
  412: 'Precondition Failed',
  413: 'Payload Too Large',
  414: 'URI Too Long',
  415: 'Unsupported Media Type',
  416: 'Range Not Satisfiable',
  417: 'Expectation Failed',
  418: "I'm a teapot",
  421: 'Misdirected Request',
  422: 'Unprocessable Entity',
  423: 'Locked',
  424: 'Failed Dependency',
  425: 'Unordered Collection',
  426: 'Upgrade Required',
  428: 'Precondition Required',
  429: 'Too Many Requests',
  431: 'Request Header Fields Too Large',
  451: 'Unavailable For Legal Reasons',
  500: 'Internal Server Error',
  501: 'Not Implemented',
  502: 'Bad Gateway',
  503: 'Service Unavailable',
  504: 'Gateway Timeout',
  505: 'HTTP Version Not Supported',
  506: 'Variant Also Negotiates',
  507: 'Insufficient Storage',
  508: 'Loop Detected',
  509: 'Bandwidth Limit Exceeded',
  510: 'Not Extended',
  511: 'Network Authentication Required',
} */

http.globalAgent

Указывает на глобальный экземпляр объекта Agent, который является экземпляром класса http.Agent.

Он используется для управления сохранением и повторным использованием соединений для HTTP-клиентов и является ключевым компонентом HTTP-сети Node.js.

Подробнее об описании класса http.Agent позже.

Методы¶

http.createServer()

Возвращает новый экземпляр класса http.Server.

Использование:

const server = http.createServer((req, res) => {
  // handle every single request with this callback
})

http.request()

Выполняет HTTP-запрос к серверу, создавая экземпляр класса http.ClientRequest.

http.get()

Аналогичен http.request(), но автоматически устанавливает метод HTTP на GET и автоматически вызывает req.end().

Классы¶

Модуль HTTP предоставляет 5 классов:

  • http.Agent
  • http.ClientRequest
  • http.Server
  • http.ServerResponse
  • http.IncomingMessage

http.Agent

Node создает глобальный экземпляр класса http.Agent для управления сохранением и повторным использованием соединений для HTTP-клиентов, ключевого компонента HTTP-сети Node.

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

Он также поддерживает пул сокетов. Это важно для повышения производительности.

http.ClientRequest

Объект http.ClientRequest создается при вызове http.request() или http.get().

Когда ответ получен, вызывается событие response с ответом, с экземпляром http.IncomingMessage в качестве аргумента.

Возвращенные данные ответа могут быть прочитаны двумя способами:

  • вы можете вызвать метод response.read().
  • в обработчике события response вы можете установить слушателя события data, чтобы вы могли прослушивать данные, передаваемые в поток.

http.Server

Этот класс обычно инстанцируется и возвращается при создании нового сервера с помощью http.createServer().

Как только у вас есть объект сервера, вы получаете доступ к его методам:

  • close() останавливает сервер от приема новых соединений
  • listen() запускает HTTP-сервер и прослушивает соединения

http.ServerResponse

Создается http.Server и передается в качестве второго параметра в событие request, которое он запускает.

Обычно известен и используется в коде как res:

const server = http.createServer((req, res) => {
    // res is an http.ServerResponse object
});

Метод, который вы всегда будете вызывать в обработчике, это end(), который закрывает ответ, сообщение завершено, и сервер может отправить его клиенту. Он должен вызываться при каждом ответе.

Эти методы используются для взаимодействия с HTTP-заголовками:

  • getHeaderNames() получить список имен уже установленных HTTP-заголовков
  • getHeaders() получить копию уже установленных HTTP-заголовков
  • setHeader('headername', value) устанавливает значение HTTP-заголовка
  • getHeader('headername') получает уже установленный HTTP-заголовок
  • removeHeader('headername') удаляет уже установленный HTTP-заголовок
  • hasHeader('headername') возвращает true, если в ответе установлен этот заголовок
  • headersSent() возвращает true, если заголовки уже были отправлены клиенту.

После обработки заголовков вы можете отправить их клиенту, вызвав функцию response.writeHead(), которая принимает в качестве первого параметра statusCode, необязательное сообщение о статусе и объект заголовков.

Чтобы отправить данные клиенту в теле ответа, вы используете write(). Она отправит буферизованные данные в поток HTTP-ответа.

Если заголовки еще не были отправлены с помощью response.writeHead(), то сначала будут отправлены заголовки с кодом статуса и сообщением, заданными в запросе, которые вы можете изменить, установив значения свойств statusCode и statusMessage:

response.statusCode = 500;
response.statusMessage = 'Internal Server Error';

http.IncomingMessage

Объект http.IncomingMessage создается:

  • http.Server при прослушивании события запрос
  • http.ClientRequest при прослушивании события response.

Он может быть использован для доступа к ответу:

  • статус с помощью методов statusCode и statusMessage
  • заголовки с помощью метода headers или rawHeaders
  • метод HTTP с помощью метода method
  • версия HTTP с помощью метода httpVersion.
  • URL с помощью метода url
  • базовый сокет с помощью метода сокет.

Доступ к данным осуществляется с помощью потоков, поскольку http.IncomingMessage реализует интерфейс Readable Stream.

Node.js Потоки¶

Потоки — это одна из фундаментальных концепций, на которых основаны приложения Node.js.

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

Потоки не являются концепцией, уникальной для Node.js. Они появились в операционной системе Unix несколько десятилетий назад, и программы могут взаимодействовать друг с другом, передавая потоки через оператор pipe (|).

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

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

Node.js stream модуль обеспечивает основу, на которой строятся все потоковые API.

Почему именно потоки?¶

Потоки в основном обеспечивают два основных преимущества перед другими методами обработки данных:

  • Эффективность памяти: вам не нужно загружать большие объемы данных в память, прежде чем вы сможете их обработать
  • Эффективность по времени: требуется гораздо меньше времени, чтобы начать обработку данных сразу после их получения, а не ждать, пока весь объем данных будет доступен для начала работы.

Пример потока¶

Типичным примером является чтение файлов с диска.

Используя модуль Node.js fs, вы можете прочитать файл и передать его по HTTP при установлении нового соединения с вашим http сервером:

const http = require('http');
const fs = require('fs');

const server = http.createServer(function (req, res) {
    fs.readFile(__dirname + '/data.txt', (err, data) => {
        res.end(data);
    });
});

server.listen(3000);

readFile() считывает полное содержимое файла и вызывает функцию обратного вызова по завершении.

res.end(data) в обратном вызове вернет содержимое файла HTTP-клиенту.

Если файл большой, операция займет довольно много времени. Вот то же самое, написанное с использованием потоков:

const http = require('http');
const fs = require('fs');

const server = http.createServer((req, res) => {
    const stream = fs.createReadStream(
        __dirname + '/data.txt'
    );
    stream.pipe(res);
});

server.listen(3000);

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

pipe()¶

В приведенном выше примере используется строка stream.pipe(res): метод pipe() вызывается на файловом потоке.

Что делает этот код? Он берет источник и передает его в место назначения.

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

Возвращаемым значением метода pipe() является поток назначения, что очень удобно и позволяет нам соединять несколько вызовов pipe() в цепочку, например, так:

src.pipe(dest1).pipe(dest2);

Эта конструкция — то же самое, что и «делать»:

src.pipe(dest1);
dest1.pipe(dest2);

API Node.js с поддержкой потоков¶

Благодаря своим преимуществам, многие модули ядра Node.js предоставляют встроенные возможности работы с потоками, в частности:

  • process.stdin возвращает поток, подключенный к stdin
  • process.stdout возвращает поток, подключенный к stdout
  • process.stderr возвращает поток, подключенный к stderr
  • fs.createReadStream() создает поток для чтения файла
  • fs.createWriteStream() создает поток записи в файл
  • net.connect() инициирует соединение на основе потока
  • http.request() возвращает экземпляр класса http.ClientRequest, который является записываемым потоком
  • zlib.createGzip() сжимает данные с помощью gzip (алгоритм сжатия) в поток
  • zlib.createGunzip() распаковывает поток gzip.
  • zlib.createDeflate() сжимает данные с помощью deflate (алгоритм сжатия) в поток
  • zlib.createInflate() распаковывает поток deflate.

Различные типы потоков¶

Существует четыре класса потоков:

  • Readable: поток, из которого можно передавать данные, но нельзя передавать в него (вы можете получать данные, но не отправлять их в него). Когда вы передаете данные в читаемый поток, они буферизируются, пока потребитель не начнет читать данные.
  • Записываемый: поток, в который можно передавать данные, но не передавать из него (можно отправлять данные, но не получать из него).
  • Duplex: поток, в который можно передавать и из которого можно передавать данные, по сути, это комбинация потоков Readable и Writable.
  • Transform: поток Transform похож на Duplex, но выход является преобразованием его входа.

Как создать читаемый поток¶

Мы получаем поток Readable из модуля stream и инициализируем его:

const Stream = require('stream');
const readableStream = new Stream.Readable();

Теперь, когда поток инициализирован, мы можем отправлять в него данные:

readableStream.push('hi!');
readableStream.push('ho!');

Как создать записываемый поток¶

Для создания записываемого потока мы расширяем базовый объект Writable и реализуем его метод _write().

Сначала создадим объект stream:

const Stream = require('stream');
const writableStream = new Stream.Writable();

тогда реализуйте _write:

writableStream._write = (chunk, encoding, next) => {
    console.log(chunk.toString());
    next();
};

Теперь вы можете передавать читаемый поток:

process.stdin.pipe(writableStream);

Как получить данные из читаемого потока¶

Как читать данные из читаемого потока? Используя записываемый поток:

const Stream = require('stream');

const readableStream = new Stream.Readable();
const writableStream = new Stream.Writable();

writableStream._write = (chunk, encoding, next) => {
    console.log(chunk.toString());
    next();
};

readableStream.pipe(writableStream);

readableStream.push('hi!');
readableStream.push('ho!');

Вы также можете потреблять читаемый поток напрямую, используя событие readable:

readableStream.on('readable', () => {
    console.log(readableStream.read());
});

Как отправить данные в поток с возможностью записи.¶

Используя метод потока write():

writableStream.write('hey!n');

Сигнализация записываемого потока о том, что вы закончили запись.¶

Используйте метод end():

const Stream = require('stream');

const readableStream = new Stream.Readable();
const writableStream = new Stream.Writable();

writableStream._write = (chunk, encoding, next) => {
    console.log(chunk.toString());
    next();
};

readableStream.pipe(writableStream);

readableStream.push('hi!');
readableStream.push('ho!');

writableStream.end();

Основы работы с MySQL и Node.js¶

MySQL — одна из самых популярных реляционных баз данных в мире.

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

Мы будем использовать mysqljs/mysql, пакет, который имеет более 12 000 звезд на GitHub и существует уже много лет.

Установка пакета Node.js MySql {#installing-the-node-js-mysql-package}.¶

Вы устанавливаете его с помощью:

Инициализация подключения к базе данных¶

Сначала вы включаете пакет:

const mysql = require('mysql');

и вы создаете связь:

const options = {
    user: 'the_mysql_user_name',
    password: 'the_mysql_user_password',
    database: 'the_mysql_database_name',
};

const connection = mysql.createConnection(options);

Вы инициируете новое соединение, позвонив по телефону:

connection.connect((err) => {
    if (err) {
        console.error(
            'An error occurred while connecting to the DB'
        );
        throw err;
    }
});

Параметры подключения¶

В приведенном выше примере объект options содержал 3 опции:

const options = {
    user: 'the_mysql_user_name',
    password: 'the_mysql_user_password',
    database: 'the_mysql_database_name',
};

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

  • host, имя хоста базы данных, по умолчанию localhost.
  • port, номер порта сервера MySQL, по умолчанию 3306
  • socketPath, используется для указания сокета unix вместо хоста и порта
  • debug, по умолчанию отключен, может быть использован для отладки
  • trace, по умолчанию включено, печатает трассировку стека при возникновении ошибок
  • ssl, используется для установки SSL-соединения с сервером (выходит за рамки данного руководства).

Выполнение запроса SELECT¶

Теперь вы готовы выполнить SQL-запрос к базе данных. После выполнения запроса будет вызвана функция обратного вызова, которая содержит возможную ошибку, результаты и поля:

connection.query(
    'SELECT * FROM todos',
    (error, todos, fields) => {
        if (error) {
            console.error(
                'An error occurred while executing the query'
            );
            throw error;
        }
        console.log(todos);
    }
);

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

const id = 223;
connection.query(
    'SELECT * FROM todos WHERE id = ?',
    [id],
    (error, todos, fields) => {
        if (error) {
            console.error(
                'An error occurred while executing the query'
            );
            throw error;
        }
        console.log(todos);
    }
);

Чтобы передать несколько значений, просто поместите больше элементов в массив, который вы передаете в качестве второго параметра:

const id = 223
const author = 'Flavio'
connection.query('SELECT * FROM todos WHERE id = ? AND author = ?', [id, author], (error,
  if (error) {
    console.error('An error occurred while executing the query')
    throw error
  }
  console.log(todos)
})

Выполните запрос INSERT.¶

Вы можете передать объект:

const todo = {
  thing: 'Buy the milk'
  author: 'Flavio'
}

connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => {
  if (error) {
    console.error('An error occurred while executing the query')
    throw error
  }
})

Если таблица имеет первичный ключ с auto_increment, его значение будет возвращено в значении results.insertId:

const todo = {
  thing: 'Buy the milk'
  author: 'Flavio'
}

connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => {
  if (error) {
    console.error('An error occurred while executing the query')
    throw error
  }}

  const id = results.resultId
  console.log(id)
)

Закрыть соединение¶

Когда вам нужно прервать соединение с базой данных, вы можете вызвать метод end():

Это гарантирует, что любой ожидающий запрос будет отправлен, и соединение будет изящно завершено.

Разница между разработкой и производством¶

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

Node.js предполагает, что он всегда работает в среде разработки. Вы можете сообщить Node.js, что вы работаете в production, установив переменную окружения NODE_ENV=production.

Обычно это делается путем выполнения команды:

export NODE_ENV=production

в оболочке, но лучше поместить ее в файл конфигурации оболочки (например, .bash_profile в оболочке Bash), поскольку в противном случае настройка не сохранится в случае перезагрузки системы.

Вы также можете применить переменную окружения, добавив ее в команду инициализации приложения:

NODE_ENV=production node app.js

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

Установка окружения в production обычно гарантирует, что:

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

Например, Pug, библиотека шаблонов, используемая Express, компилируется в режиме отладки, если NODE_ENV не установлен в production. Представления Express компилируются в каждом запросе в режиме разработки, в то время как в режиме производства они кэшируются. Существует множество других примеров.

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

app.configure('development', () => {
    // ...
});

app.configure('production', () => {
    // ...
});

app.configure('production', 'staging', () => {
    // ...
});

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

app.configure('development', () => {
    app.use(
        express.errorHandler({
            dumpExceptions: true,
            showStack: true,
        })
    );
});

app.configure('production', () => {
    app.use(express.errorHandler());
});

Заключительные слова¶

Я надеюсь, что это введение в Node.js поможет вам начать его использовать или понять некоторые его концепции. Надеюсь, теперь вы знаете достаточно, чтобы начать создавать замечательные вещи!

О проекте

Цель данного документа — помочь вам начать разработку приложений на Node.js и научить всему, что необходимо знать о «продвинутом» JavaScript.
Это больше, чем обычный «Hello world»-туториал.

Статус

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

Код примеров этой книги тестировался на Node.js версии 0.8.8 (сверено с англ. версией —прим.перев.).

Целевая аудитория

Вероятно, документ будет полезен читателям с базовыми знаниями, примерно, как у меня: опыт работы хотя бы с одним объектно-ориентированным языком, таким как Ruby, Python, PHP или Java, небольшой опыт в Javascript и полный новичок в Node.js.

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

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

Структура учебника

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

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

Мы начнём с выяснения того, чем JavaScript в Node.js отличается от JavaScript в браузере.

Далее, мы остановимся на написании традиционного «Hello world»-приложения, которое является наиболее простым примером «что-то делающего» кода Node.js.

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

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

Исходный код законченного приложения доступен в
the NodeBeginnerBook Github репозитории.

Содержание

  • О проекте
    • Статус
    • Целевая аудитория
    • Структура учебника
  • JavaScript и Node.js
    • JavaScript и Вы
    • Предупреждение
    • Server-side JavaScript
    • «Hello World»
  • Полномасштабное веб-приложение с Node.js
    • Что должно делать наше приложение
    • Задачи
  • Реализация приложения
    • Простой HTTP-сервер
    • Анализ нашего HTTP-сервера
    • Передача функций в качестве параметра
    • Как анонимная функция делает наш HTTP-сервер рабочим
    • Событийно-ориентированные обратные вызовы
    • Как наш сервер обрабатывает запросы
    • Выбор места для нашего серверного модуля
    • Что необходимо для «роутера»?
    • Исполнение королевских постановлений в царстве глаголов
    • Роутинг реальных обработчиков запроса
    • Создание ответа обработчиков запроса
      • Как делать не надо
      • Блокирование и неблокирование
      • Ответ обработчиков запроса с неблокирующими операциями.
    • Сделаем что-нибудь полезное
      • Обработка POST-запросов
      • Обработка загрузки файлов
    • Выводы и перспективы

JavaScript и Node.js

JavaScript и Вы

До того как мы поговорим о технических вещах, позвольте занять некоторое время и поговорить о вас и ваших отношениях с JavaScript.
Эта глава позволит вам понять, имеет ли смысл читать дальше.

Скорее всего, как и в моем случае, вы начали свой путь в веб-разработке с написания простых статических HTML-документов.
Вместе с этим, вы познакомились с веселой штукой, называемой JavaScript, но использовали его исключительно в простых случаях, добавляя интерактивности на ваши веб-странички.

Что вы хотели узнать — так это действительно полезные вещи; вы хотели знать, как создать сложный сайт.
Для этого вы изучали PHP, Ruby, Java и начинали писать backend-код.

Тем не менее, вы постоянно следили за JavaScript, вы видели, что с появлениям JQuery, Prototype и других фреймворков этот язык стал больше, чем просто window.open().

Однако, это всё ещё относилось к frontend-разработке.
Конечно, jQuery — очень мощный инструмент, но всякий раз, когда вы приправляли ваш сайт разными jQuery-«фишками», в лучшем случае, вы были JavaScript-пользователем нежели JavaScript-разработчиком.

А потом пришел Node.js. JavaScript на сервере: насколько это хорошо?

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

В этом — как раз и проблема.
JavaScript живёт двумя, может даже тремя разными жизнями: весёлый маленький DHMTL-помощник из середины 90-х годов, более серьезный frontend-инструмент в лице jQuery и наконец серверный (server-side, backend) JavaScript.
По этой причине не так просто найти информацию, которая поможет вам познать правильный JavaScript, пригодный для написания Node.js приложения в манере, дающий ощущение, что вы не просто использовали JavaScript, а действительно разрабатывали на JavaScript.

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

Конечно, существует отличная документация по Node.js, но её зачастую недостаточно. Нужно руководство.

Моя цель заключается в обеспечении вас руководством.

Предупреждение

Существуют действительно отличные специалисты в области JavaScript. Я не из их числа.

Я — действительно, тот парень, о котором написано в предыдущем параграфе.
Я знаю кое-что о разработке backend веб-приложений, но я всё ещё новичок в «реальном» JavaScript и всё ещё новичок в Node.js.
Я узнал некоторые продвинутые аспекты JavaScript совсем недавно.
Я неопытен.

Вот почему эта книга не из разряда «от новичка к эксперту», а скорее «от новичка к продвинутому новичку».

Если всё удастся, то этот документ станет тем руководством, которое я хотел бы иметь, когда начинал в Node.js.

Server-side JavaScript

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

Node.js — действительно, просто другой контекст: он позволяет вам запускать JavaScript-код вне браузера.

Чтобы ваш JavaScript код выполнился на вычислительной машине вне браузера (на backend), он должен быть интерпретирован и, конечно же, выполнен.
Именно это и делает Node.js. Для этого он использует движок V8 VM от Google — ту же самую среду исполнения для JavaScript, которую использует браузер Google Chrome.

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

Таким образом, Node.js состоит из 2 вещей: среды исполнения и полезных библиотек.

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

«Hello world»

Хорошо, давайте пойдём сразу с места в карьер и напишем наше первое Node.js-приложение: «Hello world».

Откройте ваш любимый редактор и создайте файл под названием helloworld.js.
Мы хотим вывести строку «Hello world» в консоль, для этого пишем следующий код:

console.log("Hello World");

Сохраняем файл и выполняем его посредством Node.js:

node helloworld.js

Это должно вывести Hello World на наш терминал.

Ладно, всё это скучно, правда? Давайте напишем что-нибудь полезное.

Полномасштабное веб-приложение с Node.js

Что должно делать наше приложение

Возьмём что-нибудь попроще, но приближенное к реальности:

  • Пользователь должен иметь возможность использовать наше приложение с браузером;
  • Пользователь должен видеть страницу приветствия по адресу http://domain/start;
  • Когда запрашивается http://domain/upload, пользователь должен иметь возможность загрузить картинку со своего компьютера и просмотреть её в своем браузере.

Вполне достаточно. Конечно, вы могли бы достичь этой цели, немного погуглив и поговнокодив. Но это не то, что нам нужно.

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

Задачи

Давайте проанализируем наше приложение.
Что нужно, чтобы его реализовать:

  • У нас — онлайн веб-приложение, поэтому нам нужен HTTP-сервер;
  • Нашему серверу необходимо обслуживать различные запросы в зависимости от URL, по которому был сделан запрос. Для этого нам нужен какой-нибудь роутер (маршрутизатор), чтобы иметь возможность направлять запросы определенным обработчикам;
  • Для выполнения запросов, пришедших на сервер и направляемые роутером, нам нужны действующие обработчики запросов;
  • Роутер, вероятно, должен иметь дело с разными входящими POST-данными и передавать их обработчикам запросов в удобной форме. Для этого нам нужен какой-нибудь обработчик входных данных;
  • Мы хотим не только обрабатывать запросы, но и показывать пользователю контент по запрошенным URL-адресам, поэтому нам нужна некая логика отображения для обработчиков запросов, чтобы иметь возможность отправлять контент пользовательскому браузеру;
  • Последнее, но не менее важное — пользователь сможет загружать картинки, поэтому нам нужен какой-нибудь обработчик загрузки, который возьмёт на себя заботу о деталях.

Давайте подумаем о том, как бы мы реализовали это на PHP.
Скорее всего, типичное решение будет на HTTP-сервере Apache с установленным mod_php5.

Это относится к первому пункту наших задач, то есть, «принимать HTTP-запросы и отправлять готовые веб-странички пользователю» — вещи, которые PHP сам не делает.

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

Может показаться, что это приведет к лишней работе, но сейчас вы увидите, что с Node.js это не так.

Давайте просто начнём реализовывать нашу первую задачу — HTTP-сервер.

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

Простой HTTP-сервер

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

Я должен делать всё в одном файле?
Большинство учебных пособий в интернете учат как создавать простой HTTP-сервер в Node.js, сохраняя всю логику в одном месте.
Что, если я хочу быть уверенным, что мой код останется читабельным по мере реализации всё большего функционала.

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

Это позволяет вам иметь чистый главный файл, который вы исполняете в Node.js и чистые модули, которые могут использоваться главным файлом и друг другом.

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

Я думаю, это более-менее традиционно назвать главным файлом index.js.
А код нашего сервера имеет смысл поместить в файл под названием server.js.

Давайте начнём с модуля сервера.
Создайте файл server.js в корневой директории вашего проекта и поместите туда следующий код:

var http = require("http");

http

.createServer(function(request, response) {
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("Hello World");
  response
.end();
}).listen(8888);

И всё!
Вы написали работающий HTTP-сервер.
Давайте проверим его, запустив и протестировав.
Во-первых, выполните ваш скрипт в Node.js:

node server.js

Теперь откройте ваш браузер и перейдите по адресу http://localhost:8888/.
Должна вывестись веб-страница со строкой «Hello world».

Правда, это довольно интересно?
Как насчёт того, чтобы поговорить о том, что здесь происходит и оставить на потом вопрос о том, как организовать наш проект?
Я обещаю, мы вернемся к нему.

Анализ нашего HTTP-сервера

Хорошо, тогда давайте проанализируем, что здесь действительно происходит.

Первая строчка подключает http-модуль, который поставляется вместе с Node.js и делает его доступным через переменную http.

Далее, мы вызываем одну из функций http-модуля createServer.
Эта функция возвращает объект, имеющий метод listen, принимающий числовое значение порта нашего HTTP-сервера, который необходимо прослушивать.

Пожалуйста, проигнорируйте функцию, которая определяется внутри скобок http.createServer.

Мы могли бы написать код, который запускает наш сервер, прослушивающий порт 8888, так:

var http = require("http");var server = http.createServer();
server
.listen(8888);

Это запустило бы HTTP-сервер прослушивающего порт 8888, который больше ничего не делает (даже не отвечает на входящие запросы).

Действительно интересная (и, если вы привыкли к более консервативным языкам как PHP, довольно странная) часть — это определение функции там, где вы бы ожидали увидеть первый параметр для createServer().

Оказывается, эта определяемая функции и есть первый (и только) параметр, который мы передаём в createServer() при вызове.
Потому что в JavaScript функции могут быть переданы как параметр в другую функцию.

Передача функций в качестве параметра

Вы можете в качестве примера сделать что-то подобное:

function say(word) {
  console
.log(word);
}function execute(someFunction, value) {
  someFunction
(value);
}

execute

(say, "Hello");

Разберите пример внимательно! Здесь мы передаём функцию say как первый параметр функции execute.
Не значение, которое возвращает функция say, а саму функцию say!

Таким образом, say становится локальной переменной someFunction внутри execute и execute может вызвать функцию в этой переменной вот так: someFunction() (то есть, добавив скобки).

Конечно же, так как say принимает один параметр (word), execute может передать какое-либо значение в качестве этого параметра, когда вызывает someFunction.

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

function execute(someFunction, value) {
  someFunction
(value);
}

execute

(function(word){ console.log(word) }, "Hello");

Мы определяем функцию, которую хотим передать в execute, прямо там, где у execute должен быть первый параметр.

Из-за того, что нам даже не надо давать имя этой функции, её называют анонимная функция.

Это первый проблеск, который я называю «продвинутый» JavaScript, но давайте всё по порядку.
А сейчас давайте просто примем то, что в JavaScript мы можем передать функцию как параметр, когда вызываем другую функцию.
Мы можем сделать это путём присвоения нашей функции переменной, которую му передаем, или путём определения функции для передачи на месте.

Как анонимная функция делает наш HTTP-сервер рабочим

С этими знаниями давайте вернемся назад к нашему минималистичному HTTP-серверу:

var http = require("http");

http

.createServer(function(request, response) {
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("Hello World");
  response
.end();
}).listen(8888);

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

Мы можем добиться того же самого через рефакторинг нашего кода:

var http = require("http");function onRequest(request, response) {
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("Hello World");
  response
.end();
}

http

.createServer(onRequest).listen(8888);

Может сейчас самое время спросить: Почему мы это делаем так?

Событийно-ориентированные обратные вызовы

Ответ на вопрос a) не так легко дать (по крайней мере для меня), и b) кроется в самой природе работы Node.js — это событийно-ориентированность, то, благодаря чему он работает так быстро.

Возможно, вы захотите занять немного своего времени и почитать отличный пост Felix Geisendörfer Понимание node.js, чтобы прояснить этот момент.

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

Когда вызываем метод http.createServer, мы, конечно, не только хотим иметь сервер, слушающий какой-то порт.
Мы также хотим что-нибудь сделать, когда приходит HTTP-запрос на этот сервер.

Проблема состоит в асинхронности: запрос происходит в любой момент времени, в то время как у нас только один процесс, в котором запущен наш сервер.

Когда пишем PHP-приложения, мы не беспокоимся обо всем этом: всякий раз, когда приходит HTTP-запрос, веб-сервер (обычно Apache) ответвляет новый процесс специально для этого запроса и запускает соответствующий PHP-скрипт с нуля, который выполняется от начала до конца.

Когда приходит новый запрос на порт 8888, относительно потоков управления, мы находимся в середине нашей Node.js-программы.
Как это понять, чтоб не помешаться?

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

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

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

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

По крайней мере для меня, это заняло некоторое время, чтобы понять.
Просто почитайте блог Felix Geisendörfer снова, если вы всё ещё не уверены.

Давайте немного поиграем с этим новым понятием.
Можем ли мы доказать, что наш код продолжает работать после создания сервера, даже если нет HTTP-запроса и callback-функция, переданная нами, не вызывается?
Давайте попробуем:

var http = require("http");function onRequest(request, response) {
  console
.log("Request received.");
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("Hello World");
  response
.end();
}

http

.createServer(onRequest).listen(8888);

console

.log("Server has started.");

Обратите внимание, что я использую console.log для вывода текста «Request received.», когда срабатывает функция onRequest (наш callback), а текст «Server has started.» — сразу после запуска HTTP-сервера.

Когда мы запустим этот код (как обычно, node server.js), он тут же выведет в командной строке «Server has started.».
Всякий раз, когда мы делаем запрос нашему серверу (через переход по адресу http://localhost:8888/ в нашем браузере), в командной строке выводится сообщение «Request received.».

Объектно-ориентированный асинхронный серверный JavaScript с callback-ми в действии :-)

(Обратите внимание, что наш сервер, возможно, будет выводить «Request received.» в консоль 2 раза при открытии страницы в браузере.
Это происходит из-за того, что большинство браузеров будут пытаться загрузить фавикон по адресу http://localhost:8888/favicon.ico при запросе http://localhost:8888/)

Как наш сервер обрабатывает запросы

Хорошо, давайте быстро проанализируем остальной код сервера внутри тела нашей callback-функции onRequest().

Когда callback запускается и наша функция onRequest() срабатывает, в неё передаются 2 параметра: request и response.

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

И наш код делает именно это: Всякий раз, когда запрос получен, он использует функцию response.writeHead() для отправки HTTP-статуса 200 и Content-Type в заголовке HTTP-ответа, а функцию Response.Write() для отправки текста «Hello World» в теле HTTP-ответа.

И последнее, мы вызываем response.end() чтобы завершить наш ответ.

На данный момент, мы не заботимся о деталях запроса, поэтому мы не используем объект request полностью.

Выбор места для нашего серверного модуля

Я обещал, что мы вернёмся к организации нашего приложения.
У нас есть код очень простого HTTP-сервера в файле server.js и я упоминал, что общепринято иметь главный файл с названием index.js, который используется для начальной загрузки и запуска нашего приложения, путём использования других модулей приложения (таких как наш модуль HTTP-сервера в server.js).

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

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

var http = require("http");...

http

.createServer(...);

Где-то внутри Node.js живёт модуль под названием «http» и мы можем использовать его в нашем коде, путём подключения и присвоения его результата локальной переменной.

Это делает нашу локальную переменную объектом, содержащим в себе все публичные методы модуля http.

Общепринитая практика — использовать имя модуля для имени локальной переменной, но мы свободны в своём выборе делать, как нам нравится:

var foo = require("http");...

foo

.createServer(...);

Теперь понятно, как использовать внутренние модули Node.js.
А как создать свой собственный модуль и как его использовать?

Давайте выясним это, превратив наш скрипт server.js в настоящий модуль.

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

Сейчас функционал нашего HTTP-сервера надо экспортировать, что довольно просто: скрипты, подключающие наш модуль сервера, просто запускают сервер.

Чтобы сделать это возможным, поместим код нашего сервера в функцию под название start и будем экспортировать эту функцию:

var http = require("http");function start() {
 
function onRequest(request, response) {
    console
.log("Request received.");
    response
.writeHead(200, {"Content-Type": "text/plain"});
    response
.write("Hello World");
    response
.end();
 
}

  http

.createServer(onRequest).listen(8888);
  console
.log("Server has started.");
}

exports

.start = start;

Теперь мы можем создать наш основной файл index.js, и запускать наш HTTP-сервер там, хотя код для сервера находится всё ещё в файле server.js.

Создаём файл index.js со следующим содержимым:

var server = require("./server");

server

.start();

Как вы могли видеть, мы можем использовать модуль сервера просто как внешний модуль: вызвав этот файл и определив для него переменную, экспортированные функции становятся доступны нам.

Вот и всё. Сейчас мы можем запустить наше приложение через главный скрипт и он будет делать всё то же самое, что и раньше:

node index.js

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

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

В очень простом приложении мы могли бы делать это напрямую внутри callback-функции onRequest().
Но, как я говорил, давайте добавим немного больше абстракции, чтобы сделать наш пример интереснее.

Задание соответствия между разными HTTP-запросами и разными частями нашего кода называется «маршрутизация» («routing», роутинг).
Давайте тогда создадим модуль под названием router.

Что необходимо для «роутера»?

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

Итак, нам надо рассматривать HTTP-запрос и извлекать запрошенный URL, а также GET/POST-параметры.
Можно поспорить, должен ли этот код быть частью роутера или сервера (или даже своего собственного модуля), но давайте сейчас пока просто сделаем его частью сервера.

Вся необходимая нам информация доступна через объект request, который передается в качестве первого параметра нашей callback-функции onRequest().
Чтобы интерпретировать эту информацию, нам необходимо добавить кое-какие Node.js-модули, а именно url и querystring.

Модуль url поддерживает методы, которые позволяют нам извлекать различные части URL (такие как запрошенный путь (URL path) и строка параметров запроса (query string)), а querystring в свою очередь, используется для парсинга строки параметров запроса (query string):

                               url.parse(string).query
                                       |
       url.parse(string).pathname      |
                   |                   |
                   |                   |
                 ------ -------------------
http://localhost:8888/start?foo=bar&hello=world
                            ---       -----
                             |          |
                             |          |
          querystring(string)["foo"]    |
                                        |
                     querystring(string)["hello"]

Конечно, мы также можем использовать querystring для парсинга тела POST-запроса, как мы увидим далее.

Давайте сейчас добавим в нашу функцию onRequest() логику, необходимую для извлечения пути URL (pathname), запрошенного браузером:

var http = require("http");
var url = require("url");function start() {
 
function onRequest(request, response) {
   
var pathname = url.parse(request.url).pathname;
    console
.log("Request for " + pathname + " received.");
    response
.writeHead(200, {"Content-Type": "text/plain"});
    response
.write("Hello World");
    response
.end();
 
}

  http

.createServer(onRequest).listen(8888);
  console
.log("Server has started.");
}

exports

.start = start;

Замечательно.
Теперь наше приложение может различать запросы на основе запрошенного пути URL.
Это позволяет нам направлять запросы нашим обработчикам запросов в зависимости от пути URL, используя наш роутер.
Таким образом, мы можем строить наше приложение RESTful-путём, потому что теперь можем реализовать интерфейс, следующий принципам Идентификации ресурсов (смотри статью в википедии REST для справки).

В контексте нашего приложения, это означает, что мы сможем обрабатывать запросы с URL /start и /upload разными частями нашего кода.
Скоро мы увидим, как всё соединяется вместе.

Теперь самое время написать наш роутер.
Создаём новый файл под названием router.js со следующим содержимым:

function route(pathname) {
  console
.log("About to route a request for " + pathname);
}

exports

.route = route;

Конечно этот код ничего не делает, но сейчас этого достаточно.
Давайте сначала посмотрим, как скрепить этот роутер с нашим сервером до того как поместим больше логики в роутер.

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

Для начала, расширим нашу серверную функцию start(), чтобы дать нам возможность передавать функцию route() как параметр:

var http = require("http");
var url = require("url");function start(route) {
 
function onRequest(request, response) {
   
var pathname = url.parse(request.url).pathname;
    console
.log("Request for " + pathname + " received.");

    route

(pathname);

    response

.writeHead(200, {"Content-Type": "text/plain"});
    response
.write("Hello World");
    response
.end();
 
}

  http

.createServer(onRequest).listen(8888);
  console
.log("Server has started.");
}

exports

.start = start;

Теперь расширим наш index.js соответственно, то есть внедрим функцию route() нашего роутера в сервер:

var server = require("./server");
var router = require("./router");

server

.start(router.route);

Мы опять передаём функцию, которая не является чем-то новым для нас.

Если мы сейчас запустим наше приложение (node index.js, как обычно) и запросим какой-нибудь URL, вы сможете увидеть в консоли, что наш HTTP-сервер использует наш роутер и передает ему запрошенный pathname:

bash$ node index.js
Request for /foo received.
About to route a request for /foo

(Я опустил слегка надоедливый вывод для запроса /favicon.ico)

Исполнение королевских постановлений в царстве глаголов

Позвольте мне ещё раз побродить вокруг и около и снова поговорить о функциональном программировании.

Передача функций связана не только с техническими соображениями.
Относительно разработки программного обеспечения это — почти философия.
Просто подумайте: в нашем index-файле мы могли бы передавать объект router в наш сервер и сервер мог бы вызывать функцию route этого объекта.

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

Но серверу не нужно нечто.
Ему нужно только получить что-то сделанное, а чтоб получить уже что-то сделанное, вам не нужно нечто совсем, вам необходимо действие.
Вам не нужно существительное, вам нужен глагол.

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

Я понял это, когда читал шедевр Стива Йегге Execution in the Kingdom of Nouns (частичный перевод на русский Исполнение королевских постановлений в царстве существительных).
Почитайте это обязательно. Это одно из лучших произведений о программировании, которое я когда-либо имел удовольствие встречать.

Роутинг реальных обработчиков запроса

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

Конечно, этого недостаточно.
«Роутинг» подразумевает, что мы хотим обрабатывать запросы на разные URL по-разному.
Мы хотели бы иметь «бизнес-логику» для запросов к /start в одной функции, а для запросов к /upload в другой.

Прямо сейчас мы закончим с нашим роутером. Роутер не место на самом деле, чтобы делать что-то с запросами, потому что будет плохо масштабироваться, а наше приложение будет становиться сложнее.

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

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

function start() {
  console
.log("Request handler 'start' was called.");
}function upload() {
  console
.log("Request handler 'upload' was called.");
}

exports

.start = start;
exports
.upload = upload;

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

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

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

Как мы собираемся передать их?
Сейчас у нас есть два обработчика, но в реальном приложении это число будет увеличиваться и меняться.
И мы уверены, что не хотим возиться с роутером каждый раз, когда добавляется новый URL + обработчик запроса.
И какие-нибудь if запрос == x then вызвать обработчик y в роутере будут более чем убоги.

Переменное число элементов и каждому соответствует строка (запрашиваемый URL)?
Так, похоже на ассоциативный массив, это наиболее подходящее.

Это решение немного разочаровывает тем фактом, что JavaScript не поддерживает ассоциативные массивы. Или нет?
Оказывается в действительности, если нам нужны ассоциативные массивы, мы должны использовать объекты!

Об этом есть хорошее введение http://msdn.microsoft.com/en-us/magazine/cc163419.aspx.
Позвольте мне процитировать подходящую часть:

В C++ или C#, когда мы говорим об объектах, мы ссылаемся на экземпляры классов или структуры.
Объекты имеют разные свойства и методы, в зависимости от шаблонов (классов), экземплярами которых они являются.
Но не в случае с JavaScript-объектами.
В JavaScript, объекты — это просто коллекция пар имя/значение — JavaScript-объект — это как словарь со строковыми ключами.

Если JavaScript-объекты это просто коллекции пар имя/значение, как тогда у них могут быть методы?
Итак, значения могут быть строками, числами и т.д. или функциями!

Хорошо, наконец-то возвращаемся к нашему коду. Мы решили, что мы хотим передать список из requestHandlers как объект и, для того, чтобы достичь слабое связывание, мы хотим внедрить этот объект в route().

Начнём с добавления объекта в наш главный файл index.js:

var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");var handle = {}
handle
["/"] = requestHandlers.start;
handle
["/start"] = requestHandlers.start;
handle
["/upload"] = requestHandlers.upload;

server

.start(router.route, handle);

Хотя handle — это больше из разряда «нечто» (коллекция обработчиков запроса), я, всё-таки, предлагаю называть его глаголом, потому что в результате это будет функциональное выражение в нашем роутере, как вы скоро увидите.

Как вы можете видеть, это действительно просто — назначать различные URL соответствующему обработчику запроса: просто добавляя пару ключ/значение из «/» и requestHandlers.start, мы можем выразить красивым и аккуратным способом, что не только запросы к «/start», но также и запросы к «/» должны быть обработаны обработчиком start.

После определения объекта мы передали его в сервер как дополнительный параметр.
Изменим наш server.js, чтобы использовать его:

var http = require("http");
var url = require("url");function start(route, handle) {
 
function onRequest(request, response) {
   
var pathname = url.parse(request.url).pathname;
    console
.log("Request for " + pathname + " received.");

    route

(handle, pathname);

    response

.writeHead(200, {"Content-Type": "text/plain"});
    response
.write("Hello World");
    response
.end();
 
}

  http

.createServer(onRequest).listen(8888);
  console
.log("Server has started.");
}

exports

.start = start;

Мы добавили параметр handle в функцию start() и передаём объект handle в callback-функцию route() в качестве перового параметра.

Соответственно, изменим функцию route() в нашем файле router.js:

function route(handle, pathname) {
  console
.log("About to route a request for " + pathname);
 
if (typeof handle[pathname] === 'function') {
    handle
[pathname]();
 
} else {
    console
.log("No request handler found for " + pathname);
 
}
}

exports

.route = route;

Что мы здесь делаем — мы проверяем, существует ли обработчик запроса для данного пути, и если существует, просто вызываем соответствующую функцию.
Из-за того, что мы имеем доступ к нашим функциям обработчиков запроса из нашего объекта просто, как если бы имели доступ к элементу ассоциативного массива, у нас есть это прекрасное выражение handle[pathname]();, о котором говорилось ранее: «Пожалуйста, handle этот pathname».

Хорошо, это всё, что нужно, чтобы связать сервер, роутер и обработчики запроса вместе!
При запуске нашего приложения и запроса http://localhost:8888/start в браузере, мы можем убедиться, что надлежащий обработчик запроса действительно был вызван:

Server has started.
Request for /start received.
About to route a request for /start
Request handler 'start' was called.

Так же открываем http://localhost:8888/ в нашем браузере и убеждаемся, что эти запросы в самом деле обрабатываются обработчиком запросов start:

Request for / received.
About to route a request for /
Request handler 'start' was called.

Создание ответа обработчиков запроса

Замечательно. Вот только если бы обработчики запроса могли отправлять что-нибудь назад браузеру, было бы ещё лучше, правильно?

Вспомните, «Hello World», который выводит ваш браузер в запрошенной странице, всё ещё исходит от функции onRequest в нашем файле server.js.

«Обработка запроса» подразумевает «ответ на запросы» в конце концов, поэтому необходимо дать возможность нашим обработчикам запроса общаться с браузером так же, как это делает функция onRequest.

Как делать не надо

Прямой подход, который мы захотим использовать как разработчики с опытом в PHP или Ruby, на самом деле ложный: он может прекрасно работать, иметь большой смысл, а потом, когда мы этого не ждём, неожиданно всё развалится.

Под «прямым подходом» я подразумеваю использование в обработчиках запроса return «» для контента, который надо показать пользователю, и отправлять этот ответ в функцию onRequest назад пользователю.

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

Мы начнём с обработчиков запроса и заставим их возвращать то, что хотели бы показать в браузере.
Нам надо изменить requestHandlers.js вот так:

function start() {
  console
.log("Request handler 'start' was called.");
 
return "Hello Start";
}function upload() {
  console
.log("Request handler 'upload' was called.");
 
return "Hello Upload";
}

exports

.start = start;
exports
.upload = upload;

Хорошо. Также, роутер должен вернуть серверу то, что обработчики запроса вернули ему.
Поэтому надо отредактировать router.js так:

function route(handle, pathname) {
  console
.log("About to route a request for " + pathname);
 
if (typeof handle[pathname] === 'function') {
   
return handle[pathname]();
 
} else {
    console
.log("No request handler found for " + pathname);
   
return "404 Not found";
 
}
}

exports

.route = route;

Как видим, возвращается некоторый текст «404 Not found», если запрос не может быть маршрутизирован.

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

var http = require("http");
var url = require("url");function start(route, handle) {
 
function onRequest(request, response) {
   
var pathname = url.parse(request.url).pathname;
    console
.log("Request for " + pathname + " received.");

    response

.writeHead(200, {"Content-Type": "text/plain"});
   
var content = route(handle, pathname)
    response
.write(content);
    response
.end();
 
}

  http

.createServer(onRequest).listen(8888);
  console
.log("Server has started.");
}

exports

.start = start;

Если запустим наше написаное приложение, всё будет работать замечательно: запрос http://localhost:8888/start выдаст в браузере результат «Hello Start», запрос http://localhost:8888/upload даст нам «Hello Upload», а http://localhost:8888/foo выведет «404 Not found».

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

Подробный ответ займёт немного больше времени.

Блокирование и неблокирование

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

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

Для этого модифицируем обработчик запроса start так, чтобы он ждал 10 секунд до того как вернёт свою строку «Hello Start».
В JavaScript нет такой штуки как sleep(), поэтому мы будем использовать хитрый хак.

Пожалуйста, измените requestHandlers.js как описано далее:

function start() {
  console
.log("Request handler 'start' was called.");function sleep(milliSeconds) {
   
var startTime = new Date().getTime();
   
while (new Date().getTime() < startTime + milliSeconds);
 
}

  sleep

(10000);
 
return "Hello Start";
}function upload() {
  console
.log("Request handler 'upload' was called.");
 
return "Hello Upload";
}

exports

.start = start;
exports
.upload = upload;

Просто объясню, что этот код делает: когда функция start() вызвана, Node.js ожидает 10 секунд и только тогда возвращает «Hello Start».
Когда вызывается upload(), она выполняется немедленно, как и раньше.

(Конечно, вы уже поняли, вместо засыпания на 10 секунд, в start() могут быть реальные блокирующие операции, такие как сложные длительные вычисления.)

Давайте посмотрим, что поменялось.

Как обычно, нам надо перезапустить сервер.
На этот раз я попрошу вас следовать немного более сложному «протоколу», чтобы увидеть, что произошло: во-первых, откройте браузер или таб.
В первом окне браузера, введите, пожалуйста, http://localhost:8888/start в адресную строку, но не переходите пока по этому адресу!

В адресную строку второго окна браузера введите http://localhost:8888/upload и снова не переходите по адресу.

Теперь сделайте, как описано далее: нажмите клавишу Enter в первом окне («/start»), а затем быстро переключитесь на второе окно («/upload») и нажмите тоже Enter.

Что вы будете наблюдать: URL /start потребуется 10 секунд для загрузки, как мы и ожидали.
Но URL /upload так же потребуется 10 секунд на загрузку, хотя в соответствующем обработчике запроса нет sleep()!

Почему? Потому что start() содержит блокирующую операцию.
Like in «it’s blocking everything else from working».

И в этом проблема, потому что, как говорят: «В node всё работает параллельно, за исключением вашего кода».

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

Но для этого нам надо использовать обратные вызовы, передавая функции внутри тех функций, которые могут сделать то, что занимает некоторое время (как, например, sleep() на 10 секунд или запрос к базе данных или какое-то дорогостоящее вычисление.)

Таким образом, мы как бы говорим: «Эй, возможноДолгаяФункция(), пожалуйста, сделай вот это, но я, однопотоковый Node.js, не собираюсь ждать здесь, пока ты закончишь, я продолжу выполнение строчек кода ниже тебя, а ты возьми пока вот эту функцию callbackFunction() и вызови её, когда всё сделаешь. Спасибо!»

(Если хотите почитать об этом более подробно, пожалуйста посмотрите пост Mixu на Understanding the node.js event loop.)

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

Ещё раз давайте попробуем испытать проблему на своей шкуре, модифицировав наше приложение.

Мы снова используем наш обработчик запроса start. Пожалуйста, измените его следующим образом (файл requestHandlers.js)

var exec = require("child_process").exec;function start() {
  console
.log("Request handler 'start' was called.");
 
var content = "empty";

  exec

("ls -lah", function (error, stdout, stderr) {
    content
= stdout;
 
});return content;
}function upload() {
  console
.log("Request handler 'upload' was called.");
 
return "Hello Upload";
}

exports

.start = start;
exports
.upload = upload;

Как можно видеть, мы просто внедрили новый модуль Node.js child_process.
Мы сделали так, потому что это позволит нам использовать очень простую, но полезную неблокирующую операцию: exec().

Что делает exec() — она выполняет shell-команду внутри Node.js.
В этом примере мы собираемся использовать её, чтобы получить список всех файлов в текущей директории («ls -lah»), позволяя нам отобразить этот список в браузере пользователя, запросившего URL /start.

Что делает этот код: создает новую переменную content (с начальным значением «empty»), выполняет «ls -lah», заполняет переменную результатом и возвращает её.

Как обычно, запустим наше приложение и посетим http://localhost:8888/start.

Которая загрузит нам красивую страничку со строкой «empty». Что тут не так?

Ну, как вы уже догадались, exec() делает свою магию в неблокирующий манере.
Это хорошая штука, потому что таким образом мы можем выполнять очень дорогостоящие shell-операции (как, например, копирование больших файлов или что-то подобное), не заставляя наше приложение полностью останавливаться, пока блокирующая sleep-операция не выполнится.

Если хотите удостовериться, замените «ls -lah» на более дорогостоящую операцию «find /»).

Но мы не совсем довольны своей элегантной неблокирующей операцией, когда наш браузер не отображает её результат, не так ли?

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

Проблемой является то, что exec(), чтобы работать без блокирования, использует callback-функцию.

В нашем примере это анонимная функция, которая передаётся как второй параметр в функцию exec():

function (error, stdout, stderr) {
  content
= stdout;
}

И здесь лежит корень нашей проблемы: наш собственный код исполняется синхронно, что означает, что сразу после вызова exec(), Node.js продолжит выполнять return content;.
К этому моменту content ещё «empty», из-за того, что callback-функция, переданная в exec(), до сих пор не вызвана — потому что операция exec() асинхронная.

Теперь «ls -lah» — очень недорогая и быстрая операция (если только в директории не миллион файлов).
Именно поэтому callback вызывается относительно оперативно — но это, всё же, происходит асинхронно.

Использование более дорогостоящих команд делает это более очевидным: «find /» занимает около 1 минуты на моей машине, но если я заменяю «ls -lah» на «find /» в обработчике запроса, то я всё ещё немедленно получаю HTTP-ответ, когда открываю URL /start.
Ясно, что exec() делает что-то в фоновом режиме, пока Node.js продолжает исполнять приложение и мы можем предположить, что callback-функция, которую мы передали в exec(), будет вызвана только когда команда «find /» закончит выполняться.

Но как нам достичь нашей цели, то есть, показать пользователю список файлов в текущей директории?

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

Ответ обработчиков запроса с неблокирующими операциями.

Я употребил фразу «правильный способ».
Опасная вещь.
Довольно часто не существует единого «правильного способа».

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

Сейчас наше приложение способно транспортировать контент (который обработчики запроса хотели бы показать пользователю) от обработчиков запроса к HTTP-серверу, возвращая его через слои приложения (обработчик запроса -> роутер -> сервер).

Наш новый подход заключается в следующем: вместо доставки контента серверу мы будем сервер доставлять к контенту.
Чтобы быть более точным, мы будем внедрять объект response (из серверной callback-функции onRequest()) через роутер в обработчики запроса.
Обработчики смогут тогда использовать функции этого объекта для ответа на сами запросы.

Достаточно разъяснений. Вот — пошаговый рецепт изменения нашего приложения.

Начнём с нашего server.js:

var http = require("http");
var url = require("url");function start(route, handle) {
 
function onRequest(request, response) {
   
var pathname = url.parse(request.url).pathname;
    console
.log("Request for " + pathname + " received.");

    route

(handle, pathname, response);
 
}

  http

.createServer(onRequest).listen(8888);
  console
.log("Server has started.");
}

exports

.start = start;

Вместо ожидания возврата значения от функции route(), мы передаём наш объект response в качестве третьего параметра.
Кроме того, мы удалили всякие вызовы методов response из обработчика onRequest(), потому что мы рассчитываем, что route позаботится об этом.

Далее идёт router.js:

function route(handle, pathname, response) {
  console
.log("About to route a request for " + pathname);
 
if (typeof handle[pathname] === 'function') {
    handle
[pathname](response);
 
} else {
    console
.log("No request handler found for " + pathname);
    response
.writeHead(404, {"Content-Type": "text/plain"});
    response
.write("404 Not found");
    response
.end();
 
}
}

exports

.route = route;

Та же схема: вместо ожидания возврата значения от наших обработчиков события, мы передаём объект respond.

Если обработчик запроса не может быть использован, мы заботимся об ответе с надлежащим заголовком «404» и телом ответа.

И последнее, но не менее важное, мы модифицируем requestHandlers.js:

var exec = require("child_process").exec;function start(response) {
  console
.log("Request handler 'start' was called.");

  exec

("ls -lah", function (error, stdout, stderr) {
    response
.writeHead(200, {"Content-Type": "text/plain"});
    response
.write(stdout);
    response
.end();
 
});
}function upload(response) {
  console
.log("Request handler 'upload' was called.");
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("Hello Upload");
  response
.end();
}

exports

.start = start;
exports
.upload = upload;

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

Обработчик start будет отвечать изнутри анонимного обратного вызова exec(), а обработчик upload будет всё ещё выдавать «Hello Upload», но теперь посредством объекта response.

Если вы запустили наше приложение снова (node index.js), всё должно работать как и ожидалось.

Если хотите убедиться, что дорогостоящая операция в /start больше не будет блокировать запросы на /upload, модифицируйте ваш requestHandlers.js как показано далее:

var exec = require("child_process").exec;function start(response) {
  console
.log("Request handler 'start' was called.");

  exec

("find /",
   
{ timeout: 10000, maxBuffer: 20000*1024 },
   
function (error, stdout, stderr) {
      response
.writeHead(200, {"Content-Type": "text/plain"});
      response
.write(stdout);
      response
.end();
   
});
}function upload(response) {
  console
.log("Request handler 'upload' was called.");
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("Hello Upload");
  response
.end();
}

exports

.start = start;
exports
.upload = upload;

Благодаря этому, HTTP-запросы к http://localhost:8888/start будут занимать не менее 10 секунд, но запросы к http://localhost:8888/upload будут получать ответ немедленно, даже если /start всё ещё занят вычислениями.

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

До сих пор мы делали всё прекрасно и изысканно, но мы не создали ничего значимого для клиентов нашего супер-сайта.

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

OK, давайте шаг за шагом, но с разъяснением больших техник и принципов JavaScript, и в то же время, давайте немного ускоримся.
Автору слишком нравится слушать самого себя.

Здесь «шаг за шагом» означает примерно 2 шага: сначала мы посмотрим как обрабатывать входящие POST-запросы (но не загрузку файла), и на втором шаге мы используем внешний модуль Node.js для обработки загрузки файла.
Я выбирал этот подход по двум причинам.

Во-первых, обрабатывать базовые POST-запросы относительно просто в Node.js, но для обучения это — достаточно стоящее упражнение.

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

Обработка POST-запросов

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

HTML-код для формы текстового поля должен формировать наш обработчик запроса /start, так давайте сразу же добавим его в файл requestHandlers.js:

function start(response) {
  console
.log("Request handler 'start' was called.");var body = '<html>'+
   
'<head>'+
   
'<meta http-equiv="Content-Type" content="text/html; '+
   
'charset=UTF-8" />'+
   
'</head>'+
   
'<body>'+
   
'<form action="/upload" method="post">'+
   
'<textarea name="text" rows="20" cols="60"></textarea>'+
   
'<input type="submit" value="Submit text" />'+
   
'</form>'+
   
'</body>'+
   
'</html>';

    response

.writeHead(200, {"Content-Type": "text/html"});
    response
.write(body);
    response
.end();
}function upload(response) {
  console
.log("Request handler 'upload' was called.");
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("Hello Upload");
  response
.end();
}

exports

.start = start;
exports
.upload = upload;

Если теперь этот код не выиграет Webby Awards, то я не знаю, какой сможет.
Вы должны увидеть эту очень простую форму, когда запросите http://localhost:8888/start в вашем браузере.
Если это не так — возможно, вы не перезагрузили приложение.

Я уже слышу вас: помещать содержимое представления прямо в обработчик запроса некрасиво.
Тем не менее, я решил не включать этот дополнительный уровень абстракции (то есть, разделение представления и логики) в наш учебник, потому что, я думаю, что это не научит нас чему-нибудь стоящему в контексте JavaScript или Node.js.

Давайте лучше использовать появившееся окно для более интересных проблем, то есть, обработки POST-запроса в нашем обработчике запроса /upload при отправке этой формы пользователем.

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

Это имеет смысл, потому что POST-запросы могут быть потенциально очень большими — ничто не мешает пользователю ввести текст размером в несколько мегабайтов.
Обработка всего массива данных за один раз может привести к блокирующей операции.

Чтобы сделать весь процесс неблокирующим, Node.js обслуживает POST-данные небольшими порциями, а callback-функции вызываются при определённых событиях.
Эти события — data (когда приходит новая порция POST-данных) и end (когда все части данных были получены).

Надо сообщить Node.js, какие функции вызывать, когда эти события произойдут.
Это делается путём добавления слушателей (listeners) в объект request, который передаётся в нашу callback-функцию onRequest, когда HTTP-запрос получен.

В основном, это выглядит так:

request.addListener("data", function(chunk) {
 
// called when a new chunk of data was received
});

request

.addListener("end", function() {
 
// called when all chunks of data have been received
});

Возникает вопрос, где реализовать эту логику.
В настоящее время мы можем получить доступ к объекту request только в нашем сервере — мы не передаём его в роутер и в обработчики запроса, как делаем это с объектом response.

На мой взгляд, это работа HTTP-сервера — предоставлять приложению все данные из запросов.
Поэтому я предлагаю обрабатывать POST-данные прямо в сервере и передавать конечные данные в роутер и обработчики запроса, которые сами решат, что с ними делать.

Таким образом, идея — в том, чтобы поместить обратные вызовы событий data и end в сервер, собирать все куски POST-данных в data и вызывать роутер при получении события end, пока идёт передача собранных порций данных в роутер, который в свою очередь передаёт их в обработчики запроса.

Начинаем с server.js:

var http = require("http");
var url = require("url");function start(route, handle) {
 
function onRequest(request, response) {
   
var postData = "";
   
var pathname = url.parse(request.url).pathname;
    console
.log("Request for " + pathname + " received.");

    request

.setEncoding("utf8");

    request

.addListener("data", function(postDataChunk) {
      postData
+= postDataChunk;
      console
.log("Received POST data chunk '"+
      postDataChunk
+ "'.");
   
});

    request

.addListener("end", function() {
      route
(handle, pathname, response, postData);
   
});}

  http

.createServer(onRequest).listen(8888);
  console
.log("Server has started.");
}

exports

.start = start;

Здесь, в основном, мы сделали три вещи: во-первых, определили, что ожидаем полученные данные в кодировке UTF-8, затем добавили слушатель для события «data», который шаг за шагом заполняет нашу новую переменную postData всякий раз, когда прибывает новая порция POST-данных, и далее — переходим к вызову нашего роутера в обратном вызове события end, чтобы убедиться, что вызов происходит, когда все POST-данные собраны.
Мы также передаём POST-данные в роутере, потому что они нам понадобятся в обработчиках запроса.

Добавление логирования в консоли для каждой порции полученных данных — возможно, плохая идея для конечного кода (мегабайты POST-данных, помните?), но это имеет смысл, чтобы посмотреть, что происходит.

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

Давайте добавим ещё больше крутизны в наше приложение.
На странице /upload мы будем показывать принятый контент.
Чтобы сделать это возможным, нам необходимо передавать postData в обработчики запроса. В router.js:

function route(handle, pathname, response, postData) {
  console
.log("About to route a request for " + pathname);
 
if (typeof handle[pathname] === 'function') {
    handle
[pathname](response, postData);
 
} else {
    console
.log("No request handler found for " + pathname);
    response
.writeHead(404, {"Content-Type": "text/plain"});
    response
.write("404 Not found");
    response
.end();
 
}
}

exports

.route = route;

И в requestHandlers.js мы включаем эти данные в нашем ответе обработчика запроса upload:

function start(response, postData) {
  console
.log("Request handler 'start' was called.");var body = '<html>'+
   
'<head>'+
   
'<meta http-equiv="Content-Type" content="text/html; '+
   
'charset=UTF-8" />'+
   
'</head>'+
   
'<body>'+
   
'<form action="/upload" method="post">'+
   
'<textarea name="text" rows="20" cols="60"></textarea>'+
   
'<input type="submit" value="Submit text" />'+
   
'</form>'+
   
'</body>'+
   
'</html>';

    response

.writeHead(200, {"Content-Type": "text/html"});
    response
.write(body);
    response
.end();
}function upload(response, postData) {
  console
.log("Request handler 'upload' was called.");
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("You've sent: " + postData);
  response
.end();
}

exports

.start = start;
exports
.upload = upload;

Вот и всё, теперь мы можем получить POST-данные и использовать их в наших обработчиках запроса.

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

Мы уже читали про модуль querystring, который поможет нам с этим:

var querystring = require("querystring");function start(response, postData) {
  console
.log("Request handler 'start' was called.");var body = '<html>'+
   
'<head>'+
   
'<meta http-equiv="Content-Type" content="text/html; '+
   
'charset=UTF-8" />'+
   
'</head>'+
   
'<body>'+
   
'<form action="/upload" method="post">'+
   
'<textarea name="text" rows="20" cols="60"></textarea>'+
   
'<input type="submit" value="Submit text" />'+
   
'</form>'+
   
'</body>'+
   
'</html>';

    response

.writeHead(200, {"Content-Type": "text/html"});
    response
.write(body);
    response
.end();
}function upload(response, postData) {
  console
.log("Request handler 'upload' was called.");
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("You've sent the text: "+
  querystring
.parse(postData).text);
  response
.end();
}

exports

.start = start;
exports
.upload = upload;

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

Обработка загрузки файлов

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

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

Внешний модуль, который мы собираемся использовать, node-formidable от Felix Geisendörfer.
Этот модуль поможет нам абстрагироваться от мерзких деталей парсинга входящих файловых данных.
В конце концов, обработка входящих файлов это не что иное, как «просто» обработка POST-данных, но, в действительности, дьявол кроется в деталях, поэтому в нашем случае имеет смысл использовать готовое решение.

Чтобы использовать код Феликса, соответствующий модуль Node.js должен быть инсталлирован.
На борту Node.js есть собственный менеджер пакетов, называемый NPM.
Он позволяет нам инсталировать внешние модули Node.js в очень удобной форме.
С учетом установленного Node.js, всё сводится к

npm install formidable

в нашей командной строке. Если вы в конце увидели следующее:

npm info build Success: formidable@1.0.9
npm ok

…это значит — всё хорошо.

Модуль formidable теперь доступен в нашем коде — всё, что нужно, это просто запросить его как один из тех модулей, которые мы использовали ранее:

var formidable = require("formidable");

По сути, formidable делает форму, отправленную через HTTP POST, доступной для парсинга в Node.js.
Всё, что нам надо — это создать новый экземпляр объекта IncomingForm, который является абстракцией отправленной формы и может быть использован для парсинга объекта request нашего HTTP-сервера, для полей и файлов, отправленных через эту форму.

Пример кода со страницы проекта node-formidable показывает, как разные части сочетаются друг с другом:

var formidable = require('formidable'),
    http
= require('http'),
    sys
= require('sys');

http

.createServer(function(req, res) {
 
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
   
// parse a file upload
   
var form = new formidable.IncomingForm();
    form
.parse(req, function(err, fields, files) {
      res
.writeHead(200, {'content-type': 'text/plain'});
      res
.write('received upload:nn');
      res
.end(sys.inspect({fields: fields, files: files}));
   
});
   
return;
 
}// show a file upload form
  res
.writeHead(200, {'content-type': 'text/html'});
  res
.end(
   
'<form action="/upload" enctype="multipart/form-data" '+
   
'method="post">'+
   
'<input type="text" name="title"><br>'+
   
'<input type="file" name="upload" multiple="multiple"><br>'+
   
'<input type="submit" value="Upload">'+
   
'</form>'
 
);
}).listen(8888);

Если вы поместите этот код в файл и исполните его посредством node, вы сможете отправлять простые формы, включая загрузку фото, и увидите, как организован объект files, который передавался в callback, определенном в вызове form.parse.

received upload:

{ fields: { title: 'Hello World' },
  files:
   { upload:
	  { size: 1558,
		path: '/tmp/1c747974a27a6292743669e91f29350b',
		name: 'us-flag.png',
		type: 'image/png',
		lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT,
		_writeStream: [Object],
		length: [Getter],
		filename: [Getter],
		mime: [Getter] } } }

Чтобы выполнить последний пункт нашей задачи, мы должны включить логику парсинга форм модуля formidable в структуру нашего кода, плюс ко всему, надо разобраться, как отдавать контент загруженного файла (который сохранен в директории /tmp) браузеру.

Давайте сначала решим последнюю задачу: если имеется файл картинки на вашем локальном диске, что сделать, чтобы передать его браузеру?

Мы, очевидно, собираемся считать содержимое этого файла в наш Node.js-сервер, и неудивительно, что для этого имеется соответствующий модуль под названием fs.

Давайте добавим ещё один обработчик запроса для URL /show, который будет «захардкоженно» показывать содержимое файла /tmp/test.png.
Конечно же, имеет смысл в первую очередь поместить реальную png-картинку в этот каталог.

Мы собираемся изменить requestHandlers.js, как показано далее:

var querystring = require("querystring"),
    fs
= require("fs");function start(response, postData) {
  console
.log("Request handler 'start' was called.");var body = '<html>'+
   
'<head>'+
   
'<meta http-equiv="Content-Type" '+
   
'content="text/html; charset=UTF-8" />'+
   
'</head>'+
   
'<body>'+
   
'<form action="/upload" method="post">'+
   
'<textarea name="text" rows="20" cols="60"></textarea>'+
   
'<input type="submit" value="Submit text" />'+
   
'</form>'+
   
'</body>'+
   
'</html>';

    response

.writeHead(200, {"Content-Type": "text/html"});
    response
.write(body);
    response
.end();
}function upload(response, postData) {
  console
.log("Request handler 'upload' was called.");
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("You've sent the text: "+
  querystring
.parse(postData).text);
  response
.end();
}function show(response, postData) {
  console
.log("Request handler 'show' was called.");
  fs
.readFile("/tmp/test.png", "binary", function(error, file) {
   
if(error) {
      response
.writeHead(500, {"Content-Type": "text/plain"});
      response
.write(error + "n");
      response
.end();
   
} else {
      response
.writeHead(200, {"Content-Type": "image/png"});
      response
.write(file, "binary");
      response
.end();
   
}
 
});
}

exports

.start = start;
exports
.upload = upload;
exports
.show = show;

Также, надо преобразовать новый обработчик запроса в URL вида /show в файле index.js:

var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");var handle = {}
handle
["/"] = requestHandlers.start;
handle
["/start"] = requestHandlers.start;
handle
["/upload"] = requestHandlers.upload;
handle
["/show"] = requestHandlers.show;

server

.start(router.route, handle);

Перезапускаем сервер, открываем http://localhost:8888/show в браузере и видим картинку /tmp/test.png.

Хорошо. Всё, что нам надо теперь — это:

  • добавить поле для загрузки файлов в форму, находящуюся по адресу /start,
  • интегрировать node-formidable в обработчик запроса upload, чтобы сохранять загруженные файлы в /tmp/test.png,
  • внедрить загруженную картинку в HTML, отдаваемый по URL /upload.

Первый шаг — простой. Нам надо добавить тип кодировки multipart/form-data в нашу HTML-форму, удалить текстовое поле, добавить поле загрузки файла и поменять текст кнопки отправки формы на «Upload file».
Давайте просто сделаем это в файле requestHandlers.js:

var querystring = require("querystring"),
    fs
= require("fs");function start(response, postData) {
  console
.log("Request handler 'start' was called.");var body = '<html>'+
   
'<head>'+
   
'<meta http-equiv="Content-Type" '+
   
'content="text/html; charset=UTF-8" />'+
   
'</head>'+
   
'<body>'+
   
'<form action="/upload" enctype="multipart/form-data" '+
   
'method="post">'+
   
'<input type="file" name="upload">'+
   
'<input type="submit" value="Upload file" />'+
   
'</form>'+
   
'</body>'+
   
'</html>';

    response

.writeHead(200, {"Content-Type": "text/html"});
    response
.write(body);
    response
.end();
}function upload(response, postData) {
  console
.log("Request handler 'upload' was called.");
  response
.writeHead(200, {"Content-Type": "text/plain"});
  response
.write("You've sent the text: "+
  querystring
.parse(postData).text);
  response
.end();
}function show(response, postData) {
  console
.log("Request handler 'show' was called.");
  fs
.readFile("/tmp/test.png", "binary", function(error, file) {
   
if(error) {
      response
.writeHead(500, {"Content-Type": "text/plain"});
      response
.write(error + "n");
      response
.end();
   
} else {
      response
.writeHead(200, {"Content-Type": "image/png"});
      response
.write(file, "binary");
      response
.end();
   
}
 
});
}

exports

.start = start;
exports
.upload = upload;
exports
.show = show;

Замечательно. Следующий шаг — немного более сложный, конечно.
Первая проблема следующая: мы хотим обрабатывать загрузку файлов в нашем обработчике запроса upload, и тут надо будет передать объект request при вызове form.parse модуля node-formidable.

Но всё, что у нас есть — это объект response и массив postData.
Грустно.
Похоже, что придётся передавать каждый раз объект request из сервера в роутер и обработчик запроса.
Может быть, имеется более элегантное решение, но этот способ может делать работу уже сейчас.

Давайте полностью удалим всё, что касается postData в нашем сервере и обработчиках запроса — он нам не нужен для обработки загрузки файла и, мало того, — даже создает проблему: мы уже «поглотили» события data объекта request в сервере, а следовательно, form.parse, которому так же надо поглащать эти события, не сможет получить больше данных (потому что Node.js не буферизирует данные).

Начнём с server.js — удалим обработку postData и строку с request.setEncoding (node-formidable сам всё сделает) и передадим request в роутер:

var http = require("http");
var url = require("url");function start(route, handle) {
 
function onRequest(request, response) {
   
var pathname = url.parse(request.url).pathname;
    console
.log("Request for " + pathname + " received.");
    route
(handle, pathname, response, request);
 
}

  http

.createServer(onRequest).listen(8888);
  console
.log("Server has started.");
}

exports

.start = start;

Следующий — router.js — мы больше не передаём postData, а вместо этого передаём request:

function route(handle, pathname, response, request) {
  console
.log("About to route a request for " + pathname);
 
if (typeof handle[pathname] === 'function') {
    handle
[pathname](response, request);
 
} else {
    console
.log("No request handler found for " + pathname);
    response
.writeHead(404, {"Content-Type": "text/html"});
    response
.write("404 Not found");
    response
.end();
 
}
}

exports

.route = route;

Теперь объект request может быть использован в функции обработчика запроса upload.
node-formidable будет заниматься сохранением загруженного файла в локальный файл /tmp, но, конечно, мы сами должны сделать, чтобы этот файл переименовывался в /tmp/test.png.
Да, мы придерживаемся действительно простых вещей и принимаем, что могут загружаться только PNG-картинки.

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

Давайте добавим в requestHandlers.js код управления загрузкой файла и переименованием:

var querystring = require("querystring"),
	fs = require("fs"),
	formidable = require("formidable");

function start(response) {
  console.log("Request handler 'start' was called.");

  var body = '<html>'+
	'<head>'+
	'<meta http-equiv="Content-Type" '+
	'content="text/html; charset=UTF-8" />'+
	'</head>'+
	'<body>'+
	'<form action="/upload" enctype="multipart/form-data" '+
	'method="post">'+
	'<input type="file" name="upload" multiple="multiple">'+
	'<input type="submit" value="Upload file" />'+
	'</form>'+
	'</body>'+
	'</html>';

	response.writeHead(200, {"Content-Type": "text/html"});
	response.write(body);
	response.end();
}

function upload(response, request) {
  console.log("Request handler 'upload' was called.");

  var form = new formidable.IncomingForm();
  console.log("about to parse");
  form.parse(request, function(error, fields, files) {
	console.log("parsing done");

/* Возможна ошибка в Windows: попытка переименования уже существующего файла */
	fs.rename(files.upload.path, "/tmp/test.png", function(err) {
	  if (err) {
		fs.unlink("/tmp/test.png");
		fs.rename(files.upload.path, "/tmp/test.png");
	  }
	});
	response.writeHead(200, {"Content-Type": "text/html"});
	response.write("received image:<br/>");
	response.write("<img src='/show' />");
	response.end();
  });
}

function show(response) {
  console.log("Request handler 'show' was called.");
  fs.readFile("/tmp/test.png", "binary", function(error, file) {
	if(error) {
	  response.writeHead(500, {"Content-Type": "text/plain"});
	  response.write(error + "n");
	  response.end();
	} else {
	  response.writeHead(200, {"Content-Type": "image/png"});
	  response.write(file, "binary");
	  response.end();
	}
  });
}

exports.start = start;
exports.upload = upload;
exports.show = show;

Вот и всё. Перезапускаем сервер, и последний пункт нашей задачи реализован.
Выбираем локальную PNG-картинку с диска, загружаем на сервер и видим её на нашей веб-страничке.

Выводы и перспективы

Поздравляю, наша миссия выполнена!
Мы написали простое, уже полностью готовое web-приложение на Node.js.
Мы поговорили о server-side JavaScript, функциональном программировании, блокирующих и неблокирующих операциях, callback-ах, событиях, обычаях, внутренних и внешних модулях и о многом другом.

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

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

Хорошей новостью является то, что Node.js-сообщество — очень активное (думаю, даже гиперактивное от кофеина, но в хорошем смысле), имеется множество ресурсов, кроме этого, и множество мест, где можно получить ответы на ваши вопросы.
Node.js community wiki и the NodeCloud directory являются, возможно, лучшими отправными точками.

Автором было замечено множество комментариев в духе “я скачал/скачала Node.js, и что теперь?”. Этот урок отвечает на данный вопрос и объясняет новичку, с чего начать.

Очень много путаницы у новичков из-за непонимания того, что такое Node.js. И даже описание с официального сайта не помощник.

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

Node.js — это просто другой способ выполнять код на вашем компьютере. Это среда выполнения языка JavaScript.

Устанавливаем Node

Node.js легко установить. Достаточно зайти на страницу загрузки официального сайта.

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

После установки у вас появилась новая команда в командной строке “node”. Вы можете использовать Node двумя способами.

  • Первый — без аргументов: данная команда откроет интерактивный режим в командной строке, где вы можете исполнять код JavaScript.$ node > console.log(‘Hello World’); Hello World undefined

В этом примере я просто набрал “console.log(‘Hello World’);” и нажал на Enter. Node начнет выполнять, и мы увидим наше сообщение. Также он напишет “undefined”, потому что печатает возвращаемое значение, иconsole.log не возвращает ничего.   — Другой способ использования Node.js — это создание файла Javascript.

Итак, создаем файл:

hello.js

console.log('Hello World');

И сохраняем его в директорию, из которой будем запускать этот файл. Чтобы перейти достаточно в командной строке написать cd полное_название_директории (ну или можно использовать относительную адресацию, о которой можно почитать здесь.

Запускаем в командной строке:

$ node hello.js
Hello World

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

Файлы ввода/вывода с node.js

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

example_log.txt

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

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

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

my_parser.js

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

// Считываем содержание файла в память
fs.readFile('example_log.txt', function (err, logData) {
 
	// Если возникла ошибка, мы кидаем исключение
	// и программа заканчивается
	if (err) throw err;
 
	// logData это объект типа Buffer, переводим в строку
  		var text = logData.toString();
});

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

Теперь добавим парсировщик (он написан на чистом JavaScript).

my_parser.js

// загружаем модуль filesystem(fs)
var fs = require('fs');
 
// считываем содержание файла в память
fs.readFile('example_log.txt', function (err, logData) {
 
    // Если возникла ошибка, мы кидаем исключение
	// и программа заканчивается
    if (err) throw err;
 
    // logData имеет тип Buffer, конвертируем в строку
    var text = logData.toString();
 
var results = {};
 
// Разбиваем текст на массив из строчек
var lines = text.split('n');
 
lines.forEach(function(line) {
    var parts = line.split(' ');
    var letter = parts[1];
    var count = parseInt(parts[2]);
 
if(!results[letter]) {
      results[letter] = 0;
}
 
results[letter] += parseInt(count);
});
 
console.log(results);
// { A: 2, B: 14, C: 6 }
});

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

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

Асинхронные вызовы в node.js

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

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

Создание веб-сервера с помощью node.js

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

mywebserver.js

var http = require('http');
 
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello Worldn');
}).listen(8080);
 
console.log('Server running on port 8080.');

Ещё раз: это базовый веб-сервер, а не сервер с полным функционалом. Он не может обслуживать картинки или HTML-файлы. На самом деле, какой бы вы ни отправили запрос, он возвратит “Hello World”. Однако, вы можете запустить этот скрипт, перейти по адресу http://localhost:8080 в своем браузере и увидеть этот текст.

Вы уже могли заметить, что ваше приложение на node.js больше не останавливается. Всё потому, что вы создали веб-сервер, и ваше приложение на node.js отвечает на запросы до тех пор, пока вы сами его не остановите.

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

Модуль Express для node.js

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

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

Когда вы установите модуль, он будет положен в папку node_modules в директории вашего приложения. Теперь вы сможете использовать его функциональность так, как будто он встроен.

Давайте создадим базовый статический сервер с использованием Express.

mystaticfile_server.js

var express = require('express'),
    app = express();
 
app.use(express.static(__dirname + '/public'));
 
app.listen(8080);

Запускаем скрипт:

$ node mystaticfile_server.js

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

NPM

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

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

package.json

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

Файл package.json содержит обзор вашего приложения. Здесь много доступных полей, но представлен необходимый минимум. Секция “dependency” описывает название и версию модулей, которые вы бы хотели установить. В данном случае мы принимаем любую версию Express.3.3. В данной секции вы можете перечислить столько библиотек, сколько вам угодно.

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

Когда вы запустите команду, npm будет искать в текущей папке файл package.json. Если найдет, то установит каждую библиотеку из списка.

Организация кода в node.js

До сих пор мы использовали один файл, но так очень тяжело обслуживать проект. В большинстве приложений ваш код будет находиться в нескольких файлах. Здесь нет стандартной или принудительной организации того, какие файлы куда идут. Это не Rails. Вы делаете то, что вам хочется.

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

parser.js

// Конструктор обработчика
var Parser = function() {
 
};
 
// Обрабатывает заданный текст
Parser.prototype.parse = function(text) {
 
var results = {};
 
// Текст превращает в массив строчек
var lines = text.split('n');
 
lines.forEach(function(line) {
    var parts = line.split(' ');
    var letter = parts[1];
    var count = parseInt(parts[2]);
 
if(!results[letter]) {
    results[letter] = 0;
}
 
results[letter] += parseInt(count);
});
 
return results;
};
 
// Экспортирует конструктор типа Parser из этого модуля
module.exports = Parser;

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

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

// Require my new parser.js file.
var Parser = require('./parser');
 
// Load the fs (filesystem) module.
var fs = require('fs');
 
// Read the contents of the file into memory.
fs.readFile('example_log.txt', function (err, logData) {
 
// If an error occurred, throwing it will
  // display the exception and kill our app.
  if (err) throw err;
 
// logData is a Buffer, convert to string.
  var text = logData.toString();
 
// Create an instance of the Parser object.
  var parser = new Parser();
 
// Call the parse function.
  console.log(parser.parse(text));
  // { A: 2, B: 14, C: 6 }
});

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

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

Вывод

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

Мы хотим напомнить, что использование Node.js ограничено только вашей фантазией. Библиотеки ядра очень аккуратно разработаны и предоставляют любые инструменты для построения приложения. Скомбинировав все модули, доступные в npm, вы можете удивиться, насколько быстро можно строить крайне трудные и настолько же интересные приложения.

Другие статьи по теме

Подборка бесплатных ресурсов для изучения Node.js

Подборка материалов по JavaScript

Источник: Absolute Beginners Guide to Node.js

Искусство Node

Введение в Node.js

Статья предназначена тем, кто хотя бы немного знаком с:

  • языками программирования, например, JavaScript, Ruby, Python, Perl, и т.д.
    Если вы пока не являетесь программистом, наверное, проще будет начать с чтения
    «JavaScript для котиков».
  • Git и GitHub. Это инструменты для совместной работы с открытым кодом, которые
    широко используются членами сообщества Node.js для обмена модулями.
    Вам достаточно знать азы. Вот три отличных самоучителя для начинающих:
    1, 2, 3

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

Содержание

  • Учим Node.js в интерактивном режиме
  • Философия Node.js
  • Базовые модули
  • Колбеки
  • События
  • Потоки
  • Модули и пакетный менеджер Node.js
  • Разработка на стороне клиента с использованием npm
  • Выбираем инструменты правильно

Учим Node.js в интерактивном режиме

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

NodeSchool.io

NodeSchool.io — это подборка интерактивных мастер-классов,
бесплатных и с открытым кодом, в которых изложены принципы Node.js и не только.

Мы научим вас Node.js — вводный мастер-класс от NodeSchool.io. Это
набор задач по программированию, который поможет вам познакомиться с
наиболее распространенными паттернами Node. Он доступен в виде консольной
программы.

программа learnyounode

Её можно установить с помощью пакетного менеджера Node:

# install
npm install learnyounode -g

# start the menu
learnyounode

Философия Node

Node.js — это проект с открытым исходным кодом, разработанный, чтобы
помочь вам создавать программы на JavaScript, которые могли бы
взаимодействовать с сетями, файловыми системами или другими I/O источниками
ввода/вывода. Вот и всё! Это простая и стабильная I/O платформа, на основе
которой предлагается создавать свои модули.

Какими бывают источники ввода/вывода? Вот схема моего приложения, построенного
на Node, которая отображает многообразие источников:

серверная схема

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

Обычно создание подобных систем подразумевает одно из двух:

  • сложный код, но с супербыстрым результатом (как в случае с написанием
    веб-сервера с нуля на С)
  • простой код, однако не слишком быстрый/надежный результат (например,
    когда кто-нибудь пытается загрузить файл весом 5Гб и сервер падает)

Цель Node.js состоит в достижении золотой середины: относительная простота
понимания и использования в сочетании со скоростью работы в большинстве
случаев применения.

Node.js не является:

  • Веб-фреймворком (вроде Rails или Django, хотя его можно использовать для
    создания таких вещей)
  • Языком программирования (для него используется JavaScript, Node.js не является
    языком сам по себе)

Node.js — это нечто среднее. Он:

  • Создан чтобы быть простым и, следовательно, относительно лёгким для
    понимания и использования
  • Будет полезным для программ, предусматривающих операции ввода/вывода,
    которые должны быть быстрыми и выдерживать большое количество соединений

На более низком уровне Node.js можно описать как инструмент для написания программ
двух типов:

  • Сетевых программ, использующих веб-протоколы: HTTP, TCP, UDP, DNS и SSL
  • Программ, производящих чтение и запись данных в/из файловых систем или
    локальных процессов/памяти.

Что следует понимать под «программами, предусматривающими операции
ввода/вывода»?
Вот некоторые наиболее типичные I/O источники:

  • Базы данных (напр. MySQL, PostgreSQL, MongoDB, Redis, CouchDB)
  • API (напр. Twitter, Facebook, рush-уведомления Apple)
  • Соединения HTTP/WebSocket (от пользователей веб-приложения)
  • Файлы (редактор изображений и видео, интернет-радио)

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

Вот несколько занимательных вещей, которые можно легко сделать на Node,
благодаря её неблокирующей сущности:

  • Управление летающими квадрокоптерами
  • Написание IRC-ботов
  • Создание двуногих роботов, способных ходить

Базовые модули

Прежде всего я советовал бы вам установить Node.js на ваш компьютер. Самый простой
способ это сделать — зайти на nodejs.org и нажать Install.

У Node.js есть небольшая базовая группа модулей (их принято называть «ядро Node»),
представленных как открытые API, на основе которых следует писать программы. Для
работы с файловыми системами существует модуль fs, для сетей используются
такие модули как net (TCP), http и dgram (UDP).

В дополнение к fs и сетевым модулям, ядро Node.js содержит ряд других базовых
модулей. Есть модуль для асинхронной обработки DNS-запросов под названием dns,
модуль os для получения сведений об ОС, например, о расположении tmpdir,
модуль buffer для распределения бинарных участков памяти, несколько модулей
для анализа ссылок и путей (url, querystring, path) и т.д. Большинство
модулей, составляющих ядро, предназначены для обеспечения главного
предназначения Node: создания быстрых программ, взаимодействующих
с файловыми системами или сетями.

Node.js выполняет операции ввода/вывода с помощью колбеков,
событий, потоков и модулей. Если вы разберётесь с тем, как работают эти четыре
компонента, вы сможете взять любой модуль из ядра Node.js и более-менее чётко
понять как с ним взаимодействовать.

Колбеки

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

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

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

var myNumber = 1;
function addOne() { myNumber++ } // определение функции
addOne(); // выполнение функции
console.log(myNumber); // в консоль выводится 2

Здесь сначала определяется функция, а затем в следующей строке происходит её
вызов, без каких-либо отсрочек. Когда происходит вызов функции, она немедленно
прибавляет 1 к переменной, так что можно рассчитывать, что после вызова функции
значение переменной должно быть равно 2. Это то, чего мы ожидаем от синхронного
кода — он последовательно выполняется сверху вниз.

Однако в Node, в большинстве случаев, используется асинхронный код. Попробуем
считать число из файла под названием number.txt, используя Node:

var fs = require('fs'); // require является специальной функцией предусмотренной в Node
var myNumber = undefined; // мы пока не знаем значение переменной, так как оно хранится в файле
 
function addOne() {
  fs.readFile('number.txt', function doneReading(err, fileContents) {
    myNumber = parseInt(fileContents);
    myNumber++;
  })
}
 
addOne();
 
console.log(myNumber); // в консоль выводится undefined — эта строчка запускается перед выполнением readFile

Почему при выводе результата в консоль мы видим undefined? В этом
коде мы используем метод fs.readFile, который является асинхронным. Обычно все
взаимодействия с жёстким диском или сетью являются асинхронными. Если требуется
всего лишь получение доступа к памяти или выполнение каких-либо действий с
процессором, можно использовать синхронный подход. Причиной этому является то,
что операции ввода/вывода ооооочень и оооочень меееедленные. По приблизительным
оценкам, взаимодействие с жёстким диском примерно в 100,000 медленнее, чем
взаимодействие с памятью (напр. с оперативной памятью).

При запуске этой программы все функции объявляются немедленно, но не все
выполняются сразу. Это основополагающая вещь для понимания асинхронного
программирования. Когда происходит вызов addOne, она вызывает readFile и
переходит дальше к тому, что ещё может быть выполнено. Если выполнять нечего,
Node.js либо ждёт окончания текущих операций с файловыми системами/сетями или же
прекращает работу и выходит в командную строку.

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

Выше мы получили undefined потому, что нигде в коде не прописано, что команда
console.log должна подождать с выводом переменной до завершения работы
команды readFile.

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

Колбеки — это функции, которые могут быть выполнены позже. Ключом к пониманию
колбеков является осознание того, что они используются, когда время окончания
какой-либо асинхронной операции неизвестно, однако известно место её окончания —
последняя строчка асинхронной функции! Порядок сверху-вниз, в котором объявлены
функции, не играет особой роли, в отличие от их логической/иерархичной вложенности.
Сначала вы разбиваете код на функции, а затем используете колбеки для объявления,
что запуск одной функции зависит от окончания другой.

Метод fs.readFile, предлагаемый в Node, является асинхронным, и
иногда его выполнение затягивается на длительное время. Вот что он делает:
он обращается к операционной системе, которая, в свою очередь, обращается к
файловой системе, которая живёт на жёстком диске, вращающемся со скоростью выше
или ниже тысячи оборотов в минуту. Затем с помощью лазера считываются данные
и отсылаются назад в программу тем же путём. Вы передаёте readFile колбек,
который он вызовет после получения данных из файловой системы. Он помещает
полученные данные в javascript-переменную и вызывает колбек с этой переменной
в качестве аргумента. В этом случае переменная носит название fileContents,
так как в неё помещено содержимое прочитанного файла.

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

Давайте поместим нашу команду console.log в функцию и добавим её в код в
качестве колбека.

var fs = require('fs');
var myNumber = undefined;
 
function addOne(callback) {
  fs.readFile('number.txt', function doneReading(err, fileContents) {
    myNumber = parseInt(fileContents);
    myNumber++;
    callback();
  });
}
 
function logMyNumber() {
  console.log(myNumber);
}
 
addOne(logMyNumber);

Теперь функции logMyNumber можно передать аргумент, который станет переменной
callback внутри функции addOne. После завершения работы readFile будет
вызвана переменная callback (callback()). Так как вызываться могут только
функции, если попробовать вызвать что-либо кроме функции, мы получим ошибку.

Когда в JavaScript происходит вызов функции, код внутри этой функции немедленно
выполняется. В нашем случае будет выполнена команда вывода в консоль, поскольку
callback по сути является logMyNumber. Помните, что если просто объявить
функцию — она не будет выполнена. Для выполнения функцию нужно вызвать.

Чтобы проанализировать этот пример еще подробней, рассмотрим последовательность
событий, которые происходят при выполнении этой программы:

  1. Сначала код анализируется, а это означает, что если будут обнаружены
    синтаксические ошибки — программа работать не будет. На этом этапе
    объявляются четыре компонента: fs, myNumber, addOne и logMyNumber.
    Обратите внимание, что происходит только объявление, вызов каких-либо функций пока не производится.
  2. При выполнении последней строчки нашей программы, вызывается addOne,
    ей передается функция logMyNumber в качестве callback, которую нам
    нужно вызвать после завершения addOne. Это немедленно запускает асинхронную
    функцию fs.readFile. Эта часть программы занимает много времени.
  3. Так как больше ему заняться нечем, Node.js пребывает в режиме ожидания пока не
    завершится работа readFile. Если бы какие-нибудь задачи требовали выполнения в
    этот промежуток времени, Node.js занялся бы их выполнением.
  4. readFile заканчивает работу и вызывает колбек doneReading, который,
    в свою очередь, увеличивает число на единицу и немедленно вызывает колбек —
    logMyNumber, переданный в addOne.

Наверное, больше всего сбивает с толку то, что с функциями можно обращаться как
с простыми объектами, хранить их в переменных и передавать туда-сюда под разными
именами. Чтобы ваш код мог прочитать кто-нибудь кроме вас, важно давать
переменным простые и наглядные названия. В общем, если вы видите в программе на
Node.js переменную вроде callback или cb, можно предположить, что это колбек.

Возможно, вы слышали термины «событийно-ориентированное программирование» или
«событийный цикл». Они описывают процесс выполнения readFile. Сначала Node
запускает операцию readFile, затем ждёт пока readFile вышлет ему событие,
означающее её завершение. В процессе ожидания Node.js может проверить
состояние других процессов. У Node.js есть список операций, которые были запущены,
но от которых пока не получен ответ, он перебирает их снова и снова, проверяя не
были ли они завершены. После окончания работы они «обрабатываются»,
например, происходит запуск колбека, привязанных к завершению
их работы.

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

function addOne(thenRunThisFunction) {
  waitAMinute(function waitedAMinute() {
    thenRunThisFunction();
  });
}
 
addOne(function thisGetsRunAfterAddOneFinishes() {});

Представьте, что у вас есть три асинхронные функции a, b и c. Выполнение
каждой из них занимает минуту, и после завершения каждой происходит запуск
колбека (которому передается первый аргумент). Если вы хотите сказать Node.js
«запусти a, после её завершения выполни b, после завершения b запусти c»
это выглядело бы так:

a(function() {
  b(function() {
    c();
  });
});

При выполнении этого кода, немедленно запустится a, через минуту она закончит
работу и вызовет b, которая закончится еще через минуту и вызовет c, и,
наконец, 3 минуты спустя после начала выполнения Node.js завершит обработку кода,
так как больше задач не останется. Конечно, можно было придумать более
изящные способы записать этот пример, однако смысл состоит в том, что если у вас
есть код, который должен подождать, пока будет завершено выполнение другого
асинхронного кода, то эту зависимость следует выразить, поместив код в колбек.

Структура Node.js требует от разработчика нелинейного мышления. Взгляните на этот
список операций:

  1. чтение файла
  2. обработка файла

Если бы вам пришлось превратить его в код, у вас получилось бы следующее:

var file = readFile();
processFile(file);

Такой линейный (пошаговый, упорядоченный) код не соответствует тому, как
работает Node. Если начать обработку такого кода, readFile и processFile
выполнялись бы одновременно. Это бессмысленно, так как выполнение readFile
займет много времени. Вместо этого вам нужно указать, что функция processFile
должна быть запущена после завершения readFile. Именно для этого и существуют
колбеки. Благодаря особенностям JavaScript, эту зависимость можно записать
несколькими разными способами:

var fs = require('fs');
fs.readFile('movie.mp4', finishedReading);
 
function finishedReading(error, movieData) {
  if (error) return console.error(error);
  // выполнение действий с movieData
}

Однако можно написать и такую структуру кода, и он все так же будет работать:

var fs = require('fs');
 
function finishedReading(error, movieData) {
  if (error) return console.error(error);
  // выполнение действий с movieData
}
 
fs.readFile('movie.mp4', finishedReading);

Или даже так:

var fs = require('fs');
 
fs.readFile('movie.mp4', function finishedReading(error, movieData) {
  if (error) return console.error(error);
  // выполнение действий с movieData
});

События

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

События, более известные как паттерн «наблюдатель» или «издатель/подписчик»,
являются широко распространённым паттерном в программировании. В то время, как колбеки
представляют собой связь «один к одному» между тем, что ожидает колбек и тем,
что его вызывает, события представляют собою такую же связь, только между многими API.

Проще всего представить себе принцип работы событий как подписку на нечто. Они
позволяют указать «когда Х, сделай Y», тогда как простые колбеки указывают
только «сделай Х, затем Y».

Вот несколько типичных случаев использования событий вместо колбеков:

  • Чат, в котором вы хотите транслировать сообщения для многих пользователей
  • Игровой сервер, которому нужно знать когда новые игроки присоединяются,
    отсоединяются, двигаются, стреляют и прыгают
  • Игровой движок, в котором вы хотите предусмотреть для разработчиков игры
    возможность подписываться на события вроде .on('jump', function() {})
  • Веб-сервер низкого уровня, на котором нужно предоставить API для простого
    подключения к происходящим событиям вроде .on('incomingRequest') или
    .on('serverError')

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

var chatClient = require('my-chat-client');
 
function onConnect() {
  // подтверждение подключения в интерфейсе
}
 
function onConnectionError(error) {
  // уведомление пользователя об ошибке
}
 
function onDisconnect() {
 // уведомление пользователя об отключении
}
 
function onMessage(message) {
 // отображение в интерфейсе сообщения из чата
}
 
chatClient.connect(
  'http://mychatserver.com',
  onConnect,
  onConnectionError,
  onDisconnect,
  onMessage
);

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

var chatClient = require('my-chat-client').connect();
 
chatClient.on('connect', function() {
  // подтверждение подключения в интерфейсе
});
 
chatClient.on('connectionError', function() {
  // уведомление пользователя об ошибке
});
 
chatClient.on('disconnect', function() {
  // уведомление пользователя об отключении
});
 
chatClient.on('message', function() {
  // отображение в интерфейсе сообщения из чата
});

Это похоже на способ с использованием колбека, однако добавлен метод .on,
который подписывает колбек на событие. Это значит, что вы можете выбирать
на какие события в chatClient нужно подписаться. Также можно подписаться
на одно и то же событие несколько раз, используя разные колбеки:

var chatClient = require('my-chat-client').connect();
chatClient.on('message', logMessage);
chatClient.on('message', storeMessage);
 
function logMessage(message) {
  console.log(message);
}
 
function storeMessage(message) {
  myDatabase.save(message);
}

Потоки

На начальной стадии существования Node.js для файловых систем и сетевых API
использовались разные подходы к обработке потоковых операций ввода/вывода.
Например, для файлов в файловых системах применялись так называемые «файловые
дескрипторы», соответственно, модуль fs был наделён дополнительной логикой,
позволяющей их отслеживать, в то время, как для сетевых модулей такая концепция
не использовалась. Несмотря на подобные незначительные различия в семантике, на
более глубоком уровне считывания и вывода данных у обеих групп кода большая
часть функционального наполнения дублировалась. Команда разработчиков Node
поняла, что не стоит всё усложнять необходимостью изучать два набора
семантических правил для выполнения одинаковых действий, и разработала
новый API под названием Stream — и для сетей, и для файловых систем.

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

Для изучения потоков в Node.js есть два отличных ресурса. Один из них —
stream-adventure, второй — справочный ресурс под названием «Справочник по
потокам».

Справочник по потокам

Справочник по потокам — это руководство, похожее на текущее, которое
содержит ссылки на всё, что вам следует знать о потоках.

stream-handbook

Модули

Ядро Node.js состоит примерно из двух дюжин модулей, некоторые из них более
низкоуровневые, такие как events и stream, другие — более высокоуровневые,
такие как http и crypto.

Такая структура была придумана специально. Ядро Node.js должно быть небольшим, а
модули, его составляющие, должны являться кросс-платформенными инструментами для
работы со всеми распространёнными протоколами и форматами ввода/вывода.

Для всего остального существует пакетный менеджер Node. Кто угодно может
создать новый модуль Nodе с дополнительными функциональными возможностями и
добавить его в npm. На момент написания этой статьи npm насчитывает
34,000 модулей.

Как найти модуль

Представьте, что вам нужно переконвертировать файлы PDF в TXT. Начать лучше
всего с команды npm search pdf:

поиск pdf

Результатов масса! npm довольно популярен, и в большинстве случаев
вы сможете найти несколько потенциальных решений. Если пройтись по модулям и
сократить количество результатов (отфильтровав, например, модули
для генерации PDF), получим следующие:

  • hummus — модуль управления pdf на с++
  • mimeograph — api на основе совмещения инструментов (poppler, tesseract,
    imagemagick и др.)
  • pdftotextjs — оболочка для утилиты pdftotext
  • pdf-text-extract — ещё одна оболочка для pdftotext
  • pdf-extract — оболочка для pdftotext, pdftk, tesseract, ghostscript
  • pdfutils — оболочка для библиотеки poppler
  • scissors — оболочка для pdftk, ghostscript с api высокого уровня
  • textract — оболочка для pdftotext
  • pdfiijs — конвертер pdf в инвертированный индекс с использованием
    textiijs и poppler
  • pdf2json — конвертер pdf в json на чистом js

Функциональные возможности многих модулей пересекаются, однако представляют
альтернативные API, и большинство из них требует установки внешних зависимостей
(таких как apt-get install poppler).

Вот несколько разных способов сравнивать модули:

  • pdf2json является единственным написанным на чистом JavaScript, что делает
    его самым простым в установке, особенно на маломощных устройствах, вроде
    одноплатного компьютера Raspberry Pi или на Windows, где внутренний код может
    быть не кроссплатформенным.
  • каждый из модулей вроде mimeograph, hummus и pdf-extract объединяют в
    себе несколько модулей низшего уровня для предоставления высокоуровневого API
  • множество модулей является надстройками для pdftotext/poppler, консольных
    инструментов unix

Давайте сравним pdftotextjs и pdf-text-extract, они оба являются
обертками для pdftotext.

pdf-модули

Оба модуля:

  • были обновлены сравнительно недавно
  • имеют собственные репозитории на github (это очень важно!)
  • имеют описания README
  • еженедельно устанавливаются некоторым количеством пользователей
  • имеют свободную лицензию (кто-угодно может их использовать)

По package.json и статистике модуля трудно понять какой
из них лучше выбрать. Давайте сравним описания в README:

pdf-описания

У обоих простые описания, указан статус сборки, есть инструкции по установке,
понятные примеры и инструкции для проведения тестирования. Отлично! Однако какой
же выбрать? Давайте сравним код:

pdf-код

pdftotextjs состоит из около 110 строчек кода, а pdf-text-extract — из 40,
однако, по сути, оба сводятся к следующей строчке:

var child = shell.exec('pdftotext ' + self.options.additional.join(' '));

Делает ли это один из них лучше другого? Трудно сказать. Важно, собственно,
вчитаться в код и сделать свой собственный вывод. Когда вы встречаете
удобные и полезные модули, используйте npm star modulename, чтобы оставить в
пакетном менеджере свой отзыв о модулях, которые вам понравились.

Организация процесса разработки с использованием модулей

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

Многие пакетные менеджеры устанавливают всё глобально. Например, если выполнить
команду apt-get install couchdb в Debian Linux, она попытается установить
последнюю стабильную версию CouchDB. Если вы хотите установить CouchDB как
зависимость для другой программы, и эта программа требует более раннюю версию
CouchDB, вам придётся деинсталлировать более новую версию CouchDB и затем
установить более старую. Установить обе версии нельзя, так как Debian умеет
устанавливать что-либо только в одном месте.

Так обстоят дела не только в Debian. Точно так же работает большинство пакетных
менеджеров для различных языков программирования. Чтобы решить проблему с
глобальной установкой зависимостей, описанную выше, было разработано виртуальное
окружение, такое как virtualenv для Python и bundler для Ruby. Они
разбивают ваше окружение на множество виртуальных, по одному на каждый проект,
однако внутри виртуального окружения зависимости устанавливаются всё так же
глобально. Виртуальные окружения не всегда решают проблему, иногда они её
приумножают, добавляя новые уровни сложности.

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

Как работает require

Когда вы в Node.js вызываете require('some_module'), происходит следующее:

  1. Если в текущей папке есть файл с названием some_module.js, Node.js его
    загрузит, в противном случае:
  2. Node.js проверит текущую папку на наличие папки node_modules с папкой
    some_module внутри неё
  3. Если он её не найдет, он поднимется на одну папку выше и повторит шаг 2

Этот цикл повторяется, пока Node.js не достигнет корневой папки файловой системы,
после чего он проверит наличие папок с глобальными модулями (например,
/usr/local/node_modules на Mac OS) и если some_module опять не будет найден,
он сгенерирует исключение.

Вот визуальный пример:

модули-01

Когда текущей рабочей директорией является subsubfolder и происходит вызов
require('foo'), Node.js ищет папку с названием subsubsubfolder/node_modules. В
этом случае он её не найдет, так как папка по ошибке названа my_modules. Затем
Node.js поднимается на одну папку выше и повторяет попытку, то есть он ищет
subfolder_B/node_modules, которая также не существует. Третья попытка, тем не
менее, оказывается удачной, поскольку folder/node_modules существует и
содержит папку с названием foo внутри. Если бы foo в ней не было, Node
продолжил бы поиск вверх по дереву директорий.

Обратите внимание, что если бы Node.js был вызван в папке subfolder_B, он ни за
что бы не нашел subfolder_A/node_modules, так как он может увидеть
folder/node_modules только по пути вверх по дереву директорий.

Одним из преимуществ подхода пакетного менеджера Node.js является то, что модули
могут устанавливать зависимые модули конкретных рабочих версий. В данном случае
очень популярен модуль foo — он установлен трижды, по одному в папке каждого
родительского модуля. Причиной может быть то, что для каждого
модуля требуется другая версия foo, например для folder нужен
foo@0.0.1, для subfolder_Afoo@0.2.1 и т.д.

Вот что произойдет, если исправить ошибку в названии папки с my_modules на
более правильное node_modules:

модули-02

Чтобы проверить, какой именно модуль будет загружен Node, можно использовать
команду require.resolve('some_module'), которая отобразит путь к модулю,
найденному Node.js в процессе прохода вверх по дереву директорий.
require.resolve может пригодиться для перепроверки того, что будет загружен
именно тот модуль, который вы ожидаете. Иногда оказывается, что существует ещё
одна версия того же модуля ближе к текущей рабочей директории, чем тот, который
вы хотели бы загрузить.

Как написать модуль

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

Наипростейший модуль из возможных

Модули Node.js исключительно мало весят. Вот один из наипростейших модулей:

package.json:

{
  "name": "number-one",
  "version": "1.0.0"
}

index.js:

По умолчанию Node.js пытается загрузить module/index.js, когда вы запрашиваете
require('module'). Никакое другое имя работать не будет, если вы не пропишите
путь к нему в package.json в поле main.

Поместите оба файла в папку number-one (id в package.json должно
соответствовать названию папки), и вы получите работающий Node-модуль.

Вызов функции require('number-one') возвращает то значение, которое указано
для module.exports внутри модуля:

простой модуль

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

mkdir my_module
cd my_module
git init
git remote add git@github.com:yourusername/my_module.git
npm init

Выполнение npm init создаст валидный package.json и, если вы запустите его
в существующем репозитории git, он также автоматически добавит в
package.json поле repositories!

Добавление зависимостей

Модуль может содержать перечень каких-либо других модулей из npm
или GitHub в поле dependencies в package.json. Чтобы установить модуль
request как новую зависимость и автоматически добавить его в package.json,
выполните эту команду в корневой директории вашего модуля:

npm install --save request

Это установит копию request в ближайшую папку node_modules и сделает
package.json примерно таким:

{
  "id": "number-one",
  "version": "1.0.0",
  "dependencies": {
    "request": "~2.22.0"
  }
}

По умолчанию, npm install установит последнюю официальную версию модуля.

Разработка на стороне клиента с использованием npm

Существует популярное заблуждение, что, поскольку в названии npm указано слово
«Node», он должен использоваться только для JS модулей на стороне сервера. Это
абсолютно не верно! Название пакетного менеджера Node.js подразумевает, что он
отвечает за управление модулями, которые Node.js упаковывает для вас в пакеты.
Модули сами по себе могут быть какими угодно — они всего лишь представляют из
себя папку с файлами, упакованную в архив .tar.gz, и файл package.json, в
котором указана версия модуля и перечень модулей, являющихся его зависимостями
(а также номера их версий, чтобы рабочие версии устанавливались автоматически).
Зависимости модулей являются обычными модулями, которые также могут иметь
зависимости — и так до бесконечности.

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

Чтобы попробовать npm в браузере, используйте RequireBin, приложение,
созданное мной на основе Browserify-CDN, в основе которого лежит
browserify, но для вывода используется HTTP (вместо командной строки, которая
обычно используется для browserify).

Скопируйте этот код в RequireBin и нажмите кнопку предварительного просмотра:

var reverse = require('ascii-art-reverse');
 
// делает HTML консоль видимой
require('console-log').show(true);
 
var coolbear =
  "    ('-^-/')  n" +
  "    `o__o' ]  n" +
  "    (_Y_) _/  n" +
  "  _..`--'-.`, n" +
  " (__)_,--(__) n" +
  "     7:   ; 1 n" +
  "   _/,`-.-' : n" +
  "  (_,)-~~(_,) n";
 
setInterval(function() { console.log(coolbear) }, 1000);
 
setTimeout(function() {
  setInterval(function() { console.log(reverse(coolbear)) }, 1000);
}, 500);

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

requirebin

Выбираем инструменты правильно

Как любой хороший инструмент, Node.js лучше всего подходит для конкретного
набора задач. Например, Rails, популярный веб-фреймворк, идеально подходит для
сложной бизнес-логики, т.е. использования кода для представления реальных
бизнес объектов, вроде счетов, ссуд и оборотного капитала. Хотя создание
подобных вещей с помощью Node.js является технически возможным, без проблем не
обойдётся, так как Node.js придуман для решения проблем ввода/вывода и не слишком
подходит для использования в сфере «бизнес-логики». Каждый инструмент
предназначен для решения своих задач. Надеюсь, это руководство поможет вам
обрести интуитивное понимание сильных сторон Node.js и того, в каких случаях он
может быть вам полезен.

Что не входит в компетенцию Node?

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

Веб-фреймворки

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

Языковой синтаксис

Node.js использует JavaScript без каких-либо изменений. Феликс Гейзендорфер (Felix
Geisendorfer) составил хорошее описание «стиля Node» здесь.

Языковые абстракции

При любой возможности Node.js использует самый простой из доступных способов
выполнить задачу. Чем навороченнее ваш JavaScript, тем больше сложностей и
компромиссов вам приходится использовать. Программирование — непростое занятие,
особенно когда речь идёт о JS, у которого на каждую проблему по 1000 решений.
Именно поэтому Node.js всегда пытается выбрать самое простое и универсальное из
них. Если вы заняты задачей, для которой требуется сложное решение, и недовольны
«простенькими вариантами», которые предлагает Node, вы можете без проблем решить
её для своего приложения или модуля, используя любые абстракции на ваш вкус.

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

  • Их сложнее использовать, чем колбеки
  • Их можно установить ввиде пакета из npm

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

fs.readFile('movie.mp4')
  .then(function(data) {
    // проведение действий с данными
  })
  .error(function(error) {
    // обработка ошибки
  });

Это всё усложняет, и не каждому такое понравится. Вместо двух отдельных функций
Node.js использует один-единственный колбек. Он следует таким
правилам:

  • Если ошибка не произошла, null передаётся в качестве первого аргумента
  • Когда происходит ошибка, она передаётся в качестве первого аргумента
  • Остальными аргументами может быть что-угодно (обычно это данные или отклики,
    так как Node.js отвечает, в основном, за чтение или запись)

Отсюда стиль колбеков Node:

fs.readFile('movie.mp4', function(err, data) {
  // обработка ошибки, выполнение действий с данными
});

Потоки исполнения/волокна/достижение параллельности без использования событий

Примечание: если эти названия ни о чём вам не говорят, возможно, вам будет проще
освоить Node, так как избавление от знаний требует столько же усилий, сколько их
получение.

Node.js использует потоки исполнения для ускорения процессов, но не отображает их
пользователю. Если вы технически продвинутый пользователь, интересующийся почему
Node.js был реализован именно таким образом, вам 100% следует почитать о структуре
библиотеки libuv, I/O-слое C++, на котором построен Node.

Логотип компании «Одноклассники»

Статья переведена благодаря спонсорской поддержке компании «Одноклассники».

Понравилась статья? Поделить с друзьями:
  • Весы напольные scarlett sc bs33ed82 инструкция
  • Уколы б12 цена в ампулах инструкция по применению взрослым
  • Налбуфин инструкция по применению цена отзывы аналоги цена
  • Налбуфин инструкция по применению цена отзывы аналоги цена
  • Нипи петон руководство