cover-image 端午六一休息三天宅了一天, 独自去市中心里转了一天, 就剩下一天了[惊恐]. 压根就没有马上要考试了的感觉嘛! 文章也是好久没更新了, 浑浑噩噩过下去曾的好嘛>.O. 好嘛, 那就来写点干货好了, 也是最近在搞Ghost, 把这个主题的逻辑 (原先是 jQ ) 用原生 js 重写了一遍 (还顺便搞了个小项目 GhostBot 来处理站内搜索), 总结总结研究出了些什么好了, 废话讲得多, 直接进正题吧.

PJAX

关于题目抛出的这个问题, 我既然都这么问了, 正常人都知道答案肯定是不是咯. 其实AJAX这个东西一直以来都是被当做只能处理小范围的, 异步的页面更新, 它的优点就在于可以实现页面的无刷新操作. 但是这也会造成一些问题, 比如对搜索引擎兼容性不好啦 (抓不到 ajax 载入的信息), 还有无法后退神马的!

对于无法前进后退, 邪恶的 W3C 就在 HTML5 中搞了一个叫history对象的东西, 使用 history 对象的pushState方法给浏览器的浏览历史中塞入一个地址, 返回的时候再借助popstate事件来还原到原先的模样, 这样一来 AJAX 和 pushState 就开始名正言顺地搞基了! 于是我们就把这俩基友称为PJAX.

(正常情况下) 它是如何工作的

这个东西其实不是什么新生事物, 国内在 2012 年就已经对它有一些提及, 只是没有流行开而已, 经常上 GitHub 和 Youtube 的人不知道有没有发现这俩其实早就使用PJAX了.

HTML5 新增的方法在这里:

  • history.pushState(state, title [, url]) : 向历史记录(把它看做一个栈)的顶端加入一条记录, state 是一个指明状态的对象, 如 {url: 'xxxxx', title: 'yyyyy', myInfo: 'zzzzz'};
  • history.replaceState(state, title [, url]) : 跟上面差不多, 功能就是替换历史记录顶上的记录;
//当页面准备改变时
window.history.pushState({  
    title: 'Home Page',
    url: 'http://balabala.com'
}, document.title, location.href);

响应前进后退的事件:

  • window.onpopstate : 上面传递的state对象会成为event的子对象,这样就可以拿到存储的title和URL了. 这个事件在我所测试的 Chrome 36dev 和 Safari 7 中表现有差异, 具体在页面首次载入时, Chrome 不会触发这一事件而 Safari 会触发.
window.onpopstate = function (ev) {  
    var state = ev.state;
    if (state) {
        //do ajax here;
    }
}

国内也早就有很多开源的 PJAX 插件, 基于 jQuery, qwrap, kissy 什么的, 但是貌似我没有找到原生的啊, 反正到最后还是自己写了个渣渣的.

前端处理

首选我们需要一个容器 (container) 来做页面中需要改变的部分. 然后将不需要改变的部分, 比如 footer, header 放在容器的外面.

<style>  
#container{height: 100%;width: 100%;}
</style>  
<div id="container">{{#content}}</div>  
<div class="footer">{{#footer}}</div>  

对于页面上所有的超链接 (a标签), 我们都需要采取 hijack(劫持) 的方式来处理点击事件.

var a = document.getElementsByTagName('a');  
Array.prototype.slice.call(a).forEach(function (_a) {  
    _a.onclick = hijackLinks; //hijackLinks为链接处理函数
});
function hijackLinks (ev) {  
    if(/* 外域链接 */) return;
    ev.preventDefault();
    // 处理PJAX
}

差异于一般的 AJAX 返回 JSON, PJAX 的后端返回的一般是 HTML 片段. 前台再直接使用 innerHTML 来完成页面无刷. 对于同一个 URL, 如何告诉后端这是一个 PJAX 请求呢? So easy! 直接在 HTTP 请求头上带上标记就好了. 在前端 Ajax 中带上

setRequestHeader(‘PJAX’, ‘true’);  

后端处理

接上面的话, 直接在后台判断 HTTP 头就行了 (以 PHP 为例):

<?php  
if(array_key_exists('HTTP_X_PJAX', $_SERVER) && $_SERVER['HTTP_X_PJAX'] === 'true'){  
    //在这里输出你的 HTML 片段
}else{
    //正常输出
}

为什么要用它呢

简而言之, PJAX = AJAX + history.pushState. 这货不仅可以无刷新地改变页面内容, 还可以改变 URL 和浏览器历史记录, 使得前进后退的体验一体化.

Wow, 不明觉厉的样子也.

好吧, 那我们上个图来看看, 以 GitHub 为例.

如下图, 我首先打开了一个 (我的0.0) GitHub 主页, 可以看到浏览器的历史记录是空的 (灰色); pjax-before

然后我点击了 Contributions 按钮. 页面改变了, 但是页面并没有刷新, 看样子是 PJAX 基佬起作用了! 浏览器的历史记录里面出现了刚才访问的位置, 地址栏也变化了, 下面的调试窗口显示有三条 XHR (XMLHttpRequest) 请求成功. pjax-after

还是不明白? 自己去 GitHub.com 看个究竟把!

噢对了, 还没说为什么要用 PJAX 呢.

  1. 无刷新, 速度快 (快就是王道)
  2. 无刷新意味着可以在页面改变的时候施展各种绚丽的动画 (漂漂的, 暖暖的)
  3. 对搜索引擎支持好, 有利于 SEO
  4. 我™就是喜欢它怎么着, 打我呀

顺便说说弊端.

  1. 逻辑比较复杂. (跟正常方式不一样的都叫复杂)
  2. 需要前后台配合. (这点后面再讲, 其实可以克服)
  3. 跨域限制. (其实这是安全原则, 也可以不算弊端拉)
  4. 兼容性什么就不提了, 不用优雅的 Chrome 的都别来我的 blog 就对了.

我的方案 (不正常情况)

本站就使用了全站内链的 PJAX 无刷, 并开启了 10 分钟的 localStorage, 以尽量减少 HTTP 请求.

因为Ghost的主题只是提供了 hbs 模板文件和静态资源文件, 我并不能通过修改 Ghost 内核的方式来使得输出 HTML 片段. 所以只能通过解析 html 的方式来进行 (电脑牛逼, 不怕卡, 打我呀).

var ajax = new Ajax(); //我封装的一个Ajax类  
ajax.get(url, function (doc) {  
    var _html = document.createElement('html');
    _html.innerHTML = doc;          //不用append到文章中即可在内存中解析HTML
    var newContainer = _html.querySelector('#container');
    document.querySelector('#container').innerHTML = newContainer.innerHTML;
    //do others.
});

这样就减少了一个弊端, 就是可以不修改后台直接在前台做好所有的工作.

兼容性

直接看图吧

兼容性

  • IE10+
  • Chrome 5+
  • Safari 5+
  • Opera 11.5+
  • Firefox 4+

扫描二维码,分享此文章

Ling.'s Picture
Ling.

Web开发者. 前端,NodeJS 😈 大三, 找实习啦 ⬇️戳简历⬇️