List icon 目录

监听 DOM 内容变化

本教程展示了如何构建一个小型 Java 应用程序,该程序监听加载到 JxBrowser 中的 HTML 页面发生的变化。

前提条件

为完成本教程,您将需要:

  • Git
  • Java 8 或更高版本
  • 有效的 JxBrowser 许可证。可以是评估版或商业版。有关许可证的更多信息,请参阅许可指南。

创建项目

本教程示例应用程序的代码与其他示例一起,存储在一个基于 Gradle 的 GitHub 仓库中。

如果您想构建一个基于 Maven 的项目,请参考 Maven 配置指南。如果您希望从头开始构建一个基于 Gradle 的项目,请参考 Gradle 配置指南。

获取代码

要获取代码,请执行以下命令:

$ git clone https://github.com/TeamDev-IP/JxBrowser-Examples
$ cd JxBrowser-Examples/tutorials/content-changes

现在我们位于所有示例的根目录下。本教程的代码位于 tutorials/content-changes 目录下。

添加许可证

要运行本教程,您需要设置许可证密钥

页面

我们将加载一个简单的 HTML 页面,在该页面上将显示一个每秒自动递增的计数器。

下面是计数器的代码:

<div>
  <span class="counter" id="counter"></span>
</div>

该计数器使用 jQuery 进行更新:

<script crossorigin="anonymous" src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script type="text/javascript">
    $(document).ready(function () {
        var counter = 1;
        setInterval(function() { $(".counter").text(counter++); }, 1000);
    });

</script>

以下是页面的完整代码,该代码作为名为 index.html 的资源文件包含在项目中:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>index</title>
</head>
<body>
<div>
  <span class="counter" id="counter"></span>
</div>

<script crossorigin="anonymous" src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script type="text/javascript">
    $(document).ready(function () {
        var counter = 1;
        setInterval(function() { $(".counter").text(counter++); }, 1000);
    });

</script>
</body>
</html>

JavaScript 代码

由于我们希望从 HTML 代码中通知我们应用程序的 Java 代码,因此需要编写 JavaScript 代码来处理 DOM 修改并将其传递给我们的 Java 代码。我们本可以直接在 HTML 代码中完成此操作,但我们想展示如何从 Java 动态添加此类代码。

首先,我们获取要跟踪的元素:

const element = document.getElementById('counter');

然后,我们创建一个 MutationObserver 实例,并为其提供一个回调函数,用于将数据传递给 Java 代码。

const observer = new MutationObserver(
    function(mutations) {
        window.java.onDomChanged(element.innerHTML);
    });

这里最重要的部分是以下调用:

window.java.onDomChanged(element.innerHTML);

我们在这里引用存储在 window 对象中的一个对象,该对象的属性名为 javajava 是包含要调用的 Java 对象的属性的名称。我们使用 java 这个词来强调我们正在调用一个 Java 对象。这个属性可以是任何符合您应用程序意义的 JavaScript 标识符。

我们调用的对象方法是 onDomChanged()。稍后,当我们创建用于监听内容更改的 Java 类时,将添加此方法。我们传递计数器元素的 innerHTML 属性。 因此,该方法将接受一个 String 参数。

现在,让我们告诉 observer 跟踪 DOM 的变化:

const config = {childList: true};
observer.observe(element, config);

以下是我们放置在资源文件中作为 observer.js 文件的完整 JavaScript 代码:

const element = document.getElementById('counter');
const observer = new MutationObserver(
    function(mutations) {
        window.java.onDomChanged(element.innerHTML);
    });
const config = {childList: true};
observer.observe(element, config);

Java 代码

加载资源的实用工具

在前面的部分中,我们审查了存储为资源的 HTML 和 JavaScript 代码。现在我们需要加载它们的代码。我们将使用 Guava 中的 Resources 来实现最简单的方法。

以下是我们稍后将使用的实用工具方法 load() 的代码:

private static String load(String resourceFile) {
    URL url = ContentListening.class.getResource(resourceFile);
    try (Scanner scanner = new Scanner(url.openStream(),
            Charsets.UTF_8.toString())) {
        scanner.useDelimiter("\\A");
        return scanner.hasNext() ? scanner.next() : "";
    } catch (IOException e) {
        throw new IllegalStateException("无法加载资源 " +
                resourceFile, e);
    }
}

创建 Browser 和 BrowserView

为了简化示例,我们将所有代码放在 main() 方法下。实际的应用程序会有更结构化的代码。

首先,我们需要创建一个 EngineBrowser

Engine engine = Engine.newInstance(
        EngineOptions.newBuilder(HARDWARE_ACCELERATED).build());
Browser browser = engine.newBrowser();

然后,在 Swing 的事件分发线程(EDT)中,我们创建一个 BrowserViewJFrame。然后将新创建的 BrowserView 添加到 Frame 中。

SwingUtilities.invokeLater(() -> {
    BrowserView view = BrowserView.newInstance(browser);

    JFrame frame = new JFrame("内容监听");
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    frame.add(view, BorderLayout.CENTER);
    frame.setSize(700, 500);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
});

现在我们需要创建一个对象来监听 DOM 变化。

用于监听 DOM 变化的 Java 对象

您可能还记得,我们之前审查的 JavaScript 代码需要一个带有名为 onDomChanged() 的方法的对象,该方法接受一个 String 参数。

以下是该类的定义:

public static class JavaObject {

    @SuppressWarnings("unused") // Invoked by the callback processing code.
    @JsAccessible
    public void onDomChanged(String innerHtml) {
        System.out.println("DOM 节点已发生变化:" + innerHtml);
    }
}

现在我们需要让 JavaScript 代码与我们的 Java 对象进行通信。让我们开始吧。

连接 JavaScript 和 Java

为了让 JavaScript 代码与我们的 Java 对象进行通信,我们将实现一个 InjectJsCallback 将实例传递给 Browser.set()

browser.set(InjectJsCallback.class, params -> {
    Frame frame = params.frame();
    String window = "window";
    JsObject jsObject = frame.executeJavaScript(window);
    if (jsObject == null) {
        throw new IllegalStateException(
                format("未找到 '%s' JS 对象", window));
    }
    jsObject.putProperty("java", new JavaObject());
    return Response.proceed();
});

在这段代码中,我们:

  1. 获取了 JavaScript window 对象的一个实例。
  2. 创建了一个监听内容变化的 Java 对象,并将其设置为名为 java 的属性添加到 window 对象中。前面在 JavaScript 代码中,我们使用 MutationObserver 来将数据传递给与此属性相关联的对象。

接下来,我们应该注册一个 FrameLoadFinished 事件监听器,该监听器将加载包含 MutationObserver 置的 JavaScript 代码,并让 Browser 执行该代码,在 DOM 模型准备就绪时完成 JavaScript 和 Java 之间的连接。

browser.navigation().on(FrameLoadFinished.class, event -> {
    String javaScript = load("observer.js");
    event.frame().executeJavaScript(javaScript);
});

剩下的步骤是将页面加载到 Browser 中:

String html = load("index.html");
String base64Html = Base64.getEncoder().encodeToString(html.getBytes(UTF_8));
String dataUrl = "data:text/html;base64," + base64Html;
browser.navigation().loadUrl(dataUrl);

完整的 Java 代码

以下是完整的 Java 代码:


import static com.teamdev.jxbrowser.engine.RenderingMode.HARDWARE_ACCELERATED;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.Charsets;
import com.teamdev.jxbrowser.browser.Browser;
import com.teamdev.jxbrowser.browser.callback.InjectJsCallback;
import com.teamdev.jxbrowser.browser.callback.InjectJsCallback.Response;
import com.teamdev.jxbrowser.engine.Engine;
import com.teamdev.jxbrowser.engine.EngineOptions;
import com.teamdev.jxbrowser.frame.Frame;
import com.teamdev.jxbrowser.js.JsAccessible;
import com.teamdev.jxbrowser.js.JsObject;
import com.teamdev.jxbrowser.navigation.event.FrameLoadFinished;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import java.awt.BorderLayout;
import java.io.IOException;
import java.net.URL;
import java.util.Base64;
import java.util.Scanner;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

/**
 * 这个示例演示了如何从一个 Java 对象监听 DOM 的变化。
 */
public final class ContentListening {

    public static void main(String[] args) {
        Engine engine = Engine.newInstance(
                EngineOptions.newBuilder(HARDWARE_ACCELERATED).build());
        Browser browser = engine.newBrowser();
        SwingUtilities.invokeLater(() -> {
            BrowserView view = BrowserView.newInstance(browser);

            JFrame frame = new JFrame("内容监听");
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.add(view, BorderLayout.CENTER);
            frame.setSize(700, 500);
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });

        browser.set(InjectJsCallback.class, params -> {
            Frame frame = params.frame();
            String window = "window";
            JsObject jsObject = frame.executeJavaScript(window);
            if (jsObject == null) {
                throw new IllegalStateException(
                        format("未找到 '%s' JS 对象", window));
            }
            jsObject.putProperty("java", new JavaObject());
            return Response.proceed();
        });

        browser.navigation().on(FrameLoadFinished.class, event -> {
            String javaScript = load("observer.js");
            event.frame().executeJavaScript(javaScript);
        });

        String html = load("index.html");
        String base64Html = Base64.getEncoder().encodeToString(html.getBytes(UTF_8));
        String dataUrl = "data:text/html;base64," + base64Html;
        browser.navigation().loadUrl(dataUrl);
    }

    /**
     * 将资源内容加载为字符串。
     */
    private static String load(String resourceFile) {
        URL url = ContentListening.class.getResource(resourceFile);
        try (Scanner scanner = new Scanner(url.openStream(),
                Charsets.UTF_8.toString())) {
            scanner.useDelimiter("\\A");
            return scanner.hasNext() ? scanner.next() : "";
        } catch (IOException e) {
            throw new IllegalStateException("无法加载资源 " +
                    resourceFile, e);
        }
    }

    /**
     * 观察 DOM 变化的对象。
     *
     * <p>从 JavaScript 代码调用的类和方法必须是公开的。
     */
    public static class JavaObject {

        @SuppressWarnings("unused") // 由回调处理代码调用。
        @JsAccessible
        public void onDomChanged(String innerHtml) {
            System.out.println("DOM 节点已发生变化:" + innerHtml);
        }
    }
}

如果您运行该程序,您应该会看到带有计数器的 Browser 窗口,并且当 Browser 窗口中的计数器变化时,控制台会输出相应的信息。

总结

在本教程中,我们创建了一个小型 Java 应用程序,用于监听已加载网页中 DOM 的变化。

该应用程序由以下部分组成:

  • 带有将要变化的 DOM 元素的 HTML 页面,并且我们知道该元素的 ID。
  • JavaScript 代码,使用 MutationObserver 来通知与 window 对象关联的 Java 对象。
  • 监听 DOM 事件的 Java 对象。
  • Java 代码,向 Browser 实例添加 InjectJsCallback,加载网页,并使 Browser 实例执行 JavaScript 代码,使 MutationObserver 将变化传递给监听的 Java 对象。