几年前,Ajax的兴起给互联网带来了新的生机,同时也使用户体验有了质的飞跃,用户无需刷新页面即可获取新的数据,而页面也以一种更具有交互性的形式为用户展现视图,可以说这种变化对互联网发展的贡献是史无前例的。
但随着Ajax大规模应用,越来越多的开发人员开始注意到其中存在的问题,因为Ajax的视图展现是在页面无刷新情况下进行的,这也就意味着在用户做了一系列操作之后,页面的URL是没有任何变化的,这些操作所产生的结果自然也就无法保留,当用户再次访问时,想要展现的数据也就无法重现。
举个例如来说,我们在一个摄影网站浏览一个相册,点击第一张图片会动态弹出一个大图的相框,可以更好地浏览图片,当看到一个自己喜欢的图片时,急于分享这张图片,于是把当前的URL发布出去了,但是我们的好友点击进去后发现,看到的只是原来的那个相册,并没有弹出的大图相框,更别说我们要分享的那张图片了,是不是很失望。也正是因为如此,这个页面无法被搜索引擎精确地抓取,SEO无法做到优化,用户的可访问性也大打折扣。
为了解决这个问题,开发者开始尝试使用URL中的hash来提高可访问性,部分浏览器开始提供hash相关的事件支持,有些知名站点也和搜索引擎约定使用某种规则来对站点页面进行抓取,但这毕竟不是一个标准的技术,还是不可避免的存在很多问题,开发者也期待出现一个标准化的完美解决方案,彻底解决这个难题。
幸运的是,HTML5中的History API给开发者带了新的希望,它很好的支持了基于URL的页面无刷新操作,也使得SEO优化得到完美的解决,毫无疑问,History API将会成为Web领域未来的标准,越来越多的开发者也将会使用它来开发自己的应用程序。
那么今天,我们就来讲解一下与History相关的技术。
为了能够更好的阐述这项新技术,我们选择使用一个示例程序来开始。如下图所示,我们展示三张小图,点击任意一张,将会弹出大的预览图,然后可以前后切换预览图片:
这个程序对于大家来说应该说是比较简单的,当点击小图时,将放置大图的页面元素显示出来,图片和简介显示出与小图对应的信息即可,切换预览图片的操作也是非常容易做到的。
这就是传统的Ajax操作,而这种操作存在一个极其严重的缺陷,我们无法记录当前浏览的图片信息,假如后面的小图有几百张,当我们浏览到某一张时,觉得非常不错,想把它发送给朋友,也有可能想自己存下来,这时候我们会发现,URL中没有任何这张图片的信息,复制地址栏也就没有任何效果了。
所以我们极为迫切的需要在URL中加入用户操作所产生的信息,这个时候History API就派上用场了,他提供的方法可以在页面不刷新的情况下对URL进行更新操作。下面就介绍一下主要的两个方法:
history.pushState(stateObj, title, url);
这个方法会往当前会话的历史栈中放入一条记录,stateObj是用户自定义的对象,用于记录一些有用的数据,title顾名思义就是标题信息,第三个参数是要放入的url信息。例如我们点击第二张图片时可以执行history.pushState({id: 2}, 'img: 2', '?preview=2');
history.replaceState(stateObj, title, url);
这个方法于pushState类似,唯一不同的是,执行这个操作后,浏览器会对会话历史栈内的记录进行替换而不是添加,这在特定场景下是比较适用的。
我们这里就来结合实例来讲解一下如何操作历史记录。
正如上图看到的那样,页面初始化时展现三张小图,点击任意一张小图时,会弹出预览图,页面的基本框架如下:
<!DOCTYPE html>
<html>
<head>
<title>History</title>
<link rel="stylesheet" type="text/css" href="css/main.css">
</head>
<body>
<ul id="gallery">
<li>
<img src="img/1.jpg" data-id="1"/>
</li>
<li>
<img src="img/2.jpg" data-id="2"/>
</li>
<li>
<img src="img/3.jpg" data-id="3"/>
</li>
</ul>
<div id="shadow-layer" class="for-dialog"></div>
<div id="preview-panel" class="for-dialog">
<img id="preview-img"/>
<div id="preview-info"></div>
<div id="preview-close" class="preview-button">Close</div>
<div id="preview-prev" class="preview-button"><</div>
<div id="preview-next" class="preview-button">></div>
</div>
</body>
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</html>
然后页面初始化时,我们需要为一些DOM元素绑定事件,就像下面这样:
//点击小图开始预览模式
$('#gallery img').click(function() {
preview(parseInt($(this).attr('data-id')));
});
//关闭预览视图
$('#preview-close').click(function() {
$('.for-dialog').hide();
window.history.back();
});
//点击预览上一张
$('#preview-prev').click(function() {
switchPrev();
});
//点击预览下一张
$('#preview-next').click(function() {
switchNext();
});
可以看到几个绑定的事件,点击小图会触发预览模式;然后在预览时点击关闭按钮,会隐藏预览框,同时调用了history的back函数,我们稍后会介绍;最后是两个切换按钮的事件,分别会切换到上一张或下一张大图。我们注意到,当小图被点击时,调用了一个叫preview的函数,这个函数正是我们重点要讲的,代码如下:
function preview(id, isSwitch) {
$('.for-dialog').show();
//获取对应的数据信息
var data = getPreviewDataById(id);
$('#preview-img').attr('src', data.img);
$('#preview-info').html(data.info);
//记录当前预览图片的ID,在前后切换时取用
$('#preview-panel').attr('data-prview-id', id);
var stateObj = {id: id};
var operation = isSwitch ? 'replaceState' : 'pushState';
//往历史记录栈中放入一条记录或者替换一条记录
window.history[operation](stateObj, 'img: ' + id, '?preview=' + id);
}
上面的preview函数主要负责几个重要的工作,首先显示预览框,然后根据数据ID获取数据信息,最后操作历史记录对象。在实际开发中,getPreviewDataById函数大多是以Ajax方式进行的,这个时候只需将后面的逻辑放到回调函数内部即可。在preview函数的最后,我们往历史记录里面放入或替换一条包含ID信息的对象,同时改变URL,放入还是替换,取决于是不是前后切换预览图,我们稍后会详细解释一下。注意,这个时候虽然URL改变了,但页面并不会刷新。下面我们就来对比一下点击前后的效果,以点击第二张小图为例:
可以明显看到,地址栏已经变化了,这其实就是我们调用pushState函数的结果:
window.history.pushState({id: 2}, 'img: 2', '?preview=2');
需要额外注意的一点是,第三个参数是新的URL地址,根据官方文档介绍,它可以是相对地址,也可以是绝对地址,所以我们也可以选择使用绝对地址,就像下面这样,效果是一样的:
window.history.pushState({id: 2}, 'img: 2', 'http://localhost/history/?preview=2');
在上面的preview函数参数列表中,存在一个isSwitch参数,如果函数被调用时指定这个参数,则会调用replaceState,而不是pushState,这是为什么呢,下面就来解释一下。
还记得我们我们为预览框右上角关闭按钮绑定的事件吗,除了隐藏预览图,还调用了history的back函数,其实我们是要利用这个函数从历史栈中弹出之前放入的历史记录,进而将URL恢复到预览前的状态。那么,如果我们在预览的时候频繁的切换到下一张或者上一张,历史记录栈中就会存在很多记录,要回到初始状态显然不易,我们也没有必要保留这么多的历史记录,所以当用户前后切换预览图时,只需要调用replaceState替换当前历史栈中那条记录即可,假如我们点击右边的切换按钮,应该是这样的:
window.history.replaceState({id: 3}, 'img: 3', '?preview=3');
为了调用replaceState函数,我们需要在执行preview函数时,传入一个isSwitch参数,我们来看看是如何调用的:
//预览下一张
function switchNext() {
//根据当前ID计算下一个ID
var currId = parseInt($('#preview-panel').attr('data-prview-id'));
currId++;
currId > 3 && (currId = 1);
preview(currId, true);
}
下面就以两个示意图来讲解一下切换前后的栈结构:
这样我们就能保证历史栈中只有一条记录,当关闭预览框结束预览时,可立即恢复到之前的视图。
而向前切换也是一样的,代码如下:
//预览上一张
function switchPrev() {
//根据当前ID计算前一个ID
var currId = parseInt($('#preview-panel').attr('data-prview-id'));
currId--;
currId < 1 && (currId = 3);
preview(currId, true);
}
到这里,我想大家都已经清楚如何操作历史栈来改变URL,那么我们不仅要问,如果使用当前的URL重新访问站点,如何还原关闭之前的视图呢,其实这个就属于基本的操作了,只需要在onload函数时得到相应的参数,然后调用preview函数即可,如代码所示:
//从URL中获取到当前预览图片的ID
function getCurrPreviewId() {
var search = location.search;
if (!search) return -1;
var params = {};
var keyValues = search.substring(1).split('&');
keyValues.forEach(function(item) {
var parts = item.split('=');
params[parts[0]] = parts[1];
});
if (!params['preview']) return -1;
return params['preview'];
}
//当页面加载时,如果URL中有预览信息,取出然后
window.onload = function() {
var id = getCurrPreviewId();
if (id < 1 || id > 3) return;
preview(id);
};
到这里其实我们已经将整个过程讲解完成了,但还有一个重要的事件我们还没有涉及到,那就是window下面的onpopstate事件,当用户点击后退按钮或前进按钮时,或者当我们的程序执行history.back(),history.forward(),history.go()时,都会触发这个事件,它能够捕获这些操作完成之后,当前历史栈中的状态,我们可以利用这些信息,做进一步的操作。那么我们就来做个试验,看看onpopstate事件是如何被触发的:
window.onpopstate = function(e) {
console.log('e.state:', e.state);
};
在这个事件函数内,我们会打印事件对象中的state对象,其实就是我们执行pushState函数时放进去的对象,下面我们在控制台做下面几个操作看看效果:
如图所示,我们放入两条记录,当后退或前进的时候,都会执行到onpopstate事件函数,并且能够获取到当前的状态对象信息,如果我们的图片预览功能需要记录每一步的操作,那么我们就可以在onpopstate事件函数中获取到当前需要展现的图片ID,然后直接调用preview函数即可。
最后,需要注意的是,当前有些浏览器还不能很好的支持History API,所以我们最好判断一下浏览器的支持情况:
var isHistoryStateSupported = 'pushState' in window.history;
讲到这里,关于History的相关知识及应用基本上就告一段落了,希望大家能够细细体会它的应用场景,并加以练习,最后希望大家能够很好地掌握并运用到未来的项目中去。