Helios Kernel (удобный include в Javascript)

    Hello World,

    Всё начилось с простой идеи: мне захотелось, чтоб я мог в шапке скрипта написать что-то вроде

    include( "path/to/someLibrary.js" );
    

    а ниже использовать объекты, объявленные в скрипте someLibrary.js. Так появилась библиотека Helios Kernel.



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

    В итоге, каждый скрипт выглядит примерно следующим образом:

    // инклудим требуемые скрипты
    include( "path/to/someLibrary.js" );
    include( "path/to/someOtherLibrary.js" );
    
    function init() {
        // эта функция - инициализатор нашего скрипта, здесь можно
        // использовать объекты, объявленные в скриптах, которые мы заинклудили
        someLibrary.doSomething();
        var mySomething = new someLibrary.Something();
        someOtherLibrary.doSomethingElse();
    }
    

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


    Update: Здесь наверное будет уместно пояснить, чем эта библиотека отличается от десятка других библиотек для загрузки скриптов. Helios Kernel предоставляет такой же подход к определению зависимостей между модулями, какой используется во многих других языках программирования и обычно доступен там «из коробки» — это возможность определять зависимости модуля в шапке самого модуля. То есть теперь, если нужно загрузить какой-то модуль, не нужно беспокоиться о том, чтоб предварительно загрузить другие модули, которые ему требуются для работы. Указывать зависимости модуля — задача для автора модуля, а не для его пользователей. Пользователь просто сообщает, что ему нужен какой-то модуль, и библиотека Helios Kernel сама загружает необходимые зависимости и инициализирует их в соответствующем порядке.


    Я постарался сделать эту библиотеку максимально простой (в плане функциональности). Она также содержит несколько полезных штук, которые естественным образом её дополняют.

    Библиотека объявляет ещё две полезные функции: kernel.require() и kernel.release(), которые могут использоваться для того, чтобы динамически в рантайме загружать и выгружать необходимые скрипты (вместе со всеми зависимостями). Выглядит это примерно так:

    // пользователь нажимает какую-нибудь кнопку
    // и в контроллере приложения вызывается обработчик
    AppController.prototype.doSomethingHandler = function() {
        // обработчик загружает дополнительный код
        this.magicLibraryTicket = kernel.require(
            "path/to/magicLibrary.js",
            function() {
                // и когда этот код загрузился и проинициализировался,
                // использует новые объекты и функции
                magicLibrary.doSomething();
            }
        );
    }
    

    // затем пользователь нажимает другую кнопку, означающую,
    // что ему больше не нужна дополнительная функциональность
    AppController.prototype.stopSomethingHandler = function() {
        // другой обработчик делает необходимые завершающие действия
        magicLibrary.stopDoingSomething();
        // и сообщает, что код можно выгрузить
        kernel.release( this.magicLibraryTicket );
    }
    

    Эти две функции (kernel.require() и kernel.release()) информируют библиотеку о том, что понадобился какой-то скрипт (или что какой-то скрипт больше не требуется). Библиотека сама решает, что когда нужно загружать и выгружать — если, например, какой-то скрипт используется ещё и в другом месте, он не будет выгружен по запросу kernel.release().

    Кроме того, Helios Kernel умеет управляться со всякими странными ситуациями. Например: у функции kernel.require() есть ещё третий необязательный параметр (помимо пути скрипта, который нужно загрузить, и колбэка, который нужно вызвать, когда этот скрипт загрузится). Этот третий параметр — ещё один колбэк, который вызывается в случае ошибки при загрузке скрипта (если, например, возникли проблемы с сетью, или обнаружилась циклическая зависимость). Путь скрипта, который не удалось загрузить, передаётся в качестве единственного аргумента при вызове этого колбэка.

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

    Более подробно всё описано в документации (290 kb): helios-kernel-0.9-guide.pdf
    Загрузить библиотеку можно здесь (8 kb): helios-kernel-0.9.tar.gz
    Домашняя страничка проекта тут: home.gna.org/helios/kernel (хотя, там написано примерно то же самое, только кратко и на английском).

    Предыстория


    Два года назад я опубликовал вот эту заметку: Helios Javascript Framework. В ней я рассказал о том, что замышляю сделать целый фреймворк для разработки веб-приложений, и показал демку с гламурным калькулятором. Эта простенькая демка содержит довольно много кода — по сути там реализована библиотека виджетов. Но для широкого использования эти наработки были малопригодны. Мне хотелось поскорее посмотреть, как оно всё будет работать, поэтому писалось всё на скорую руку, и я даже забил на документацию к коду. Впрочем, документация там и не нужна: это был, что называется, proof of concept, и моя первая попытка написать библиотеку для виджетов (поэтому она весьма ужасна).

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

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

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

    Теперь я буду думать, что кодить дальше. Надеюсь, следующий мой отчёт будет раньше, чем через два года :-)
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 43

      +1
      тоже думал над таким, но всё же остановился на препроцессинге и генерации одного js-файла.
      собственно как и делает множество популярных фреймворков (jQuery, extJS, ...)
        +1
        Соглашусь, все же js имеет свои особенности. Тем более не всегда эта init нужна. Лучше все одним файликом слать или не одним но с начало все же подгружать все что надо.
          +1
          Ну это два разных подхода. У обоих и преимущества есть, и недостатки.

          Я выбрал такой подход, потому что мне он кажется яснее. Девелоперский цикл короче, опять же (не нужно компилировать мега-скрипт).

          Да и кроме того. Фреймворк — это с точки зрения приложения атомарный элемент. Его один раз загрузил и используешь. Поэтому его разумно после разработки скомпилить в один скрипт. Но разработчик приложения, основанного на этом фреймворке, всё равно будет разбивать свой код на модули.

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

          В место этого обычно используют костыли :-) причём, часто серверные. Какой-нибудь ужасный xml-файл, содержащий список всех скриптов, которые нужно подключить для каждого приложения. Из этого на сервере генерится огромная хтмл-ка, которая подключает скрипты «классическим» способом.

          А, хотя, не слушайте меня. На самом деле мой подход мне нравится больше, потому что он мой :-D
            +1
            >Я выбрал такой подход, потому что мне он кажется яснее. Девелоперский цикл короче, опять же (не нужно компилировать мега-скрипт).
            кастомный хэндлер для жс запросов на вебсервере в ~30 строк кода и девелоперский цикл как при редактировании обычных жс'ников, только в итоге на выходе мы можем получать как debug, так и release сборки конкретных модулей.

            >В место этого обычно используют костыли :-)
            То что у вас — это ещё больший костыль.
            Посмотрите YUI3 + YLS.

            p.s. советую прочитать книжку javascript patterns
              0
              Возможно, вы и правы. Мне тут сложно спорить, потому что я с YUI не знаком.

              Но у меня уже очень давно создалось впечатление, что так делают чаще всего, потому что по какой-то причине стало принято собирать код в один скрипт. Может быть, потому что в js нет нативного инклуда. Точно также, как принято создавать веб-приложения посредством манипуляции элементами DOM. Наверное как раз поэтому я и пытаюсь использовать другой подход. Без лишних хуков и хендлеров.
                0
                А с каким фреймворком вы знакомы? По-моему сейчас все так делают. YUI, JQuery UI, Google Closure, ExtJS. Вы правы, делают так «по какой-то причине». Объясню по какой — один большой файл быстрее скачивается и лучше сжимается, чем много маленьких. Причем это применяется не только к скриптам, но и к CSS и даже картинкам (в т.н. виде CSS-спрайтов).
                  0
                  > один большой файл быстрее скачивается и лучше сжимается, чем много маленьких.
                  В yui3 не так просто :) Одним файлом выгружается только то что реально нужно в данный момент времени.
                  0
                  >Наверное как раз поэтому я и пытаюсь использовать другой подход.
                  очень советую прочитать про модульность в javascript patterns, там вроде даже описаны исторические причины по которым различные мелочи добавлялись :) модульность в yui3 реализована примерно так как в той книжке и описано.

                  А если про загрузку модулей, то в yui3 есть выбор:
                  1. (девелоперский вариант) в сервер встроена примитивная компиляция модулей(либо ручная компиляция), на клиенте все метаданные и на клиенте происходит разруливание всех зависимостей.
                  2. combo — это уже на продакшене висит серверный кусок, который умеет склеивать несколько модулей(модули так же могут быть разбиты на несколько файлов) в один файл, но тут опять есть недостаток — метаданные и лоадер на клиенте.
                  3. yls — новый подход, скоро будут доступны исходники. Здесь уже лоадер встроен в сервер и он занимается разруливанием зависимостей и выдаёт весь нужный результат. В твиттере можно легко найти восхищения от того насколько сильно поменялась отзывчивость с внедрением подобной системы на некоторых крупных сайтах.

                  Девелоперский цикл такой же простой как и при обычной разработке, сохранил, в браузере нажал f5 :)

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

                    А чего там творится на dev-версии никто никогда не узнает, можно написать себе хоть 100500 комбинаций из скриптов, которые вам нужны и компрессить их тем же YUI-компрессором, не используя при этом YUI.

                  0
                  > Девелоперский цикл короче, опять же (не нужно компилировать мега-скрипт).
                  добавить в IDE 2 (release + debug) кастомных кнопки?
                  так же как вариант — commit hook в cvs'ке, только на девелоперской машине по-другому работать будет (не 1 js-файл, а куча модулей).
                  0
                  всё очень сильно зависит от приложения, например во фликре в каждой странице стоит небольшой кусок кода, который перехватывает все события и записывает в очередь(на самом деле всё немного сложнее), но в это время пользователь уже может сидеть и наслаждаться фотографией. А когда уже подгрузится жаваскрипт, тот мелкий скрипт начинает плеваться событиями, которые накопил в своей очереди :)
                  0
                  Так экст же нынче автолоадером обзавелся.
                  0
                  Спасибо! Пригодится.
                    +1
                    использую модули YUI
                      0
                      ждал этого твоего комментария)
                      +2
                      А зачем нужна функция init? Почему бы не инициализировать через тут же вызываемую анонимную функцию
                      
                      (function() {
                      …
                      })();
                      
                        0
                        Если так написать, функция будет вызываться сразу во время парсинга скрипта. Мне здесь нужно, чтобы init() вызывалась тогда, когда библиотека посчитает нужным (то есть, когда все зависимости готовы).

                        Ну и кроме того, это трюк. Стараюсь, чтоб всё проще было, когда возможно.
                          +1
                          Библиотека инклуд же изначально грузится синхронно. Вместо Init лучше сделать:

                          include( function(){
                            someLibrary.doSomething();
                            var mySomething = new someLibrary.Something();
                            someOtherLibrary.doSomethingElse();
                          } )



                          Потому что init может уже быть, а в инклуде соответственно проверять не функция ли пришла. Если она, то добавлять в массив коллбэков. А когда всё проинициализировалось — запускать всё в цикле. Можно немного расширить и сделать бонусный параметр после функции с именем необходимого модуля (принимаемые параметры — текстовая строка или массив), тогда можно будет запускать код по мере подгрузки файлов (может некоторому коду достаточно someLibrary и ему не нужно ждать полного окончания загрузки).
                            0
                            Я не думаю, что есть смысл дополнительно усложнять ради фич.

                            Функция include() нужна для того, чтобы указать, что модуль А требует модуль Б. Функция kernel.requre() нужна для того, чтобы в рантайме сказать «загрузи модуль Ц и выполни этот код». Мне кажется, если приравнивать инициализатор к колбэку, то это приведёт к запутыванию кода. Сейчас всё ясно: вот инклуды, а вот код модуля.

                            А если какому-то коду нужны не все зависимости, тогда этот код нужно выделить в отдельный скрипт и прописать инклуды нужные только ему.
                      • UFO just landed and posted this here
                          0
                          Наверняка можно, и наверняка я его делать не буду :-)

                          Но если кто возьмётся — я готов проконсультировать.
                            +1
                            А зачем jQuery-плагин, чем он должен отличаться от текущей реализации? Баксом вместо «kernel»? =)
                            • UFO just landed and posted this here
                                +3
                                Сначала прочитал «быдлокодить» :-D

                                Если честно, ничего не имею против jQuery и бакса, просто это для меня сейчас не самая интересная задача. Поэтому разве что если кто-то другой портирует.
                                  0
                                  (function(global, $, $include){
                                  
                                      $include('some/module.js');
                                  
                                      $(function(){
                                          // Kill all humanz
                                      });
                                  
                                  })(this, jQuery, kernel.include);


                                  Не?
                              0
                              Мне кажется, что инициализация и очистка должны быть повыше уровнем, т.к. в модуль может входить не только js, но и шаблоны, стили, ресурсы, например — а их готовности тоже надо дожидаться, да и чистить хорошо бы. Потому пришлось написать отдельный менеджер модулей, использующий в том числе и лоадер скриптов (я пользую www.dustindiaz.com/scriptjs).
                                +4
                                Существует множество скрипт лоадеров: LAB.js, head.js, yepnope.js. Последнтй позволяет подгружать скрипты при определенном условии:
                                yepnope({
                                test : Modernizr.geolocation,
                                yep : 'normal.js',
                                nope : ['polyfill.js', 'wrapper.js']
                                });

                                Недавно он даже стал частью Modernizer.js
                                  +2
                                  +туда же:
                                  RequireJs
                                  0
                                  Я добавил в текст небольшой update, поясняющий отличие от других js-лоадеров
                                    0
                                    в YUI3 автор модуля так же определяет зависимости.
                                  0
                                  Так или иначе, молодец, саморазвитие — это всегда хорошо, а когда пишешь что-то подобное, то набираешься опыта.
                                  Имхо автор молодец!
                                    0
                                    Вот ещё одна реализация
                                    www.artlebedev.ru/tools/technogrette/js/include/
                                      0
                                      Эту статью я закончил читать на строчке

                                      А дальше достаточно выполнить eval(xhttp.responseText)


                                      :-D
                                        0
                                        Да, это смущает. Но я вот тут думаю: а чем это может быть плохо, если подключаемый любым другим путём скрипт всё равно тоже запускается на исполнение?
                                      +5
                                      Утки, надеюсь, не пострадали? :D
                                        +1
                                        CommonJS структура, на мой взгляд, лучше подходит для использования в javascript. В node.js можно использовать github.com/substack/node-browserify — он автоматически заменить вызовы require так, что все будет работать, как будто вызов был блокирующим.
                                          0
                                          уровни модуляризации:

                                          0 — всё единым файлом

                                          когда объём кода превышает 9000:

                                          1 — разбиение на файлы с ручным указанием зависимостей

                                          когда число модулей превышает 9000:

                                          2 — автоматическое вычисление зависимостей

                                          когда число разработчиков модулей превышает 9000:

                                          3 — автоматическое скачивание и установка зависимостей

                                            0
                                            Такое ощущение, что в этой области каждый хочет повелосипедить по своему.
                                            Мой велосипед github.com/dmitry-dedukhin/jsLoader и демо dmitry-dedukhin.github.com/jsLoader/
                                              0
                                              В рамках инициативы common.js
                                              загрузку модулей стандартизовали, как для клиентского и сервеного JS
                                              See Asynchronous Module Definition (AMD)

                                              Одна из реализаций AMD => Require.js
                                                0
                                                nicola, я уже давно внимательно изучил эту спецификацию, и считаю, что это полнейший балщит :-D

                                                я уверен, людей которые составляли эту спецификацию скоро отпустит, и они поймут, что так кодить нельзя :-)
                                                  0
                                                  why?
                                                  слишком много библиотек уже использует AMD:
                                                  dojo => dojotoolkit.org/features/1.6/async-modules
                                                  ace editor (bespin) => ace.ajax.org/
                                                  все модули Node.js => nodejs.org/docs/v0.5.7/api/modules.html


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

                                                    ну а иначе всем скучно было бы — ещё один велосипед для AMD :-)

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