许多桌面应用程序,如 Slack、Notion、Microsoft Teams 和 Linear,都采用基于 Web 的用户界面。这已成为现代软件开发中的常见做法,开发者可以借助熟悉的 Web 技术构建应用,从而简化开发流程。
在本篇文章中,我们将向您如何基于 shadcn/ui、React、Tailwind CSS 和 TypeScript,开发一款具有现代网页界面的跨平台 Java 桌面应用程序。
Web vs 原生 UI
传统的 Java 原生 UI 工具包(如 Swing、JavaFX 和 SWT)存在诸多局限:
- 维护困难:UI 修改复杂,难以构建现代化的界面。
- 功能缺失:对富样式、动态图形、动画和过渡效果的支持有限。
- 界面陈旧:默认控件外观笨重,开发者不得不自行定制。
这并不令人意外,因为这些工具包设计于多年前,早已无法满足当今界面开发的需求。
相比之下,Web UI 大幅简化了开发流程。它拥有丰富的现成库和成熟的生态系统,同时还有活跃的社区提供支持。借助这些优势,开发者能够轻松实现高 DPI 显示适配、触摸屏交互支持、针对不同屏幕尺寸的自适应布局,以及跨平台统一的界面效果。
将 Web UI 引入桌面应用,不仅带来了网页开发的诸多优势,还避免了与过时 UI 工具包打交道的繁琐,也无需再费力寻找掌握老旧技术的开发者。
Java 桌面应用中的 Web UI 实践
在本文中,我们将构建一个具有 Web 界面的 Java 桌面应用程序。我们将使用 shadcn/ui —— 这是一个基于 React 的组件库,提供即用型、响应式界面元素。同时,编程语言使用 TypeScript。
该应用将展示一个偏好设置对话框,并将用户的设置保存到本地文件系统中,从而在重启后依然保留设置。以下是其在 macOS 上的实际界面截图:
基于 Web UI 的桌面应用界面截图。
完整源代码可在 GitHub 上获取。
在开发基于 Web UI 的桌面应用时,需要解决以下关键问题:
- 可靠的 Web 视图组件:Java 内置组件无法满足现代 Web 标准。
- 无服务器资源加载:避免依赖本地或远程服务器,简化部署流程。
- Java 与 JavaScript 通信:实现文件系统读写等操作,无需 Web 服务器。
应用窗口与 Web 视图
我们将使用 Swing 的 JFrame
创建一个窗口。JFrame
是 Java 的内置组件,简单易用。接着,我们将从 JxBrowser 添加一个基于 Chromium 的 Web 视图组件,用于在此窗口中显示 Web 内容。
以下是将 Web 视图添加到 Java 窗口的方法:
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);
});
在桌面应用中加载 Web UI
Web 部分是一个标准的 React 应用,我们将其加载到上面创建的浏览器组件中。
加载 Web UI 的方式取决于当前是开发环境还是生产环境。在开发环境中,我们使用典型的 Web 开发流程:启动本地开发服务器,支持热更新和其他常见功能。而在生产环境中,我们需要一个压缩后的、可离线运行的安全版本,避免依赖任何本地或远程服务器。
开发环境
在开发环境中加载 Web 应用非常直接。我们只需启动本地开发服务器并导航到 localhost:
./gradlew startDevServer
if (!AppDetails.isProduction()) {
browser.navigation().loadUrl("http://localhost:[port]");
} else {
...
}
生产环境
在生产环境中,我们不能继续使用本地开发服务器的方式,因为此时服务器已不存在。同时,使用远程服务器也不可取。我们的目标是让 Web 应用具备离线运行能力。
此外,无论是本地还是远程服务器,都会带来额外的安全风险:用户可能通过浏览器直接访问 Web 应用的地址,从而查看其源代码,暴露敏感逻辑。显然,这并非我们所希望的。我们希望 Web UI 只能在桌面应用内部访问,并将其源代码完全隐藏在应用包中。
为了实现这一点,我们将 Web 应用文件添加到资源中,并使用 JxBrowser 的 UrlRequest
拦截器 API (Interceptor API)从类路径提供这些文件。
为了允许请求拦截器处理 Web 资源请求,我们将其分配给一个自定义协议:
var options = EngineOptions.newBuilder(HARDWARE_ACCELERATED)
.addScheme(Scheme.of("jxb"), new UrlRequestInterceptor());
var engine = Engine.newInstance(options.build());
每当浏览器尝试加载 jxb://
URL 时,拦截器就会处理该请求,并从应用的资源目录中返回 index.html
。之后,网页请求 CSS、JavaScript 文件等其他资源时,拦截器也会一并处理并返回相关文件。
通过这种方式,所有 Web 资源的加载都发生在应用内部,外部无法拦截,也无法访问这些资源。
同时,使用自定义协议可确保常规 HTTPS/XHR 请求(例如身份验证、API 调用)能够顺利通过。 为了在开发 URL 和生产 URL 之间切换,我们使用以下方法:
if (!AppDetails.isProduction()) {
browser.navigation().loadUrl("http://localhost:[port]");
} else {
browser.navigation.loadUrl("jxb://my-app.com");
}
Web 与 Java 的通信
就像任何 Web 应用程序与后端通信一样,我们的 Web 应用程序也需要与负责读取偏好设置并将其保存到文件系统的 Java 代码进行通信。
JxBrowser JavaScript-Java Bridge API
最直接的方式是从 JavaScript 直接调用 Java 代码。大多数 Web 视图组件都支持这一功能:有的通过 JSON 对象进行数据交换,有的,如 JxBrowser,甚至允许直接调用 Java 方法、访问 Java 对象。
以下是在 JxBrowser 中设置 JavaScript 和 Java 之间通信的方法:
@JsAccessible
class PrefsService {
void setFontSize(int size) {
}
}
declare class PrefsService {
setFontSize(size: number): void;
}
declare const prefService: PrefsService;
...
prefsService.setFontSize(12);
这种方式适用于小型项目。当通信方法较少时,维护起来尚属轻松,也不容易出错。
但随着项目规模的扩大,这种方式变得越来越难以管理,也更容易出错。由于缺乏编译期检查和自动补全支持,出错的可能性增加,同时难以及时发现它们。对于大型项目,我们需要一种更稳健的通信协议以及自动生成代码的机制,以确保可靠性——否则,我们最终会陷入大量的 bug 之中。
在本节中,我们将提出一个更好的解决方案。
Protobuf + gRPC
我们决定采用 Protobuf 方案。这项技术使我们能够以简单的格式定义 API,并自动生成类型安全的客户端和服务器代码。我们在 .proto
文件中定义消息与服务,随后 Protobuf 会为我们自动生成对应的 Java 和 TypeScript 代码。
service PrefsService {
rpc SetFontSize(FontSize) returns (google.protobuf.Empty);
}
enum FontSize {
SMALL = 0;
DEFAULT = 1;
LARGE = 2;
}
这种方式建立了一个稳定且类型安全的通信契约,同时还会提供一些有助于加快开发速度的有用功能,例如代码自动补全和类型检查。
我们使用 gRPC 作为可以发送/接收 Protobuf 消息的传输层。通常情况下,Java 端运行 gRPC 服务器,而 Web 端充当客户端。
class PrefsService extends PrefsServiceImplBase {
@Override
public void setTheme(Theme request, StreamObserver<Empty> responseObserver) {
}
}
...
var serverBuilder = Server.builder()
.http(50051)
.service(GrpcService.builder()
// 注册服务器的实际实现。
.addService(new PrefsService())
.build());
try (var server = serverBuilder.build()) {
server.start();
server.blockUntilShutdown();
}
当服务器启动后,下一步就是连接 gRPC 客户端。
import {createGrpcWebTransport} from "@connectrpc/connect-web";
import {createClient} from "@connectrpc/connect";
import {PreferencesService} from "@/gen/prefs_pb.ts";
const transport = createGrpcWebTransport({
baseUrl: `http://localhost:50051`,
});
const prefsClient = createClient(PrefsService, transport);
然后,可以使用 prefsClient
来接收和更新偏好设置:
prefsClient.setFontSize(FontSize.SMALL);
通信示意图。
总结
基于 Web UI 的桌面应用开发具有显著优势:
- 简化开发:丰富的组件库和活跃的社区支持。
- 现代化界面:轻松实现响应式设计、动画效果等。
- 跨平台一致性:界面在不同操作系统上表现一致。
在本文中,我们构建了一个简单的 Java 桌面应用程序,其 UI 使用 shadcn/ui、React、Tailwind CSS 和 TypeScript 创建。
在开发过程中,我们解决了构建 Web UI 桌面应用中开发者面临的几项关键难题,包括:
- 在 Java 窗口中嵌入 Web 视图以展示 Web UI。
- 无需依赖本地或远程服务器加载 Web 资源。
- 实现 Java 与 JavaScript 的通信。
带有 Web UI 的桌面 Java 应用程序如下所示:
发送中。。。
您的个人 JxBrowser 试用密钥和快速入门指南将在几分钟内发送至您的电子邮箱。