繁体   English   中英

从p:remoteCommand的oncomplete处理函数调用JavaScript函数-使用一些JavaScript代码对其进行仿真

[英]Invoking a JavaScript function from oncomplete handler of p:remoteCommand - simulating the same using some JavaScript code

注意:尽管此问题涵盖了很长的文本信息以及<p:remoteCommand> Java代码片段,但它仅针对JavaScript / jQuery和一些PrimeFaces东西(只是<p:remoteCommand> ),如开头部分的介绍中所述。


我从WebSockets(Java EE 7 / JSR 356 WebSocket API)收到一条JSON消息,如下所示。

if (window.WebSocket) {
    var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");

    ws.onmessage = function (event) {
        jsonMsg=event.data;
        var json = JSON.parse(jsonMsg);        
        var msg=json["jsonMessage"];

        if (window[msg]) {
            window[msg](); //It is literally interpreted as a function - updateModel();
        }
    };
}

在上面的代码中, event.data包含一个JSON字符串{"jsonMessage":"updateModel"} 因此, msg将包含一个字符串值updateModel

在以下代码段中,

if (window[msg]) {
    window[msg](); //It is literally interpreted as a JavaScript function - updateModel();
}

window[msg](); 导致与<p:remoteCommand>关联的JavaScript函数被调用(进而调用与<p:remoteCommand>关联的actionListener="#{bean.remoteAction}" )。

<p:remoteCommand name="updateModel"
                 actionListener="#{bean.remoteAction}" 
                 oncomplete="notifyAll()"
                 process="@this"
                 update="@none"/>

不一定需要update="@none"


收到此消息后,我需要通知所有相关的客户端此更新。 我使用以下JavaScript函数来实现此功能,该功能与上述<p:remoteCommand>oncomplete处理程序相关联。

var jsonMsg;

function notifyAll() {
    if(jsonMsg) {
       sendMessage(jsonMsg);
    }
}

请注意,变量jsonMsg已在第一个代码段中分配了一个值-它是全局变量。 sendMessage()是另一个JavaScript函数,该函数实际上通过WebSockets向所有关联的客户端发送有关此更新的通知,此问题不需要此通知。


这很好用,但是在以下情况下有办法做一些魔术

if (window[msg]) {
    window[msg]();

    //Do something to call notifyAll() on oncomplete of remote command.
}

这样,可以直接通过某些JavaScript代码(当前附加到<p:remoteCommand> oncomplete上的预期代码,而预期的JavaScript代码(或其他内容)应该模拟此oncomplete )直接调用notifyAll() ,从而基本上无需依赖在全局JavaScript变量( jsonMSg )上?


编辑:我正在尝试解决的问题(它可能被视为其他信息)。

例如,当管理员对名为Category的JPA实体进行一些更改(通过DML操作)时,将触发实体侦听器,进而导致引发CDI事件,如下所示。

@ApplicationScoped
public class CategoryListener {

    @PostPersist
    @PostUpdate
    @PostRemove
    public void onChange(Category category) throws NamingException {
        BeanManager beanManager = (BeanManager) InitialContext.doLookup("java:comp/BeanManager");
        beanManager.fireEvent(new CategoryChangeEvent(category));
    }
}

不用说,实体Category是通过注释@EntityListeners(CategoryListener.class)来指定的。

只是一个旁注( 完全不在主题之内 ):如前面的代码片段所述,通过JNDI查找获取BeanManager的实例是临时的。 具有Weld版本2.2.2最终版本的GlassFish Server 4.1无法注入CDI事件javax.enterprise.event.Event<T> ,该事件javax.enterprise.event.Event<T>如下方式注入。

@Inject
private Event<CategoryChangeEvent> event;

然后,可以参考上面的相关代码片段按如下所述触发事件。

event.fire(new CategoryChangeEvent(category));


在Web项目中观察到此事件,如下所示。

@ApplicationScoped
public class RealTimeUpdate {

    public void onCategoryChange(@Observes CategoryChangeEvent event) {
        AdminPush.sendAll("updateModel");
    }
}

管理员按如下方式使用自己的端点( AdminPush.sendAll("updateModel");在其中手动调用)。

@ServerEndpoint(value = "/AdminPush", configurator = ServletAwareConfig.class)
public final class AdminPush {

    private static final Set<Session> sessions = new LinkedHashSet<Session>();

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        if (Boolean.valueOf((String) config.getUserProperties().get("isAdmin"))) {
            sessions.add(session);
        }
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
    }

    private static JsonObject createJsonMessage(String message) {
        return JsonProvider.provider().createObjectBuilder().add("jsonMessage", message).build();
    }

    public static void sendAll(String text) {
        synchronized (sessions) {
            String message = createJsonMessage(text).toString();
            for (Session session : sessions) {
                if (session.isOpen()) {
                    session.getAsyncRemote().sendText(message);
                }
            }
        }
    }
}

在这里,只有管理员可以使用此端点。 使用onOpen()方法中的条件检查可防止所有其他用户创建WebSocket会话。

session.getAsyncRemote().sendText(message); foreach循环内,会向管理员发送有关实体Category所做的这些更改的通知(以JSON消息的形式)。

如第一个代码段所示, window[msg](); 调用与应用程序作用域Bean相关联的操作方法(通过<p:remoteCommand> (如前所示) actionListener="#{realTimeMenuManagedBean.remoteAction}"

@Named
@ApplicationScoped
public class RealTimeMenuManagedBean {

    @Inject
    private ParentMenuBeanLocal service;

    private List<Category> category;
    private final Map<Long, List<SubCategory>> categoryMap = new LinkedHashMap<Long, List<SubCategory>>();
    // Other lists and maps as and when required for a dynamic CSS menu.

    public RealTimeMenuManagedBean() {}

    @PostConstruct
    private void init() {
        populate();
    }

    private void populate() {
        categoryMap.clear();
        category = service.getCategoryList();

        for (Category c : category) {
            Long catId = c.getCatId();
            categoryMap.put(catId, service.getSubCategoryList(catId));
        }
    }

    // This method is invoked through the above-mentioned <p:remoteCommand>.
    public void remoteAction() {
        populate();
    }

    // Necessary accessor methods only.
}

仅当actionListener="#{realTimeMenuManagedBean.remoteAction}"完全完成时,才应通知所有其他用户/客户端(在其他面板上,而不在管理面板上)-应该在操作方法完成之前发生-通过<p:remoteCommand>oncomplate事件处理程序通知。 这就是为什么要采用两个不同的端点的原因。


这些其他用户使用他们自己的端点:

@ServerEndpoint("/Push")
public final class Push {

    private static final Set<Session> sessions = new LinkedHashSet<Session>();

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
    }

    @OnMessage
    public void onMessage(String text) {
        synchronized (sessions) {
            for (Session session : sessions) {
                if (session.isOpen()) {
                    session.getAsyncRemote().sendText(text);
                }
            }
        }
    }
}

当通过<p:remoteCommand> oncomplete发送消息时,使用@OnMessage注释的方法开始播放,如上所示。

这些客户端使用下面的JavaScript代码从上述应用程序范围的Bean中获取新值(管理员已经从数据库中充分查询了该Bean。因此,无需每个人都再次荒谬地对其进行查询。单独的客户端(而不是管理员),因此,它是一个应用程序范围的bean)。

if (window.WebSocket) {
    var ws = new WebSocket("wss://localhost:8181/ContextPath/Push");
    ws.onmessage = function (event) {
        var json = JSON.parse(event.data);
        var msg = json["jsonMessage"];

        if (window[msg]) {
            window[msg]();
        }
    };

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

结合以下<p:remoteCommand>

<p:remoteCommand name="updateModel"
                 process="@this"
                 update="parentMenu"/>

其中parentMenu要由此<p:remoteCommand>更新的组件是容器JSF组件<h:panelGroup>id ,该组件包含带有一堆<ui:repeat>的纯CSS菜单。

希望这可以使情况更加清晰。


更新:

这个问题已经被准确地回答了在这里根据<p:remoteCommand>至于具体问题,唯一的问题是,在一个全球性的JavaScript变量删除的依赖在这个问题的介绍部分所述)。

我认为我不了解您问题的各个方面,但是无论如何,我都会尽力帮助您。 请注意,我不了解PrimeFaces,所以我所做的只是阅读文档。

我了解的是,您尝试摆脱全局变量。 但是,恐怕我认为这是不可能的。

这里的问题是,PrimeFaces不允许您透明地将某些内容从远程调用进一步传递给oncomplete调用(除非您将其传递给Bean的Java代码,然后再传递给UI,通常是不是您想要的)。

但是,我希望您可以非常接近它。

第1部分,JS早日返回

另请注意,关于Java和JavaScript可能存在一些误解。

Java是多线程的,可以并行运行多个命令,而JavaScript是单线程的,通常从不等待完成。 为了获得响应式Web UI,必须异步执行操作。

因此,您的remoteCommand调用(从JS端看到)将(通常是异步情况)返回很久,然后才会调用oncomplete处理程序。 这意味着,如果window[msg]()返回,则说明还没有完成remoteCommand

因此,您要使用以下代码进行管理

if (window[msg]) {
    window[msg]();

    //Do something to call notifyAll() on oncomplete of remote command.
    dosomethinghere();
}

将失败。 remoteCommand返回时, remoteCommand dosomethinghere()将不会被调用(因为JS不想等待某些事件,这可能永远不会发生)。 这意味着,当Ajax请求刚刚向远程(向Java应用程序)打开时,将调用dosomethinghere() )。

要在Ajax调用完成后运行某些东西,必须在oncomplete例程(或onsuccess )中完成。 这就是为什么它在那里。

第2部分,验证msg

请注意关于window[msg]()一些不同之处。 如果您不能完全信任所推送的消息,这将被认为有些危险。 window[msg]()本质上运行任何以msg变量的内容命名的函数。 例如,如果msg恰好close则将运行window.close() ,这可能不是您想要的。

您应该确保msg是一个期望的词,并拒绝所有其他词。 示例代码:

var validmsg = { updateModel:1, rc:1 }

[..]

if (validmsg[msg] && window[msg])
  window[msg]();

第3部分:如何并行处理多个JSON消息

全局变量有一些缺点。 只有一个。 如果在上一条消息仍在remoteCommand处理的同时,您恰巧在WebSocket上收到另一条JSON消息,这将覆盖上一条消息。 因此notifyAll()将看到两次更新的消息,而旧的则丢失。

经典的比赛条件。 您必须做的是,创建类似于注册表的内容来注册所有消息,然后将一些值传递给notifyAll()来告知应处理哪些已注册消息。

只需稍作更改,您就可以并行(此处)或串行(第4部分)处理消息。

首先,创建一个计数器以区分消息。 也是存储所有消息的对象。 然后,我们声明我们期望的所有有效消息(请参阅第2部分):

var jsonMsgNr = 0;
var jsonMessages = {};
var validmsg = { updateModel:1 }

现在,每当我们收到一条消息时添加一条消息:

if (window.WebSocket) {
    var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");

    ws.onmessage = function (event) {
        var jsonMsg = event.data;
        var json = JSON.parse(jsonMsg);        
        var msg=json["jsonMessage"];

        if (validmsg[msg] && window[msg]) {
            var nr = ++jsonMsgNr;
            jsonMessages[nr] = { jsonMsg:jsonMsg, json:json };

为了能够将nr传递给NotifyAll() ,需要将一个附加参数传递给Bean。 我们称之为msgNr

            // Following might look a bit different on older PrimeFaces
            window[msg]([{name:'msgNr', value:nr}]);
        }
    }
}

也许看看https://stackoverflow.com/a/7221579/490291,以了解更多有关以这种方式传递值的信息。

现在, remoteAction bean会传递一个附加参数msgNr ,该参数必须通过Ajax传递回来。

不幸的是,我不知道(抱歉)在Java中的外观。 因此,请确保您对AjaxCall的msgNr再次将msgNr复制出来。

另外,由于文档对此主题比较安静,因此我不确定如何将参数传递回oncomplete处理程序。 根据JavaScript调试器的信息, notifyAll()获得3个参数: xhdrpayloadpfArgs 不幸的是,我无法设置测试用例来了解情况。

因此,该函数看起来有点像(请与我同在):

function notifyAll(x, data, pfArgs) {
   var nr = ???; // find out how to extract msgNr from data

   var jsonMsg = jsonMessages[nr].jsonMsg;
   var json = jsonMessages[nr].json;
   jsonMessages[nr] = null;  // free memory

   sendMessage(jsonMsg);

   dosomething(json);
}

如果将其拆分为两个函数,则可以从应用程序的其他部分调用notifyAll()

function notifyAll(x, data, unk) {
   var nr = ???; // find out how to extract msgNr from data

   realNotifyAll(nr);
}

function realNotifyAll(nr) {
  if (!(nr in jsonMessages)) return;

  var jsonMsg = jsonMessages[nr].jsonMsg;
  var json = jsonMessages[nr].json;
  delete jsonMessages[nr];  // free memory

  sendMessage(jsonMsg);

  dosomething(json);
}

这里有些东西有点多余。 例如,你也许并不需要json的元素jsonMessages或想解析json再节省一些内存在的情况下JSON是非常大的。 但是,该代码不是最佳代码,而是易于调整以适应您的需求。

第4部分:序列化请求

现在进行更改以序列化事物。 通过添加一些信号量,这很容易。 JavaScript中的信号量只是变量。 这是因为只有一个全局线程。

var jsonMsgNr = 0;
var jsonMessages = {};
var validmsg = { updateModel:1 }
var jsonMsgNrLast = 0;           // ADDED

if (window.WebSocket) {
    var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");

    ws.onmessage = function (event) {
        var jsonMsg = event.data;
        var json = JSON.parse(jsonMsg);        
        var msg=json["jsonMessage"];

        if (validmsg[msg] && window[msg]) {
            var nr = ++jsonMsgNr;
            jsonMessages[nr] = { jsonMsg:jsonMsg, json:json };

            if (!jsonMsgNrLast) {    // ADDED
                jsonMsgNrLast = nr;  // ADDED
                window[msg]([{name:'msgNr', value:nr}]);
            }
        }
    }
}

function realNotifyAll(nr) {
  if (!(nr in jsonMessages)) return;

  var jsonMsg = jsonMessages[nr].jsonMsg;
  var json = jsonMessages[nr].json;
  delete jsonMessages[nr];  // free memory

  sendMessage(jsonMsg);

  dosomething(json);

  // Following ADDED
  nr++;
  jsonMsgNrLast = 0;
  if (nr in jsonMessages)
    {
      jsonMsgNrLast = nr;
      window[jsonMessages[nr].json.msg]([{name:'msgNr', value:nr}]);
    }
}

注意: jsonMsgNrLast可能只是一个标志(是/ jsonMsgNrLast )。 但是,将当前处理过的数字放在变量中可能会有所帮助。

话虽如此,如果sendMessagedosomething失败,就会出现饥饿问题。 因此,也许您可​​以对它进行一些交叉处理:

function realNotifyAll(nr) {
  if (!(nr in jsonMessages)) return;

  var jsonMsg = jsonMessages[nr].jsonMsg;
  var json = jsonMessages[nr].json;
  delete jsonMessages[nr];  // free memory

  nr++;
  jsonMsgNrLast = 0;
  if (nr in jsonMessages)
    {
      jsonMsgNrLast = nr;
      // Be sure you are async here!
      window[jsonMessages[nr].json.msg]([{name:'msgNr', value:nr}]);
    }

  // Moved, but now must not rely on jsonMsgNrLast:
  sendMessage(jsonMsg);
  dosomething(json);
}

这样,在sendMessage运行时,AJAX请求已经发出。 如果现在dosomething出现JavaScript错误或类似错误,则消息仍会正确处理。

请注意:所有这些都是未经测试输入的。 可能存在语法错误或更严重的错误。 抱歉,我已经尽力了。 如果发现错误,请编辑为您的朋友。

第5部分:从JS直接调用

现在,有了所有这些并进行了序列化运行,您始终可以使用realNotifyAll(jsonMsgNrLast)调用先前的notifyAll() realNotifyAll(jsonMsgNrLast) 或者,您可以在列表中显示jsonMessages并选择任意数字。

通过跳过对window[jsonMessages[nr].json.msg]([{name:'msgNr', value:nr}]); (并且在window[msg]([{name:'msgNr', value:nr}]);您还可以暂停Bean处理并使用常规的JQuery回调按需运行它。 为此,创建一个函数并再次更改代码:

var jsonMsgNr = 0;
var jsonMessages = {};
var validmsg = { updateModel:1 }
var jsonMsgNrLast = 0;
var autoRun = true;        // ADDED, set false control through GUI

if (window.WebSocket) {
    var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");

    ws.onmessage = function (event) {
        var jsonMsg = event.data;
        var json = JSON.parse(jsonMsg);        

        if (validmsg[msg] && window[msg]) {
            var nr = ++jsonMsgNr;
            jsonMessages[nr] = { jsonMsg:jsonMsg, json:json };

            updateGuiPushList(nr, 1);

            if (autoRun && !jsonMsgNrLast) {
                runRemote(nr);
            }
        }
    }
}

function realNotifyAll(nr) {
  if (!(nr in jsonMessages)) return;

  var jsonMsg = jsonMessages[nr].jsonMsg;
  var json = jsonMessages[nr].json;
  delete jsonMessages[nr];  // free memory

  updateGuiPushList(nr, 0);

  jsonMsgNrLast = 0;
  if (autoRun)
    runRemote(nr+1);

  // Moved, but now must not rely on jsonMsgNrLast:
  sendMessage(jsonMsg);
  dosomething(json);
}

function runRemote(nr) {
  if (nr==jsonMsgNrLast) return;
  if (nr in jsonMessages)
    {
      if (jsonMsgNrLast) { alert("Whoopsie! Please wait until processing finished"); return; }
      jsonMsgNrLast = nr;

      updateGuiPushList(nr, 2);

      // Be sure you are async here!
      window[jsonMessages[nr].json.msg]([{name:'msgNr', value:nr}]);
    }
}

现在你可以开始与处理runRemote(nr)并调用完成函数realNotifyAll(nr)

updateGuiPushList(nr, state)state=0:finished 1=added 2=running的函数updateGuiPushList(nr, state) state=0:finished 1=added 2=running是对您的GUI代码的回调,该GUI代码将更新屏幕上等待处理的推送列表。 设置autoRun=false停止自动处理,设置autoRun=true进行自动处理。

注意:将autoRunfalse设置为true您当然需要以最低nr触发一次runRemote

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM