Ajax是Web开发非常重要的工具,只不过有一点单纯拿AJAX来请求一个页面时特别是单页应用时,无法正常使用浏览器历史记录、后退、刷新时同时替换文档标题。
而pjax就是用来解决这个问题,使用的是jQuery ajax 和 pushState(查看支持pushState浏览器版本情况)。
一、pushState、replaceState
pushState往历史堆栈的顶部添加一条记录,replaceState更改当前历史记录,包括三个参数:
- data为一个可以被解析的对象或null,不能够是一个jQuery对象。在触发popstate事件时,会做为state参数传递过去;
- title为页面的标题;
- url为页面的URL,不写则为当前页。
二、pjax应用
pjax并不是完全自动的,需要自己调用。
首先:需要给页面设定一下被替换的位置,例:
<!DOCTYPE html>
<html>
<head>
<!-- styles, scripts, etc -->
</head>
<body>
<h1>My Site</h1>
<div class="container" id="pjax-container">
Go to <a href="/page/2">next page</a>.
</div>
</body>
</html>
如果我们希望点击 /page/2
链接,会将 /page/2
HTML代码替换到 #pjax-container
容器中,则还需要告知pjax监听 a
标签。
$(document).pjax('a', '#pjax-container')
以上是一个完整的例子;如果你希望跟Unobtrusive JavaScript兼容,那么可以尝试使用符合Unobtrusive JavaScript写法,当然并不能完整的利用data-ajax-loading、data-ajax-mode、data-ajax-update属性。
每个PJAX请求都会自动加入一个 X-PJAX
请求头,以便于我们可以来判断是否需要引入 layout
文件,这里我是通过一个Filter来判断:
public class PJaxFilter : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { base.OnActionExecuting(filterContext); filterContext.Controller.ViewBag.layout = "~/Views/Shared/_Layout.cshtml"; string pjaxHeader = filterContext.RequestContext.HttpContext.Request.Headers["X-PJAX"]; if (!String.IsNullOrEmpty(pjaxHeader)) filterContext.Controller.ViewBag.layout = null; } }
cshtml页面对于布局需要根据 @{ Layout = ViewBag.layout; }
来,这样当我们直接通过 /page/2
请求页面时也能够正常显示。
页面标题可以通过在HTML中输出 <title>页面标题</title>
,pjax会自动获取。
pjax也提供一系列事件,比如 pjax:send
和 pjax:complete
来处理加载状态:
$(document).on('pjax:send', function() { $('#loading').show() }) $(document).on('pjax:complete', function() { $('#loading').hide() })
二、pjax原理
pjax的AJAX请求是基于jquery的ajax,所以pjax的options参数大部分才是jquery ajax请求的原样参数。
1、每次PAJX发生请求时会先通过 pushState
将请求页压入历史堆栈的顶部。
if (xhr.readyState > 0) { if (options.push && !options.replace) { // Cache current container element before replacing it cachePush(pjax.state.id, context.clone().contents()) window.history.pushState(null, "", stripPjaxParam(options.requestUrl)) } fire('pjax:start', [xhr, options]) fire('pjax:send', [xhr, options]) }
2、请求成功后会:替换当前历史记录页、修改页面标题、把HTML写入相应容器当中。
if (options.push || options.replace) { window.history.replaceState(pjax.state, container.title, container.url) } // Clear out any focused controls before inserting new page contents. try { document.activeElement.blur() } catch (e) { } // 这里的页面标题,会依次按<head><title /></head>、按Response片断根节点的title属性、Response带有<title />。 if (container.title) document.title = container.title fire('pjax:beforeReplace', [container.contents, options], { state: pjax.state, previousState: previousState }) context.html(container.contents)
这里要特意提一下pjax对脚本的处理方式。
因为 .html()
是基于innerHTML来改变容器代码,所以如果在页面使用 document.write
变成很不可取的方式,因为不同版本浏览器解析会有不可预知的情况发生,参考。
还有针对 <script src="" />
的处理是将未加载过的脚本写入到 <head>
中。
这里pjax的顺序是先写入HTML,再加载script,这好处就是避免DOM渲染和脚本资源请求受阻,倒置页面假死。
所以这里要特别注意加载顺序问题,如果某个页面每次请求都需要引用外部脚本,建议用 getScript。
3、监控window.onpopstate事件,后退时重新发送AJAX请求。
pjax会先从缓存中获取数据,而缓存并非无限制的,可以通过调用 pjax 时指定 maxCacheLength 参数大小(默认:20);和重新发送请求。
详见 onPjaxPopstate
函数。
4、重新加载布局
我们可以重新指定Response的请求头pjax版本号,来强制重新刷新页面。
if request.headers['X-PJAX'] response.headers['X-PJAX-Version'] = "v123" end
或html代码加入:
<meta http-equiv="x-pjax-version" content="v123">
其实pjax和模块化编程(例:require.js)真的是天生一对,之前有好几个小项目已经做了大量开发,效率、用户体验真的上升一个档次。
以上。
发表评论