⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7505713576684388367 「TeamDev」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

Slack、Notion、Teams、Linear——这些桌面应用有什么共同点?它们的 UI 全是 Web 技术写的。 这不是偷懒,而是务实的工程选择。

本文用 shadcn/ui + React + TypeScript 构建一个跨平台的 Java 桌面应用,解决三个核心问题:怎么嵌、怎么加载、怎么通信。完整源码在 GitHub。

Swing/JavaFX 为什么不够用了?

Java 的原生 UI 工具包(Swing、JavaFX、SWT)有几个共同的硬伤:

  • 改 UI 痛苦:想实现个现代化的动画过渡?自己造轮子
  • 生态荒漠:组件库少、社区活跃度低、招人也难
  • 颜值掉队:默认控件的外观停留在十年前

Web UI 恰好是反面:组件库多到选不过来、高 DPI / 触屏 / 响应式开箱支持、跨平台一致性天然具备。 把 Web 技术塞进桌面壳子里,既能用上前端生态的红利,又不用再和过时的 UI 框架搏斗。

整体方案:JxBrowser + React + shadcn/ui

我们要构建的是一个偏好设置对话框,用户选好设置后保存到本地文件系统,重启后依然保留。

基于 Web UI 的桌面应用界面截图

要让这套东西跑起来,得解决三件事:

  1. 可靠的 Web 视图:Java 内置的 WebView 跟不上现代 Web 标准
  2. 无服务器加载:生产环境不依赖本地/远程服务器
  3. Java ↔ JS 通信:读写文件系统等操作不走 Web 服务器

窗口与 Web 视图

用 Swing 的 JFrame 建窗口,塞一个 JxBrowser 提供的 Chromium 内核 Web 视图进去:

var engine = Engine.newInstance(HARDWARE_ACCELERATED);
var browser = engine.newBrowser();

SwingUtilities.invokeLater(() -> {
var view = BrowserView.newInstance(browser);
var frame = new JFrame("Application");
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
engine.close();
}
});
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
frame.add(view, BorderLayout.CENTER);
frame.setSize(1280, 900);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});

资源加载:开发 vs 生产

Web 部分就是个标准的 React 应用,但加载方式在开发和生产环境完全不同。

开发环境

直接起 dev server,热更新照常用:

./gradlew startDevServer
if (!AppDetails.isProduction()) {
browser.navigation().loadUrl("http://localhost:[port]");
}

生产环境

生产环境不能用 dev server——不仅多一个进程,还有安全隐患:用户可以通过浏览器直接访问 localhost 看到源码。

解法:用 JxBrowser 的自定义协议拦截器,把 Web 资源打包进 JAR,通过 jxb:// 协议从 classpath 直接提供:

var options = EngineOptions.newBuilder(HARDWARE_ACCELERATED)
.addScheme(Scheme.of("jxb"), new UrlRequestInterceptor());
var engine = Engine.newInstance(options.build());

浏览器请求 jxb://my-app.com → 拦截器从资源目录返回 index.html → 后续 CSS/JS 请求同理。所有加载发生在应用内部,外部无法访问。 同时不影响正常的 HTTPS/API 请求。

if (!AppDetails.isProduction()) {
browser.navigation().loadUrl("http://localhost:[port]");
} else {
browser.navigation.loadUrl("jxb://my-app.com");
}

Java ↔ Web 通信

Web 前端需要调用 Java 代码来读写偏好设置文件。两种方案:

方案一:JS-Java Bridge(小项目够用)

JxBrowser 支持从 JavaScript 直接调 Java 方法:

@JsAccessible
class PrefsService {
void setFontSize(int size) { }
}

// TypeScript 侧
declare class PrefsService {
setFontSize(size: number): void;
}
prefsService.setFontSize(12);

通信方法少的时候没问题。一旦接口多了,没有编译期检查、没有自动补全,维护成本指数上升。

方案二:Protobuf + gRPC(正经项目首选)

用 .proto 文件定义 API,自动生成类型安全的 Java 和 TypeScript 代码:

service PrefsService {
rpc SetFontSize(FontSize) returns (google.protobuf.Empty);
}

enum FontSize {
SMALL = 0;
DEFAULT = 1;
LARGE = 2;
}

Java 端起 gRPC 服务器,Web 端用 Connect 客户端连接:

// Java 服务端
class PrefsService extends PrefsServiceImplBase {
@Override
public void setTheme(Theme request, StreamObserver<Empty> responseObserver) { }
}

// TypeScript 客户端
const transport = createGrpcWebTransport({
baseUrl: `http://localhost:50051`,
});
const prefsClient = createClient(PrefsService, transport);
prefsClient.setFontSize(FontSize.SMALL);

通信示意图

好处很明确:类型安全、代码自动生成、IDE 补全、编译期检查——项目越大越值。

参考资料

GitHub:

https://github.com/TeamDev-IP/JxBrowser-Gallery

文章目录
  1. 1. Swing/JavaFX 为什么不够用了?
  2. 2. 整体方案:JxBrowser + React + shadcn/ui
  3. 3. 窗口与 Web 视图
  4. 4. 资源加载:开发 vs 生产
    1. 4.1. 开发环境
    2. 4.2. 生产环境
  5. 5. Java ↔ Web 通信
    1. 5.1. 方案一:JS-Java Bridge(小项目够用)
    2. 5.2. 方案二:Protobuf + gRPC(正经项目首选)