Импорт и экспорт модулей в JavaScript
Прежде, чем разбирать такие языковые конструкции, как import и export, нужно знать что такое модули. Ранее, когда вэб-приложения представляли собой несложные странички с неким набором функционала, программистам не нужно было создавать большие скрипты. Скрипт помещался во внешний файл или в разметке HTML. Современные приложения состоят из огромных скриптов, которые значительно расширяют их функциональные возможности.
В JavaScript модули появились с появлением версии EcmaScript 2015. Постепенно модульность развивалась и обновлялась. Сегодня модули поддерживаются большинством браузеров, а также широко используется в Node JS. Теперь разберем что такое модули.
Модуль — это отдельный файл со скриптом на JavaScript. Когда проект состоит из тысяч строчек кода, то логично разделить его на отдельные файлы, каждый из которых отвечает за определенную функциональность. К примеру: в одном файле помещены классы, в другой важные функции, а основной алгоритм пишется в центральном html файле.
Модули можно использовать по принципу взаимодополнения благодаря директивам import и export. Это позволяет одному модулю заимствовать функциональность у другого, что требует лишь записи одой строчки кода.
Классическое подключение файлов со скриптами в html разметке осуществлялся так: <script src = «classes.js»></script>. Это достаточно ограниченный способ подключения так называемых модулей. Import и Exsport предоставляют куда расширенный и при этом простой способ подключения файлов js в приложение.
Отличие import от export
Стоит отметить, что import и export являются директивами, ссылающимися на отдельные модули, а также их классы, переменные и функции:
- Import. Директива для импортирования функциональности из других модулей.
- Export. Данная директива отвечает за указание на члены модуля, которые могут быть доступны за его пределами.
Рассмотрим применение директив на простом примере.
Пример:
// Допустим у нас есть файл functions.js, из которого мы желаем экспортировать функцию sum_of_integers
// в содержимом файла входят различные функции, в том числе и нашаexport sum_of_integers(x, y){ // Отмечаем функцию, которая будет доступна для импортирования
return x+y
}//здесь идут остальные функции файла…
Теперь давайте рассмотрим пример импортирования функции sum_of_integer.
Пример:
// Допустим у нас есть файл главного скрипта main_algoritm.js
// Теперь импортируем сюда нашу экспортируемую функцию sum_of_integersimport {sum_of_integers} from «functions.js»
// Теперь проверим есть ли в файле main_algoritm.js функция, взятая из внешнего файла
alert(sum_of_integer(10, 15)); // 25
Обратите внимание на строку from «functions.js». Здесь имеется в виду из какого файла(модуля) импортируется функция. Такая простая запись осуществлена благодаря тому, что наш файл находится в одной папке с модулем main_algoritm.js. Если эти модуль с функциями был бы в отдельной папке, то пришлось бы добавить точку и слэш, сигнализирующие о выходе из текущей папки. Выглядело бы это так: from «./functions/functions.js». Чтобы понять что тут написано, необходимо разобраться по частям:
- ./ — точка и слэш означает выход из текущей папки в котором находится главный файл скрипта(модуль) main_algoritm.js.
- functions — это название папки с файлом модуля.
- functions.js — название файла(модуля) с расширением javascript.
Особенности модулей
Есть ряд правил, которым подчиняются модули. Их нужно обязательно учитывать! Иначе придется столкнуться с неожиданными ошибками.
Модули подчиняются следующим правилам:
- Если модуль не является отдельным файлом, а создается в основном коде HTML, то в нем действует режим «use strict» При этом, в HTML наличие модуля указывается в атрибуте <script type = «module»></script>.
- Область видимости членов модуля локальное. Все переменные и функции без директивы export не видны в других скриптах. Для каждого модуля в браузере своя отдельная область видимости. Это правило действует не зависимо от того, каким способом были добавлены в скрипт модули — импортированием или за счет атрибута «module».
- Все инструкции модуля выполняются только один раз, даже если один и тот же модуль будет использован несколько раз в разных местах. Допустим если определить в модуле функцию alert(«выполнено»); и три раза импортировать этот модуль, то alert выполнится только при первом импорте модуля. При последующих импортах alert не выполнится. При этом, если мы импортируем объект в несколько файлов, то объект будет создан при первом импорте, а остальным файлам будет передаваться ссылка на него. Если в этом объекте изменить что-либо, то и остальные импортеры также получат изменения.
- Задача импорта — инициализация его членов. Для их многократно, требуется импортировать нужные переменные и функции их модуля или весь модуль.
- Информационным объектом модулей является import.meta. Он позволяет получить информацию о модуле. К примеру, если вывести на экран import.meta.url, то мы получим ссылку на текущий модуль. Опять таки, свойства конкретного модуля можно получить, если обращаться к этому объекту внутри модуля. Снаружи модуля данный объект равен undefined.
- Внутри модуля отсутствует ссылка this. Если же в основном скрипте this == window, то в модуле this == undefined.
- При определении модуля в браузере или его подключении посредством атрибута src, код модуля будет выполняться только после полной загрузки HTML страницы.
- Инструкции в модуле выполняются в порядке очереди по всем правилам «use strict» режима.
- В модуле после полной загрузки страницы доступны все элементы HTML. Когда обычному скрипту доступны только те объекты, которые загрузились до тега <script>.
- Скрипт с кодом на странице выполняется первым, а затем скрипт модуля.
- Нельзя импортировать модуль, не указав расширение его файла.
- Браузеры старых версий не распознают атрибут type = «module». Поэтому, для старых браузеров нужно скопировать модуль, но добавить вместо type, атрибут без значения «nomodule». В новых браузерах выполнится модуль с любым из этих атрибутов. В старых только с атрибутом nomodule.
Директива Export
Еще раз следует учитывать, что директива export отмечает члены модуля, которые можно импортировать. Ранее мы рассматривали простое использование данной директивы. Однако есть несколько вариантов ее вызова.
Экспортирование до объявления и инициализации
Этот вариант предусматривает экспорт, установленный до объявления переменной.
Пример:
// экспортируем переменную
export var x = 10;
// массив
export var arr = [10, 20, 30, 40];
// класс
export class Device{
constructor(type){
this.type = type;
}
}
Обратите внимание, что экспорт не является выражением и не предусматривает фигурных скобок. Данная директива является ключевым словом и лишь дополняет объявление членов модуля.
Экспорт как отдельная конструкция
Отдельная конструкция — экспорт в одну строчку без указания ключевого слова перед каждым членом модуля. Это более сокращенная запись, где можн через запятую указать множество членов модуля.
Пример:
// Представим, что у нас есть модуль mod.js со следующими функциями и переменными
var str = «hello»;
function print(){
alert(«everything are good»);
}// экспортируем все в одну строчку
export {str, print} // в фигурных скобках через запятую перечисляем экспортируемые члены модуля
Не обязательно экспортировать в самом низу модуля. Благодаря встроенной технологии hoisting declaration конструкцию экспортирования можно писать в любом участке модуля, даже в самом начале до объявления остальных членов.
Экспорт через ключевое слово «as»
Это необходимо для того, чтобы изменить имена длинных экспортируемых идентификаторов.
Пример:
// сократим имена переменной и функции нашего модуля
export {str as s, print as prt};// вызов по псевдонимам
alert(s); // hello
prt(); // everything are good
Импорт и различные способы импортирования
Импорт членов модуля позволяет использовать эти члены в основном коде. Есть два способа импорта.
Импорт через запятую в одну строчку
Способ аналогичен экспорту в одну строчку.
Пример:
// Будем использовать модуль из прошлого примера
// импортируем все что было отмечено экспортомimport {str, print} from «mod.js»;
Стоит отметить, что нет смысла импортировать те члены, которые могут не понадобиться в коде. Это создает утечки памяти. При этом, если использовать webpack, то его система оптимизирует импорт и удаляет из буфера те члены модуля, которые помечены экспортом, но еще небыли импортированы.
Импорт через ключевое слово «as»
Данная конструкция аналогична экспорту, однако есть и одна особенность.
Пример:
import {st as str, pr as print} from «mod.js»;
// Также можно использовать оператор «*’, который позволяет импортировать все из модуля
import * as obj from «mod.js»;
obj.str; // hello
obj.print(); // everything are good
В последней конструкции происходит следующее:
- При импортировании всех членов модуля через звездочку, создается объект с идентификатором, указанным после as.
- Импортированные функции и переменные становятся экземплярами этого объекта, то есть — свойствами и методами.
- Соответственно, обращение к ним происходит через ссылку (идентификатор объекта).
Экспортирование по умолчанию
Экспорт по умолчанию применяется в том случае, если дерево папок с модулями хорошо структурировано и в каждом модуле находится минимальное количество членов. Желательно, чтобы модуль отвечал за экспорт чего-то одного.
Экспортирование по умолчанию предусматривает вставку ключевого слова default. Тогда при импорте можно опустить фигурные скобки и вся конструкция получится гораздо красивее.
Пример:
/Представим, что у нас есть дерево модулей
classes/
User
Car
House
functions/
sum
multy//Мы по умолчанию хотим экспортировать User
export default class User { // все, что нужно сделать, добавить ключевое слово default
constructor(name) {
this.name = name;
}
}// импортируем этот класс по умолчанию
import User from «user.js»;
// класс импортирован и в дальнейшем мы можем создавать объекты на его основе
Пример:
// Рассмотрим отличия экспорта по умолчанию и обычного экспорта
// значение экспортируемое по умолчанию
import exportedFunction from «exporting-module.js»;// обычное импортирование членов модуля без пометки default
import { constantString, constantNumber } from «exporting-module.js»;
Это не приводит к путанице со стороны module.exports против exportsи дает приятный, звучащий по-человечески синтаксис! Определенно есть проекты, которые еще предстоит перенести на Node.js версии 14 и выше и поэтому не могут использовать этот новый синтаксис. Однако, если у вас есть шанс (потому что вы начинаете новый проект или ваш проект был успешно перенесен на Node.js 14 и выше), нет причин не переключаться на этот удивительный футуристический способ решения задач.
Второй способ импортирования по умолчанию выполняется с ключевым словом «as».
Пример:
// Допустим у нас есть в модуле следующая функция
function setData(a, b, c){
return [a, b, c];
}// теперь экспортируем данную функцию по умолчанию
export {setData as default};
Зачем вообще нужны модули?
Как уже упоминалось ранее, модули предоставляют отдельный контекст для различного рода переменных и функций. Если просто подключить внешний скрипт без атрибута default, то все идентификаторы скрипта будут доступны на всей странице. Когда в модулях мы можем отмечать за счет экспорта какие члены могут быть использованы в общем скрипте. Это не единственное преимущество модулей.
Модули организовывают как библиотеки классов, шаблонов проектирования и для расширения функциональности приложения коротким путем. Однажды созданная библиотека модулей может быть использована в различных проектах. При этом, программист сам выбирает какие члены модуля могут быть экспортированы и импортированы.
Ошибочно воспользоваться ненужными членами модуля не получится. Это некое подобие инкапсуляции данных, не доступных во внешнем коде без разрешения программиста.
Webpack
Для облегченного использования модулей существует сборщик, который называется Webpack. В проектах webpack обрабатывает все файлы и внешние скрипты как модули. Сборка проекта подразумевает основу графа зависимости, на которую опирается webpack.
Эта зависимость описывает взаимосвязь и взаимодействие модулей между собой в проекте. Сборщик перемещается по всем модулям проекта и на их основе создает граф, который в последствии генерируется в одиночный собранный бандл.
Бандлом является внешний файл JS, в котором собран код всех модулей. Здесь модули объединены в определенном и правильном с точки зрения сборки порядке. При создании бандла, webpack выполняет единоразово весь код всех модулей, то есть — бандла.
Принцип работы Webpacka
Webpack настраивает главный HTML файл проекта index.html из всех файлов, на которые он ссылается. Это внешние файлы JS, CSS и другие. Сборщик обрабатывает каждый модуль, с которым сталкивается. Конечно делается это после загрузки всей страницы, ведь модули выполняют свой код именно так. Как только сборщик обработал все модули, он создает несколько бандлов, но чаще всего один.
В этой статье мы не будем рассматривать установку и настройку webpack. Это отдельная и продолжительная тема, так как сборщик является достаточно мощным инструментом. Его настройка чем-то похожа на настройку Gulp. Здесь также создается package.Json.
Пример: Содержание package.json
//Установка в командной строке осуществляется так
npm install webpack webpack-cli —save-dev
//Содержание файла
{
«name»: «learn_webpack»,
«version»: «1.0.0»,
«description»: «»,
«main»: «index.js»,
«scripts»: {
«test»: «echo «Error: no test specified» && exit 1″
},
«keywords»: [],
«author»: «»,
«license»: «ISC»,
«devDependencies»: {
«webpack»: «^4.30.0»,
«webpack-cli»: «^3.3.0»
}
}
Вот список некоторых составляющих сборщика, знание которых может пригодиться:
- Entry. Модуль построения графа зависимостей.
- Output. Свойство, указывающее путь сохранения бандла и названия его файла.
- Загрузчики. Специальные компоненты, которые конвертируют модули с других языков в JavaScript.
- Плагины. Расширяют функционал сборщика.
- Режимы. Есть три режима: development, production и none. Режим none установлен по умолчанию и отключает различные дополнительные опции для сборщика. Остальные два режима предоставляют разные опции для оптимизации сборки модулей.
Итог
Модули используют для импортирования и экспортирования данных и функций. Это отдельные файлы с кодом, который можно добавить в основной код страницы. В каждом модули локальная область видимости, что не позволяет просто обращаться к их членам, но только к экспортируемым после импорта.
Проект может использовать множество и один модуль. Для больших проектов подключают десятки модулей. Чтобы упростит работу с модулями используют сборщик webpack, который исключает большое количество рутинной работы для программиста.
Однако на изучение использования сборщика понадобится какое-то время, так как он также работает, исползуя синтаксис javascript и не имеет визуальной среды управления.