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

от автора

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

В этой статье хочу поделиться переводом статьи о нативных 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+. Давайте скачаем и попробуем модули в действии.

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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *