ADS-ajaxj详解(2)-检测hash变化解决ajax后退按钮和书签问题
// 后退按钮和书签功能 /* 通过一些DOM脚本并利用URL的hash值可以补救后退按钮和标签的问题。 其中,hash值表示浏览器应该保持在当前页面上,但同时必须通过重新定位页面使得与hash匹配的命名的锚(或带有相同ID属性的元素),在浏览器的视口(view port)中可见。如果文档中不存在与hash匹配的项,则浏览器只会改变地址栏中的URL。这里的hash可以连同URL一起被作为标签使用,而且通过一些DOM脚本及hack技术,hash还可以在用户单击后退按钮时用于更新页面。 */ /** * 1.不那么简单的修复 * 修复后退按钮和变迁涉及监视和识别URL中hash值的变化,并通过该变化来调用Ajax请求。要做到这一点,需要创建一个检测地址中hash值变化的对象,同时以预先定义的适当的方法进行响应。这个对象需要做以下几件事: * (1)是永不唐突的DOM脚本增强文档以跟踪页面的变化。 * (2)允许注册根据不同hash值作为反映的不同方法。 * (3)监事地址栏的变化并调用注册过的适当方法。 * (4)在处理后退按钮和标签时,适应各种不同产品化浏览器的古怪行为。 * 每种浏览器处理地址的方式都有点不一样: * (1)在IE中,后退和前进按钮会忽略hash值,而只基于URL的其余部分进行导航。为了解决这个问题,需要使用一个隐藏的<iframe>并通过向GET字符串中添加hash来模拟导航到不同的网页。 * (2)在Safari中,当通过带hash的URL向前或向后导航时,浏览器的history及history.length会相应改变。但是,window.location.href的值会保持为通过后退和前进按钮导航之前,浏览器中打开的最后一个地址。考虑到安全原因,我们无法访问history中的URL,因此需要相对于Safari的history的长度保持跟踪访问过的hash值,并且能够从存储列表中找回适当的hash。 * (3)Firefox和Opera的行为则都更符合常理,他们会在使用后退和前进按钮通过hash来导航时,同步更新window对象的location值。 * * 2.针对产品功能的浏览器嗅探 * 在处理具体浏览器的产品功能时,浏览器嗅探是一种可以接受的方案,而且也是唯一可能的方案,因为产品的差异并不针对Javascript中的能力或对象。 * * 3.跟踪地质变化 */ /* 一个用来基于hash触发注册的方法的URL hash侦听器 */ var actionPager = { // 前一个hash lastHash: '', // 为hash模式注册的方法列表 callbacks: [], // Safari历史记录列表 safariHistory: false, // 对为IE准备的iframe的引用 msieHistory: false, // 应该被转换的链接的类名 ajaxifyClassName: '', // 应用程序的根目录。当创建hash时 // 它将是被清理后的URL ajaxifyRoot: '', /** * 初始化页面(pager)功能 * @param {String} ajaxifyClass 预定义的类名,只有符合该类名的链接才具备功能,默认为 ADSActionLink * @param {String} ajaxifyRoot 预定义的根路径 * @param {String} startingHash 预定义的hash值,默认为start * @example actionPager.init(); */ init: function (ajaxifyClass, ajaxifyRoot, startingHash) { this.ajaxifyClassName = ajaxifyClass || 'ADSActionLink'; this.ajaxifyRoot = ajaxifyRoot || ''; var ua = navigator.userAgent; if (/Safari/i.test(ua) && /Version\/3/.test(ua)) { this.safariHistory = []; } else if (/MSIE/i.test(ua) && (parseInt(ua.split('MSIE')[1]) < 8)) { // 如果是MSIE 6/7,添加一个iframe以便 // 跟踪重写(override)后退按钮 this.msieHistory = document.createElement('iframe'); this.msieHistory.setAttribute('id', 'msieHistory'); this.msieHistory.setAttribute('name', 'msieHistory'); this.msieHistory.setAttribute('src', 'fakepage'); setStyleById(this.msieHistory, { 'width': '100px', 'height': '100px', 'border': '1px solid black', 'display': 'none', 'z-index': -1 }); document.body.appendChild(this.msieHistory); this.msieHistory = frames.msieHistory; } // 将链接转换为Ajax链接 this.ajaxifyLinks(); // 取得当前的地址 var locat = this.getLocation(); // 检测地址中是否包含hash(来自书签) // 或者是否已经提供了hash if (!locat.hash && !startingHash) { startingHash = 'start'; } // 按照需要保存hash var ajaxHash = this.getHashFromURL(locat.hash) || startingHash; this.addBackButtonHash(ajaxHash); // 添加监视事件以观察地址栏中的变化 var watcherCallback = makeCallback(this.watchLocationForChange, this); if (/MSIE/i.test(ua) && (parseInt(ua.split('MSIE')[1]) < 8)) { window.setInterval(watcherCallback, 200); } else { addEvent(window,'hashchange',watcherCallback); } }, /** * 会自动将任何带标识的锚标签转换为页面(pager)可以识别的链接 */ ajaxifyLinks: function () { // 将链接转换为锚以便Ajax进行处理 var links = getElementsByClassName(this.ajaxifyClassName, document, 'a'); for (var i = 0, len = links.length; i < len; i++) { var curLink = links[i]; if (hasClassName(curLink, 'ADSActionPagerModified')) { continue; } // 将href属性转换为#value形式 curLink.setAttribute('href', this.convertURLToHash(curLink.getAttribute('href'))); addClassName(curLink, 'ADSActionPagerModified'); // 注册单击事件以便在必要时添加历史纪录 addEvent(curLink, 'click', function () { if (this.href && this.href.indexOf('#') > -1) { actionPager.addBackButtonHash(actionPager.getHashFromURL(this.href)); } }); } }, /** * 记录后退行为 * @param {String} ajaxHash hash值 * @return {Boolean} */ addBackButtonHash: function (ajaxHash) { // 保存hash if (!ajaxHash) { return false; } if (this.safariHistory) { // 为Safari使用特殊数组 if (this.safariHistory.length === 0) { this.safariHistory[window.history.length] = ajaxHash; } else { this.safariHistory[window.history.length + 1] = ajaxHash; } return true; } else if (this.msieHistory) { // 在MSIE中通过导航iframe this.msieHistory.document.execCommand('Stop'); this.msieHistory.location.href = 'fakepage?hash=' + ajaxHash + '&title=' + document.title; return true; } else { // 通过改变地址的值 // 使用makeCallback包装函数 // 以便在超时方法内部使this // 引用actionPager var timeoutCallback = makeCallback(function () { if (this.getHashFromURL(window.location.href + window.location.hash) !== ajaxHash) { location.replace(location.href + '#' + ajaxHash); } }, this); setTimeout(timeoutCallback, 200); return true; } return false; }, /** * 间隔地检测hash的变化,还可执行注册的侦听器 */ watchLocationForChange: function () { var newHash; // 取得新的hash值 if (this.safariHistory) { // 在Safari中从历史记录数组中取得 if (this.safariHistory[history.length]) { newHash = this.safariHistory[history.length]; } } else if (this.msieHistory) { // 在MSIE中从iframe的地址中取得 newHash = this.msieHistory.location.href.split('&')[0].split('=')[1]; } else if (location.hash !== '') { // 对其他浏览器从window.location中取得 newHash = this.getHashFromURL(window.location.href); } // 如果新hash与最后一次的hash不相同,则更新页面 if (newHash && this.lastHash !== newHash) { if (this.msieHistory && this.getHashFromURL(window.location.href) !== newHash) { // 修复MSIE中的地址栏 // 以便能适当地加上标签(或加入收藏夹) location.hash = newHash; } // 在发生异常的情况下使用try/catch // 结构尝试执行任何注册的侦听器 try { this.executeListeners(newHash); // 在通过处理程序添加任何 // 新链接的情况下进行更新 this.ajaxifyLinks(); } catch (ex) { // 这里将捕获到回调函数中的任何异常JS alert(ex); } // 将其保存为最后一个hash this.lastHash = newHash; } }, /** * 用于根据特殊的hash来注册侦听器 * @param {RegExp || String} regex 正则表达式 * @param {Function} method 执行的方法 * @param {Object || Element} context 执行环境,上下文 */ register: function (regex, method, context) { var obj = {'regex': regex}; context = context || window; // 一个已经指定的环境 obj.callback = function (matches) { method.apply(context, matches); }; // 将侦听器添加到回调函数数则中 this.callbacks.push(obj); }, /** * 把链接的URL地址转换为hash值 * @param {String} url * @return {*} */ convertURLToHash: function (url) { if (!url) { // 没有url,因而返回一个'#' return '#'; } else if (url.indexOf('#') > -1) { // 存在hash,因而返回它 return url.split('#')[1]; } else { // ie67会自动添加域名 // 如果URL中包含域名(MSIE)则去掉它 if (url.indexOf('://') > -1) { var locatH = window.location.href; locatH = locatH.substring(0, locatH.lastIndexOf('/')); var s = ''; var len = Math.min(locatH.length, url.length); for (var i = 0; i < len; i++) { if (locatH.charAt(i) !== url.charAt(i)) { break; } else { s += url.charAt(i); } } var reg = new RegExp(s + '/'); url = url.replace(reg, ''); //url = url.match(/:\/\/[^\/]+(.*)/)[1]; } // 按照init()约定去掉根目录 return '#' + url.substring(this.ajaxifyRoot.length); } }, /** * 从指定的URL中提取出hash值 * @param url * @return {*} */ getHashFromURL: function (url) { if (!url || url.indexOf('#') === -1) { return ''; } return url.split('#')[1]; }, /** * 获取当前URL地址 * @return {*} */ getLocation: function () { // 检查hash if (!window.location.hash) { // 没有则生成一个 var url = { domain: null, hash: null }; if (window.location.href.indexOf('#') > -1) { var parts = window.location.href.split('#')[1]; url.domain = parts[0]; url.hash = parts[1]; } else { url.domain = window.location; } return url; } return window.location; }, /** * 执行侦听器 * @param hash */ executeListeners: function (hash) { var matches; // 执行与hash匹配的任何侦听器 if (this.callbacks.length) { for (var i in this.callbacks) { if ((matches = hash.match(this.callbacks[i].regex))) { this.callbacks[i].callback(matches); } } } } }; window.ADS.actionPager = actionPager;