Не знаю на сколько верно я описал данную библиотеку в заголовке, но рассказать я хочу именно о ней.
Что это?
Библиотека 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й способ — такой-же как и 2й — но в массиве находится ещё и конструктор
- 1й способ
Какие особенности данного скрипта?
Кроме совместимости с TOM.processor, в созданных классах более-менее адекватно работает вызов функций из родителя, при помощи:
this.__parentCall__( ); // Вызов функции родителя исходя из arguments.callee this.__parentFunction__( 'имя функции', 'аргументы' ); // Вызов функции по имени
И много других мелочей.
Примечание: Я знаю что arguments.callee это плохо, и оно не работает в strict mode, но пока удобной замены не придумал.
Дэмо страница: tredsnet.github.io/TOM
GitHub репозиторий: github.com/tredsnet/TOM
Библиотека конечно не очень подготовлена для публикации, не «вычухан» код, не убраны лишние комментарии и заметки, где-то возможно нестандартное поведение (так как тестировалось только на нашем проекте). Но всему своё время, возможно и в таком виде библиотека будет кому-то полезна, а в случае заинтересованности пользователей — возможно и развитие в нужном направлении.
Спасибо что дочитали до конца. Буду рад любым комментариям, но прошу не забывать что библиотека создавалась под конкретные нужды, конкретного проекта.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (31)
Fen1kz
26.02.2016 20:29+1А в чем преимущество перед CommonJS?
ange007
26.02.2016 21:38Если речь именно о загрузке, то для тех кто знает как правильно готовить CommonJS — то никаких наверно.
Но тут ведь не только загрузчик.
Ну и под разные задачи — разный инструмент.
Ниже я написал почему для меня на данный момент не актуально на разрабатываемом проекте ES6 и подобное.
stardust_kid
26.02.2016 20:38Имхо, лучше делить по функциональным частям.
ange007
26.02.2016 21:30boot.js в принципе самостоятелен, а вот classes зависит от processor в одном месте.
bromzh
26.02.2016 20:43+4Зачем оно, если есть es6 и webpack?
Загрузка модулей прям как в умирающем requirejs. Зачем, если есть стандарт и commonjs?
Классы и прокси есть в es2016. Опять же, стандарт.ange007
26.02.2016 21:34-2Тогда когда делалась основа проекта (> 4 года назад) ажиотажа на es6 и всякие webpack`и не было, поэтому всё пилилось в таком виде, а в дальнейшем только дорабатывалось под конкретные задачи.
Исключение только в boot.js, но опять таки если начинать переписывать что-то на es6 то тогда уж весь проект, а он очень уж не маленький.
Поэтому работал с тем что есть, и что знаю.bromzh
26.02.2016 22:55+5Когда стандарта не было, все развлекались как могли. Сейчас же куда лучше писать в соответствии со стандартом подключая полифилы при необходимости (тем более, что темп развития JS/ES резко, увеличился). Ведь в один прекрасный момент полифилы можно будет убрать, и код без изменений будет работать нативно. Если же использовать дубликаты каких-то фичей, но с другим синтаксисом, то может получится то, что у вас: куча легаси-кода который очень трудно рефакторить и поддерживать. Тем более, что гиганты веба (MS в частности) наконец сдвинулись с места и новые спецификации появляются намного быстрее.
Кстати, RequireJS "реализует" AMD. Какие модули предоставляет ваша библиотека? Если там что-то неизвестное широким массам, то опять же, незачем использовать несовместимые и неизвестные решения.
Я это к тому, что webpack умеет загружать те же AMD, так что процесс перехода со старой модульной системы к новой может происходить постепенно и прозрачно.ange007
27.02.2016 01:00-6Сейчас же куда лучше писать в соответствии со стандартом подключая полифилы при необходимости (тем более, что темп развития JS/ES резко, увеличился). Ведь в один прекрасный момент полифилы можно будет убрать, и код без изменений будет работать нативно.
Я не сколько не против, а только за. Только вот даже как-то нет времени и опыта чтоб начать что-либо переделывать (Я думаю Вы сами можете представить сколько сил нужно на то чтоб переделать то что уже давно функционирует, и всё это после переработки — заставить работать. Ну и ситуация с проектом такая что мне нужно хоть как-то продвигаться по работе, а не останавливаться на "переработке" старого кода).
Но это только моя ситуация, остальных могу только призвать — использовать новые инструменты, с новыми возможностями если такая возможность есть (простите за каламбур).
Кстати, RequireJS «реализует» AMD. Какие модули предоставляет ваша библиотека?
Никаких. Модули которые предоставляет наша библиотека — относительные. Они из себя представляют только список загружаемых/зависимый и инициализируемых скриптов. Использовать в принципе можно любые скрипты/модули которые можно загрузить браузером. Я конечно сильно не вникал в это, но посмотрите пожалуйста на пример и скажите если это не так.
monolithed
27.02.2016 14:19+1Все это есть сейчас. Лучше бы рассказали с какими трудностями столкнулись продолжая использовать данное решение.
Ничего плохого нет в том, чтобы выкладывать свои наработки в открытый доступ, но зачем делать из этой богом забытой библиотеки анонс?ange007
27.02.2016 21:12Да нет трудностей вроде как, всё работает как надо для тех задач под которые это и писалось.
kroshanin
26.02.2016 22:16Очень не хватает работающих (!) примеров. Попытался реализовать пример перехвата функций, но ничего не выходит:
http://jsbin.com/tumayopuje/edit?html,console,output
В js уже включена возможность проксей, вот пример:
http://jsbin.com/vocuzoxegi/edit?html,console,output
Но хочется "поюзать" и вашу реализацию.
Также, как мне кажется, было бы лучше, если бы вы разделили свою библиотеку на три (по функциональности), а не "все в кучу".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 (для верного перехвата создания класса). В остальном зависимостей каких-то особых — нет.
DenimTornado
27.02.2016 14:19вот пример:
http://jsbin.com/vocuzoxegi/edit?html,console,output
""ReferenceError: Proxy is not defined at :12:17"kroshanin
27.02.2016 17:24Proxy включен в стандарт ES-2015 и на текущий момент (еще пока) поддерживается далеко не всеми браузерами.
Поэтому меня очень и заинтересовала реализация перехвата функций в библиотеке TOM.js
Указанный мной пример точно будет работать на браузере Мозилла.
Полный список поддерживающих Proxy браузеров можно глянуть здесь: https://kangax.github.io/compat-table/es6/
monolithed
27.02.2016 14:12+1Нарушен основной принцип SOLID — Принцип единственной обязанности.
Как вообще можно было связать загрузчик и наследование?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 ); };
Сделано это для возможности перехвата события по созданию класса, более элегантного решения для подобного не нашёл.
RubaXa
27.02.2016 19:57Особо порадовал TravisCI, который просто запускает сборку проекта, никаких тесто у либы нет ;]
А их стоило бы написать, даже не вооруженным взглядом в исходниках видно потенциальные баги, просто в банальномfor-in
безhasOwnProperty
.
Ну и главное, посмотрите на реализацию тех же RequireJS или SystemJS, эти ребята про загрузку JS много чего могут поведать: тык, тык и у вас. Загрузка CSS тоже доверия не вызывает.ange007
27.02.2016 21:19Особо порадовал TravisCI, который просто запускает сборку проекта, никаких тесто у либы нет ;]
А их стоило бы написать, даже не вооруженным взглядом в исходниках видно потенциальные баги, просто в банальном for-in без hasOwnProperty.
Сборка через TravisCI добавлена исключительно для GitHub и перенесена из другого проекта.
А тесты да, неплохо бы сделать.
Ну и главное, посмотрите на реализацию тех же RequireJS или SystemJS, эти ребята про загрузку JS много чего могут поведать: тык, тык и у вас. Загрузка CSS тоже доверия не вызывает.
Спасибо, изучу.
savostin
27.02.2016 21:06'{pre или post}-имя функции вызова которой ждём'
Чем вызвано такое странное, если не сказать хуже, решение именования?
Почему не 2 параметра:pre|post
и нормальное имя функции, а лучше ссылка на нее.ange007
27.02.2016 21:21Простите, не понял вопроса.
Если для чего использовать вообще pre|post в имени функции? То для вот такого:
TOM.processor.bind( 'pre-core.Button.create post-core.TestButton.create', function( ) {} );
То-есть перехват нескольких функций при помощи одного обработчика.savostin
27.02.2016 21:24Почему это — строка, которая потом, вероятно, парсится?
Не лучше ли
TOM.processor.bind( 'pre', core.Button.create post-core.TestButton.create, function( ) {} );
ange007
27.02.2016 21:39Ну я показал пример почему.
Как быть в случае когда обработка идёт нескольких функций?
В вашем случае получается "по умолчанию" — для всех идёт pre, и для избранных — post, вроде как это неразбериху добавляет.
А так вообще идёт по умолчанию post (если пишется просто имя функции, без приставки).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( ) {} );
ange007
27.02.2016 21:47Как вариант конечно, интересная идея — спасибо.
А про обработку ссылок — нужно подумать можно ли это безболезненно внедрить.
TheShock
27.02.2016 21:50Примечание: Я знаю что arguments.callee это плохо, и оно не работает в strict mode, но пока удобной замены не придумал.
https://habrahabr.ru/company/wargaming/blog/271357/#comment_8671617
Не портит стек-трейс, не ударяет по производительности и не использует сомнительных возможностей. Но, впринципе, уже пора переходить на ES6 Classes.
ArturSitnikoff
28.02.2016 04:46-5TOM.js — особая библиотека, для особых случаев
Не ТОМ.js оригинальная библиотека называется, а feel.js, от слова "чувство", так как реагирует на взаимодействие с html, сначала реагирует, потом выполняет анализ ситуации. Смысл библиотеки — построить алгоритм работы веб-приложений, визуально напонимающих Flash приложения, путем парализации переходов по ссылкам коротким скриптом. Лично ручками писал.
Рад что тебе интересны мои работы, жаль что без разрешения их берешь.ange007
28.02.2016 12:09+1Что? Вы о чём?
При таких громких заявлениях, будьте добры предоставить ссылки.
extempl
28.02.2016 13:51+1Касательно прокси — есть в js микро либа 2007 года под названием
ajaxpect
(от AOP) https://github.com/tmp0230/ajaxpect.
romy4
Very nice.