最近在开发一款跨平台的工业软件,需求是需要支持 macOS、Windows 以及 Linux,并且最好能同时兼容 arm 和 x86 架构。因此,我研究了一下现有的跨平台方案。首先排除了 Avalonia,因为它和 WPF 很像,使用的 XAML 语法比较啰嗦。我个人认为,像 SwiftUI 和 Compose 那种现代化的声明式 UI 才是未来的趋势。然而,SwiftUI 不能跨平台,而 Compose Multiplatform 的基础功能也才刚刚完善。综合考虑后,我选择了几个不同的方案并逐一进行了尝试。下面是我测试后得到的结果。

框架特性对比 Link to heading

特性 Compose Multiplatform Flutter Tauri Electron
主要语言 Kotlin Dart Rust + Web技术 JavaScript/TypeScript
UI渲染 原生渲染 自带渲染引擎 系统WebView Chromium
包体积 中等 中等 小 (几MB) 大 (>100MB)
内存占用 中等 中等 低 高
跨平台支持 桌面、Android、iOS、Web 移动端优先,桌面端使用的不多 仅桌面端 仅桌面端
生态系统 发展中 丰富 可用Web生态 非常丰富
开发体验 类SwiftUI,声明式 声明式,但不是SwiftUI那种 Web + Rust Web技术栈
热重载 支持(alpha) 支持 支持 支持
编译速度 快 快 慢 快
调试工具 一般 优秀 一般 优秀
Linux支持 取决于JVM 没试 V1打包报错,V2依赖glibc2.35,系统版本不够新就没法用 完美
适用平台 全平台应用,iOS是beta阶段 移动应用为主 轻量级桌面应用 重型桌面应用
技术复用性 高(Kotlin全栈) 低(仅Flutter生态) 中(Web技术) 中(Web技术)

Compose Multiplatform Link to heading

谷歌在 2021 年 7 月宣布 Android 官方支持 Jetpack Compose(1.0 版本发布),将其作为现代 Android UI 开发工具包推广。随后,JetBrains 扩展为 Compose Multiplatform,支持桌面、Web 和移动端。我测试后发现,它与 SwiftUI 的声明式 UI 风格高度相似,这种语法(称为"声明式编程"或"Declarative UI")是未来的趋势。

让我们通过一个用户信息卡片的实现来对比不同框架的语法差异:

SwiftUI 实现 Link to heading

struct UserCard: View {
    let user: User
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Image(user.avatarName)
                    .resizable()
                    .frame(width: 50, height: 50)
                    .clipShape(Circle())
                
                VStack(alignment: .leading) {
                    Text(user.name)
                        .font(.headline)
                    Text(user.title)
                        .font(.subheadline)
                        .foregroundColor(.gray)
                }
            }
            
            Text(user.bio)
                .font(.body)
                .lineLimit(3)
        }
        .padding()
        .background(Color.white)
        .cornerRadius(10)
        .shadow(radius: 5)
    }
}

Compose Multiplatform 实现 Link to heading

@Composable
fun UserCard(user: User) {
    Column(
        modifier = Modifier
            .padding(16.dp)
            .background(Color.White)
            .clip(RoundedCornerShape(10.dp))
            .shadow(5.dp)
    ) {
        Row(
            modifier = Modifier.padding(8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Image(
                painter = painterResource(user.avatarRes),
                contentDescription = null,
                modifier = Modifier
                    .size(50.dp)
                    .clip(CircleShape)
            )
            
            Column(
                modifier = Modifier.padding(start = 8.dp)
            ) {
                Text(
                    text = user.name,
                    style = MaterialTheme.typography.h6
                )
                Text(
                    text = user.title,
                    style = MaterialTheme.typography.subtitle1,
                    color = Color.Gray
                )
            }
        }
        
        Text(
            text = user.bio,
            style = MaterialTheme.typography.body1,
            maxLines = 3,
            overflow = TextOverflow.Ellipsis
        )
    }
}

React 实现 Link to heading

import styled from 'styled-components';

const Card = styled.div`
  padding: 16px;
  background: white;
  border-radius: 10px;
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
`;

const TopSection = styled.div`
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
`;

const Avatar = styled.img`
  width: 50px;
  height: 50px;
  border-radius: 50%;
  object-fit: cover;
`;

const UserInfo = styled.div`
  display: flex;
  flex-direction: column;
`;

const Name = styled.h3`
  margin: 0;
  font-size: 1.1rem;
  font-weight: 600;
`;

const Title = styled.p`
  margin: 0;
  color: gray;
  font-size: 0.9rem;
`;

const Bio = styled.p`
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
`;

const UserCard: React.FC<UserCardProps> = ({ user }) => {
  return (
    <Card>
      <TopSection>
        <Avatar src={user.avatarUrl} alt={user.name} />
        <UserInfo>
          <Name>{user.name}</Name>
          <Title>{user.title}</Title>
        </UserInfo>
      </TopSection>
      <Bio>{user.bio}</Bio>
    </Card>
  );
};

从代码对比可以看出,SwiftUI 和 Compose Multiplatform 的声明式语法更为简洁优雅,而 React 虽然也是声明式,但需要更多的样式代码和结构分离。

Flutter Link to heading

Flutter 由谷歌推出,已非常成熟,凭借 Dart 和自渲染引擎在移动端表现优异。能快速构建漂亮的 UI,社区活跃。然而,Dart 的经验难以复用到其他领域,除非专职 Flutter 开发,否则投入回报比不高。

适用场景:专注移动端的团队。

Tauri Link to heading

Tauri 使用系统自带 WebView,搭配 Rust 后端,主打轻量和高性能。在 macOS 和 Windows 测试顺利,包体积小、性能略优于 Electron。但 Linux 兼容性存在问题:v1 打包 AppImage 问题多,v2 在 Ubuntu 22.04 以下因 glibc 版本无法运行。

适用场景:不考虑 Linux 或能解决兼容性的团队。

Electron Link to heading

Electron 基于 Chromium 和 Node.js,跨平台支持最完善。只要会 Web 开发就能上手,macOS 上甚至能轻松编译 ARM 版本。但包体积大、性能开销高,移动端经验无法复用。

适用场景:熟悉 Web 技术、追求快速上手的开发者。

我的选择:Compose Multiplatform Link to heading

我最终选择 Compose Multiplatform,原因有三:

  1. Kotlin 优势:语言多用途,后端、前端皆宜
  2. 声明式语法:与 SwiftUI 一致的 Declarative UI 是未来趋势,代码简洁且直观
  3. 未来潜力:JetBrains 的推动加上语法的前瞻性,让我相信它是跨平台开发的未来

如果精通 Web,Electron 是首选;不考虑 Linux,Tauri 的轻量很吸引人;Flutter 则适合专注移动端。但综合来看,Compose Multiplatform 更符合我的长期目标。