Helios Kernel — include в джаваскрипте, теперь и для nodejs

    Hello World,

    Helios Kernel — это библиотека позволяющая описывать зависимости между javascript-модулями «в стиле include»:

    // объявление зависимостей
    include("path/to/library1.js");
    include("../path/to/another/library2.js");
    
    init = function() {
        // использование зависимостей
        library1.doSomething();
        ...
    }
    


    Я уже писал про Helios Kernel раньше, с тех пор проект переехал на гитхаб, а поводом к новому релизу стал тот факт, что весь платформо-зависимый код был выделен, и после некоторого допиливания тесты наконец прошил и под nodejs:



    Поэтому теперь я могу написать здесь про библиотеку громкое слово «кроссплатформенная»



    Определение зависимостей с помощью include() — главная черта Helios Kernel. Из неё естественным образом следуют остальные особенности:

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


    Это почти всё, что нужно знать, чтобы управлять зависимостями в проекте. Библиотека создана с мыслью, что для простой задачи «модулю a.js требуется для работы модуль b.js» не нужно ломать себе мозг например таким мануалом.



    Динамическое подключение модулей в рантайме — это другой юзкейс по сравнению с описанием зависимостей. Поэтому в Helios Kernel для этого используется отдельная функция, kernel.require(), которая загрузит требуемые модули и вызовет колбэк. Для отключения модулей используется функция kernel.release(). Таким образом include() не перегружена, и используется только для описания зависимостей.

    Исходя из простоты, в новой версии я постарался выкинуть из API библиотеки всё что можно, и оставил только include(), kernel.require() и kernel.release(). (На самом деле, kernel.release() тоже чуть было не пошёл под нож).

    «Экспорт» библиотечных объектов с помощью глобальных переменных также упрощает структуру модуля. Такой способ не требует ничего от API Helios Kernel. Это позволяет, например, легко создавать главный модуль библиотеки, подключающий остальные. В этом случае не понадобится «протаскивать» части библиотеки через экспортируемые объекты, и главный модуль будет содержать только список инклюдов. Это также упростит документацию и использование — каждый библиотечный объект будет всегда называться одинаково, и не будет зависеть от того, как пользователь распорядится экспортированным объектом.

    Несмотря на простоту использования, система упровления модулями в Helios Kernel довольно гибкая. Библиотека отслеживает модули затребованные пользователем, и дополнительный код загружается / выгружается исходя из нужд различных независимых частей приложения. Управление состоянием каждого модуля осуществляется по отдельности: когда какой-то модуль изменяет своё состояние, об этом оповещаются зависимые и зависящие от него модули, каждый из которых принимает решение, что делать дальше. Поэтому когда одна часть дерева зависимостей ещё только загружается и парсится, другая уже может быть проинициализирована и готова к использованию. Всё это происходит прозрачно для пользователя библиотеки, ему только нужно сообщать, когда нужно загрузить какой-то новый модуль или выгрузить ненужый. Кроме того, такой подход позволяет отслеживать ошибки в загрузке / инициализации модулей, и, например, циклических зависимостей. В таких случаях ошибки будут отображены в консоли, но приложение продолжит нормально работать, а сломанный и зависящие от него модули будут корректно выгружены.

    Ещё собираюсь написать отдельный пост, описывающий как работет управление модулями в Helios Kernel.

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

    Сайт проекта: asvd.github.io/helios-kernel

    Скачать бесплатно: github.com/asvd/helios-kernel/releases/download/v0.9.5/helios-kernel-0.9.5.tar.gz
    Смотреть онлайн: github.com/asvd/helios-kernel
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 28
      +3
      include("models/Abstract.js");
      include("views/Abstract.js");
      

      Что же делать, как же быть…
        0
        models.Abstract и views.Abstract например?
          +1
          Нет, это я у вас спрашиваю, как ваша библиотека разруливает такие случаи?
            0
            Библиотека с разруливанием таких случаев никак не взаимосвязана. Здесь-то как раз случай простой- когда такие объекты создаются в пределах одного проекта, достаточно просто договориться об именовании, например как я привёл выше.

            Более «сложный» случай — это когда что-то инклюдится, и коллизии возникают где-то глубоко в зависимостях, а мы даже не знаем, где конкретно.

            А вообще у меня основной тезис как раз состоит в том, что если вынести этот вопрос за пределы компетенции библиотеки, то это позволит сильно упростить управление зависимостями. Политику именования объектов при этом можно выбирать как угодно, не привязываясь к ограничениям библиотеки.
              0
              То есть, если у меня будет примерно такая вложенность:
              js/Collection/Filtered/Paginated.js
              то, чтобы избежать конфликтов, в начале init мне нужно будет написать примерно так?
              collection = collection || {}
              collection.filtered = collection.filtered || {}
              collection.filtered.paginated = SomeFabricCall({});
                0
                Вы можете также описать какую-нибудь функцию для автоматического построения пустого объекта. Тогда достаточно будет написать что-то вроде

                // "Экспортируемый" объект
                createNamespace("collection.filtered");
                
                // Разные штуки, предоставляемые вашей билиотекой - модулем
                collection.filtered.paginated = SomeFabricCall({});
                
                collection.filtered.whatever = ...
                


                Иными словами — да, предлагается (но не обязательно) определять в каждом модуле один глобальный объект в котором будет всё остальное.
                  0
                  Ясно, спасибо.

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

                    А что для вас сложностью является? Определение библиотечного объекта или соглашение об именовании?
                      0
                      Ни разу не встречал сложностей с AMD, если честно. Зависимости прописываются точно так же, как у вас — в строковом литерале, содержащем путь, что достаточно очевидно, имхо. Описываются модули точно так же, как у вас — через коллбэк. Понятно, что определение AMD-модуля занимает несколько больше строк кода, нежели у вас, но, если подумать, там все на своем месте и ничего лишнего нет. Никаких сложностей, повторюсь, я не встречал.

                      Это не сложности, это проблемы.
                      Почему я не перейду с AMD на Helios Kernel: AMD, во-первых, вообще не нуждается ни в каких конвенциях по наименованию (именованные модули вообще объявлены устаревшими, ЕМНИП). Во-вторых, она не требует ручного (пусть даже и библиотечного) объявления неймспейсов. Из этих двух пунктов вытекает, например, что при желании можно подключить одновременно zepto и jQuery (или абстрактную вьюху и абстрактную модель, как в моем первом комментарии). В-третьих, архитектурная сила модулей не только в том, что их можно прицепить один к другому, а и в том, что они не мешают друг другу (читай, не засоряют глобальное пространство).
                        0
                        Вроде бы в AMD «module ID» мэпится в путь по конфигам и обстоятельствам? Это как раз хороший пример лишней функциональности, без которой я хотел обойтись. Это может быть удобно использовано, если нужны разные версии одной библиотеки. Но из-за этой возможности нельзянаверняка сказать, где именно расположен исходник модуля.

                        Мне кажется, вы ожидаете от Helios Kernel, что он должен обладать возможностями AMD, но на мой взгляд Helios Kernel даже и не совсем конкурент, он претендует быть решением скорее не для модулей, которые генерят объекты на экспорт, а для файлов с исходным кодом. Внутри функции init() может быть что угодно, любой код, который будет выполнен как только модуль загрузится. То есть, хотелось сделать что-то вроде аналога макроса инклюд.

                        Продолжая думать в эту сторону — код внутри функции init(), в том числе и определять объекты модулей в формате наподобие AMD, с экспортом зависимостей и без коллизий. Это можно было бы реализовать поверх в виде отдельной библиотеки с умным разруливанием зависимостей и генерацией имени файла по id модуля, такая библиотека могла бы использовать kernel.require() для загрузки нужных модулей (интересная кстати мысль)

                        Но моя идея состоит в том, что без этих фич часто можно обойтись. Особенно если формат модуля при этом проще становится. Поэтому про Helios Kernel можно сказать, что это решение
                        для управления зависимостей между js-скриптами без лишних наворотов.
                          0
                          Но из-за этой возможности нельзянаверняка сказать, где именно расположен исходник модуля.
                          Можно. require.js же знает, откуда что грузить. Да, можно написать кривой и извращенный конфиг, где '$' будет алиасом Backbone, 'Backbone' — алиасом для underscore и т.д., но кто в здравом уме будет это делать? #define true false // happy debugging, bitches?

                          не для модулей, которые генерят объекты на экспорт, а для файлов с исходным кодом
                          Не вижу особых отличий между
                          require(['lib/vendor/jquery'], function ($) { $.fn.myPlugin })
                          
                          и
                          include("lib/vendor/jquery");
                          init = function () {
                              $.fn.myPlugin
                          }
                          


                          Да, нередко можно обойтись без алиасов, путей, конфигов и прочего, но в этих же случаях можно обойтись и вообще без модулей.
            0
            или, другими словами — откуда берется имя переменной library1? Это глобальная переменная в файле path/to/library1.js или автоматически сгенеренное на основе пути к файлу имя?
              0
              Да, переменную определяет автор модуля в его функции init(). На сайте описано, как это делается
          +1
          В связи с началом поддержки Node.js возникают два вопроса:

            0
            На сайте описано на английском, сейчас не успею ответить. Возможно, вечером отпишусь
              +1
              Описаны сомнительные достоинства. Как минимум записывать в плюсы то, что при обнаружении циклических зависимостей просто выкидывать ошибку, мол, так делать не надо, уже как-то странно. Другие пункты по поводу того, что мол с зависимостями как-то проще… а оно и в commonJS довольно легко и просто, если вы конечно не вызываете какую-то библиотеку глубоко в исходниках.
              Да нету modeule.exports, зато есть init. По сути структура модуля получается конечно логичнее, но как-то ограничивает.

              И еще вопрос, зачем могла понадобится функциональность выгрузки модулей? Это поидее противоречит здравому смыслу. Загрузилось в память — ну и пусть себе сидит. В следующий раз быстрее отработает. Ну и может еще v8 оптимизаций успел докрутить.

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

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

                Функциональность выгрузки задумывалась для веба. Загрузились какие-то данные, мы с ними поработали, они больше не нужны — мы их выгрузили.

                У Helios Kernel нет двух версий для веба и для nodejs, это один и тот же код, который работает одинаково под обоими средами.

                А вообще, идея с работой в nodejs появилась ради возможности создавать совместимые модули, которые без конвертации будут работать и в node и в вебе.
            +1
            Полные пути к модулям требуют сразу продуманной иерархии директорий, которой зачастую сходу в проекте нет. То есть, привязываясь к пути вы теряете в гибкости в последующих потенциальных изменениях в структуре папок.

            Для успешного использования модулей зачастую достаточно неймспейса + названия модуля.

            Конечно хотелось бы услышать более конкретные плюсы и минусы вашего подхода на конкретном примере. Пока я не вижу плюсов.
              0
              Пути же относительные. В крайнем случае, аргумент для include() можно генерить (если вы хотите в вашем проекте часто менять расположение модулей, но мне кажется не стоит это часто делать). Или какая гибкость имеется в виду?
                0
                К примеру, расположение библиотек. Изначально все хранилось в общей директории. Приложение должно поступить в опенсорс, код нужно изменить с учетом того, что библиотеки под свободными лицензиями должны быть в vendors + каждая в своей директории + содержать текст лицензии.

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

                Как мне кажется, хорошая практика — держать конфиг, который бы отвечал за пути к библиотекам / модулям, а на уровне модуля был бы утвержден определенный стандарт namespace/ModuleName, namespace/LibraryName.
              0
              В следующих сериях: include_once, requre_once и другие вещи, подсмотренные в php.
                0
                include() уже так и работает
                  0
                  ну тут все же система модулей… просто вместо require идет include. Но лично мое мнение — система сомнительная.
                  +1
                  У меня один вопрос — зачем?

                  Есть же спецификации AMD, CommonJS (асинхронная и синхронная загрузка модулей соответственно).

                  Тот же нодовский commonjs можно спокойно использовать в браузере (browserify, component).
                    –1
                    Зачем? Зачем? Зачем мне Ваш пехапе в node.js, чем Вам Modules/1.1 не понравился? TJH сейчас бы повесился, смотря на это чудо!
                    Ладно, будем считать, что Вам захотелось написать велосипед. Но из этого велосипеда видно, что Вы в node.js/JavaScript вошли недавно и так же недавно вышли из PHP. И не совсем поняли Modules/1.1.
                      0
                      Почему PHP? Тогда уж Си например :-)
                      +1
                      библиотека позволяющая описывать зависимости между javascript-модулями «в стиле include»
                      Но зачем?!

                      require это элегантное своей простотой, и одновременно мощное решение:
                      1. мы задаём импортируемому объекту имя на месте, что делает поиск имён более прозрачным
                      2. можно импортировать суб-объект из exports, ставя точку после require(...)
                      3. можно импортировать под любым именем
                      4. можно импортировать локально
                        0
                        Я недавно накатал текст по этому вопросу: gist.github.com/asvd/7619633

                        Всё никак руки не доходят перевести.

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

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое