Back-button problem

При создании RIA возникает целый пласт различных проблем.
Самая, пожалуй, известная проблема – проблема «кнопки назад» (это я так перевел «back-button problem» в меру своих лингвистических способностей).

Хочу поделиться своим вариантом ее решения:

Сам принцип работы очень простой – есть объект, который занимается подгрузкой нужного контента в главный контейнер, назовем его PageFlow, у него есть общедоступный (если кто не понял, это я все еще упражняюсь в литературном переводе – как еще перевести public?!) метод loadPage, который выбирает, какой именно контент подгрузить, основываясь на параметре pageName. Есть также объект History, который отрабатывает первым на странице, и на основе части anchor URL-а вызывает loadPage метод, передавая ему location.hash. Также этот объект History запоминает переходы пользователя по anchor (я все о своем – якорь? нет?). Получается красивая схема – пользователь, кликнув на ссылку, скажем, с id’ом товара, вызывает объект History, который, в свою очередь, вызывает объект PageFlow, который уже подгружает нужный контент в страницу. При этом, если пользователь кликнет по кнопке «назад» или «вперед», все работает, как и ожидает этот самый пользователь.

Прежде, чем я начную хуячить кодом, условимся: мы используем jQuery в качестве фреймворка и разрабатываем магазин, у которого есть список всех товаров (anchor ‘all’) и странички с подробным описанием товара (anchor ‘id’). Погнали:

/**
 * History managment, for ajax-based pages
 * @class History
 * @constructor
 */
History = function () {
    var
    /**
     * @property currentHash
     * @private
     */
    currentHash,
    /**
     * @property _callback
     * @private
     */
    _callback,
 
    historyBackStack,
 
    historyForwardStack,
 
    isFirst,
 
    dontCheck,
 
    check = function () {
        var i, hash;
        if($.browser.msie) {
            // On IE, check for location.hash of iframe
            var ihistory = $("#APHistory")[0];
            var iframe = ihistory.contentDocument || ihistory.contentWindow.document;
            hash = iframe.location.hash;
            if(hash != currentHash) {
 
                location.hash = hash;
                currentHash = hash;
                _callback(hash.replace(/^#/, ''));
 
            }
        } else if ($.browser.safari) {
            if (dontCheck) {
                var historyDelta = history.length - historyBackStack.length;
 
                if (historyDelta) { // back or forward button has been pushed
                    isFirst = false;
                    if (historyDelta < 0) { // back button has been pushed
                        // move items to forward stack
                        for (i = 0; i < Math.abs(historyDelta); i++) {
                            historyForwardStack.unshift(historyBackStack.pop());
                        }
                    } else { // forward button has been pushed
                        // move items to back stack
                        for (i = 0; i < historyDelta; i++) {
                            historyBackStack.push(historyForwardStack.shift());
                        }
                    }
                    var cachedHash = historyBackStack[historyBackStack.length - 1];
                    if (cachedHash != undefined) {
                        currentHash = location.hash;
                        _callback(cachedHash);
                    }
                } else if (historyBackStack[historyBackStack.length - 1] == undefined && !isFirst) {
                    // back button has been pushed to beginning and URL already pointed to hash (e.g. a bookmark)
                    // document.URL doesn't change in Safari
                    if (document.URL.indexOf('#') >= 0) {
                        _callback(document.URL.split('#')[1]);
                    } else {
                        _callback('');
                    }
                    isFirst = true;
                }
            }
        } else {
            // otherwise, check for location.hash
            hash = location.hash;
            if(hash != currentHash) {
                currentHash = hash;
                _callback(hash.replace(/^#/, ''));
            }
        }
    };
 
    return {
        initialize : function (callback) {
            _callback = callback;
            currentHash = location.hash;
 
            if ($.browser.msie) {
                // To stop the callback firing twice during initilization if no hash present
                if (currentHash == '') {
                    currentHash = '#';
                }
 
                // add hidden iframe for IE
                $("body").prepend('');
                var iframe = $("#APHistory")[0].contentWindow.document;
                iframe.open();
                iframe.close();
                iframe.location.hash = currentHash;
            } else if ($.browser.safari) {
                // etablish back/forward stacks
 
                historyBackStack = [];
                historyBackStack.length = history.length;
                historyForwardStack = [];
                isFirst = true;
                dontCheck = false;
            }
            _callback(currentHash.replace(/^#/, ''));
            setInterval(check, 100);
        },
 
        add : function (hash) {
            // This makes the looping function do something
            historyBackStack.push(hash);
 
            historyForwardStack.length = 0; // clear forwardStack (true click occured)
            isFirst = true;
        },
 
        /**
         *
         * @param hash {String} desiring hash without first #
         */
        load: function(hash) {
            var newhash;
 
            if ($.browser.safari) {
                newhash = hash;
            } else {
                newhash = '#' + hash;
                location.hash = newhash;
            }
            currentHash = newhash;
 
            if ($.browser.msie) {
                var ihistory = $("#APHistory")[0]; // TODO: need contentDocument?
                var iframe = ihistory.contentWindow.document;
                iframe.open();
                iframe.close();
                iframe.location.hash = newhash;
                _callback(hash);
            }
            else if ($.browser.safari) {
                dontCheck = true;
                // Manually keep track of the history values for Safari
                this.add(hash);
 
                // Wait a while before allowing checking so that Safari has time to update the "history" object
                // correctly (otherwise the check loop would detect a false change in hash).
                var fn = function() {AP.History.setCheck(false);};
 
                window.setTimeout(fn, 200);
 
                _callback(hash);
                // N.B. "location.hash=" must be the last line of code for Safari as execution stops afterwards.
                //      By explicitly using the "location.hash" command (instead of using a variable set to "location.hash") the
                //      URL in the browser and the "history" object are both updated correctly.
                location.hash = newhash;
            }
            else {
              _callback(hash);
            }
        },
 
        /**
         * Set need we check, or not.
         * @param check {Boolean}
         * @protected
         */
        setCheck : function (check) {
            dontCheck = check;
        },
 
        /**
         * @method getCurrentHash
         * @return {String}
         */
        getCurrentHash : function () {
            return currentHash;
        }
    };
}();

PageFlow:

/**
 * Page Flow controller - load hash-specific data, show appropriate container and all that
 * @Class PageFlow
 */
var PageFlow = function () {
    /**
     * show whole list of goods
     * @method loadListOfGoods
     * @private
     */
    loadListOfGoods = function () {
        $('#goodsItemDetails').css('display', 'none');
        $('#listOfGoodsWorkArea').css('display', 'block');
    },
    /**
     * show detailed goods item view
     * @method loadGoodsItemDetails
     * @private
     */
    loadGoodsItemDetails = function (id) {
        var item = M.ListOfGoods.getGoodsItemById(id);
        if (item.pluralizedProfit.length == 0) {
            item.pluralizedProfit = item.pluralizedPrice;
        }
        // fill container with appropriate data
        M.Renderer.renderGoodsItemDetails([item]);
        // show goodsItemDetails
        $('#goodsItemDetails').css('display', 'block');
        $('#listOfGoodsWorkArea').css('display', 'none');
    },
 
    return {
        /**
         * decide what page to load
         * @method loadPage
         * @param pageName {String|Number} location.hash with stripped `#` sign
         * @public
         */
        loadPage : function (pageName) {
            if (L.isUndefined(pageName)) {
                if (M.ClientURI.isMainPage()) {
                    loadListOfGoods();
                }
            }
            // if pageName is number
            if (L.isNumber(pageName) || pageName.replace(/\d+/, '').length == 0) {
                // need to load goods item details page with provided id
                loadGoodsItemDetails(pageName);
            } else {
                switch (pageName) {
                    case 'all':
                        loadListOfGoods();
                        break;
                }
            }
        }
    };
}();

Ну а дальше все просто:

При загрузке страницы инициализируем объект History, в качестве callback передаем ему метод loadPage объекта PageFlow, а на все ссылки с rel == history навешиваем обработчик, который вместо перехода на якорь будет вызывать метод load объекта History:

$(function () {
    $('a @rel=[history]').click(function () {
        History.load(this.href.replace(/^.*#/, ''));
        return false;
    });
 
    History.initialize(PageFlow.loadPage);
});

Все файлы в заархивированном виде: BackButton Archive

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

Последние твиты