Модульный подход к разработке web-приложений с использованием JavaScript: AMD и RequireJS

    RequireJSПри разработке приложений с модульной структурой на JavaScript возникает две проблемы:
    • описание и удовлетворение зависимостей различных частей приложения, необходимость организации подключения зависимостей на серверной стороне;
    • экспорт переменных в глобальную область видимости и их коллизия.

    Обе эти задачи решаются при использовании подхода Asynchronous Module Definition. Он сводится к описанию модулей функцией define и подключению их с помощью require. На данный момент есть несколько инструментов, реализующих AMD. Я начал своё знакомство с ними с RequireJS и был удивлён, насколько удобно и просто можно описывать зависимости модулей. Расскажу, как это работает, на простом примере.

    Подключение загрузчика

    Имеем следующую структуру каталогов:
    siteroot/
      js/
        app.js
        require.js
        jquery.js
        mymodule.js
      index.html
    

    Для начала, подключим в index.html загрузчик. Будем использовать RequireJS:
    <script data-main="/js/app" src="/js/require.js"></script>

    Отлично, это единственный тег script, который нам нужен. Остальную работу по подключению JS сделает загрузчик. Указанный в data-атрибуте файл (расширение .js для краткости в RequireJS всегда опускается) будет своеобразной точкой входа нашего приложения. В нём мы сможем подключить необходимые модули с помощью require и совершить задуманные действия.

    Описание модуля

    Опишем наш модуль в /js/module.js с помощью define:
    define(
        'mymodule',
        ['jquery'],
        function( $ ){
            return {
                foo : 'bar'
            };
        }
    );
    

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

    Использование

    В /js/app.js подключим нужные модули с помощью JS и выполним свой код:
    require(
        ['mymodule', 'jquery'],
        function( Module, $ ){
            $('body').append( Module.foo );
        }
    );
    

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

    Конфигурация

    RequireJS обладает рядом параметров, которые можно передавать перед использованием. Для этого служит объект require.config.

    Что делать, если вам необходимо подключить модуль, которая не оформлен в виде AMD и экспортирует переменную в глобальную область видимости? Можно, конечно, модифицировать его исходный код, но это плохая практика. Для описания таких модулей служит параметр shim. Можно вручную указать его зависимости и экспортируемую переменную, и он станет частью нашего приложения наравне с другими AMD-парнями:
    require.config = {
        shim: {
            'oldmodule' : {
                deps: [],
                exports: 'OldModule'
            }
        }
    };
    

    Теперь можно указывать его в качестве зависимости:
    require(
        ['mymodule', 'jquery', 'oldmodule'],
        function(){}
    );
    

    Помимо shim есть ещё много параметров: корневая директория подключения файлов baseUrl, псевдонимы для более удобного подключения paths, и т.д.

    Заключение

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

    На прощание, приведу несколько ссылок, которые помогут продолжить изучение вопроса:

    Исходный код из статьи доступен в репозитории на GitHub.
    Happy hacking!
    Share post

    Comments 97

      +1
      В принципе, этот пост покрывает 90% того, что нужно знать про RequireJS. Спасибо!
        +2
        Думаю, про асинхронную загрузку и оптимизатор бы стоило ещё упомянуть.
          0
          Спасибо за дополнение. Постараюсь упомянуть в следующий раз, если будет к месту.
            +7
            Или можно отредактировать этот пост.
        +2
        Очень полезная штука в случае многофункционального приложения на JS. Жаль только, что большая часть библиотек полагаются на глобальную область видимости — приходится делать обертки, или переписывать, чтобы общая логика работы с файлами при разработке не менялась.

        Кроме плюсов, у использования AMD есть и минусы. В основном они связаны с тем, что это не настолько распространенная технология, как хотелось бы. Например, написание unit-тестов с помощью известных тестовых фремворков и их исполнение при работе с AMD требует требует допиливания запускаторов (использую JSTestDriver, для которого написан адаптер, но все равно периодически глючит).

        Ещё не стоит забывать, что технологии AMD построены поверх JavaScript, и чудес тут не будет.
          0
          Технология AMD очень молодая, ей всего около года (описание API на GitHub и первые релизы RequireJS). Надеюсь, со временем её подхватят контрибьюторы плагинов и библиотек, как это было с jQuery. А Underscore.js в релизе 1.3.0, напротив, отказались от AMD.
            +1
            Вместо Underscore могу порекомендовать lodash.
            0
            В YUI уже года четыре применяется
              0
              Как показывает практика, всем плевать на YUI. Уже как четыре года…
              0
              Технология? Решение в 200 строчек кода теперь называется технологией? Вы серьезно?
                0
                Согласен, громковато сказано. «Техника» было бы уместнее.
                  +1
                  Технология (от др.-греч. τέχνη — искусство, мастерство, умение; λόγος — мысль, причина; методика, способ производства) — в широком смысле — совокупность методов, процессов и материалов, используемых в какой-либо отрасли деятельности, а также научное описание способов технического производства; в узком — комплекс организационных мер, операций и приемов, направленных на изготовление, обслуживание, ремонт и/или эксплуатацию изделия с номинальным качеством и оптимальными затратами, и обусловленных текущим уровнем развития науки, техники и общества в целом.

                  (http://ru.wikipedia.org/)

                  AMD — технология поддержки модульности кода. Технически она реализована с помощью функций, замыканий и пр. конструкций, предлагаемых языком. С т.з. пользователя, это обычная модульная технология, и он ждет от неё таких же возможностей, как и от пакетов в Java или C# и т.д., но у технической реализации есть ограничения.

                  Пассаж про количество строк я не понял. ;)
                  0
                  Уже два года назад RequireJS можно было нормально пользоваться.
                  Если не верите, можете, например, историю коммитов посмотреть.
                    0
                    Ага, спасибо. Я не ту испорию коммитов смотрел, видимо.
                    0
                    Underscore.js прекрасно подхватывается с помощью shim в конфиге, как описано в посте.
                      0
                      кстати, да, я так и не понял, что такое «unsupported», работает на ура
                        0
                        Наверное, имеется ввиду, что она все равно выдает _ в глобальный неймспейс. Но подгрузка работает вполне себе.
                  0
                  В случае jQuery на самом сайте RequireJS предлагают использовать гибрид ужа и ежа require-jquery.js. Что позволяет без проблем использовать плагины без поддержки AMD (которых 99.99%)
                    +2
                    А разве jQuery из коробки не поддерживает AMD?!
                      +1
                      Поддерживает и прекрасно работает. Проверено.
                        0
                        Речь не о самом jQuery, а о плагинах
                        +2
                        Может кому пригодится — можно решить это при помощи onBuildWrite при сборке и загружать jquery c CDN:

                        var config = {
                          //Path to the source folder
                          baseUrl : '/js',
                          //Path to build folder, file structure will be mimicked from baseUrl folder
                          dir : '/build-js',
                          //optimize : 'none',
                          removeCombined : true,
                        
                          paths : {
                            'jquery' : 'empty:'
                          },
                        
                          modules : [
                            {
                              name : 'app/main'
                            },
                            {
                              name : 'app/ie6-9',
                              include : ['shims/PIE'],
                              exclude : ['app/main']
                            }
                          ],
                        
                          onBuildWrite: function (moduleName, path, contents) {
                            var wrap = {
                                start : "(function (factory) {\n\
                                      if (typeof define === 'function' && define.amd) {\n\
                                        define(['jquery'], factory);\n\
                                      } else {\n\
                                        factory(jQuery);\n\
                                      }\n\
                                    }(function ($) {\n",
                                end : '\n}));'
                              };
                        
                            if (~moduleName.search(/(jq\/slides\.jquery)/)) {
                              contents = wrap.start + contents + wrap.end;
                            }
                        
                            return contents;
                          }
                        };
                        


                        А для разработки использовать shim
                        (function () {
                          var dev = false,
                            config = {
                              baseUrl : 'build-js/',
                              waitSeconds : 15,
                              paths : {
                                jquery : ['//ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min', 'jq/jquery.min'],
                                'app/main' : 'app/main.v1.0'
                              }
                            };
                        
                          if(dev) {
                            config.baseUrl = config.baseUrl.replace('build-js/', 'js/');
                            config.shim = {
                              'jq/slides.jquery' : ['jquery']
                            };
                            config.urlArgs = "bust=" + (new Date()).getTime();
                          }
                        
                          require.config(config);
                        
                          window.appRootPath = require.s.contexts._.config.baseUrl.replace(/(build-)?js\/$/, '');
                        }());
                        
                        require(['jquery', 'app/main']);
                        


                        Пожалуй единственный минус для меня это дублирование для разработки и для сборки.
                          0
                          большое спасибо за такое развернутое дополнение!
                            0
                            Пожалуйста, хотя всё же в коде есть ошибка, вместо onBuildWrite нужно использовать onBuildRead иначе будут проблемы с билдом из-за того что теряется имя модуля. Если кто захочет использовать код выше — замена на onBuildRead сохранит вам пару часов дебага.
                        –1
                        Недавно копался в require.js, возможно, кому-то сэкономят время ссылки на AMD underscore 1.3.3 и backbonejs 9.2 github.com/amdjs/underscore github.com/amdjs/backbone
                        • UFO just landed and posted this here
                            +1
                            browserify намного удобнее чем RequireJS, в нем из коробки поддерживается CommonJS Modules 1.1
                            • UFO just landed and posted this here
                          +3
                          Вас не напрягает постоянно писать подобную обертку и менять пути во всех файлах в случае изменения его места или его зависимостей?
                          define(["exports", "./sniff", "./_base/lang", "./dom", "./dom-style", "./dom-prop"],
                          function (exports, has, lang, dom, style, prop) {
                              // code
                          });
                          

                          Не кажется ли, что nodejs-way модули и зависимости, которые лежат вне модуля более естественны? Плюс приходит консистентность модулей на клиенте и сервере.
                          var exports = require("exports"),
                              has = require("./sniff"),
                              lang = require("./_base/lang"),
                              dom = require("./dom"),
                              style = require("./dom-style"),
                              prop = require("./dom-prop");
                          

                          PS Спрашиваю не ради холивара.
                          • UFO just landed and posted this here
                              0
                              На данный момент я разрабатываю альтернативу AMD/RequireJS — LMD. В нем писать такие модули можно из коробки. В данном топике я пытаюсь получить фидбэк от активных пользователей RequireJS и сделать LMD еще лучше.
                                0
                                А вы browserify видели?
                                Если вкратце, в чем преимущества вашего подхода перед browserify?
                                  +2
                                  Да, конечно. В первую очередь я не позиционирую LMD как адаптр Node.js модулей(не пытаюсь перенести окружение Node.js среды в Dom.js). LMD — сборщик, оптимайзер, загрузчик, профайлер. Он ближе к RequireJS.

                                  Концептуальное различие в том, что browserify использует «детектива» для статического поиска зависимостей — следовательно browserify не умеет из коробки подгружать модули динамически, не может делать так require('pewpew-ololo' + someVar);. Соответственно LMD умеет рабоать с модулями динамически, но приходится писать конфиг, который делает жизнь ни чуть не сложнее.

                                  Кроме концептуальных особенностей они различаются набором фичей (на которые просто нужно время) — browserify может работать с .coffee, LMD — нет. Есть и несколько других штук, которые LMD не умеет (SourceMap) и возможно не будет уметь(jade-компилятор).

                                  LMD, в свою очередь, имеет встроенный прозрачный кэшер в localStorage, ленивую инициализацию модулей, встроенный code coverage tool (для динамических файлов без участия сервера) и профайлер модулей. Все это предельно просто включается.
                              +1
                              Не обязательно писать полные пути до файлов. Можно в конфиге RequireJS указать пути:
                              requirejs.config({
                                     <...>
                              	paths: {
                              		jquery: 'third-party/jquery.min',
                              		underscore: 'third-party/underscore.min',
                              		backbone: 'third-party/backbone.min',
                                              <...>
                              	},
                              });
                              
                              И если даже что-то изменится, то достаточно будет поменять путь к файлу в загрузчике.
                                0
                                Спасибо. Как правило у среднего проекта бывает штук 20 файлов и тогда придется прописывать все аллиасы в одном месте. Те рано или поздно настанет такой момент, когда этот список станет неподдерживаемым. Есть ли возможность сделать наследование конфигов?
                                  0
                                  gist.github.com/3806235
                                  у нас paths вот такой и ничего. удобно
                                    0
                                    Удобно понятие в данном случае относительное :) А как вы делаете production/development/testing сборки, когда необходимо заменить один из элементов такого списка?
                                      0
                                      про тесты сказать не могу ничего. пока не могу.
                                      про продакшн — в билде на node.js + r.js я указываю какие файлы не билдить, какие подменить
                                        0
                                        а вообще у нас пока этой проблемы нет. файлы которые на деве — те же и на проде.
                                        меняется только конфигурационный файл, который мы выключили из билда и на каждом сервере он свой
                                          0
                                          Я вот так делал в paths:
                                          'dataAccess': devMode ? 'dataAccessMock' : 'dataAccessWs'
                                          

                                          Теперь, при запросе модуля 'dataAccess/userData', будет подгружен модуль из папки, соответствующей текущему контексту.
                                        0
                                        В приложении, над которым я работаю, порядка 600-700 отдельных JS файлов. Но для продакшена они склеиваются в один, режутся комментарии, сжимаются. Для отладки склеиваются в пару десятков, помодульно. Над проектом в данный момент с клиентской стороны работают 7 человек. И никаких проблем с путями. Обычно они прописываются один раз и навсегда.
                                        Вот не вижу как наследование конфигов может решить сложность зависимостей. Скорее только усугубит — надо будет бегать по конфигам, чтобы найти откуда растут ноги и в каком месте возникает конфликт.
                                          0
                                          Можно используя development сборку красиво унаследовать от нее production (переписать конфиг, добавить плагинов, включить сжатие). Так же я не представляю как можно писать адекватные конфиги для локализированных сборок (копипаст либо препроцессоры), а наследование может гибко изменить особенность каждый сборки (не только локаль).
                                      0
                                      Пути менять не придётся, если описать в require.config( { paths: [] } ); алиасы к библиотекам. А по поводу обёртки да, такой формат будет удобнее, если миллион зависимостей. Вот в документации пишут про него.
                                        0
                                        А не надо писать в обсолютных путях. Вы прописываете в настройке baseUrl и потом в зависимостях спокойно пляшете от него, без вводного слэша:
                                        define([ "exports", "sniff", "_base/lang" ], function(exports,has,lang){ ... })
                                        Это спасает, если вдруг вы переместили всю папку со скриптами, а структура не поменялась. Если же внезапно меняется структура проекта, то это довольно странно. Как если бы в Java класс внезапно сменил пакет, его тоже пришлось бы вручную переподключать везде, где он импротится.
                                          0
                                          Предположим вам необходимо, чтобы какой-то модуль лениво инициализировался (он должен быть загружен, быть в сборке, но не выполнен). Используя схему define() он будет инициализирован в начале — не подходит. А если не включить его в define() и вызывать через require(), то он по умолчанию не попадет в сборку(возможно я ошибаюсь). Как решается эта проблема?
                                            0
                                            define может возвращать не уже готовый модуль, а функцию его инициализирующую, например:
                                            define(function(){
                                              return function(){
                                                var A = function(){}
                                                A.prototype = { ... }
                                                return A;
                                              }
                                            })
                                            
                                              0
                                              Т.е. нам надо будет позаботиться о том, чтобы наш разработчик сам стартанул такой модуль и сделал это только 1 раз. Внутри модуля придется писать обвязку, а пользователю такого модуля не забывать ставить () — var require('lazy')();
                                                0
                                                Должно сработать:
                                                define(function(){
                                                  var Mod;
                                                  Mod = function(){
                                                    var A = function(){}
                                                    A.prototype = { ... }
                                                    Mod = A;
                                                    return A;
                                                  }
                                                  return Mod;
                                                })
                                                

                                                Надо только уточнить, не потечем ли где-нибудь по памяти при такой работе.
                                                  0
                                                  Не, не сработает. Заврался.
                                              0
                                              я решаю следующим образом: мой сайт разбит на «модули». есть common, для него одна сборка, со всеми библиотеками и их расширениями, и есть конкретные, грубо говоря page1.js, page2.js, являющиеся страницами сайта.
                                              в начале грузится common, потом в зависимости от урла — нужный модуль. маленький и красивый, ничего лишнего.
                                            0
                                            requirejs.org/docs/commonjs.html 4 параграф
                                            В requirejs можно писать в стиле Ноды.
                                              0
                                              Да отчасти, но чтобы использовать такое в node.js (ну вдруг надо будет), то придется подключить какой-нибудь require-node.
                                              define(function (require) {
                                                  
                                              });
                                              

                                              Те мы фактически навешиваем еще один слой абстракции над уже существующей системой модулей. А хочется все-таки не писать обертку.
                                                +1
                                                Ну requirejs все-таки, имхо, клиентский фреймворк. На ноде не пишу, так что не не знаю какие там реалии.
                                                  0
                                                  На самом деле Node-JavaScript и DOM-JavaScript крайне редко пересекаются (шаблоны, валидаторы, процессоры данных). Однако же для консистентности избавиться от такой обвязки было бы не плохо. Спасибо за ответ.
                                                0
                                                CommonJS Modules 1.1 != 2.0
                                                RequireJS Использует 2 версию модулей и то коряво
                                              0
                                              Вот совпадение. Делаю как раз сейчас перевод этой статьи. Как считаете, стоит продолжать?
                                                0
                                                Так это не перевод.
                                                Можно, например, подробнее осветить оптимизацию и другие вещи, которые я не упомянул.
                                                    0
                                                    Хмм, этой статьи не читал перед написанием. Там вперемешку с Backbone и про оптимизацию тоже не упомянули.
                                                  0
                                                  А можно забандлить все модули в один .js файл, что бы не делать десяток http-запросов?
                                                    0
                                                    Можно, в RequireJS есть утилита для сборки и оптимизации, которая это делает.
                                                      0
                                                      Только зачем это делать на клиенте?
                                                        0
                                                        так это не на клиенте делается, сбилдил все в один файл и запускай на продакшене
                                                      0
                                                      Да, вот как раз про это раздел в документации.
                                                        0
                                                        Можно собрать все файлы при помощи r.js

                                                        Вообще идеология — один файл=один модуль. Иначе теряется смысл асинхронной загрузки модулей. Но я делаю так: если какие-то модули нужны исключительно в одном большом модуле, то я пишу несколько define в одном файле и возвращаю только основной модуль, так что он подключает все свои зависимости, объявленные в этом же файле, а другие не могут. Для этого надо явно указывать в define имя модуля и вызывать зависимость не по пути, а по имени. Например модуль B.js:

                                                        define("A", function(){ ... });
                                                        define ("B", ["A"], function("B"){ ... });

                                                        Теоретически, после загрузки B все смогут подключать А по имени, но никто не знает, когда В загрузится, а до А напрямую достучаться нельзя.
                                                          +1
                                                          Решает ли r.js проблему циклической зависимости?

                                                          define("A", ["B"], function(B){ ... }); 
                                                          define("B", ["A"], function(A){ ... });
                                                          
                                                            0
                                                            нет, так работать не будет
                                                              +2
                                                              Боюсь, что r.js такое правильно не соберет, но в принципе в requirejs предусмотрены циклические зависимости: requirejs.org/docs/api.html#circular
                                                              Хотя, если один кусок кода не работает без другого, а второй без первого, то они должны быть одним модулем, имхо.
                                                                0
                                                                Такое и в ноде не будет абсолютно корректно работать. В один из модулей второй подгрузится не до конца инициализировнным.
                                                            0
                                                            Интересно, месяц назад написал адаптацию классического паттерна призванного решать эти же проблемы. Но получил минусы в карму с аргументацией — «В Javascript это не нужно!».
                                                              0
                                                              И у вас, и в RequireJS, используется не Dependency Injection, а Service Locator.
                                                              Dependency Injection реализован в wire.js.
                                                                0
                                                                Не соглашусь. В Service Locator классы сами определяют свои зависимости, пусть даже и при помощи Service Locator. В моей реализации классы получают все зависимости извне, и они не знают, как и кем, они определяются и передаются. Безусловно, я написал там, что это не Auto DI как таковой, но по идеологии он намного ближе к нему, а не к Service Locator.
                                                              0
                                                              А никто еще не написал server-side решение для компиляции модулей ES Harmony в один файл с заменой неподдерживаемых сейчас инструкций?
                                                              +2
                                                              Многие не осознают, что require.js это не единственное решение для хорошей модульности фронтэнда. А по-моему, это один из худших вариантов. Предпочитаю CommonJS для фронта, ну, который и в ноде используется.

                                                              1. Нужно использовать очень хреновый и невнятный синтаксис == ужасно для дебаггинга. В commonjs можно просто написать в консоли браузера require('models/user') и выдаст то, что ожидается.
                                                              2. По-умолчанию, requirejs шлет AJAX-реквесты, когда вызывается require и модуль не находится в кеше. Такой подход просто невероятно неэффективен в продакшене. Ведь круче сжать все в один файл и работать с синхронными модулями. Асинхронная дозагрузка может быть полезна только на штуках типа локализаций, но их можно прибить и к common.js тоже.
                                                              3. Реализация де-факто стандартного require.js занимает 77 килобайт. Зепто, который почти полностью копирует API jQuery без поддержки старых браузеров, к слову, занимает 47K. CommonJS реализации на клиенте занимают в 30+ раз меньше, обычно.
                                                              4. CommonJS означает возможность использования модуля в node.js. Для аналогичного действия с require.js надо проделывать трюки и прикручивать костыли.


                                                              Пример полностью модульного commonjs приложения без глобальных переменных (backbone + chaplin): ost.io (папка «app»). Автоматическую сборку и объединение всего в два файла (один для app/, второй для сторонних библиотек) делает Бранч. Как видно, можно даже шаблоны подключать таким же образом (прекомпиляцией тоже занимается бранч).
                                                                0
                                                                Да, самое главное с использованием этого стека: не нужно заморачиваться со сборщиком, с конфигами requirejs. Все это just works при выполнении одной команды в консоли, и очень похоже на другие языки (руби, питон).
                                                                  0
                                                                  по остальным пунктам все понятно, но второй…
                                                                  По-умолчанию, requirejs шлет AJAX-реквесты, когда вызывается require и модуль не находится в кеше. Такой подход просто невероятно неэффективен в продакшене. Ведь круче сжать все в один файл и работать с синхронными модулями. Асинхронная дозагрузка может быть полезна только на штуках типа локализаций, но их можно прибить и к common.js тоже.

                                                                  а как же optimize? все так как ты пишешь
                                                                    0
                                                                    Можно и так, но зачем? Не лучше ли иметь окружение разработки (асинхронные модули) консистентное с окружением продакшена (конкатенированные синхронные модули)?

                                                                    Есть аргумент за легкость дебаггинга каждого файла по одиночке, да. Однако дебажить конкат. файлы в целом довольно просто, буквально за сутки можно привыкнуть. Особенно расставляя точки остановки в исходниках (debugger). И этот аргумент умрет с повсеместным распространением source maps (конец этого года).
                                                                      0
                                                                      Можно и так, но зачем? Не лучше ли иметь окружение разработки (асинхронные модули) консистентное с окружением продакшена (конкатенированные синхронные модули)?

                                                                      я не понял, а в чем разница моего и твоего подхода?

                                                                      Однако дебажить конкат. файлы в целом довольно просто, буквально за сутки можно привыкнуть.
                                                                      пока не привык дебажить один сбилженный на продакшене.

                                                                      И этот аргумент умрет с повсеместным распространением source maps (конец этого года).
                                                                      ждем-посмотрим:)
                                                                  0
                                                                  я тут написал обзор методов модуляризации. думаю он будет небезынтересен тут присутствующим.

                                                                  nin-jin.github.com/etc/modules/index.xml

                                                                  пишите тут в коментах плюсы и минусы упомянутых подходов. об ошибках и неточностях тоже. есть ли подходы, которые я не упомянул, а стоило бы?
                                                                    0
                                                                    JAM — Что не нравится: статический анализ сильно ограничивает, возможен overhead по коду, по соглашениям получается какая-то Java, модули ничем не ограничены, нет ленивой инициализации(все сразу интерпретируется).

                                                                    В require() на самом деле больше плюсов, чем минусов: он явный(документация зависимостей по коду, IDE не ругается на глобалы), с помощью него возможна ленивая инициализация модуля. Это полезный слой абстракции, который может изменить способ загрузки модуля (.coffee -> .js), а так же позволяет собирать статистику без наглых хаков.
                                                                      0
                                                                      > статический анализ сильно ограничивает

                                                                      а что именно? для динамической загрузки (необходимость которой сильно преувеличена) можно использовать хоть тот же requireJS. статический анализ на самом деле даёт гораздо больше возможностей. можно даже статически превращать модули хоть в LMD, хоть в AMD, хоть в CJS

                                                                      > возможен overhead по коду

                                                                      имеется ввиду, что рядом с каждым модулем нужно писать имя пакета? он не большой и, кстати, легко минимизируется статически. у меня даже был минификатор, который находил пары пакет-модуль и заменял их на короткие алиасы во всех файлах (скрипты, стили, шаблоны и даже серверные скрипты), выхлоп от этого был мизерный, а отлаживать минифицированный код — то ещё удовольствие. Ещё у меня была версия с заворачиванием кода в with( $jam ) with( $wc ) { $Component(… ) } то есть, модули используются без указания пакета, но пакеты в которых нужно искать модули перечисляются вначале файла, но из-за этого возникала неоднозначность вида «а это модуль из какого пакета?», что вносило лишь путаницу и проблемы при переносе кода между файлами (а подключён ли нужный пакет? а не используется ли модуль из не того пакета?). В результате я остановился на варианте: вырезаем коментарии и отступы, склеиваем и зипуем. А одинаковые последовательности — очень хорошо зипуются)

                                                                      > по соглашениям получается какая-то Java

                                                                      это каким местом? в яве используются развесистые пространства имён, что напрягает. тут же больше похоже на php с его автолоадом.

                                                                      > модули ничем не ограничены

                                                                      а чем они должны быть ограничены? там вся и соль, что PMS и JAM в частности не мешают реализовывать приложение так как хочется. Они лишь помогают работать с зависимостями.

                                                                      > нет ленивой инициализации(все сразу интерпретируется).

                                                                      ну, время интерпретации — это такой мизер, что его даже измерить толком не получается. вот тут я стенал по этому поводу: nin-jin.ya.ru/replies.xml?item_no=76
                                                                      так что временем интерпретации можно пренебречь. а вот что модуль будет делать при старте, а что по необходимости — это уже зависит от разработчика этого модуля)

                                                                      > В require() на самом деле больше плюсов, чем минусов: он явный(документация зависимостей по коду,

                                                                      если в этом есть необходимость, в PMS несложно сделать вывод списка зависимостей для каждого модуля, также как сейчас это делается для пакетов. так что посмотреть от чего зависит не сложно. а вот захламлять код портянкой require() и заставлять за ними следить вручную — плохо. в результате получается объявление зависимостей в двух местах («по факту использования» и «по факту включения»), которые обязаны быть синхронизированными.

                                                                      > IDE не ругается на глобалы),

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

                                                                      > с помощью него возможна ленивая инициализация модуля.

                                                                      а если нужно не весь модуль инициализировать лениво, а только часть? опять же, нет смысла смешивать зависимости и ленивые вычисления. это перпендикулярные понятия, которые лучше оставить независимыми.

                                                                      > Это полезный слой абстракции, который может изменить способ загрузки модуля (.coffee -> .js),

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

                                                                      > а так же позволяет собирать статистику без наглых хаков.

                                                                      о какой статистике идёт речь?
                                                                        0
                                                                        >а что именно?

                                                                        var r = require;
                                                                        
                                                                        r("pewpew-ololo");
                                                                        


                                                                        > а чем они должны быть ограничены

                                                                        тем, что они не должны, по крайней мере, лежать в одном скоупе

                                                                        > нет ленивой инициализации

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

                                                                        > заставлять за ними следить вручную

                                                                        Лучше явно чем как-то так автоматически. require() дает возможноть использовать любые имена импорта втч короткие. Максимум документации должно быть в коде — код должен быть самодокументируемым. То, что доки где-то лежат вне файла многим наплевать(не удобно туда-сюда прыгать).

                                                                        > а только часть

                                                                        делим модуль на 2 части во время оптимизации

                                                                        > нет смысла смешивать зависимости и ленивые вычисления

                                                                        в require() ленивая загрузка — бонус архитектуры

                                                                        > не стоит тех проблем с отладкой, которые он вызывает

                                                                        Согласен, просто как вариант. С require() возможно автоматическое применение Code Coverage инструкций без участия бэкэнда.

                                                                        > а так же позволяет собирать статистику без наглых хаков

                                                                        Статистика подключений — вызовы require(), профилирование времени инициализации модуля, сбор динамической статистики.

                                                                          0
                                                                          > var r = require;
                                                                          > r(«pewpew-ololo»);

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

                                                                          > тем, что они не должны, по крайней мере, лежать в одном скоупе

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

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

                                                                          которые вовсе не обязательно выполнять при старте, если в том нет необходимости. хотя да, если либа изначально не ленива, то загрузчик с эвалом в нутрях сделает её ленивой. правда цена — координаты строк будут указывать в стратосферу. и не надо рассказывать про светлое будущее с sourcemap — нам сейчас мучиться)

                                                                          > Лучше явно чем как-то так автоматически.

                                                                          автоматически на основе _явной_ декларации использования. $jam.Component — не менее явно, чем require( 'jam/Component' ).

                                                                          > Максимум документации должно быть в коде — код должен быть самодокументируемым. То, что доки где-то лежат вне файла многим наплевать(не удобно туда-сюда прыгать).

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

                                                                          > в require() ленивая загрузка — бонус архитектуры

                                                                          а у меня бонус фреймворка — модуль $jam.Lazy, который позволяет сделать ленивым не только модуль, но и любую функцию)

                                                                          > Согласен, просто как вариант. С require() возможно автоматическое применение Code Coverage инструкций без участия бэкэнда.

                                                                          и всётаки мы скатились до бесполезного спора «а у нас в квартире газ, а нас свой водолаз»). в конце концов всегда можно нагенерировать аннотацию со списком зависимостей, если она всё-таки нужна. у всех разные приоритеты — на том и порешим)

                                                                          > Статистика подключений — вызовы require(), профилирование времени инициализации модуля, сбор динамической статистики.

                                                                          ну, ничто не мешает статически трансформировать JAM модули в AMD и обретать все его плюсы и минусы и переключаться между сборками в зависимости от того, что важнее)

                                                                          основной-то посыл был всё-таки такой:
                                                                          1. собирать нужно не только js, но и прочие прилагающиеся ресурсы. авторы CJS, AMD и других стандартов упорно про это забывают.
                                                                          2. не за чем плодить рутину там, где достаточно простого соглашения и нехитрой автоматики.

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

                                                                            На крайняк можно заюзать wildcard "*": "*.js" и включить в сборку все модули.

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

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

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

                                                                            > ты всё-таки попробуй поиграться с примером
                                                                            — спасибо, я уже с browserify наигрался =)

                                                                            $jam.Lazy — «а у нас в квартире газ, а нас свой водолаз» :) Все подобные фичи это дело времени.

                                                                            Скажи как ты собираешься организовывать локализацию на разные языки, которые могут влеч за собой отключение каких-то фичей? Ну и как делать версии для dev/prod/test? Желательно без копипаста.
                                                                              0
                                                                              > должно быть явное разделение того, что входит в сборку и что используется.

                                                                              почему должно? одно без другого не имеет смысла. включить модуль в сборку, но нигде им не воспользоваться… зачем? обычно это не надо. а когда надо — всегда можно «воспользоваться» им в *.meta.tree. использовать модуль, не подгрузив его… зачем? код же не будет работать, а будет сыпать ошибками. разве что какой-то хитрый вариант типа «нужный модуль будет подгружен потом другим модулем, чтобы этот модуль мог им воспользоваться» необходимость которого я с трудом представляю.

                                                                              > На крайняк можно заюзать wildcard "*": "*.js" и включить в сборку все модули.

                                                                              а если не все из них используются? у меня вот есть условно 2 типа пакетов: библиотеки и приложения. когда собирается приложение — его модули грузятся все, а вот из библиотек только те, что реально используются.

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

                                                                              ничего не влечёт. я плохо акцентировал на этом внимание… автоматика сильно упрощает работу, но она не творит чудеса) если требуется плагин, который регистрируется как, например, функция в инстансе jq. то есть факт его использования в общем случае статически определить сложно, то эту зависимость надо будет определить явно. в meta.tree или же в самом jam.js в комментарии (то есть опуститься до уровня cjs ;). другой вариант — добавить алиас, чтобы обращение к нему удовлетворяло принципам jam:
                                                                              $jd( '.fancybutton' ).$jq_fancybutton()
                                                                              но тогда, разумеется, появляется некоторая избыточность обращения.

                                                                              > И еще я считаю, что модули должны быть гибко абстрагируемы от файловой системы.

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

                                                                              > каждый объект скоупа увеличивает цену вызова функции, лежащей в этом скоупе

                                                                              ну там же хэш-таблица. разница незначительна.

                                                                              > спасибо, я уже с browserify наигрался =)

                                                                              ну это же совсем не то: о второй уровень из 4 по моей шкале х)

                                                                              > как ты собираешься организовывать локализацию на разные языки, которые могут влеч за собой отключение каких-то фичей?

                                                                              хороший вопрос) я пока делал только подмену текстов, но там был отдельный формат для локалей, который собирался также как и все остальные, но на выходе получались собранные бандлы типа index.locale=ru.xml из файлов типа ya_search.locale=ru.xml
                                                                              думаю можно реализовать аналогичную поддержку «плоскостей сборки» для всех форматов. а ковыряться в конфигах, прописывая каждую локализованную версию файла — совсем не хочется.

                                                                              > Ну и как делать версии для dev/prod/test?

                                                                              а это зачем? на мой взгляд разница должна быть настолько минимальной, на сколько это возможно. а если нужны какие-то дополнительные инструменты для дебага — грузить их отдельным пакетом.
                                                                                0
                                                                                > нужный модуль будет подгружен потом другим модулем, чтобы этот модуль мог им воспользоваться

                                                                                У тебя архитектура предполагает факт того, что логика приложения может динамически расширятся?

                                                                                > гибко абстрагируемы от файловой системы

                                                                                хороший тому пример "i18n": "ru.json" и "i18n": "en.json"

                                                                                > ну там же хэш-таблица. разница незначительна

                                                                                При каждом вызове функции скоуп функции пересоздается. Это, конечно, копейки, но в некоторых случаях и копейка важна — тормозные браузеры, горячие функции, время-зависимые приложения (спиддиал например)

                                                                                > прописывая каждую локализованную версию файла

                                                                                И тут в тред врывается абстракция над файловой системой и наследование конфигов! ;-)

                                                                                > разница должна быть настолько минимальной, на сколько это возможно

                                                                                Тут минимум dev — один хост бэкэнда, test — другой. Ну и всякие хитрые оптимизации production-сборки.
                                                                                  0
                                                                                  > У тебя архитектура предполагает факт того, что логика приложения может динамически расширятся?

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

                                                                                  > хороший тому пример «i18n»: «ru.json» и «i18n»: «en.json»

                                                                                  пример чего? я могу отвечать только раз в сутки, так что переспрашивать не очень удобно

                                                                                  > Это, конечно, копейки, но в некоторых случаях и копейка важна

                                                                                  сферическая копейка в вакууме конечно важна, но на практике бутылочные горлышки находятся в совсем других местах

                                                                                  > спиддиал например

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

                                                                                  > И тут в тред врывается абстракция над файловой системой и наследование конфигов! ;-)

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

                                                                                  > Тут минимум dev — один хост бэкэнда, test — другой. Ну и всякие хитрые оптимизации production-сборки.

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

                                                                                    0
                                                                                    > чтобы фреймворк зависел от опционального плагина
                                                                                    Тут я про линивую загрузку модулей.

                                                                                    > пример чего
                                                                                    Пример абстракции над файловой системой

                                                                                    > Это, конечно, копейки, но в некоторых случаях и копейка важна
                                                                                    Недавно буквально наткнулись на такую проблему (плагины для браузра) без ленивой загрузки и без прогретого JIT он стартует 150 мс (ну очень тяжелый), а с ленивой загрузкой 20мс

                                                                                    > тоже занимался спиддиалом?
                                                                                    Консультировал по оптимизации визуальных закладок 2,0.

                                                                                    > а у меня нет конфигов и нечего не надо наследовать
                                                                                    ок

                                                                                    > тот хост с которого загружена страница
                                                                                    особенность архитектуры, часто не мы решаем какая она должна быть (адрес бэкнда другой у дева и продакшена)

                                                                                    Что-то у нас тред разросся :) Понаписали уже больше чем в статье.

                                                                                      0
                                                                                      > Тут я про ленивую загрузку модулей.

                                                                                      в том-то и дело, что ленивая нужна загрузка не модулей, а пакетов. иначе будет 100500 запросов.

                                                                                      > Пример абстракции над файловой системой

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

                                                                                      > Недавно буквально наткнулись на такую проблему (плагины для браузра) без ленивой загрузки и без прогретого JIT он стартует 150 мс (ну очень тяжелый), а с ленивой загрузкой 20мс

                                                                                      я.бар?) ну, вообще говоря, да, специфика плагинов в том, что подгрузка модулей из памяти не даёт такого пенальти, как подгрузка с сервера, зато есть куча левого функционала, который практически не используется. поэтому для мозиллы, например, я написал клёвый ленивый загрузчик (https://github.com/nin-jin/fenix-snippet#%D0%9C%D0%BE%D0%B4%D1%83%D0%BB%D1%8F%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F) — кстати, хороший пример, ленивой загрузки и изоляции модулей, оставаясь при этом в рамках PMS. правда ценой типовой шапки из нескольких строк: подключение загрузчика и подключение каждого необходимого пакета, модули из которых грузятся уже лениво.

                                                                                      > Консультировал по оптимизации визуальных закладок 2,0.

                                                                                      под какой браузер? и какие выводы?

                                                                                      > особенность архитектуры, часто не мы решаем какая она должна быть (адрес бэкнда другой у дева и продакшена)

                                                                                      и всё же, ошибки в архитектуре тоже надо исправлять, иначе есть риск погрязнуть в костылях.

                                                                                      > Что-то у нас тред разросся :) Понаписали уже больше чем в статье.

                                                                                      автор нас наверно уже проклял, сглазил и навёл порчу х)
                                                                                        0
                                                                                        Да нет, мне, наоборот, приятно, что мой скромный пост вызвал такую дискуссию.
                                                                                          0
                                                                                          Отдельные модули можно кэшировать в localStorage, а загрузка сборок — overhead.

                                                                                          > либо громко ругаться, что не хватает переводов. но уж точно не просто заменять один файлик на другой.
                                                                                          Так и делается. Берется сервис переводов(который ругается на непереведенные штуки) с него все сливаем в один файл, затем натравливаем локализатор и вуаля. Ну тут уже дело не в том как организуется этот процесс, а в том сколько нужно накопипастить без наследования конфигов.

                                                                                          > какие выводы
                                                                                          прогревать все функции (для JIT) и хранить в расшаренной области.

                                                                                          > ошибки в архитектуре тоже надо исправлять

                                                                                          это не ошибка, а особенность (не всегда бывает возможно стучаться к локалдомену)

                                                                                            0
                                                                                            оверхед — это 100500 хттп запросов и как следствие лишние задержки. а кеширование в ls — ещё один костыль, не добавляющий ни надежности ни поддерживаемости.

                                                                                            тут нужно наследование локалей. и не через какой-то там сервис, а прямо тут, при разработке. и не конфигов, а самих локализованных строк. или строки у тебя в конфигах? для каждого модуля?

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

                                                                                            а в каких случаях таких возможностей нет?
                                                                                              0
                                                                                              Переводы получаются по ключу. Браузер прогревает сам некоторые функции расширений (сам видел, как это точно делается не скажу — все гдето в глубинах модели расширений ФФ).

                                                                    Only users with full accounts can post comments. Log in, please.