JavaFX 嵌套 WebView 加载本地 Html 与 JS 互相调用传参 home 编辑时间 2023/08/30 ![Banner](https://leanote.zzzmh.cn/api/file/getImage?fileId=64eebc79da74050014007df9) ## 前言 很久以前听说过一种写法 比如写个安卓程序,刚学几天还不太会写安卓,就把每个页面都写成本地的html,再用WebView嵌套。 图方便,运行效率低,用户体验差,曾几何时,这是一种很糟嫌弃的写法 没想到,等我学JavaFX,学到吐血的时候,发现JavaFX也可以WebView嵌套Html 比起百度半天答非所问 全英文文档啃到吐血 html我不到5分钟就能写一个页面 一不小心真香了属于是 `嘲笑WebView` => `质疑WebView` => `理解WebView` => `成为WebView` ![滑稽](/api/file/getImage?fileId=64eebb72da74050014007df8) <br><br> ## 折腾 最终目的是希望写个Demo 前端展示全部用本地的Html文件 后端逻辑控制全部用Java和JS交互实现 以达到一个节省学习JavaFX前端组件的目的 <br> 环境大致是这样 系统 `LinuxDeepin 20.9` IDE `IntelliJ IDEA 2023.1.2 (Ultimate Edition)` 环境 `OpenJDK 11.0.9` <br> **新建JavaFX项目** 上一篇文章详细讲过了,这里略过 地址: [2023 IDEA 开发桌面图形界面程序 JavaFX 并导出 Jar 支持多系统](https://zzzmh.cn/post/eee76yv5u9e48dkarmzymnfryohapwm0) <br> **稍作修改** 由于不用写JavaFX的组件模块,直接简化开发,删除了fxml和相关依赖代码,全部用Java代码即可搞定 maven核心依赖是这三个 `pom.xml` ```xml <dependencies> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>17.0.6</version> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-web</artifactId> <version>17.0.6</version> </dependency> </dependencies> ``` 手动引入 `javafx.web` 到模块化 `module-info.java` ```java module com.zzzmh.jfx { requires javafx.controls; // 手动引入到模块化 requires javafx.web; exports com.zzzmh.jfx; } ``` 删除了不需要的文件 最终目录如下 ```treeview JFX-WebView-Demo/ |-- src/ | |-- main/ | | |-- java/ | | | |-- com.zzzmh.jfx/ | | | | |-- controller/ | | | | | |-- Controller.java | | | | |-- App.java | | | |-- module-info.java | | |-- resources/ | | | |-- com.zzzmh.jfk/ | | | | |-- index/ | | | | | |-- index.html `-- pom.xml ``` `index.html` 先随便写点东西占位 ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>JavaFX WebView Demo</title> </head> <body> <h1>测试</h1> </body> </html> ``` 最后改下启动类 把fxml去掉,改为WebView作为最外层Panel WebView直接加载本地的index.html显示 ```java package com.zzzmh.jfx; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.web.WebView; import javafx.stage.Stage; public class App extends Application { @Override public void start(Stage stage) { WebView webView = new WebView(); webView.getEngine().load(getClass().getResource( "/com/zzzmh/jfx/html/index.html").toExternalForm()); Scene scene = new Scene(webView, 600, 400); stage.setTitle("JavaFX WebView Demo"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(); } } ``` 执行`App.java` `main`方法 一次跑通! ![截图](/api/file/getImage?fileId=64eee1e5da74050014007e23) 既然能用html作为前端显示,那写个页面就是分分钟的事情了 目前只剩下最后一个问题 不像node js可以直接调用系统api 这里的js只是webview内部的脚本 连接系统的是靠java代码 如果用传统方法,java写个api接口,js调接口,相当于是机关枪打蚊子 所以下一步的思路就是js直接调用java方法,实现交互逻辑 <br> **JS调用Java方法 前后端交互** 参考了这几个链接 [JavaFX学习之在javascript中调用javaFX中提供的java方法](https://blog.csdn.net/zy103118/article/details/127425406) [JavaFx Webview 与js(vue)交互](https://blog.csdn.net/weixin_44517645/article/details/128180261) 大致代码如下 `Controller.java` ```java package com.zzzmh.jfx.controller; public class Controller { /** * 获取后端数据 */ public String getData() { // 这里假装去数据库查询了一套json数据 return "{\"name\":\"张三\",\"age\":9}"; } } ``` <br> `App.java` ```java package com.zzzmh.jfx; import com.zzzmh.jfx.controller.Controller; import javafx.application.Application; import javafx.beans.value.ObservableValue; import javafx.concurrent.Worker; import javafx.scene.Scene; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; import netscape.javascript.JSObject; public class App extends Application { @Override public void start(Stage stage) { WebView webView = new WebView(); WebEngine engine = webView.getEngine(); // 注入方法 engine.getLoadWorker().stateProperty().addListener( (ObservableValue<? extends Worker.State> ov, Worker.State oldState, Worker.State newState) -> { if (newState == Worker.State.SUCCEEDED) { // 获取JS的window对象 JSObject window = (JSObject) engine.executeScript("window"); // 讲controller注入到window对象中 window.setMember("controller", new Controller()); } }); // 加载页面 engine.load(Controller.class.getResource( "/com/zzzmh/jfx/html/index.html").toExternalForm()); Scene scene = new Scene(webView, 600, 400); stage.setTitle("JavaFX WebView Demo"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(); } } ``` <br> `index.html` ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>JavaFX WebView Demo</title> </head> <body> <h4>功能测试</h4> <button onclick="getData()">获取数据</button> <div id="data"></div> <script> function getData(){ document.querySelector('#data').innerText += window.controller.getData(); } </script> </body> </html> ``` 然后就遇到问题了 无论怎么点`获取数据`按钮都没有反应 后端的 `public String getData()` 方法里打断点,也没任何反应 关键看不到报错信息,Java的控制台没报错,前端JS的控制台看不到 百度半天也没查到有用信息 于是没办法我只能用一个笨办法来查报错了 前端html里加个try,从div输出报错看看 `index.html` ```html <button onclick="getData()">获取数据</button> <div id="data"></div> <script> function getData(){ try{ document.querySelector('#data').innerText += window.controller.getData(); }catch (e){ document.querySelector('#data').innerText += e; } } </script> ``` 错误信息终于打出来了 `java.lang.IllegalAccessException: module javafx.web cannot access class com.zzzmh.jfx.controller.Controller (in module com.zzzmh.jfx) because module com.zzzmh.jfx does not open com.zzzmh.jfx.controller to javafx.web` 好家伙!这问题不用百度我就知道了,模块化里没写opens,只能说怪我 `java 11` 没学好,知识水平还永久性的停留在了`java 8` <br> 补上模块化配置 `module-info.java` ```java module com.zzzmh.jfx { requires javafx.controls; requires javafx.web; requires jdk.jsobject; // 关键是这行代码 opens com.zzzmh.jfx.controller to javafx.web; exports com.zzzmh.jfx; } ``` 解决完这个问题,终于是跑通了,js可以直接获得java的数据,这样java连数据库就可以获取数据库里的数据了 ![截图](/api/file/getImage?fileId=64eef82cda74050014007e4e) <br> 到这里基本已经大功告成了 顺手再写几个可能以后用得到的简单方法 `Controller.java` ```java package com.zzzmh.jfx.controller; import javafx.application.Platform; import javafx.scene.Scene; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; public class Controller { /** * 获取后端数据 */ public String getData() { // 这里假装去数据库查询了一套json数据 return "{\"name\":\"张三\",\"age\":9}"; } /** * 新开一个窗口 */ public void open() { WebView webView = new WebView(); WebEngine engine = webView.getEngine(); engine.load(Controller.class.getResource( "/com/zzzmh/jfx/html/new.html").toExternalForm()); Scene scene = new Scene(webView, 400, 280); Stage stage = new Stage(); stage.setTitle("新开页面"); stage.setScene(scene); stage.show(); } /** * 彻底退出程序 */ public void exit(){ Platform.exit(); } } ``` (配合上文新开窗口这个功能,需在`resources/com.zzzmh.jfx/html/` 下新开一个 `new.html`,内容随意写点,略过) `index.html` ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>JavaFX WebView Demo</title> </head> <body> <h4>功能测试</h4> <button onclick="getData()">获取数据</button> <button onclick="controller.open()">新开窗口</button> <button onclick="controller.exit()">退出程序</button> <div id="data"></div> <script> function getData(){ document.querySelector('#data').innerText += controller.getData(); } </script> </body> </html> ``` 最终执行效果 没有任何问题 ![截图](/api/file/getImage?fileId=64eef92dda74050014007e50) <br> **打包二进制** 到这里就大功告成了 结束之前再复习一下上节课研究的打包二进制 复习上节课地址: [2023 IDEA 开发桌面图形界面程序 JavaFX 并导出 Jar 支持多系统](https://zzzmh.cn/post/eee76yv5u9e48dkarmzymnfryohapwm0) 先确保一下maven配置正确 完整maven配置如下 关键是mainClass要对其启动类App.java的main方法 `pom.xml` ```xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.zzzmh</groupId> <artifactId>JFX-WebView-Demo</artifactId> <version>1.0-SNAPSHOT</version> <name>JFX-WebView-Demo</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <junit.version>5.9.2</junit.version> </properties> <dependencies> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-controls</artifactId> <version>17.0.6</version> </dependency> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-web</artifactId> <version>17.0.6</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>11</source> <target>11</target> </configuration> </plugin> <plugin> <groupId>org.openjfx</groupId> <artifactId>javafx-maven-plugin</artifactId> <version>0.0.8</version> <executions> <execution> <!-- Default configuration for running with: mvn clean javafx:run --> <id>default-cli</id> <configuration> <mainClass>com.zzzmh.jfx/com.zzzmh.jfx.App</mainClass> <launcher>app</launcher> <jlinkZipName>app</jlinkZipName> <jlinkImageName>app</jlinkImageName> <noManPages>true</noManPages> <stripDebug>true</stripDebug> <noHeaderFiles>true</noHeaderFiles> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> ``` 随后在IDEA的maven中执行 `javafx/javafx:run` 看看能否启动成功 如果成功就可以在maven中执行 `javafx/javafx:jlink` 打包 ![截图](/api/file/getImage?fileId=64eefbc7da74050014007e53) 最终会在根目录 `target` 文件夹下得到打包后的文件 其中 `app.zip` 是所有文件的压缩包 只有 `66.1MB` 这是在包含了所有运行环境 即 `OpenJDK 11` 以及 `javafx.web` 浏览器内核 的情况下,以及算非常小了 通过shell命令就可以执行二进制文件,来启动这个桌面程序 ```shell cd target/app/bin ./app ``` 结果在这里遇到一堆问题 1. 之前一直存在的渲染问题,css在初始化页面的时候不生效 2. 直接执行App的main方法和javafx:run都正常,但jlink打包后的程序用shell执行,前端就找不到window下的controller类 最后做了这几个修改解决 在WebView外面套了一层BorderPane,解决了WebView的渲染问题 在`engine.getLoadWorker().stateProperty().addListener`的外面申明Controller类,在里面传入参数(之前是直接在里面new一个类有问题) 修改后最终解决了上述2个问题 完整代码如下 `App.java` ```java public class App extends Application { @Override public void start(Stage stage) { BorderPane pane = new BorderPane(); WebView webView = new WebView(); WebEngine engine = webView.getEngine(); Controller controller = new Controller(); // 注入方法 engine.getLoadWorker().stateProperty().addListener( (ObservableValue<? extends Worker.State> ov, Worker.State oldState, Worker.State newState) -> { if (newState == Worker.State.SUCCEEDED) { // 获取JS的window对象 JSObject window = (JSObject) engine.executeScript("window"); // 讲controller注入到window对象中 window.setMember("controller", controller); } }); // 加载页面 engine.load(Controller.class.getResource( "/com/zzzmh/jfx/html/index.html").toExternalForm()); pane.setCenter(webView); Scene scene = new Scene(pane, 600, 400); stage.setTitle("JavaFX WebView Demo"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(); } } ``` 重新执行了javafx:jlink 用shell cd到bin目录 执行二进制app 成功,功能全部正常 ```shell cd target/app/bin ./app ``` 大功告成 ![](https://leanote.zzzmh.cn/api/file/getImage?fileId=64c9c860da74050014005b69) ## END 本文所有源码已发布到开源平台 源码地址: [https://gitee.com/zzzmhcn/JFX-WebView-Demo](https://gitee.com/zzzmhcn/JFX-WebView-Demo) [https://github.com/zzzmhcn/JFX-WebView-Demo](https://github.com/zzzmhcn/JFX-WebView-Demo) 最后补充一下jlink我目前尚未解决的一些问题 jlink简单来说就是按需引入的打包,把所有需要的环境都打包成二进制文件 可以直接在windows mac linux执行 (目前测试需要在对应系统打包,比如win打包的只能win执行,linux打包只能linux执行) 最大的优点就是小和方便,正常一个程序连依赖20MB,JDK需要200~300MB 而且JDK需要配置环境变量,且占用系统默认的JDK的位置 jlink打包后才30~60mb,就可以实现不入侵系统直接运行二进制文件 目前一个已知的缺点就是模块化,需要所有代码都是模块化开发,才可以jlink打包 目前已知的例如jedis jdbc都不支持模块化,一旦用了这些依赖就无法打包 我百度了说也有解决办法,但我暂时还没时间深入折腾,以后如果研究出来了再更新 送人玫瑰,手留余香 赞赏 Wechat Pay Alipay JavaFX 嵌套 WebView 加载本地 Html 与 JS 互相调用传参 2023 IDEA 开发JavaFX图形界面入门 并导出Jar 支持多系统