JavaFX 嵌套 WebView 加载本地 Html 与 JS 互相调用传参
2023-08-31
阅读 {{counts.readCount}}
评论 {{counts.commentCount}}
## 前言
很久以前听说过一种写法
比如写个安卓程序,刚学几天还不太会写安卓,就把每个页面都写成本地的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都不支持模块化,一旦用了这些依赖就无法打包
我百度了说也有解决办法,但我暂时还没时间深入折腾,以后如果研究出来了再更新