При создании 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