blogs
blogs copied to clipboard
无刷新跳页 与 history API
比 iframe 实现异步更新网页局部,ajax 要优异很多,但两者都有一个问题,就是失去了浏览器的前进后退的跳页功能。使用修改 hash 能弥补该效果,再配合下 history API 那就更棒了。
- history.pushState(state, title, url)
- history.replaceState(state, title, url)
- window.onpopstate
- 具体应用和我的实践经验
history.pushState(state, title, url)
字面意思,向 history 历史记录中加入一个声明。 state 表示前往新页面时带上的数据,title 指定新页面的页面标题,url 就是新页面的链接了。
history.pushState({from: 'xx'}, null, 'to.html');
张鑫旭大神的案例:http://www.zhangxinxu.com/study/201306/ajax-page-html5-history-api.html
PS:可以传 JSON 序列化了的 state,所以其实是可以传普通字符串数字等类型的参数的。
PS:state 传递后可以在当前页通过 histroy.state 来获取,比如 pushState({a:1},'','#main'),只有在 #main 这条记录中才能获取到,切换或者后退了就获取的是其他的 state 了。
PS:state 大小不得大于 640K,否则会报错。如果非要传递较大的数据,还是选用 sessionStorage 或者 localStorage 吧。
PPS:title 在如今(20171118) 好像依旧没有实现,但并没有被移除,也获取不到,还是得写上这个参数。如果确实需要这个功能,可以用 document.title = "xx"。
插个楼,document.title 用 setInterval 频繁修改在移动端效果不佳,需使用 requestAnimationFrame。 DEMO:https://foreverz133.github.io/demos/single/PageTitle.html
history.replaceState(state, title, url)
也是字面意思,把本页的历史记录替换掉,相当于不刷新页面的 location.replace。 参数和 history.pushState 一致。
// 地址栏:index.html
history.pushState({}, 'page2', '#main'); // index.html#main
history.replaceState({}, 'page2', '#detail'); // index.html#detail
history.back(); // index.html,效果上跳过了 #main
window.onpopstate
popstate 事件可以监听本页无刷新跳链的变化
但 pushState 和 replaceState 不会触发 popstate,其他 history 操作(如 go/back/forword)和浏览器前进后退操作才会触发,鸡肋呀。 且 pushState 和 replaceState 的 hash 操作也是不触发 hashchange 的,但回退时触发,这就更心酸了。
再者,location.hash 的改变是会触发 popstate 事件的,且在 hashchange 之前,两者关系真是纠葛呀。
整理一下,这意味着,使用 popstate 无法得知主动跳页,使用 hashchange 就不便用 popstate(因为会触发两次),非常尴尬有木有。 至于 location.search 方面的问题,也是有些尴尬的,在 下文 还会细讲。
为了解决这些问题,有两条路可走:
一、要么保持原生的跳转方法不变,不断改进监听机制,尽量解决上述问题。
// 解决 pushState 不触发 popstate 问题
var oldPushState = history.pushState;
history.pushState = function(state, title, url) {
oldPushState.call(history, state, title, url);
// 运行一次 popstate 对应的 function 当做触发
popStateFunction();
};
// 解决 location.hash 的改变既触发 popstate 也触发 hashchange 的问题(不完美)
if (e && e.type == 'popstate') this.skip = true;
if (e && e.type == 'hashchange' && this.skip) return;
setTimeout(function(){ this.skip = false; }.bind(this), 1);
详细代码可见:https://foreverz133.github.io/demos/single/router2.html
二、要么直接另写一套跳转机制,那么跳转监听自然也全权掌握。
$(window).on('linkchange', function(e, state, title){
console.log(state, title);
});
$.hrefTo = function(state, title, url) {
history.pushState(state, title, url);
$(window).trigger('linkchange', state, title);
}
PS:pushState 等触发的 popstate 比 load 更快,虽然意料之中,但很容易被忽视。 PS:写在全局下直接运行的 location.hash = 'xx',有可能 会不创建历史记录哟。 PS:location.hash = 'xx' 多次那也还是一条记录,返回一次就行,但 pushState 就是每运行一次加一条记录,多点几下就麻烦了。
一些骚操作
只出现一次的欢迎页
假设我们把 index.html 作为欢迎页,main.html 为主页面。 在 index.html 中延时 3s 然后 replaceState 到 main.html。 只有通过主入口进来的就欢迎一下,放完就消失,不管怎么 F5 刷新还是 main.html。
它相比 location.replace 没有刷新的感觉,相比在同一页操作要省去了好些是首次访问还是刷新的判断。
location.search
我们都知道不管是 href 里写 search 还是直接修改 location.search,页面都将刷新。 虽然可以像 vue-router 等单页面应用一样,把 hash 和 search 写在一块,比如 #/detail?id=xxx,这样只监听 hashchange 要方便很多。 但 search 放在 hash 前面才是 search,放在后面是合并了的 hash,用 location.search 是获取不到的。
而为了让 search 和 hash 能分离,这里恐怕需要绕个大圈... 不管怎样,我们肯定要使用 pushState 才能不刷新,但 pushState 不触发 popstate 事件,所以这需要上面那个 oldPushState 式的改写。 或者,索性不监听 popstate 了,全靠 hashchange,先 location.hash = '#page',然后再 replaceState({}, '', '?id=xx#page') 也算是跳页了。
合并还是分离哪种更好呢,真的没有定数。
我个人更偏好 hash 与 search 分离的这种,可能我是个比较喜欢强调尊重原生和语义的程序员吧。 再者,还可以使用 CSS3 的 :target 伪类,在 hash 变化时添加动画非常方便,而如果 hash 和 search 合并了,元素所需的 id 就变得很难书写了。 不过 :target 只知道有无 hash,同个元素无法分辨 #main 和 #page,这个有点不美妙。
<style>
.box { width:100px; height: 100px; background: pink; }
.box:target { background: red; transition: 1s; } /* 当链接有 #detail 的 hash 时,方块变红 */
</style>
<div class="box" id="detail"></div>
再者,就是 search 键值对的获取可以改造一下了:
function getQueryString(name, str) {
var reg = new RegExp('(^|\\?|&)' + name + '=([^&#$]*)', 'i');
var r = (str || location.search || location.hash).match(reg);
return r != null ? decodeURIComponent(r[2]) : null;
}
function locationConvert(str) {
var str = str || (location.search + location.hash) || '';
var hash = str.match(/#[^\?&$]*/);
hash = hash ? hash[0] : '#';
var search = str.replace(hash, '').split(/\?|&/);
search = search.reduce(function(re,b){
var x = b.split('=');
if (!b || x.length < 2) return re;
re[x[0]] = decodeURIComponent(x[1]);
return re;
}, {});
return { hash: hash, search: search }
}
pjax
那么基于以上这些,我们其实可以做些可能的封装,也就是 pushState + ajax,所谓 pjax。 https://github.com/search?q=pjax&type=Repositories&utf8=%E2%9C%93
想象一下,<a href="#detail?id=xxx"> 再指定一个放内容的 div,屏蔽原生跳链进行点击监听,每次点击变成 pushState 跳链,再直接(或监听 popstate)进行 ajax 请求和绘制操作。 单页面应用实现起来是不是就无比简单了呢,这个过程中再加上动画和结果缓存,棒棒哒。