1、背景javascript
使用ajax,能够实现不须要刷新整个页面就能够进行局部页面的更新。这样能够开发交互性很强的富客户端程序,减小网络传输的内容。但长期以来存在一个问题,就是没法利用浏览器自己提供的前进和后退按钮进行操做。好比在页面执行某个动做,该动做利用ajax请求到服务器获取数据,更新了当前页面的某些内容,这时想回到操做前的界面,用户就会习惯点击浏览器的后退按钮,实际这里是无效的(要么页面没反应,要么打开一个前面打开的过的页面),或者想收藏当前页面(以便于从新打开时直接显示当前的信息),也是没法作到的。css
这个问题由于html5的新特性而得以能够解决。但不是直接解决了。而是提供了一些新的api,须要程序员编写代码来实现。下面咱们将详细的来介绍。html
若是你对此问题和html5的这些新特性已经有些了解,能够直接跳到最后的案例章节。html5
2、history对象分析java
浏览器是经过 window对象的 history对象来对浏览器历史访问记录,从而能够实现前进和后退。history对象能够理解其保存了一个有序的列表对象,每一个对象都表明了一个页面信息(包括页面的url等信息),注意当前页面也被保存在里面。jquery
这样就能够经过浏览器自己提供的前进和后退按钮来操做,也能够利用javascript调用history对象的back(),forward(),和go()方法来实现页面的切换。程序员
咱们先来理解下history的机制。history对象中记录了浏览器窗口访问过的url,但出于安全考虑,没法经过程序获取history对象中的具体信息,只能经过back、forward、go方法进行页面跳转,此外length属性记录了history中的记录(url)条数。web
咱们设想下,当在浏览器窗口打开第一个地址,好比 url1时, 这时history中就有了url1这个记录,且length属性值为1,history对象中有个当前页面指针(从概念上能够这么理解)指向url1;若是再打开一个url2页面(不管是经过在地址栏直接输入、或经过url1中的连接或js代码打开),这时history中就有了url1和url2这两个记录,是一个有序的列表,这时length属性值为2,history对象中的当前页面指针指向url2,这时url2是最新的页面,页面不能够前进,但能够后退到url1,这时若是点击浏览器自己提供的后退按钮(或用js调用back方法),这时url1页面会被从新加载显示,history对象的length仍然为2,url1和url2组成的列表仍然不变,但history对象中的当前页面指针指向url1了,这时就不能后退但能够前进了。能够理解成一个数据结构中的双向链表机制。ajax
经过上面的描述咱们能够看出,咱们说的历史记录都是指一个完整的页面请求url,而ajax并非一个完整的页面请求,所以浏览器没法记录ajax的操做信息。chrome
3、history对象的新特性
HTML5引入了histtory.pushState()和history.replaceState()这两个方法,它们会更新history对象的内容。同时,结合window.onpostate事件,就能够解决文章开头提出的问题。
咱们先来看pushState方法的含义,咱们经过举例子的方式来更好的说明,先给出一段代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>测试</title> <script type="text/javascript" src="jquery.min.js"></script> </head> <body> <button onclick="doPushState()">pushState</button> <button onclick="count()">count</button> <script> var index=1; $(function(){ alert(location.href); }); function doPushState(){ history.pushState({}, "newtitle","test"+(index++)+".html"); } function count(){ alert(window.history.length); } </script> </body> </html>
上面是一个完整的html文件,文件名为demo.html。 把该文件放到web服务器上,从浏览器访问。若是是直接从本地磁盘打开,文件中的js代码执行会报错误。
在一个新的浏览器窗口访问该demo.html。 首先会执行 $()方法,弹出代码中的location.href信息。 这时执行count按钮,显示为1,注意若是在ie或chrome的新的浏览器窗口打开,值可能为2,由于它们的窗口会加载系统默认的一个页面,不是一个空白的窗口。
这时咱们每点击一下pushState按钮,发现浏览器的地址会发生变化,前后变为test1.html , test2.html, test3.html, .......,而且经过点击count按钮发现,弹出的值加1. 这说明每调用一次pushState方法,history中就会新增长一条url记录。
咱们先来解释下pushState方法,该方法有三个参数:
1)第一个参数是个js对象,能够听任何的内容,能够在onpostate事件中(后面介绍)获取到便于作相应的处理。
2)第二个参数是个字符串,目前好像没有起做用,能够传个空串。
3)第三个参数是个字符串,就是保存到history中的url。
结合例子的代码和输出能够看出,调用pushState方法的做用,就是至关于打开一个新页面,把当前页面做为历史记录,而当前的地址栏显示的是pushState方法中的url(这里是test.html)。可是与普通的打开一个新页面不一样。浏览器将不会在调用pushState()方法后加载这个url,也就是说即便你写一个错误的url,也不会报错。
能够这么理解,当咱们在一个新的浏览器窗口打开 demo.html后,点击n次pushState按钮后,history对象中存在这样的一个ulr列表。
demo.html(第1个url)----> index1.html(第2个url).......->index?.html(第n个url)----->indexn.html(当前页面的url)。
这时咱们须要点击浏览器上的回退按钮n次,才能将浏览器上的地址退回到 demo.html。并且不管是在点击pushState按钮 或点击回退按钮的过程当中发现,$()方法根本没有被触发,也就是说整个过程浏览器的页面内容都没有发生变化,变化的只是地址信息。
这也进一步说明,pushState只是将当前页面保存到history的历史记录中(并做为最近的一个记录),而且将当前浏览器的地址栏改成参数url的指定的值,但并不会加载它。这点与普通的经过连接打开或浏览器地址输入url彻底不同。
到了这里咱们能够想象一下文章开头提出的问题了,若是咱们在页面中执行一个ajax操做,当操做成功(如更新页面的局部内容)后,咱们经过代码调用pushState方法,设置一个新的url,这样看上去就像发起了一个全新的请求,实际上只是个ajax操做。这时回退按钮也能用了,问题仅仅这样,回退按钮点了也没有任何反应。若是咱们能经过代码,来响应这个回退按钮触发的事件,在事件中让界面恢复到ajax请求以前的界面,问题不就解决了吗?
得确如此,解决思路就是上面说的。下面咱们来经过一个实际的例子看如何实现。在介绍例子以前,咱们先来解释下html5中 history新增的另外一个方法replaceState方法。
replaceState方法与pushState相似,一样有三个参数。区别在于,replaceState()是用来修改history对象中记录的当前页面的信息,它不是新建一个记录。若是将上面例子中的 代码 history.pushState({}, "newtitle","test"+(index++)+".html"); 中的pushState改成replaceState,其它代码都不动。这时咱们点击pushState按钮后,看到的现象是同样的,地址栏的地址不断变化,页面内容不变。但咱们点击count按钮,发现history中的记录数不变。这说明replaceState只是改变当前页面在history对象中的记录信息;而pushState是会产生一个新记录做为当前记录,把当前页面做为历史的记录保存。
咱们再来看下window对象的popstate事件,当进行页面的前进或回退时,会触发该事件,而且在事件响应函数中经过 history.state 能够获取到 pushState方法和replaceState方法中第一个参数指定的对象。
解释了这几个api后,咱们来一个具体的例子。
4、具体案例
咱们来设想这样一个应用。一个页面来显示一篇长文章,该文章内容很长,分为不少章节。咱们但愿页面不会一次把全部章节的内容都加载起来,而是有一个章节导航,点击每一个章节连接,经过jax加载具体章节的内容,而其它页面部分不须要要变化。
咱们先看下传统的实现代码(注意,这里只注意核心逻辑代码的实现,其它的页面布局等尽可能简化):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test2</title> <style type="text/css"> div { padding-bottom:100px; } </style> <script type="text/javascript" src="jquery.min.js"></script> </head> <body> <div style="float:left;border:1px solid red;margin:20px"> <p><a href="javascript:;" id="section1">第1章</a></p> <p><a href="javascript:;" id="section2">第2章</a></p> <p><a href="javascript:;" id="section3">第3章</a></p> </div> <div style="float:left;border:1px solid red;margin:20px" id="content"> </div> <script> $(function(){ //添加连接的处理事件 $("a").click(ajax); //加载默认的章节,默认显示第1章 $("#section1").trigger("click"); }); function ajax(event){ //实际的流程是发起ajax请求,获取内容并显示。这里为了简化,没有写实际的ajax请求。 //这段代码应该在ajax的请求响应中编写。 $("#content").html(this.id+"的内容"); var title = this.id; document.title = title; } </script> </body> </html>
在浏览器加载该页面,当咱们点击不一样的章节连接时,内容会跟着变化,浏览器的标题也跟着变化。可是:
1)回退、前进按钮用不了
2)当咱们刷新页面时,无论当前在哪一个章节,都会从新回到第一个章节。
3)地址栏的url没有变化,也意味着咱们无法把某个章节的地址保存下来,之后再次打开直接显示该章节内容。
上面就是传统ajax应用的一些弊端。下面咱们就来解决这些问题。
咱们先给出解决代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test2</title> <style type="text/css"> div { padding-bottom:100px; } </style> <script type="text/javascript" src="jquery.min.js"></script> </head> <body> <div style="float:left;border:1px solid red;margin:20px"> <p><a href="javascript:;" id="section1">第1章</a></p> <p><a href="javascript:;" id="section2">第2章</a></p> <p><a href="javascript:;" id="section3">第3章</a></p> </div> <div style="float:left;border:1px solid red;margin:20px" id="content"> </div> <script> $(function(){ //添加连接的处理事件 $("a").click(ajax); //加载默认的章节 changeContent(); //添加popstate事件 $(window).on("popstate",function(){ changeContent(); }); }); function changeContent(){ var query = location.href.split("?")[1]; if (!query) { // 若是没有查询条件,则显示默认第1个章节 history.replaceState(null, "", location.href + "?name=" + $("#section1")[0].id); changeContent(); } else { //触发按钮click事件,加载内容, //注意不要漏了true参数,这样能够和用户直接点击触发的页面变化区别开来 $("#"+query.split("=")[1]).trigger("click",true); } } function ajax(event,isPopstate){ $("#content").html(this.id+"的内容"); var title = this.id; document.title = title; if(!isPopstate){ history.pushState(null, "", location.href.split("?")[0] + "?name=" + title); } } </script> </body> </html>
加载上面页面,测试下,全部的问题都解决了。下面咱们来解释下上述代码。
咱们先看changeContent方法,该方法首先获取页面的url地址,判断该地址是否有查询条件(是否带章节信息),若是没有,认为要显示第一章节。咱们利用history的replaceState方法来改变当前的url,加上 name=section1的查询条件,表示是第1章。由于replaceState方法不会改变页面内容,所以还须要接着再调用changeContent方法。若是地址带了查询条件,认为已经指定显示某个章节内容,这时触发章节连接的click事件。
咱们再看ajax方法,就是章节连接的click事件响应函数,为了简化,该函数没有发起实际的ajax请求,而是至关于直接处理ajax返回的结果。首先是用获得的结果更新页面(这里是直接写死的),而后更新标题,这与传统的ajax作法同样。关键的区别是,判断该方法若是是用户点击的(不是onpopstate事件处理的),就会调用history对象的pushState方法来将当前页面信息保存到history对象中,并新增一个记录信息表明ajax请求后的页面。
changeContent方法一样是onpopstate事件的处理函数,其功能就是利用获取到的url信息(保存在history记录)中,来经过ajax获取到对应的内容,让页面显示相应的信息。 从用户感知上看,就跟正常的回退、前进致使的页面切换同样。用户感受不到是ajax请求,还觉得就是多个独立的页面在切换。
5、小结
本文详细的介绍了如何利用html5的新特性来解决传统ajax请求致使的一些缺陷。经过上面的介绍能够看出,为了解决问题,仍是须要程序员作很多的事情,对于一个实际的项目来讲,最好能在框架层面进行封装解决,而不是要让每一个具体页面的实现者都来处理。这个能够是下一步要考虑的内容。