Классический подход к управлению зависимостями в сравнении с RequireJS

    Hello World,

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

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

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

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

    В этой статье классический подход реализованный в Helios Kernel сравнивается с управлением зависимостями с помощью RequireJS и показывается, каким образом подход Helios Kernel позволяет избежать некоторых сложностей.


    Введение

    Helios Kernel обладает такими особенностями:

    • Зависимости определяются с помощью функции include() в шапке модуля. В качестве аргумента используется точный относительный путь к файлу
    • После перечисления зависимостей определяется функция init() содержащая код модуля. Этот код будет выполнен после загрузки всех зависимостей


    Поэтому описание зависимости между двумя модулями выглядит следующим образом.

    Библиотечный модуль myLibrary.js, объявляющий некоторую функциональность:

    init = function() {
        // объявление (глобального) библиотечного объекта
        myLibrary = {};
    
        // библиотечная функция
        myLibrary.writeHello = function() {
            console.log("Hello World!");
        }
    }
    


    И модуль подключающий и использующий эту библиотеку:

    include("path/to/myLibrary.js");
    
    init = function() {
        // здесь библиотека подключена и можно её использовать
        myLibrary.writeHello();
    }
    


    Здесь для «экспорта» библиотечного объекта используется определение глобальной переменной myLibrary в первом модуле. Сейчас такой способ принято считать «неправильным», потому что к глобальным объектам можно получить доступ откуда угодно. Но я его здесь использую, поскольку он является самым наглядным в качестве примера. Helios Kernel не накладывает никаких ограничений на способ передачи объектов между модулями: внутри функции init может быть любой код. В том числе там можно определить и функцию-фабрику, которая будет возвращать только «экспортируемый объект».

    Но если нужно только определять локальные переменные модуля, для этого можно использовать область видимости функции init().

    Далее я буду приводить примеры, в которых использование Helios Kernel позволяет сделать код проще по сравнению с RequireJS

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

    Формат описания модуля с зависимостями выглядит так:

    RequireJS Helios Kernel
    define(
        // зависимости
        ['fooLibrary', 'barLibrary'],
        function( foo, bar ){
            // использование
            foo.doSomething();
        }
    );
    

    // зависимости
    include("path/to/fooLibrary.js");
    include("path/to/barLibrary.js");
    
    init = function() {
        // использование
        foo.doSomething();
    }
    



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

    Это особенно усложняет читаемость модулей RequireJS, когда зависимостей становится больше:

    RequireJS Helios Kernel
    define(
        [ 'dep1', 'dep2', 'dep3', 'dep4', 'dep5', 'dep6', 'dep7' ],
        function (  dep1,   dep2,   dep3,   dep4,   dep5,   dep6,   dep7) {
            // ...
        }
    );
    

    include("deps/dep1.js");
    include("deps/dep2.js");
    include("deps/dep3.js");
    include("deps/dep4.js");
    include("deps/dep5.js");
    include("deps/dep6.js");
    include("deps/dep7.js");
    include("deps/dep8.js");
    
    init = function() {
        // ...
    }
    



    Для того чтобы упростить такой код, в RequireJS был даже придуман альтернативный способ описания зависимостей, simplified CommmonJS. Используя этот приём, можно описывать зависимости следующим образом:

    define(
        [ 'dep1', 'dep2', 'dep3', 'dep4', 'dep5', 'dep6', 'dep7' ],
        function (require) {
            var dep1 = require('dep1'),
                dep2 = require('dep2'),
                dep3 = require('dep3'),
                dep4 = require('dep4'),
                dep5 = require('dep5'),
                dep6 = require('dep6'),
                dep7 = require('dep7');
                // ...
        }
    
    });
    


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

    Helios Kernel позволяет всего этого избежать. Дополнительная зависимость требует только одного упоминания с помощью include().

    Создание собирающих модулей

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

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

    Однако в случае с RequireJS нужно будет ещё экспортировать требуемые зависимости и «пробросить» их через экспортируемый объект общего модуля.

    В случае с Helios Kernel достаточно просто перечислить зависимости:

    RequireJS Helios Kernel
    define(
        'depCommon',
        [ 'dep1', 'dep2', 'dep3' ],
        function( dep1, dep2, dep3 ) {
            return {
                dep1 : dep1,
                dep2 : dep2,
                dep3 : dep3
            }
        }
    );
    

    include('deps/dep1.js');
    include('deps/dep2.js');
    include('deps/dep3.js');
    



    Для общего модуля, написанного в формате Helios Kernel даже не нужно описывать функцию init(). Это также упрощает читаемость — если заглянуть в код этого модуля — сразу видно, что он не совершает никаких действий, а только подключает дополнительные модули.

    А вот в случае с общим модулем с RequireJS придётся ещё и переписать все случаи его экспорта. Если раньше к отдельным экспортированным частям нужно было обращаться как dep1, dep2,..., то теперь это нужно заменить на depCommon.dep1, depCommon.dep2,…

    Простота использования обычных javascript-библиотек

    Речь идёт о библиотеках, которые предполагается подключать на html-страницу с помощью тега script.

    Формат модуля Helios Kernel гораздо ближе к обычным библиотекам, потому что внутри функции init() может содержаться любой код. Поэтому для того, чтобы превратить обычную библиотеку в модуль, достаточно обернуть её в определение функции init(), и после этого её можно будет подключить с помощью include().

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

    Простота портирования между вебом и nodejs

    Для того чтобы использовать модули RequireJS на сервере в среде nodejs, можно воспользоваться библиотекой r.js, которая позволяет такие модули подключать в nodejs.

    В обратную сторону: проект, написанный в формате модулей CommonJS, используемом в node, можно отконвертировать в веб-библиотеку с помощью browserify. Но это получится не модуль, а большой бандл, подключаемый тегом script (или конфигом shim в RequireJS).

    Helios Kernel упрощает эту задачу за счёт того, что модули работают без изменений и конвертации на обоих платформах.

    Сайт: http://asvd.github.io/helios-kernel/
    Код: https://github.com/asvd/helios-kernel/releases/download/v0.9.5/helios-kernel-0.9.5.tar.gz
    Гитхаб: https://github.com/asvd/helios-kernel

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 16

      +5
      Такая запись позволяет немного упростить форматирование и улучшить читаемость, но даже здесь приходится упоминать название зависимостей несколько раз. Кроме того, этот синтаксический сахар добавляет очередное усложнение в виде ещё одного способа обращения к зависимостям.

      Вы неправы. Достаточно вот такой записи:

      define(function (require) {
          var dep1 = require('dep1'),
              dep2 = require('dep2'),
              dep3 = require('dep3'),
              dep4 = require('dep4'),
              dep5 = require('dep5'),
              dep6 = require('dep6'),
              dep7 = require('dep7');
              // ...
      
      });
      
        0
        Возможно, но не в этом суть.

        Этим примером я хотел продемонстрировать, что есть неудобство, для которого в RequireJS даже пришлось придумать воркэраунд. Helios Kernel не создаёт этого неудобства.

        Ну и в итоге получается два способа делать одно и то же.
          +2
          Это не «воркэраунд», а непосредственный функционал библиотеки.
        +5
        это плохо.
        Уже есть 3 варианта, которые являются стандартами де-факто

        1) AMD модули, которые использует, например, require.js
        2) CommonJS модули, которые решают зависимости синхронно (например node.js, или для браузеров browserify).
        3) И модули из стандрта ES6.

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

        Не нужно больше велосипедов, и изучайте инфраструктуру языка на котором пишите, это гораздо важнее самого языка.
          +6
          Если бы создатели этих вариантов следовали тогдашним «стандартам де-факто», не было бы сейчас ни нода, ни commonjs. Был бы бэкэнд на php и инклюд через script
            +3
            стандартов никаких не было, до того момента пока они не понадобились, ровно до этого момента. и то что эти решения выжили это именно потому что они что то дают, людям их использующих.
            и появление нод.жс никак не зависело от системы модулей, вообще.
            инфрастукрные велосипеды от обычных отличаются тем, что писать их надо только в том случае если понимаешь профит того что они дадут, а страдать not invented here лучше не надо, в таких случаях.
              0
              Так этот пост собственно и есть почти целиком про профиты. Вы правы в том, что мне было скорее интересно сделать эту штуку, но если бы не было профита — я бы наверное и не начинал.

              Ну или можно объявить киллерфичей возможность запуска приложения в nodejs и в web без изменений и трансляции. У меня есть мысли развить проект как раз в эту сторону.
          +2
          Глядя на всё это, начинаешь ещё больше любить CommonJS модули.
            0
            Тогда уж посмотрите на библиотечку StealJS. Она предлагает такой формат:
            steal(
            "path/to/fooLibrary.js",
            "path/to/barLibrary.js",
            
            function() {
                // использование
                foo.doSomething();
            });


            По мне так это еще компактнее, чем Helios Kernel + нет этого магического вызова функции с именем «init».
              0
              «Собирающий» модуль будет выглядеть как
              steal('deps/dep1.js','deps/dep2.js','deps/dep3.js');


              Управление зависимостями библиотек, незнающих про зависимости выглядит так:
              steal('jquery.js').then('jquery.ui.js');

              Менять код самих библиотек не нужно.
                0
                Спасибо, интересная библиотека, может быть по способу записи ближе всего к Helios Kernel. Только странно, что у steal столько разнообразного функционала: у меня впечатление, что некоторые вещи должны быть в виде отдельных серверных инструментов (steal.clean).

                Про формат записи это же вопрос вкуса. Мне кажется, тут нужно стремиться не к компактности и минимальному размеру кода, а к ясности. Конкретно в steal такое место — аргументы функции steal, которые могут менять свой смысл (быть путём или колбэком) в зависимости от их количества. Ну то есть я бы не стал определять например тело модуля в виде анонимной функции прямо в перечислении аргумента только лишь потому, что язык позволяет этим способом сделать запись более компактной.

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

                А функция init() в некотором смысле даже аналогична сишному int main(). Её логичнее было именно определять в теле модуля, а не передавать в качестве аргумента как раз потому что это не колбэк.
                  0
                  аргументы функции steal, которые могут менять свой смысл (быть путём или колбэком) в зависимости от их количества
                  Там зависимости от количества нет. Только от типа. Все строки — пути к файлам. Все функции — коллбэки. Комбинации можно делать произвольные.

                  Ну а всё остальное — спор о вкусах :)
              0
              Как вариант: библиотека MicroRequireJs github.com/dsheiko/micro-requirejs.
              Очень простое и легкое (1K) решение. Не требует адаптации зависимых модулей:

              rjs.define("//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js", "jQuery");
              rjs.require(['jQuery"], function(){
                 var $ = window.jQuery;
              });
              
                0
                ['jQuery"]
                
                Вот видите, что бывает когда используете двойные кавычки вместо одинарных. :)
                +1
                include("path/to/myLibrary.js");
                
                init = function() {
                    // здесь библиотека подключена и можно её использовать
                    myLibrary.writeHello();
                }
                

                а как насчет use strict?
                  0
                  Интересно, я об этом ещё не задумывался, надо поразбираться.

                  Проблема в том, что если снаружи всех функций написать var init = ..., то, насколько я помню, для nodejs будет объявлена всё равно локальная переменная модуля, и возможно это сломает совместимость…

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