Что за файл main js
JavaScript – это язык программирования, который добавляет интерактивность на ваш веб-сайт (например: игры, отклик при нажатии кнопок или при вводе данных в формы, динамические стили, анимация). Эта статья поможет вам начать работать с этим захватывающим языком и даст вам представление о том, на что он способен.
Взгляд со стороны — .mjs против .js
В этой статье мы используем расширение .js для файлов наших модулей, но в других источниках вы можете встретить расширение .mjs . Например, в документации движка V8 используется .mjs . Причины следующие:
- Это полезно для ясности, то есть дает понять, какие файлы являются модулями, а какие — обычными JavaScript-файлами.
- Это гарантирует, что файлы вашего модуля будут проанализированы как модуль средами выполнения, такими как Node.js, и инструментами сборки, такими как Babel.
Это нормально, если вы уже используете такую среду или ещё нет, но знаете, что делать, и имеете нужные доступы (то есть вы можете настроить свой сервер, чтобы он устанавливал корректный Content-Type -заголовок для .mjs -файлов). Однако это может вызвать путаницу, если вы не контролируете сервер, с которого отдаются файлы, или публикуете файлы для общего пользования, как мы здесь.
В целях обучения и переносимости на разные платформы мы решили остановится на .js .
Если вы действительно видите ценность и ясность использования .mjs для модулей по сравнению с использованием .js для обычных JavaScript-файлов, но не хотите столкнуться с проблемой описанной выше, вы должны всегда использовать .mjs во время разработки и конвертировать их в .js во время сборки.
Также стоит отметить, что:
- Некоторые инструменты могут никогда не добавить поддержку .mjs , например, TypeScript.
- атрибут используется для обозначения того, что файл является модулем. Вы увидите примеры использования данного атрибута ниже.
ES модуль: ECMAScript2015 или ES6 модуль
В 2015 году в 6 версии спецификации JS был представлен новый модульный синтаксис. Данная сецификаци получила название ECMAScript 2015 (ES2015) или ECMAScript 6 (ES6). Основа нового синтаксиса — ключевые слова import и export . Следующий код демонстирует использование ES модуля для именованного и «дефолтного» (по умолчанию) импорта/экспорта:
Для использования модульного файла в браузере необходимо добавить тег и определить его как модуль: . Для использования этого модуля в Node.js меняем его расширение на .mjs :
Для обратной совместимости в браузере можно добавить тег с атрибутом nomodule :
TypeScript модуль: транспиляция CJS, AMD, ES и SystemJS модулей
TypeScript поддерживает все разновидности синтаксиса JS, включая ES6. При транспиляции синтаксис ES6 модуля может быть сохранен или преобразован в другой формат, в том числе CommonJS/Node.js, AMD/RequireJS, UMD/UmdJS или SystemJS согласно настройкам транспиляции в tsconfig.json :
Модульный синтаксис ES, поддерживаемый TypeScript, получил название внешних модулей.
Внутренние модули и пространство имен
TypeScript также имеет ключевые слова module и namespace . Они называются внутренними модулями:
Оба транспилируются в JS объекты:
TypeScript module и namespace могут иметь несколько уровней вложенности через разделитель . :
Sub module и sub namespace транспилируются в свойства объекта:
TypeScript module и namespace также могут использоваться в операторе export :
Приведенный код также транспилируется в sub module и sub namespace:
Синтаксис систем модулей
В современном JavaScript осталось два основных стандарта модульных систем. Это CommonJS, которая является основной для платформы Node.js, и ESM (ECMAScript 6 модули), которая была принята как стандарт для языка и внесена в спецификацию ES2015.
История развития модульных систем JavaScript хорошо описана в статьях «Эволюция модульного JavaScript» и «Путь JavaScript-модуля».
Если вам хорошо известен весь синтаксис модульных систем ESM и CommonJS, то можно пропустить следующую главу.
Переменные
Переменные — это контейнеры, внутри которых вы можете хранить значения. Вы начинаете с того, что объявляете переменную с помощью ключевого слова var (не рекомендуется, продолжайте читать, чтобы получить объяснения) или let , за которым следует любое имя, которым вы захотите её назвать:
Примечание: Точка с запятой в конце строки указывает, где заканчивается оператор. Точку с запятой нужно обязательно использовать для разеделения операторов, располагающихся на одной строке. Однако некоторые люди считают, что хорошая практика — указывать её в конце каждого оператора. Существуют и другие правила, когда нужно ставить точку с запятой, а когда нет. Смотрите более подробно в статье ваше руководство по точкам с запятой в JavaScript.
Примечание: вы можете назвать переменную практически как угодно, но есть некоторые ограничения для её имени (смотрите в правилах именования переменных.) Если вы не уверены, вы можете проверить имя вашей переменной, чтобы увидеть корректно ли оно.
Примечание: JavaScript чувствителен к регистру — myVariable отличается от переменной myvariable . Если у вас возникают проблемы в вашем коде, проверьте регистр!
Примечание: Для получения более подробной информации о разнице между var и let, смотрите: Разница между var и let.
После объявления переменной вы можете присвоить ей значение:
Вы можете сделать обе эти операции на одной и той же строке, если вы захотите:
Вы можете получить значение, просто вызвав переменную по имени:
После установки значения переменной вы можете изменить его позже:
Обратите внимание, что переменные имеют разные типы данных:
Переменная | Пояснение | Пример |
---|---|---|
String | Последовательность текста, называемая строкой. Чтобы указать, что это значение является строкой, вы должны заключить его в кавычки. | var myVariable = 'Bob'; |
Number | Числа. Числа не имеют кавычек вокруг них. | var myVariable = 10; |
Boolean | Значение True(Правда)/False(Ложь). Слова true и false специальные ключевые слова в JS, и не нуждаются в кавычках. | var myVariable = true; |
Array | Массив, который позволяет хранить несколько значений в одной ссылке. | var myVariable = [1,'Bob','Steve',10]; Обратиться к каждому элементу массива можно так: myVariable[0] , myVariable[1] , и т.д. |
Object | В принципе, что угодно. Все в JavaScript является объектом, и может храниться в переменной. Имейте это в виду, пока вы учитесь. | var myVariable = document.querySelector('h1'); Все это из вышеприведённых примеров. |
Пример "hello world"
Предыдущий раздел звучит очень многообещающе, и это на самом деле так — JavaScript является одной из самых перспективных веб-технологий, и когда вы освоитесь и начнёте использовать его, ваши веб-сайты перейдут в новое измерение мощи и креативности.
Тем не менее, с JavaScript немного более сложно освоиться, чем с HTML и CSS. Вам придётся начать с малого, продолжая изучение небольшими шагами. Для начала мы покажем вам, как добавить некоторые основы JavaScript на вашу страницу, чтобы создать "hello world!" пример (стандарт в начальных примерах программирования).
Важно: Если вы не следили за остальным нашим курсом, скачайте этот пример кода и используйте его в качестве стартовой точки.
- Для начала перейдите на ваш тестовый сайт и создайте папку с именем 'scripts' (без кавычек). Затем, в новой папке скриптов, которую вы только что создали, создайте новый файл с именем main.js . Сохраните его в вашей папке scripts .
- Далее перейдите в ваш index.html файл и введите следующий элемент на новой строке прямо перед закрывающим тегом :
CommonJS
Первый стандарт, который описывает API для создания и подключения модулей, был разработан рабочей группой CommonJS. Этот стандарт был придуман для использования в серверном JS, и его реализацию можно увидеть, например, в node.js.
Для подключения зависимостей используется глобальная функция require() , которая принимает первым параметром строку с путем к модулю. Для экспортирования интерфейса модуля мы используем свойство exports объекта module . И когда этот модуль будет подключен как зависимость с помощью функции require , где-то в коде другого модуля, то эта же функция вернет экспортируемый объект.
Данный подход решает все вышеперечисленные проблемы. Никаких оберток делать не нужно, каждый файл — это отдельный модуль со своей областью видимости. Исходный код можно разбивать на мелкие логические единицы. И каждый модуль четко определяет все свои зависимости.
НО! В браузере, просто так, такой синтаксис не заработает. Для этого нужно использовать специальный сборщик. Например, популярны browserify или Brunch, которые работают на node.js. Эти инструменты довольно удобны, их функциональность не ограничивается только лишь возможностью создавать CommonJS-модули, и многие разработчики предпочитают использовать их в своих проектах. Суть у них одинакова: сборщик проходится по дереву зависимостей модулей и собирает все в один файл, который в свою очередь будет загружаться браузером. Даже при разработке в debug-режиме нужно постоянно запускать сборщик из командной строки, или, что удобнее, запускать watcher, который будет следить за изменениями в файлах и автоматически производить сборку. Стоит заметить, что отлаживать приходится не исходные файлы, а то, что сгенерирует сборщик. Если вы не планирует отлаживать ваш код в старых браузерах, то это не будет проблемой, потому что сборщики умеют генерировать Source Maps, благодаря которым результирующий сжатый файл будет связан с исходниками. Это позволит вести отладку так, как будто вы работаете с самим исходным кодом. Также, сборка в один файл — это не всегда хорошо. Например, если мы хотим подгружать модуль удаленно, с CDN, или загружать часть кода только по требованию.
Заключение
В этой статье я собрал всю основную информацию о модульных системах в Javascript, чтобы у читателя не осталось пробелов относительно того, как их использовать и как они работают. Надеюсь, у меня это получилось, и статья оказалась вам полезной. Буду рад обратной связи!
При разработке front-end части приложения на языке JavaScript, мы можем столкнуться с рядом традиционных проблем. Все они решаются при помощи модульных подходов. Ниже мы рассмотрим самые популярные подходы для описания модулей в JavaScript, которые существуют на сегодняшний день.
Итак, какие проблемы имеются в виду:
Большие файлы. Довольно часто возникает такая ситуация, когда в проекте есть файлы, названные в стиле app.js или common.js, в которых все просто свалено в одну кучу: функции, хелперы, виджеты и т.д. Работать и поддерживать код в таких файлах довольно тяжело. Приходится постоянно прокручивать туда-сюда, выискивая нужный кусочек кода, ставить много закладок при помощи IDE, чтобы не потерять нужные места в файле. Также есть тенденция, что чем больше размер файла, который содержит в себе кучу общей логики, тем быстрее он продолжает расти. Плюс ко всему, в большой команде это может стать причиной постоянных конфликтов в системе контроля версий.
Зависимости и порядок подключения. Зачастую JS-код в приложении разбит на несколько файлов. Мы используем плагины, которые зависят от библиотек; библиотеки, которые зависят от других библиотек; а код, написанный собственноручно, зависит уже и от того, и другого. Поэтому разработчик вынужден строго следить за порядком подключения JS-файлов и регулярно тратить время и энергию на их упорядочивание. Если нарушен порядок, то получаем ошибку. В любом случае, ошибки, которые возникают в связи с неправильным порядком подключения файлов, обычно достаточно легко заметить. В таком случае чаще всего мы просто получим исключение в браузере, например, $ is undefined .
Более трудной в обнаружении причиной ошибок может стать дублирование подключения одних и тех же файлов. Например, есть файл с куском кода, который вешает обработчик события на какой-либо dom-элемент. Ваш коллега может не заметить, что этот файл уже был подключен, и подключает его еще раз. В результате обработчик будет выполняться два раза, что может привести к неприятной ошибке, которую довольно трудно заметить сразу.
Переменные в глобальной области видимости. Проблемы с глобальными переменными заключаются в том, что они будут доступны во всем коде вашего приложения или страницы. Они находятся в глобальном пространстве имен, и всегда есть шанс возникновения коллизий именования, когда две разных части приложения определяют глобальные переменные с одинаковым именем, но для разных целей. Плюс ко всему, объявляя переменные в глобальной области видимости мы загрязняем объект window . И обращаясь каждый раз к глобальной переменной, интерпретатору JS приходится искать ее в раздутом объекте window , что сказывается на производительности кода.
Неструктурированный и не очевидный код. Еще одна довольно неприятная ситуация — когда нет четких границ, разделяющих логические куски кода. Когда, не вникая в код, сразу и не скажешь, какие другие части приложения он использует.
Переименование импорта и экспорта
Внутри фигурных скобок инструкций import и export вы можете использовать ключевое слово as вместе с новым именем функционала, чтобы задать ему новое имя, которое вы будете использовать для него внутри модуля верхнего уровня.
Так, например, оба следующих элемента будут выполнять одну и ту же работу, хотя и немного по-разному:
Давайте посмотрим на реальный пример. В нашей renaming директории вы увидите ту же модульную систему, что и в предыдущем примере, за исключением того, что мы добавили модули circle.js и triangle.js для рисования кругов и треугольников и создания отчетов по ним.
Внутри каждого из этих модулей у нас есть функции с одинаковыми именами, которые экспортируются, и поэтому у каждого из них есть один и тот же оператор export внизу файла:
Если бы в main.js при их импорте мы попытались использовать
то браузер выдал бы ошибку — "SyntaxError: redeclaration of import name" (Firefox).
Вместо этого нам нужно переименовать импорт, чтобы он был уникальным:
Обратите внимание, что вместо этого вы можете решить проблему в файлах модуля, например.
И это сработало бы точно так же. Какой способ вы будете использовать, зависит от вас, однако, возможно, имеет смысл оставить код модуля в покое и внести изменения в импорт. Это особенно важно, когда вы импортируете из сторонних модулей, над которыми у вас нет никакого контроля.
Ускоренный курс по основам языка
Давайте познакомимся с некоторыми основными возможностями языка JavaScript, чтобы дать вам больше понимания, как это всё работает. Более того, эти возможности являются общими для всех языков программирования. Если вы сможете понять эти основы, вы будете в состоянии начать программировать, как ни в чём не бывало!
Важно: В этой статье попробуйте вводить примеры строк кода в вашей JavaScript консоли, чтобы увидеть, что происходит. Для более подробной информации о JavaScript консоли смотрите статью Откройте для себя браузерные инструменты разработчика.
Другие отличия модулей от обычных скриптов
- Вы должны быть осторожны во время локального тестирование — если вы попытаетесь загрузить файл HTML локально (то есть по file:// URL), вы столкнётесь с ошибками CORS из-за требований безопасности JavaScript-модулей. Вам нужно проводить тестирование через сервер.
- Также обратите внимание, что вы можете столкнуться с отличным от обычных файлов поведением кода в модулях. Это происходит из-за того, что модули используют strict mode автоматически.
- Нет необходимости использовать атрибут defer (см. атрибуты элемента) при загрузке модуля, модули являются deferred по умолчанию.
- Модули выполняются только один раз, даже если на них есть ссылки в нескольких тэгах.
- И последнее, но не менее важное: давайте проясним это — функции модуля импортируются в область видимости одного скрипта, они недоступны в глобальной области видимости. Следовательно, вы сможете получить доступ к импортированному функционалу только в скрипте, в который он импортирован, и, например, вы не сможете получить к нему доступ из консоли JavaScript в DevTools. Вы по-прежнему будете получать синтаксические ошибки в DevTools, но вы не сможете использовать некоторые методы отладки, которые, возможно, ожидали использовать.
Модули и классы
Как мы намекали ранее, вы также можете экспортировать и импортировать классы — это ещё один способ избежать конфликтов в вашем коде, и он особенно полезен, если у вас уже есть код модуля, написанный в объектно-ориентированном стиле.
Вы можете увидеть пример нашего модуля для рисования фигур, переписанного с помощью классов ES в нашей classes директории. В качестве примера, файл square.js теперь содержит всю свою функциональность в одном классе:
который мы затем экспортируем:
Далее в main.js , мы импортируем его так:
А затем используем импортированный класс, чтобы нарисовать наш квадрат:
Что такое JavaScript на самом деле?
JavaScript ("JS" для краткости) — это полноценный динамический язык программирования, который применяется к HTML документу, и может обеспечить динамическую интерактивность на веб-сайтах. Его разработал Brendan Eich, сооснователь проекта Mozilla, Mozilla Foundation и Mozilla Corporation.
JavaScript невероятно универсален и дружелюбен к новичкам. Обладая большим опытом, вы сможете создавать игры, анимированную 2D и 3D графику, полномасштабные приложения с базами данных и многое другое!
JavaScript сам по себе довольно компактный, но очень гибкий. Разработчиками написано большое количество инструментов поверх основного языка JavaScript, которые разблокируют огромное количество дополнительных функций с очень небольшим усилием. К ним относятся:
- Программные интерфейсы приложения (API), встроенные в браузеры, обеспечивающие различные функциональные возможности, такие как динамическое создание HTML и установку CSS стилей, захват и манипуляция видеопотоком, работа с веб-камерой пользователя или генерация 3D графики и аудио сэмплов.
- Сторонние API позволяют разработчикам внедрять функциональность в свои сайты от других разработчиков, таких как Twitter или Facebook.
- Также вы можете применить к вашему HTML сторонние фреймворки и библиотеки, что позволит вам ускорить создание сайтов и приложений.
Поскольку эта статья должна быть только лёгким введением в JavaScript, мы не собираемся путать вас на этом этапе, подробно рассказывая о том, какая разница между основным языком JavaScript и различными инструментами, перечисленными выше. Вы можете подробно изучить все это позже, в нашей учебной области JavaScript и в остальной части MDN.
Ниже мы познакомим вас с некоторыми аспектами основного языка, и вы также будете играть с несколькими функциями API браузера. Веселитесь!
Поддержка в браузерах
Встроенная обработка модулей JavaScript связана с функциями import и export , которые поддерживаются браузерами следующим образом:
Дополнительные свойства у module и require
У module и require есть дополнительные свойства, которые могут быть полезны.
module.id — уникальный идентификатор модуля. Обычно это полностью определенный путь до модуля.
require.cache — представляет из себя объект с информацией о каждом импортированном модуле. Если при импорте модуля Node.js находит его в кеше, код модуля не будет выполняться повторно, а экспортируемые сущности будут взяты из закешированного значения. При необходимости повторного «чистого» импорта модуля необходимо сбросить закешированное значение, удалив его из кеша:
Заключение
Если вы следовали всем инструкциям в этой статье, в конечном итоге вы должны получить страницу, которая выглядит примерно так (вы также можете посмотреть нашу версию здесь):
Если вы застряли, вы всегда можете сравнить свою работу с нашим готовым примером кода на Github.
Здесь мы узнали только самую поверхность JavaScript. Если вам понравился этот язык и вы хотите изучить его поглубже, перейдите к нашему разделу изучение JavaScript.
Представляю вашему вниманию перевод статьи «Understanding (all) JavaScript module formats and tools» автора Dixin.
При создании приложения часто возникает желание разделить код на части, логические или функциональные блоки (модули). Однако JavaScript изначально не имел поддержки модулей. Это привело к появлению различных модульных технологий. В настоящей статье обсуждаются все основные понятия, шаблоны, библиотеки, синтаксис и инструменты для работы с модулями в JavaScript.
Сборка
Загружать много мелких файлов удобно при разработке и отладке, но не очень производительно, поэтому не подходит для production. И тут на помощь приходит утилита оптимизации r.js, которая идет в поставке с require.js.
Работает эта утилита на JS, поэтому на компьютере должен быть установлен node.js. Перед тем как запускать оптимизацию, нужно ее сконфигурировать. Для этого в приложении нужно создать файл app.build.js, который будет содержать обычный JS-объект с набором параметров.
Параметр baseUrl назначается так же, как описывалось выше. dir — директория для результирующего файла. main — путь к файлу, который содержит конфигурацию RequireJS. preserveLicenseComments — удалить комментарии о лицензиях, wrapShim — обернуть все shim-модули функцией define . Остальные возможные параметры можно посмотреть здесь: example.build.js.
Запустить сборку можно командой >node r.js -o app.build.js
Будет удобно добавить эту команду, например, в pre-build event в Visual Studio, чтобы JS-код собирался в тот же момент, когда запускается компиляция проекта.
Сначала программы на JavaScript были небольшими — в прежние времена они использовались для изолированных задач, добавляя при необходимости немного интерактивности веб-страницам, так что большие скрипты в основном не требовались. Прошло несколько лет, и вот мы уже видим полномасштабные приложения, работающие в браузерах и содержащие массу кода на JavaScript; кроме того, язык стал использоваться и в других контекстах (например, Node.js).
Таким образом, в последние годы появились причины на то, чтобы подумать о механизмах деления программ на JavaScript на отдельные модули, которые можно импортировать по мере необходимости. Node.js включал такую возможность уже давно, кроме того, некоторые библиотеки и фреймворки JavaScript разрешали использование модулей (например, CommonJS и основанные на AMD системы модулей типа RequireJS, а позднее также Webpack и Babel).
К счастью, современные браузеры стали сами поддерживать функциональность модулей, о чем и рассказывает эта статья. Этому можно только порадоваться — браузеры могут оптимизировать загрузку модулей, что было бы гораздо эффективнее использования библиотеки, и взять на себя обработку на стороне клиента и прочие накладные расходы.
Системный модуль: SystemJS модуль
SystemJS — это библиотека для обеспечения работы ES модулей в старых браузерах. Например, следующий модуль написан с использованием синтаксиса ES6:
Этот код не будет работать в браузерах, не поддерживающих синтаксис ES6. Одним из решений данной проблемы является транспиляция кода с помощью интерфейса System.register библиотеки SystemJS:
Нового модульного синтаксиса ES6 больше нет. Зато код будет прекрасно работать в старых браузерах. Эта транспиляция может быть выполнена автоматически с помощью Webpack, TypeScript и т.д.
Динамическая загрузка модуля
SystemJS также содержит функцию import для динамического импорта:
Создание объекта модуля
Вышеупомянутый способ работает нормально, но он немного запутан и многословен. Существует решение получше — импортировать функции каждого модуля внутри объекта модуля. Для этого используется следующая синтаксическая форма:
Эта конструкция берёт все экспорты, доступные внутри module.js и делает их доступными в качестве свойств объекта Module , фактически давая ему собственное пространство имен. Так например:
Опять же, давайте посмотрим на реальный пример. Если вы посмотрите на нашу директорию module-objects, вы снова увидите тот же самый пример, но переписанный с учётом преимуществ этого нового синтаксиса. В модулях все экспорты представлены в следующей простой форме:
С другой стороны, импорт выглядит так:
В каждом случае теперь вы можете получить доступ к импорту модуля под указанным свойством объекта, например:
Таким образом, теперь вы можете написать код точно так же, как и раньше (при условии, что вы включаете имена объектов там, где это необходимо), и импорт будет намного более аккуратным.
Добавление модуля на HTML-страницу
Далее нам необходимо подключить модуль main.js на нашу HTML-страницу. Это очень похоже на то, как мы подключаем обычный скрипт на страницу, с некоторыми заметными отличиями.
Вы также можете встроить скрипт модуля непосредственно в HTML-файл, поместив JavaScript-код внутрь -элемента:
Скрипт, в который вы импортируете функционал модуля, в основном действует как модуль верхнего уровня. Если вы упустите это, то Firefox, например, выдаст ошибку "SyntaxError: import declarations may only appear at top level of a module".
Вы можете использовать import и export инструкции только внутри модулей, внутри обычных скриптов они работать не будут.
Динамическая загрузка модулей
Самая свежая возмжность JavaScript-модулей доступная в браузерах,— это динамическая загрузка модулей. Это позволяет вам динамически загружать модули только тогда, когда они необходимы, вместо того, чтобы загружать всё заранее. Это даёт очевидные преимущества в производительности — давайте продолжим читать и посмотрим, как это работает.
Поддержка динамической загрузки модулей позволяет вызывать import() в качестве функции, передав ей аргументом путь к модулю. Данный вызов возвращает Promise , который резолвится объектом модуля (см. Создание объекта модуля), предоставляя вам доступ к экспорту указанного модуля, например:
Давайте посмотрим на пример. В директории dynamic-module-imports у нас есть ещё один пример, основанный на примере наших классов. Однако на этот раз мы ничего не рисуем на холсте при загрузке страницы. Вместо этого мы добавляем на страницу три кнопки — «Circle», «Square» и «Triangle», которые при нажатии динамически загружают требуемый модуль, а затем используют его для рисования указанной фигуры.
В этом примере мы внесли изменения только в наши index.html и main.js — экспорт модуля остается таким же, как и раньше.
Далее в main.js мы взяли ссылку на каждую кнопку, используя вызов document.querySelector() :
Затем мы добавляем обработчик событий на каждую кнопку, чтобы при нажатии соответствующий модуль динамически загружался и использовался для рисования фигуры:
Обратите внимание: поскольку выполнение Promise возвращает объект модуля, класс затем становится подкомпонентом объекта, поэтому теперь для получения доступа к конструктору нам нужно добавить к нему Module. , например Module.Square(. ) .
Работа с модулями в Node.js
Устранение проблем
Вот несколько советов, которые могут помочь вам, если вам не удаётся заставить ваши модули работать. Не стесняйтесь дополнять список, если найдете что-то ещё!
AMD модуль или RequireJS модуль
AMD (асинхронное определение модуля) — это шаблон для определения и использования модулей. Он используется в библиотеке RequireJS. AMD содержит функцию define для определения модуля, которая принимает название модуля, названия зависимостей и фабричную функцию:
Он также содержит функцию require для использования модуля:
require AMD отличается от require CommonJS тем, что в качестве аргументов функции принимает названия модулей и сами модули.
Динамическая загрузка
Функция define также имеет другое назначение. Она принимает функцию обратного вызова и передает похожую на CommonJS require этой функции. Внутри функции обратного вызова require вызывается для динамической загрузки модуля:
AMD модуль из CommonJS модуля
Приведенная выше функция define , кроме require , может принимать в качестве аргументов переменные exports и module . Поэтому внутри define может выполняться код из CommonJS:
CommonJS
Экспорт. Для экспорта в CommonJS используются глобальные объекты module и exports . Для этого необходимо просто добавить новое поле в объект exports .
Для удобства экспорта части фунциональности в глобальной области существует переменная exports , которая является ссылкой на module.exports . Поэтому возможен и такой синтаксис экспорта:
В CommonJS cуществует что-то схожее с импортом по умолчанию, для этого необходимо просто присвоить module.exports значению экспортируемой функции:
Сохранение значения в exports напрямую, в отличие от именованного экспорта, не будет работать:
Стоит обратить внимание, что если были экспортированы части модуля, они затрутся и будет экспортировано только последнее значение module.exports :
Импорт. Для импорта необходимо воспользоваться конструкцией require() и указать путь до модуля:
Можно воспользоваться деструктуризацией и получить значение необходимой функции сразу после импорта:
UMD модуль: универсальное определение модуля или UmdJS модуль
UMD (универсальное определение модуля) — набор шаблонов для обеспечения работы модуля в разных средах выполнения.
UMD для AMD (RequireJS) и браузера
Следующий код обеспечивает работу модуля как в AMD (RequireJS), так и в браузере:
Выглядит сложно, но это всего лишь IIFE. Анонимная функция определяет наличие функции define из AMD/RequireJS.
- Если define обнаружена, фабричная функция вызывается через нее.
- Если define не обнаружена, фабричная функция вызывается напрямую. В этот момент аргумент root — это объект Window браузера. Он получает зависимые модули из глобальных переменных (свойств объекта Window). Когда factory возвращает модуль, он также становится глобальной переменной (свойством объекта Window).
UMD для AMD (RequireJS) и CommonJS (Node.js)
Следующий код обеспечивает работу модуля как в AMD (RequireJS), так и в CommonJS (Node.js):
Не пугайтесь, это снова всего лишь IIFE. При вызове анонимной функции, происходит «оценка» ее аргумента. Оценивание аргумента позволяет определить среду выполнения (определяется наличие переменных module и exports из CommonJS/Node.js, а также функции define из AMD/RequireJS).
- Если средой выполнения является CommonJS/Node.js, аргумент анонимной функции вручную создает функцию define .
- Если средой выполнения является AMD/RequireJS, аргументом анонимной функции является функция define из этой среды. Выполнение анонимной функции гарантирует работу функции define . Внутри анонимной функции для создания модуля вызывается функция define .
Агрегирующие модули
Возможны случаи, когда вы захотите объединить модули вместе. У вас может быть несколько уровней зависимостей, где вы хотите упростить вещи, объединив несколько подмодулей в один родительский модуль. Это возможно с использованием следующего синтаксиса экспорта в родительском модуле:
Для примера посмотрите на нашу директорию module-aggregation. В этом примере (на основе нашего предыдущего примера с классами) у нас есть дополнительный модуль с именем shapes.js , который собирает функциональность circle.js , square.js и triangle.js вместе. Мы также переместили наши подмодули в дочернюю директорию внутри директории modules под названием shape . Итак, структура модуля в этом примере:
В каждом из подмодулей экспорт имеет одинаковую форму, например:
Далее идет агрегирование. Внутри shapes.js , мы добавляем следующие строки:
Они берут экспорт из отдельных подмодулей и фактически делают их доступными из модуля shape.js .
Примечание: Экспорты, указанные в shape.js , по сути перенаправляются через файл и на самом деле там не существуют, поэтому вы не сможете написать какой-либо полезный связанный код внутри того же файла.
Итак, теперь в файле main.js мы можем получить доступ ко всем трём классам модулей, заменив:
на единственную строку кода:
Что произошло?
Итак, ваш заголовок текста был изменён на "Hello world!" с помощью JavaScript. Мы сделали это с помощью вызова функции querySelector() , захватив ссылку на наш заголовок и сохранив её в переменной, названной myHeading . Это очень похоже на то, что мы делали в CSS с помощью селекторов. Если вы хотите что-то сделать с элементом, то для начала вам нужно его выбрать.
После этого, вы устанавливаете значение переменной myHeading в textContent свойство (которое представляет собой контент заголовка) "Hello world!".
Экспорт функционала модуля
Первое, что нужно сделать, чтобы получить доступ к функционалу модуля, — экспортировать его. Это делается с помощью инструкции export .
Самый простой способ использовать экспорт — поместить конструкцию export перед любыми элементами, которые вы хотите экспортировать из модуля, например:
Вы можете экспортировать var -, let -, const -переменные, и — как мы увидим позже — классы. Они должны быть в верхней области видимости, вы не можете использовать export внутри функции, например.
Более удобный способ экспорта всех элементов, которые вы хотите экспортировать,— использовать одну конструкцию export в конце файла модуля, где указать переменные, функции, классы, который вы хотите экспортировать, через запятую в фигурных скобках. Например:
Поддержка ESM-модулей
До недавнего времени Node.js поддерживал только CommonJS, но с версии 13.2.0 команда разработчиков анонсировала поддержку ESM (с версии 8.5.0 поддержка модулей ECMAScript 6 была скрыта за экспериментальным флагом). Подробно о том, как работать с модулями ECMAScript 6 в Node.js, можно прочитать в анонсе команды разработчиков Node.js.
Будущее уже наступило
В новом стандарте ECMAScript 6, помимо всяких крутых штук, описан новый синтаксис для создания и подключения модулей.
Один модуль, как и в CommonJS, — это один файл. Область видимости также ограничена этим файлом. Ключевое слово export экспортирует нужные значения в остальные части программы. Его можно использовать где угодно: посреди кода модуля или в конце, экспортируя все скопом.
Для подключения модуля используются ключевые слова import , from и as . Можно импортировать только одно нужное вам значение…
…или сразу несколько.
Либо можно импортировать весь модуль в качестве объекта со всеми экспортированными значениями.
Есть возможность изменять имена переменных с импортируемыми значениями, что может быть полезным, если импортируются значения с одинаковым именем из разных модулей.
Для того, чтобы, например, отводить один файл под один класс, удобно определять экспортируемое значение по умолчанию.
Чтобы импортировать значение по умолчанию, достаточно лишь не использовать фигурные скобки.
В приведенных примерах использовался так называемый декларативный синтаксис. Также есть возможность использовать программный интерфейс, что позволяет загружать модули асинхронно и по условию. Для этого используется System.import() .
В качестве единственного параметра необходимо передавать путь к модулю. В результате выполнения System.import() возвращается объект Promise. Таким образом, поток выполнения не блокируется и код, который не имеет отношения к импорту модуля, будет выполняться дальше.
Браузеры еще толком не поддерживают новый синтаксис, но возможность использовать уже есть. В этом вам поможет один из специальных трансляторов, например, Babel. Как и в случае с CommonJS, нужно запускать транслятор из командной строки или ставить watcher, благодаря которому исходники, написанные на ES6, при изменении будут преобразовываться в кроссбраузерную форму.
Некоторые разработчики уже сейчас используют новый синтаксис, и точно можно сказать, что за этим подходом будущее. Большинство же не решаются пока использовать новую технологию в реальных проектах.
Вот уже несколько лет подход под названием Asynchronous Module Definition позволяет разбивать код приложений на модули во всех популярных браузерах (IE6+), используя при этом только возможности браузера.
AMD — это подход к разработке, который позволяет создавать модули таким образом, чтобы они и их зависимости могли быть загружены асинхронно и параллельно. Это позволяет ускорить загрузку страницы, так как загрузка JS-модулей не будет блокировать загрузку остального контента сайта. Плюс, AMD дает возможность загружать модули по мере их востребованности. Например, есть страница со сложным модальным окном, в котором сосредоточено много логики: разные «визарды», несколько форм и т.д. При этом предполагается, что окно будет использоваться крайне редко. В таком случае, AMD позволяет загружать JS-код для этого окна не со страницей, а перед тем, как оно будет открыто пользователем.
Проще говоря, подход AMD сводится к описанию модулей с помощью функции define и подключению их с помощью require .
Самая популярная реализация подхода AMD — библиотека RequireJS.
CJS модуль: CommonJS модуль или Node.js модуль
CommonJS, первоначально названный ServerJS, это шаблон для определения и использования модулей. Он встроен в Node.js. По умолчанию каждый JS файл — это CJS. Переменные module и exports обеспечивают экспорт модуля (файла). Функция require обеспечивает загрузку и использование модуля. Следующий код демонстрирует определение модуля счетчика на синтаксисе CommonJS:
Вот как этот модуль используется:
В среде выполнения (движке) Node.js этот шаблон используется путем оборачивания кода внутри файла в функцию, которой в качестве параметров передаются переменные exports, module и функция require :
Как избежать конфликтов имён
Пока что наши модули для рисования фигур на холсте работают нормально. Но что произойдёт, если мы попытаемся добавить модуль, который занимается рисованием другой фигуры, например круга или треугольника? С этими фигурами, вероятно, тоже будут связаны такие функции, как draw() , reportArea() и т.д.; если бы мы попытались импортировать разные функции с одним и тем же именем в один и тот же файл модуля верхнего уровня, мы бы столкнулись с конфликтами и ошибками.
К счастью, есть несколько способов обойти это. Мы рассмотрим их в следующих разделах.
Первые модули
Изначально в JS не было возможности создавать настоящие модули. Хотя раньше это и не требовалось: на сайтах было относительно маленькое количество JS-кода. В основном нужно было где-то «прикрутить карусель», где-то красивое анимированное меню, и на этом все. Но затем web-приложения по сложности интерфейса и насыщенной функциональности начали догонять традиционные настольные. И тогда стал популярным так называемый паттерн «модуль».
Этот подход работает достаточно просто: создается немедленно-вызываемая анонимная функция-обертка, которая возвращает публичный интерфейс модуля, а вся остальная реализация инкапсулирована в замыкании. Это помогает решить две проблемы из вышеперечисленных: количество глобальных переменных сильно уменьшается, а сам код становится немного нагляднее благодаря тому, что мы разграничиваем его на логические куски. Но проблема управления зависимостями и порядком подключения файлов остается открытой.
Комментарии
Комментарии - это, по сути, короткие фрагменты текста, которые могут быть добавлены в код, и которые игнорируются браузером. Вы можете поместить комментарии в JavaScript-код, так же как вы делали это в CSS:
Если ваш комментарий не содержит переноса строк, то зачастую легче поставить две косые черты, как тут:
Экпорт по умолчанию против именнованого экспорта
Экспорты функций и переменных, которые мы использовали в примерах выше являются именованными экспортами — каждый элемент (будь то функция или const -переменная, например) упоминается по имени при экспорте, и это имя также используется для ссылки на него при импорте.
Существует также тип экспорта, который называется экспорт по умолчанию — это сделано для того, чтобы упростить использование экпортируемого функционала сторонним модулем, а также помогает модулям JavaScript взаимодействовать с существующими модульными системами CommonJS и AMD (это хорошо объясняется в статье ES6 в деталях: Модули от Джейсон Орендорфа — ищите по ключевому слову «Default exports»).
Давайте посмотрим на пример, и мы объясним, как это работает. В модуле square.js из нашего примера вы можете найти функцию randomSquare() , которая создаёт квардрат случайного цвета и размера со случайными координатами. Мы хотим экпортировать эту функции по умолчанию, поэтому в конце файла пишем следующее:
Обратите внимание на отсутствие фигурных скобок.
Кстати, можно было бы определить функцию как анонимную и добавить к ней export default :
В нашем файле main.js мы импортируем функцию по умолчанию, используя эту строку:
Снова обратите внимание на отсутствие фигурных скобок. Такой синтакис допустим, поскольку для каждого модуля разрешен только один экспорт по умолчанию, и мы знаем, что это randomSquare . Вышеупомянутая строка является сокращением для:
Примечание: «as» синтаксис для переименования экспортируемых элементов поясняется ниже в разделе Переименование импорта и экмпорта.
Поиск модулей
Все относительные пути, начинающиеся c './' или '../' будут обрабатываться только относительно рабочей папки проекта. Пути с '/' будут обрабатываться как абсолютные пути файловой системы. Для остальных случаев Node.js начинает поиск модулей в папке проекта node_modules (пример: /home/work/projectN/node_modules). В случае, если интересующий модуль не был найден, Node.js поднимается на уровень выше и продолжает свой поиск там. И так до самого верхнего уровня файловой системы. Поиск необходимой библиотеки будет выглядеть следующим образом:
Если в папках node_modules не удалось обнаружить искомый модуль, то в запасе у Node.js есть еще места, которые он анализирует в поисках необходимой библиотеки. Это так называемые GLOBAL_FOLDERS . В них добавляются пути, переданные через переменную окружения NODE_PATH , и три дополнительных пути, которые существуют всегда:
При желании можно посмотреть все возможные директории, где Node.js ищет модули из папки проекта, обратившись к методу paths() внутри require.resolve .
Заключение
Фронтенд-разработчики каждый день используют модули. Это может быть функция из локального файла или сторонняя библиотека из node_modules. Сегодня я кратко расскажу об основных модульных системах в JavaScript и некоторых нюансах их использования.
Пример использования модулей
Для того, чтобы продемонстрировать использование модулей, мы создали простой набор примеров, который вы можете найти на GitHub. В этих примерах мы создаём элемент на веб-странице и затем рисуем различные фигуры на нём (и выводим информацию об этом).
Примеры довольно тривиальны, но они намеренно сделаны простыми для ясной демонстрации модулей.
Примечание: Если вы хотите скачать примеры и запустить их локально, вам нужно будет запустить их через локальный веб-сервер.
Babel модуль: транспиляция ES модуля
Babel — это еще один транспилятор для обеспечения работы ES6+ кода в старых браузерах. Приведенный выше ES6+ модуль может быть преобразован в Babel модуль следуюшим образом:
А вот код в index.js , демонстрирующий использование этого модуля:
Это транспиляция по умолчанию. Babel также умеет работать с другими инструментами.
Babel и SystemJS
SystemJS может использоваться как плагин для Babel:
Данный плагин должен быть добавлен в babel.config.json :
Теперь Babel может работать с SystemJS для транспиляции CommonJS/Node.js, AMD/RequireJS и ES модулей:
Весь синтаксис AMD, CommonJS и ES модулей транспилирован в синтаксис SystemJS:
Импорт функционала в ваш скрипт
После того, как вы экспортировали некоторые функции из своего модуля, вам необходимо импортировать их в свой скрипт, чтобы иметь возможность использовать их. Самый простой способ сделать это:
Используйте конструкцию import , за которой следует разделенный запятыми список функций, которые вы хотите импортировать, заключённый в фигурные скобки, за которым следует ключевое слово from, за которым следует путь к файлу модуля — путь относительно корня сайта, который для нашего примера basic-modules будет равен /js-examples/modules/basic-modules .
Однако, мы написали путь немного иначе — мы используем ( . ) синтаксис, означающий "текущую директорию", за которым следует путь к файлу, который мы пытаемся найти. Это намного лучше, чем каждый раз записывать весь относительный путь, поскольку он короче и делает URL-адрес переносимым - пример все равно будет работать, если вы переместите его в другое место в иерархии сайта.
Вы можете найти подобные строки кода в файле main.js .
Примечание: В некоторых модульных системах вы можете опустить расширение файла и начальные / , ./ , or ../ (например 'modules/square' ). Это не работает в нативных JavaScript-модулях.
После того, как вы импортировали функции в свой скрипт, вы можете использовать их так же, как если бы они были определены в этом же файле. Следующий пример можно найти в main.js , сразу за строками импорта:
Примечание: Хотя импортированные функции доступны в файле, они доступны только для чтения. Вы не можете изменить импортированную переменную, но вы всё равно можете изменять свойства у const -переменных. Кроме того, переменные импортируется как "live bindings" - это означает, что они могут меняться по значению, даже если вы не можете изменить привязку, в отличие от const .
Функции
Функции - способ упаковки функциональности, которую вы хотите использовать повторно. Всякий раз, когда вам нужна определённая процедура, вы можете просто вызвать функцию по её имени, а не переписывать весь код каждый раз. Вы уже видели некоторые функции, описанные выше, например:
Эти функции, document.querySelector и alert , встроены в браузер для того, чтобы вы использовали их всякий раз, когда вам это необходимо.
Если вы видите что-то, что выглядит как имя переменной, но имеет после него скобки — () , скорее всего, это функция. Функции часто принимают аргументы — биты данных, которые им необходимы для выполнения своей работы. Они находятся в скобках, и разделяются запятыми, если присутствует более одного аргумента.
Например, функция alert() вызывает всплывающий блок, появляющийся в окне браузера, но мы должны дать ему строку в качестве аргумента, чтобы сказать функции, что писать во всплывающем блоке.
Хорошая новость заключается в том, что вы можете определить свои собственные функции — в следующем примере мы напишем простую функцию, которая принимает два числа в качестве аргументов и умножает их:
Попробуйте запустить вышеупомянутую функцию в консоли, затем попробуйте изменить аргументы, например:
Примечание: Инструкция return сообщает браузеру, что нужно вернуть переменную result из функции, которую можно будет использовать. Это необходимо потому, что переменные, определённые внутри функций, доступны только внутри этих функций. Это называется областью видимости (en-US) переменной. (Читайте больше об области видимости переменных.)
IIFE модуль: шаблон JS модуля
Определяя переменную в JS, мы определяем ее как глобальную переменную. Это означает, что такая переменная будет доступна во всех файлах JS, загружаемых на текущей странице:
Для того, чтобы избежать загрязнения глобального пространства имен, можно использовать анонимную функцию:
Вуаля, глобальных переменных больше нет. Однако код внутри функции не выполняется.
IIFE: немедленно вызываемое функциональное выражение
Для того, чтобы выполнить код внутри функции f , ее необходимо вызвать с помощью () как f() . Для выполнения кода внутри анонимной функции (() => <>) следует также использовать () . Это выглядит так (() => <>)() :
Это называется IIFE (немедленно вызываемым функциональным выражением). Модуль может быть определен следующим образом:
Мы оборачиваем код модуля в IIFE. Анонимная функция возвращает объект. Это заменяет интерфейс экспорта. Присутствует только одна глобальная переменная — название модуля (или его пространство имен). Впоследствии название модуля может использоваться для его вызова (экспорта). Это называется шаблоном JS модуля.
Примеси импорта
При определении модуля могут потребоваться некоторые зависимости. При использовании модульного шаблона каждый зависимый модуль — глобальная переменная. Зависимые модули могут определяться внутри анонимной функции или передаваться ей в качестве аргументов:
Ранние версии популярных библиотек, таких как jQuery, использовали этот шаблон (в последней версии jQuery используется UMD модуль).
Циклические зависимости
При большой вложенности модулей друг в друга может возникнуть циклическая зависимость:
Для наглядности, эту цепочку зависимостей можно упростить до:
ES-модули нативно умеют работать с циклическими зависимостями и корректно их обрабатывать. Принцип работы подробно описан в спецификации. Однако, ESM редко используются без обработки. Обычно с помощью транспилятор (Babel) сборщик модулей (например, Webpack) преобразует их в CommonJS для запуска на Node.js, или в исполнямый скрипт (bundle) для браузера. Циклические зависимости не всегда могут быть источником явных ошибок и исключений, но могут стать причиной некорректного поведения кода, которое трудно будет отловить.
Есть несколько хаков, как можно обходить циклические зависимости для некоторые ситуаций, но лучше просто не допускать их возниковения.
Webpack модуль: транспиляция и сборка CJS, AMD и ES модулей
Webpack — это сборщик модулей. Его транспилятор объединяет CommonJS, AMD и ES модули в единый сбалансированный модульный шаблон и собирает весь код в один файл. Например, в следующих 3 файлах определяются 3 модуля с помощью различного синтаксиса:
Следующий код демонстрирует использование этого модуля:
Webpack способен объединить эти файлы, несмотря на то, что они представляют собой разные модульные системы, в один файл main.js :
Поскольку Webpack основан на Node.js, он использует модульный синтаксис CommonJS. В webpack.config.js :
Для транспиляции и сборки необходимо выполнить следующие команды:
В результате Webpack создаст файл main.js . Следующий код из main.js отформатирован для улучшения читаемости:
И снова это всего лишь IIFE. Код из 4 файлов преобразован в массив из 4 функций. И этот массив передается анонимной функции в качестве параметра.
События
Для создания действительной интерактивности на веб-сайте вам необходимы события. События — это структура, которая следит за тем, что происходит в браузере, а затем позволяет вам запускать код в ответ на это. Наиболее очевидным является событие клика (en-US), которое вызывается браузером, когда мы щёлкаем по чему-то мышью. Для демонстрации этого события введите следующую команду в вашу консоль, а затем щёлкните по текущей веб-странице:
Существуют множество способов прикрепить событие к элементу. Здесь мы выбираем элемент и устанавливаем ему обработчик свойства onclick (en-US) анонимной функцией (т.е. безымянной) которая содержит код, который мы хотим запустить для события клика.
Обратите внимание, что
Просто так короче.
Базовая структура примера
В первом примере (см. директорию basic-modules) у нас следующая структура файлов:
Примечание: Все примеры в этом руководстве в основном имеют одинаковую структуру.
Давайте разберём два модуля из директории modules:
- canvas.js — содержит функции, связанные с настройкой canvas:
- create() — создаёт холст заданной ширины width и высоты height внутри -обертки с указанным id и помещённой в родителя parent . Результатом выполнения функции будет объект, содержащий 2D-контекст холста и id обертки.
- createReportList() — создаёт неупорядоченный список, добавленный внутри указанного элемента-обёртки, который можно использовать для вывода данных отчёта. Возвращает id списка.
- name — переменная со строковым значением 'square'.
- draw() — функция, рисующая квадрат на указанном холсте с заданными размером, положением и цветом. Возвращает объект, содержащий размер, положение и цвет квадрата.
- reportArea() — функция, которая выводит посчитанную площадь квадрата в указанный список отчета.
- reportPerimeter() — функция, которая выводи посчитанный периметр квадрата в указанный список отчета.
Прокачаем пример нашего веб-сайта
Теперь, когда мы прошли некоторые основы JavaScript, давайте добавим несколько крутых несложных функций в пример нашего сайта, чтобы дать вам некоторое представление о принципах работы.
Открытый модуль: шаблон открытого JS модуля
Шаблон открытого модуля был придуман Christian Heilmann. Этот шаблон также является IIFE, но акцент в нем делается на определении всех интерфейсов как локальных переменных внутри анонимной функции:
Такой синтаксис облегчает понимание того, за что отвечает (или что делает) каждый интерфейс.Условия
Условия — это конструкции в коде, которые позволяют проверить истинность или ложность выражения и выполнить другой код в зависимости от полученного результата. Самая распространённая форма условия — инструкция if . else . Например:
Выражение внутри if ( . ) — это проверка, которая использует тождественный оператор (как описано выше), чтобы сравнить переменную iceCream со строкой chocolate и увидеть равны ли они. Если это сравнение возвращает true , выполнится первый блок кода. Если нет, этот код пропустится и выполнится второй блок кода, после инструкции else .
Операторы
operator (en-US) — это математический символ, который производит результат, основанный на двух значениях (или переменных). В приведённой ниже таблице вы можете увидеть некоторые из наиболее простых операторов, наряду с некоторыми примерами, которые опробуете в JavaScript консоли.
Основное выражение true , но сравнение возвращает false , потому что мы отрицаем его:
var myVariable = 3;
!(myVariable === 3);Здесь мы проверяем " myVariable НЕ равно 3". Это возвращает false , потому что myVariable равно 3.
var myVariable = 3;
myVariable !== 3;Существует намного больше операторов для изучения, но этих пока хватит. Смотрите их полный список в разделе выражения и операторы.
Примечание: Смешивание типов данных может привести к некоторым неожиданным результатам при выполнении вычислений, поэтому будьте осторожны, правильно ссылайтесь на ваши переменные, чтобы получать ожидаемые результаты. Например, введите "35" + "25" в вашу консоль. Почему вы не получили результат, который вы ожидали? Потому, что кавычки превратили числа в строки, так что у вас в итоге получилась конкатенация строк, а не сложение чисел. Если вы введёте, 35 + 25 , то получите правильный результат.
Что происходит при повторном импорте модуля
В предыдущей главе мы упомянули метод Evaluate() . При очередном импорте модуля Evaluate() вызывается повторно, но если импорт модуля был успешно выполнен до этого, то метод возвращает undefined и сценарий модуля запущен не будет. Поэтому запись состояния модуля происходит единожды.
Но остался открытым вопрос, создаётся ли новая сущность Module Records при повторном импорте? Например в данном случае:
За получение Module Records для каждого import отвечает метод HostResolveImportedModule, который принимает два аргумента:
- referencingScriptOrModule — идентификатор текущего модуля, откуда происходит импорт;
- specifier — идентификатор импортируемого модуля, в данном случае ./modulePath .
В спецификации говорится, что для одинаковых парах значений referencingScriptOrModule и specifier возвращается один и тот же экземпляр Module Records.
Рассмотрим еще один пример, когда один и тот же модуль импортируется в нескольких файлах:
Будут ли здесь создаваться дублирующие Module Records для moduleB.js ? Для этого обратимся к спецификации:
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 динамический модуль: ECMAScript2020 или ES11 динамический модуль
В последней 11 версии спецификации JS 2020 года представлена встроенная функция import для динамического использования ES модулей. Данная функция возвращает промис, поэтому использовать модуль можно с помощью then :
Благодаря тому, что функция import возвращает промис, в ней может использоваться ключевое слово await :Что происходит в момент импорта ES-модуля
В момент выполнения файла Javascript-движок выполняет несколько этапов загрузки модулей:
- построение графа зависимостей;
- оценка расположения модулей и загрузка файлов;
- анализ модулей;
- запись информации о модулях и создание полей всех экспортируемых значений (без их состояний);
- выполнение сценария модулей для получение состояний;
- запись состояний экспортируемых частей модулей.
Структура данных, содержащая информацию о модуле (уникальный идентификатор, список зависимостей и состояния всех экспортируемых значений) называется Module Records.
При выполнении скрипта строится граф зависимостей и создается запись по каждому импортируемому модулю внутри него. В момент каждого импорта, вызывается метод Evaluate() внутри модуля Module Records. При первом вызове этой функции выполняется сценарий для получения и сохранения состояния модуля. Подробнее об этом процессе можно прочитать в статье «Глубокое погружение в ES-модули в картинках».Добавление смены изображения
В этом разделе мы добавим ещё одно изображение на наш сайт и добавим некоторый простой JavaScript для переключения между двумя изображениями, когда по ним щёлкнули.
- В первую очередь найдите другое изображение, которые вы хотели бы показать на вашем сайте. Убедитесь что оно такого же размера, как ваше первое изображение или максимально близкое к нему.
- Сохраните изображение в вашу папку images .
- Переименуйте это изображение в 'firefox2.jpg' (без кавычек).
- Перейдите в ваш файл main.js и введите следующий JavaScript. (Если ваш "hello world" JavaScript по-прежнему существует, удалите его.)
- Мы получаем значение из атрибута src изображения.
- Мы используем условие для проверки значения src, равен ли путь к исходному изображению:
- Если это так, мы меняем значение src на путь ко 2-му изображению, заставляя другое изображение загружаться внутри элемента .
- Если это не так (значит, оно должно было уже измениться), мы меняем значение src , возвращаясь к первоначальному пути изображения, каким он был изначально.
-
В index.html , добавьте следующую строку перед элементом :
import
BCD tables only load in the browser
ESM-модули
Именованный импорт/экспорт
В случае, когда необходимо экспортировать несколько сущностей из модуля, применяется именованный экспорт. Он выполняется с помощью инструкции export .
export можно использовать в момент объявления функции, переменной или класса:
Для больших модулей удобнее использовать группированный экспорт, это позволяет наглядно увидеть все экспортируемые сущности внутри модуля:
Чтобы импортировать какой-либо метод, необходимо воспользоваться инструкциeй import , указав интересующие части модуля и путь до него:
Импорт/Экспорт по умолчанию
В случае, когда из файла модуля экспортируется только одна сущность, удобнее использовать экспорт по умолчанию. Для этого необходимо добавить default после инструкции export :
Импорт модуля в случае экспорта по умолчанию:
Дополнительные возможности
Переименование. Для изменения имени метода в момент импорта/экспорта существует инструкция as :
Импорт этой функции будет доступен только по новому имени:
Переименование в момент импорта:
Этот синтаксис полезен для случаев, когда имя импортируемой части уже занято. Также можно сократить имя функции/переменной/класса, если она часто используется в файле:
Инициализация модуля без импорта его частей. Используется, когда необходимо выполнить импорт модуля для выполнения кода внутри него, но не импортировать какую-либо его часть:
Импорт всего содержимого модуля. Можно импортировать всё содержимое модуля в переменную и обращаться к частям модуля как к свойствам этой переменной:
Такой синтаксис не рекомендуется использовать, сборщик модулей (например, Webpack) не сможет корректно выполнить tree-shaking при таком использовании.
Реэкспорт. Существует сокращенный синтаксис для реэкспорта модулей. Это бывает полезно, когда нужно собрать модули из разных файлов в одном экспорте:
при таком реэкспорте наименования частей модуля будут сохраняться, но можно изменять их с помощью инструкции as :
Аналогичным способом можно реэкспортировать значения по умолчанию:
Динамические импорты. Кроме «статических» импортов можно загружать модули ассинхронно, для этого есть специальное выражение import() . Пример использования:
Это выражение возвращает promise , который при успешном завершении возвращает объект со всеми экспортами модуля, а при исключении — ошибку выполнения импорта. В Webpack синтаксис динамических импортов используется для создания отдельных чанков.
Использование модулей в браузере
Современные браузеры нативно поддерживают модули. Для того, чтобы браузер понимал, что мы экспортируем не просто исполняемый JS-файл, а модуль, необходимо в тэг script , где импортируется модуль, добавить атрибут type="module" .
Рассмотрим на примере небольшого проекта.
Импорт модуля внутри index.html:
По атрибуту type="module" браузер понимает, что экспортирует файл с модулями, и корректно его обработает. Стоит отметить, что пути импорта, указанные в main.js (./dist/module1 и ./dist/module2), будут преобразованы в абсолютные пути относительно текущего расположения, и браузер запросит эти файлы у сервера по адресам /dist/module1 и /dist/module2 соответственно. Практического применения у этой возможности не так много, в основном в проектах используется сборщик (например Webpack), который преобразует ESM-модули в bundle. Однако использование ESM-модулей в браузере может позволить улучшить загрузку страницы за счет разбиения bundle-файлов на маленькие части и постепенной их загрузки.
Подключение
Для работы понадобится всего один единственный тег
Указанный в data-main атрибуте файл main.js (расширение .js для краткости в RequireJS всегда опускается) является своеобразной точкой входа для выполнения JS-кода приложения.RequireJS имеет ряд параметров, которыми можно его сконфигурировать, поэтому перед началом выполнения кода они должны быть указаны с помощью метода requirejs.config .
При помощи первого параметра baseUrl можно указать путь, относительно которого будут загружаться все JS-файлы. Если файлы вдруг переедут в другое место, то достаточно в одном месте поменять корневой путь. Если его не указать, то базовой будет директория, в которой находится файл самой библиотеки require.js. Параметр path позволяет «мапить» пути к модулям, чтобы использовать потом более короткие варианты.Многие сторонние библиотеки и плагины уже оформлены в стиле AMD, то есть определены как модули с помощью функции define . Но есть такие, которые еще так не оформлены. Подключая подобные файлы, RequireJS не знает об их зависимостях и экспортируемом значении. Для таких модулей есть параметр shim , благодаря которому можно указать deps (зависимости) и exports (экспортируемое значение).
RequireJS позволяет загружать не только JS-файлы, но и, например, HTML, используя плагин text.
В данном примере require загрузит файл module.html и вернет строку, содержащую HTML-код файла. Это удобно для работы с клиентскими шаблонами, не нужно мучатся с HTML-кодом внутри JS-файлов.
export
BCD tables only load in the browser
RequireJS
В RequireJS методы require и define имеют несколько вариаций.
Метод define может принимать три параметра:
Первый параметр — это id модуля. id можно использовать вместо пути к файлу, чтобы подключить модуль как зависимость другого модуля, но только когда файл с кодом модуля уже был загружен в браузере. На самом деле, это необязательный параметр. И не просто необязательный, его даже нежелательно использовать в разработке. Он, скорее, нужен для корректного управления зависимостями в том случае, если в одном файле определено сразу несколько модулей. Оптимизационный инструмент, использующийся для сборки модулей в один файл для production, автоматически добавляет эти id.define может принимать только остальные два параметра:
В данном случае первый параметр — это массив зависимостей модуля. Чтобы определить зависимость, нужно просто добавить в массив строку, содержащую путь к модулю или его id. Последний параметр — функция-фабрика, которая занимается созданием модуля. Эта функция выполнится только тогда, когда все зависимости модуля будут загружены, и принимает в качестве аргументов экспортированные значения со всех зависимостей. Внутри функции находится реализация модуля, которая не доступна извне. В конце с помощью вызова return экспортируется сам модуль. Экспортировать можно все, что угодно: обычную функцию, конструктор, объект, строку; в общем, любой тип данных. Важно понимать, что функция-фабрика выполняется только один раз, когда мы впервые подключаем модуль как зависимость. Остальные модули, которые тоже подключат эту зависимость, получат уже закешированное значение модуля.Есть одна проблема — вызов define может выглядеть вот так:
Это довольно неудобно. Нужно следить, чтобы порядок указанных зависимостей в массиве совпадал с порядком аргументов, которые принимает функция-фабрика. Поэтому в RequireJS есть еще один вариант define , который позволяет передавать только один параметр — функцию-конструктор.
Этот вариант напоминает подход CommonJS. В качестве первого аргумента будет передана так называемая локальная функция require . Подключать зависимости модуля теперь можно с помощь вызова этой функции, а возвращать она будет те значения, которые экспортирует подключаемый модуль. Нужно помнить, что такой синтаксис функционально не отличается от предыдущего. Все файлы зависимостей, подключенные с помощью вызова require , будут загружены до того, как начнет выполнятся функция-фабрика модуля. Это происходит потому, что, когда файл модуля загружен в браузер, загрузчик ищет все локальные вызовы require с помощью регулярных выражений. По этой причине локальный вызов require нельзя использовать, например, для загрузки модуля по условию.Также можно определить модуль как простой объект.
Такие модули удобно использовать как набор констант.Для того, чтобы начать выполнение клиентской логики, нужно вызвать глобальную функцию require .
Этот вариант функции require отличается от локального варианта набором принимаемых параметров. В этом случае первым параметром нужно передавать массив подключаемых модулей. Даже если нужно подключить только один модуль, все равно нужно передавать его в массиве, иначе библиотека распознает его как локальный вызов и будет ошибка, так как локальные вызовы возможны только внутри определения модуля. Второй необязательный параметр — callback-функция, которая, как и в случае с define , выполнится сразу, как только будут загружены все необходимые зависимости.Есть возможность делать вложенные вызовы require внутри callback-функции или внутри определения модуля.
Еще одно существенное отличие такого варианта вызова require заключается в том, что подключаемые файлы начнут загружаться только тогда, когда поток выполнения дойдет до вызова функции. Благодаря этому есть возможность загружать модули только по требованию или по условию.Читайте также: