使用 Shadcn UI 构建 Java 桌面应用

使用 Shadcn UI 构建 Java 桌面应用

使用 Shadcn UI 构建 Java 桌面应用

别把 Shadcn UI 当成 JavaFX 皮肤,它不是。

它跑不进 VBox ,也不能塞进 JPanel 。这东西第一眼就得看清楚:Shadcn UI 给你的不是一个 Java 控件库,而是一套可改、可拷、可维护的 React/Tailwind UI 组件。官方定位也是开放代码、可定制、可扩展的组件基础。

所以用它做 Java 桌面应用,路子绝不是:

new ShadcnButton()

没有这玩意。桌面开发如果还被框在传统的 Swing 或 JavaFX 组件体系里,做出来的东西不仅耗时,而且界面往往缺乏现代感。

认清本质:本地 HTTP 通信桥梁

真正靠谱的路子是:Java 做桌面壳和本地能力,Shadcn UI 做界面,二者通过本地 HTTP 接口通信。

我一般不会一上来就搞什么复杂桥接。桌面应用最怕后面调试困难,前端一报错、Java 一报错,两边互相甩锅。用本地 HTTP 最直接,日志也好看。这种分离式的架构设计,让前后端各司其职,有效降低了耦合度。

大概结构是这样:

desk-app/
java-app/
src/main/java/...
ui/
dist/
index.html
assets/...

HTTP通信桥梁架构

前端用 Shadcn UI 写完后,打包成静态文件,Java 启动时把 dist 目录挂起来,然后 WebView 或 JCEF 加载本地地址。这里我用 JavaFX WebView 写个壳,内部工具够用。要是界面比较重,或者依赖更完整的 Chromium 能力,我会换 JCEF,不在这个点上硬撑。

服务端实现:启动本地 HTTP

Java 启动本地服务这段别写太花,先把静态资源和接口分清楚。使用轻量级的 HttpServer 足以应付大多数本地调用场景:

public final class LocalDeskServer {

private final Path uiDir;
private HttpServer server;

public LocalDeskServer(Path uiDir) {
this.uiDir = uiDir;
}

public int start() throws IOException {
// 特意只监听本地 127.0.0.1,防止局域网暴露
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);

server.createContext("/api/profile", exchange -> {
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(405, -1);
return;
}

String body = """
{"name":"warehouse-admin","role":"local","theme":"dark"}
""";

byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json;charset=utf-8");
exchange.sendResponseHeaders(200, bytes.length);

try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
});

server.createContext("/", exchange -> {
String reqPath = exchange.getRequestURI().getPath();
if (reqPath.equals("/")) {
reqPath = "/index.html";
}

Path file = uiDir.resolve(reqPath.substring(1)).normalize();
if (!file.startsWith(uiDir) || !Files.exists(file)) {
exchange.sendResponseHeaders(404, -1);
return;
}

String contentType = guessType(file);
byte[] bytes = Files.readAllBytes(file);

exchange.getResponseHeaders().set("Content-Type", contentType);
exchange.sendResponseHeaders(200, bytes.length);

try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
});

server.start();
return server.getAddress().getPort();
}

private String guessType(Path file) {
String name = file.getFileName().toString();
if (name.endsWith(".html")) return "text/html;charset=utf-8";
if (name.endsWith(".js")) return "application/javascript;charset=utf-8";
if (name.endsWith(".css")) return "text/css;charset=utf-8";
if (name.endsWith(".svg")) return "image/svg+xml";
return "application/octet-stream";
}

public void stop() {
if (server != null) {
server.stop(1);
}
}
}

这段代码里我特意只监听 127.0.0.1 ,别偷懒绑 0.0.0.0 。桌面应用开本地端口已经够敏感了,再把接口暴露到局域网,后面排查安全问题会很难看。

使用 Shadcn UI 构建 Java 桌面应用

窗口启动也别复杂,直接用 JavaFX 加载端口:

public class DeskLauncher extends Application {

private LocalDeskServer localServer;

@Override
public void start(Stage stage) throws Exception {
Path ui = Paths.get("ui", "dist").toAbsolutePath();

localServer = new LocalDeskServer(ui);
int port = localServer.start();

WebView webView = new WebView();
webView.getEngine().load("http://127.0.0.1/" + port + "/");

Scene scene = new Scene(webView, 1180, 760);
stage.setTitle("库存核对工具");
stage.setScene(scene);
stage.show();
}

@Override
public void stop() {
if (localServer != null) {
localServer.stop();
}
}

public static void main(String[] args) {
launch(args);
}
}

权限与异常边界:远离前端直读文件的坑

前端 Shadcn UI 页面里就按正常 Web 应用写,按钮、表格、弹窗都走 React 组件。比如页面加载时请求:

fetch("/api/profile")
.then(r => r.json())
.then(user => setUser(user))

这里有个坑我得单独拎出来。

不要让前端直接读本地文件,也不要想着在 JS 里拼路径访问用户目录。权限边界一乱,后面必出脏问题。文件选择、数据库、串口、打印、本地缓存,这些都让 Java 做。前端只管发请求。

比如导入一个 CSV,前端点按钮后调:

POST /api/import

Java 这边打开文件选择器、校验字段、写入本地 SQLite 或者 H2。失败就返回明确错误,不要给前端一个空白 500。

桌面端最怕这种日志:

java.io.FileNotFoundException: data.csv
frontend: TypeError Cannot read properties of undefined

这俩放一起看,很烦。还不如 Java 直接把错误收口:

record ApiError(String code, String message) {}

private void writeJson(HttpExchange exchange, int status, String json) throws IOException {
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json;charset=utf-8");
exchange.sendResponseHeaders(status, bytes.length);
try (OutputStream out = exchange.getResponseBody()) {
out.write(bytes);
}
}

我不太建议一开始就把 Java 和前端绑得特别死。比如 Java 调 JS、JS 再调 Java 对象,看着高级,调试时很恶心。HTTP 慢一点,但桌面工具大多数场景不差这点耗时,换来的是边界清楚。

打包与交付:专注业务价值

打包的时候再处理 Java 运行时。 jpackage 可以把 Java 应用和运行时一起打成平台相关安装包,Windows、macOS、Linux 都有对应格式,但它不是跨平台一次构建所有包,在哪个平台上构建,就产出那个平台的包。

命令大概长这样:

jpackage \
--name StockDesk \
--input target \
--main-jar stock-desk.jar \
--main-class com.dong.demo.DeskLauncher \
--type app-image

Shadcn UI 放在 Java 桌面应用里,价值不是“炫”。它真正省的是后台工具那堆难看的表单、筛选区、弹窗、状态 Badge。以前 Swing 写个像样点的设置页,光布局就够折腾半天。现在前端把界面做干净,Java 专心处理本地能力,这个分工我觉得更顺手。

别追求纯 Java。

桌面应用交付给用户看的不是技术血统,是启动速度、界面一致性、异常能不能说人话。Shadcn UI 负责好看,Java 负责稳,中间用一层本地 HTTP 隔开,这个方案不花哨,但能落地。

💻 配套实训环境与动手练习

本文涉及的相关技术指令、开发环境与工具链已内置在边学边练在线实验室中,无需繁琐安装配置,随时在浏览器中实践体验:

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

最新文章

热门文章

本栏目文章