Перевод статьи Understanding Modules and Import and Export Statements in JavaScript.
Содержание
Введение
С первых дней существования Интернета веб-сайты разрабатывались в основном на HTML и CSS. И если JavaScript код все-таки загружался на страницу то, это обычно происходило в виде небольших фрагментов, которые обеспечивали простейшие визуальные эффекты или попытки сделать страничку более интерактивной. Поэтому программы на JavaScript чаще в объеме одного файла помещались в тег script
. Разработчик мог также разбить код JavaScript на несколько скриптов (файлов), но даже при таком подходе все переменные и функции находящиеся в них будут добавлены в общую глобальную область видимости.
В настоящее время, при разработке современных приложений на Javascript, существует необходимость в использовании стороннего кода для решения общих задач, разделения кода на файлы отдельных модулей, а также внедрения комплекса мер для защиты от засорения глобального пространства имен.
Спецификация ECMAScript 2015 представила новое средство языка JavaScript: модули. Которые позволяют использовать ключевые слова import
и export
, уже наверняка знакомые по другим языкам программирования. Из этого руководства вы узнаете, что такое модуль JavaScript, и как использовать инструкции import
и export
для правильной организации кода в соответствии с требованиями спецификации.
Модульное программирование
До появления концепции модулей в JavaScript, в случае если разработчик хотел организовать свой код как отдельные сегменты кода, он создавал несколько файлов и связывал их как отдельные сценарии. Чтобы продемонстрировать этот подход, создадим для примера следующие файлы: index.html
и два файла JavaScript, functions.js
и script.js
.
В файл index.html
будет отображаться сумма, разность, произведение и частное двух чисел, в него же в тегах script
поместим ссылку на два JavaScript файла.
И так создайте файл index.html
в текстовом редакторе и добавьте в него следующий код:
JavaScript Modules Answers
and
Addition
Subtraction
Multiplication
Division
Этот HTML код будет отображать в h2
заголовке страницы значения переменных x
и y
, а также результат операций с этими переменными в следующих за ним элементах p
. Значения атрибутов id
для элементов устанавливаются путем манипулирования DOM , которое будет производиться в коде script.js
. Этот код также будет устанавливать начальные значения переменных x
и y
.
Файл functions.js
содержит математические функции, которые используются в первом сценарии script.js
.
Откройте файл functions.js
и добавьте в него следующий код:
function sum(x, y) { return x + y } function difference(x, y) { return x - y } function product(x, y) { return x * y } function quotient(x, y) { return x / y }
И наконец, код в файле script.js
определяет значения переменных x
и y
, применяет к их значениям математические функции и отображает результат:
const x = 10 const y = 5 document.getElementById('x').textContent = x document.getElementById('y').textContent = y document.getElementById('addition').textContent = sum(x, y) document.getElementById('subtraction').textContent = difference(x, y) document.getElementById('multiplication').textContent = product(x, y) document.getElementById('division').textContent = quotient(x, y)
После подключения к index.html
Javascript файлов, можете открыть его в браузере, где увидите следующий результат.
Для веб-сайтов в которых используются несколько небольших скриптов это достаточно эффективный способ: просто разделить код по разным скриптам (файлам). Однако с этим подходом связаны некоторые проблемы, в том числе:
-
Загрязняется глобальное пространство имен. Все переменные, которые вы создали в своих сценариях:
sum
,difference
и т.д., теперь находятся в глобальном объектеwindow
. И если вы попытаетесь использовать другую переменную с именемsum
, вызываемую в другом файле, то будет трудно разобраться, какое значение будет использоваться в текущем сценарии, поскольку все они будут использовать одну и ту же глобальную переменнуюwindow.sum
. Единственный способ сделать переменную приватной private: это поместить ее в область действия некоторой функции. Однако при этом может возникнуть конфликт, например, между значениемx
атрибутаid
элемента DOM и переменнойvar x
. -
Усложняется управление зависимостями. Сценарии должны загружаться в заданном порядке следования сверху вниз, чтобы корректно обеспечить доступность всех переменных в нужное время. Сохранение сценариев в виде разных файлов создает иллюзию разделения, но по сути это то же самое, что сохранение всего Javascript кода вместе в inline теге
.
Прежде чем ES6 добавил собственные модули к языку JavaScript, сообщество попыталось предложить несколько решений. Первые решения были написаны на ванильном JavaScript, например, весь код записывался в объект или немедленно вызываемое функциональное выражение (IIFE), а затем помещался в один объект в глобальном пространстве имен. Это было лучшим решением, чем подход использования нескольких файлов скриптов, но по-прежнему возникали те же проблемы с наличием одного объекта в глобальное пространстве имен и это не решало проблемы эффективного разделения кода сторонних библиотек.
После этого появилось несколько других решений, реализующих модульность Javascript кода: CommonJS , синхронный подход, реализованный в Node.js, Asynchronous Module Definition (AMD), который реализовывал асинхронный подход, а также Universal Module Definition (UMD) , который задумывался как универсальный подход, поддерживающий оба предыдущих стиля разделения кода.
Появление этих решений упростило разработчикам совместное и повторное использование кода в форме пакетов, аналогов модулей, которые можно распространять и совместно использовать, например, их можно найти на npm . Несмотря на то, что было разработано много решений, ни одно из них не было непосредственно встроено в язык JavaScript, и для использования модулей в браузерах пришлось реализовать такие инструменты, как Babel , Webpack или Browserify .
Из-за множества проблем, связанных с многофайловым подходом и сложностью предложенных решений, разработчики были заинтересованы в переносе модульного подхода к программированию в язык JavaScript. По этой причине стандарт ECMAScript 2015 теперь поддерживает использование модулей JavaScript.
Модуль представляет собой сборку кода bundle и выступает в качестве интерфейса для обеспечения функциональных возможностей использования отдельных модулей. Модуль экспортирует свой код и импортируется для использования в другом коде. Модули полезны тем, что позволяют разработчикам повторно использовать код, при этом обеспечивают стабильный код и понятный интерфейс к нему, который могут использовать другие разработчики, при этом их использование не загрязняет глобальное пространство имен.
Модули (называемые также модулями ECMAScript или модулями ES) теперь доступны нативно (встроены в JavaScript), и в оставшейся части этого руководства вы узнаете, как использовать их в своем коде.
Модули в JavaScript
Ключевые слова import
и export
используются для работы с модулями в JavaScript:
-
import
: применяется для чтения кода, экспортируемого из другого модуля. -
export
: применяется для предоставления кода другим модулям.
Чтобы продемонстрировать, как это можно использовать на практике, обновим файл functions.js
до модуля и экспортируем его функции. Для этого просто добавим ключевое слово export
перед каждой функцией, что автоматически сделает их код доступным для любого другого модуля.
Добавим в файл следующий код:
export function sum(x, y) { return x + y } export function difference(x, y) { return x - y } export function product(x, y) { return x * y } export function quotient(x, y) { return x / y }
Теперь в верхней части файла script.js
, нам необходимо использовать ключевое слово import
для извлечения (импорта) кода функций из модуля functions.js
.
Примечание : инструкции кода с ключевым словом
import
всегда должны быть вверху файла перед любым другим кодом, при этом необходимо указать относительный (./
в нашем случае случае), либо абсолютный путь к импортируемому файлу модуля.
Добавьте следующий код в файл script.js
:
import { sum, difference, product, quotient } from './functions.js' const x = 10 const y = 5 document.getElementById('x').textContent = x document.getElementById('y').textContent = y document.getElementById('addition').textContent = sum(x, y) document.getElementById('subtraction').textContent = difference(x, y) document.getElementById('multiplication').textContent = product(x, y) document.getElementById('division').textContent = quotient(x, y)
Обратите внимание, что отдельные функции, импортируются из модуля путем помещения их имени в фигурные скобки.
Чтобы этот код загружался в браузере как модуль, а не как обычный скрипт, добавьте в файл index.html
к тегу script
атрибут type="module"
. Любой код, который использует инструкции import
или export
должен содержать указанный атрибут при подключении соответствующего файла модуля:
Теперь вы сможете перезагрузить обновленную страницу, и ваш веб-сайт будет использовать нативный механизм модулей Javascript. Поддержка браузерами этой новой возможности достаточно высока, используйте сервис caniuse, чтобы узнать какие браузеры ее поддерживают. Обратите внимание, что если вы загружаете файл модуля путем помещения в значение атрибута src
прямой ссылки на локальный файл, то столкнетесь со следующей ошибкой:
Access to script at 'file:///Users/your_file_path/script.js' from origin 'null' has been blocked by CORS policy: Cross-origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.
Это следствие политики CORS, согласно которой модули должны использоваться на серверной стороне, которая настраивается локально на компьютере с помощью http-сервера или в сети Интернет с помощью хостинг-провайдера.
Модули отличаются от обычных скриптов несколькими существенными особенностями:
- Модули ничего не добавляют в global глобальную область видимости (или в объект
window
). - Модули всегда выполняются в строгом режиме strict mode.
- Загрузка одного и того же модуля дважды в один и тот же файл не будет иметь никакого эффекта, поскольку модули выполняются только один раз.
- Работа с модулями требует серверной среды выполнения кода.
Модули по-прежнему широко используются при работе со сборщиками кода такими, как Webpack, для обеспечения поддержки браузерами дополнительных возможностей языка, а также доступны для использования непосредственно в браузерах.
Далее мы изучим еще несколько эффективных способов использования синтаксиса import
и export
.
Именованный экспорт
Как уже говорилось ранее, использование синтаксиса export
позволяет выборочно импортировать именованные инструкции кода, которые экспортируются из модуля по имени. Например, рассмотрим следующую упрощенную версию файла functions.js
:
export function sum() {} export function difference() {}
Этот код позволяет импортировать функции sum
и difference
по их имени, используя фигурные скобки:
import { sum, difference } from './functions.js'
Также можно использовать синтаксис псевдонимов для переименования импортируемых функций. Эта возможность позволяет избежать конфликтов имен в импортирующем файле. В следующем примере функция sum
будет переименована в add
, difference
— в subtract
.
import { sum as add, difference as subtract } from './functions.js' add(1, 2) // 3
Вызов функции add()
эквивалентен вызову функции sum()
.
Используя *
синтаксис, вы можете импортировать содержимое всего модуля в составе одного объекта. В этом случае функции sum
и difference
будут доступны как методы объекта mathFunctions
:
import * as mathFunctions from './functions.js' mathFunctions.sum(1, 2) // 3 mathFunctions.difference(10, 3) // 7
Примитивные значения, выражения, определения функций , асинхронные функции , классы и экземпляры классов могут быть экспортированы, если у них есть свой идентификатор:
// Примитивные значения export const number = 100 export const string = 'string' export const undef = undefined export const empty = null export const obj = { name: 'Homer' } export const array = ['Bart', 'Lisa', 'Maggie'] // Функциональное выражение export const sum = (x, y) => x + y // Определение функции export function difference(x, y) { return x - y } // Асинхронная функция export async function getBooks() {} // Класс export class Book { constructor(name, author) { this.name = name this.author = author } } // Экземпляр класса export const book = new Book('Lord of the Rings', 'J. R. R. Tolkien')
Теперь все экспортируемое можно успешно импортировать. Другой вид экспорта, с которым мы познакомимся в следующем разделе, известен как экспорт по умолчанию.
Экспорт по умолчанию
В предыдущих примерах мы успешно экспортировали несколько именованных инструкций кода, а затем импортировали их по отдельности или в составе одного объекта, в котором импортируемый код содержался в его свойствах и методах. Модули Javascript поддерживают возможность экспорта по умолчанию с использованием ключевого слова default
. Экспорт по умолчанию не использует фигурные скобки и содержимое модуля будет импортироваться непосредственно в идентификатор с заданным вами именем.
Перепишем код файла functions.js
в следующем виде:
export default function sum(x, y) { return x + y }
В файле script.js
импортируем функцию sum
по умолчанию, как это показано в коде ниже:
import sum from './functions.js' sum(1, 2) // 3
Использование этой конструкции кода может привести к неожиданным ошибкам, поскольку нет ограничений на то, что вы можете назначить в качестве имени идентификатора экспорта по умолчанию, и что, соответственно, будете импортировать в инструкции импорта. В следующем примере из модуля по умолчанию экспортируется функция sum
, а в переменную difference
в инструкции импорта будет фактически импортирована функция sum
:
import difference from './functions.js' difference(1, 2) // 3
По этой причине рекомендуется использовать именованный экспорт из модуля, так как экспорт по умолчанию не требует идентификатора для передачи в него экспортируемого кода. Экспортом же по умолчанию обычно экспортируются примитивные значения или анонимные функции. Ниже приведен пример объекта, экспортируемого по умолчанию:
export default { name: 'Lord of the Rings', author: 'J. R. R. Tolkien', }
Импортировать объект book
можно следующим образом:
import book from './functions.js'
Точно так же в следующем примере демонстрируется экспорт по умолчанию анонимной стрелочной функции:
export default () => 'This function is anonymous'
Функция импортируется из модуля файла script.js
следующим образом:
import anonymousFunction from './functions.js'
Именованный экспорт и экспорт по умолчанию можно использовать совместно друг с другом, как в модуле из примера ниже, который экспортирует два именованных значения и одно значение (функцию) по умолчанию:
export const length = 10 export const width = 5 export default function perimeter(x, y) { return 2 * (x + y) }
Импортируются эти переменные и функция следующим образом:
import calculatePerimeter, { length, width } from './functions.js' calculatePerimeter(length, width) // 30
Теперь в скрипте доступны как значение по умолчанию, так и именованные значения.
Вывод
Методы использования модульного подхода программирования позволяют разделять код на отдельные компоненты, что делает ваш код более пригодным для повторного использования, согласованным, а также защищает глобальное пространство имен от загрязнения. Интерфейс модуля может быть реализован в нативном JavaScript с использованием ключевых слов import
и export
.
В этой статье вы узнали об истории модулей в JavaScript, как разделить файлы с кодом в JavaScript на несколько сценариев для имитации модульности, как обновить содержимое этих файлов с использованием модульного подхода и инструкций с ключевыми словами import
и export
, а также как использовать именованный экспорт и экспорт по умолчанию.