Pull to refresh

Comments 62

А YM умеет как require.js — у девелопера несжатые и нетрансформированные модули, а на проде — сжатые и скленные?
YM занимается только тем, что касается модулей и разрешения зависимостей между ними. Причем здесь сжатие кода я пока не очень понимаю.
В паре к require.js есть r.js, который умеет отслеживать зависимости между AMD-модулями и согласно этим зависимостям и конфигу require.js склеивает файлы.
Как вы делаете деплой? Вручную перечисляете файлы?
Нет, конечно. Есть соответствующие тулзы, которые на основании графа зависимостей, полученного из описания модулей, собирают результирующий файл.
Какие тулзы, они опенсорсные? Потому что без них не имеет смысла использовать modules в продакшене, так как большого желания писать самому нет.
Ну если бы вы использовали прекрасный стэк технологий сборки ENB, то я бы порекомендовал использовать плагин github.com/enb-make/enb-modules, который умеет строить граф сборки на основания зависимостей самой модульной системы.
Написано же — это усложнение расширение системы модулей нужно для нормального использования Яндекс-карт тех модулей, которые не могут считаться «готовыми» сразу после своей загрузки. Почему же оно вдруг ненужное?
Ок, смотри.

Мне кажется это смешивание системы модулей и загрузчика модулей в одно целое. Более того, не самое удачное.

Самое удачное — это ES6 modules, где можно просто использовать асинхронную стратегию загрузки для каких-то модулей и не мучаться. Понятно, что с ES6 сейчас так не получится работать — нет поддержки в рантайме. Но никто не мешает сделать тоже самое с AMD — необходимо просто «асинхронную» часть модуля вынести в отдельный модуль, который не будет забандлен (в production) со всем остальным кодом, а будет подгружаться асинхронно (генерироваться бэкендом). Непонятно, что мы получаем реализую «загрузку» модуля (provide) внутри самого модуля, а не системой модулей.

Далее, у меня мнение, что система модулей должна быть статически анализируема — это значит, что без запуска кода должно быть понятно что модуль экспортирует и когда. Это очень удобно когда читаешь код и, я думаю, позволит избежать некоторых возможных багов.
Мне кажется это смешивание системы модулей и загрузчика модулей в одно целое. Более того, не самое удачное.
Вы ошибатесь. Загрузчик — это всего лишь обычный, ни чем ни лучше других, модуль в модульной системе. Сама модульная система знает только про модули и разрешение зависимостей между ними, ничего более. Модуль-загрузчик приведен лишь в качестве примера, иллюстрирующего случай, когда нужен асинхронный провайд модуля.

Но никто не мешает сделать тоже самое с AMD
Можете привести пример кода для AMD, иллюстрирующего модуль, которые экспортирует класс, который наследуется от класса из API Яндекс.Карт? Тот же пример, который в статье разобран.

Далее, у меня мнение, что система модулей должна быть статически анализируема.
А что в YM этому сейчас препятствует? В этом плане он ничем не отличается ни от CommonJs, ни от AMD.
Можете привести пример кода для AMD, иллюстрирующего модуль, которые экспортирует класс, который наследуется от класса из API Яндекс.Карт? Тот же пример, который в статье разобран.


По URL config.hosts.ymaps + '/2.1.4/?lang=ru-RU&load=package.full&coordorder=longlat&format=AMD' отдается AMD модуль.

А что в YM этому сейчас препятствует? В этом плане он ничем не отличается ни от CommonJs, ни от AMD.


Нельзя ответить на вопрос «когда?».
По URL config.hosts.ymaps + '/2.1.4/?lang=ru-RU&load=package.full&coordorder=longlat&format=AMD' отдается AMD модуль.
Как это помогает, в плане асинхронности, наследованию моего модуля от класса, предоставляемого из апи? Приведите пример описания модуля ComplexLayer для AMD из статьи.

Нельзя ответить на вопрос «когда?».
А зачем на него отвечать для статического анализа кода?
Как это помогает, в плане асинхронности, наследованию моего модуля от класса, предоставляемого из апи? Приведите пример описания модуля ComplexLayer для AMD из статьи.


Вы указываете в конфигурации AMD-загрузчика, что модуль ymaps находится под таким URL (map.yandex.ru/api/...) потом ComplexLayer просто его указывает в зависимостях и использует (прям как у вас в коде).
Вы указываете в конфигурации AMD-загрузчика, что модуль ymaps находится под таким URL (map.yandex.ru/api/...) потом ComplexLayer просто его указывает в зависимостях и использует (прям как у вас в коде).
А вам не кажется, что это усложнение модульной системы? Более того тут есть нарушение принципов модульности, так как помимо самого модуля, мне еще что-то куда-то нужно дописывать.

И можно, все-таки, код увидеть? Как там обрабатывается, допустим, ситуация с ymaps.ready, которая наступает позже загрузки самого апи?
ОК, я пропустил что ymaps ещё требуется инициализация (ждет DOM или ещё что-то там?). Тогда конечно чистым AMD не получится.

Но на мой взгляд все равно не имеет смысла делать такую функциональность частью системы модулей. Остаются следующие вопросы — как будет обработана ошибка если ymaps.ready не может быть выполнена? Если делать это с помощью промисов/коллбэков, то код, который должен работать с ymaps может эту ситуацию поправить нужным для себя образом (покажет красивое сообщение об ошибке, например).

ymaps:
define('ymaps', function() {
    return loadYMapsAndInitAndReturnPromise();
});


ComplexLayer:
define('ComplexLayer', ['inherit', 'ymaps'], function(inherit, ymaps) {
    return ymap
      .then(
        function(api) { 
          return inherit(api.Layer, ...);
        },
        function(err) {
          if (err.isFlashBlocked) {
            showNotification("please disable flashblocker or something...");
          } else {
            ...
          }
        });
});
Ну это же очень странно. с точки зрения пользователей ComplexLayer, они все должны будут знать, что им не класс предоставляется, а какой-то промис, который будет зарезолвлен потом классом. Это, мягко говоря, неудобно.
Ну наверное оригинальный модуль ymaps может сразу api без промиса вернуть, а промис отдельно экспортировать — разве класс будет как-то зависеть от инициализации? Даже если и будет, то лучше это все внести в методы класса и сделать их возвращающими промис:

YMapAPI.prototype.something = function() {
  return this.ready().then(function(api) { ... });
}


Мне кажется, что такие моменты с асинхронностью должны быть явными.
Не может ymaps сразу ничего вернуть, он использует дозагрузку своих собственных модулей.
Да и не нужна асинхронность, там где она не нужна, извините за тафтологию. Не нужно пользователю класса ComplexLayer знать о том, откуда и как он берется, вроде это очевидно. Если мне нужно написать:
var complexLayer = new ComplexLayer();
я должен так и писать, а не делать странные вещи типа:
var complexLayerPromise = ComplexLayerClassPromise.then(function(ComplexLayer) {
    return new ComplexLayer;
});

и все последующие методы complexLayer звать только через промисы.
Я говорю о том, что надо разделять процесс загрузки модулей от инцициализации, которая может зависеть от DOM.
Непонятно, как это предложение соотносится с тем, что я написал.
В вашем примере YM ждет произвольный коллбэк ymaps.ready перед тем как зарезолвить модуль.
Да, потому что только после этого события появляется нужный класс. И все пользователи этого класса должны быть избавлены от этого знания.
Я не говорю, что ваш подход не работает, просто тут на хабре что ни неделя, то появляется статья про новую систему модулей в JS, которая 1) не совместима с существующим кодом (AMD, CommonJS) 2) выглядит так, как будто автор не делал анализ существующих решений.

P.S. Это ещё вроде разработка из Яндекса? Тогда мне это вообще странно.
выглядит так, как будто автор не делал анализ существующих решений.
Все модульные системы из статьи (CommonJS, AMD) я использую и использовал (собственно, для NodeJs алльтернативы CommonJS и нет), и именно из-за их недостатков была и сделана YM, а не потому что надо было сделать очередное «свое». Если бы хоть одна отвечала нашим потребностям, я бы никогда не стал писать свое.
Я анализ не делал, но на первый взгляд кажется, что можно было попробовать доработать AMD, добавив provide опциональным параметром. Не рассматривали такой вариант?
Я не уверен, что такая доработка будет обратносовместима со всеми инструментами вокруг AMD.
Плюс, в YM еще есть несколько отличий от AMD, которые на рассмотрены в статье, но для нас являются важными, например возможность додекларации/передекларации существующих модулей.
В AMD есть возможность передекларации существующих модулей, для этого используется параметр map в конфигурационном файле. Да, я знаю, «тут есть нарушение принципов модульности, так как помимо самого модуля, мне еще что-то куда-то нужно дописывать.», но тем не менее.
Даже, если бы проблема была только в этом, это неприемлемый компромисс для нас. Все про модуль должно быть написано в модуле, уж извините.
Я согласен с вашей позицией, более того, сам пользуюсь вашей модульной системой с момента релиза соответствующей версии bem-core.
Можете воспринимать мой комментарий как критику статьи: мне кажется, она была бы интереснее, если бы было проведено сравнение модульных систем по всем пунктам, указанным в репозитории YM, с разбором таких вот кейсов. А ля «смотрите, используя AMD, вы конечно можете переопределить модуль, но вам приходится писать конфиги и это плохо потому и потому, а у нас переопределение сделано вот так круто».
Вы про многое подумали, прежде чем писать YM, многое для вас очевидно благодаря опыту — а читатель ни о чем не думал и, может быть, вовсе не понимает ничего, пока ему не разжуешь.
«Провайдить себя асинхронно» выражаясь вашими словами можно в YUI Loader!
YUI — JS фреймворк от Yahoo! Одна из особенностей — очень-очень модульная архитектура с подгрузкой модулей асинхронно по мере необходимости. Этим в фреймворке занимается отдельный компонент YUI Loader, в котором как раз есть искомая вами фича! yuilibrary.com
Судя по примерам на yuilibrary.com/yui/docs/yui/index.html, это выглядит все очень ненатурально, как будто подшито костылями уже постфактум. Руками где-то описываются вещи типа async: true, сам Loader, опять же, не является просто модулем, а зашит в глобальный объект. В общем, совсем не то, что хотелось от модульной системы.
Поддерживаю, уже давно столкнулись с тем, что фича аля `provide` действительно необходима. У нас это выглядит примерно так:
Code
// Foo.js
include
    .js('./Baz.js')
    .done(function(resp){
        alert(resp.Baz);
    });


// Baz.js
var resource = include,
    resume = include.pause();
setTimeout(function(){
    resource.exports = 'Baz';
    resume();
}, 5000);


я один тут чувствую подвох и желание написать так?
module.exports = new Promise(...)
Видя в этом подвох и желание написать через промисы только запутают архитектуру вашего приложения. Данная проблема относится к ресурсам модуля и это должна на себя перенимать модульная система. Очень не логично, загружать модуль, а потом ещё ждать пока загрузятся зависимости модуля. Представьте, что вам нужно загрузить 5 таких асинхронных модулей, конечно вы можете промисы загнать в `when` и дождаться завершения всех загрузок, но это усложняет систему.
Плюс, данный метод отлично подходит, если вам вдруг нужно, что бы один из модулей, загружал данные/конфигурация (да что угодно) асинхронно, при этом не нужно будет изменять архитектуру «отцов».
Для существующего проекта, пожалуй, самое правильное решение.

Я бы просто добавил один метод в API объекта модуля:
require.define('A', function() {
    var module = this;
    setTimeout(function() {
        module.exports = 'A';
        module.ready(); // сообщаем о том, что модуль готов
    });
});

require.define('B', ['A'] function() { // Функция вызовется при загрузке всех зависимостей
    // Если в this.ready передан callback,
    // модуль будет считаться готовым только после его выполнения...
    this.ready(function (moduleA) {
        // ...а он выполнится только после готовности всех зависимостей
        this.exports = moduleA + 'B';
    });
});


Получается примерно как и в YM, только более элегантно, имхо.
Напрягает только то, что теперь нужно будет всегда вызывать this.ready, даже если модуль синхронный. Но и в YM тоже есть такая «проблема».
Не, я о другом как раз. Из коробки поддерживать промисы было бы куда разумнее, чем городить свой велосипед для футурок.
Т.е. если передался промис — ждем его выполнения, в противном случае отдаем все сразу.
Завязываться на стороннее API…
А если мы не хотим, что бы модуль был промисом? Нам ведь просто нужно выполнить какие-то асинхронные действия (которые могут вернуть промис) при инициализации, а само API может представлять из себя объект с методами (возвращающими те же промисы). Или предполагается заменять module.exports при готовности?
Или наоборот, API будет представлять из себя промис, но модуль инициализируется синхронно?

Т.е. если передался промис — ждем его выполнения, в противном случае отдаем все сразу.

Как реализовать проверку на то, что вернули промис? По наличию then и прочего? Не очень красиво.
Можно поступить по-другому:
// Асинхронная инициализация
require.define('A', function() {
    var module = this;
    setTimeout(function() {
        module.exports = 'A';
        module.ready(); // сообщаем о том, что модуль готов
    });
    // Если ничего не вернули - ждем вызова module.ready();
});

// Синхронная инициализация
require.define('B', function() {
    module.exports = 'B';
    return module;
    // return true; module.ready(); return module.exports - можно реализовать любой подходящий вариант,
    // который однозначно обработается
});

require.define('C', ['A', 'B'] function() {
    // Здесь мы имеем возможность начать инициализацию до готовности зависимостей (если они нам тут не нужны),
    // что позволяет быстрее проинициализировать все модули.
    this.ready(function (moduleA, moduleB) {
        // Но нам все равно нужен callback, в который оповестит о готовности.
        this.exports = moduleA + moduleB + 'C';
        // return module; return true; module.ready(); return module.exports - если на этом этапе модуль готов.
        // Иначе выполняем необходимые асинхронные действия и вызываем module.ready() по их завершению.
    });
});
Забавно. Недавно решал ту же проблему, но мой АМД вместо загрузки модулей просто ждёт пока модуль появится (у меня все скрипты грузятся стандартно броузером). То есть точно так же заявляется модуль-прокладка и когда броузер его загрузил, заканчивается иницализация зависимостей.

tinyAMD X
В require.js можно использовать кастомные загрузчики для достижения асинхронной загрузки. В итоге API остаётся привычным AMD и никак не зависит от способа загрузки — когда все зависимости готовы, код модуля выполняется.
Неудобством является необходимость всегда указывать загрузчик перед именем модуля, впрочем это можно обойти костылями с маппингом.

В итоге код выглядит как-то так. Пишу по памяти после недавних экспериментов, возможно, прямо так и не заработает, у меня цели были немного другие.

loader.js
define([], function () {
    return {
        load: function (name, req, onload, config) {
            req([name], function (module) {
                module.ready(function () {
                    onload(module);
                });
            });
        }
    };
});


asyncModule.js
define([], function () {
    var callbacks = [];
    
    setTimeout(function () {
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i]();
        }
    }, 5000);
    
    return {
        // ...
        someApiMethod: function () {
            // ...
        },
        ready: function (callback) {
            callbacks.push(callback);
        })
    };
});


Использовать можно так:
main.js
define(['loader!asyncModule'], function (am) {
    am.someApiMethod();
});


Или так, если упоминать загрузчик везде не хочется:
myModule.js
define(['asyncModule'], function (am) {
    return {
        someMethod: function () {
            // blah blah
        },
        someOtherMethod: function () {
            // fdsfds
            am.someApiMethod();
            // burp
        }
    };
});

main.js
require.config({
    paths: {
        'raw/asyncModule': 'asyncModule'
    },
    map: {
        '*': {
            'asyncModule': 'loader!raw/asyncModule'
        }
    },
    deps: ['myModule'],
    callback: function (myModule) {
        myModule.someMethod();
        myModule.someOtherMethod();
    }
});
Впрочем, если нужно именно завернуть стороннюю асинхронную библиотеку, как в ymaps.js из поста, можно даже проще. Надо лишь указывать ! после имени модуля, чтобы сообщить requirejs, что это плагин.

ymaps.js
define(['module'], function (module) {
    var config = module.config();
    return {
        load: function (name, req, onload) {
            // Можно использовать любой другой способ загрузки 
            req(config.hosts.ymaps + '/2.1.4/?lang=ru-RU&load=package.full&coordorder=longlat', function() {
                ymaps.ready(function() {
                    onload(ymaps);
                });
            });
        }
    };
});


main.js
define(['ymaps!'], function (ymaps) {
    ymaps.someApiMethod();
});
А почему, то что ymaps это какой-то особый модуль должен знать именно потребитель этого модуля? Мой посыл в том, что все особенности модуля должны быть в самом модуле, а никак у его пользователей.
Ну, можно сделать два модуля. Особенный будет внутренним — а внешний будет обычным. Хотя, все равно не так красиво, как хотелось бы.
А вдруг я не хочу его получить синхронно? Модуль не должен решать за меня как я хочу им пользоваться.
Еще раз — модуль ничего ни за кого, кроме себя, ничего не решает, он только декларирует под неким именем некую функциональность, которую может предоставить. Модули, которые его используют, просто декларируют зависимость от него, для них он — черный ящик. Все. В AMD же это как раз не так.
Хм… а не будет ли запись function() { load.apply(this, arguments); } эквивалентна просто load?
Это не то, что нужно. Это тоже свмое, что во всех примерах с AMD выше. Нужна абстракция от этого.
ок) вы ведь уже объяснили зачем это было нужно «картам», и почему появился YM. вы классные (мм, история enb vs bem make вообще эпична). ни кто не сомневается, что ради правильной цели, у вашей команды вместе с Яндексом, хватит ресурсов и на то, чтобы переписать пол гитхаба как вам захочется, если вдруг. я серьезно.

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

а 'promise!anyName', это не два отдельных слова, а «такое-вот-имя-модуля-целиком» — синтаксический компромисс в именовании, благодаря которому модули могут резовлить реализацию разными способами. «tpl!name», «ym!name» и т.п. главное, что результат для клиента не отличим. так что тут как раз все правильно.

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

вы несомненно молодцы, что сумели стандартизировать этот момент внутри Яндекса. CommonJS/AMD решают схожую задачу, но не в масштабах отдельной компании, а для всего мира. это на порядок сложнее, отсюда больше компромиссов. зря вы ставите в один ранг с CommonJS и AMD, противопоставляя YM — это лишь сбивает с толку и провоцирует бесполезный флейм.

вероятность встретить за пределами яндексных инкубаторов, на открытом воздухе, код, завернутый в YM, такая же как увидеть в лесу динозавра. и год тому назад, когда он заопенсорсился, и сейчас. как только я перестал заниматься околояндексной разработкой, в коде не осталось практически ни чего из этого великолепия. потому, что во всем остальном мире — другие соглашения. за пределами Яндекса YM — лишь элегантное решение частной задачи, коих тысячи. как видите есть альтернативы.

CommonJS и AMD это общепринятые вещи — в смысле использования в проектах, рецептов и инструментов для комбинирования с другими технологиями, поддержки, документации. YM как еще один стандарт — не нужен (имхо). в этом смысле, было бы гораздо полезнее, если бы вы делились с сообществом своими революционными идеями, не в виде альтернатив (это лишь все усложняет), а коммитами в общепринятые технологии.
Спасибо за ваше интересное мнение. Но, в плане общее решение/своя альтернатива, я выделяю три стадии развития разработчика:
1. Когда он не использует общие решения, не понимая зачем они, под каждую задачу пишет свою альтернативу. Как правило, на этой стадии альтернатива гораздо хуже общего решения.
2. Когда он понимает, зачем общие решения, и использует их.
3. Когда он понимает, чем его не устраивает общее решение, что можно сделать лучше, и он пишет альтернативу. На этой стадии альтернатива имеет гораздо больше шансов оказаться лучше, чем общее решение.

Есть еще, конечно, момент с исправлением существующих общих решений, но: это далеко не всегда возможно, у авторов общих решений есть свое видение, как правило. не совпадающее с твоим; не всегда вообще можно общее решение доработать без потери обратной совместимости; и, что вообще не любят разработчики, надо потратить уйму времени на разговоры и объяснения, часто безрезультатные. Возможно, это 4-я стадия :)
Так вы пытались найти общий язык с существующими разработчиками или нет?
Если даже не пытались — тогда вы просто всё усложнили и у нас появился ещё один новый «стандарт».
Если вы проскроллите чуть ниже, то увидите, что автор require.js думает на этот счет.
1. Но это не ответ на ваши предложения (которые во многом разумные).
2. Его мнение выдрано из контекста.

Попробуйте ему написать и показать как у вас всё получилось лаконично. :)
А вы видели cujoJS/curl? Автор require.js рекомендует его каждому, кто предлагает ввести поддержку асинхронных модулей к нему в библиотеку.

Это AMD-совместимый загрузчик, умеющий принимать promise вместо модуля и ждать его resolve. Использование AMD намного лучше «еще одной модульной системы», потому что его поддержка уже встроена во многие популярные библиотеки, даже разработчик underscore после долгого сопротивления наконец-то сдался.

А использование promise это здорово потому, что они не ломают привычную схему «собрал зависимости — вызвал фабрику — получил модуль», при этом они еще и почти нативные.
Модульная система должна заниматься модулями и разрешением их зависимостей, а вместо этого я вижу нечто, подобное этому:
curl(['js!nonAMD.js'])
    .next(['dep1', 'dep2', 'dep3'], function (dep1, dep2, dep3) {
        // do something before the dom is ready
    })
    .next(['domReady!'])
    .then(
        function () {
            // do something after the dom is ready
        },
        function (ex) {
            // show an error to the user
        }
    );

В итоге, теплое смешано с мягким.
Каждый видит, то что хочет увидеть. Дополнительными плюшками можно и не пользоваться.
Между тем, require.js заменяется на эту библиотеку просто заменой библиотеки. И мы сразу получаем более сильное API, и не надо бегать по всему проекту и заменять define на modules.define, а return на provide().

Что вам помешало просто написать свою реализацию AMD?
Тем, что она необратносовместима с AMD.
Так опишите в каких моментах она не совместима и всё. Может для мнгие проекты и не узнают об этом. ;)
Вот именно! Была бы совместима — встретили бы здесь поддержку, новых благодарных пользователей и все такое.

А какова цель публикации этого решения — непонятно. Даже если начинать проект с нуля, все равно есть проблемы:
  • Нет возможности загружать обычные скрипты как модули. В статье есть пример с jquery, но как-то не хочется на каждый немодуль создавать лишний файл с описанием загрузки
  • Нельзя ставить в зависимости json и html. Особенно не будет хватать html для шаблонов
  • Нет готового способа собрать все модули в один файл и упаковать, нужен свой optimizer

Целт публикации — показать проблемы в существующих решениях и рассказать как эти проблемы мы решили у себя.
Sign up to leave a comment.

Articles