pjax使用小结

作者: anyesu | 来源:发表于2016-12-30 00:46 被阅读15209次
pjax

前言


上周看到一篇文章在分析简书 我的主页 页面3个tab页切换的bug,起先以为是寻常的样式bug而已没怎么在意,后来在文章中看到 pjax 这个术语,长得和 ajax 有点像,遂去了解了下。

简介


虽然传统的ajax方式可以异步无刷新改变页面内容,但无法改变页面URL,因此有种方案是在内容发生改变后通过改变URL的hash的方式获得更好的可访问性(如 https://liyu365.github.io/BG-UI/tpl/#page/desktop.html),但是 hash 的方式有时候不能很好的处理浏览器的前进、后退,而且常规代码要切换到这种方式还要做不少额外的处理。而 pjax 的出现就是为了解决这些问题,简单的说就是对 ajax 的加强。

pjax结合pushState和ajax技术, 不需要重新加载整个页面就能从服务器加载Html到你当前页面,这个ajax请求会有永久链接、title并支持浏览器的回退/前进按钮。

pjax项目地址在 https://github.com/defunkt/jquery-pjax 。 实际的效果见: http://pjax.herokuapp.com 没有勾选 pjax 的时候点击链接是跳转的, 勾选了之后链接都是变成了 ajax 刷新(实际效果如下图的请求内容对比)。

不使用pjax 使用pjax
优点:
  • 减轻服务端压力

按需请求,每次只需加载页面的部分内容,而不用重复加载一些公共的资源文件和不变的页面结构,大大减小了数据请求量,以减轻对服务器的带宽和性能压力,还大大提升了页面的加载速度。

  • 优化页面跳转体验

常规页面跳转需要重新加载画面上的内容,会有明显的闪烁,而且往往和跳转前的页面没有连贯性,用户体验不是很好。如果再遇上页面比较庞大、网速又不是很好的情况,用户体验就更加雪上加霜了。使用pjax后,由于只刷新部分页面,切换效果更加流畅,而且可以定制过度动画,在等待页面加载的时候体验就比较舒服了。

缺点:
  • 不支持一些低版本的浏览器(如IE系列)

pjax使用了pushState来改变地址栏的url,这是html5中history的新特性,在某些旧版浏览器中可能不支持。不过pjax会进行判断,功能不适用的时候会执行默认的页面跳转操作。

  • 使服务端处理变得复杂

要做到普通请求返回完整页面,而pjax请求只返回部分页面,服务端就需要做一些特殊处理,当然这对于设计良好的后端框架来说,添加一些统一处理还是比较容易的,自然也没太大问题。另外,即使后台不做处理,设置pjax的fragment参数来达到同样的效果。

综合来看,pajx的优点很强势,缺点也几乎可以忽略,还是非常值得推荐的,尤其是类似博客这种大部分情况下只有主体内容变化的网站。关键它使用简单、学习成本小,即时全站只有极个别页面能用得到,尝试下没什么损失。pjax的github主页介绍的已经很详细了,想了解更多可以看下源码。

用法


  1. 引入 jquery 和 jquery.pjax.js
  2. 注册事件
/**
  * 方式一 按钮父节点监听事件
  *
  * @param selector  触发点击事件的按钮
  * @param container 展示刷新内容的容器,也就是会被替换的部分
  * @param options   参数
  */
$(document).pjax(selector, [container], options);

// 方式二 直接对按钮监听,可以不用指定容器,使用按钮的data-pjax属性值查找容器
$("a[data-pjax]").pjax();

// 方式三 常规的点击事件监听方式
$(document).on('click', 'a', $.pjax.click);
$(document).on('click', 'a', function(event) {
    var container = $(this).closest('[data-pjax-container]');
    $.pjax.click(event, container);
});

// 下列是源码中介绍的其他用法,由于本人暂时没有那些需求暂时没深究,有兴趣的各位自己试试看哈
// 表单提交
$(document).on('submit', 'form', function(event) {
    var container = $(this).closest('[data-pjax-container]');
    $.pjax.submit(event, container);
});
// 加载内容到指定容器
$.pjax({ url: this.href, container: '#main' });
// 重新当前页面容器的内容
$.pjax.reload('#container');

options默认参数说明


参数名 默认值 说明
timeout 650 ajax 超时时间(单位 ms),超时后会执行默认的页面跳转,所以超时时间不应过短,不过一般不需要设置
push true 使用 window.history.pushState 改变地址栏 url(会添加新的历史记录)
replace false 使用 window.history.replaceState 改变地址栏 url(不会添加历史记录)
maxCacheLength 20 缓存的历史页面个数(pjax 加载新页面前会把原页面的内容缓存起来,缓存加载后其中的脚本会再次执行)
version 是一个函数,返回当前页面的pjax-version,即页面中 <meta http-equiv="x-pjax-version"> 标签内容。使用 response.setHeader("X-PJAX-Version", "") 设置与当前页面不同的版本号,可强制页面跳转而不是局部刷新。
scrollTo 0 页面加载后垂直滚动距离(与原页面保持一致可使过度效果更平滑)
type "GET" ajax 的参数,http 请求方式
dataType "html" ajax的参数,响应内容的 Content-Type
container 用于查找容器的CSS选择器,[container]参数没有指定时使用
url link.href 要跳转的连接,默认a标签的href属性
target link pjax事件参数e的 relatedTarget 属性,默认为点击的 a标签
fragment 使用响应内容的指定部分(css选择器)填充页面,服务端不进行处理导致全页面请求的时候需要使用该参数,简单的说就是对请求到的页面做截取

除了上述参数外,ajax的一些参数也是可以设置在这里的,不过一般没什么必要。

// ajax 最终参数: 
options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options);

pjax失效情况


会有一些情况导致pjax失效,下面结合源码分析下(省略部分无关代码)

function handleClick(event, container, options) {
    ...
    
    // 1. 点击事件的事件源不是a标签。使用a标签可以做到对旧版本浏览器的兼容,所以不建议使用其他标签注册事件
    if (link.tagName.toUpperCase() !== 'A')
        throw "$.fn.pjax or $.pjax.click requires an anchor element"

    // 2. 使用鼠标滚轮点击(新标签页打开)
    // 点击超链接的同时按下Shift、Ctrl、Alt和Meta(在Windows键盘中是Windows键,在苹果机中是Cmd键)
    // 作用分别代表新窗口打开、新标签打开(不切换标签)、下载、新标签打开(切换标签)
    if (event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
        return

    // 3. 跨域(网络通讯协议,域名不一致)
    if (location.protocol !== link.protocol || location.hostname !== link.hostname)
        return

    // 4. 当前页面的锚点定位
    if (link.href.indexOf('#') > -1 && stripHash(link) == stripHash(location))
        return

    // 5. 已经阻止元素发生默认的行为(url跳转)
    if (event.isDefaultPrevented())
        return

    ...

    var clickEvent = $.Event('pjax:click')
    $(link).trigger(clickEvent, [opts])

    // 6. pjax:click事件回调中已经阻止元素发生默认的行为(url跳转)
    if (!clickEvent.isDefaultPrevented()) {
        pjax(opts)
        event.preventDefault()// 阻止url跳转
        $(link).trigger('pjax:clicked', [opts])
    }
}

除了上述情况之外,还有下列几种情况:

  • ajax 请求失败,或者timeout后请求被中止
  • 当前页面的 X-PJAX-Version 和请求的新页面版本不一致
  • 请求得到完整的页面(包含html标签)却没设置 fragment 参数

事件


1. 点击链接后触发的一系列事件, 除了 pjax:clickpjax:clicked 的事件源是点击的按钮,其他事件的事件源都是要替换内容的容器。可以在 pjax:start 事件触发时开始过度动画,在 pjax:end 事件触发时结束过度动画。
事件名 支持取消 参数 说明
pjax:click options 点击按钮时触发。可调用 e.preventDefault(); 取消pjax
pjax:beforeSend xhr, options ajax 执行 beforeSend 函数时触发,可在回调函数中设置额外的请求头参数。可调用 e.preventDefault(); 取消 pjax
pjax:start xhr, options pjax开始(与服务器连接建立后触发)
pjax:send xhr, options pjax:start 之后触发
pjax:clicked options ajax 请求开始后触发
pjax:beforeReplace contents, options ajax 请求成功,内容替换渲染前触发
pjax:success data, status, xhr, options 内容替换成功后触发
pjax:timeout xhr, options ajax请求超时后触发。可调用 e.preventDefault(); 继续等待 ajax 请求结束
pjax:error xhr, textStatus, error, options ajax 请求失败后触发。默认失败后会跳转url,如要阻止跳转可调用 e.preventDefault();
pjax:complete xhr, textStatus, options ajax 请求结束后触发,不管成功还是失败
pjax:end xhr, options pjax 所有事件结束后触发
  • 注意:
    pjax:beforeReplace 事件前 pjax 会调用 extractContainer 函数处理页面内容,即以 script[src] 的形式引入的 js 脚本不会被重复加载,有必要可以改下源码
2. 浏览器前进/后退导航时触发的事件(暂时没做过多研究)
事件名 参数 说明
pjax:popstate 页面导航方向: 'forward'/'back'(前进/后退)
pjax:start null, options pjax 开始
pjax:beforeReplace contents, options 内容替换渲染前触发,如果缓存了要导航页面的内容则使用缓存,否则使用 pjax 加载
pjax:end null, options pjax 结束

服务端配置


我的项目是 Spring MVC + velocity 的组合,这里就以此为例子,其他语言和框架的服务端可以参考下这里的思路。
项目中使用的视图解析器是 org.springframework.web.servlet.view.velocity.VelocityLayoutViewResolver 这个类,好处是可以使用模版技术,每个页面可以只写主体内容,公共部分统一写在模版里面,是不是和 pjax 绝配哈!pjax.js 默认会在请求头加入 X_PJAX 字段,并置为 true,所以以此来判断是否 pjax 请求。对于普通的请求使用常规的模版,pjax 请求则使用空模版或者特定的模版。

  • 常规模版内容:
<!doctype html>
<html>
    #set($basePath = "screen/contain")
    <head>
        <meta http-equiv="x-pjax-version" content="$!{X-PJAX-Version}"/>
        #parse("$basePath/html-head.vm")
    </head>
    <body>
        <section id="container">
            #parse("$basePath/frame-head.vm")
            #parse("$basePath/frame-left.vm")
            <section id="main-content">
                <section class="wrapper">
                    $screen_content ##页面内容
                </section>
            </section>
            #parse("$basePath/frame-bottom.vm")
        </section>
    </body>
</html>
  • 添加 SpringMVC 中的 Interceptor 拦截器,用于后端渲染前插入 pjax 处理
public class PjaxInterceptor extends HandlerInterceptorAdapter {

    @Value("${X-PJAX-Version}")
    private String X_PJAX_VERSION;

    /**
     * Controller 方法调用之后,页面渲染前执行
     * 
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (modelAndView != null) {
            boolean isPajx = Boolean.parseBoolean(request.getHeader("X-PJAX"));// 值为true表示pjax请求,这是重点
            ModelMap model = modelAndView.getModelMap();
            model.addAttribute("X-PJAX-Version", X_PJAX_VERSION);// 设置当前页面的pjax版本
            if (isPajx) {
                model.addAttribute("layout", "layout_pjax.vm");// 指定pjax请求时使用的模版
                // 在vm页面中通过 #set($layout = 'xxx.vm') 的方式指定模版
                response.setHeader("X-PJAX-Version", X_PJAX_VERSION);// 响应内容的pjax版本,有新模版发布时,通过配置文件修改版本来强制页面刷新
            }
        }
    }
}
  • xml配置
<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean id="pjaxInterceptor" class="xxx.PjaxInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>
  • pjax请求模版页面:layout_pjax.vm
<title>$!{title}</title>
$screen_content

模版中使用 title 标签,这样执行 pjax 请求时不仅地址栏 url 会变化,而且浏览器标签的标题内容也会变化。

针对没有服务端处理的方案如下:

// fragment一般同container一致
$(document).pjax('a[data-pjax]', '#main-content .wrapper', {fragment: '#main-content .wrapper'});

插件伴侣——NProgress


比较漂亮的一款进度条插件,用法十分简单,很适合做pjax的过度动画,详细用法在该项目github上有介绍

NProgress
  • 示例:
$(document).on('pjax:start', NProgress.start).on('pjax:end', NProgress.done);

结语


虽然个人还是比较喜欢造轮子(有成就感),不怎么喜欢用插件(一般插件使用复杂,文档少学习成本大,还不如自己写),但看了 pjax 的源码后感觉真要自己也使用 pushState + ajax 的方式简单的实现它的功能,还是要踩不少坑的,所以为什么要放着这么个易用又精致的小轮子不用呢?我的项目是一个管理系统,统一的 左侧菜单 + 右侧table 的布局,每个页面都需要一个独立访问的url,非常适合使用 pjax。由于使用的 velocity 模版技术,集成 pjax 就是分分钟的事,不仅对原先的代码完全没影响,还提升了加载速度,页面过度效果更好,再用上了 NProgress,感觉逼格又上升不少,哈哈。

前段时间工作比较忙好久没写文章了,这段时间有点闲下来就抽空学了些新东西记录下,对于这次的学习成果还是比较满意的。( *_* )

相关文章

网友评论

  • 3accb31faf03:楼主,pjax下的meta是固定的,那对跨境电商的seo的影响大吗
    anyesu:@3accb31faf03 对于爬虫来说,应该只会当作普通的超链接来解析,pjax应该是不起作用的
    anyesu:@3accb31faf03 如果你想变meta可以修改pjax源码,参考替换title的方式替换meta。然后pjax子页面中带上对应的meta内容。
    anyesu:@3accb31faf03 seo这块我不是很熟,我们只有首页设了keywords,可能我们是假的跨境电商吧:joy:
  • Dany__:学习了 , 优点这么突出,为这么没见周围的人用这呢
    anyesu:@Dany__ 见过很多网站都是用锚点的方式自己实现一套类似的效果。没用pjax一方面可能是没人推广,另一方面可能是和他们的业务场景、前端框架不太兼容
  • 无技之谈:楼主,有个以为,Pjax可以在静态页面使用吗?还是说必须需要后端处理?
    如果静态页面可以使用,使用以下这段代码没有效果
    $(document).pjax('a[data-pjax]', '#main-content .wrapper', {fragment: '#main-content .wrapper'});
    无技之谈:@anyesu 明白了,已经调通,感谢楼主!:)
    anyesu:@ray_1001 后端不处理没问题的,就是请求的是完整页面,数据量大一点,会根据fragment设置的区域做截取,把当前页面的main-content中的内容替换为新页面的main-content中的内容。如果没生效,1.确认下新旧页面是否有对应的节点;2.F12看下是否有报错,看下ajax请求的响应内容是否正确
  • 95年的志远:博主的教程很好,不过是否缺少一个预览版?我新做的网站正好使用了 Pjax + NProgress,如果没有了解的同学可以参考下我这个网站 http://www.ivusic.com
    anyesu:@做最好的音乐站 界面还不错,不过提几点bug哈:1. 虽然用了pjax但后端未处理还是全页面加载,而且存在事件重复绑定;2. favicon.ico用的是相对路径,点击二级菜单会无法加载;3. 音乐播放:chrome下播放一声就卡住了,IE下时好时坏(可能是加载太慢)
  • 非也缘也:请问。这个技术,刷新页面后,页面内容会有缓存么?还是初始化了?
    anyesu:@非也缘也 刷新就重新加载了,前进后退才有缓存
  • Leaves丶幻:您好,我想请问一下,我使用pjax写的页面点击浏览器的前进后退和刷新按钮,pjax不能重新加载主框架的js和css,依旧是进行了局部加载数据,应该怎么解决
    anyesu:@Leaves丶幻 页面前进后退时如果需要对框架主体做修改,可以把相应的处理添加到 `pjax:popstate` 事件的回调中。示例:
    function onPjaxPopstate(event) {
    console.log(event.direction);
    // do something
    }
    $(document).on('pjax:popstate', onPjaxPopstate);
    Leaves丶幻:@anyesu 我现在是在pjax中进行事件处理,尝试对浏览器的前进后退刷新及按F5时进行相应的处理,目前还没有成功,0.o
    anyesu:@Leaves丶幻 一种方案是把需要重新加载的js包含在局部数据中,每次执行。
    另一种方案是在pjax事件中进行处理,参考文中的事件部分。
    具体操作可能会复杂很多,需要不断尝试。其实如果决定使用pjax实现页面骨架就需要在功能上做一点让步,制定一些规范,极个别的页面可以特殊处理下
  • 林Vic:请问一下,为什么用了这个之后切换页面。切换完页面页面绑定的事件都失效了
    c7b893401922:@anyesu 刷新后页面导航消失,怎么解决?
    彼岸青园:@anyesu 解析的很清楚!感谢
    anyesu:@林Vic 应该是绑定事件的dom节点移除了,新加的dom节点没重新绑事件。可以把事件绑父节点上比如document,或者在异步刷新的内容加入绑事件的代码,不过这样做很容易重复执行一些js,需要自己特殊处理下
  • Blacker丶Boom:学习了:+1:

本文标题:pjax使用小结

本文链接:https://www.haomeiwen.com/subject/czalvttx.html