Заметки с тегом «utility» :: Хранитель заметок

noteskeeper.ru

Персональный журнал для заметок Владимира Кузнецова

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

Оповещение модулей через 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.

Улучшенный namespace

Для определения модулей и констант приложения на JavaScript я люблю применять функцию аналогичную namespace из библиотеки YUI3. Она возвращает объект, который соответствует указанному пути или создает новый, если таковой не нашелся. Ценность этой функции в том, что можно не заботиться о том, что какая-то многоуровневая вложенность будет испорчена. Так же она позволяет определять поля в любом порядке.

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

(function (window, undefined) {

    var NS = "APP",
        app = window[NS] = window[NS] || {};

    /**
     * Get or create namespace for module
     * @param    ns        {String}    namespace
     * @param    origin    {Object}    initial object (optional)
     * @return             {Object}   
     */
    app.namespace = app["namespace"] || function (ns, origin) {
        var i, obj = window[NS], nsParts = ns.split('.');
        for (i = (nsParts[0] === NS) ? 1 : 0; i < nsParts.length; i++) {
            obj[nsParts[i]] = obj[nsParts[i]] || (i === nsParts.length - 1 && origin) || {};
            obj = obj[nsParts[i]];
        }
        return obj;
    };

}(window));

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

Её можно вызывать с одним строковым параметром

APP.namespace("APP.Module1");

или двумя параметрами — строковым и объектом, все поля которого будут перенесены в целевой namespace.

APP.namespace("APP.Module2", {
    method1: function () {
    },
    FIELD: "test"
});

Для названий можно использовать укороченный синтаксис и опускать базовое имя. Так вызовы APP.namespace("Module1") и APP.namespace("APP.Module1") будут эквивалентны.

Такую функцию удобно переносить из проекта в проект. Достаточно поменять только значение константы NS.

Обнаружение браузера Opera 9

Совсем недавно я начал применять web-шрифты в реальных проектах. Благодаря простым утилитам (например, @font-face Generator) можно быстро сконвертировать шрифт в нужные форматы. Но статья совсем не о том. Эти web-шрифты прекрасно (если не брать во внимание проблемы со сглаживанием) отображаются во всех браузерах (даже в IE6!!!) кроме Opera версии 9 и ниже. В связи с эти фактом я и озадачился методами обнаружения Opera 9.

Очевидным способом является проверка строчки User-Agent. Этот вариант решения проблемы хорош до тех пор, пока не сталкиваешься с подменой строки.

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

Так для браузеров Opera до 10 версии будет характерным отличительным признаком отсутствие события contextmenu у DOM-элемента.

function () {
  var isPresent = null;
  if (document.createElement) {
    var el = document.createElement("p");
    if (el && el.setAttribute) {
      el.setAttribute("oncontextmenu", "");
      isPresent = typeof el.oncontextmenu != "undefined";
    }
  }
  return isPresent;
}

Обнаружение такой не очевидной особенности гораздо эффективнее, например, по сравнению с обнаружением поддержки web-шрифтов потому, что шрифту придется загрузить по сети.

Так же, наверное, можно использовать какие-то CSS-хаки, специфичные для Opera, по аналогии с тем, как я делал определение IE6.

Событие окончания загрузки картинки

Чтобы отследить момент загрузки картинки одного обработчика события «load» бывает не достаточно. Если он будет прикреплен к картинке после того, как она уже была загружена, то событие больше не повторится и обработчик не будет вызван.

Поможет решить эту проблему вспомогательный плагин к jQuery.

;(function ($) {
    $.fn.bindImageLoad = function (callback) {
        function isImageLoaded(img) {
            // Во время события load IE и другие браузеры правильно
            // определяют состояние картинки через атрибут complete.
            // Исключение составляют Gecko-based браузеры.
            if (!img.complete) {
                return false;
            }
            // Тем не менее, у них есть два очень полезных свойства: naturalWidth и naturalHeight.
            // Они дают истинный размер изображения. Если какртинка еще не загрузилась,
            // то они должны быть равны нулю.
            if (typeof img.naturalWidth !== "undefined" && img.naturalWidth === 0) {
                return false;
            }
            // Картинка загружена.
            return true;
        }

        return this.each(function () {
            var ele = $(this);
            if (ele.is("img") && $.isFunction(callback)) {
                ele.one("load", callback);
                if (isImageLoaded(this)) {
                    ele.trigger("load");
                }
            }
        });
    };
})(jQuery);

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

Рассчитать ширину скролбара

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

  1. добавить два div в body и разместить их за пределами экрана;
  2. определить ширину внутреннего div;
  3. установить значение «scroll» свойства overflow у внешнего div;
  4. опять определить ширину внутреннего div;
  5. удалить оба div;
  6. вернуть разницу между измеренными ранее значениями ширины.

Для простоты манипуляции с документом используем jQuery

function scrollbarWidth() {
    var div = $('<div style="width:50px; height:50px; overflow:hidden; position:absolute; top:-200px; left:-200px;"><div style="height:100px;"></div></div>').appendTo('body');
    var w1 = $('div', div).innerWidth();
    div.css('overflow-y', 'scroll');
    var w2 = $('div', div).innerWidth();
    $(div).remove();
    return (w1 - w2);
}

Источник: http://jdsharp.us/jQuery/minute/calculate-scrollbar-width.php

Система оповещения модулей

Приятно наблюдать за тем как развиваются языки программирования, за появлением различных фреймворков и паттернов. Так в моем любимом JavaScript в последнее время появилась хорошая тенденция оформлять код в виде модулей (классов). Это обеспечивает и лучшую читаемость программ и повышает повторное использование кода.

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

/**
 *  Observable Object
 *  Just call it in constructor of your class (delegate pattern)
 *  Ex: var me = Observable(); // create new object
 *      or
 *      Observable(me);        // extend existing object
 */
var Observable = function (target) {
    // callback holder
    var observers = {};
    // create new object if needed
    target = target || {};

    target.attachObserver = function (eventType, callback) {
        if (!observers[eventType]) {
            observers[eventType] = [];
        }
        observers[eventType].push(callback);
        return this;
    };

    target.detachObserver = function (eventType, callback) {
        var a, i;
        if (eventType) {
            if (observers[eventType] && callback) {
                a = observers[eventType];
                for (i = 0; i < a.length; i++) {
                    if (a[i] === callback) {
                        a.splice(i, 1);
                        break;
                    }
                }
            } else {
                delete observers[eventType];
            }
        } else {
            observers = {};
        }
        return this;
    };

    target.notify = function () {
        var a, i, handler, data = [];
        // copy notification arguments
        Array.prototype.push.apply(data, arguments);
        a = observers[data.shift()];
        if (a && a.length > 0) {
            for (i = 0; i < a.length; i++) {
                handler = a[i];
                if (typeof handler === 'string') {
                    handler = this[handler];
                }
                if (handler instanceof Function) {
                    handler.apply(this, data);
                }
            }
        }
        return this;
    };

    return target;
};

Этот компонент при инициализации создает в указанном объекте 3 метода:

  • attachObserver — добавляет колбек для определенного события
  • detachObserver — удаляет один или все колбеки для определенного события
  • notify — инициализирует событие, при этом вызываются все зарегистрированные колбеки с указанными аргументами

Все указанные методы поддерживают цепочный вызов.

var FieldObject = function (root) {
    var me = Observable(this);

    $(root).append('<div class="field"><input type="text" value="" /></div>');
    $('div.field input', root).change(function () {
        me.notify('valueDidChange', this.value);
    });

    return me;
};

FieldObject($('body'))
    .attachObserver('valueDidChange', function (data) {
        alert(data);
    });

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

Привязка данных к объектам страницы

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

Для небольших виджетов я нашел оптимальным отделять этап создания представления и подключения контролера.

var obj = [], i;
obj.push('<div class="wrapper">');
for (i = 0; i < model.lenght; i++) {
    obj.push('<div class="item">');
    obj.push('<h1>', model[i].title, '</h1>');
    obj.push('<div class="input"><input type="text" value="', model[i].value, '" /></div>');
    obj.push('</div>');
}
obj.push('</div>');
$(root).html(obj.join(''));
$('div.input input', root).change(function () {
    // нужно сохранить значение в модель
});

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

(function () {
    var uidx = 1, storage = {};

    /**
     * Generate an id that is unique among application
     * @method guid
     * @param pre {String} optional guid prefix
     * @return {String} the guid
     */
    APP.guid = function (pre) {
        var p = (pre) || 'app';
        return p + '-' + uidx++;
    };

    /**
     * Make a save point for linking DOMNodes and Objects via IDs
     * @param obj {Object}
     * @param pre {String} optional guid prefix
     * @return {String} the guid
     */
    APP.savepoint = function (obj, pre) {
        var guid = APP.guid(pre);
        storage[guid] = obj;
        return guid;
    };

    /**
     * Recall data for save point
     * @param guid {String} id of save point
     * @return {Object} stored data
     */
    APP.recall = function (guid) {
        return (guid in storage) ? storage[guid] : null;
    };
})();

Теперь поле ввода я создаю с уникальным id.

obj.push('<div class="input"><input type="text" value="', model[i].value, '" id="', APP.savepoint(model[i]), '" /></div>');

А реализация обработчика выглядит не менее сложной.

function () {
    var o = APP.recall(this.id);
    o.value = this.value;
}

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

Динамическая загрузка JavaScript файлов

Техника динамической загрузки данных через XHR (в простонародии, AJAX) очень популярна в современном WEB-е. Но вот динамическую загрузку программных компонент используют пока крайне редко. Компактный модуль ScriptLoader наглядно демонстрирует то, как это может быть просто и функционально.

/**
 * JavaScript loader
 * @param modules {Array} массив имен js файлов
 */
var ScriptLoader = function (modules) {
    this.modules = modules.slice();
    this.queue = new Queue();
    this._init();
};

ScriptLoader.prototype = {
    run: function () {
        // test for header ready
        var head = document.getElementsByTagName("head");
        if (head.length > 0) {
            this.queue.iterate();
        } else {
            setTimeout(arguments.callee, 500);
        }
    },

    _init: function () {
        var i, me = this;
        for (i = 0; i < me.modules.length; i++) {
            (function (name) {
                me.queue.add(function () {
                    var head, script;
                    head = document.getElementsByTagName("head");
                    if (head.length > 0) {
                        head = head[0];
                        script = document.createElement("script");
                        script.src = name;
                        script.type = "text/javascript";
                        script.onload = script.onreadystatechange = function () {
                            if ((!this.readyState || this.readyState == "loaded" || this.readyState == "complete") ) {
                                me.queue.iterate();
                                script.onload = script.onreadystatechange = null;
                                head.removeChild(script);
                            }
                        };
                        head.appendChild(script);
                    }
                });
            })(me.modules[i]);
        }
    }
};

Обратите внимание, что в этом модуле используется Queue.

Пример использования.

var loader = new ScriptLoader(["file1.js", "file2.js", "http://server.org/js/framework.js"]);
// на этом этапе в очередь могут быть добавлены дополнительные вызовы
loader.queue.add(function () {
    Module1.init();
});
loader.run();

Очередь: синхронное выполнение функций

С использованием событийной (event-driven) модели программирования появляется множество асинхронных вызовов, которые не всегда должны выполняться асинхронно. Чтобы упорядочить этот хаос я написал небольшой модуль, реализующий очередь.

/**
 * @method  add       добавить функцию в очередь
 * @method  interate  выполнить первую в очереди функцию и удалить ее из очереди
 * @method  clear     очистить очередь
 */
var Queue = function () {
    this.members = [];
};
Queue.prototype = {
    add: function (f) {
        if (f instanceof Function) {
            this.members.push(f);
        }
    },
    iterate: function () {
        if (this.members.length > 0) {
            var func = this.members.shift();
            func.call(this);
        }
    },
    clear: function () {
        this.members = [];
    }
};

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

function () {
    // сохраняем контекст (очередь) в локальную переменную
    var queue = this;
    $.get('data.xml', function (data) {
        // в этом месте this уже не ссылается на очередь
        process(data);
        // подолжаем исполнение очереди
        queue.iterate();
    });
}