JS Загрузчик + шаблонизатор или история очередного велика

в 13:33, , рубрики: css, html, javascript, templates, шаблонизатор

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

Автор: AlexWriter

Источник

Поделиться новостью

* - обязательные к заполнению поля