Node.js для фронтенд-разработчиков | В паутине

Node.js для фронтенд-разработчиков

Это руководство ориентировано на разработчиков, которые знают JavaScript, но еще не очень хорошо владеют Node.

Вам, вероятно, уже приходится работать с Node.js: npm scripts, конфигурацией webpack, задачами gulp, автоматической сборкой и запуском тестов. Несмотря на то, что для работы не требуется глубокое понимание всех этих действий, иногда они могут сбивать с толку.

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

Версия Node

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

У Node.js есть публичный график релизов, из которого видно, что нечетные версии не имеют долгосрочной поддержки. Текущая LTS-версия (long-term support) будет активно разрабатываться до апреля 2019 года, а затем поддерживаться до 31 декабря 2019 года.

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

Node.js широко применяется в современном фронтенде – трудно найти проект проект, который не использует инструменты этой платформы. Скорее всего, вы уже знакомы с nvm (node version manager), который позволяет установить несколько версий Node одновременно для разных проектов. Это полезно, ведь если разные приложения используют разные релизы, было бы сложно синхронизировать и тестировать их на одной машине. Такие инструменты существуют и для многих других языков, например, virtualenv для Python, rbenv для Ruby.

Babel не нужен

Так как вы можете выбирать любую LTS-версию Node.js, то ничего не мешает использовать ту, в которой поддерживаются практически все современные возможности языка (ES 2015), за исключением хвостовой рекурсии.

Следовательно, Babel вам нужен, только если вы застряли с очень старой версией, или используете JSX-синтаксис, или хотите применять суперновые возможности, которые еще не вошли в стандарт.

Также нет необходимости в использовании webpack или browserify, и поэтому у нас нет инструмента для перезагрузки кода – вы можете использовать nodemon для перезапуска приложения после внесения изменений.

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

Стиль коллбэков

Исторически сложилось так, что асинхронные функции в Node.js принимают обратные вызовы с сигнатурой (err, data), где первый аргумент представляет ошибку. Если он равен null, то все хорошо, иначе придется что-то предпринимать. Эти обработчики вызываются после получения ответа, например, при попытке прочитать файл:

Вскоре выяснилось, что этот стиль крайне затрудняет написание читаемого и поддерживаемого кода, именно с ним связано callback hell. Позже появился асинхронный Promise – он был стандартизирован в ECMAScript 2015 (это глобальный объект как в браузере, так и в Node.js). Недавно ECMAScript 2017 ввела синтаксис async / await, доступный в Node.js 7.6+, который вы уже можете использовать.

С промисами мы избегаем «ада коллбэков», но появляется другая проблема: старый код и многие встроенные модули до сих пор используют обратные вызовы. Однако их можно преобразовать в обещания. Для примера давайте конвертируем метод fs.readFile:

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

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

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

Цикл событий

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

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

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

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

Этот метод уже существует в некоторых браузерах, но еще не стандартизирован, и его следует использовать с осторожностью. Он похож на setTimeout(0), но иногда имеет приоритет над ним. Именование здесь также не самое подходящее – мы говорим о следующей итерации цикла событий, а это не совсем immediate.

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

Давайте посмотрим на пример со всем вышеперечисленным:

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

Вот правильный вывод:

Вы можете узнать больше о цикле событий и методе process.nextTick в официальной документации Node.js.

Event Emitters

Многие встроенные модули Node.js создают или получают различные события. Платформа реализует EventEmitter по шаблону публикации-подписки. Это очень похоже на события в браузере, но с немного другим синтаксисом. Чтобы разобраться, проще всего реализовать все самостоятельно:

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

Этот код позволяет подписаться на события, отписаться от них позже позже, а также создавать. Объект ответа, объект запроса, потоки в Node – все они фактически расширяют или реализуют EventEmitter!

Эта простая концепция реализована во многих npm-пакетах: 1, 2, 3 и ряде других.

Потоки

Потоки в Node – самая лучшая и непонятная идея.

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

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

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

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

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

Объект ответа – это записываемый поток, а fs.createReadStream создает читаемый поток. Они оба являются дуплексными и трансформируемыми. Узнать больше о потоках в Node вы можете здесь.

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

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

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

Система модулей

Node.js использует модули commonjs. Каждый раз, когда вы используете require,чтобы получить какой-то модуль внутри конфигурации webpack или когда объявляете module.exports, вы фактически применяете этот механизм. Возможно, также видели что-то вроде exports.something = {}, без слова module. Давайте разберемся, как это работает.

Прежде всего, речь пойдет о модулях commonjs с обычным расширением .js, а не о .esm/.mjs-файлах (модули ECMAScript), которые позволяют использовать синтаксис import/export. Кроме того, важно понимать, что webpack и browserify (и другие инструменты сборки) используют свою собственную функцию require, которая немного отличается.

Итак, откуда мы на самом деле получаем эти «глобальные» объекты module, require и exports? Их добавляет сама платформа – вместо того, чтобы просто выполнять файл javascript, она фактически обертывает его в функцию со всеми этими переменными:

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

Эти переменные вводятся в модуль и доступны в нем как «глобальные», хотя на самом деле таковыми не являются. Обязательно взгляните на них внутри модуля и в «главном «файле – можно просто вызвать console.log(module) – и сравните.


Далее рассмотрим объект exports и тонкости работы с ним:

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


Наконец, require – это функция, которая берет имя модуля и возвращает его объект exports. Как именно она находит модуль? Существует довольно простая последовательность действий:

  • сравнить модули ядра с указанным именем;
  • если путь начинается с ./ или. ./ , попробовать найти такой файл;
  • если файл не найден, попробовать найти каталог с таким именем с файлом index.js
  • если путь не начинается с ./ или ./, зайти в node_modules/ и проверить наличие там папки/ файла:
    • в той директории, где запустился скрипт
    • на один уровень выше, пока не найдется /node_modules

Есть и некоторые другие места поиска, которые в основном предназначены для совместимости. Также вы можете указать свой путь для поиска с помощью переменной NODE_PATH. Если вы хотите видеть точный порядок поиска модулей, просто выведите объект модуля на консоль и найдите свойство paths. Оно выглядит примерно так:

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

Переменные окружения

Как указано в 12-factor-app, рекомендуется хранить конфигурацию в переменных среды. Можно настроить переменные для сеанса терминала:

Node – это кросс-платформенный движок, и в идеале ваше приложение должно запускаться на любой платформе. Примеры в статье охватывают только MacOS / Linux и не будут работать для Windows. Синтаксис переменных среды в Windows отличается, вы можете использовать что-то вроде cross-env, но имейте это ввиду и в других случаях.

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

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

Доступ к ним можно получить с помощью объекта process.env:

Все вместе

В этом примере мы создадим простой http-сервер, который вернет файл с именем, указанным в URL после символа /. Если файл не существует, вернется ошибка 404 Not Found. Если пользователь попытается использовать относительный или вложенный путь, мы отправим ему ошибку 403.

На этот раз задокументируем код как следует:

Заключение

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

Комментарии (0)

Ваш email не будет опубликован. Все поля обязательны