Примечание. Вы можете получить версию этого руководства в формате PDF, ePub или Mobi для более удобного использования или чтения на Kindle или планшете.

Оглавление

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

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

Обзор

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

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

Node.js запускает движок JavaScript V8, ядро ​​Google Chrome, вне браузера. Node.js может использовать работу инженеров, которые сделали (и будут продолжать делать) быстродействующую среду выполнения JavaScript в Chrome, и это позволяет 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 с флагами.

Имеет огромное количество библиотек

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

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

Самый распространенный пример Hello World of 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 World\n')
})
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 событие, предоставляющее два объекта: запрос (объект http.IncomingMessage) и ответ (объект http.ServerResponse).

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

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

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

В этом случае с:

res.statusCode = 200

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

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

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

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

res.end('Hello World\n')

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

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

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

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

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

Взгляд на историю Node.js с 2009 года по сегодняшний день

Вы не поверите, но Node.js всего 9 лет.

Для сравнения, JavaScript исполнилось 23 года, а Интернету, каким мы его знаем (после появления Mosaic), 25 лет.

9 лет - такой небольшой срок для технологии, но, похоже, Node.js существует всегда.

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

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

Немного истории

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

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

Одним из ключевых факторов, которые привели к развитию Node.js, было время. Несколько лет назад JavaScript начал рассматриваться как серьезный язык благодаря приложениям «Web 2.0», которые показали миру, каким может быть современный опыт в Интернете (например, Google Maps или GMail).

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

Но, конечно, Node.js популярен не только из-за чистой удачи или времени. Он представил много новаторских идей о том, как программировать на JavaScript на сервере.

2009

Родился Node.js

Создана первая форма npm

2010

Экспресс родился

Socket.io родился

2011

npm Hit 1.0

Крупные компании начинают переходить на Node: LinkedIn, Uber.

Родился Хапи

2012

Принятие продолжается очень быстро

2013

Первая крупная блог-платформа с использованием Node.js: Ghost

Коа родился

2014

Большая драма: IO.js - это основной форк Node.js, целью которого является внедрение поддержки ES6 и ускорение работы.

2015

Рождение Node.js Foundation.

IO.js снова объединяется с Node.js

npm представляет частные модули

Узел 4 (ранее не было выпущено 1, 2, 3 версий)

2016

Инцидент с левой панелью

Пряжа рождается: Узел 6

2017

npm больше фокусируется на безопасности: Node 8

HTTP / 2

V8 представляет Node в своем пакете тестирования, официально делая Node мишенью для движка JavaScript в дополнение к Chrome.

3 миллиарда загрузок в минуту каждую неделю

2018

Узел 10

Модули ЭС.

Экспериментальная поддержка mjs

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

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

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

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

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

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

brew install node

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

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

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

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

Какой объем JavaScript вам нужно знать, чтобы использовать Node.js?

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

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

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

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

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

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

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

  • Асинхронное программирование и обратные вызовы
  • Таймеры
  • Обещания
  • Асинхронный и ожидающий
  • Закрытие
  • Цикл событий

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

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

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

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

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

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

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

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

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

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

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

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

Это означает, что вы можете написать весь современный JavaScript ES6–7–8–9, который поддерживает ваша версия 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:

и многие другие тоже существуют.

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

В поисках производительности

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

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

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

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

Компиляция

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

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

JavScript внутренне компилируется V8 с JIT-компиляцией для ускорения выполнения.

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

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

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

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

Есть несколько способов закрыть приложение Node.js.

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

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

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

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

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

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

process.exit(1)

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

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

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

process.exitCode = 1

и когда программа позже завершится, 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!')
})
app.listen(3000, () => console.log('Server ready'))
process.on('SIGTERM', () => {
  app.close(() => {
    console.log('Process terminated')
  })
})

Что такое сигналы? Сигналы - это система связи интерфейса переносимой операционной системы (POSIX): уведомление, отправляемое процессу, чтобы уведомить его о произошедшем событии.

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

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

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

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

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

Как читать переменные среды из Node.js

Базовый модуль process Node предоставляет envproperty, в котором размещаются все переменные среды, которые были установлены в момент запуска процесса.

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

process.env.NODE_ENV // "development"

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

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

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

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

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

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

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

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

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

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

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

Еще одна услуга, которую вы можете использовать - localtunnel.

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

Сбой

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

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

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

Codepen

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

Бессерверный

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

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

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

PAAS

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

Zeit Now

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

Нанобокс

Нанобокс

Heroku

Heroku - потрясающая платформа.

Это отличная статья о том, как начать работу с Node.js на Heroku.

Microsoft Azure

Azure - это предложение Microsoft Cloud.

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

Облачная платформа Google

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

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

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

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

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

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

Оголенный метал

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

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

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

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

node script.js

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

node

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

❯ node
>

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

Совет: если вы не знаете, как открыть терминал, введите в Google «Как открыть терминал в‹ вашей операционной системе ›».

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

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

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

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

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

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

Замечательная особенность REPL в том, что он интерактивен.

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

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

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

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

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

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

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

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

Команды точки

В 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 app.js

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

Например:

node app.js flavio

or

node app.js name=flavio

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

Вы получаете его с помощью объекта process, встроенного в Node.js.

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

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

Второй элемент - это полный путь к исполняемому файлу.

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

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

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

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

const args = process.argv.slice(2)

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

node app.js flavio

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

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

В этом случае:

node app.js name=flavio

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

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.

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

Раскрасьте вывод

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

Пример:

console.log('\x1b[33m%s\x1b[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 интерактивной?

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()
})

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

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

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

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

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

Самый простой способ - использовать пакет readline-sync, который очень похож по 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, но если вы планируете вывести ввод командной строки на новый уровень, Inquirer.js - оптимальный выбор.

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

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

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

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

Если вы хотите импортировать то, что используете:

const library = require('./library')

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

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

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

Это то, что позволяет нам module.exports API, предлагаемый module системой.

Когда вы назначаете объект или функцию как новое свойство 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

or

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

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

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

Введение в npm

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

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

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

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

Загрузки

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

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

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

npm install

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

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

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

npm install <package-name>

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

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

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

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

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

npm update

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

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

npm update <package-name>

Управление версиями

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

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

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

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

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

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

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

npm <task-name>

Например:

{
  "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 (или пряжи), вы можете выполнить 2 типа установки:

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

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

npm install lodash

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

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

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

npm install -g lodash

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

Где именно?

Команда npm root -g сообщит вам, где это точное местоположение на вашем компьютере.

В macOS или Linux это местоположение может быть /usr/local/lib/node_modules. В Windows это может быть C:\Users\YOU\AppData\Roaming\npm\node_modules

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

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

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

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

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

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

npm install lodash

Пакет будет установлен в локальной node_modules папке.

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

const _ = require('lodash)

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

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

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

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

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

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

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

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

npx cowsay

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

Руководство по package.json

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

Если вы работаете с JavaScript или когда-либо взаимодействовали с проектом JavaScript, Node.js или интерфейсным проектом, вы наверняка встречали файл 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 определяет набор сценариев узлов, которые вы можете запустить
  • dependencies устанавливает список npm пакетов, установленных как зависимости
  • devDependencies устанавливает список npm пакетов, установленных в качестве зависимостей разработки
  • engines устанавливает, на каких версиях Node работает этот пакет / приложение.
  • browserslist используется, чтобы указать, какие браузеры (и их версии) вы хотите поддерживать.

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

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

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

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

name

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

Пример:

"name": "test-project"

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

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

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

author

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

Пример:

{
  "author": "Flavio Copes <[email protected]> (https://flaviocopes.com)"
}

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

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

contributors

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

Пример:

{
  "contributors": [
    "Flavio Copes <[email protected]> (https://flaviocopes.com)"
  ]
}

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

{
  "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

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

Пример:

"version": "1.0.0"

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

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

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

license

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

Пример:

"license": "MIT"

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

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

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

Пример:

"main": "src/main.js"

private

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

Пример:

"private": true

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, установленных в качестве зависимостей разработки.

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

При установке пакета с использованием 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 и другие инструменты, чтобы добавить только полифиллы и резервные варианты, необходимые для браузеров, на которые вы нацелены.

Пример:

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

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

Свойства, зависящие от команды

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

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

Версии пакета

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

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

Учитывая, что при использовании semver (семантическое управление версиями) все версии имеют 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 автоматически создается при установке пакетов узлов.

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

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

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

Это решает очень специфическую проблему, которая осталась нерешенной package.json. В package.json вы можете указать, какие версии вы хотите обновить (патч или минор), используя нотацию 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+BTTDkM
Ajufp+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-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
      "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, включая их зависимости:

npm list

Пример:

❯ npm list
/Users/flavio/dev/node/cowsay
└─┬ [email protected]
  ├── [email protected]
  ├─┬ [email protected]
  │ ├── [email protected]
  │ └── [email protected]
  ├─┬ [email protected]
  │ ├── [email protected]
  │ └─┬ [email protected]
  │   └── [email protected]
  └── [email protected]

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

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

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

❯ npm list --depth=0
/Users/flavio/dev/node/cowsay
└── [email protected]

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

❯ npm list cowsay
/Users/flavio/dev/node/cowsay
└── [email protected]

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

❯ npm list minimist
/Users/flavio/dev/node/cowsay
└─┬ [email protected]
  └─┬ [email protected]
    └── [email protected]

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

❯ npm view cowsay version
1.3.1

Установите старую версию пакета npm

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

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

npm install <package>@<version>

Пример:

npm install cowsay

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

Установите версию 1.2.0 с:

npm install [email protected]

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

npm install -g [email protected]

Вам также может быть интересно перечислить все предыдущие версии пакета. Вы можете сделать это с помощью 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+0F9eLjzRnOHzVAYeIYFF5po5NjRrgefnRMQ==",
      "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.

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

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

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

npm install -g npm-check-updates

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

ncu -u

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

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

npm update

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

npm install

Семантическое управление версиями с использованием npm

Семантическое управление версиями - это соглашение, используемое для придания значения версиям.

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

Концепция семантического управления версиями проста: все версии имеют 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_modules), запустите:

npm uninstall <package-name>

Используя флаг -S или --save, эта операция также удалит ссылку в package.json файле.

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

npm uninstall -S <package-name>
npm uninstall -D <package-name>

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

npm uninstall -g <package-name>

Пример:

npm uninstall -g webpack

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

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

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

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

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

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

require('package-name')

Итак, когда следует так или иначе устанавливать?

Как правило, все пакеты следует устанавливать локально.

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

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

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

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

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

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

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

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

npm list -g --depth 0

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

зависимости npm и devDependencies

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

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

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

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

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

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

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

Средство выполнения пакетов npx Node

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

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

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

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 без локальной установки:

npx cowsay "Hello"

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

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

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

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

Запустите код, используя другую версию Node.js

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

npx node@6 -v #v6.14.3
npx node@8 -v #v8.11.3

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

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

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

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

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

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

Цикл событий

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

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

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

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

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

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

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

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

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

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

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

Стек вызовов

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

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

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

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

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

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

Возьмем пример:

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

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

foo
bar
baz

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

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

На данный момент стек вызовов выглядит так:

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

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

Выполнение функции очереди

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

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

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

Возьмем этот пример:

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

Этот код печатает, может быть, неожиданно:

foo
baz
bar

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

На данный момент стек вызовов выглядит так:

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

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

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

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

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

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

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

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

ECMAScript 2015 представил концепцию очереди заданий, которая используется обещаниями (также представленная в 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

В этом большая разница между обещаниями (и async/await, который основан на обещаниях) и простыми старыми асинхронными функциями через 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 ')

напечатает before after.

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

Некоторые браузеры (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(function() {
  if (App.somethingIWait === 'arrived') {
    clearInterval(interval)
    // otherwise do things
  }
}, 100)

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

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

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

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

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

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

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

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

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

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 представил среду неблокирующего ввода-вывода, чтобы расширить эту концепцию на доступ к файлам, сетевые вызовы и так далее.

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

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

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

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)
  })
})

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

Как решить эту проблему?

Альтернативы обратным вызовам

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

  • Обещания (ES6)
  • Асинхронный / Ожидание (ES8)

Обещания

Обещания - это один из способов справиться с асинхронным кодом в JavaScript без написания слишком большого количества обратных вызовов в вашем коде.

Введение в обещания

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

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

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

Коротко о том, как работают обещания

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

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

Какой JS API использует обещания?

Помимо вашего собственного кода и кода библиотек, обещания используются стандартными современными веб-API, такими как:

Маловероятно, что в современном 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, слой поверх XMLHttpRequest API, который мы можем использовать для получения ресурса и постановки в очередь цепочки обещаний для выполнения при извлечении ресурса.

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() возвращает ответ, который имеет множество свойств, в том числе и в тех, на которые мы ссылаемся:

  • 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 также позволяет:

Promise.all([f1, f2]).then(([res1, res2]) => {
    console.log('Results', res1, res2)
})

Конечно, вы не ограничены в использовании fetch, любое обещание можно выполнить.

Promise.race()

Promise.race() запускается, когда разрешается первое из переданных ему обещаний, и он запускает прикрепленный обратный вызов только один раз, с разрешенным результатом первого обещания.

Пример:

const first = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'first')
})
const second = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'second')
})
Promise.race([first, second]).then((result) => {
  console.log(result) // second
})

Распространенная ошибка: Uncaught TypeError: undefined не является обещанием

Если вы получаете сообщение об ошибке Uncaught TypeError: undefined is not a promise в консоли, убедитесь, что вы используете new Promise(), а не просто Promise().

Асинхронный и ожидающий

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

JavaScript за очень короткое время эволюционировал от обратных вызовов до обещаний (ES2015), а с ES2017 асинхронный JavaScript стал еще проще с синтаксисом async / await.

Асинхронные функции представляют собой комбинацию обещаний и генераторов и, по сути, представляют собой абстракцию более высокого уровня по сравнению с обещаниями. Повторяю: async/await построен на обещаниях.

Почему были введены async / await?

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

Когда Promises были представлены в ES2015, они предназначались для решения проблемы с асинхронным кодом, и они это сделали, но за 2 года, которые разделяли ES2015 и ES2017, стало ясно, что обещания не могут быть окончательным решением.

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

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

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

Как это работает

Функция 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 => response.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 user.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').EventEmitter()

Этот объект предоставляет, среди многих других, методы on и emit.

  • emit используется для запуска события
  • on используется для добавления функции обратного вызова, которая будет выполняться при срабатывании события.

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

eventEmitter.on('start', () => {
  console.log('started')
})

Когда мы бежим:

eventEmitter.emit('start')

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

Вы можете передать аргументы обработчику событий, передав их в качестве дополнительных аргументов в 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 и нажимаете клавишу ВВОД?».

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

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

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

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

Протокол 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, используя системный вызов gethostbyname POSIX для получения информации о хосте.

gethostbyname

gethostbyname сначала просматривает файл локальных хостов, который в macOS или Linux находится в /etc/hosts, чтобы узнать, предоставляет ли система информацию локально.

Если это не дает никакой информации о домене, система делает запрос к DNS-серверу.

Адрес DNS-сервера хранится в системных настройках.

Это 2 популярных DNS-сервера:

  • 8.8.8.8: общедоступный DNS-сервер Google
  • 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-сервер TLD будет иметь IP-адреса авторитетных серверов имен для домена, который мы ищем.

Как? Когда вы покупаете домен, регистратор домена отправляет соответствующий TDL серверам имен. Когда вы обновляете серверы имен (например, при смене хостинг-провайдера), эта информация будет автоматически обновлена ​​вашим регистратором домена.

Это DNS-серверы хостинг-провайдера. Обычно их больше 1, чтобы служить резервным.

Например:

  • ns1.dreamhost.com
  • ns2.dreamhost.com
  • ns3.dreamhost.com

DNS-преобразователь запускается с первого и пытается запросить IP-адрес домена (с поддоменом), который вы ищете.

Это окончательный источник истины для IP-адреса.

Теперь, когда у нас есть IP-адрес, мы можем продолжить наше путешествие.

Подтверждение TCP-запроса

Теперь, когда IP-адрес сервера доступен, браузер может инициировать TCP-соединение с ним.

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

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

Отправка запроса

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

Он состоит из 3 частей:

  • строка запроса
  • заголовок запроса
  • тело запроса

Строка запроса

Строка запроса задается одной строкой:

  • метод HTTP
  • расположение ресурса
  • версия протокола

Пример:

GET / HTTP/1.1

Заголовок запроса

Заголовок запроса - это набор field: value пар, которые устанавливают определенные значения.

Есть 2 обязательных поля, одно из которых - Host, другое - Connection, а все остальные поля являются необязательными:

Host: flaviocopes.com
Connection: close

Host указывает имя домена, на которое мы хотим настроить таргетинг, в то время как Connection всегда устанавливается в close, если соединение не должно оставаться открытым.

Некоторые из наиболее часто используемых полей заголовка:

  • Origin
  • Accept
  • Accept-Encoding
  • Cookie
  • Cache-Control
  • Dnt

но существует гораздо больше.

Заголовочная часть заканчивается пустой строкой.

Тело запроса

Тело запроса является необязательным, не используется в запросах GET, но очень часто используется в запросах POST, а иногда и в других глаголах, и оно может содержать данные в формате JSON.

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

Ответ

После отправки запроса сервер обрабатывает его и отправляет ответ.

Ответ начинается с кода состояния и сообщения о состоянии. Если запрос успешен и возвращает 200, он начнется с:

200 OK

Запрос может вернуть другой код состояния и сообщение, например одно из следующих:

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.

Создайте HTTP-сервер с помощью Node.js

Вот веб-сервер HTTP, который мы использовали в качестве приложения Node.js Hello World во введении:

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

Разберем его вкратце. Включаем http модуль.

Мы используем модуль для создания HTTP-сервера.

Сервер настроен на прослушивание указанного порта 3000. Когда сервер готов, вызывается функция listencallback.

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

request предоставляет подробную информацию о запросе. Через него мы получаем доступ к заголовкам запросов и данным запроса.

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

В этом случае с:

res.statusCode = 200

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

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

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

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

res.end('Hello World\n')

Выполнение 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 используют один и тот же формат запроса POST и просто меняют значение options.method.

HTTP-запросы в Node.js с использованием Axios

Axios - очень популярная библиотека JavaScript, которую вы можете использовать для выполнения HTTP-запросов, которая работает как на платформах Browser, так и на платформе Node.js.

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

Он основан на обещаниях, и это позволяет нам писать код async / await для очень простого выполнения запросов XHR.

Использование Axios имеет ряд преимуществ перед собственным Fetch API:

  • поддерживает старые браузеры (для Fetch нужен полифилл)
  • есть способ прервать запрос
  • есть способ установить время ожидания ответа
  • имеет встроенную защиту от CSRF
  • поддерживает прогресс загрузки
  • выполняет автоматическое преобразование данных JSON
  • работает в Node.js

Установка

Axios можно установить с помощью npm:

npm install axios

или пряжа:

yarn add axios

или просто добавьте его на свою страницу с помощью 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, wss://.

ws:// относится к небезопасной версии WebSockets (http:// WebSockets), и ее следует избегать по очевидным причинам.

Создайте новое соединение WebSockets

const url = 'wss://myserver.com/something'
const connection = new WebSocket(url)

connection - объект WebSocket.

Когда соединение успешно установлено, запускается openevent.

Прислушайтесь к нему, назначив функцию обратного вызова onopenproperty объекта 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 для связи между двумя внутренними службами.

Легко установить, используя:

yarn init
yarn add ws

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

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

Каждый файл содержит набор деталей, которые мы можем проверить с помощью 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 также предоставляет метод синхронизации, который блокирует поток до тех пор, пока не будет готова статистика файла:

const fs = require('fs')
try {
  const stats = fs.stat('/Users/flavio/test.txt')
} catch (err) {
  console.error(err)
}

Информация о файле включается в переменную 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:\users\flavio\file.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')
  console.log(data)
} catch (err) {
  console.error(err)
}

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

И fs.readFile(), и fs.readFileSync() читают все содержимое файла в памяти перед возвратом данных.

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

В этом случае лучше прочитать содержимое файла с помощью потоков.

Написание файлов с помощью Node.js

Самый простой способ записи в файлы в Node.js - использовать fs.writeFile() API.

Пример:

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+ открыть файл для чтения и записи, поместив поток в конец файла. Если файл не существует, создается

Вы можете узнать больше о флагах.

Добавить в файл

Удобный метод добавления содержимого в конец файла - 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(): проверьте, существует ли файл и может ли узел получить к нему доступ со своими разрешениями
  • 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()

Это имеет огромное значение в потоке вашего приложения.

Узел 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

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

const os = require('os')

Есть несколько полезных свойств, которые говорят нам о некоторых ключевых моментах, связанных с обработкой файлов:

os.EOL задает последовательность разделителей строк. Это \n в Linux и macOS и \r\n в 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 скомпилирован с прямым порядком байтов или прямым порядком байтов.

os.freemem()

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

os.homedir()

Вернуть путь к домашнему каталогу текущего пользователя.

Пример:

'/Users/flavio'

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
  • …более

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:

door.eventNames()

emitter.getMaxListeners()

Получите максимальное количество слушателей, которое можно добавить к объекту EventListener, которое по умолчанию равно 10, но может быть увеличено или уменьшено с помощью setMaxListeners():

door.getMaxListeners()

emitter.listenerCount()

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

door.listenerCount('open')

emitter.listeners()

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

door.listeners('open')

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)

HTTP-модуль Node.js

Модуль http в Node.js предоставляет полезные функции и классы для создания HTTP-сервера. Это ключевой модуль в работе с Node.js.

Его можно включить с помощью:

const http = require('http')

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

Характеристики

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.Agentclass.

Он используется для управления сохранением и повторным использованием соединений для клиентов 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 узла.

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

Он также поддерживает пул сокетов. Это ключ к производительности.

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') вернет истину, если в ответе установлен этот заголовок
  • headersSent() вернуть истину, если заголовки уже были отправлены клиенту

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

Чтобы отправить данные клиенту в теле ответа, вы используете write(). Он отправит буферизованные данные в поток ответа HTTP.

Если заголовки еще не были отправлены с использованием response.writeHead(), он сначала отправит заголовки с кодом состояния и сообщением, указанным в запросе, который вы можете изменить, установив значения свойств statusCode и statusMessage:

response.statusCode = 500
response.statusMessage = 'Internal Server Error'

http.IncomingMessage

Объект http.IncomingMessage создается:

  • http.Server при прослушивании события request
  • http.ClientRequest при прослушивании события response

Его можно использовать для доступа к ответу:

  • статус с помощью методов statusCode и statusMessage
  • заголовки, используя его метод headers или rawHeaders
  • HTTP-метод, использующий свой method метод
  • Версия HTTP с использованием метода httpVersion
  • URL с использованием метода url
  • базовый сокет с использованием метода socket

Доступ к данным осуществляется с помощью потоков, поскольку http.IncomingMessage реализует интерфейс Readable Stream.

Node.js потоки

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

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

Потоки - это не уникальная концепция Node.js. Они были введены в операционную систему Unix несколько десятилетий назад, и программы могут взаимодействовать друг с другом, передавая потоки через оператор конвейера (|).

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

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

Модуль 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-клиенту, как только у нас есть фрагмент данных, готовый к отправке.

трубка()

В приведенном выше примере используется строка 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 возвращает поток, подключенный к стандартному вводу
  • 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() распаковать поток спуска

Различные типы потоков

Есть четыре класса потоков:

  • Readable: поток, из которого вы можете перенаправить, но не в него (вы можете получать данные, но не отправлять в него). Когда вы помещаете данные в читаемый поток, они буферизуются до тех пор, пока потребитель не начнет читать данные.
  • Writable: поток, в который вы можете подключиться, но не из него (вы можете отправлять данные, но не получать от них)
  • Duplex: поток, в который можно передавать и передавать по каналу, в основном комбинация потока с возможностью чтения и записи.
  • Transform: поток преобразования похож на дуплексный, но вывод является преобразованием его ввода.

Как создать читаемый поток

Получаем поток Readable из модуля stream, и инициализируем его:

const Stream = require('stream')
const readableStream = new Stream.Readable()

Теперь, когда поток инициализирован, мы можем отправлять в него данные:

readableStream.push('hi!')
readableStream.push('ho!')

Как создать доступный для записи поток

Чтобы создать доступный для записи поток, мы расширяем базовый объект Writable и реализуем его метод _write().

Сначала создайте объект потока:

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())
})

Как отправить данные в поток с возможностью записи

Используя метод stream 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 и существует уже много лет.

Установка пакета MySql для Node.js

Вы устанавливаете его, используя:

npm install mysql

Инициализация подключения к базе данных

Сначала вы включаете пакет:

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, todos, fields) => {
  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.insertIdvalue:

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():

connection.end()

Это гарантирует, что любой ожидающий запрос будет отправлен, а соединение будет корректно завершено.

Разница между разработкой и производством

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

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

Обычно это делается путем выполнения команды:

export NODE_ENV=production

в оболочке, но лучше поместить ее в файл конфигурации оболочки (например, .bash_profile с оболочкой Bash), потому что в противном случае настройка не сохраняется в случае перезапуска системы.

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

NODE_ENV=production node app.js

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

Установка среды на production обычно гарантирует, что:

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

Например, Pug, библиотека шаблонов, используемая Express, компилируется в режиме отладки, если NODE_ENV не установлено в production. Экспресс-представления компилируются в каждом запросе в режиме разработки, а в производстве они кэшируются. Есть еще много примеров.

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 поможет вам начать его использовать или поможет понять некоторые из его концепций. И, надеюсь, теперь вы знаете достаточно, чтобы начать создавать что-то замечательное!