javascript

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

  • четверг, 20 апреля 2017 г. в 03:14:28
https://habrahabr.ru/company/tuturu/blog/326716/
  • Разработка веб-сайтов
  • Программирование
  • Браузеры
  • JavaScript
  • Блог компании Туту.ру


Бандлеры и компайлеры против нативных модулей

В этой статье хочу поделиться переводом статьи о нативных 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. Конечно, совсем без бандлеров обходиться мы не собираемся, так как у нас большое количество модулей во всех проектах. Но приход нативных модулей мог бы поменять воркфлоу сборки и деплоя.