再详解history.pushState和history.replaceState以及page ajax的实现

在之前的文章《ajax无刷新加载页面,结合history.state修改url》中,我详细解释了history.pushState、history.replaceState、history.state以及window.popstate这四个关键元素,并试图建立一个合理的ajax无刷新更换URL的页面加载方式。但那之后,我遇到一个比较复杂的问题,集中在state的内容上,本文则来详解state这个要素。

state是什么?

状态对象(state object) — 一个JavaScript对象,与用pushState()方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate事件都会被触发,并且事件对象的state属性都包含历史记录条目的状态对象的拷贝。(原文

说白了,state就是我们用于保存到history序列中的一个数据集合。我们可以在pushState之前,先构建自己的state,里面可以存放很多我们在监听window.popstate事件时所需要的东西。

例如,我们用代码来看:

var state = {
    title: title,
    url: url,
    content: $('body').html(),
    prev: window.location.href,
    time: (new Date()).getTime()
};
history.pushState(state, title, url);

上面title和url两个值是其他地方已经准备好的。通过上面这段代码,在history序列中就多出一个state,并且可以通过window.popstate识别。

而在window.popstate的监听动作中,我们可以这样去做:

window.onpopstate = function(event) {
    var state = history.state; // 等价于 var state = event.state;
    if(state && state.content) $('body').html(state.content);
};

当点击浏览器的前进或后退按钮时,popstate事件被触发,浏览器会到history序列中去找到上一个或下一个state,并且将这个state放到event.state中。这个时候,该state就是当前浏览器所显示的页面所使用的state了,所以使用history.state可以读取到同样的内容。

而这个时候,该state中,正好保存着我们在pushState时,保存到content内容,直接使用它即可。为了实现title,css,js之类的进一步重载,你还可以做更多的内容保存和逻辑处理。

从上面可以看到,当我们在使用pushState和popstate时,可以在脑海中先想象一个history序列,通过pushState加入一个新state到序列中,通过history.state取出当前屏幕显示区域所需要的state。

replaceState和pushState的区别

虽然从上面的讲解中,我们大致掌握了state序列的问题,但是,我们还有一个点没有讲到,那就是改变url。毫无疑问,我们知道通过replaceState可以无刷新页面改变url。

history.replaceState(state,title,url);

其中,state,title,url都是我们事先准备好的。其中,要点就是url,如果我们仅仅希望实现URL的变化,甚至可以做下面这个实验:

history.replaceState(null,null,'/test.html');

把这段代码加入到页面后,观察url的变化。实际上,整个页面没有发生任何变动,唯独url改变了。

这个时候,我们回头来看replaceState之后,state的一些变化。通过下面这段代码来试验:

console.log(history.state); // 得到一个内容丰富的state内容
history.replaceState(null,null,'/test.html');
console.log(history.state); // 得到null

这说明replaceState将对当前显示界面对应的state发生作用,甚至替换state的内容,这是一个会产生负影响的操作,实际上,我们希望通过pushState将一个配置好的state加入到history序列中,但是replaceState改变了我们加入到序列中的state的内容。因此,我们必须保证replceState的时候,所传入的state参数所我们想要使用的state的内容。这个很难理解,我们用一张图来解释:

historyState

上图中,1是原本的history序列,我们在阅读到某个界面到时候,执行pushState操作,使history序列变成了2的状态,再对pushState的state执行replaceState后,变成了3的状态。从2变为3这个过程,把原本的state修改了,我们要想清楚,我们是要把{title,content}放入history还是把{name,number}放入history,如果是前者,那么就要在replaceState的时候,格外小心。

配合ajax实现无刷新切换url的页面跳转

通过上面这些讲解,我们大致可以总结出如下一些思路:

1.ajax加载新页面
2.加载完后,通过pushState记录历史,通过replaceState切换url,页面无刷新,但url变了
3.通过window.popstate实现点击前进后退时,切换到之前的页面内容

$('a[href]').on('click',function(e){
    e.preventDefault();
    var $this = $(this),href = $this.attr('href');
    if($this.attr('disable-ajax') !== undefined) return true;
    if(href.indexOf('javascript') === 0) return false;
    if(href.indexOf('http') === 0 && href.indexOf(window.location.host) < 0) return true;

    $.get(href,function(html){
        var title = $(html).find('title').html(), content = $(html).find('#content');
        $('title').html(title);
        $('#content').html(content);

        var state = { // 准备用于push的state
            title: title,
            url: href,
            selector: '#content',
            content: content,
            prev: window.location.href,
            time: (new Date()).getTime()
        };

        if(href != state.prev) history.pushState(state, title, href);
        history.replaceState(state, title, href);
    });
});

$(window).on("popstate", function() {
    var state = history.state;
    if(!state || !state.selector) { // 如果不存在state,则说明是第一次进入该页,就没有必要进行前进或后退操作
        return;
    }

    $(state.selector).html(state.content);
    $('title').html(state.title);

    history.replaceState(state,state.title,state.url);
});

将上面这段代码放到你的jquery代码中,注意要放到$(document).ready()中。我们这里有一个规定,就是使用#content作为选择器,抓取和替换的内容仅限于#content区域。这样的话,你的页面上的菜单、边侧栏都没有变化。所以,最好可以把选择器范围扩大,并增加一些页面变化的动作。

2016-03-25 | ,