JS Загрузчик + шаблонизатор или история очередного велика
Изобретение своего «уникального» велосипеда считаю делом весьма полезным, если: это не отвлекает от работы (или отвлекает, но не сильно); дает некий новый положительный опыт; результаты можно где-то как-то использовать; сам процесс в кайф. От того я и отталкивался, начав конструировать свой «велик» года 3 назад и, наверное, раза 3–4 переписав его к сегодняшнему дню.
А началось все с загрузчика и JQ
RequireJS — безусловно милая и весьма эффективная утилита, позволяющая организовать модульную систему на клиентской стороне весьма быстро и непринужденно. И всем она меня устраивала, кроме двух моментов:
- «внешний вид»
- система кеширования.
Под «внешним видом» я понимаю то, что ссылки на модули помещаются в строке аргументов родительского модуля.
requirejs(["module_0", "module_1"], function(module_0, module_1) {
});
И когда модулей становилось все больше и больше — это превращалось в какое-то безобразие.
С одной стороны, можно вызвать нужный модуль где-то внутри кода (если он используется очень и очень редко), с другой — хотелось бы видеть все зависимости родительского модуля в одном месте.
Кроме того, меня немного раздражала необходимость оперирования путями при объявлении зависимостей, а не переменными, содержащими необходимые данные. Нет, можно, конечно где-то объявить глобальный объект с путями и привести все к примерно такому виду (но это все равно как-то некрасиво):
requirejs([modules.mod_0, modules.mod_1], function(module_0, module_1) {
});
Что же касается управления кэшем, то мне оно показалось, скажем так, не явным.
Со временем у меня стали вырисовываться требования к уже своему загрузчику и на сегодняшний день они сформулированы следующим образом:
- все JS и CSS файлы должны кэшироваться, а система кэширования должна иметь явное и понятное управление.
- объявляемые во всем приложении модули должны быть описаны в одном конкретном месте (единый регистр).
- объявление зависимостей модулей друг от друга должно происходить без использования путей, а с использованием ссылок на единый регистр (пункт 2) или имен модулей.
- должен быть контроль очередности загрузки модулей, а также возможность асинхронной подкачки нужных ресурсов.
И вот что у меня получилось. Ядро состоит из трех файлов, из названий которых вполне следует и их назначение:
Замечу, что собственно подключать нужно только [flex.core.js], а регистр модулей и настройки будут подхвачены автоматически.
Но здесь же и первая неприятная новость. Разработчик строго привязан к именам файлов и их расположению. [flex.registry.modules.js] и [flex.settings.js] должны быть там же, где и базовый модуль [flex.core.js], а имена их не могут быть изменены.
Но поскольку это мой велик и пока я единственный разработчик — меня это обстоятельство не особо беспокоит. Кроме того, такая организация меня очень даже устраивает. Уже сейчас есть с десяток проектов, написанных с использованием flex, и я всегда знаю, где мне найти настройки и полный перечень используемых модулей.
Итак, давайте взглянем на [flex.registry.modules.js] (регистр модулей из «живого» проекта).
flex.libraries = {
//Basic binding controller
binds : { source: 'KERNEL::flex.binds.js', hash: 'HASHPROPERTY' },
//Basic events controller
events : { source: 'KERNEL::flex.events.js', autoHash: false },
//Collection of tools for management of DOM
html : { source: 'KERNEL::flex.html.js' },
css : {
//Controller CSS animation
animation : { source: 'KERNEL::flex.css.animation.js'},
//Controller CSS events
events : { source: 'KERNEL::flex.css.events.js' },
},
//Collection of UI elements
ui : {
//Controller of window (dialog)
window : {
//Controller of window movement
move : { source: 'KERNEL::flex.ui.window.move.js' },
//Controller of window resize
resize : { source: 'KERNEL::flex.ui.window.resize.js' },
//Controller of window resize
focus : { source: 'KERNEL::flex.ui.window.focus.js' },
//Controller of window maximize / restore
maximize: { source: 'KERNEL::flex.ui.window.maximize.js' },
},
//Controller of templates
templates : { source: 'KERNEL::flex.ui.templates.js' },
//Controller of patterns
patterns : { source: 'KERNEL::flex.ui.patterns.js' },
//Controller of scrollbox
scrollbox : { source : 'KERNEL::flex.ui.scrollbox.js' },
//Controller of itemsbox
itemsbox : { source: 'KERNEL::flex.ui.itemsbox.js' },
//Controller of areaswitcher
areaswitcher: { source: 'KERNEL::flex.ui.areaswitcher.js' },
//Controller of areascroller
areascroller: { source: 'KERNEL::flex.ui.areascroller.js' },
//Controller of arearesizer
arearesizer : { source: 'KERNEL::flex.ui.arearesizer.js' },
},
presentation: { source: 'program/presentation.js' },
};
Как вы видите это просто перечень всех модулей, используемых в приложении. Если вы заметили, то для каждого модуля мы можем определить пару переменных (помимо собственно пути [source]):
- [string] hash — строка, которая служит для «ручного» управления кэшем. До тех пор, пока эта строка будет оставаться неизменной, модуль будет подгружаться из кэша. Но как только мы изменим ее значение, модуль обновится.
- [bool] autoHash — позволяет вовсе отключить кэширование указанного модуля. Дело в том, что если [hash] строка не задана, то flex будет управлять кэшем в автоматическом режиме, и чтобы исключить какой-то модуль из кэширования вовсе, достаточно лишь определить для него [autoHash = false].
Еще один момент, который вы наверняка заметили — это группировка. Модули не представлены сквозным списком, а разбиты на группы, что делает всю модульную систему в целом более осмысленной и прозрачной.
Ну и еще раз на всякий случай — это всего лишь регистр (список) модулей. Определение здесь того или иного модуля вовсе не означает, что он будет загружен. Загрузка регулируется иным образом.
Идем дальше. Посмотрим на настройки. Файл [flex.settings.js] с работающего сайта.
flex.init({
resources: {
MODULES: [
'presentation', 'ui.patterns'
],
EXTERNAL: [
{ url: '/program/body.css', hash: 'HASHPROPERTY' },
],
ASYNCHRONOUS: [
{
resources: [
{ url: 'http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js' },
{ url: '/program/highcharts/highcharts.js', after: ['http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js'] },
{ url: '/program/highcharts/highcharts-more.js', after: ['/program/highcharts/highcharts.js'] },
{ url: '/program/highcharts/exporting.js', after: ['/program/highcharts/highcharts.js'] },
],
storage : false,
finish : function () {
//Do something
}
}
],
},
events: {
onFlexLoad: function () {
//Do something
},
onPageLoad: function () {
var presentation = flex.libraries.presentation.create();
presentation.start();
}
},
settings: {
CHECK_PATHS_IN_CSS: true
},
logs: {
SHOW: ['CRITICAL', 'LOGICAL', 'WARNING', 'NOTIFICATION', 'LOGS', 'KERNEL_LOGS']
}
});
Здесь тоже все довольно просто.
В секции с говорящим названием [MODULES] определяется перечень тех модулей, которые необходимо загрузить до старта всего приложения. Обратите внимание, что мы указываем не ссылки, а названия модулей в соответствии с регистром, то есть так как это определено в [flex.registry.modules.js] (исключая «flex.libraries»).
Массив [EXTERNAL] содержит перечень тех ресурсов, которые должны быть загружены по заданным URL. Так же как в списке модулей, здесь можно оперировать такими свойствами как [hash] и [authHash] для управления кэшем отдельно взятого ресурса.
Секция [ASYNCHRONOUS] фактически тоже самое, что и [EXTERNAL], но: во-первых, начинает загружаться сразу (не дожидаясь загрузки основных модулей); во-вторых, здесь мы имеем возможность предопределить порядок загрузки. В данном конкретном примере, файл [highcharts.js] не будет загружаться до тех пор, пока не будет загружена библиотека JQ.
Кроме того, в секции [ASYNCHRONOUS] мы можем определить любое количество групп ресурсов (bundles) со своими обработчиками завершения загрузки (событие [finish]).
Ресурсы из секции [ASYNCHRONOUS] тоже кэшируются, но управление кэшем здесь менее гибкое. Мы можем лишь включить его или выключить, определяя значение свойства [storage: true / false].
Очень укрупненно процесс загрузки выглядит следующим образом:
- Загрузка flex.core.js
- Подхват flex.registry.modules.js, flex.registry.events.js и flex.settings.js
- Старт загрузки модулей, определенных в [MODULES] и здесь же старт загрузки всего, что есть в [ASYNCHRONOUS]
- Формирование списка зависимостей (модули и ресурсы, запрашиваемые модулями из [MODULES]). Загрузка всех требуемых модулей и ресурсов.
- По завершению загрузки модулей из [MODULES] (вместе с зависимостями) старт загрузки ресурсов из [EXTERNAL]
- По завершению загрузки всего из [EXTERNAL] и [ASYNCHRONOUS] (если в настройках указано, что нужно ожидать асинхронно загружаемые ресурсы) вызов события [onFlexLoad] и ожидание события [onPageLoad]
Вот собственно и весь процесс загрузки.
Я намерено сделал разделение на три группы [MODULES], [EXTERNAL] и [ASYNCHRONOUS]. Такой подход позволяет мне ясно и четко видеть, что есть часть текущего приложения, а что есть сторонние решения, используемые в проекте.
Что еще за flex.registry.events.js?
Этот файл часть общей системы, но он пригождается лишь тогда, когда используются некоторые уже написанные мной библиотеки. Вот его содержание:
flex.registry.events = {
//Events of UI
ui: {
//Events of scrollbox
scrollbox : {
GROUP : 'flex.ui.scrollbox',
REFRESH : 'refresh',
},
//Events of itemsbox
itemsbox : {
GROUP : 'flex.ui.itemsbox',
REFRESH : 'refresh',
},
//Events of arearesizer
arearesizer : {
GROUP : 'flex.ui.arearesizer',
REFRESH : 'refresh',
},
window : {
//Events of window resize module
resize : {
GROUP : 'flex.ui.window.resize',
REFRESH : 'refresh',
FINISH : 'finish',
},
//Events of window maximize / restore module
maximize: {
GROUP : 'flex.ui.window.maximize',
MAXIMIZED : 'maximized',
RESTORED : 'restored',
CHANGE : 'change',
}
}
},
//Events of Flex (system events)
system: {
//Events of logs
logs: {
GROUP : 'flex.system.logs.messages',
CRITICAL : 'critical',
LOGICAL : 'logical',
WARNING : 'warning',
NOTIFICATION: 'notification',
LOGS : 'log',
KERNEL_LOGS : 'kernel_logs',
},
cache: {
GROUP : 'flex.system.cache.events',
ON_NEW_MODULE : 'ON_NEW_MODULE',
ON_UPDATED_MODULE : 'ON_UPDATED_MODULE',
ON_NEW_RESOURCE : 'ON_NEW_RESOURCE',
ON_UPDATED_RESOURCE : 'ON_UPDATED_RESOURCE',
}
}
};
Как вы уже догадались — это всего лишь идентификаторы событий в ядре и модулях. Зачем я это вынес в отдельный файл? Чтобы создать некий уровень абстракции и дать возможность модулям «общаться» друг с дружкой. Кроме того, имея такой регистр в публичной пространстве, разработчик получает замечательную возможность реагировать на интересные ему события:
flex.events.core.listen(
flex.registry.events.ui.window.resize.GROUP,
flex.registry.events.ui.window.resize.REFRESH,
function (node, area_id) {
//Do something
}
);
Ну наконец-то настало время посмотреть и на шаблон модуля и если вы еще не зеваете от скуки, то вот он:
var protofunction = function () {
//Constructor of module
};
protofunction.prototype = function () {
//Module body
var //Get modules
html = flex.libraries.html.create(),
events = flex.libraries.events.create();
return {
//Some methods
};
};
flex.modules.attach({
name : 'ui.itemsbox',
protofunction : protofunction,
reference : function () {
flex.libraries.events();
flex.libraries.html();
},
resources : [
{ url: 'KERNEL::/css/flex.ui.itemsbox.css' }
],
});
Вся магия, как не сложно догадаться, сокрыта в методе [flex.modules.attach]. Пробежимся его по свойствам.
- name — это то как наш модуль называется и это вот [name] должен соответствовать имени определенному в регистре, том самом [flex.registry.modules.js].
- protofunction — это собственно наш модуль. Он может иметь и свой конструктор, который будет инициирован лишь однажды, при инициализации модуля.
- references — это место, где определяются зависимости. Обратите внимание на то, как они определяются: нет никаких строковых значений. К моменту, когда ваш модуль начнет загрузку регистр модулей уже будет содержать функции-вызовы. То есть выполнение [flex.libraries.events ()] приведет к тому, что до инициализации данного модуля будет загружен и инициализирован модуль [events].
- Массив resources — это локальный (для модуля) аналог [EXTERNAL] из настроек [flex.settings.js]. Здесь вы вольны определить перечень ресурсов (JS и/или CSS), которые должны быть загружены до инициализации модуля.
- В дополнение можно еще определить два события [onBeforeAttach] и [onAfterAttach], которые сработают до и после инициализации модуля соответственно.
Вызов же самих модулей можно производить в любом месте кода с помощью нашего регистра, а именно через функцию — create, например так: html = flex.libraries.html.create (), после чего переменная html станет ссылкой на функционал вызываемого модуля.
Итак, вот основная часть того, как организуется модульная система с помощью моего flex-велосипеда. Есть одно место, где описываются модули; есть место где указываются настройки и производится запуск приложения; и есть сами модули.
Для очень маленьких проектов (буквально с парой, тройкой модулей) такая система может показаться излишней, и я думаю, что так оно и есть. Однако для решения подобных задач мы можем вовсе не определять регистр и настройки, то есть не создавать файлы flex.registry.modules.js и flex.settings.js. В этом случае, создание модулей будет очень походить на то, как это делает RequireJS:
_append({
name : 'Base.B',
require : [
{ url: 'ATTACH::D_file.js' },
],
module : function () {
var //Get modules.
D = flex.libraries.D.create();
//Module body
return {
//Module methods
};
},
});
Как вы можете заметить, несмотря на то что файл-регистр flex.registry.modules.js не используется, список модулей все равно создается и их вызов производится также, как и для обычных модулей, через функцию create.
Единственная разница заключается в том, что теперь мы вынуждены указывать зависимости в виде ссылок (путей), что для маленьких проектов, безусловно, не критично.
Также без файла настроек flex.settings.js встает вопрос запуска приложения, ведь события onFlexLoad и onPageLoad не определены. Этот вопрос решен с помощью запускаемого модуля:
_append({
name : 'A',
require : [
{ url: 'ATTACH::B_file.js' },
{ url: 'ATTACH::C_file.js' },
{ url: 'ATTACH:css/B_file.css' }
],
launch : function(){
var //Get modules.
B = flex.libraries.Base.B.create(),
C = flex.libraries.C.create();
//Do something
}
});
То есть заменив свойство [module] на свойство [launch] мы создадим запускающий модуль, который, разумеется, может быть только один в приложении.
Кроме того, вы также можете группировать свои модули. Обратите внимание как определено название модуля [B] — «Base.B». Это значит, что будет создана группа «Base» и к ней будет привязан наш модуль [B].
Еще чуть-чуть о скучном и будет самое интересное
Закономерно у вас может возникнуть вопрос: «Велоспорт — это, конечно, полезно, но бро, ты всего лишь повторил все то, что делает RequireJS. Зачем?».
Все ради двух вещей: первое — это упорядочивание (через файл настроек и единый регистр модулей), а второе — это кэширование.
Flex не опирается на стандартное кэширование браузера, используя для этих целей локальное хранилище — localStorage. Допустим имеем в системе от 20 до 30 модулей (в зависимости от страницы). В первый запуск приложения все будет подключено путем банального создания
Login
{{login}}Password
{{password}}