使用<o:deferredScript>
是的,可以使用自 OmniFaces 1.8.1 以来新增的 <o:deferredScript> 组件。对于技术上感兴趣的,这里是涉及的源代码:
基本上,组件将在postAddToView 事件期间(因此,在视图构建期间)通过UIViewRoot#addComponentResource() 将自身作为新的脚本资源添加到<body> 的末尾并通过Hacks#setScriptResourceRendered() 通知JSF 该脚本资源已经呈现(使用Hacks 类,因为没有标准的JSF API 方法(还没有?)),因此JSF 不会再强制自动包含/呈现脚本资源。对于 Mojarra 和 PrimeFaces,必须设置具有 name+library 键和 true 值的上下文属性,以禁用资源的自动包含。
渲染器将使用OmniFaces.DeferredScript.add() 写入<script> 元素,从而传递JSF 生成的资源URL。这个 JS 助手将依次收集资源 URL,并在 onload 事件期间为每个资源动态创建新的 <script> 元素。
用法相当简单,只需像<h:outputScript>一样使用<o:deferredScript>,加上library和name。将组件放置在哪里并不重要,但大多数自文档将在 <h:head> 的 end 中,如下所示:
<h:head>
...
<o:deferredScript library="libraryname" name="resourcename.js" />
</h:head>
您可以拥有多个它们,它们最终将按照它们声明的顺序加载。
如何在 PrimeFaces 中使用<o:deferredScript>?
这有点棘手,确实是因为所有由 PrimeFaces 生成的内联脚本,但仍然可以使用辅助脚本并接受 jquery.js 不会被推迟(它可以通过 CDN 提供服务,见下文)。为了覆盖那些几乎 220KiB 大的对 primefaces.js 文件的内联 PrimeFaces.xxx() 调用,需要创建一个小于 0.5KiB minified 的帮助脚本:
DeferredPrimeFaces = function() {
var deferredPrimeFaces = {};
var calls = [];
var settings = {};
var primeFacesLoaded = !!window.PrimeFaces;
function defer(name, args) {
calls.push({ name: name, args: args });
}
deferredPrimeFaces.begin = function() {
if (!primeFacesLoaded) {
settings = window.PrimeFaces.settings;
delete window.PrimeFaces;
}
};
deferredPrimeFaces.apply = function() {
if (window.PrimeFaces) {
for (var i = 0; i < calls.length; i++) {
window.PrimeFaces[calls[i].name].apply(window.PrimeFaces, calls[i].args);
}
window.PrimeFaces.settings = settings;
}
delete window.DeferredPrimeFaces;
};
if (!primeFacesLoaded) {
window.PrimeFaces = {
ab: function() { defer("ab", arguments); },
cw: function() { defer("cw", arguments); },
focus: function() { defer("focus", arguments); },
settings: {}
};
}
return deferredPrimeFaces;
}();
另存为/resources/yourapp/scripts/primefaces.deferred.js。基本上,它所做的就是捕获PrimeFaces.ab()、cw() 和focus() 调用(您可以在脚本底部找到)并将它们推迟到DeferredPrimeFaces.apply() 调用(因为您可以在脚本中途找到)。请注意,可能还有更多 PrimeFaces.xxx() 函数需要延迟,如果您的应用程序中出现这种情况,那么您可以自己将它们添加到 window.PrimeFaces = {} 中(不,在 JavaScript 中不可能有“包罗万象” " 方法来覆盖未确定的函数)。
在使用此脚本和<o:deferredScript> 之前,我们首先需要确定生成的 HTML 输出中自动包含的脚本。对于问题中显示的测试页面,以下脚本会自动包含在生成的 HTML <head> 中(您可以通过在 webbrowser 中右键单击该页面并选择 查看源代码来找到它):
<script type="text/javascript" src="/playground/javax.faces.resource/jquery/jquery.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/jquery/jquery-plugins.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/primefaces.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/layout/layout.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/watermark/watermark.js.xhtml?ln=primefaces&v=4.0"></script>
<script type="text/javascript" src="/playground/javax.faces.resource/fileupload/fileupload.js.xhtml?ln=primefaces&v=4.0"></script>
您需要跳过jquery.js 文件并以完全相同的顺序为其余脚本创建<o:deferredScripts>。资源名称是/javax.faces.resource/ 排除 JSF 映射之后的部分(在我的例子中是.xhtml)。库名由ln请求参数表示。
因此,应该这样做:
<h:head>
...
<h:outputScript library="yourapp" name="scripts/primefaces.deferred.js" target="head" />
<o:deferredScript library="primefaces" name="jquery/jquery-plugins.js" />
<o:deferredScript library="primefaces" name="primefaces.js" onbegin="DeferredPrimeFaces.begin()" />
<o:deferredScript library="primefaces" name="layout/layout.js" />
<o:deferredScript library="primefaces" name="watermark/watermark.js" />
<o:deferredScript library="primefaces" name="fileupload/fileupload.js" onsuccess="DeferredPrimeFaces.apply()" />
</h:head>
现在所有这些总大小约为 516KiB 的脚本都被推迟到 onload 事件。注意DeferredPrimeFaces.begin() 必须在<o:deferredScript name="primefaces.js"> 的onbegin 中调用,DeferredPrimeFaces.apply() 必须在最后一个 <o:deferredScript library="primefaces"> 的onsuccess 中调用。
如果您使用的是 PrimeFaces 6.0 或更高版本,其中 primefaces.js 已替换为 core.js 和 components.js,请改用以下内容:
<h:head>
...
<h:outputScript library="yourapp" name="scripts/primefaces.deferred.js" target="head" />
<o:deferredScript library="primefaces" name="jquery/jquery-plugins.js" />
<o:deferredScript library="primefaces" name="core.js" onbegin="DeferredPrimeFaces.begin()" />
<o:deferredScript library="primefaces" name="components.js" />
<o:deferredScript library="primefaces" name="layout/layout.js" />
<o:deferredScript library="primefaces" name="watermark/watermark.js" />
<o:deferredScript library="primefaces" name="fileupload/fileupload.js" onsuccess="DeferredPrimeFaces.apply()" />
</h:head>
关于性能提升,重要的测量点是DOMContentLoaded 时间,您可以在 Chrome 开发者工具的 Network 标签底部找到。使用 Tomcat 在 3 年旧笔记本电脑上提供的问题中所示的测试页面,它从 ~500ms 减少到 ~270ms。这是相对较大的(几乎是一半!),并且在手机/平板电脑上的差异最大,因为它们呈现 HTML 相对较慢,并且触摸事件在加载 DOM 内容之前被完全阻止。
请注意,您是否使用(自定义)组件库取决于它们是否遵守 JSF 资源管理规则/指南。例如,RichFaces 并没有在其上自制另一个自定义层,因此无法在其上使用 <o:deferredScript>。另见what is the resource library and how should it be used?
警告:如果您之后在同一个视图上添加新的 PrimeFaces 组件并且遇到 JavaScript undefined 错误,那么新组件也有自己的 JS 文件的可能性很大这也应该被推迟,因为它取决于primefaces.js。找出正确脚本的一种快速方法是检查生成的 HTML <head> 是否有新脚本,然后根据上述说明为其添加另一个 <o:deferredScript>。
奖励:CombinedResourceHandler 识别 <o:deferredScript>
如果您碰巧使用了 OmniFaces CombinedResourceHandler,那么很高兴知道它可以透明地识别 <o:deferredScript> 并将所有具有相同 group 属性的延迟脚本组合成一个单一的延迟资源。例如。这……
<o:deferredScript group="essential" ... />
<o:deferredScript group="essential" ... />
<o:deferredScript group="essential" ... />
...
<o:deferredScript group="non-essential" ... />
<o:deferredScript group="non-essential" ... />
... 最终会生成两个组合的延迟脚本,它们彼此同步加载。注意:group 属性是可选的。如果您没有任何资源,那么它们将全部合并为一个延迟资源。
作为一个实际示例,请查看ZEEF 网站的<body> 底部。所有基本的 PrimeFaces 相关脚本和一些特定于站点的脚本都组合在第一个延迟脚本中,所有非必要的社交媒体相关脚本组合在第二个延迟脚本中。至于 ZEEF 的性能提升,在现代硬件上的测试 JBoss EAP 服务器上,DOMContentLoaded 的时间从 ~3s 变为 ~1s。
奖励 #2:将 PrimeFaces jQuery 委托给 CDN
无论如何,如果您已经在使用 OmniFaces,那么您始终可以使用 CDNResourceHandler 通过 web.xml 中的以下上下文参数将 PrimeFaces jQuery 资源委托给真正的 CDN:
<context-param>
<param-name>org.omnifaces.CDN_RESOURCE_HANDLER_URLS</param-name>
<param-value>primefaces:jquery/jquery.js=http://code.jquery.com/jquery-1.11.0.min.js</param-value>
</context-param>
请注意,与 PrimeFaces 4.0 内部使用的 1.10 相比,jQuery 1.11 有一些主要的性能改进,并且完全向后兼容。在 ZEEF 上初始化拖放时节省了几百毫秒。