使用 Shadcn UI 构建 Java 桌面应用

使用 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 。桌面应用开本地端口已经够敏感了,再把接口暴露到局域网,后面排查安全问题会很难看。

窗口启动也别复杂,直接用 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 桌面应用

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


公众号二维码

长按二维码关注 “边学边练”

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

最新文章

热门文章

本栏目文章