長久以來,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 版本發布說明頁面下載所需文件。
如果您使用的是標準的 Maven 或 Gradle,只需像平常一樣添加 Maven 倉庫中的依賴項即可:
Maven
<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
plugins {id("com.teamdev.jxbrowser") version "{gradle_plugin_version}"
}
jxbrowser {version = "{version}"
}
dependencies {implementation(jxbrowser.javafx)implementation(jxbrowser.win64)
}
線程安全性
WebView
和 WebEngine
并非線程安全的;訪問它們及其 DOM/JavaScript 對象時,必須始終僅從 JavaFX 應用程序線程進行。
而 JxBrowser 是線程安全的。您可以在不同線程中安全地使用 JxBrowser 對象。不過,在 JavaFX 應用線程中調用 JxBrowser API 時需謹慎,因為它的許多方法是阻塞式的。為了避免影響用戶體驗,我們通常建議不要在 JavaFX 應用線程中調用 JxBrowser。
遷移
創建瀏覽器
JavaFX 提供了可視化的 WebView
組件(可添加到場景中),以及非可視的 WebEngine
(包含實際的 Browser API)。
創建和使用方法如下:
WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();
webEngine.load("https://example.com");
scene.getRoot().getChildren().add(webView);
JxBrowser 同樣由可視化和非可視化部分組成。該庫提供了非可視化的 Engine
和 Browser
對象,它們封裝了 Browser API;還提供了一個可視化的 BrowserView
組件,可將其添加到場景中以顯示加載的 Web 內容。
// 非可視化部分:
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
不會關閉瀏覽器并釋放所有已分配的資源。你必須手動關閉 Browser
和 Engine
對象:
JavaFX
scene.getRoot().getChildren().add(webView);
JxBrowser
// 關閉單個 Browser。
browser.close();// 關閉 Engine。此操作將自動關閉其包含的所有 Browser。
engine.close();
頁面導航
WebEngine
中的導航功能幾乎可以直接轉換為 JxBrowser 的調用方式:
JavaFX
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
Navigation navigation = browser.navigation();
navigation.loadUrl("https://example.com");
navigation.reload();navigation.goBack();
navigation.goForward();// 跳轉到指定的歷史記錄索引
navigation.goToIndex(2);
導航監聽器
在這兩種解決方案中,加載過程都是在后臺進行的,因此需要注冊監聽器來檢測加載何時完成。
在 JavaFX 中,可以通過監聽加載工作器的狀態來實現:
var worker = webEngine.getLoadWorker();
worker.stateProperty().addListener((ov, oldState, newState) -> {if (newState == State.SUCCEEDED) {// 此處可以執行 JavaScript 并訪問 DOM 樹。} else {System.out.println("導航失敗!");}
});
在 JxBrowser 中,通知更加精細:
// 當導航操作完成時會觸發該事件。
// 此時 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
// 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
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 類型(例如函數、Promise
、ArrayBuffer
等)提供了專用的 Java 類型。在類型轉換指南中可查看完整列表。
對于用于訪問帶索引的 JavaScript 對象的 JSObject.getSlot()
和 JSObject.setSlot()
方法,JxBrowser 沒有直接的替代方案。
JavaScript 對象的生命周期
在 JavaFX 和 JxBrowser 中,只要對應的 JavaScript 對象還存在,JSObject
和 JsObject
實例就能正常工作。當 JavaScript 對象被垃圾回收或所在的 frame 加載了新的文檔時,該對象就會失效。無論是在 JavaFX 還是 JxBrowser 中,嘗試使用已失效的 JavaScript 對象都會拋出異常。
JavaScript 對象被垃圾回收的時間難以預測,因此在 JxBrowser 中,傳遞給 Java 的 JavaScript 對象會被保護,防止被垃圾回收,直到新文檔加載時才會關閉。若想釋放對該 JavaScript 對象的引用,使其可以被垃圾回收,可以調用 JsObject.close()
方法:
JsObject persistentObject = frame.executeJavaScript("dialogs");
persistentObject.close();
從 JavaScript 調用 Java
要從 JavaScript 調用 Java 代碼,需要將 Java 對象注入到 JavaScript 環境中。這兩種方案中的做法非常相似:
JavaFX
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
@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 訪問:
// 類的所有公共成員都將可被訪問。
@JsAccessible
public class AccessibleClass {public String sayHelloTo(String firstName) {...}
}// 僅該類的單個方法可被訪問。
public class RestrictedClass {@JsAccessiblepublic String sayHelloTo(String firstName) {...}
}
對于無法添加注解的類(例如標準庫類),可以使用以下方式使其可訪問:
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
// 配置代理設置。
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
// 配置代理設置。
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
var document = webEngine.getDocument();
var element = document.getElementById("exit-app");
((EventTarget) element).addEventListener("click", listener, false);
JxBrowser
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
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
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
webEngine.setUserAgent("custom user agent");
JxBrowser
// 在 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
webEngine.setUserDataDirectory(new File("/path/to/directory"));
JxBrowser
var opts = EngineOptions.newBuilder(HARDWARE_ACCELERATED).userDataDir(Paths.get("/path/to/directory")).build();
var engine = Engine.newInstance(opts);
請注意,同一個用戶數據目錄不能被單個或不同 Java 應用中運行的多個 Engine
實例同時使用。嘗試使用同一個用戶數據目錄將導致 Engine
創建過程中拋出異常。
彈出窗口
在 JavaFX 中,當網頁想要在新窗口中打開內容時,WebEngine
不會創建新窗口。相反,它會用新窗口替換當前加載的頁面。
通過注冊自定義彈出處理器,可以更改此行為:
webEngine.setCreatePopupHandler(features -> {if (noPopups) {// 返回 null 會取消彈出窗口的創建。return null;}// 通過創建新的 WebView,可以指示 JavaFX 為新彈出窗口使用它。var popupView = new WebView();scene.getRoot().getChildren().add(popupView);return popupView.getEngine();
});
在 JxBrowser 中,所有彈出窗口默認都是被抑制的。要更改此設置,需注冊 CreatePopupCallback
:
browser.set(CreatePopupCallback.class, params -> {return noPopups? CreatePopupCallback.Response.suppress(): CreatePopupCallback.Response.create();}
});
如果允許創建彈出窗口且 BrowserView
在 UI 中可見,JxBrowser 會在新的 Stage
中打開彈出窗口。你可以在 OpenBrowserPopupCallback
中自定義此行為:
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
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
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
webEngine.setUserStyleSheetLocation("file:///path/theme.css");
JxBrowser
// 此回調在文檔準備就緒后觸發,此時可注入 CSS。
browser.set(InjectCssCallback.class, params -> {var styles = readFile("file:///path/theme.css")return InjectCssCallback.Response.inject(styles);
});
總結
JavaFX WebView
和 JxBrowser 都提供了類似的功能,從 WebView
遷移至 JxBrowser 不會給您帶來太多麻煩。
在本指南中,我們提供了遷移 WebView
大部分功能的代碼示例,并附上了相關文檔的鏈接。
盡管實際項目中的遷移工作可能會比較復雜,但我們相信通過本指南可以大大簡化這一過程。您可以將其作為遷移項目的起點,如有疑問,歡迎隨時聯系我們。