Заметки с тегом «pattern»

Компилируемые шаблоны и способы их хранения

Я уже писал в одной из заметок о шаблонах на JS. В этот раз я рассмотрю диаметральный подход – компилируемые шаблоны. Примерами таких шаблонов могут служить mustache.js, Underscore template и многие другие библиотеки.

В их основе лежит один и тот же принцип: конструктору передается исходный шаблон в виде строки текста; на выходе получаем некую функцию. Эта функция, в свою очередь, возвращает строку, в которой вместо размеченных особым образом фрагментов будут подставляться аргументы, использованные при вызове. Так же в шаблонах может быть реализована простая логика ветвления, циклы или вызов вспомогательных функций.

Интересным аспектом применения компилируемых шаблонов является то, где и как будет храниться исходный текст шаблона и скомпилированный шаблон. На ум приходят несколько вариантов:

  • в виде строки в js-файле;
  • внутри тега <script> с произвольным типом в html;
  • в отдельном файле;
  • компилирование исходного текста шаблона при сборке проекта.

Хранение в js-файле

var templateSource = "<div class=\"welcome\">" +
    '<p class="welcome__message">Hello: <%= name %></p>' +
    '</div>';

var template = _.template(templateSource);

Плюсы:

  • исходный текст шаблонов конкретного модуля сразу доступен для компиляции в момент инициализации этого модуля;
  • с большой вероятностью файл, содержащий шаблон, будет закеширован браузером, что в общем счёте сэкономит трафик при регулярном посещении страницы.

Минусы:

  • текст шаблона перемежается с логикой;
  • содержимое строки в JS нужно правильно экранировать;
  • для разных ролей пользователей могут понадобиться разные шаблоны, а это, в свою очередь, может скомпрометировать какую-то часть функциональности пользователей с высоким уровнем доступа.

Хранение в исходном HTML-коде страницы

<script id="template-welcome" type="text/x-template">
    <div class="welcome">
        <p class="welcome__message">Hello: <%= name %></p>
    </div>
</script>

Браузер содержимое тега <script> считает простым текстом, а так как в атрибуте type у него указан неизвестный ему MIME-тип, то интерпретировать или отображать он его не станет. Зато содержимое этого тега можно получить после загрузки документа, обратившись к нему по id.

var templateSource = document.getElementById("template-welcome").innerText;

Плюсы:

  • размещается вместе с другой разметкой;
  • набор шаблонов или их содержимое может динамически меняться в зависимости от внешних факторов.

Минусы:

  • если страница не будет закеширована браузером, то эти шаблоны будут каждый раз загружаться с сервера;
  • возможно, потребуется подождать окончания загрузки страницы, прежде чем к элементу, содержащему исходный текст шаблона, можно будет получить доступ.

Хранение в отдельном файле

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

<?xml version="1.0" encoding="UTF-8"?>
<templates>
    <template id="template-welcome"><![CDATA[
        <div class="welcome">
            <p class="welcome__message">Hello: <%= name %></p>
        </div>
    ]]></template>
    <template id="template-product"><![CDATA[
        …
    ]]></template>
</templates>

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

$.ajax("/static/templates/welcome.xml")
    .done(function (templates) {
        var templateEle = templates.getElementById("template-welcome");
        if (templateEle) {
            callback(_.template(templateEle.innerText));
        }
        templateEle = null;
    });

Набор шаблонов загружается с помощью XHR, а затем методом getElementById() получаем нужный объект, содержащий исходный текст шаблона.

Плюсы:

  • с большой вероятностью статический файл, подгружаемый через XHR, будет закеширован браузером, и в дальнейшем некоторое время не будет запрашиваться с сервера;
  • можно организовать дополнительный уровень кеширования шаблонов (Local Storage, например);
  • наборы шаблонов можно формировать для конкретной комбинации ролей пользователя, что позволит защитить функциональность недоступную текущему пользователю.

Минусы

  • дополнительный HTTP-запрос при первом посещении;
  • инициализация модуля будет «отложенной» из-за асинхронной загрузки требуемых шаблонов.

Компилирование при сборке

В твиттере и комментариях мне подсказали ещё один способ хранения шаблонов. Каждый шаблон хранится в виде HTML-файла, а на этапе сборки эти файлы компилируются в js-функции и сериализуются прямо на сервере. В итоге клиент будет загружать обычные статические js-файлы.

Плюсы:

  • шаблоны будут кешироваться браузером как любые другие статические ресурсы;
  • браузер не тратит время на компиляцию шаблона.

Минусы:

  • для работы нужен процесс сборки проекта;
  • для компилирования шаблонов при сборке потребуется соответствующая технология (Node.js или нативная реализация на соответствующем языке программирования), что может быть накладно.

Заключение

Метод хранения сложных шаблонов в отдельных статических файлах мне кажется достаточно перспективным по сравнению с хранением тех же данных в HTML. С другой стороны, инициализация модуля оказывается гораздо сложнее, чем в случае с хранением шаблона в JS. Компилирование шаблона при сборке может стать идеальным вариантом, если в проекте есть сборка как таковая.

Комментарии к заметке: 6

Очередь с одновременным выполнением нескольких задач

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

Модуль использует Backbone.Events для оповещения о ходе выполнения задач и несколько полезных функций из Underscore. Экспорт объекта выполняется через мой фреймворк.

/**
 * Queue for simultaneous task execution.
 * Execution method MUST return the promise object.
 *
 * @param limit {Integer} number of simultaneous tasks
 * @event schedule
 * @event before
 * @event after
 */
(function () {

    var Task = function (obj, execMethod) {
        _.extend(this, {
            id: _.uniqueId("queueitem-"),
            obj: obj,
            execMethod: execMethod,
            active: false
        });
    };

    _.extend(Task.prototype, {
        run: function () {
            var func, value;

            this.active = true;

            func = this.obj[this.execMethod];
            if (_.isFunction(func)) {
                value = func.call(this.obj);
            }
            // return promise object
            return value;
        }
    });

    function runTasks() {
        var activeTasks = _.filter(queue, function (task) {
            return task.active;
        });

        if (queue.length > 0 && activeTasks.length < limit) {
            // we can run another task
            var candidate = _.find(queue, function (task) {
                return !task.active;
            });

            if (candidate) {
                Q.trigger("before", candidate.obj);
                var taskDfd = candidate.run();
                Q.trigger("after", candidate.obj, taskDfd);
                if (taskDfd) {
                    taskDfd.always(function () {
                        var i, id = candidate.id;
                        for (i = 0; i < queue.length; i++) {
                            if (queue[i].id === id) {
                                queue.splice(i, 1);
                                break;
                            }
                        }
                        runTasks();
                    });
                }
                // check tasks one more time
                setTimeout(runTasks, 500);
            }
        }
    }

    var queue, limit;

    var Q = _.extend({
        init: function (opts) {
            queue = [];
            limit = opts.limit;
        },
        schedule: function (obj, execMethod) {
            var task = new Task(obj, execMethod);
            if (queue) {
                queue.push(task);
                Q.trigger("schedule", obj);
                runTasks();
            }
        }
    }, Backbone.Events);

    App.namespace("App.Queue", Q);

}());

Очередь конфигурируется с помощью одного обязательного параметра limit, который задает количество одновременно выполняемых задач.

App.Queue.init({limit: 5});

var message, i;
for (i = 0; i < 20; i += 1) {
    message = new Message("text " + i);
    App.Queue.schedule(message, "delivery");
}

В этом примере будет сгенерировано 20 объектов сообщений, у которых есть метод delivery, реализующий всю работу по доставке этого сообщения. Они все будут сразу поставлены в очередь, но одновременно отправляться смогут только 5 сообщений.

После добавления задания в очередь генерируется событие schedule, а до и после запуска задачи — before и after соответственно. В событии after в качестве параметра передается promise-объект задачи. Через него можно так же отслеживать окончание её выполнения в другом модуле.

App.Queue.on("after", function (messagePromise) {
    messagePromise.done(function () {
        console.log("message " + this.get("id") + " was delivered");
    });
    messagePromise.fail(function () {
        console.log("delivering message " + this.get("id") + " was failed");
    });
});

Контекстом во всех событиях выступает объект, который ставился в очередь.

Оставте свой комментарий

Дизайн приложения с применением Deferred Object

В заметке Deferred Object я уже приводил пример приложения, построенного из отдельных компонент и связанных между собой с помощью отложенных объектов. Сейчас я еще раз хочу акцентироваться на некоторых аспектах.

Promise object

Состояние отложенного объекта должен изменять только тот, кто его создал.

Ранее я писал:

Так как в общем случае deferred object предполагает двустороннюю коммуникацию, то ничего не мешает отменить выбор извне.

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

Все остальные компоненты, заинтересованные в изменении состояния, могут только подписываться на это изменение. Т.е. замыкание, в котором создается отложенный объект, должно возвращать promise object.

В том примере нужно исправить функцию buildSelect.

function buildSelect() {
    var select = $.Deferred();

    ...

    // возвращаем отложенный объект селектора
    return select.promise();
}

В метод promise() можно передать в качестве параметра другой объект, который будет расширен методами promise-объекта.

function createJob() {
    var dfd = $.Deferred(), value = 0, t, me;

    function schedule() {
        return setTimeout(updateValue, 500);
    }

    function updateValue() {
        value = value + 10 * Math.random();

        dfd.notifyWith(me, [value > 100 ? 100 : value]);

        if (value >= 100) {
            if (value - 100 > 5) {
                dfd.resolveWith(me);
            } else {
                dfd.rejectWith(me);
            }
            t = 0;
        } else {
            t = schedule();
        }
    }

    me = dfd.promise({
        start: function () {
            if (!t) {
                t = schedule();
            }
        },
        stop: function () {
            if (t) {
                clearTimeout(t);
                t = 0;
            }
        }
    });

    return me;
}

$(function () {

    var job = createJob();

    job.done(function () {
        alert("done");
    }).fail(function () {
        alert("fail");
    }).progress(function (value) {
        $(".progress-gauge").width(value + "%");
    }).always(function () {
        $(".btn-start, .btn-stop").addClass("hidden");
    });

    $(".btn-start").on("click", function () {
        job.start();
    });

    $(".btn-stop").on("click", function () {
        job.stop();
    });

});

Объект job, помимо методов наблюдения за процессом выполнения и результатом завершения работы, имеет метод запуска задачи и метод остановки задачи. Вся логика расчета скрыта внутри одного замыкания, а работа с GUI сконцентрирована в другом.

Пример на jsfiddle.

Контекст

Отложенный объект имеет по паре методов для изменения состояния. Например, resolve и resolveWith, reject и rejectWith. Обе пары методов позволяют передать подписчикам какие-либо параметры. Однако методы с суффиксом with позволяют указать контекст вызова (это обязательный параметр методов), а без суффикса with вызывают подписчиков в контексте отложенного объекта.

Тут возникает некоторое противоречие в дизайне. Сначала мы скрываем от внешнего мира исходный отложенный объект, а потом сами же его и показываем. Лучшим всего вызывать подписчиков в контексте того объекта, который был создан в замыкании.

Инверсия управления

Чтобы грамотно применять отложенные объекты нужно понимать принцип инверсии управления. Мы не можем управлять состоянием отложенного объекта. Он сам вызывает наши функции, когда его состояние меняется. Нам лишь остается реализовать некие действия, связанные с изменением состояния.

Комментарии к заметке: 2

Оповещение модулей через jQuery.Callbacks

Обмен событиями между отдельными компонентами программы можно построить различными способами. В зависимости от потребностей это можно сделать:

  1. через интерфейс Observable или произвольные DOM-события;

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

  2. через глобальный объект;

    Экземпляры компонент ничего не знают друг о друге. Все события пересылаются через один и тот же объект.

  3. через объекты событий.

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

Интерфейс Observable удобен тем, что события одного экземпляра ни как не пересекаются с событиями другого экземпляра одной и той же компоненты.

$("div.primary div.widget").on("update", function () {
    alert("update in primary block");
}

$("div.secondary div.widget").on("update", function () {
    alert("update in secondary block");
}

Два экземпляра одного и того же виджета, но расположенных в разных блоках генерируют разные события «update».

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

Создается глобальный объект, имеющий методы регистрации функции обратного вызова для события и отмены регистрации, а так же метод вызова зарегистрированных колбеков.

var Topic = (function () {
    var topics = {};
    return function (name) {
        var callbacks, method, topic;
        if (!name) {
            throw new Error("topic must be specified");
        }
        topic = topics[name];
        if (!topic) {
            callbacks = jQuery.Callbacks();
            topic = {
                publish: callbacks.fire,
                subscribe: callbacks.add,
                unsubscribe: callbacks.remove
            };
            topics[name] = topic;
        }
        return topic;
    };
}());

Замыкание содержит в себе список всех зарегистрированных событий. Для каждого из них создается менеджер очереди колбеков jQuery.Callbacks, который регистрирует и вызывает их.

Наружу возвращается объект с методами экземпляра jQuery.Callbacks, но именованными в соответствии с действиями, за которые они отвечают: subscribe, unsubscribe и publish.

Topic("archiveAdd").subscribe(function (id) {
    console.log("add file with id = " + id + " to archive");
});
Topic("archiveCreate").subscribe(function () {
    console.log("start archiving");
});

И в другой компоненте:

$(root).on("click", "li.file button.add", function () {
    Topic("archiveAdd").publish($(this).data("fileId"));
});
$(root).on("click", "button.create", function () {
    Topic("archiveCreate").publish();
});

В некоторых случаях может быть полезным для каждого события создать свой отдельный объект и использовать ссылку на него в компонентах

var EventObject = function () {
    var callbacks = jQuery.Callbacks();
    return {
        publish: callbacks.fire,
        subscribe: callbacks.add,
        unsubscribe: callbacks.remove
    };
};

Такая реализация может быть близка сторонникам процедурных языков, но мне кажется чужеродным для функционального стиля JavaScript.

Оставте свой комментарий

Очередь функций обратного вызова jQuery.Callbacks

В jQuery 1.7 появился новый объект для создания и управления очередью колбеков – jQuery.Callbacks. Он был предложен на рассмотрение команде разработчиков еще 6 месяцев назад. Менеджер было успешно опробован и пошел в релиз. На его основе был переписан jQuery.Deferred, который в свою очередь лежит в основе системы колбеков jQuery.ajax.

Конструктор jQuery.Callbacks можно вызывать как с оператором new, так и без него. В результате будет создан объект с несколькими методами: add(), remove(), fire(), disable() и др.

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

Вызов конструктора с различными флагами

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

var c = $.Callbacks();
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

После выполнения этого кода в консоль будет выведено:

1: foo

1: bar

2: bar

Метод add() добавляет в очередь новую функцию обратного вызова, а метод fire() по очереди вызывает все функции из очереди с указанными параметрами.

Флаг once указывает на то, что метод fire() будет выполнен только один раз для такой очереди.

var c = $.Callbacks("once");
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

В этом коде в консоль выведутся результаты работы только первой функции.

1: foo

Флаг memory предписывает запоминать последнее значение, с которым был вызван метод fire() и автоматически вызывать вновь добавляемые колбеки с этим значением.

var c = $.Callbacks("memory");
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

Когда вызывается метод fire("foo"), то второго колбека еще нет в очереди. Но как только он будет добавлен, так сразу вызовется с параметром "foo".

1: foo

2: foo

1: bar

2: bar

Флаги можно комбинировать. Так одновременное использование once и memory даст результат аналогичный по свой сути работе jQuery.Deferred – вызов колбеков будет сделан только один раз, а новые колбеки сразу будут вызываться при их добавлении.

var c = $.Callbacks("once memory");
c.add(function (val) {
    console.log("1: " + val);
});
c.fire("foo");
c.add(function (val) {
    console.log("2: " + val);
});
c.fire("bar");

1: foo

2: foo

Если будет установлен флаг unique, то одна и та же функция обратного вызова в очередь будет добавлена только один раз.

И последний флаг stopOnFalse прерывает выполнение очереди, если какой-либо колбек вернет значение false.

Применение

Благодаря этому менеджеру, например, в плагинах можно легко добавить поддержку не одного колбека, а сразу цепочки нескольких функций. Или же с помощью него можно уменьшить связанность различных компонент.

Комментарии к заметке: 1