摆脱在外部 JavaScript 文件中对 Web 应用程序的上下文路径进行硬编码

Get rid of hard-coding the context path of web apps in external JavaScript files

我有几个 WebSockets 端点,例如,

wss://localhost:8181/ContextPath/Push

所有此类端点 URL 都硬编码在单独的外部 JavaScript 文件 (.js) 中。这些 JavaScript 文件在需要时包含在各自的 XHTML 文件中。主机名和上下文路径应该以编程方式评估,而不是在需要它们的地方进行硬编码。

可以在JavaScript中使用document.location.host获取主机名(localhost:8181),但在JavaScript中没有standard/canonical获取上下文的方法应用程序运行的路径。


我正在做类似下面的事情。

全局JavaScript变量在主模板上声明如下。

<f:view locale="#{bean.locale}" encoding="UTF-8" contentType="text/html">
        <f:loadBundle basename="messages.ResourceBundle" var="messages"/>

        <ui:param name="contextPath" value="#{request.contextPath}"/>
        <ui:insert name="metaData"></ui:insert>

        <h:head>
            <script type="text/javascript">var contextPath = "#{contextPath}";</script>
        </h:head>

        <h:body id="body">

        </h:body>
    </f:view>
</html>

其中主机名和上下文路径被硬编码的JavaScript文件包含在各自的模板客户端或模板北、南、东、西的任何部分中,如下所示。

<html lang="#{bean.language}"
      xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html">

    <h:form>
        <h:outputScript library="default" name="js/websockets.js" target="head"/>
    </h:form>

仅出于观点考虑,websockets.js如下所示(您可以忽略它)。

if (window.WebSocket) {
    // The global variable "contextPath" is unavailable here
    // because it is declared afterwards in the generated HTML.
    var ws = new WebSocket("wss://"+document.location.host + contextPath + "/Push");
    ws.onmessage = function (event) {
        // This handler is invoked, when a message is received through a WebSockets channel.
    };

    $(window).on('beforeunload', function () {
        ws.close();
    });
} else {}

现在,主模板中声明的全局 JavaScript 变量 contextPath 预计在包含的 JavaScript 文件即 websockets.js 中可用。然而,这是不正确的。

所包含的 JavaScript 文件即 websockets.js 尝试访问全局变量 contextPath 的位置位于 之前 主模板中生成的 HTML <head> 标签中的硬编码 <script> 标签。

换句话说,全局JavaScript变量contextPath实际上是在被声明之前尝试在包含文件websockets.js中使用。

无论如何,如何摆脱在外部 JavaScript 文件中对上下文路径进行硬编码?

这样做的唯一目的是与 CSS 文件不同,EL 不在外部 JavaScript 文件中计算。因此,除非将它放在 XHTML 文件中,否则 #{} 不会起作用。

以下是您的选择吗?

  1. 在主模板中定义一个隐藏的 html-tag,例如:

    <span id="pageContextPath" data="#{contextPath}" style="display:none;"></span>
    
  2. 将您的 JavaScript 代码更改为:

    jQuery(document).ready(function ($) {
        if (window.WebSocket) {
            contextPath = $("#pageContextPath").attr("data");
            var ws = new WebSocket("wss://" + document.location.host + contextPath + "/Push");
            //...
        } else {}
    });
    

我这里用的是jQuery。你可以用普通的 JavaScript 重写它。但是应该在"document ready"之后进行,以确保隐藏的标签已经被渲染。否则js找不到那个元素。

What happens is that the included JavaScript file named websockets.js where the global variable contextPath is attempted to be accessed, is placed before the hard-coded <script> tag in the generated HTML <head> tag in the master template

这是出乎意料的。您在 <h:body> 中用 target="head" 声明了 <h:outputScript> 引用 websockets.js 文件。这应该在 已在 <h:head> 中声明的所有其他脚本资源之后结束。另见 a.o。 How to reference CSS / JS / image resource in Facelets template? After all, this appears to be caused by PrimeFaces bundled HeadRenderer 旨在自动包含一些 CSS 资源并处理 <facet name="first|middle|last">.

这值得向 PF 人员报告问题(如果尚未完成)。同时,最好的办法是通过在 faces-config.xml 中显式注册 JSF 实现自己的 HeadRenderer 来关闭它(前提是您使用的是 Mojarra)。

<render-kit>
    <renderer>
        <component-family>javax.faces.Output</component-family>
        <renderer-type>javax.faces.Head</renderer-type>
        <renderer-class>com.sun.faces.renderkit.html_basic.HeadRenderer</renderer-class>
    </renderer>
</render-kit>

并在 <h:head> 中明确包含 PrimeFaces 主题特定 theme.css

<h:outputStylesheet library="primefaces-aristo" name="theme.css" />

回到真正的问题,

Anyway, how to get rid of hard-coding the context path in external JavaScript files?

要么将其设置为基本 URI(注意:HTML4 / IE6-8 不支持相对路径)。

<h:head>
    <base href="#{request.contextPath}/" />
    ...
</h:head>
var baseURI = $("base").attr("href");

或者将其设置为 HTML 根元素的数据属性。

<!DOCTYPE html>
<html lang="en" data-baseuri="#{request.contextPath}/" ...>
    ...
</html>
var baseURI = $("html").data("baseuri");

与具体问题无关,作为建议,要透明地涵盖 http+ws 和 https+wss,请考虑使用 location.protocol 而不是硬编码wss.

var ws = new WebSocket(location.protocol.replace("http", "ws") + "//" + location.host + baseURI + "Push");