使用 Shadcn UI 构建 Java 桌面应用

1.1k
Category: 
开发交流

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

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

Swing/JavaFX 为什么不够用了?

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

  1. 改 UI 痛苦:想实现现代化动画过渡,需要自己造轮子
  2. 生态荒漠:组件库少、社区活跃度低、相关岗位招人难
  3. 颜值掉队:默认控件外观停留在十年前

Web UI 恰好相反:组件库丰富、高 DPI / 触屏 / 响应式原生支持、跨平台界面一致性天然具备。
把 Web 技术封装到桌面壳子里,既能复用前端完整生态,又不用和老旧原生UI框架对抗。

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

本文目标:构建一个偏好设置对话框,用户配置后持久化保存到本地文件,重启应用配置不丢失。

核心需要解决三大问题:

  1. 提供可靠现代标准的 Web 渲染视图(Java 自带 WebView 标准落后)
  2. 无服务化静态资源加载,生产环境不依赖本地服务
  3. 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());

流程:

  1. 前端访问 jxb://my-app.com
  2. 拦截器从项目资源目录读取并返回 index.html
  3. CSS、JS、图片等静态资源同理拦截读取
  4. 所有资源读取逻辑封闭在应用内部,外部无法访问;不影响正常 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 完整补全、编译期报错拦截,项目规模越大收益越高。

Comments 1
/ 1000
Trending
New
这个思路挺新颖的
Like
Comment
3
1
Favorite