Изобретение своего «уникального» велосипеда считаю делом весьма полезным, если: это не отвлекает от работы (или отвлекает, но не сильно); дает некий новый положительный опыт; результаты можно где-то как-то использовать; сам процесс в кайф. От того я и отталкивался, начав конструировать свой «велик» года 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) {
});


Что же касается управления кэшем, то мне оно показалось, скажем так, не явным.

Со временем у меня стали вырисовываться требования к уже своему загрузчику и на сегодняшний день они сформулированы следующим образом:
  1. все JS и CSS файлы должны кэшироваться, а система кэширования должна иметь явное и понятное управление.
  2. объявляемые во всем приложении модули должны быть описаны в одном конкретном месте (единый регистр).
  3. объявление зависимостей модулей друг от друга должно происходить без использования путей, а с использованием ссылок на единый регистр (пункт 2) или имен модулей.
  4. должен быть контроль очередности загрузки модулей, а также возможность асинхронной подкачки нужных ресурсов.


И вот что у меня получилось. Ядро состоит из трех файлов, из названий которых вполне следует и их назначение:


Замечу, что собственно подключать нужно только [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].

Очень укрупненно процесс загрузки выглядит следующим образом:
  1. Загрузка flex.core.js
  2. Подхват flex.registry.modules.js, flex.registry.events.js и flex.settings.js
  3. Старт загрузки модулей, определенных в [MODULES] и здесь же старт загрузки всего, что есть в [ASYNCHRONOUS]
  4. Формирование списка зависимостей (модули и ресурсы, запрашиваемые модулями из [MODULES]). Загрузка всех требуемых модулей и ресурсов.
  5. По завершению загрузки модулей из [MODULES] (вместе с зависимостями) старт загрузки ресурсов из [EXTERNAL]
  6. По завершению загрузки всего из [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 модулей (в зависимости от страницы). В первый запуск приложения все будет подключено путем банального создания <script> и <link> для JS-файлов и CSS-файлов соответственно. Но при этом в фоновом режиме flex попытается «добыть» контент всех ресурсов (содержание файлов). Получив его, flex его сохранит и привяжет к конкретным URL, по которым они (ресурсы) были запрошены. И уже со второй загрузки страницы будет не 20 – 30 обращений к серверу за файлами, а всего 4 (для flex.core.js, настроек, регистра модулей и регистра событий). Все остальное будет взято из localStorage и интегрировано в страницу, что самым благоприятным образом сказывается на скорости загрузки всего приложения.

Но и это еще не все. Кэширование можно контролировать как вручную (задавая хэши для тех или иных модулей), так и доверить сие рутинное дело flex, который после окончательной загрузки страницы в фоновом режиме производит «опрос» HEADERs по URL всех ресурсов, пытаясь определить размер файла и дату его создания (изменения). Основываясь на полученных данных flex инициализирует обновление модуля (или ресурса), чьи параметры изменились, таким образом поддерживая всю систему в актуальном состоянии.

Я, наверное, вас уже весьма утомил, но теперь будет самое интересное. Надеюсь )

Шаблоны


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

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

Еще одна вещь, которую мне всегда хотелось иметь – это возможность не только увидеть шаблон отдельно от приложения, но запустить его «в отрыве» от страницы. Например, есть у нас шаблон окна авторизации – вот мне бы хотелось в пару кликов открыть страницу на которой был бы только этот шаблон. Зачем? Быстро отладить стили, например, или быстро отловить жучка в коде.

По результатам долгих размышлений были выведены следующие требования к контроллеру шаблонов:
  • Файл шаблона можно открывать в браузере, как отдельную страницу.
  • Шаблон можно инициализировать с тестовым контентом для отладки в режиме «standalone».
  • Шаблон должен кэшироваться и обновляться только по необходимости (если есть изменения на серверной стороне). То же самое касается и всех ресурсов шаблона (JS и CSS файлы).


В общем давайте смотреть, что получилось (контроллер – flex.ui.patterns.js).

Основной файл шаблона – это банально html-файл. Как этот, например:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Flex.Template</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <link rel="stylesheet" type="text/css" href="pattern.css" />
    <script type="text/javascript" src="controler.js"></script>
</head>
<body>
    <div data-type="Pattern.Login">
        <p>Login</p>
        {{login}}
        <p>Password</p>
        {{password}}
        <div data-type="Pattern.Controls">{{controls}}</div>
    </div>
</body>
</html>


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

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

Дальше давайте на примере. Создадим всплывающее окно для авторизации пользователя. Нам понадобятся несколько шаблонов (для экономии места, привожу только содержимое тега <body>).

(A) Шаблон popup’а

    <div data-style="Popup" data-flex-ui-window-move-container="{{id}}" data-flex-ui-window-resize-position-parent="{{id}}" data-flex-ui-window-maximize="{{id}}">
        <div data-style="Popup.Container" data-flex-ui-window-resize-container="{{id}}">
            <div data-style="Popup.Title" data-flex-ui-window-move-hook="{{id}}">
                <p data-style="Popup.Title">{{title}}</p>
                <div data-style="Popup.Title.Switcher" data-state="max" data-flex-window-maximize-hook="{{id}}"></div>
            </div>
            <div data-style="Popup.Content">{{content}}</div>
            <div data-style="Popup.Bottom">
                <p data-style="Popup.Bottom" id="test_bottom_id">{{bottom}}</p>
                <div data-style="Window.Resize.Coner"></div>
            </div>
        </div>
    </div>


(B) Шаблон окна авторизации (он уже был чуть выше)

    <div data-type="Pattern.Login">
        <p>Введите логин</p>
        {{login}}
        <p>Введите пароль</p>
        {{password}}
        <div data-type="Pattern.Controls">{{controls}}</div>
    </div>


( C) Шаблон текстовых полей

   <p>Введено: <span>{{::value}}</span></p>
    <div data-type="TextInput.Wrapper">
        <div data-type="TextInput.Container">
            <input data-type="TextInput" type="{{type}}" value="{{::value}}" name="TestInput" />
        </div>
    </div>


(D) И шаблон кнопок

<a data-type="Buttons.Flat" id="{{id}}">{{title}}</a>


По синтаксису есть пара моментов. В фигурных скобках {{controls}} указываются зацепки (hooks). Да, позаимствовал у WordPress. В фигурных скобках с двойным двоеточием {{::value}} указываются данные, которые необходимо связать с DOM деревом и поместить в модель. Чуть позже все увидите.

Итак, вызов (сборка) итогового шаблона будет выглядеть так:

_node(document.body).ui().patterns().append({
    url     : '/patterns/popup/pattern.html', //A
    hooks   : {
        id      : id,
        title   : 'Login popup',
        content : patterns.get({
            url     : '/patterns/patterns/login/pattern.html',//B
            hooks   : {
                login   : patterns.get({
                    url     : '/patterns/controls/textinput/pattern.html',//C
                    hooks   : { type: 'text' }
                }),
                password: patterns.get({
                    url     : '/patterns/controls/textinput/pattern.html',//C
                    hooks   : { type: 'password' }
                }),
                controls: patterns.get({
                    url     : '/patterns/buttons/flat/pattern.html',//D
                    hooks   : [{ title: 'Войти', id: 'login_button' }, { title: 'Вернуться', id: 'cancel_button' }]
                }),
            },
            resources: {
                one: 'one',
                two: 'two'
            },
        })
    },
    callbacks: {
        success: function (model, binds, map, resources) {
            var instance = this;
        }
    },
});


После выполнения этого метода будет «собрано» окно авторизации и присоединено к тегу body. Само собой, метод работает асинхронно (ведь как минимум при первой загрузки страницы придется обращаться к серверу за файлами шаблона), поэтому ничего не возвращает.
Обратите внимание на аргументы функции обратного вызова success (из секции callbacks). Давайте посмотрим повнимательнее – там полезные штуки внутри.

model – это ссылка на модель, которая была собрана под данный конкретный экземпляр шаблона. Если вы посмотрите на шаблон текстового поля ©, то увидите, что мы связали значение input.value и span.innerHTML (расположенный в первом параграфе). Теперь, если изменится значение текстового поля, то изменится и надпись над ним. Кроме того, можно «влезть в модель» и изменить значение input.value через модель, а именно:
  • model.__content__.__login__.value (для логина)
  • model.__content__.__password__.value (для пароля).

Заметьте, свойства, обрамленные в двойное подчеркивание __something__ повторяют вложенность шаблона (его структуру).

binds своей структурой полностью повторяет model. То есть в binds тоже будет и model.__content__.__login__.value и model.__content__.__password__.value. Но только теперь вам будет доступно не значение свойства [value], а два метода: addHandle(handle) и removeHandle(id). Как вы уже догадались, так мы можем «подцепить» обработчик, который сработает как при изменении модели напрямую (через изменение свойств model), так и если изменится DOM (то есть значение input).

map – это ссылки на родительские узлы для каждого hook’a. Иными словами – это карта узлов для данного конкретного шаблона. Она может использоваться для разных целей, но лично мне она пригождается для указания контекста при поиске узлов через селекторы.

И последнее, это resources. Это просто вспомогательный объект. Посмотрите на метод, создающий шаблон. Как видите, там есть аналогичное свойство. Вот именно оно и будет возвращено здесь.

Теперь давайте вернемся к первому примеру. Как я и сказал, мы вольны подключать любое количество JS и CSS ресурсов к шаблону.

    <link rel="stylesheet" type="text/css" href="pattern.css" />
    <script type="text/javascript" src="controler.js"></script>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>


Инициализация шаблона будет начата только после того, как все ресурсы будут загружены. Ну и само собой разумеющееся, все «складируется» в localStorage и по возможности достается оттуда вместо запросов к серверу.

Но в данном конкретном случае нам важна не сама возможность подключения JQ к отдельно взятому шаблону, а файлик controller.js. Он, кстати, называться может как угодно, главное, что там внутри. А внутри – контроллер:

_controller(function (model, binds, map, resources) {
    var instance    = this,
        clone       = null;
    clone = instance.clone({
        id      : 'clonned_pattern',
        title   : 'Clonned dialog window',
        content : {
            login: {
                type: 'text',
            },
            password: {
                type: 'password',
            },
            controls: [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }],
        }
    });
    //Do something;
}); 


С аргументами контроллера я вас уже познакомил (они такие же как в callbacks.success). Мне остается только добавить, что, получив экземпляр шаблона, вы можете создавать его клоны. Заметьте, что синтаксис создания клона существенно упрощается по сравнению с инициализацией шаблона.

Контролер будет вызваться всякий раз не только при инициализации шаблона, но при создании клона.

Ну и наконец о том, ради чего все это затевалось. Если мы имеем простой шаблон (без вложенности и контроллеров), то мы можем его отрыть браузером просто как html-файл и отладить стили. Быстро и эффективно.

Если же наш шаблон имеет вложенность, да еще и контроллер, то для отладки нам всего-то нужно создать отдельный html-файл для теста. Его создание – это преимущество copy/paste.

Тестовый файл для нашего примера с всплывающим окном авторизации

<!DOCTYPE html>
<html>
<head>
    <title></title>
	<meta charset="utf-8" />
    <script type="text/javascript" src="../../../kernel/flex.core.js"></script>
</head>
<body>
    <script>
        function flexPatternTest() {
            var id          = flex.unique(),
                patterns    = flex.libraries.ui.patterns.create(),
                _pattern    = patterns.get({
                url     : '/patterns/popup/pattern.html',
                node    : document.body,
                hooks   : {
                    id      : id,
                    title   : 'Test dialog window',
                    content : patterns.get({
                        url     : '/patterns/patterns/login/pattern.html',
                        hooks   : {
                            login       : patterns.get({
                                url     : '/patterns/controls/textinput/pattern.html',
                                hooks   : {
                                    type: 'text',
                                }
                            }),
                            password    : patterns.get({
                                url     : '/patterns/controls/textinput/pattern.html',
                                hooks   : {
                                    type: 'password',
                                }
                            }),
                            controls    : patterns.get({
                                url     : '/patterns/buttons/flat/pattern.html',
                                hooks   : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }]
                            }),
                        }
                    })
                },
                callbacks: {
                    success: function (model, binds, map, resources) {
                        var instance = this,
                    }
                }
            }).render();
        };
        flexPatternTest.include = [
            'ui.window.move',
            'ui.window.resize',
        ];
        flexPatternTest.exclude = [
            'flex.presentation',
        ];
    </script>
</body>
</html>


Все что мы сделали – это «прицепили» flex и определили функцию [flexPatternTest]. Как только flex закончит инициализироваться, будет предпринята попытка найти эту функцию и запустить. Profit – ваш шаблон запущен со всей необходимой инфраструктурой в режиме “standalone”.
Замечу, что как видно из примера, вы можете исключить отдельные модули из загрузки, а также включить необходимые, через свойства [exclude] и [include] соответственно.

Первая «сборка» приведенного в пример шаблона занимает в среднем 200 – 300 мс.; последующие 100 – 150 мс.

Непосредственно контроллер шаблонов (flex.ui.patterns.js) вначале извлекает содержимое тега <body> (что дает нам возможность передавать в html-файлах шаблона только этот тег (если не требуется подключение JS или CSS)); затем строит DOM-дерево шаблона и все дальнейшие манипуляции производит только с DOM-деревом. Такой режим работы как минимум на 10-15% (по моим наблюдениям) медленнее, если бы использовались регулярные выражения для манипулирования содержимым innerHTML. Однако я старался максимально сбалансировать производительность и «оттянуть» момент начала работы с DOM до последнего.

И все же для задач, где пусть и есть вложенность шаблонов, но нет необходимости ни в модели, ни в DOM-карте собранного шаблона, ни в контроллере, я сделал шаблонизатор-light – flex.ui.templates.js. Отличие как раз в подходе, если flex.ui.patterns.js преимущественно работает с DOM деревом, то flex.ui.templates.js максимально долго «возится» с регулярными выражениями, подменяя hooks в innerHTML и «собирая» DOM-дерево только в последний момент.

Вот такой контроллер(ы) шаблонов у меня получился. Но, идем дальше.

А при чем здесь JQ?


Действительно в самом начале я упомянул о библиотеке JQuery. У меня к ней очень смешенные чувства. С одной стороны, она невероятно удобна. С другой, я иногда сталкиваюсь с таким кодом (написанным в стиле JQ), с такими цепочками, что хочется застрелиться, предварительно застрелив разработчика.

Конечно, идеального кода не бывает, но, как мне кажется JQ сильно расслабляет что ли. Не знаю. В общем по возможности я стремлюсь от JQ отказываться и не применять в проектах.

Однако, такая клевая штука как упомянутые цепочки вызовов мне нравится, но не так как это реализовано у JQ. Мне нравится сама концепция, что есть некая функция-обертка, которая проверят или преобразует входной объект, а дальше предоставляет возможность вызова тех или иных методов, которые будут к нему применены.

Посему у себя я определил пять типов входных объектов:
  • _node – один узел;
  • _nodes – множество узлов;
  • _array – массив;
  • _object – объект;
  • _string – строка.


Это глобальные «обертки». Их можно вызвать из любого места кода и делать что-то вроде этого:

_nodes('.buttons').events().add('click', function (event) {
    //Do something
});

var pos = _node('#this_button').html().position().byPage();

_object(some_object).forEach(function (key, value) {
    //Do something
});


Для меня был очень важен момент группировки. Как вы можете видеть, функция определения позиции узла на странице находится в группе [html.position], а функция добавления обработчика событий в группе [events]. На мой субъективный взгляд это делает код более ясным, потому как та же позиция может быть определена и как [byPage()], и как [byWindow()].

При чем добавление нового функционала дело весьма простое и не затейливое.

Например, сделаем два метода: сокрытие и показ узла.

flex.callers.define.node('html.hide', function () {
    if (this.target) {
        this.target.__previous_display = this.target.style.display;
        this.target.style.display = 'none';
    }
});
flex.callers.define.node('html.show', function () {
    if (this.target) {
        this.target.style.display = this.target.__previous_display !== void 0 ? this.target.__previous_display : '';
        delete this.target.__previous_display;
    }
});


И теперь мы можем пользоваться вновь созданными методами.

_node('.buttons').html().hide();
_node('.buttons').html().show();


***


Ну вот. Теперь кажется все. Очень надеюсь, что вы не уснули, а если и уснули, то выспались (что для людей нашей профессии – праздник).

Важно понимать, что все описанное выше разрабатывалось «под себя», поэтому нет какой бы то не было приемлемой документации и описания API, хотя на github есть файлы, создающие необходимую подсветку кода (intellisense) для Visual Studio. Именно из-за них, засранцев, я до сих пор не заставил себя описать API – ведь все подсвечивается.

Кроме того, наверняка в проекте много багов и мест для оптимизации, ведь используется он весьма «местечково», что просто не может покрыть множество способов применения, где могли бы быть выявлены какие-то недоработки. Есть и нерешенные проблемы. Например, при создании шаблона таблиц, все hook’и, внутри <table> следует определять как html-комментарии — <!--{{rows}}-->.

Очень волнуюсь в преддверии вашей реакции, но постараюсь держать себя в руках. В любом случае, спасибо за ваше внимание.

P.S.
Несколько ссылок:


P.P.S.
Забыл, есть неполная wiki на плохом английском

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


  1. MetaDone
    20.04.2016 16:42
    -5

    я конечно не фронтэндщик, но чем не мил Вам Angular?


    1. AlexWriter
      20.04.2016 20:04

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


  1. dom1n1k
    20.04.2016 17:17

    Автор в прошлом писал на C++?


    1. k12th
      20.04.2016 18:18
      +2

      На чем бы автор не писал, он был занят этим последние года 3-4 минимум.


      1. AlexWriter
        20.04.2016 20:00

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


    1. AlexWriter
      20.04.2016 19:59

      Нет, я ее босюсь. И в сфере web-разработки всего лишь с 2011 года. История моих постов как раз с того момента )


  1. Per_Ardua
    20.04.2016 20:00
    +1

    Крутяк) Тоже когда-то задумывался о кешировании шаблонов handlebars, но так руки и не дошли до создания своего велика. Спасибо, статья была интересной)


    1. AlexWriter
      20.04.2016 20:01

      Спасибо )


      1. Carduelis
        21.04.2016 00:36

        Не могли бы вы подсказать подводные камни кеширования шаблонов (у меня lodash из backbone) в localStorage? Пока пришло в голову подсчет md5 на сервере и подсчет его на клиенте. О прочтении header'ов что-то не додумался. Может быть что подскажете? что было трудным?
        Спасибо.


        1. AlexWriter
          21.04.2016 01:44

          Спасибо за комментарий. И вопрос.

          Если честно, на счет «трудного» не знаю. Как только реализовано – уже и не трудное, вроде.

          Ну вот был момент, с которого «нахлебался». Разного рода артефакты встречались. «Достаешь» из localStorage что-то, а там что-то, чего быть не должно (какой-нибудь левый символ). Я не изучал что это было и не смог бы изучить, так как было это 2-3 года назад, когда опыта у меня было меньше. Но вот еще тогда принял решение ничего в первозданном виде в localStorage не хранить – все преобразуется в base64String.

          Что еще? Вот есть до сих пор не разрешенная проблема – сброс. До сих пор ломаю голову как его реализовать корректно. Суть проста. В проекте используется модули A, B, С и D. Спустя какое-то время их взяли и переименовали на _A, _B, _С и _D. С точки зрения контроллера кэша – это новые ресурсы, так что в localStorage мы получим не 4-е модуля, а 8-ь, что не хорошо. Пока проблема решена через параметр при подключении flex.core.js?v=xxx, где xxx – произвольное число (версия). Если оно не совпадет с тем, что был ранее зафиксирован на клиенте (или не был зафиксирован вовсе), то будет выполнено localStorage.clear(). Кстати по этой же причине я отказался от кэширования картинок, что ранее задумывалось в рамках хотя бы иконок. Лучше браузера это пока никто не сделает :)

          Вот другая сложность. Например, не все JS получится получить через xmlhttprequest – origin policy может быть настроена, что никак.

          Есть еще проблема куда хуже, чем те что я перечислил уже. И тоже связана с JS. Это плохой код. Например, вот забыли вы поставить где-то банально «;». В 99 случаях из 100 вы этого даже и не узнаете, потому как браузер такие вещи «подправляет» за нас (что мне лично очень не нравится). Но браузер это подправляет только если скрипт подключен обычным способом, а вот если вы из localStorage его достали и интегрировали через new Function(content), просто как пример, то вас может ожидать сюрприз – throw какого-нибудь исключения, потому как в данном случае браузер уже ничего не «подправляет» за нас.

          Или у вас есть old-school модуль, который требуется включить в проект. И этот вот модуль использует глобальные переменные. И объявляет их не как window[‘my_global_var’], а вот так var my_global_var. В результате такой модуль не будет работать корректно, потому как его интеграция будет проводится через new Function(content), то есть переменная my_global_var станет не глобальной к window, а глобальной в контексте функции.

          Еще сложно «ловить» окончание загрузки CSS файлов. Это вам лучше посмотреть в коде flex.core.js, начиная со строки 4140. Сама проблема на 4178.

          Если же говорить непосредственно о шаблонах, а не о ресурсах вроде JS и CSS, то здесь проблем не было. Были и есть проблемы со сборкой, как уже упомянутый тег table. Дело в том, что браузер не дает вам поместить, например, div в table (оно и верно), но это вас ограничивает, так как вы не можете создать временные «обертки» при сборке. В результате приходится проверять «совместимость» тегов, что несколько усложняет логику.

          Надеюсь я в верном русле понял вопрос и с пользой ответил )

          П.С.
          В целом, задача сохранения ресурсов (JS и CSS) в localStorage может быть решена безусловно, но выставляет требования к качеству самих ресурсов, особенно JS.



          1. faiwer
            21.04.2016 09:08

            Или у вас есть old-school модуль, который требуется включить в проект.

            Решается примерно так: eval.apply(window, ...) либо with(window){ code }.


            1. AlexWriter
              21.04.2016 12:04

              Спасибо за ваш комментарий.

              Я просто отвечал на вопрос о встреченных трудностях, ну и перечислял те что встретил. eval я не использую, создав функцию через module = new Function(js_txt), я просто делаю module.call(window), что тоже самое с точки зрения результата.

              А интеграция модулей вообще идет и без function и без call, чтобы сохранить привычку браузера «править» наши рутинные ошибки. Вот здесь flex.core.js, строки 4400 — 4410 – то как интегрируются модули в систему.


              1. AlexWriter
                21.04.2016 12:16

                Хотя нет… eval.apply(window, ...) и module.call(window) далеко не тот же самый результат. faiwer, спасибо за наводку на мысль )


                1. faiwer
                  21.04.2016 12:31

                  Про eval.apply(window, ...) я узнал случайно, когда изучал исходники jQuery. В последствии они отказались от такого подхода в пользу чего-то вроде document.head.appendChild(script).


              1. faiwer
                21.04.2016 12:29

                eval я не использую, создав функцию через module = new Function(js_txt)

                Ну это как сказать, не используете. new Function это тот же самый eval, со всеми вытекающими. Всех нюансов не знаю, но не стоит обманываться ;)


  1. AlexWriter
    20.04.2016 20:06
    +2

    Спасибо за «звездочку» на github — со мной это в первый раз ))) Радуюсь, как ребенок )


  1. Akuma
    20.04.2016 20:34
    +1

    Как-то все замудрено у вас. Мне лично нравится использовать Webpack для этих целей. Там и компоновка и ленивая загрузка и пр.
    Подмечу дизайн промо-сайта. Просто, но круто. Мне прям очень нравится :)


    1. AlexWriter
      20.04.2016 22:30

      Спасибо


  1. Zdomb
    20.04.2016 23:31

    Говорят, через localStorage шрифты можно кешировать, а не только js и css.
    А вообще, хорошая работа!


    1. AlexWriter
      21.04.2016 01:49

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


  1. babylon
    21.04.2016 07:11

    Аlex не пробовали писать в реляционном стиле «node»:{«types»:[],«vars»:[],«data»:[]}. Входных объектов будет больше, но зато к элементам массивов можно применять some, map и т.д. Ну как сейчас модно. Дело вкуса конечно.


    1. AlexWriter
      21.04.2016 12:22

      Пока не пробовал реализовать, но мысли такие есть )


  1. faiwer
    21.04.2016 09:15
    +2

    Честно говоря, читая ваши терзания про localStorage, ручное управление кешем и пр., меня не покидает ощущение, что если организовать всё иначе, то будет проще и надёжнее. Скажем я делаю так:


    • Собираю JS-файлы в группы по смыслу и группирую по бандлам (для production-а).
    • Все ресурсы, которые могут быть обновлены со временем гружу по /etag/public... ссылке. Т.е. как только ресурс будет обновлён, он будет грузиться по новому URI, и, таким образом проблема с инвалидацией кеша решена.
    • Все подобные ресурсы кешируются браузером, скажем, на месяц-два.

    Итого, без localStorage, bse64, eval-а, используя только нативные базовые возможности и никаких хаков, пользователь при повторной загрузке получает ответ 200 от собственного кеша, не обращаясь к серверу (т.е. без 304). При первой же загрузке, вместо нескольких десятков файлов грузит 1-2 бандла. Это даёт некий оверхед по трафику (копеечный), зато большой буст к скорости загрузки, что, имхо, куда важнее.


    Плюс, ещё не понравилось, то, что вы назвали едиными файлами конфигурации… Столько возни на ровном месте, и всё ради чего? Вместо тех же групп можно использовать директории и относительные пути. Значительно меньше телодвижений же. А от дубляжа зависимость-использование вы вроде бы не избавились.


    Может быть я что-нибудь недопонял, sorry.


    1. faiwer
      21.04.2016 09:21

      По поводу раздела JQ: возьмите knockoutJS или какую-нибудь альтернативу ему. И забудете про DOM и ручную работу с ним. Экономит время с чудовищной силой. Т.е. никаких _node и .events().add не потребуется :) 90% такого грязного кода будет решать либа, а вы будете писать только… гхм, viewModel-и, которые куда ближе к устной логике, которой вы бы могли описать задачу, чем возня с DOM-ом.


      Спустя годик-два использования angularJS, knockoutJS или какой-нибудь другой реактивной либы вы сможете делать интерфейсы поразительной мощи и сложности за весьма краткое время. То что вы бы ранее отбросили просто потому, что "да на это год уйдёт", будете приниматься клепать с энтузиазмом, потому что теперь хватит и месяца :D


      1. AlexWriter
        21.04.2016 11:48
        +1

        Спасибо за комментарий.

        Знаете, меня столько раз это стопорило «Все уже придумано, написано – бери и используй!». Вот упрусь в какую-нибудь дилемму и сразу подобные мысли в голову лезут.

        А потом вспоминаю, что ключевая мотивация у меня – это вопрос «а как это работает вообще?» и возвращаюсь к разработке ). Собственно, большую часть того, что можно делать в JS я узнал не из рабочих проектов (основная работа), а в процессе изобретения этого велика. Поэтому личный profit в виде опыта уже есть )

        Что же касается AngularJS или же knockoutJS, или чего-то еще, то тут дело в том, что нет «конфликта интересов» — у меня просто нет такого выбора: использовать мои решения или AngularJS или же knockoutJS. Если заказчик говорит – делай что хочешь, я вежливо его спрашиваю – не против ли он стать «полигоном» для моих собственных наработок (если я понимаю, что оно не навредит). Не против? Чудно, беру ответственность на себя. Если решение должно быть на чем-то другом – не вопрос.


        1. faiwer
          21.04.2016 11:56

          Нет, я не против велосипедов. Сам навелосипедил уже довольно много. Ну так навелосипедьте себе свой KnockoutJS, в рамках программы "хочу разобраться". Предварительно изучив и попробовав сущ-ие решения, ибо так будет быстрее и конечный продукт будет лучше. Суть в том, что руками ковыряться в DOM-е это муторно, долго и очень дорого.


          1. AlexWriter
            21.04.2016 12:06

            Ну так у меня все еще впереди )


    1. AlexWriter
      21.04.2016 11:54

      Вы все верно поняли ) И предложенные вами решения верны со всех точек зрения и мало того применяются в том числе мною. Не в этом ли вся прелесть нашей профессии – во множестве решений одних и тех же задач? )

      Тут большую роль играет личная мотивация. Посмотрите мой ответ вам чуть ниже выше ).


      1. faiwer
        21.04.2016 12:01

        В таком случае рекомендую следующий велосипед организовать уже через браузерное кеширование, е-tag-и, при необходимости 304, сборку в бандлы. Правда, если честно, современный frontend даже без велосипедов уже слишком сложный. Ынтерпрайз пришёл и к нам. Даже DI используют.


  1. zxcabs
    21.04.2016 11:02
    +2

    В поносящему больших проектах ручное управление зависимостями приведет к аду, это очень плохое решение.


    1. AlexWriter
      21.04.2016 12:07

      Поясните, пожалуйста, что вы имеете ввиду под «ручным» управлением зависимостей? Спасибо.


      1. zxcabs
        21.04.2016 14:16
        +2

        То что вы делаете в конфиге описывая руками какие файлы откуда тянуть это и есть ручное управление зависимостями. В больших проектах таки конфиги вырастают до неприличных 3к-5к тысяч строк и управлять всем этим становится не просто тяжело, а практически невозможно. Самый правильный и верный способ это описывать зависимости в коде и точка, не нравится amd паттерн который в requirejs, пишите commonjs модули, которые являются стандартом в мире nodejs. Не устраивает commonjs? Используйте новый стандарт Systemjs, но не выносите зависимости из кода иначе потом так и хлебнете что отмываться устанете.


        1. AlexWriter
          21.04.2016 14:47

          Спасибо.

          Таких больших проектов просто не встречал в практике и, честно сказать, не думал о них вовсе, работая над своим великом. Наиболее крупный, что был у меня – это где-то около 200 модулей (только JS файлы), где хоть и не использовались мои решения, но все же был единый регистр всех модулей, вынесенный в один файл. Было порядка 500 срок с описанием всяких особенностей и это было довольно удобно.

          Может быть существует некий предел, после которого архитектура и применяемые решения должны как-то учитывать масштаб системы. Конечно, для проекта, где описание модулей занимает 4 – 5 тыс. строк что-то должно быть сделано иначе.

          И еще, просто чтобы убедиться, что мы говорим об одном и том же. Под модулем я понимаю модуль без учета ресурсов. То есть модуль может запрашивать еще и какие-то ресурсы (CSS к примеру), которые не в какие регистры не вносятся, а определяются в рамках объявления модуля.

          Я видел регистр всех ресурсов для большого проекта. Да, там было, если не ошибаюсь, порядка 6 тыс. строк. Но убери оттуда все CSS и вспомогательные ресурсы (вроде сторонних библиотек JS) и количество строк сократится кратно (именно кратно, потому что многие вспомогательные ресурсы объявляются многократно для указания зависимостей одного от другого).

          flex.register.modules.js задумывался не как перечень всего и вся, а как место, где определяются «ключевые» (не знаю какое слово подобрать) модули. И никаких зависимостей, никаких ресурсов, ничего кроме просто путей к модулям в этом регистре не объявляется. Все ресурсы объявляются только на уровне объявления модуля (то есть в файле самого модуля). И с этой точки зрения для проекта с количеством модулей, скажем меньше 100, наличие единого регистра мне видится больше в позитивном свете, нежели, чем в негативном.

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


  1. vvscode
    21.04.2016 12:07
    +1

    Вы смотрели секцию Plugins ( requirejs.org/docs/plugins.html#apiload ) в документации к Require.js?


    1. AlexWriter
      21.04.2016 14:48

      Честно — не погружался глубоко.


      1. vvscode
        22.04.2016 11:41
        +1

        Стоило бы
        внешний вид — фактически решается плагинами для сборщика
        система кеширования — решается этими самыми плагинами