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



Что это?


Библиотека TOM.js даёт возможность облегчить такие задачи как:
  • загрузка/подгрузка скриптов/стилей с зависимостями
  • создание/наследование классов
  • перехват функций в пределах приложения

Зачем это если есть аналоги?


Я прекрасно осведомлен о том что есть всяческие RequireJS, klass.js и прочее, но данная библиотека представляет из себя наработки за несколько лет под конкретные задачи в проекте над которым я работаю.

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

TOM.boot — загрузка модулей и скриптов с зависимостями


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

Что-же умеет данная часть библиотеки?


  • Загрузка модулей и скриптов


    Для загрузки «модулей» (о них я расскажу немного ниже) и скриптов можно использовать около 5 вариаций вызовов

    1 способ, задача: загрузить /libraries/jquery/jquery.boot.js и /libraries/scroll/scroll.boot.js
    TOM.boot.load( 'libraries/*', [ 'jquery', 'scroll' ], function( ){ } );
    

    2 способ, задача: загрузить /jquery/jquery.boot.js
    TOM.boot.load( '*', 'jquery',  function( ){ } );
    

    3 способ, задача: загрузить /jquery.boot.js
    TOM.boot.load( '', 'jquery', function( ){ } );
    

    4 способ, задача: загрузить /jquery.js
    TOM.boot.load( '', 'jquery.js', function( ){ } );
    

    5 способ, задача: загрузить code.jquery.com/jquery-1.12.0.min.js
    TOM.boot.load( '', 'https://code.jquery.com/jquery-1.12.0.min.js', function( ){ } );
    

    Как можно понять по примерам — структура функции вызова следующая:
    TOM.boot.load( 'путь к скрипту/модулю', 'имя файла/модуля' {строка или массив}, 'callback по окончанию загрузки' );
    

    Логика подбора полного пути тут проста — если в имени нет расширения значит мы загружаем модуль (*.boot.js), если есть — то конкретный файл. А * (звёздочка) в пути подставляет в данное место имя модуля/файла, что позволяет сохранять понятную структуру директорий и файлов в больших приложениях.

  • Загрузка скриптов из модуля с учётом зависимостей


    Для начала следует разобрать что такое «модуль» в понимании данной библиотеки.
    Модуль — это файл *.boot.js в котором прописаны конкретные файлы и их зависимости от других «модулей» и скриптов.

    Содержимое *.boot.js выглядит следующим образом:
    TOM.boot.initiate( 'button', [
    	{ file: '*.style.css' }, // загружаем button.style.css
    	{ file: '*.interface.js' }, // загружаем button.interface.js
    	{ file: '*.core.js', require: '*.interface.js' }, // загружаем button.core.js с зависимостью от button.interface.js
    	{ file: 'testButton.core.js', require: [ 'jquery', '*.core.js' ] } // загружаем testButton.core.js с зависимостями
    ] );
    

    Здесь структура имеет следующий вид:
    TOM.boot.initiate( 'имя модуля', 'список объектов загружаемых файлов с параметрами' );
    

    Ну а сами объекты загружаемых файлов имеют следующую структуру:
    • file — имя загружаемого файла, где * (звёздочка) подставляет имя модуля
    • require — зависимость (список зависимостей) как от файлов текущего модуля, так и от других модулей
    • initialize — функция (список функций) которую следует выполнить после загрузки скрипта
    • main — булевая переменная указывающая что все в данном модуле зависят от данного файла


TOM.processor — перехват функций выполняемых внутри приложения


На самом деле перехват функций будет происходить только там где Вам это необходимо, только в тех объектах которые вы «пропроксируете».
В нашем проекте например имеется 3 объекта которые прописаны в window и с которыми мы работаем, это наши так-называемые «области видимости»: api, core, interface именно с ними мы и работаем, потому только их и проксируем.

TOM.js по умолчанию создаёт core и interface, но работать с ними или нет это личное дело каждого.

Как с этим работать?


  • Проксирование


    Первое что необходимо сделать — это произвести «проксирование» нужных объектов подобным образом:
    TOM.processor.proxy( core );
    TOM.processor.proxy( interface );
    

    Суть проксирования проста до безобразия — проходим по объекту и обёртываем функции определённым видом (добавляем pre-callback и post-callback).

  • Обработка/Перехват вызова функций


    После обработки нужных объектов вызванные внутри них функции- можно обрабатывать подобным образом:
    // Обработка вызова создания кнопки
    TOM.processor.bind( 'pre-core.test.addTestButton', function( sender )
    {	
    	// Если мы не хотим на самом деле создавать кнопку - прерываем её создание
    	if( !confirm( 'Действительно создать кнопку?' ) )
    	{
    		return false;
    	}
    } );
    

    Обработчик имеет такую структуру:
    TOM.processor.bind( '{pre или post}-имя функции вызова которой ждём', 'callback функция', 'параметры' );
    

    • pre или post — это обработка «до вызова» оригинала функции, или после — соответственно
    • параметры — это объект с настройками данного обработчика
      • stage — аналог pre/post в имени функции
      • label — «метка» по которой мы сможем снять именно этот обработчик, не затрагивая другие
      • priority — добавлять данный обработчик в начало или в конец очереди?

  • Другие возможности


    Помимо непосредственно возможности добавления и снятия «обработчиков», можно так-же:
    • «возбуждать фейковые события»:
      TOM.processor.signal( 'момент запуска (pre/post)', 'имя функции', 'объект вызывающий функцию', 'аргументы' );
    • производить единоразовую обработку события:
      TOM.processor.one( 'перечень тех же аргументов что и в TOM.processor.bind' );


TOM.classes — создание и наследование классов


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

  • Как создать/унаследовать класс?


    • 1й способ
      TOM.classes.create(
      	 'область видимости / объект в котором нужно создавать класс',
      	 'имя создаваемого класса',
      	 'класс от которого нужно наследоваться', 
      
      	// Функция конструктор
      	function constructor( )
      	{
      	},
      	
      	// -- Дальше идут функции в виде аргументов -- //
      
      	function foo( )
      	{
      	},
      
      	function bar( )
      	{
      	}
      )
      
    • 2й способ
      TOM.classes.create(
      	 'область видимости / объект в котором нужно создавать класс',
      	 'имя создаваемого класса',
      	 'класс от которого нужно наследоваться', 
      
      	// Функция конструктор
      	function constructor( )
      	{
      	},
      	
      	// Функции в массиве или объекте
      	[
      		function foo( )
      		{
      		},
      
      		function bar( )
      		{
      		}
      	]
      )
      
    • 3й способ — такой-же как и — но в массиве находится ещё и конструктор


  • Какие особенности данного скрипта?


    Кроме совместимости с TOM.processor, в созданных классах более-менее адекватно работает вызов функций из родителя, при помощи:
    this.__parentCall__( ); // Вызов функции родителя исходя из arguments.callee
    this.__parentFunction__( 'имя функции', 'аргументы' ); // Вызов функции по имени
    

    И много других мелочей.
    Примечание: Я знаю что arguments.callee это плохо, и оно не работает в strict mode, но пока удобной замены не придумал.


Дэмо страница: tredsnet.github.io/TOM
GitHub репозиторий: github.com/tredsnet/TOM

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

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

Проголосовало 134 человека. Воздержался 91 человек.

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

Комментарии (31)


  1. romy4
    26.02.2016 19:49
    -6

    Very nice.


  1. Fen1kz
    26.02.2016 20:29
    +1

    А в чем преимущество перед CommonJS?


    1. ange007
      26.02.2016 21:38

      Если речь именно о загрузке, то для тех кто знает как правильно готовить CommonJS — то никаких наверно.
      Но тут ведь не только загрузчик.

      Ну и под разные задачи — разный инструмент.
      Ниже я написал почему для меня на данный момент не актуально на разрабатываемом проекте ES6 и подобное.


  1. stardust_kid
    26.02.2016 20:38

    Имхо, лучше делить по функциональным частям.


    1. ange007
      26.02.2016 21:30

      boot.js в принципе самостоятелен, а вот classes зависит от processor в одном месте.


  1. bromzh
    26.02.2016 20:43
    +4

    Зачем оно, если есть es6 и webpack?
    Загрузка модулей прям как в умирающем requirejs. Зачем, если есть стандарт и commonjs?
    Классы и прокси есть в es2016. Опять же, стандарт.


    1. ange007
      26.02.2016 21:34
      -2

      Тогда когда делалась основа проекта (> 4 года назад) ажиотажа на es6 и всякие webpack`и не было, поэтому всё пилилось в таком виде, а в дальнейшем только дорабатывалось под конкретные задачи.
      Исключение только в boot.js, но опять таки если начинать переписывать что-то на es6 то тогда уж весь проект, а он очень уж не маленький.
      Поэтому работал с тем что есть, и что знаю.


      1. bromzh
        26.02.2016 22:55
        +5

        Когда стандарта не было, все развлекались как могли. Сейчас же куда лучше писать в соответствии со стандартом подключая полифилы при необходимости (тем более, что темп развития JS/ES резко, увеличился). Ведь в один прекрасный момент полифилы можно будет убрать, и код без изменений будет работать нативно. Если же использовать дубликаты каких-то фичей, но с другим синтаксисом, то может получится то, что у вас: куча легаси-кода который очень трудно рефакторить и поддерживать. Тем более, что гиганты веба (MS в частности) наконец сдвинулись с места и новые спецификации появляются намного быстрее.

        Кстати, RequireJS "реализует" AMD. Какие модули предоставляет ваша библиотека? Если там что-то неизвестное широким массам, то опять же, незачем использовать несовместимые и неизвестные решения.
        Я это к тому, что webpack умеет загружать те же AMD, так что процесс перехода со старой модульной системы к новой может происходить постепенно и прозрачно.


        1. ange007
          27.02.2016 01:00
          -6

          Сейчас же куда лучше писать в соответствии со стандартом подключая полифилы при необходимости (тем более, что темп развития JS/ES резко, увеличился). Ведь в один прекрасный момент полифилы можно будет убрать, и код без изменений будет работать нативно.

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

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

          Кстати, RequireJS «реализует» AMD. Какие модули предоставляет ваша библиотека?

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


      1. monolithed
        27.02.2016 14:19
        +1

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


        1. ange007
          27.02.2016 21:12

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


  1. kroshanin
    26.02.2016 22:16

    Очень не хватает работающих (!) примеров. Попытался реализовать пример перехвата функций, но ничего не выходит:
    http://jsbin.com/tumayopuje/edit?html,console,output

    В js уже включена возможность проксей, вот пример:
    http://jsbin.com/vocuzoxegi/edit?html,console,output
    Но хочется "поюзать" и вашу реализацию.

    Также, как мне кажется, было бы лучше, если бы вы разделили свою библиотеку на три (по функциональности), а не "все в кучу".


    1. ange007
      27.02.2016 00:44
      +1

      Дело собственно в следующем (я это совсем упустил из виду при описании, нужно некоторые моменты уточнить в документации видимо):
      К тем объектам которые создаются в TOM.js добавляется параметр scopeName, именно он выступает в качестве "имени стартового объекта", при перехвате.
      Варианта тут 2, либо добавлять scopeName к проксируемому объекту (что собственно не особо красиво и нужно), либо же просто писать так:

      TOM.processor.proxy( 'объект', 'имя объекта по которому будут происходить "всплытия" событий' );
      TOM.processor.proxy( MyObject, 'MyObject' );

      Исправленный пример: http://jsbin.com/yexexogadi/1/edit?html,console,output

      Также, как мне кажется, было бы лучше, если бы вы разделили свою библиотеку на три (по функциональности), а не «все в кучу».

      Я собственно думал в эту сторону, просто тот-же classes в одном месте зависим от processor (для верного перехвата создания класса). В остальном зависимостей каких-то особых — нет.


    1. DenimTornado
      27.02.2016 14:19

      вот пример:
      http://jsbin.com/vocuzoxegi/edit?html,console,output

      ""ReferenceError: Proxy is not defined at :12:17"


      1. kroshanin
        27.02.2016 17:24

        Proxy включен в стандарт ES-2015 и на текущий момент (еще пока) поддерживается далеко не всеми браузерами.
        Поэтому меня очень и заинтересовала реализация перехвата функций в библиотеке TOM.js
        Указанный мной пример точно будет работать на браузере Мозилла.
        Полный список поддерживающих Proxy браузеров можно глянуть здесь: https://kangax.github.io/compat-table/es6/


  1. monolithed
    27.02.2016 14:12
    +1

    Нарушен основной принцип SOLID — Принцип единственной обязанности.
    Как вообще можно было связать загрузчик и наследование?


    1. ange007
      27.02.2016 21:16
      -1

      Как вообще можно было связать загрузчик и наследование?

      Связка там только в одном месте, и убирается удалением 2х строчек:

      newClass = function( )
      {
          var args = Array.prototype.slice.call( arguments, 1 ),
              constructorFullName = ( ( this.__classScopeName__ !== '') ? this.__classScopeName__ + '.' + this.__className__ : this.__className__ ) + '.constructor';
      
          // Дополнительные "костыли" для TOM.processor, которые возволяют определять момент создания класса 
      ---->  TOM.processor.signal( 'pre', constructorFullName, this, args );
          classConstructor.apply( this, arguments );
      ---->  TOM.processor.signal( 'post', constructorFullName, this, args );
      };

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


  1. RubaXa
    27.02.2016 19:57

    Особо порадовал TravisCI, который просто запускает сборку проекта, никаких тесто у либы нет ;]
    А их стоило бы написать, даже не вооруженным взглядом в исходниках видно потенциальные баги, просто в банальном for-in без hasOwnProperty.

    Ну и главное, посмотрите на реализацию тех же RequireJS или SystemJS, эти ребята про загрузку JS много чего могут поведать: тык, тык и у вас. Загрузка CSS тоже доверия не вызывает.


    1. ange007
      27.02.2016 21:19

      Особо порадовал TravisCI, который просто запускает сборку проекта, никаких тесто у либы нет ;]
      А их стоило бы написать, даже не вооруженным взглядом в исходниках видно потенциальные баги, просто в банальном for-in без hasOwnProperty.

      Сборка через TravisCI добавлена исключительно для GitHub и перенесена из другого проекта.
      А тесты да, неплохо бы сделать.

      Ну и главное, посмотрите на реализацию тех же RequireJS или SystemJS, эти ребята про загрузку JS много чего могут поведать: тык, тык и у вас. Загрузка CSS тоже доверия не вызывает.

      Спасибо, изучу.


  1. savostin
    27.02.2016 21:06

    '{pre или post}-имя функции вызова которой ждём'

    Чем вызвано такое странное, если не сказать хуже, решение именования?
    Почему не 2 параметра: pre|post и нормальное имя функции, а лучше ссылка на нее.


    1. ange007
      27.02.2016 21:21

      Простите, не понял вопроса.

      Если для чего использовать вообще pre|post в имени функции? То для вот такого:

      TOM.processor.bind( 'pre-core.Button.create post-core.TestButton.create', function( ) {} );

      То-есть перехват нескольких функций при помощи одного обработчика.


      1. savostin
        27.02.2016 21:24

        Почему это — строка, которая потом, вероятно, парсится?
        Не лучше ли

        TOM.processor.bind( 'pre', core.Button.create post-core.TestButton.create, function( ) {} );


        1. ange007
          27.02.2016 21:39

          Ну я показал пример почему.
          Как быть в случае когда обработка идёт нескольких функций?
          В вашем случае получается "по умолчанию" — для всех идёт pre, и для избранных — post, вроде как это неразбериху добавляет.

          А так вообще идёт по умолчанию post (если пишется просто имя функции, без приставки).


          1. savostin
            27.02.2016 21:41

            Пардон, я post не заметил:

            TOM.processor.bind( {'pre' : core.Button.create, 'post' : core.TestButton.create}, function( ) {} );

            или даже

            TOM.processor.bind( {'pre' : [core.Button.create], 'post' : [core.TestButton.create]}, function( ) {} );


            1. ange007
              27.02.2016 21:47

              Как вариант конечно, интересная идея — спасибо.
              А про обработку ссылок — нужно подумать можно ли это безболезненно внедрить.


  1. TheShock
    27.02.2016 21:50

    Примечание: Я знаю что arguments.callee это плохо, и оно не работает в strict mode, но пока удобной замены не придумал.

    https://habrahabr.ru/company/wargaming/blog/271357/#comment_8671617
    Не портит стек-трейс, не ударяет по производительности и не использует сомнительных возможностей. Но, впринципе, уже пора переходить на ES6 Classes.


    1. ange007
      28.02.2016 14:21

      Спасибо. Посмотрю в эту сторону.


  1. ArturSitnikoff
    28.02.2016 04:46
    -5

    TOM.js — особая библиотека, для особых случаев

    Не ТОМ.js оригинальная библиотека называется, а feel.js, от слова "чувство", так как реагирует на взаимодействие с html, сначала реагирует, потом выполняет анализ ситуации. Смысл библиотеки — построить алгоритм работы веб-приложений, визуально напонимающих Flash приложения, путем парализации переходов по ссылкам коротким скриптом. Лично ручками писал.

    Рад что тебе интересны мои работы, жаль что без разрешения их берешь.


    1. ange007
      28.02.2016 12:09
      +1

      Что? Вы о чём?
      При таких громких заявлениях, будьте добры предоставить ссылки.


  1. extempl
    28.02.2016 13:51
    +1

    Касательно прокси — есть в js микро либа 2007 года под названием ajaxpect (от AOP) https://github.com/tmp0230/ajaxpect.


    1. ange007
      28.02.2016 14:20

      Благодарю.
      Элегантно и лаконично написано.