浅析列表页请求优化(history API)
最近搞了下列表页请求的功能,并做了一下调研整理,记此文备忘。
列表页请求的功能到处可见,比如在博客园。
点击相应的页码,页面返回相应的内容,看上去似乎大同小异,但是一些小的细节还是可以区分优劣。
full load
公司原来的代码采用的是 full load 的方式,也就是每点击一次,页面完全加载。并不只有我们网站这样做,很多大厂也这样搞,比如 新浪。
列表页中的很多部分内容,其实都是一样的,这样做就每次需要重新加载这部分的内容,没有必要,而且 css、js 都需要重新加载(虽然可能有缓存)。以前我逛学校的论坛,是用 PHP 的 Discuz! 搭建的,每个主题后的回复页之间的跳转都是 full load 的方式,体验很差。
所以个人觉得,不管是性能还是用户体验上,full load 的方式在现在的 web 开发中,都是不可取的。
ajax
接着 ajax 出现了。ajax 就不多做介绍了,局部刷新,体验非常好。但是单纯的 ajax 虽然性能上比 full load 提高了不少,用户体验还不是很好,主要有以下两点。
- 保存不了书签
- 不支持浏览器的后退前进操作
究其根本,是因为传统的 ajax 操作不改变 url。
ajax + #
为了解决以上问题,聪明的开发者们用 # 来改善体验。
以博客园为例,我们请求第二页的时候,实际的 url 是 http://www.cnblogs.com/#p2,当点击页码发送请求时,同时改变页面的 url,因为改变的是 lcation.hash,所以页面并不会重载。我们将其保存为书签,当我们打开 http://www.cnblogs.com/#p2 时,我们可以提取 hash 值,据此发起相应的 ajax 请求。
接着我们来看第二个需求,如何支持浏览器的后退前进操作。有些童鞋可能会问,已经有了上一页、下一页的功能,支持浏览器的后退前进操作,有必要吗?灰常有必要,比如我们先点了第二页,然后点了第四页,我需要回到第二页,又忘了刚才点的是第几页,会条件反射地去点浏览器的回退。其实博客园 http://www.cnblogs.com/ 没有做这个功能,如何支持?我们可以监听 hashchange
事件,当 url 的 hash 值发生变化时,重新发送请求。但是 hashchange
事件并不支持某些 IE (http://caniuse.com/#search=hashchange),对于不支持的浏览器,我们只能设置一个定时器,不断得去查看页面的 hash 是否改变,会造成不小的性能问题(或者直接放弃这部分浏览器,或者降级处理)。
简单地写了个 demo,猛戳 https://github.com/hanzichi/practice/tree/master/2016/pjax/hash 查看(没有兼容不支持 hashchange 事件的浏览器)。
这里再插点题外话,讲点小历史。以前的搜索引擎爬虫,是不会抓取 ajax 请求的内容的(毕竟木有这么智能),只会去抓网页的源代码,这就蛋疼了,我们既希望用 ajax 改善体验,又希望内容可以被搜索引擎爬虫抓取,二者不可得兼?Google 搜索制定了一套规则。
- 网站提交 sitemap 给 Google;
- Google 发现 URL 里有 #! 符号,例如example.com/#!/detail/1,于是 Google 开始抓取 example.com/?_escaped_fragment_=/detail/1;
_escaped_fragment_ 这个参数是 Google 指定的命名,如果开发者希望把网站内容提交给 Google,就必须通过这个参数生成静态页面。
也就是说,每个 ajax 请求的内容,都需要提供一个相同内容的静态页面,供爬虫爬取。
随着 web 的发展,这一切已经成为了历史,现在的爬虫已经可以执行 JavaScript,爬取 ajax 请求的内容了!这部分的内容,就不展开了,有兴趣的可以参考下以下链接。
- Understanding web pages better
- Updating our technical Webmaster Guidelines
- 我们将弃用 AJAX 抓取方案
- AJAX Crawling (Deprecated)
- Making AJAX applications crawlable
ajax + pushState
ajax + #,似乎可以满足一般的需求了,但是如果不止限于列表请求呢?改变 hash 值搞的 URL 看起来不像一个正常的 URL,而且 hash 本来的用处并非如此,这样搞有点黑科技的感觉。HTML5 的出现,能让 ajax 变的更加优雅。
为了解决传统的 ajax 带来的问题,HTML5 里加强了 history API,加入了 pushState、replaceState 接口和 popstate 事件。
举个简单的例子,我们看 GitHub,首先定位到页面 https://github.com/hanzichi/underscore-analysis,然后点击该 repo 下第一个行第一个文件夹 『underscore-1.8.3.js』,URL 变为 https://github.com/hanzichi/underscore-analysis/tree/master/underscore-1.8.3.js,页面局部刷新,看了下 Network 面板,是一个 ajax 请求,且该操作支持保存书签、回退前进等功能。这一切的实现都基于 history 新增的 API。
history 原有的 API 大都灰常简单,比如 history.length
(该 tab 访问过的网页数量,新建 tab 时的空标签该属性值为 1),history.back()
,history.forward()
,history.go(-1)
等等,不多加介绍,简单介绍下新增的 history.pushState
,history.replaceState
以及 popstate
事件。
history.pushState 方法接受三个参数,依次为:
- state:一个与指定网址相关的状态对象,popstate 事件触发时,该对象会传入回调函数。如果不需要这个对象,此处可以填 null。history.state 属性能保存当前页面的 state 对象。
- title:新页面的标题,但是所有浏览器目前都忽略这个值,因此这里可以填 null。
- url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
假设现在的网页是 http://localhost/1.htm,我们使用 pushState 方法在浏览记录(history 对象)中添加一个新纪录。
var stateObj = {page: 2 };
history.pushState(stateObj, "page 2", "2.htm");
浏览器地址栏立刻显示 <localhost/2.htm>,但是并不会跳到 2.htm 的页面(pushState 不会触发页面刷新),甚至这个页面不存在也不会报错,它只是成为了浏览器中的最新记录,可以查看 history.length,会发现该属性值增加了 1。如果这时点击倒退,url 将显示 1.htm,内容不变。
如果 pushState 的 url 参数,设置了一个当前网页的 # 值(hash),并不会触发 hashchange 事件。如果设置了一个非同域的网址,则会报错。
history.replaceState 方法的参数和 pushState 一样,区别是它修改浏览器历史中当前页面的值,即使用 replaceState,history.length 并不会增加,只是替换了当前页面在 history 中的记录。
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // url 显示为http://example.com/example.html?page=1
history.back(); // url 显示为http://example.com/example.html
history.go(2); // url 显示为http://example.com/example.html?page=3
前面 ajax + # 的例子中,我们用 hashchange 去判断浏览器的前进后退操作,那么,是否有原生的监听浏览器前进回退操作的事件呢?有的,popstate 事件。每当同一个文档的浏览历史(即 history 对象)出现变化时,就会触发 popstate 事件,只有当用户手动点击浏览器后退前进按钮,或者调用 back、forward、go 方法时才会触发。
window.onpopstate = function(e) {
console.log(e.state);
// 等价于
// console.log(history.state);
}
于是我们要实现一个列表页请求的功能,就呼之欲出了。点击页码,用 pushState 塞入一条新的记录,同时改变 url,然后发送 ajax 请求,局部更新。点击浏览器后退前进按钮,触发 popstate 事件,发送请求,局部更新,请求的字段,可以根据 url 去判断,也可以储存在 state 中。写了个简单的 demo,猛戳 https://github.com/hanzichi/practice/tree/master/2016/pjax/pushState 查看(根据 url 判断了)。
pushState vs #
ajax + pushState 以及 ajax + hash 的作用类似,但是推荐使用前者,有以下几个优势:
- pushState 能改变的 URL 的范围大,在一个域名下的都可以,而 hash 的方法,因为 URL 只能改变 location.hash 的值,所以 URL 其实是只能在一个文档(document)下改变。比如 GitHub 中的路由,用 hash 去做,就会很麻烦,而且也很丑
- 插入一条新的 history 记录,用 pushState,不一定要改变 URL,而 hash 必须改变当前的 URL(精确地说是当前文档的 hash 值)
- 毫无疑问,我们需要把一些数据存储起来,在页面上提取,然后发起相应的 ajax。用 pushState 的方法,我们可以把数据存在 history.state 中,也可以根据 URL 去判断;而 hash 法只能改变 URL,根据 URL 判断(准确说是根据 hash 值判断)
- 目前浏览器还不支持 pushState 的 title 参数,一旦支持,就可以被利用;而 hash 法是无法改变 title 的。
pjax
上面只是个简单的例子,如果要是实际生产环境中使用,大可用封装过的插件。
pjax = pushState + ajax,GitHub 使用的就是封装过的 pjax 插件。
- https://github.com/defunkt/jquery-pjax (GitHub 使用)
- https://github.com/welefen/pjax (welefen 基于上面那个插件改造的,不过好像已经不维护了)
使用方式可以参照相应的 README,不多做介绍了。