Модули ES6, Node.js и решение Майкла Джексона

В JavaScript никогда не было стандартного способа импорта и экспорта функций из исходного файла в другой. Что ж, в нем есть: глобальные переменные. Например:

<script src="https://code.jquery.com/jquery-1.12.0.min.js"></script><script>
// `$` variable available here
</script>

Это далеко не идеально по нескольким причинам:

  • У вас могут возникнуть конфликты с другими библиотеками, использующими те же имена переменных. Вот почему во многих библиотеках есть метод noConflict ().
  • Вы не можете правильно сделать циклические ссылки. Если модуль A зависит от модуля B и наоборот, в каком порядке мы помещаем теги <script>?
  • Даже если нет циклических ссылок, порядок, в котором вы помещаете теги <script>, важен, и его трудно поддерживать.

CommonJS спешит на помощь

Когда начали появляться Node.js и другие серверные решения JavaScript, они договорились о способе решения этой проблемы. Они создали более широкую спецификацию под названием CommonJS. Что касается проблемы импорта / экспорта, эта спецификация определяет функцию require(), которая вводится средой выполнения, и переменную exports для функциональности экспорта.

Примечание. CommonJS - не единственная спецификация. Есть и другие, такие как UMD, которые фактически могут использоваться как во фронтенде, так и в бэкэнде.

Со временем появилось множество инструментов, особенно для создания одностраничных приложений. Из-за большей базы кода во внешнем интерфейсе и необходимости совместного использования кода между интерфейсом и бэкендом многие инструменты, такие как browserify и webpack, начали реализовывать и понимать спецификацию CJS как способ обойти ограничения платформы: отсутствие хорошей модульной системы на базовой платформе (JavaScript и браузеры).

Это явно взлом, потому что браузер не реализует require() или exports. Эти инструменты реализуют эту функциональность, упаковывая весь код вместе. Подробнее о том, как работают сборщики пакетов JavaScript.

Как работают модули ES6 и почему Node.js еще не реализовал их

JavaScript сильно развивается, особенно с ES6, и эту проблему нужно было решить. Вот почему родились модули ES. Синтаксически они очень похожи на CJS.

Давайте сравним их. Вот как мы что-то импортируем в обе системы:

const { helloWorld } = require('./b.js') // CommonJS
import { helloWorld } from './b.js' // ES modules

Вот как мы экспортируем функциональность:

// CommonJS
exports.helloWorld = () => {
  console.log('hello world')
}
// ES modules
export function helloWorld () {
  console.log('hello world')
}

Очень похоже, правда?

Прошло много времени с тех пор, как Node.js реализовал 99% ECMAScript 2015 (он же ES6), но нам нужно подождать до конца 2017 года для поддержки модулей ES6. И он будет доступен только за флагом времени выполнения! Почему так долго внедряют модули ES6 в Node.js, если они так похожи на CJS?

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

Хотя модули ES не реализованы в Node.js, они уже реализованы в некоторых браузерах. Например, мы можем протестировать их в Safari 10.1. Давайте посмотрим на несколько примеров, и мы поймем, почему семантика так важна. Я создал эти три файла:

// index.html
<script type="module" src="./a.js"></script>
// a.js
console.log('executing a.js')
import { helloWorld } from './b.js'
helloWorld()
// b.js
console.log('executing b.js')
export function helloWorld () {
  console.log('hello world')
}

Что мы видим в консоли при ее запуске? Вот результат:

executing b.js
executing a.js
hello world

Однако тот же код с использованием CJS и запуском его в Node.js:

// a.js
console.log('executing a.js')
import { helloWorld } from './b.js'
helloWorld()
// b.js
console.log('executing b.js')
export function helloWorld () {
  console.log('hello world')
}

Дадут нам:

executing a.js
executing b.js
hello world

Итак ... он выполнил код в другом порядке! Это связано с тем, что модули ES6 сначала анализируются (без выполнения), затем среда выполнения ищет импорт, загружает их и, наконец, выполняет код. Это называется асинхронной загрузкой.

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

Node.js и веб-браузеры должны реализовать этот новый способ загрузки кода, сохранив предыдущий. Как они узнают, когда использовать одну систему, а когда другую? Браузеры знают это, потому что вы указываете это на уровне <script>, как мы видели в примере со свойством type:

<script type="module" src="./a.js"></script>

Однако откуда Node.js знает? По этому поводу было много дискуссий и было много предложений (сначала проверка синтаксиса, а затем решение, следует ли рассматривать его как модуль, определяя его в файле package.json,…). Наконец, утвержденное предложение было: Решение Майкла Джексона. Обычно, если вы хотите, чтобы файл загружался как модуль ES6, вы должны использовать другое расширение: .mjs вместо .js.

Расширение (.mjs) - причина, по которой его иногда называют решением Майкла Джексона.

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

Если вы хотите узнать больше о статусе внедрения модулей ES6 в Node.js, прочтите это обновление.

Заметка о Вавилоне

Babel реализует модули ES6, но… неправильно. Он не реализует полную спецификацию. Так что будьте осторожны, если вы используете Babel при переходе на реализацию собственных модулей ES6, у вас могут возникнуть побочные эффекты.

Чем хороши модули ES6 и как получить лучшее из обоих миров

Модули ES6 хороши по двум основным причинам:

  • Это кроссплатформенный стандарт. Они будут работать как в Node.js, так и в веб-браузерах.
  • Импорт и экспорт статичны. Так должно быть из-за того, как работает процесс загрузки. Помните, мы говорили, что среда выполнения сначала загружает файл, анализирует его, а затем, перед его выполнением, загружает зависимости? Это возможно только в том случае, если импорт и экспорт статичны. Вы не можете этого сделать import 'engine-' + browserVersion Это хорошо по одной причине: инструменты могут проводить статический анализ кода, выяснять, какой код на самом деле используется, и раскачивать дерево. Это особенно полезно при использовании сторонних библиотек: вы никогда не используете все функции, которые они предоставляют, поэтому вы можете удалить много байтов кода, которые пользователь никогда не выполнит.

Но означает ли это, что я больше не могу динамически импортировать функциональность? Для меня это очень полезно. Часто я делаю что-то вроде:

const provider = process.env.EMAIL_PROVIDER
const emailClient = require(`./email-providers/${provider}`)

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

Итак, что происходит с модулями ES6? Что ж, не волнуйтесь, есть предложение этапа 3 (а это значит, что оно, скорее всего, скоро будет одобрено), в котором добавлена ​​import() функция. Эта функция принимает путь и возвращает экспортированную функциональность как обещание.

Таким образом, с модулями ES6 и import () мы получим лучшее из обоих миров. 🚀

Модули ES6 великолепны, но для их внедрения потребуется время. Надеюсь, вся эта информация поможет вам подготовиться к тому, когда придет время!