Require javascript что это
Что такое require и module.exports
November 09, 2015
Данная статья является прочтением и переосмыслением перевода Node.js, Require и Exports статьи-оригинала Node.js, Require and Exports.
С помощью этих команд Node.js осуществляет взаимодействие своих модулей между друг другом.
Команда require подключает один модуль к другому. Команда module.exports делает модуль доступным для других модулей.
В Node.js все штуки видны друг другу только в рамках одного и того же файла. Под штуками я подразумеваю переменные, функции, классы и их члены.
Например, у нас есть файл misc.js следующего содержания:
Дело в том, что Node.js состоит из блоков, называемых модулями; и каждый отдельный файл по своей сути — отдельный блок (модуль), чья область видимости изолирована от других таких же блоков (модулей).
Теперь перед тем как мы узнаем, как сделать эти штуки видимыми вне модуля, рассмотрим более подробно, как загружаются модули в Node.js.
Другими словами можно сказать, что в Node.js служебное слово require служит для подключения одного независимого модуля (файла) к другому независимому модулю (файлу). Принцип подключения через require заключается в создании в целевом модуле объекта и помещении в этот объект подключаемого модуля.
Конечно же, до тех пор, пока наш модуль ( some_module ) ничего не отдает, все приведенные примеры бесполезны.
А чтобы наш модуль ( some_module ) что-нибудь отдал, мы будем использовать служебное слово module.exports :
Вот теперь наш модуль ( some_module ) стал гораздо более полезным:
В результате в целевом модуле будет создан объект someModule со свойством x = 5 ( someModule.x ) и методом summ ( someModule.summ ).
Есть ещё такой способ отдать штуки из нашего модуля:
Разница между этими подходами не велика, но важна. Как видно, в данном случае мы экспортируем функцию напрямую:
Всё это к тому, что потом ее будет легче использовать:
Выгода от использования такого способа заключается в том, что в данном случае нам не важно, будет ли ваш модуль представлять контейнер c экспортируемыми значениями или нет.
Чтобы еще более красочно представить процесс взаимодействия модулей, давайте рассмотрим следующий пример:
Что, по сути, является упрощенной записью следующего кода:
Модульный подход к разработке web-приложений с использованием JavaScript: AMD и RequireJS
Подключение загрузчика
Имеем следующую структуру каталогов:
Для начала, подключим в index.html загрузчик. Будем использовать RequireJS:
Описание модуля
Опишем наш модуль в /js/module.js с помощью define:
Первый аргумент — строка, название модуля, не обязателен. Вторым аргументом передаются зависимости в виде массива строк, также опционально. Третий аргумент — функция-фабрика, которая выполняется только после удовлетворения всех зависимостей (загрузки перечисленных файлов). В неё в качестве аргументов передаются экспортируемые зависимостями переменные. А возвращать она должна сам модуль. В данном случае это объект с одним полем.
Использование
В /js/app.js подключим нужные модули с помощью JS и выполним свой код:
Module при этом не будет доступна в глобальной области видимости, как и другие переменные, экспортируемые библиотеками из зависимостей. Не смотря на то, что библиотека jQuery с версии 1.7 поддерживает AMD-архитектуру, она является исключением: экспортирует свой доллар в глобальную область видимости. Скорее всего, это сделано для сохранения совместимости с армией плагинов, написанных за многие годы.
Конфигурация
Заключение
Надеюсь, концепция AMD зацепила вас, так же, как и меня. Она помогает избежать хаоса при использовании большого количества JS-файлов в разработке, подталкивает к написанию реюзабельного кода, снимает ответственность за подключение файлов с бэкенда. А если у вас реально большое MVC-приложение из пары десятков файлов, то подобная система просто незаменима.
Исходный код из статьи доступен в репозитории на GitHub.
Happy hacking!
Понимание require() в Node.js
Node.js это асинхронная JavaScript бибилиотека для построения серверных приложений, которые используют конвенцию CommonJS. Весь этот синтаксис, модули поначалу достаточно запутали меня. Но давайте все-таки попытаемся разобраться.
В этой статье будет использоваться Node.js v0.5.8-pre. Поехали!
Для начала давайте определим пару функций, работающих с окружностями (вероятно вы их уже встречали на страницах документации):
exports.area = function ( r ) <
return PI * r * r
>
exports.circumference = function ( r ) <
return 2 * PI * r
>
Отлично. Теперь попробуем использовать их в node. Запускаем консоль командой node и подключаем наш файл circle.js. Обратите внимание, что в пути указан относительный путь. Расширение можно не указывать, node подразумевает, что это будет js файл. Первый раз у вас скорее всего получится что-то типа этого:
node> require(‘./circle’)
< area: [Function], circumference: [Function] >
node> area
ReferenceError: area is not defined
at EventEmitter.anonymous (eval at readline (/usr/local/lib/node/libraries/repl.js:48:9))
at EventEmitter.readline (/usr/local/lib/node/libraries/repl.js:48:19)
at node.js:845:9
Что же произошло?
Давайте разбираться. Имея опыт работы с другими объектными языками, я привык использовать методы, которые я предварительно определил. Но с CommonJS модулями не все так просто. Давайте посмотрим, как можно использовать наш модуль:
Отлично. Результат уже лучше. Однако если попробовать следующее:
Мы получим “ничего”. Ведь мы корректно вызвали атрибут circle, но ничего не получили. Поначалу это может несколько сбить с толку. Собственно как и меня по началу. Если заглянуть в документацию, то там есть что посмотреть по этому поводу.
To export an object, add to the special exports object. (Alternatively, one can use this instead of exports.)
Мы можем переписать наш модуль следующим образом:
“Причем здесь this?” — спросите вы. Да при том, что это и есть наш объект. Мы можем представить наш модуль в следующем виде:
Знакомо? Это обычный JavaScript-объект. Таким образом модули в Node.js не более, чем привычные нам объекты, определенные в короткой форме. То есть по сути мы объявляем лишь тело объекта. Теперь мы имеем представление об CommonJS модулях.
Вывод
Файлы модулей являются ничем иным, как обычными обектами. В файле мы определяем непосредственно тело модуля. В CommonJS модулях, если выхотите что-то сделать доступным из вне, необходимо использовать export. На самом деле можно получить доступ к PI (в примерах выше), но это неправильный подход. Используя глобальную область видимости мы просто рискуем переопределить уже существующие атрибуты или методы. А потом сиди и отлавливай баги. Экспортируем только публичные методы, все остальное оставляем внутри черного ящика. Export — это крайне удобный и практичный подход.
Правильное использование require в node.js
Предисловие
Не так давно проект, на котором я работаю в данный момент, начал использовать модульную систему ES2015. Я не буду заострять внимание на этой технологии JavaScript, т.к статья совсем не об этом, а о том как технология сподвигла меня к одной мысли.
Как многие знают, ES2015 Modules представляют собой импортирование/экспортирование скриптов крайне схожее по синтаксису с python и многими другими языками программирования. Пример:
Все, кто интересовался модулями JavaScript знают, что импортирование и экспортирование возможно только на верхнем уровне модуля (файла с кодом).
Следующий грубый пример кода вызовет ошибки:
В отличие от ES2015 Modules — в модульной системе node.js импортирование и экспортирование возможны на любом уровне вложенности.
Аналогичный код на node.js не вызовет ошибку:
Преимущество такого способа в том, что модули необходимые в обработчике явно импортированы внутри и не засоряют пространство имен модуля (особенно актуально, если импортируемый модуль нужен только в одном обработчике). Так же появляется возможность отложенного экспортирования данных модуля.
Разбор полетов
В большинстве задач для которых используется node.js — front-end или основной веб-сервер, и высокая нагрузка на node.js частое явление. Пропуская способность вашего сервера должны быть максимально возможная.
Измерение пропускной способности
Для измерения пропускной способности веб-сервера используется великолепная утилита от Apache — ab. Если вы еще с ней не знакомы, то настоятельно рекомендую это сделать.
Код веб-сервера одинаков за исключением обработчиков.
Тест запускался на node.js 6.0 с использованием модуля ifnode , сделанного на базе express
Импортирование модулей непосредственно в обработчик
Результат:
Импортирование модулей в начале файла
Результат:
Анализ результатов
Импортирование модулей в начале файла уменьшило время одного запроса на
23%(!) (в сравнение с импортированием непосредственно в обработчик), что весьма существенно.
Note: пробовал «прогревать» кеш для случая с непосредственным импортированием модулей в обработчик (перед запуском утилиты ab, модули были уже закешированы) — производительность улучшалась на 1-2%.
Модули в JavaScript
Фронтенд-разработчики каждый день используют модули. Это может быть функция из локального файла или сторонняя библиотека из node_modules. Сегодня я кратко расскажу об основных модульных системах в JavaScript и некоторых нюансах их использования.
Синтаксис систем модулей
В современном JavaScript осталось два основных стандарта модульных систем. Это CommonJS, которая является основной для платформы Node.js, и ESM (ECMAScript 6 модули), которая была принята как стандарт для языка и внесена в спецификацию ES2015.
История развития модульных систем JavaScript хорошо описана в статьях «Эволюция модульного JavaScript» и «Путь JavaScript-модуля».
Если вам хорошо известен весь синтаксис модульных систем ESM и CommonJS, то можно пропустить следующую главу.
ESM-модули
Именованный импорт/экспорт
export можно использовать в момент объявления функции, переменной или класса:
Для больших модулей удобнее использовать группированный экспорт, это позволяет наглядно увидеть все экспортируемые сущности внутри модуля:
Импорт/Экспорт по умолчанию
В случае, когда из файла модуля экспортируется только одна сущность, удобнее использовать экспорт по умолчанию. Для этого необходимо добавить default после инструкции export :
Импорт модуля в случае экспорта по умолчанию:
Дополнительные возможности
Переименование. Для изменения имени метода в момент импорта/экспорта существует инструкция as :
Импорт этой функции будет доступен только по новому имени:
Переименование в момент импорта:
Этот синтаксис полезен для случаев, когда имя импортируемой части уже занято. Также можно сократить имя функции/переменной/класса, если она часто используется в файле:
Инициализация модуля без импорта его частей. Используется, когда необходимо выполнить импорт модуля для выполнения кода внутри него, но не импортировать какую-либо его часть:
Импорт всего содержимого модуля. Можно импортировать всё содержимое модуля в переменную и обращаться к частям модуля как к свойствам этой переменной:
Такой синтаксис не рекомендуется использовать, сборщик модулей (например, Webpack) не сможет корректно выполнить tree-shaking при таком использовании.
Реэкспорт. Существует сокращенный синтаксис для реэкспорта модулей. Это бывает полезно, когда нужно собрать модули из разных файлов в одном экспорте:
при таком реэкспорте наименования частей модуля будут сохраняться, но можно изменять их с помощью инструкции as :
Аналогичным способом можно реэкспортировать значения по умолчанию:
Использование модулей в браузере
Рассмотрим на примере небольшого проекта.
Импорт модуля внутри index.html:
По атрибуту type=»module» браузер понимает, что экспортирует файл с модулями, и корректно его обработает. Стоит отметить, что пути импорта, указанные в main.js (./dist/module1 и ./dist/module2), будут преобразованы в абсолютные пути относительно текущего расположения, и браузер запросит эти файлы у сервера по адресам /dist/module1 и /dist/module2 соответственно. Практического применения у этой возможности не так много, в основном в проектах используется сборщик (например Webpack), который преобразует ESM-модули в bundle. Однако использование ESM-модулей в браузере может позволить улучшить загрузку страницы за счет разбиения bundle-файлов на маленькие части и постепенной их загрузки.
CommonJS
В CommonJS cуществует что-то схожее с импортом по умолчанию, для этого необходимо просто присвоить module.exports значению экспортируемой функции:
Сохранение значения в exports напрямую, в отличие от именованного экспорта, не будет работать:
Стоит обратить внимание, что если были экспортированы части модуля, они затрутся и будет экспортировано только последнее значение module.exports :
Импорт. Для импорта необходимо воспользоваться конструкцией require() и указать путь до модуля:
Можно воспользоваться деструктуризацией и получить значение необходимой функции сразу после импорта:
Работа с модулями в Node.js
Поддержка ESM-модулей
До недавнего времени Node.js поддерживал только CommonJS, но с версии 13.2.0 команда разработчиков анонсировала поддержку ESM (с версии 8.5.0 поддержка модулей ECMAScript 6 была скрыта за экспериментальным флагом). Подробно о том, как работать с модулями ECMAScript 6 в Node.js, можно прочитать в анонсе команды разработчиков Node.js.
Поиск модулей
Все относительные пути, начинающиеся c ‘./’ или ‘../’ будут обрабатываться только относительно рабочей папки проекта. Пути с ‘/’ будут обрабатываться как абсолютные пути файловой системы. Для остальных случаев Node.js начинает поиск модулей в папке проекта node_modules (пример: /home/work/projectN/node_modules). В случае, если интересующий модуль не был найден, Node.js поднимается на уровень выше и продолжает свой поиск там. И так до самого верхнего уровня файловой системы. Поиск необходимой библиотеки будет выглядеть следующим образом:
Дополнительные свойства у module и require
У module и require есть дополнительные свойства, которые могут быть полезны.
module.id — уникальный идентификатор модуля. Обычно это полностью определенный путь до модуля.
module.children — объект, содержащий модули, которые импортированы в текущем файле. Ключами объекта являются module.id :
require.cache — представляет из себя объект с информацией о каждом импортированном модуле. Если при импорте модуля Node.js находит его в кеше, код модуля не будет выполняться повторно, а экспортируемые сущности будут взяты из закешированного значения. При необходимости повторного «чистого» импорта модуля необходимо сбросить закешированное значение, удалив его из кеша:
Что происходит в момент импорта ES-модуля
В момент выполнения файла Javascript-движок выполняет несколько этапов загрузки модулей:
Структура данных, содержащая информацию о модуле (уникальный идентификатор, список зависимостей и состояния всех экспортируемых значений) называется Module Records.
При выполнении скрипта строится граф зависимостей и создается запись по каждому импортируемому модулю внутри него. В момент каждого импорта, вызывается метод Evaluate() внутри модуля Module Records. При первом вызове этой функции выполняется сценарий для получения и сохранения состояния модуля. Подробнее об этом процессе можно прочитать в статье «Глубокое погружение в ES-модули в картинках».
Что происходит при повторном импорте модуля
Но остался открытым вопрос, создаётся ли новая сущность Module Records при повторном импорте? Например в данном случае:
За получение Module Records для каждого import отвечает метод HostResolveImportedModule, который принимает два аргумента:
В спецификации говорится, что для одинаковых парах значений referencingScriptOrModule и specifier возвращается один и тот же экземпляр Module Records.
Рассмотрим еще один пример, когда один и тот же модуль импортируется в нескольких файлах:
Multiple different referencingScriptOrModule, specifier pairs may map to the same Module Record instance. The actual mapping semantic is host-defined but typically a normalization process is applied to specifier as part of the mapping process. A typical normalization process would include actions such as alphabetic case folding and expansion of relative and abbreviated path specifiers
Таким образом, даже если referencingScriptOrModule отличается, а specifier одинаков, может быть возвращен одинаковый экземпляр Module Records.
Однако этой унификации не будут подвержены импорты с дополнительными параметрами в specifier :
Циклические зависимости
При большой вложенности модулей друг в друга может возникнуть циклическая зависимость:
Для наглядности, эту цепочку зависимостей можно упростить до:
ES-модули нативно умеют работать с циклическими зависимостями и корректно их обрабатывать. Принцип работы подробно описан в спецификации. Однако, ESM редко используются без обработки. Обычно с помощью транспилятор (Babel) сборщик модулей (например, Webpack) преобразует их в CommonJS для запуска на Node.js, или в исполнямый скрипт (bundle) для браузера. Циклические зависимости не всегда могут быть источником явных ошибок и исключений, но могут стать причиной некорректного поведения кода, которое трудно будет отловить.
Есть несколько хаков, как можно обходить циклические зависимости для некоторые ситуаций, но лучше просто не допускать их возниковения.
Заключение
В этой статье я собрал всю основную информацию о модульных системах в Javascript, чтобы у читателя не осталось пробелов относительно того, как их использовать и как они работают. Надеюсь, у меня это получилось, и статья оказалась вам полезной. Буду рад обратной связи!