HistoryAPI是HTML5的新特性,它是浏览器提供给网页的一组接口,即BOM特性。它用于在不重载页面的情况下修改页面url,或是向浏览器的历史记录中添加访问记录。
绝大多数现代浏览器都支持HistoryAPI,在这里可以查看到它的兼容性,如果浏览器不支持,仍然可以使用History.js垫片库来模拟实现。

有关HistoryAPI的知识,可以参阅MDNW3CPlus教程

需求

现在大部分的移动端网页、微信内活动页、手机商城页面等,我们称之为单页面应用(SPA)。在移动设备上,这种前端技术可以提供优秀的用户体验。
实现SPA往往要做到以下两点:

  • 首先,用户在站内浏览,网页不能重新加载,而是尽量模拟成原生APP的跳转形式。
  • 因此,网站的整体框架要提前在客户端缓存好。这一需求可以通过webpack等打包工具来实现。

这里举一个例子:在网易云音乐,网页中有一个迷你播放器可以播放音乐,而用户可以切换浏览不同的专辑、不同的歌曲列表,如果网页重新加载,播放就会中断。因此必须实现一种技术,使得用户既能使正常浏览网站,又不会导致网页重新加载。

常见的解决办法

在以前没有HistoryAPI时,这种需求通常可以使用以下方案来实现:

  • ajax:使用ajax来获取数据并实现局部页面更新;
  • 内嵌iframe页面:使用一个内嵌的iframe来显示页面内容,页面跳转操作通过js控制iframe里的页面来实现。这种实现方式非常简陋,往往要花费很多工作来解决样式、交互、数据传递等问题。

但是,以上方式最多只能实现不重载页面这一需求,为了防止网页重载,所有的链接都不能使用a标签来跳转,也不能使更改window.href的值,而是要调用相应的js代码来完成页面更新,所以说页面的url也是不会轻易改变的。
但是,页面正常浏览和跳转,而url无法跟随改变,这显然不合理。通常情况下,url代表网页内容的一个标示或标记,要能反映出页面的内容。况且,使用这些方法进行页面跳转,用户无法使用浏览器的“前进”、“后退”等按钮功能。因此,还要实现几个需求,要能使url能随着页面内容而改变,还要保持浏览器的前进后退等功能的可用性。

在任一个url中,例如https://example.com/index#xxxx,其中#(井号)后面的内容,我们称之为hash。它往往用于标记页面的某一个锚点,实现“书签”这种功能。因为hash设计时就是为了标记和定位到网页中的某一个元素,因此url中的这一部分即使发生改变,网页也不会重新加载,而且,如果hash发生改变,大部分现代浏览器会生成一个历史记录,这样浏览器的前进、后退等功能也可以随之配合使用。

假设我们正在访问一个页面,它的url为https://example.com/section-1/categorie-1/article-1,对于这个url,可以按如下方式来拆分:

  • https:// 这一部分表示网站使用https协议,通常在一个网站内,协议不会发生改变;
  • example.com 这一部分是网站的域名(主机名),站内浏览,域名显然不会改变;
  • /section-1/categorie-1/article-1 这是网站中我们访问的页面资源的路径。当用户前往新的页面,实际上url中总是只有这一部分发生改变,前面的协议名、域名都不会轻易改变。

我们得知在一个url地址中,只有路径这一块是用来反映和标示用户当前访问页面资源的,而之前我们也知道url中的hash可以随意更改,它不会导致浏览器进行重载。因此,可以将url做出如下改动:
原本是https://example.com/section-1/categorie-1/article-1
改变为https://example.com/#/section-1/categorie-1/article-1

可以看到,在域名和资源路径之间,添加了一个#(井号),这样一来,我们实际上做的是将网站的资源路径这一部分移至hash内,因此以往的改变资源路径来实现页面切换,被我们改成了改变hash内容来实现页面切换,hash值怎么变浏览器都不会重载网页,这样便实现了使用不同的url反映不同的页面内容。

使用hash来代替资源路径,解决了页面和url之间的对应关系,但是,hash设计的本意是用来实现定位到网页元素,而这一步是浏览器行为,也就是说,用户从url中输入的hash是不会提交到服务器的。
例如,用户访问example.com/index#hash,浏览器发出的request的请求如下:

1
2
3
4
GET /index HTTP/1.1
Host: example.com
Connection: keep-alive
...(其他请求头等)

总之浏览器只会请求资源路径,hash中的内容是不会提交到服务器的。为了用hash实现url资源路径,我们还必须使用js来额外向服务器提交hash中的内容才行。
浏览器提供了处理hash内容变化的事件,可以通过在body标签上添加onhashchange属性,或是注册window.onhashchange事件来绑定当hash值浏览器所要触发的回调,如果想绑定多个回调,也可以使用window.addEventListener("hashchange", 回调函数, false)这种方式。具体使用方式可以参阅MDN

还有一个问题:如果url使用https://example.com/#/section-1/categorie-1/article-1这种形式,搜索引擎是不会将#后面的内容当做资源路径的。为了解决这个问题,Google提出了使用#!来代替#,对于使用这种url格式的网站,搜索引擎爬虫会将hash也视为url的一部分。因此上述的url可以改为https://example.com/#!/section-1/categorie-1/article-1,这样便能正确的被搜索引擎爬虫收录。

无论使用哪种方式,url中总会有一个丑陋的#,而且使用hash作为资源路径,那hash本身的功能,即定位页面元素,这一功能也无法再使用了。
因此HTML5规范中,提出了HistoryAPI。使用它,便可以解决我们的上述问题。现代前端SPA框架中,页面切换访问往往也都是采取了调用HistoryAPI的方式来实现,例如Vue-router、React等。

HistoryAPI用法

浏览器的window对象上有一个history对象,这是一个History类的实例,它也是一个宿主对象。这个history对象用于保存页面的浏览历史等信息。
history对象具备以下属性:

  • .length:会话历史中的条目数,它包含当前页
  • .state:获取历史堆栈顶部的state对象
  • .scrollRestoration:可用manual或者auto两个值,表示从前一个页面返回后,是否自动滚动页面到之前浏览的位置

出于安全和保护隐私的考虑,history无法访问或是获取用户的浏览历史记录。但是,为了能方便的切换页面,它提供以下方法:

  • .back( ):前往上一页,类似返回按钮或history.go(-1)
  • .forward( ):前往下一页,类似前进按钮或history.go(1)
  • .go(n):以当前页面的相对位置来访问,history.go(0)则将刷新页面,超出页面范围的界限该方法将无作用。参数也可以是一个表示相对或绝对url的字符串,这种情况下浏览器会尝试前往当前历史记录中匹配该url的最近的一次访问记录的位置
  • .pushState(state, title, url):向会话记录创建一条访问记录而不刷新页面,注意访问是不能跨域
    • 参数state:一个可被序列化的对象,它用于存储该会话的状态,history.state获取的就是会话记录栈的栈顶对象的该state属性。在浏览器关闭后再打开,该对象仍然保留
    • 参数title:标题,暂无浏览器支持,设为null即可
    • 参数url:要存储进记录的url,一般设置为相对路径,如果设置绝对路径需要注意不要跨域
  • .replaceState(state, title, url):它类似于pushState,但是它用这些参数来替换当前页面的访问记录条目,而不是新建一条访问记录

同样,浏览器提供了事件window.onpopstate来实现监听用户点击前进、后退按钮的切换页面的访问行为,因为用户前进或是后退,实际上改变当前页面在会话记录栈中的位置。需要注意的是,pushState( )replaceState( )是不会触发这个事件的。该事件回调参数e具备一个成员.state,它的值和history.state相同

附注

  • 浏览器为每个页面维护一个会话记录栈。如果当前在栈顶,则pushState( )向栈顶压入一个会话记录,replaceState( )替换掉栈顶的当前的记录;如果当前不在访问记录的栈顶,那pushState( )会创建一个新的栈分支,之前分支的内容将会被移除
  • history.back( )history.go( )只是在会话记录栈中前后移动,因此这两个方法不会导致hostory.length发生变化
  • history.pushState即使传入空的url或者#hash也会产生一个新的历史记录。另外如果url是一个普通字符串,则默认写入主机名后面斜杠/后,如果指定了协议、端口等,则当做绝对路径来访问,但该操作依旧无法跨域