简介
Compose Multiplatform for Web 的宣传语很吸引人:只需编写一次代码(包括 UI),就可以在多个平台上共享你的应用程序,包括 Android、iOS、桌面,是的,甚至是 Web。
在本文的其余部分,我们将 "Compose Multiplatform for Web" 简称为 "C4W"。
如果你是一名 Android 开发者,你可能会想:"等等,我可以直接使用现有的代码库和/或多年的 Jetpack Compose 经验,几乎不需要额外的努力就能拥有一个可用的网站??快让我试试!"
如果你经营一家创业公司,你可能会想:"我只需要雇一个程序员就能同时构建移动应用和网站,还能节省成本??快让我试试!"
相比之下,Compose HTML 是一个较早的、独立的 Compose 实现,它输出 HTML DOM 而不是渲染指令,需要一种完全不同的方法,要求开发者学习一个陌生的 API 以及 HTML/CSS 和 JavaScript 标准库(但是是 Kotlin 化的)。使用 Compose HTML 编写的任何 UI 代码都不能与其他平台共享。
也许不足为奇的是,Compose HTML 已经被降级为 Compose Multiplatform README 底部的一个转瞬即逝的库章节。
然而...
我认为对于许多 Kotlin Web 项目来说,选择 C4W 可能是一个耗时巨大的错误,大多数开发者应该认真考虑使用 Compose HTML。
我是谁?
我是 Kobweb 的作者,这是一个在 Compose HTML 之上添加了一些主观决策的框架,使其更易于使用,并且能够更快地搭建和运行网站。(你可以在这里阅读更多相关信息。)
到目前为止,我已经在 Kobweb 上工作了三年。所以,我当然有偏见,但我相信 Compose HTML 的前景。否则,我早就放弃这个项目了。
我也一直关注着 Kobweb 社区中的讨论,因为一些用户尝试过 C4W 并分享了他们的经验。他们的反馈帮助塑造了我将在下面分享的想法。
C4W 的问题
全屏画布
当你创建一个 C4W 网站时,你实际上是在创建一个托管单个全屏画布元素的简单 HTML 页面。你还需要下载页面本身的脚本以及渲染引擎(通过 Skiko 的 Skia),这使得 C4W 可以在画布上绘图。
画布由图像缓冲区支持,根据用户的窗口大小,通常范围在 8MB 到 30MB 之间。这些缓冲区需要由浏览器管理,浏览器必须在标签页激活或停用时决定是否保持分配状态。当页面调整大小时,缓冲区可能需要不断重新分配。
当你右键点击画布时,默认情况下它会向用户呈现类似于右键点击图片时的选项。这意味着用户将看不到标准的浏览器上下文菜单,其中包含"在新标签页中打开"、"复制链接地址"或"检查"等选项,除非框架正确拦截并认真实现这些功能。
考虑到每个浏览器都有其独特的功能集,而且某些菜单选项可能由自定义扩展提供,满足每个用户的期望是一项不可能完成的任务。
C4W 依赖于画布这一事实也是我们将在本文剩余部分讨论的许多其他问题的根本原因。
SEO / 索引
也许开发者在创建网站时最重要的期望是它最终能被搜索引擎发现。
然而,由于 C4W 通过在不透明的画布中渲染工作,你创建的任何按钮、文本或其他小部件对搜索引擎都是不可见的,无法为页面的索引做出贡献。
当我曾经与 JetBrains 的某人讨论这个问题时,他们提到计划尝试一些方法来解决这个问题,但应该注意到,Flutter(另一个使用全屏画布方法的框架)明确指出它不是 SEO 友好的,也不打算成为 SEO 友好的。
如果谷歌支持的 Flutter 从未解决 SEO 问题,我很怀疑 JetBrains 能否自己解决这个问题。
下载大小
一个中等规模的 Compose HTML 站点编译后大约为 1MB,但这种大小经过压缩后通常在 200-300K 之间。
相比之下,当我检查一个 Flutter Web 应用时,它的渲染引擎需要 2MB 的下载量,而网站本身还需要额外几百 KB。
C4W 应用目前的大小是这个的两倍,约为 5MB。团队可能会在未来缩小这个大小,但很难想象他们能显著超越 Flutter 的当前大小,即使能够匹配也很困难。
如果你需要支付带宽费用或针对网络条件较差的用户,你应该意识到 C4W 可能始终比 Compose HTML 大 10-20 倍。
共享 UI 被高估了
在我的整个职业生涯中,我一直听到关于共享 UI 的承诺,可以追溯到 Java 1995 年的"一次编写,随处运行"口号。
然而,在现实中,每个目标平台通常最终都需要自己的自定义 UI 代码库。
对于早期阶段的产品来说,共享 UI 框架可能确实能帮助小团队很快地接触到大量用户。
然而,一般来说,随着产品的成熟,公司会雇用多个团队来处理不同的平台。如果允许每个团队独立发展,包括做出自己的 UI 决策,他们都能更快地前进。
在我在 YouTube 工作期间,一个中央团队成立,目的是为所有内部产品提供他们应该遵循的 UI 布局。这种方法的一个目标是确保所有平台的一致性。然而,在实践中,各个团队都有独特的硬件限制或截然不同的用户期望,所以这些团队最终会告诉中央团队他们需要什么样的 UI,然后他们必须等待团队发布这些布局。
换句话说,迭代需要与一个忙于服务许多其他团队的团队进行协调。
回想起来,让每个团队构建自己的 UI,然后由一个中央委员会审查以验证共享 UI 指南,可能会减少很多摩擦。
让我们诚实地说:大多数程序员不喜欢编写 UI!这是一项缓慢、精细的工作,很难测试。对他们来说,共享 UI 框架的承诺听起来很动听。
话虽如此,这通常是开发者问题的解决方案,但用户并不关心东西是如何构建的。他们只想要一个在他们首选设备上运行良好且感觉良好的产品。在许多情况下,一旦来自不同平台的用户反馈开始涌入,共享 UI 代码库就会被放弃。
JavaScript 生态系统
JavaScript 生态系统非常庞大。能够访问它是 Kotlin/JS 的一个巨大且被低估的优势。
例如,我在 Droidcon SF '24 上做了一个关于 Kobweb 的演讲,在开始工作之前,我就有信心能找到一个 JavaScript 库来帮我创建幻灯片。
果然,我找到了 Reveal.js,并且能够快速集成它,在几个小时内就建立了一个初步的概念验证。
我还在这个博客中使用 Highlight.js 进行语法高亮。Prism.js 是这个领域的另一个流行库。这两个库都在大量网站上经过了实战测试。
还有许多 JavaScript 图形库可以用来创建令人惊叹的视觉效果、图表和图像。事实上,Kobweb 提供了一个 OpenGL 演示(运行 kobweb create examples/opengl
来查看),它使用 WebGL 渲染一个旋转的立方体。
所有这些例子只是触摸了表面,几乎没有暗示出那些你可以在自己的网站中免费使用的强大 JS 库的所有可能性。
随着时间的推移,支持 Web 的 Compose Multiplatform 库的数量可能会增长,但即便如此,JavaScript 生态系统也有绝对巨大的先发优势,而且它只会在同一时间继续增长。
浏览器开发者工具
Chrome 和 Firefox 都带有一套出色的开发者工具,让你可以检查网站的性能、布局和样式、内存使用情况以及网络请求。
在这些工具中,Chrome 中的 Elements 面板和 Firefox 中的 Inspector 面板显示了网站的 DOM 元素树,让你既能理解每个元素是如何样式化的,也能通过快速调整样式并实时查看效果来进行实验。这些工具在 C4W 网站中变得毫无用处,因为所有样式都发生在画布内部。
Chrome 还提供了全面的 Lighthouse 质量审计工具,它会为你的网站的性能、可访问性以及搜索引擎优化程度打分。这个工具是量化网站健康状况的强大方式,但在处理 C4W 网站时它部分放弃了,因为它无法从不透明画布内的实际内容中获取太多有用信息。
初始渲染时间
页面的初始渲染发生在浏览器完成下载你的网站时。如果存在任何脚本标签,它们也会被下载和运行,通常会进一步修改页面内容。
对于 C4W 和 Compose HTML 来说,初始下载只会显示一个空白屏幕,因为这两种方法都需要执行代码来填充页面。页面的脚本(对于 C4W 来说,还包括渲染引擎)需要下载并运行后,才会向用户显示任何内容。
由于 C4W 的下载大小远大于 Compose HTML,这意味着用户访问你的网站时会看到空白页面的时间更长。
如果你在 Compose HTML 之上使用 Kobweb,它会添加一个导出页面的步骤,对每个页面进行快照并将其烘焙到 HTML 文件中。这意味着当你访问 Kobweb 网站时,它会在 HTML 本身完成下载后立即进行初始渲染,这几乎是瞬间完成的。在此之后,页面脚本将完成下载并运行,尽管这对用户来说通常是不可见的,因为在大多数情况下,脚本只是重新计算已经显示的内容。
Kobweb 的导出过程之所以可行,是因为它使用 DOM。而使用 C4W,用户只能等待所有内容下载完成并且 JavaScript 运行后,页面才会被填充。
浏览器管理的功能
已访问的超链接
网络是建立在超链接之上的,这是用户体验的基本组成部分。当用户看到一个链接时,他们期望它能显示一个颜色来表明他们是否曾经访问过。
事实证明,出于安全考虑,关于用户访问过哪些链接的信息只有浏览器本身知道,开发者无法访问。这是明智的,否则恶意网站可能会查询用户的浏览历史。
因此,为了让未访问和已访问的链接显示不同的样式,你必须使用 CSS 来设置它们的样式。这让你可以指定已访问和未访问的颜色,然后浏览器会适当地应用它们,而不需要开发者的参与。
也许这在你的网站中并不是什么大问题 —— 有些网站出于艺术目的故意禁用已访问链接的颜色。如果是这样,那很好!但如果这个功能对你来说很重要,那么你应该知道 C4W 是不可行的。
密码自动填充
当用户访问一个有密码字段的网站时,浏览器可以为用户提供自动填充密码的功能。
使用 Compose HTML,你只需创建两个带有自动完成参数的文本输入(一个设置为 username
,另一个设置为 currentPassword
),它们就会自动与浏览器集成并填充预期的值。
由于 C4W 使用自己的自定义密码文本字段概念,浏览器将无法与之集成。
无障碍性
网络的建立基于人人都应该能够使用它的理念。因此,所有浏览器和许多 HTML 元素都内置了数十年的无障碍设计。
例如,如果你使用 <button>
元素,它会自动支持键盘焦点和点击。如果你使用 <input>
元素,它会自动支持焦点并允许用户在其中输入内容。
屏幕阅读器也是网络的重要组成部分,它允许视障用户浏览和理解你网站的内容。
当你运行 Lighthouse(参见浏览器开发者工具▲),它会为你的网站的无障碍性打分,帮助你识别可以改进的地方,或者让你确信你做得很好。
Jetpack Compose,以及扩展的 C4W,确实支持无障碍性概念,但严重依赖无障碍工具的用户可能会发现,与大多数其他网站相比,C4W 网站的某些功能可能工作方式不同或完全不工作。
国际语言支持
如果你尝试在 C4W 中默认显示中文,你会看到缺失的字符:
上面的 C4W 示例无法显示 "你好" 和 "nǐ hǎo"
要解决这个问题,你需要加载一个支持所需字符的字体文件,并显式实例化一个使用该字体的 Font
值。
C4W 的默认字体可能是最小化的,以避免显著增加网站的下载大小。支持中文字符的字体很容易就达到至少 10MB,这对许多用户来说将是巨大的浪费。
你可能会想:"好吧,国际字体的下载成本可能很高,但如果应用程序需要它,程序员就应该咬紧牙关把它们包含进来,对吧?"
但是,如果用户只是想使用你的应用程序,比如说一个笔记应用,他们想用自己的语言输入一些文本呢?或者你最终要显示一些外部内容,比如新闻文章或用户评论,恰好使用了国际字符?用户将看到难看的方块。
简单来说,你的 C4W 网站可能会无意中排除那些期望国际语言支持的用户,而除非用户恰好报告这个问题,否则你可能永远都不会知道。
相比之下,Compose HTML(或者说,网络本身)默认使用操作系统提供的系统字体。无需任何额外的下载或你这边的特殊工作,国际文本往往可以开箱即用。
结论
基于上述原因 —— 特别是 SEO / 索引、下载大小和对更广泛的 JS 生态系统的访问 —— 我认为大多数 Kotlin Web 开发者使用 Compose HTML 而不是 Compose Multiplatform for Web 会更好。
需要注意的是,我也听说开发者在尝试 C4W 时遇到了一些重大问题,因为它仍然处于 alpha 状态。此外,带有垃圾回收的 Wasm 目前还处于初期阶段,即使在撰写本文时,Safari 也不支持它,这对某些人来说就已经是一个致命问题。
然而,我没有提到这些问题,因为感觉时间会解决这些问题。
相比之下,我之前列出的所有问题即使在 C4W 稳定和成熟后仍然会存在。
C4W vs Compose HTML
现在,如果你问我哪个 API 更好 —— Jetpack Compose 还是 Compose HTML —— 我强烈倾向于 Jetpack Compose。
Jetpack Compose 提供了一种更现代的构建 UI 的方式,从之前的框架中吸取了经验教训,并提供了非常简洁和富有表现力的布局原语。一旦你掌握了基础知识,它就非常直观易用,并且包含了一些令人难以置信的动画 API。
相比之下,HTML/CSS 承载着大量的历史包袱。有时你可能会感觉被几十年前做出的决定所束缚,在跨越多个时代的功能中摸索,这些功能有时会根据你使用的浏览器提供不同的体验。
话虽如此,Compose HTML 将网络视为其自己的原生平台,坦白说,网络是一个具有显著优势的平台。开发者可以从拥抱它中获得很多好处,也可能因为背离它而失去很多。
当然,也有一些用例 C4W 绝对是正确的工具!
如前所述,如果你是一个早期创业公司,想要接触尽可能多的用户来展示你的应用程序理念,Compose Multiplatform 是一个很好的解决方案。
或者也许你正在为你的公司创建内部工具。编写 Web 应用而不是桌面应用可以帮助你确保每个人都能保持最新版本。而且对于内部团队,你可以要求每个人使用最新版本的 Chrome 或 Firefox,而不用担心失去用户。
在画布中渲染的网站是一个目前由 Flutter 很好服务的大众市场。我很希望看到 C4W 与 Flutter 竞争并获胜,因为我认为 Kotlin 是一种比 Dart 更具表现力和广泛适用性的语言。
然而,如果你正在构建一个网站,而不是恰好运行在网络浏览器上的通用应用程序,Compose HTML 几乎肯定是更好的选择。
共享业务逻辑
虽然选择 Compose HTML 确实需要为你的网站维护一个单独的 UI 代码库,但这并不意味着我完全拒绝代码共享的想法。
事实上,Kotlin Multiplatform 是一种在所有平台之间共享业务逻辑的强大方式,即使是对于 Web 目标也非常适用。
例如,使用 Compose Multiplatform 开发 Android 和 iOS 应用,然后使用 Compose HTML 开发网站可能是一个完全合理的决定,这样你就可以在它们之间共享大量的通用工具代码。
事实上,Kobweb 在全栈网站开发中深度使用 Kotlin Multiplatform,建议用户在公共代码中创建可序列化的数据类,这些数据类可以在服务器(JVM)和客户端(Kotlin/JS)代码库之间共享。你甚至可以在移动应用中使用这些数据类,这些应用可以与 Kobweb 后端 Web 服务器通信!
虽然共享 UI 代码可能被高估了,但共享业务逻辑是非常有效的,而且强烈推荐。
最终思考
最终,我认为 Compose HTML 和 C4W 都应该存在。这些技术互为补充,它们共同赋予任何 Kotlin Web 开发者实现任何目标的能力!
我认为 JetBrains 应该更透明地说明每种方法的权衡和限制,而不是仅仅将 C4W 宣传为一个万能的解决方案。
在这种情况发生之前,希望这篇文章能帮助用户在开始他们自己的 Kotlin Web 项目时做出更明智的决定。