Нативные ECMAScript модули — первый обзор

https://blog.hospodarets.com/native-ecmascript-modules-the-first-overview
  • Перевод
Бандлеры и компайлеры против нативных модулей

В этой статье хочу поделиться переводом статьи о нативных ECMAScript модулях, которые все больше и больше обсуждаются среди фронтендеров. Javascript ранее никогда не поддерживал нативно работу с модулями, и нам, фронтендерам, всегда приходилось использовать дополнительные инструменты для работы с модулями. Но вы только представьте, что в скором времени не нужно будет использовать Webpack для создания бандлов модулей. Представьте мир, в котором браузер будет собирать все за вас. Подробнее об этих перспективах я и хочу рассказать.

В 2016 году в браузеры и Nodejs было добавлено много интересных фич и полезностей из новых стандартов, в частности спецификации ECMAScript 2015. Сейчас мы сталкиваемся с ситуацией, когда поддержка среди браузеров близка к 100%:

Таблица совместимости EcmaScript 6

Также фактически в стандарт введены ECMAScript модули (часто называют ES/ES6 модули). Это единственная часть спецификации, которая требовала и требует наибольшего времени для реализации, и ни один браузер пока не выпустил их в стабильной версии.

Недавно в Safari 19 Technical Preview и Edge 15 добавили реализацию модулей без использования флагов. Уже близится то время, когда мы можем отказаться от использования привычных всем бандлов и транспиляции модулей.

Чтобы лучше понять, как мир фронтенда пришел к этому, давайте начнем с истории JS модулей, а затем взглянем на текущие преимущества и реализации ES6 модулей.

Немного истории


Было много способов подключения модулей. Приведу для примера наиболее типичные из них:

1. Просто длинный код внутри script тега. Например:

<!--html-->
<script type="application/javascript">
    // module1 code
    // module2 code
</script>

2. Разделение логики между файлами и подключение их с помощью тегов script:

/* js */

// module1.js
    // module1 code

// module2.js
    // module2 code

<!--html-->
<script type="application/javascript" src="PATH/module1.js" ></script>
<script type="application/javascript" src="PATH/module2.js" ></script>

3. Модуль как функция (например: модуль функция, которая возвращает что-то; самовызывающаяся функция или функция конструктор) + Application файл/модель, которые будут точкой входа для приложения:

// polyfill-vendor.js
(function(){
    // polyfills-vendor code
}());

// module1.js
function module1(params){
    // module1 code
    return module1;
}

// module3.js
function module3(params){
    this.a = params.a;
}

module3.prototype.getA = function(){
    return this.a;
};

// app.js
var APP = {};

if(isModule1Needed){
    APP.module1 = module1({param1:1});
}

APP.module3 = new module3({a: 42});

<!--html-->
<script type="application/javascript" src="PATH/polyfill-vendor.js" ></script>
<script type="application/javascript" src="PATH/module1.js" ></script>
<script type="application/javascript" src="PATH/module2.js" ></script>
<script type="application/javascript" src="PATH/app.js" ></script>

Ко всему этому Frontend сообщество изобрело много разновидностей и новых способов, которые добавляли разнообразие в этот праздник анархии.

Основная идея заключается в том, чтобы обеспечить систему, которая позволит вам просто подключить одну ссылку JS файла, вот так:

<!--html-->
<script type="application/javascript" src="PATH/app.js" ></script>

Но всё свелось к тому, что разработчики выбрали сторону бандлеров — систем сборки кода. Далее предлагается рассмотреть основные реализации модулей в JavaScript.

Асинхронное определение модуля (AMD)


Такой подход широко реализуется в библиотеке RequireJS и в инструментах, таких как r.js для создания результирующего бандла. Общий синтаксис:

// polyfill-vendor.js
define(function () {
    // polyfills-vendor code
});

// module1.js
define(function () {
    // module1 code
    return module1;
});

// module2.js
define(function (params) {
    var a = params.a;

    function getA(){
        return a;
    }

    return {
        getA: getA
    }
});

// app.js
define(['PATH/polyfill-vendor'] , function () {
    define(['PATH/module1', 'PATH/module2'] , function (module1, module2) {
        var APP = {};

        if(isModule1Needed){
            APP.module1 = module1({param1:1});
        }

        APP.module2 = new module2({a: 42});
    });
});

CommonJS


Это основной формат модулей в Node.js экосистеме. Одним из основных инструментов для создания бандлов для клиентских устройств является Browserify. Особенность этого стандарта — обеспечение отдельной области видимости для каждого модуля. Это позволяет избежать непреднамеренной утечки в глобальную область видимости и глобальных переменных.

Пример:

// polyfill-vendor.js
    // polyfills-vendor code

// module1.js
    // module1 code
    module.exports= module1;

// module2.js
module.exports= function(params){
    const a = params.a;

    return {
        getA: function(){
            return a;
        }
    };
};

// app.js
require('PATH/polyfill-vendor');

const module1 = require('PATH/module1');
const module2 = require('PATH/module2');

const APP = {};

if(isModule1Needed){
    APP.module1 = module1({param1:1});
}

APP.module2 = new module2({a: 42});

ECMAScript модули (ака ES6/ES2015/нативные JavaScript модули)


Еще один способ работы с модулями пришел к нам с ES2015. В новом стандарте появился новый синтаксис и особенности, удовлетворяющие потребностям фронтенда, таким как:

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

Есть множество реализаций загрузчиков, компиляторов и подходов, которые поддерживают одну или несколько из этих систем. Например:


Инструменты


На сегодняшний день в JavaScript мы привыкли к использованию различных инструментов для объединения модулей. Если мы говорим о ECMAScript модулях, вы можете использовать один из следующих:


Как правило, инструмент предоставляет CLI интерфейс и возможность настроить конфигурацию для создания бандлов из ваших JS файлов. Он получает точки входа и набор файлов. Обычно такие инструменты автоматически добавляют “use strict”. Некоторые из этих инструментов также умеют транспилировать код, чтобы заставить его работать во всех окружениях, которые необходимо (старые браузеры, Node.js и т.д.).

Давайте посмотрим на упрощенной WebPack конфиг, который устанавливает точку входа и использует Babel для транспиляции JS файлов:

// webpack.config.js
const path = require('path');

module.exports = {
  entry: path.resolve('src', 'webpack.entry.js'),
  output: {
    path: path.resolve('build'),
    filename: 'main.js',
    publicPath: '/'
  },
  module: {
    loaders: {
     "test": /\.js?$/,
     "exclude": /node_modules/,
     "loader": "babel"
   }
  }
};

Конфиг состоит из основных частей:

  1. начинаем с файла webpack.entry.js
  2. используем Babel лоудер для всех файлов JS (то есть, код будет транспилироваться в зависимости от пресетов/плагинов + сгенерируется бандл)
  3. Результат помещается в файл main.js

В этом случае, как правило, файл index.html содержит следующее:

<script src="build/main.js"></script>

И ваше приложение использует бандлы/транспилируемый код JS. Это общий подход для работы с бандлерами, давайте посмотрим, как заставить его работать в браузере без каких-либо бандлов.

Как сделать так, чтобы JavaScript модули работали в браузере


Поддержка Браузеров


На сегодняшний день каждый из современных браузеров имеет поддержку модулей ES6:



Где можно проверить


Как вы видели, в настоящее время можно проверить нативные JS модули в Safari Technology Preview 19+ и EDGE 15 Preview Build 14342+. Давайте скачаем и попробуем модули в действии.

ES модули доступны в Firefox


Вы можете скачать Firefox Nightly, а это означает, что скоро модули появятся в FF Developer Edition, а затем в стабильной версии браузера.

Чтобы включить ES модули:

  • откройте страницу `about:config`
  • нажмите “I accept the risk!”
  • найдите флаг `dom.moduleScripts.enabled`
  • дважды кликните для смены значения флага в значение true

И все, теперь вы у вас доступны ES модули в Firefox.

Safari Technology Preview с доступными ES модулями


Если вы используете MacOS, достаточно просто загрузить последнюю версию Safari Technology Preview (TP) с developer.apple.com. Установите и откройте его. Начиная с Safari Technology Preview версии 21+, модули ES включены по умолчанию.

Если это Safari TP 19 или 20, убедитесь, что ES6 модули включены: откройте меню «Develop» → «Experimental Features» → «ES6 Modules».

image

Другой вариант — скачать последнюю Webkit Nightly и играться с ним.

EDGE 15 — включаем ES модули


Вы можете скачать бесплатную виртуальную машину от Microsoft.

Просто выберите виртуальную машину (VM) «Microsoft EDGE на Win 10 Preview (15.XXXXX)» и, например, «Virtual Box» (также бесплатно) в качестве платформы.

Установите и запустите виртуальную машину, далее откройте браузер EDGE.

Зайдите на страницу about:flags и включите флаг «Включить экспериментальные функции JavaScript» (Enable experimental JavaScript features).



Вот и все, теперь у вас есть несколько сред, где вы можете играть с нативной реализацией модулей ECMAScript.

Отличия родных и собранных модулей


Давайте начнем с нативных особенностей модулей:

  1. Каждый модуль имеет собственную область видимости, которая не является глобальной.
  2. Они всегда в строгом режиме, даже когда директива «use strict» не указана.
  3. Модуль может импортировать другие модули с помощью import директивы.
  4. Модуль может экспортироваться с помощью export.

До сих пор мы не увидели особенно серьезные отличия от того, к чему мы привыкли с бандлерами. Большая разница в том, что точка входа должна быть предусмотрена в браузере. Вы должны предоставить script тег с конкретным атрибутом type=«module», например:

 <script type= "module" scr= "PATH/file.js" ></script> 

Это говорит браузеру, что ваш скрипт может содержать импорт других скриптов, и они должны быть соответствующим образом обработаны. Главный вопрос, который появляется здесь:
Почему интерпретатор JavaScript не может определять модули, если файл и так по сути является модулем?

Одна из причин — нативные модули в строгом режиме по умолчанию, а классические script-ы нет:

  1. скажем, интерпретатор анализирует файл, предполагая, что это классический сценарий в нестрогом режиме;
  2. потом он находит «импорт\экспорт» директивы;
  3. в этом случае, он должен начать с самого начала, чтобы разобрать весь код еще раз в строгом режиме.

Еще одна причина — тот же файл может быть валидным без строгого режима и невалидным с ним же. Тогда валидность зависит от того, как он интерпретируется, что и приводит к неожиданным проблемам.

Определение типа ожидаемой загрузки файла открывает множество способов для оптимизации (например, загрузка импортируемых файлов параллельно/до парсинга оставшейся части файла html). Вы можете найти некоторые примеры, используемые движками Microsoft Chakra JavaScript для модулей ES.

Node.js способ указать файл как модуль


Node.js окружение отличается от браузеров и использовать тег script type=«module» не особо подходит. В настоящее время все еще продолжается спор, каким подходящим способом сделать это.

Некоторые решения были отклонены сообществом:

  1. добавить «use module» к каждому файлу;
  2. метаданные в package.json.

Другие варианты все еще находятся на рассмотрении (спасибо @bmeck за подсказку):

  1. определение, если файл является ES модулем;
  2. новое расширение файла для ES6 Модули .mjs, которое будет использоваться в качестве запасного варианта, если предыдущая версия не сработает.

Каждый метод имеет свои плюсы и минусы, и в настоящее время до сих пор нет четкого ответа, каким путем Node.js будет идти.

Простой пример нативного модуля


Во-первых, давайте создадим простую демку (вы можете запустить его в браузерах, которые вы установили ранее, чтобы проверить модули). Так что это будет простой модуль, который импортирует другой и вызывает метод из него. Первый шаг — включить файл, используя:

<script type="module"/>

<!--index.html-->
<!DOCTYPE html>
<html>
  <head>
    <script type="module" src="main.js"></script>
  </head>
  <body>
  </body>
</html>

Вот файл модуля:

// main.js
import utils from "./utils.js";

utils.alert(`
  JavaScript modules work in this browser:
  https://blog.whatwg.org/js-modules
`);

И, наконец, импортированные утилиты:

// utils.js
export default {
    alert: (msg)=>{
        alert(msg);
    }
};

Как вы могли заметить, мы оставили расширение файла .js, когда используется директива import. Это еще одно отличие от поведения бандлеров — нативные модули не добавляют .js расширения по умолчанию.

Во-вторых, давайте проверим область видимости у модуля (демо):

var x = 1;

alert(x === window.x);//false
alert(this === undefined);// true

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

// module.js
var x;
delete x; // !!! syntax error

alert(`
    THIS ALERT SHOULDN'T be executed,
    the error is expected
    as the module's scripts are in the strict mode by default
`);

// classic.js
var x;
delete x; // !!! syntax error

alert(`
    THIS ALERT SHOULD be executed,
    as you can delete variables outside of the strict mode
 `);

Строгий режим нельзя обойти в нативных модулях.

Итого:

  • .js расширение не может быть опущено;
  • область видимости не является глобальной, this ни на кого не ссылается;
  • нативные модули в строгом режиме по умолчанию (больше не требуется писать «use strict»).

Встроенный модуль в тег script


Как и обычные скрипты, вы можете встраивать код, вместо того, чтобы разделять их по отдельным файлам. В предыдущем демо вы можете просто вставить main.js непосредственно в тег script type=«module» что приведет к такому же поведению:

<script type="module">
  import utils from "./utils.js";

  utils.alert(`
    JavaScript modules work in this browser:
    https://blog.whatwg.org/js-modules
  `);
</script>

Итого:

  • script type=«module» можно использовать как для загрузки и выполнения внешнего файла, так и для выполнения встроенного кода в тег script.


Как браузер загружает и выполняет модули


Нативные модули (асинхронные) по умолчанию имеют поведение deffered скриптов. Чтобы понять это, мы можем представить каждый тег script type=«module» с атрибутом defer и без. Вот изображение из спецификации, которое объясняет поведение:



Это означает, что по умолчанию скрипты в модулях не блокируют, загружаются параллельно и выполняются, когда страница завершает парсинг html. Вы можете изменить это поведение, добавив атрибут async, тогда скрипт будет выполнен, как только он загрузится.

Главное отличие нативных модулей от обычных скриптов заключается в том, что обычные скрипты загружаются и выполняются сразу же, блокируя парсинг html. Чтобы представить это, посмотрите демо с разными вариантами атрибутов в теге script, где первым будет выполнен обычный скрипт без атрибутов defer \ async:

<!DOCTYPE html>
<html>
  <head>
    <script type="module" src="./script1.js"></script>
    <script src="./script2.js"></script>
    <script defer src="./script3.js"></script>
    <script async src="./script4.js"></script>
    <script type="module" async src="./script5.js"></script>
  </head>
  <body>
  </body>
</html>

Порядок загрузки зависит от реализации браузеров, размера скриптов, количества импортируемых скриптов и т. д.

Итого:

  • модули по умолчанию асинхронны и ведут себя как deffered скрипты

Мы вступаем в эпоху нативной поддержки модулей в JavaScript. JS прошел долгий путь становления, и, наконец-то, он добрался до этой точки. Наверное, это одна из самых долгожданных и востребованных фич. Никакой синтаксический сахар и новые языковые конструкции не идут в сравнение с этим новым стандартом.

Все вышесказанное дается для первого знакомства с нативными ECMAScript модулями. В следующей статье будут разобраны способы взаимодействия модулей, определение поддержки в браузерах, конкретные моменты и различия с обычными бандлами и т. д.

Если хотите узнать больше сейчас, предлагаю пройтись по ссылкам:


Честно говоря, когда я пробовал нативные модули в первый раз, и они заработали в браузере, я почувствовал то, чего не чувствовал с появлением таких языковых фич, как const/let/arrow functions и прочих новомодных фишек, когда они начали работать непосредственно в браузерах. Я надеюсь, что вы будете, как и я, рады добавлению нативного механизма работы с модулями в браузеры.

Другие статьи автора по данной теме



От переводчика


Я Frontend разработчик в команде Авиа в Tutu.ru. Сейчас у нас в проектах используется Webpack в качестве бандлера. Есть легаси код и старые проекты с RequireJS. Нативные модули очень интересны и ждем их с нетерпением, тем более мы уже перевели все наши проекты на HTTP/2. Конечно, совсем без бандлеров обходиться мы не собираемся, так как у нас большое количество модулей во всех проектах. Но приход нативных модулей мог бы поменять воркфлоу сборки и деплоя.
  • +29
  • 18,1k
  • 8

Туту.ру

536,00

Tutu.ru — сервис путешествий №1 в России.

Поделиться публикацией
Комментарии 8
    +1
    Конечно, совсем без бандлеров обходиться мы не собираемся, так как у нас большое количество модулей во всех проектах.


    Если не секрет, а как при «большое количество модулей» бандлер лучше, чем связка native modules + HTTP/2? Меньше нагрузка на веб сервер? Так он вроде статику очень быстро раздает? Вы замеряли уже цифры, или пока только здравый смысл?
      +4
      Пока не замеряли, но есть соблазн. Как минимум это было бы полезно при разработке, дебажить отдельные моудли, а не бандл. Так мы раньше поступали с RequireJS, пока он не перестал справляться с тем объемом файлов что у нас есть сейчас.
      +1

      Скорость загрузки, в большей степени, будет зависить от длины цепочки зависимостей, а не только от скорости отдачи файлов HTTP/2. Иначе придется включать прелоад всех модулей, а это требует тулзы подобной бандлеру. В этом не очень много смысла.

      +1
      Хоть убейте не понимаю в чём преимущество нативных модулей в браузере. Как сказал автор, бандлеры всё равно придётся использовать, потому что загружать 100 модулей на сайте всё же никто не будет (надеюсь). А вместо этого соберут пару бандлов и подгрузят их когда надо. Т.е. бандлеры остаются. Согласен, что удобно для разработки. Но тут возникает опасность, что разрабатывал в одной среде (100 нативных модулей), а в проде другая среда (пару больших бандлов).

      Еще минус, что при загрузке модулей в браузере бандлеру/компилятору становится сложно удалять неиспользуемый код да и вообще делать какие либо меж-файловые оптимизации надо кодом.

      Нативные модули конечно хороши как спецификация, чтобы избавится от разных реализаций модулей каждая из которых имеет свой синтаксис.
        0
        Не браузером единым.
        Допустим у меня есть приложение с поддержкой скриптов ECMAScript. Кто-то из разработчиков скриптов реализовал нечто полезное, что может использоваться другими скриптами. Теперь вместо копипаста можно просто подключить модуль. И это афигенно! Я джва года ждал поддержки модулей.
          +1
          Три раза перечитал ваше сообщение, но так и не понял, о чем вы. О браузере? Ну выше уже ответили, что смысла там в модулях мало. Про ноду? Так там commonjs-модули испокон веков. Какая копипаста?
            0
            Было бы полезно иметь возможность подгружать JS-файл внутрь другого JS-файла в расширениях для браузеров, где накладных расходов на HTTP-запросы нет, а использование сборщиков не слишком удобно.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое