Комментарии 34
Мне нравились AMD-модули при всех их недостатках — кажется, это очень элегантно, когда модульную систему можно реализовать в рамках самого языка, не вводя новый синтаксис и препроцессинг, раз уж из коробки не завезли.
Основная беда нативных модулей — невозможность объединить несколько модулей в один файл. И ладно бы была поддержка jar-ов, так нет же, нативные модули невозможно использовать на клиенте без компиляции в какой-нибудь UMD.
И нет, http/2 не решает проблему "пока модуль не загружен, браузер не знает какие зависимости надо загрузить ещё".
Сейчас у меня написан таск для Gulp который собирает модуль в один файл. При этому модуль содержит кучу AMD файлов, которые находятся внутри модуля.
Возможно я не правильно вас понял. Тогда можете поподробнее пояснить.
Заранее спасибо!
Речь про нативные модули (последние в статье). С амд -то всё ок.
Есть webpack.
В предыдущей версии дополненный babel-ем прекрасно собирает нативные модули в один файл.
В webpack@2 вроде и без babel умеет.
Да, внутренняя кухня сложнее(трансформация кода вместо простой склейки).
Но результат впрочем тот же — собранный файл, с возможностью отделить необязательные модули в отдельный файл для ленивой загрузки.
Ну и бонусом куча дополнительных плюшек сборки.
В предыдущей версии дополненный babel-ем прекрасно собирает нативные модули в один файл.
Ну вы загляните внутрь этого файла. Нативные модули преобразуются в ненативные (в тот же амд). То есть для самого распространённого юзкейса нативные модули не годятся и приходится заниматься сложной транспиляцией. И это не временная мера — с этой зависимостью от пропиетарных транспайлеров мы останемся на долго.
Я немного не понял, что Вы имеете в виду под проприетарными транспайлерами?
Утилиты, которые берут стандартные модули и преобразуют их в нестандартные. В результате модули собранные одним бандлером не возможно подгрузить из модулей, собранных другим бандлером.
Зачем нужно использовать результат работы бандлера в каких-либо модулях?
Почему нельзя напрямую использовать исходные коды, и бандлы использовать только как скрипты из нулевых?
Webpack не умеет собирать ES6 модули в один файл.
То, что вы называете "прекрасно собирает нативные модули в один файл", это преобразование ES6 модулей в CommonJS модули и собирание в один файл уже их.
К модульным системам пока что остается много вопросов. Но в первую очередь это отсутсвие механизма управления подключением скриптов в API браузера, до этого момента все попытки будут в некоторой форме костылями. Даже вводимый import не решит проблемы, в виду того, что воспроизвести его поведение будет невозможно, т.е. мы получим очередное неуправляемое поведение, что согласитесь странно.
Теперь, что касается YModules. Концепция логичная, но я бы заменил provide промисом. Гораздо удобнее и лаконичнее, по-моему:
modules.define('theTrue', () => {
return {
theTrue: true,
};
});
modules.define('theTrue.async', () => {
return Promise.resolve({
theTrue: true,
});
});
В том-то и дело, что пользователям менять ничего не надо.
Библиотека должна понимать, что если вернулся промис, нужно подождать пока он разрезолвится. Пользователи будут получать результат как и раньше, когда модуль возвращал синхронный результат.
В первом случае модуль возвращает объект:
return {
theTrue: true,
};
Во втором — зарезолвленный промис:
return Promise.resolve({
theTrue: true,
});
В последнем случае пользователю библиотеки надо будет добавить
.then(yourFunction)
Изначально в этом треде предлагается избавиться от provide вообще. Модуль определяется возвращаемым значением. Если возвращается промис, то значит модуль асинхронный и библиотека должна подождать его резолва.
Сравните сами. Что смотрится опрятнее?
Provide
modules.define('asyncModule', provide => {
fetch('/api/endpoint').then(response => provide(response))
});
Promise
modules.define('asyncModule', () => {
return fetch('/api/endpoint');
});
modules.require(['asyncModule'], function(asyncModule) {
asyncModule.then(response => doSomething(response));
});
При этом пользователь становится привязан к промису, т.е. если интерфейс библиотеки в будущем вновь станет возвращать объект, это вызовет ошибку.
Ну нет же.
Можно сделать YModules чуть умнее и научить резолвить промис, полученный от функции модуля.
И пользователь получит результат резолва и никак не заметит, что модуль стал асинхронным
modules.require(['asyncModule'], function(asyncResponse) {
console.log(asyncResponse);
});
Вы не так поняли. Я предлагаю спрятать provide внутрь метода define, т.е. чтобы provide вызывался на успешное разрешение промиса. Так же промисы помогут избежать еще одной проблемы: в данной реализации некуда передавать ошибку возникающую при асинхронной инициализации модуля, она легко может потеряться. Приведу пример интерфейса загрузки модулей в singular:
singular.inject(['mongo', 'redis'])
.then(([mongo, redis]) => {
// Отлично! Работаем дальше.
}, (err) => {
// Обрабатываем ошибки.
});
Как видите здесь используются стандартные возможности языка и появляется возможность применить await, например так:
let [mongo, redis] = await singular.inject(['mongo', 'redis']);
Сейчас механизм DI используется во фреймворках Angular 2...
Но и в первом Aнгуляре DI присутствует.
Спасибо за статью, очень интересно было вспомнить разом, как мы пришли к тому что имеем.
Никогда не любил Module Pattern и AMD, они всегда казались костылями, хотя использовать все-равно приходилось, чтоб избежать еще более костыльных решений. Из смешанных в свое время очень впечатлил ExtJS 4. Также, сразу прижился CommonJS, а когда впервые услышал про ES Modules — радости не было придела. Очень жду, когда его можно будет использовать без babel, хотя-бы в node.
Зачем нам надо было создавать что-то свое, когда можно было воспользоваться существующими форматами CommonJS или AMD? Дело в том, что хотя они и предоставляют возможность определения зависимостей и должный уровень изоляции кода, но у них был фатальный недостаток.
Динамический импорт тоже будет
if(someCondition) {
import('/path/to/script').then(
(value) => console.log('успех')
(error) => console.log('ошибка', error)
)
}
Эволюция модульного JavaScript