从 JavaFX WebView 迁移至 JxBrowser

长久以来,JavaFX 一直包含一个内置的 WebView 组件,这是在 Java 应用中渲染 Web 内容的一个稳定方案。然而,在更复杂或要求更高的使用场景中,它可能就不够用了。因此,许多开发者转向了像 JxBrowser 这样的替代方案。

在本迁移指南中,我们将详细介绍如何从 JavaFX WebView 迁移至 JxBrowser,并提供代码示例以及相关 JxBrowser 文档的链接。

本文侧重介绍如何 迁移至 JxBrowser。若想了解为何 要迁移,请参阅文章 JxBrowser 还是 JavaFX WebView,我们在其中详细解析了这两种方案的技术和架构差异。

依赖项

将 JxBrowser 添加到项目中,就像将一些 JAR 文件添加到类路径中一样简单。例如,一个运行于 Windows 的 JavaFX 应用将需要以下文件:

  • jxbrowser-8.9.2.jar. 该文件包含 JxBrowser 的大部分 API。
  • jxbrowser-javafx-8.9.2.jar. 该文件包含 JxBrowser 的 JavaFX 组件。
  • jxbrowser-win64-8.9.2.jar. 该文件包含适用于 64 位 Windows 的 Chromium 二进制文件。

你可以从 JxBrowser 8.9.2 版本发布说明页面下载所需文件。

如果您使用的是标准的 MavenGradle,只需像平常一样添加 Maven 仓库中的依赖项即可:

Maven

xml 复制代码
<repositories>
    <repository>
        <id>com.teamdev</id>
        <url>https://europe-maven.pkg.dev/jxbrowser/releases</url>
    </repository>
</repositories>
<dependency>
    <groupId>com.teamdev.jxbrowser</groupId>
    <artifactId>jxbrowser-javafx</artifactId>
    <version>{version}</version>
</dependency>
<dependency>
    <groupId>com.teamdev.jxbrowser</groupId>
    <artifactId>jxbrowser-win64</artifactId>
    <version>{version}</version>
</dependency>

Gradle

kotlin 复制代码
plugins {
    id("com.teamdev.jxbrowser") version "{gradle_plugin_version}"
}
jxbrowser {
    version = "{version}"
}
dependencies {
    implementation(jxbrowser.javafx)
    implementation(jxbrowser.win64)
}

线程安全性

WebViewWebEngine 并非线程安全的;访问它们及其 DOM/JavaScript 对象时,必须始终仅从 JavaFX 应用程序线程进行。

而 JxBrowser 是线程安全的。您可以在不同线程中安全地使用 JxBrowser 对象。不过,在 JavaFX 应用线程中调用 JxBrowser API 时需谨慎,因为它的许多方法是阻塞式的。为了避免影响用户体验,我们通常建议不要在 JavaFX 应用线程中调用 JxBrowser。

迁移

创建浏览器

JavaFX 提供了可视化的 WebView 组件(可添加到场景中),以及非可视的 WebEngine(包含实际的 Browser API)。

创建和使用方法如下:

java 复制代码
WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();
webEngine.load("https://example.com");
scene.getRoot().getChildren().add(webView);

JxBrowser 同样由可视化和非可视化部分组成。该库提供了非可视化的 EngineBrowser 对象,它们封装了 Browser API;还提供了一个可视化的 BrowserView 组件,可将其添加到场景中以显示加载的 Web 内容。

java 复制代码
// 非可视化部分:
Engine engine = Engine.newInstance(HARDWARE_ACCELERATED);
Browser browser = engine.newBrowser();
browser.navigation().loadUrl("https://example.com");

// 可视化部分:
Platform.runLater(() -> {
    BrowserView browserView = BrowserView.newInstance(browser);  
    scene.getRoot().getChildren().add(browserView);
});

在上述示例中,我们创建了一个非可视化的 Engine 实例,用于表示 Chromium 主进程。然后,我们创建一个非可视化的 Browser 实体,用于表示主进程中的特定浏览器------类似于 Google Chrome 中的浏览器标签页。最后,我们创建一个可视化的 BrowserView 节点并将其添加到场景中。

提示: 就像在 Google Chrome 中可以打开多个标签页一样,您可以在同一个 Engine 实例中创建多个 Browser 对象。

如需了解 JxBrowser API 中主要组件、进程模型及其他架构细节,请查阅架构指南

关闭浏览器

在 JavaFX 中,并不需要显式关闭 WebView 实例。通常将其从场景图(scene graph)中移除就已足够。

而在 JxBrowser 中,仅从场景图中移除 BrowserView 不会 关闭浏览器并释放所有已分配的资源。你必须手动关闭 BrowserEngine 对象:

JavaFX

java 复制代码
scene.getRoot().getChildren().add(webView);

JxBrowser

java 复制代码
// 关闭单个 Browser。
browser.close();

// 关闭 Engine。此操作将自动关闭其包含的所有 Browser。
engine.close();

页面导航

WebEngine 中的导航功能几乎可以直接转换为 JxBrowser 的调用方式:

JavaFX

java 复制代码
webEngine.load("https://example.com");
webEngine.reload();

WebHistory history = webEngine.getHistory();
var currentIndex = history.getCurrentIndex();
var historySize = history.getEntries().size();

// 后退到上一个历史页面。
var previousPage = currentIndex - 1;
if (previousPage >= 0 && previousPage < historySize) {
    history.go(previousPage);
}

// 前进到下一个历史页面。
var nextPage = currentIndex + 1;
if (nextPage < historySize) {
    history.go(nextPage);
}

JxBrowser

java 复制代码
Navigation navigation = browser.navigation();
navigation.loadUrl("https://example.com");
navigation.reload();

navigation.goBack();
navigation.goForward();

// 跳转到指定的历史记录索引
navigation.goToIndex(2);

导航监听器

在这两种解决方案中,加载过程都是在后台进行的,因此需要注册监听器来检测加载何时完成。

在 JavaFX 中,可以通过监听加载工作器的状态来实现:

java 复制代码
var worker = webEngine.getLoadWorker();
worker.stateProperty().addListener((ov, oldState, newState) -> {
    if (newState == State.SUCCEEDED) {
        // 此处可以执行 JavaScript 并访问 DOM 树。
    } else {
        System.out.println("导航失败!");
    }
});

在 JxBrowser 中,通知更加精细:

java 复制代码
// 当导航操作完成时会触发该事件。
// 此时 frame 和 DOM 树可能尚未初始化。
navigation.on(NavigationFinished.class, event -> {
    if (event.error() != OK) {
        System.out.println("Navigation failed!");
    }
});

// 当 frame 的文档加载完成,且可以访问 DOM 时,会触发此事件。
navigation.on(FrameDocumentLoadFinished.class, event -> {
    // 此处可以执行 JavaScript 并访问 DOM 树。
});

// 此回调允许您在 frame 刚刚完成加载、但**尚未执行其自身的 JavaScript**之前
// 执行您的 JavaScript 代码。
browser.set(InjectJsCallback.class, params -> {
    Frame frame = params.frame();
    JsObject window = frame.executeJavaScript("window");
    if (window != null) {
        ...
    }
    return Response.proceed();
});

JxBrowser 共提供 种细粒度的导航事件。完整列表请参阅导航事件文档

从 Java 调用 JavaScript

在这两种方案中,您都可以执行任意的 JavaScript 代码,在 Java 中获取 JavaScript 对象,并享受自动类型转换的便利:

JavaFX

java 复制代码
// JavaScript 对象会被转换为 JSObject。
JSObject dialogs = (JSObject) webEngine.executeScript("dialogs");
dialogs.call("showError", "The card number is not correct!");

// JavaScript 字符串会被转换为 String。
String locale = (String) dialogs.getMember("locale");

JxBrowser

java 复制代码
browser.mainFrame().ifPresent(frame -> {
    // JavaScript 对象会被转换为 JsObject。
    JsObject dialogs = frame.executeJavaScript("dialogs");
    jsObject.call("showError", "The card number is not correct!");

    // JavaScript 字符串会被转换为 String。
    String locale = dialogs.property("locale");
});

JavaFX 会自动转换传入的 JavaScript 值。原始类型会被转换为对应的 Java 类型,JavaScript 对象则会被转换为 JSObject 实例。

JxBrowser 执行类似的转换,但为特定的 JavaScript 类型(例如函数、PromiseArrayBuffer 等)提供了专用的 Java 类型。在类型转换指南中可查看完整列表。

对于用于访问带索引的 JavaScript 对象的 JSObject.getSlot()JSObject.setSlot() 方法,JxBrowser 没有直接的替代方案。

JavaScript 对象的生命周期

在 JavaFX 和 JxBrowser 中,只要对应的 JavaScript 对象还存在,JSObjectJsObject 实例就能正常工作。当 JavaScript 对象被垃圾回收或所在的 frame 加载了新的文档时,该对象就会失效。无论是在 JavaFX 还是 JxBrowser 中,尝试使用已失效的 JavaScript 对象都会抛出异常。

JavaScript 对象被垃圾回收的时间难以预测,因此在 JxBrowser 中,传递给 Java 的 JavaScript 对象会被保护,防止被垃圾回收,直到新文档加载时才会关闭。若想释放对该 JavaScript 对象的引用,使其可以被垃圾回收,可以调用 JsObject.close() 方法:

java 复制代码
JsObject persistentObject = frame.executeJavaScript("dialogs");
persistentObject.close();

从 JavaScript 调用 Java

要从 JavaScript 调用 Java 代码,需要将 Java 对象注入到 JavaScript 环境中。这两种方案中的做法非常相似:

JavaFX

java 复制代码
public static class GreetingService {
    public void greet(String name) {
        System.out.println("Hello, " + name + "!");
    }
}

...

GreetingService greetings = new GreetingService();
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("greetings", greetings);

JxBrowser

java 复制代码
@JsAccessible
public static class GreetingService {
    public void greet(String name) {
        System.out.println("Hello, " + name + "!");
    }
}

...

GreetingService greetings = new GreetingService();
JsObject window = frame.executeScript("window");
window.putProperty("greetings", greetings);

成员访问权限

在 JavaFX 中,JavaScript 可以访问被注入的 Java 对象的所有公共成员。

而在 JxBrowser 中,需要显式地将 Java 类及其成员标记为允许被 JavaScript 访问:

java 复制代码
// 类的所有公共成员都将可被访问。
@JsAccessible
public class AccessibleClass {
    public String sayHelloTo(String firstName) {
        ...
    }
}

// 仅该类的单个方法可被访问。
public class RestrictedClass {

    @JsAccessible
    public String sayHelloTo(String firstName) {
        ...
    }
}

对于无法添加注解的类(例如标准库类),可以使用以下方式使其可访问:

java 复制代码
JsAccessibleTypes.makeAccessible(java.util.HashMap.class);

更多关于如何使对象对 JavaScript 可访问的信息,请参阅 JavaScript 指南

Java 对象的生命周期

JavaFX 对传递给 JavaScript 的 Java 对象使用弱引用 。这意味着如果该对象被垃圾回收,其对应的 JavaScript 对象会变为 undefined

而 JxBrowser 使用的是强引用,会防止 Java 对象被回收。只有在以下几种情况下,引用才会被移除:frame 加载了新文档;frame 被移除;或 browser 被关闭时。

代理配置

JavaFX 的 WebView 使用的是 Java 运行时自带的网络栈,因此会自动遵循 Java 的代理配置。

在 JxBrowser 中,Chromium 在单独的进程中使用其自身的网络,并遵循系统代理设置。如果您不想使用系统设置,可以为每个 Profile 单独配置代理:

JavaFX

java 复制代码
// 配置代理设置。
System.setProperty("http.proxyHost", "proxy.com");
System.setProperty("http.proxyPort", "8080");
System.setProperty("https.proxyHost", "proxy.com");
System.setProperty("https.proxyPort", "8081");
System.setProperty("nonProxyHosts", "example.com|microsoft.com");

// 配置代理身份验证。
Authenticator.setDefault(new Authenticator() {
    protected PasswordAuthentication getPasswordAuthentication() {
        return new PasswordAuthentication("username", "password".toCharArray());
    }
});

JxBrowser

java 复制代码
// 配置代理设置。
var profile = engine.profiles().defaultProfile();
var exceptions = "example.com,microsoft.com";
var proxyRules = "http=proxy.com:8080;https=proxy.com:8081";
profile.proxy().config(CustomProxyConfig.newInstance(proxyRules, exceptions));

// 配置代理身份验证。
profile.network().set(AuthenticateCallback.class, (params, tell) -> {
   if (params.isProxy()) {
       tell.authenticate("username", "password");
   } else {
       // 跳过其他身份验证请求。
       tell.cancel();
   }
});

DOM 访问

JavaFX 和 JxBrowser 提供了一组类似的功能来访问 DOM 树:

JavaFX

java 复制代码
var document = webEngine.getDocument();
var element = document.getElementById("exit-app");
((EventTarget) element).addEventListener("click", listener, false);

JxBrowser

java 复制代码
browser.mainFrame()
       .flatMap(Frame::document)
       .flatMap(document -> document.findElementById("exit-app"))
       .ifPresent(element -> {
           element.addEventListener(CLICK, listener, true);
        });

在 JxBrowser 中,DOM 节点在 Java 和 JavaScript 之间传递时会自动转换为对应的 Java 类型。与 JsObject 类似,它们在浏览器中会被保护,不会被垃圾回收。如需手动释放资源,可调用 close() 方法,使其可被回收。

更多关于 DOM 操作的内容,请参阅 DOM 指南

打印功能

JavaFX 提供了 API 来打印任何可视节点,包括 WebView。您可以选择打印机、通过代码配置部分打印参数,或者向用户展示系统打印对话框。

JxBrowser 则使用 Chromium 的打印能力。它同样支持选择打印机、以编程方式设置打印参数,并在需要时显示 Chromium 的打印预览对话框。

JavaFX

java 复制代码
var printer = findMyPrinter(Printer.getAllPrinters());
var job = PrinterJob.createPrinterJob(printer);
if (showDialogs) {
    // 向用户显示系统对话框。
    job.showPageSetupDialog(stage);
    job.showPrintDialog(stage);
} else {
    // 或者静默打印
    var settings = printerJob.getJobSettings();
    settings.setCopies(3);
    settings.setCollation(COLLATED);

    webView.getEngine().print(job);
    printerJob.job();
}

JxBrowser

java 复制代码
browser.set(PrintCallback.class, (params, tell) -> {
    if (showDialogs) {
        // 向用户显示系统对话框。
        tell.showPrintPreview();
    } else {
        // 或静默打印。
        tell.print();
    }
});

// 注册 `PrintHtmlCallback` 用于打印 HTML 页面。
// 若从 PDF 文件发起打印,则需使用 `PrintPdfCallback`。
browser.set(PrintHtmlCallback.class, (params, tell) -> {
    var printer = findMyPrinter(params.printers());

    var job = printer.printJob();
    var settings = job.settings();
    settings.copies(3);
    settings.enableCollatePrinting();
    job.on(PrintCompleted.class, event -> {
        System.out.println("Printing completed");
    });
    tell.proceed(printer);
});

browser.set(PrintPdfCallback.class, (params, tell) -> {
    ...
});

提示: 即使没有系统打印机,您也可以使用 Chromium 内置的 PDF 打印机:params.printers().pdfPrinter()

有关如何配置打印功能的更多信息,请参阅打印指南

用户代理

在 JavaFX 中,您可以自定义 Browser 的用户代理(User-Agent)。

在 JxBrowser 中,您可以自定义单个 Browser 的用户代理,也可以自定义整个 Engine:

JavaFX

java 复制代码
webEngine.setUserAgent("custom user agent");

JxBrowser

java 复制代码
// 在 Engine 启动时配置全局用户代理。
var opts = EngineOptions
        .newBuilder(HARDWARE_ACCELERATED)
        .userAgent("custom user agent")
        .build();
var engine = Engine.newInstance(opts);

// 或者,为特定 Browser 配置 UI。
browser.userAgent("custom user agent");

用户数据目录

在 JavaFX 中,用户数据目录用于存储本地存储中的数据。您可以显式配置该目录,或者 Engine 会根据操作系统和用户偏好自动选择。

在 JxBrowser 中,用户数据目录存储所有用户数据,包括缓存、本地存储和其他相关信息。您可以在启动 Engine 时配置该目录,或者 JxBrowser 会使用临时目录,该目录将在 Engine 关闭时被删除:

JavaFX

java 复制代码
webEngine.setUserDataDirectory(new File("/path/to/directory"));

JxBrowser

java 复制代码
var opts = EngineOptions
        .newBuilder(HARDWARE_ACCELERATED)
        .userDataDir(Paths.get("/path/to/directory"))
        .build();
var engine = Engine.newInstance(opts);

请注意,同一个用户数据目录不能被单个或不同 Java 应用中运行的多个 Engine 实例同时使用。尝试使用同一个用户数据目录将导致 Engine 创建过程中抛出异常。

弹出窗口

在 JavaFX 中,当网页想要在新窗口中打开内容时,WebEngine 不会创建新窗口。相反,它会用新窗口替换当前加载的页面。

通过注册自定义弹出处理器,可以更改此行为:

java 复制代码
webEngine.setCreatePopupHandler(features -> {
    if (noPopups) {
        // 返回 null 会取消弹出窗口的创建。
        return null;
    }
    // 通过创建新的 WebView,可以指示 JavaFX 为新弹出窗口使用它。
    var popupView = new WebView();
    scene.getRoot().getChildren().add(popupView);
    return popupView.getEngine();
});

在 JxBrowser 中,所有弹出窗口默认都是被抑制的。要更改此设置,需注册 CreatePopupCallback

java 复制代码
browser.set(CreatePopupCallback.class, params -> {
    return noPopups
                ? CreatePopupCallback.Response.suppress()
                : CreatePopupCallback.Response.create();
    }
});

如果允许创建弹出窗口且 BrowserView 在 UI 中可见,JxBrowser 会在新的 Stage 中打开弹出窗口。你可以在 OpenBrowserPopupCallback 中自定义此行为:

java 复制代码
browser.set(OpenBrowserPopupCallback.class, params -> {
    var popupBrowser = params.popupBrowser();
    var popupBounds = params.initialBounds();
        Platform.runLater(() -> {
        var popupView = BrowserView.newInstance(browser);
        scene.getRoot().getChildren().add(popupView);
    });
    return OpenBrowserPopupCallback.Response.proceed();
});

有关处理弹出对话框的更多信息,请参阅弹出窗口指南

JavaScript 对话框

JavaFX 和 JxBrowser 都允许您自定义 JavaScript 对话框(如 confirm、prompt 和 alert)的行为:

JavaFX

java 复制代码
webEngine.setConfirmHandler(value -> {
    if (silent) {
        return null;
    } else {
        return showMyConfirmDialog();
    }
});

webEngine.setPromptHandler(promptData -> {
    if (silent) {
        return null;
    } else {
        return showMyPromptDialog(promptData);
    }
});

webEngine.setOnAlert(event -> System.out.println("Alert happened!"));

JxBrowser

java 复制代码
browser.set(ConfirmCallback.class, (params, action) -> {
    if (silent) {
        action.cancel();
    } else {
        var result = showMyConfirmDialog(params);
        if (result) {
            action.ok();
        } else {
            action.cancel();   
        } 
    }
});

browser.set(PromptCallback.class, (params, action) -> {
    if (silent) {
        action.cancel();
    } else {
        action.ok(showMyPromptDialog(params));
    }
});

browser.set(AlertCallback.class, (params, action) -> {
    System.out.println("Alert happened");
    action.ok();
});

如果您不配置这些处理程序,JavaFX 默认会抑制对话框 ------ confirm 对话框返回 false,prompt 对话框返回空字符串。而 JxBrowser 则会调用 JavaFX 的默认对话框实现来显示这些对话框。

您可以阅读对话框指南,了解如何自定义文件选择器、身份验证和其他类型的对话框。

自定义 CSS

在 JavaFX 中,可以通过设置样式表文件路径,或使用包含样式的 Data URL 来注入自定义 CSS。

在 JxBrowser 中,可以通过将 CSS 样式作为字符串传入来注入自定义样式:

JavaFX

java 复制代码
webEngine.setUserStyleSheetLocation("file:///path/theme.css");

JxBrowser

java 复制代码
// 此回调在文档准备就绪后触发,此时可注入 CSS。
browser.set(InjectCssCallback.class, params -> {
    var styles = readFile("file:///path/theme.css")
    return InjectCssCallback.Response.inject(styles);
});

总结

JavaFX WebView 和 JxBrowser 都提供了类似的功能,从 WebView 迁移至 JxBrowser 不会给您带来太多麻烦。

在本指南中,我们提供了迁移 WebView 大部分功能的代码示例,并附上了相关文档的链接。

尽管实际项目中的迁移工作可能会比较复杂,但我们相信通过本指南可以大大简化这一过程,并为您提供一个清晰的起点。

相关推荐
曾曜5 分钟前
PostgreSQL逻辑复制的原理和实践
后端
A了LONE5 分钟前
h5的底部导航栏模板
java·前端·javascript
豌豆花下猫5 分钟前
Python 潮流周刊#110:JIT 编译器两年回顾,AI 智能体工具大爆发(摘要)
后端·python·ai
轻语呢喃19 分钟前
JavaScript :事件循环机制的深度解析
javascript·后端
ezl1fe20 分钟前
RAG 每日一技(四):让AI读懂你的话,初探RAG的“灵魂”——Embedding
后端
经典199222 分钟前
spring boot 详解以及原理
java·spring boot·后端
星光542223 分钟前
飞算JavaAI:给Java开发装上“智能引擎”的超级助手
java·开发语言
Aurora_NeAr24 分钟前
Apache Iceberg数据湖高级特性及性能调优
大数据·后端
程序员清风35 分钟前
程序员要在你能挣钱的时候拼命存钱!
后端·面试·程序员
学习3人组1 小时前
JVM GC长暂停问题排查
java