blogs icon indicating copy to clipboard operation
blogs copied to clipboard

无刷新跳页 与 history API

Open forever-z-133 opened this issue 7 years ago • 0 comments

比 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 请求和绘制操作。 单页面应用实现起来是不是就无比简单了呢,这个过程中再加上动画和结果缓存,棒棒哒。

forever-z-133 avatar Nov 18 '17 15:11 forever-z-133