Skip to content

Latest commit

 

History

History

james-snell-an-update-on-es6-modules-in-node-js

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Как продвигается внедрение ES6-модулей в Node.js

Перевод заметки James M Snell: An Update on ES6 Modules in Node.js.

Несколько месяцев назад я написал статью, описывающую различия, существующие между CommonJS модулями в Node.js и новыми ES6-модулями. Я также описал ряд проблем, связанных с внедрением новой модели в ядро Node.js. Здесь я хочу поделиться информацией о том, как идут дела.

Самый важный вопрос — «когда»

Если вы ещё этого не сделали, прежде чем двигаться дальше, воспользуйтесь моментом, чтобы прочитать мой первый пост, поскольку он описывает многие фундаментальные различия между двумя модульными архитектурами. Сведём его до самых простых понятий: ключевое различие между модулями CommonJS и ES6 сводится к тому, когда форма модуля становится известна и может быть использована кодом.

Например, предположим, что у меня есть следующий простой CommonJS-модуль (давайте назовем его foobar):

function foo() {
  return 'bar';
}
function bar() {
  return 'foo';
}
module.exports.foo = foo;
module.exports.bar = bar;

Теперь давайте используем этот модуль в *.js файле с именем app.js:

const {foo, bar} = require('foobar');
console.log(foo(), bar());

Когда я запускаю $node app.js, бинарник Node.js загружает файл app.js, анализирует его и начинает исполнять код. При исполнении вызывается функция require(), которая синхронно загружает содержимое foobar.js в память, синхронно анализирует и компилирует код JavaScript, и синхронно выполняет код, возвращая значение module.exports в качестве возвращаемого значения require('foobar') в app.js. Как только функция require() возвращает ответ в app.js, форма модуля foobar известна и может быть использована. Все это происходит в течение одного и того же цикла событий Node.js.

Критически важным для понимания разницы между модулями CommonJS и ES6 является тот факт, что форма (API) модуля CommonJS не может быть определена до тех пор, пока код не будет выполнен - и даже после выполнения форма может быть изменена другим кодом в любое время.

Вот «эквивалентный» модуль, написанный с использованием синтаксиса ES6:

export function foo() {
  return 'bar';
}
export function bar() {
  return 'foo';
}

И код, использующий его:

import {foo, bar} from 'foobar';
console.log(foo());
console.log(bar());

То, что происходит с модулем ES6, в соответствии со стандартом ECMAScript, представляет собой совсем другой набор шагов, чем то, что реализовано в случае CommonJS. Первый шаг: загрузка содержимого файла с диска в основном такая же, но может происходить асинхронно. Когда содержимое файла доступно, оно анализируется. При синтаксическом анализе форма модуля, определяемая инструкциями экспорта, определяется до выполнения кода. Как только форма определена, код исполняется. Важно помнить, что все инструкции import и export решают их цели до того, как какой-либо код будет выполнен на самом деле. Важно также отметить, что спецификация ES6 позволяет исполнить этот шаг разрешения асинхронно. В терминах Node.js это означает, что загрузка кода приложения, разрешение импортов и экспортов модуля и исполнение кода модуля будут происходить в течении нескольких циклов Event Loop.

Тайминги - это всё

Одна из ключевых целей, которые мы впервые изложили при оценке возможности внедрения ES6-модулей, заключается в обеспечении максимально возможной реализации. Например, мы надеялись, что можно будет реализовать поддержку обеих моделей таким образом, чтобы сделать её в значительной степени прозрачной для пользователя (например, require('es6-module') и import from 'commonjs-module' будут «просто работать»).

К сожалению, это не так просто.

В частности, поскольку модули ES6 загружаются, разрешаются и исполняются асинхронно, невозможно будет сделать require() для ES6 модуля. Причина в том, что require() является полностью синхронной функцией. Изменение семантики require() для разрешения асинхронной загрузки было бы чересчур разрушительным для экосистемы. Поэтому мы рассматриваем возможность реализации функции require.import(), которая сконструирована на основе предложенной ES6 функции import() (см. здесь). Эта функция возвращает Promise, завершающийся после загрузки ES6-модуля. Это не оптимально, но это позволит использовать ES6-модули из существующего Node.js кода в CommonJS стиле.

Однако, одна из хороших новостей заключается в том, что легко использовать CommonJS-модули внутри ES6-модуля, используя оператор import. Это связано с тем, что асинхронная загрузка не всегда требуется. Есть ряд модификаций спецификации языка ECMAScript необходимых для лучшей поддержки этого, но когда всё будет сделано, это должно «просто работать».

Есть одно существенное но...

Увы, бедный именованный импорт

Именованный импорт - фундаментальная особенность ES6 модулей. Например, в примере:

import {foo, bar} from 'foobar';

Переменные foo и bar импортируются из foobar во время фазы разрешения - до того, как какой-либо код действительно будет исполнен. Это возможно в мире ES6 модулей, потому что форма модуля известна заранее.

С CommonJS, с другой стороны, форма модуля не известна до тех пор, пока код не будет исполнен. Это означает, что без внесения существенных изменений в спецификацию языка ECMAScript невозможно будет использовать именованные импорты из CommonJS-модуля. Вместо этого разработчикам будет необходимо использовать то, что ES6-модули называют экспортом по умолчанию. Например, при использовании примера CommonJS модуля из начала этого поста, импортом, использующим его, будет:

import foobar from 'foobar';
console.log(foobar.foo(), foobar.bar());

Разница здесь тонкая, но важная. При использовании инструкции import для импорта из CommonJS-модуля, будет просто невозможно использовать синтаксис:

import {foo, bar} from 'foobar';

И вы не сможете разрешить в foo и bar функции foo() и bar(), экспортируемые CommonJS-модулем.

Но это работает в Babel!

Любой, кто в настоящее время использует транслятор (например, Babel) для работы с синтаксисом ES6-модулей, вероятно, знаком с использованием именованных импортов. Как работает Babel? ES6 синтаксис преобразуется в код в стиле CommonJS, который может работать в Node.js. Хотя синтаксис соответствует ES6, реализация таковой не является. Это крайне важно понять. ES6 именованные импорты в Babel принципиально не то же самое, что именованные импорты, использующие полную реализацию спецификации.

Майкл Джексон скрипт

Ещё одно ключевое отличие CommonJS модулей от ES6 заключается в том, что компилятор ECMAScript кода должен заранее знать, загружает ли он код модуля CommonJS или ES6. Причина этого кроется в том, что модули ES6 требуют разрешения конструкций import и export до того, как код будет исполнен.

На практике это означает, что Node.js необходимо иметь какой-то механизм для идентификации типа загружаемого файла. Было изучено множество вариантов решения проблемы, и решение, к которому мы постоянно возвращаемся как к наименее плохому — это новое расширение файла *.mjs, позволяющее явно идентифицировать JavaScript файлы, которые будут обрабатываться ES6 как модули. (Раньше мы с любовью называли эти файлы «Майкл Джексон скрипт»).

Другими словами, при использовании двух файлов foo.js и bar.mjs вызов import * from 'foo' будет обрабатывать foo.js как CommonJS, а import * from 'bar' будет рассматривать bar.mjs как ES6 модуль.

График работ

На текущий момент времени все ещё существует ряд проблем со спецификацией и реализацией со стороны ES6 и виртуальной машины, прежде чем Node.js может даже начать работу над поддержкой реализации ES6-модулей. Работа идет, но это займет какое-то время. В настоящее время мы предполагаем, что это займёт по крайней мере примерно год.


Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Статья на Medium