Модули 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 великолепны, но для их внедрения потребуется время. Надеюсь, вся эта информация поможет вам подготовиться к тому, когда придет время!