• content
    {:toc}

起因

起因是这样的,在尝试前后端分离的这条道路上,我自己也在不断摸索,感觉要把大部分的坑都踩踩了。目前我用的技术是:

  • webpack 自动构建
  • AMD 模块化 js
  • Sass 预处理 CSS
  • 使用前端模板引擎 handlebars 解决动态操作将 html 拼接在 js 中的问题

但最近写了一个项目类似知乎这样的多页网站。前端 url 的处理让我觉得不够优雅。我使用的是 hash 的方式处理动态 url 的,为此我专门在知乎上提了一个问题:前端如何处理动态url?

这里我将问题描述如下:

前后端彻底分离的情况下,页面跳转页全部由前端控制。那么如何更好的处理动态url地址?
例如本问题的url为
https://www.zhihu.com/question/38802932
这肯定是用后台路由处理的url

纯前端怎么处理?用hash吗,如下:
https://www.zhihu.com/question#38802932
那如果本页跳转,只改变hash的话,页面不会刷新。
使用location.reload()倒是可以解决。

但总觉得这样处理不够优雅。大家在工作中是如何处理此类场景的?还是用传统的后台路由来提供动态url?

感谢郑海波和剧中人的热心回答。都提到了history对象中的pushState,这是我第一次接触到这方面的内容(顿时觉得自己真是才疏学浅)。

同时也有人提到了pjax,这个就是pushState+Ajax的封装,也很有意思。

下面就来研究和实践一下吧。

History

window对象通过history对象提供对浏览器历史记录的访问能力。它暴露了一些非常有用的方法和属性,让你在历史记录中自由前进和后退,而在 HTML5 中,更可以操纵历史记录中的数据。

back(), forward(), go(), length

浏览器的历史记录就好像一个栈,最新的在最上面,较早之前看过的在下面。

如下图,Chrome的历史记录:

chrome history

下面介绍怎么在这些历史记录中跳转,但要注意,上图中的浏览器历史记录和本文说的 history 还不太同。

  • back()

    在历史记录中后退

      history.back();
    
  • forward()

    在历史记录中前进

      history.forward();
    
  • go()

    移动到指定的历史记录点

      history.go(-1);
    

    通过指定一个相对于当前页面位置的数值,你可以使用go()方法从当前会话的历史记录中加载页面(当前页面位置索引值为0,上一页就是-1,下一页为1)。

    go()不填参数或参数为go(0)时,页面会刷新,即history.go()history.go(0)相当于location.reload()

  • length

    length为history的属性,显示history长度。

本节在线demo见:History & pjax demo 源代码:

经过亲自测试,history对象只记录同一个 tab 页内的历史。如果是在新窗口打开的,则无效。如:在a标签中添加target="_blank",或按住ctrl点击,这类场景下,在新的tab页中,history对象也是新的。

history对象记录的信息与是否同源也无关,所以唯一要满足的就是同一个标签页。

pushState(), replaceState()

HTML5 引进了history.pushState()方法和history.replaceState()方法,它们允许你逐条地添加和修改历史记录条目,能够在不加载新页面的情况下没改变浏览器的URL。这些方法可以协同window.onpopstate事件一起工作。

使用history.pushState()会改变referrer的值,而在你调用方法后创建的 XMLHttpRequest 对象会在 HTTP 请求头中使用这个值。referrer的值则是创建 XMLHttpRequest 对象时所处的窗口的 URL。

  • pushState(any data, string title, [string url])

    第一个参数为history对象的state属性值,可以放任意数据,记录历史状态。第二个参数是新状态的标题,目前浏览器基本不支持。第三个参数为可选的相对url。

    执行pushState后,可以在不加载新页面的情况下,更改url。同时history栈中新增一条数据。

    例如,我们有这样一段代码:

      <button id="push1">pushState()</button>
    
      document.querySelector('#push1').addEventListener('click', function() {
          history.pushState('abc','pushStatePageTitle','pushState.html');
          document.querySelector('#length').innerHTML = history.length;//重新读取历史长度
      });
    

    当点击按钮的时候,页面不会刷新,但url地址的最后已经变为pushState.html。这一点非常像hash的作用,但比hash更优雅。

  • replaceState(any data, string title, [string url])

    pushState()类似,只是在history栈中不是新增记录,而是替换一条记录。

需要注意的是:pushState()replaceState()方法存在安全方面的限制,本地测试是无效的,会报错,可以简单放到任何服务端测试,或者使用http-server开启简单服务器,通过访问localhost来查看效果。

本节demo见:History & pjax demo - pushState

pjax

现在再看本文一开始提出的问题,如何让前端优雅的控制 url,这里就可以考虑 pjax 技术了。我们把 pushState + ajax 进行封装,合起来简称为 pjax。虽然不是什么新的技术,但概念已然不同。

如果不使用 pjax。我们依然可以使用hash来实现文本开始的需求。但会不利于 SEO,看着也不够优雅。

Pjax的原理十分简单。

  1. 拦截 a 标签的默认跳转动作或某些按钮的点击事件。
  2. 使用 Ajax 请求新页面。
  3. 将返回的 Html 替换到页面中。
  4. 使用 HTML5 的pushState()修改Url。

个人理解3中也可以仅仅请求数据,再由浏览器渲染。

每当同一个文档的浏览历史(即history对象)出现变化时,会触发window.onpopstate事件。

window.onpopstate = function(event) {
    console.log(event.state);
    console.log(location);
};

这样在用户点击前进后退时也可以很好的监听url,来做相应的页面渲染。

若用户刷新了页面,但没有相应的页面资源,这时页面就会显示不存在。所以我认为较好的方法是在写pushState()第三个参数的时候,写为?a=1这样的参数形式。History.js 也是这么写的。但是这样应该会多一次请求。也许使用 nodeJS 作为中间层会好一些吧。

对于上述的探索,不知道是不是我还不够深入,总觉得还是不够完美。

参考