使用 Shadcn UI 构建 Java 桌面应用
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 渲染视图(Java 自带 WebView 标准落后)
- 无服务化静态资源加载,生产环境不依赖本地服务
- Java 与前端双向通信,文件读写等底层操作交由 Java 处理
窗口与 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 服务,支持热更新:
./gradlew startDevServer
if (!AppDetails.isProduction()) {
browser.navigation().loadUrl("http://localhost:[port]");
}
生产环境
生产环境禁止使用 dev 服务:额外占用进程、存在源码泄露安全风险。
解决方案:JxBrowser 自定义协议拦截器,前端资源打包进 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 网络请求
环境加载区分代码:
if (!AppDetails.isProduction()) {
browser.navigation.loadUrl("http://localhost:[port]");
} else {
browser.navigation.loadUrl("jxb://my-app.com");
}
Java ↔ Web 通信
前端需要调用 Java 底层能力完成本地配置文件读写,提供两种通信方案。
方案一:JS-Java Bridge(小项目够用)
JxBrowser 原生支持 JS 直接调用 Java 暴露方法。
Java 侧:
@JsAccessible
class PrefsService {
void setFontSize(int size) {
}
}
TypeScript 侧:
declare class PrefsService {
setFontSize(size: number): void;
}
prefsService.setFontSize(12);
缺点:接口数量多后,无编译校验、缺少自动类型提示,维护成本大幅上涨。
方案二:Protobuf + gRPC(正经项目首选)
通过 .proto 文件统一定义接口,自动生成类型安全 Java / TS 两端代码。
proto 定义示例:
service PrefsService {
rpc SetFontSize(FontSize) returns (google.protobuf.Empty);
}
enum FontSize {
SMALL = 0;
DEFAULT = 1;
LARGE = 2;
}
Java gRPC 服务端:
class PrefsService extends PrefsServiceImplBase {
@Override
public void setTheme(Theme request, StreamObserver<Empty> responseObserver) {
}
}
TypeScript Web 客户端:
const transport = createGrpcWebTransport({
baseUrl: `http://localhost:50051`,
});
const prefsClient = createClient(PrefsService, transport);
prefsClient.setFontSize(FontSize.SMALL);
方案优势:类型安全、代码自动生成、IDE 完整补全、编译期报错拦截,项目规模越大收益越高。









