使用 WebEngine 执行多个脚本(或依赖于另一个脚本的脚本)

Executing multiple scripts with WebEngine (or a script that depends on another script)

我想使用(JavaFX 的 WebEngine)在网页上执行一些 Java脚本(和 jQuery)并处理结果(使用 Java代码)。
我对自己执行某些脚本的回调函数有问题。

为了尽可能简单地说明我的问题,我做了一个最小化的代码来显示不需要的结果(它只是一个更大项目的一部分)。

所以,我创建了三个 classes:

  1. Browser - 一个 class 包装了 WebEngine 并用作加载网页和执行脚本的浏览器。
  2. JQueryFunction - class 列出了几个与 jQuery 回调函数一起使用的函数,可以通过多种方式实现。 (基本上它们应该是用户按照自己的方式实现的功能接口——但为了简单起见,我将其设为普通功能)。
  3. Test - class 具有执行两个脚本的主要方法。

浏览器

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.scene.Scene;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class Browser extends Application {
    private static WebEngine webEngine;
    private static JSObject window;

    @Override
    public void start(Stage primaryStage) throws Exception {
        WebView browser = new WebView();
        webEngine = browser.getEngine();

        webEngine.getLoadWorker().stateProperty().addListener(
                new ChangeListener<State>() {
                    @Override
                    public void changed(ObservableValue<? extends State> observable, State oldState, State newState) {
                        if (newState == State.SUCCEEDED) {
                            window = (JSObject) webEngine.executeScript("window;");

                            // The following lines inject jQuery (for pages that don't use already)
                            webEngine.executeScript("var script = document.createElement(\"script\");");
                            webEngine.executeScript("script.src = \"http://code.jquery.com/jquery-1.12.0.min.js\";");
                            webEngine.executeScript("document.getElementsByTagName(\"body\")[0].appendChild(script);");
                        }
                    }
                });

        primaryStage.setScene(new Scene(browser));
        primaryStage.show();
        //Platform.setImplicitExit(false);
    }

    public static JSObject getWindow() {
        return window;
    }

    public static Object executeScript(String script) throws InterruptedException, ExecutionException {
        FutureTask<Object> task = new FutureTask<>(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                return webEngine.executeScript(script);
            }
        });
        Platform.runLater(task);
        return task.get();
    }

    public static void load(String url) {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                webEngine.load(url);
            }
        });
    }

    public static void start() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Application.launch(Browser.class);
                } catch (IllegalStateException e) {}
            }
        }).start();
    }
}

JQueryFunction

import java.util.concurrent.ExecutionException;

public class JQueryFunction {
    public String function1(int index, String text) {
        return (index+1) + ": " + text;
    }

    public String function2(int index, String text) throws InterruptedException, ExecutionException {
        return (String) Browser.executeScript("$($(\".BlueArrows:eq(2) li\").find(\"a:first\")[index]).text();");
    }
}

测试

public class Test {
    public static void main(String[] args) throws Exception {
        Browser.start();
        Thread.sleep(1000);  // Only used here to simplify the code
        Browser.load("https://docs.oracle.com/javase/tutorial/");
        Thread.sleep(3500);  // Only used here to simplify the code
        Browser.getWindow().setMember("javaApp", new JQueryFunction());
        Browser.executeScript("$(\".BlueArrows:eq(0) li\").find(\"a:first\").text(function(index, text) { return javaApp.function1(index, text); });");
        Browser.executeScript("$(\".BlueArrows:eq(0) li\").find(\"a:first\").text(function(index, text) { return javaApp.function2(index, text); });");
    }
}

当我 运行 测试时,第一个 executeScript 运行s 如预期的那样改变了一些元素的文本(前置索引号)。
但是 second executeScript 永远 stuck 并粘住 GUI,实际上整个 JavaFX 应用程序。

我明白为什么会这样了...
executeScript 方法调用 WebEngine(通过 Platform.runLater)执行 jQuery 遍历元素并调用 Java 函数(每次都有不同的参数)。

WebEngine 的编程方式是他将仅在一个线程中(在 FX 线程内)一个任务接一个任务(串行)执行。

有什么办法可以解决吗?
为什么 WebEngine 仅限于在 JavaFX 应用程序下工作?
为什么它必须只与一个线程一起工作?

避免您遇到的 "deadlock" 的一种可能方法是当您已经 运行 在 GUI 线程(JavaFX 应用程序线程)中时不调用 Platform.runLater。类似于:

public static Object executeScript(String script) throws InterruptedException, ExecutionException {
        if(Platform.isFxApplicationThread()) {
            return webEngine.executeScript(script);
        } 
        FutureTask<Object> task = new FutureTask<>(new Callable<Object>() {
             @Override
             public Object call() throws Exception {
                 return webEngine.executeScript(script);
             }
         });
         Platform.runLater(task);
         return task.get();

    }