进阶 flutter.dev 2026-10-10 05:26:19 · 6 阅读

第21章 Hummingbird:将 Flutter 引入 Web 平台的架构与设计

# Hummingbird:为 Web 打造 Flutter 在今天的 Flutter Live 上,我们宣布正在实验让 Flutter 运行在 Web 上。 Yegor Jbanov 2018年12月4日 · 12 分钟阅读 分享到 X 分享到 Bluesky 分享到 LinkedIn 在今天的 Flutter Live 上,我们宣布正在实验让 Flutter 运行在 Web 上。这篇文章将介绍我们应对这一挑战的思路以及这项技术目前的进展。文末还提供了关于互操作(interop)和嵌入问题的解答。 先快速回顾一下 Flutter 的架构。Flutter 是一个多层系统:上层更易用,可以用很少的代码表达丰富的功能;下层则提供更强的控制力,代价是需要处理更多复杂度。当某一层无法满足开发者需求时,可以下沉到更低一层。开发者可以使用 Flutter Engine 之上的所有层。 Flutter 移动端架构 Flutter Engine 以 Flutter 中最底层的库 dart:ui 暴露出来。它不了解 widget、物理效果、动画或布局(文本布局除外),它只负责把画面合成到屏幕上并转化为像素。直接基于 dart:ui 编写应用会很困难,所以才有了上面这些层。 dart:ui 之上的部分我们称为“框架(framework)”,之下的部分称为“引擎(engine)”。框架完全用 Dart 编写。引擎大部分用 C++ 编写,Android 相关部分用 Java,iOS 相关部分用 Objective-C。dart:ui 中的一些基础类和函数用 Dart 编写,主要充当 Dart 与 C++ 之间的桥梁。 Flutter 还提供插件系统。插件是用能直接访问 OEM 库以及移动生态多年积累的第三方库的语言编写的代码。为 Android 写插件要用 Java 或 Kotlin,为 iOS 写插件则用 Objective-C 或 Swift。 你好,Web Web 平台已经发展了几十年,包含大量技术和规范。人们常用几个统称来概括相关功能的集合:HTML、CSS、SVG、JavaScript、WebGL。要让 Flutter 跑在 Web 上,我们需要: 编译 Dart 代码:Flutter 用 Dart 编写,我们需要让 Dart 能在 Web 上运行。 选择 Flutter 中要在 Web 上运行的子集:把全部 Flutter 代码搬到 Web 上既不现实也没必要,其中一部分是平台特定的,比如 Android 和 iOS 相关的代码。 选择足够的 Web 功能子集:Web 平台多年来积累了不少功能重叠的技术。例如,绘制图形可以用 HTML+CSS、SVG、Canvas 或 WebGL。 Dart 自诞生起就支持编译为 JavaScript。许多重要的应用从 Dart 编译为 JavaScript 并在生产环境中运行,Flutter 的编译策略正是依赖同一套基础设施。 探索之初,我们在 UI 渲染上面临多种选择。很快我们意识到,想让 Flutter 哪些层跑在 Web 上,直接决定了该用哪些 Web 技术来实现。我们做了三个原型: 仅 widgets:这个原型实现了 Flutter 的 widget 框架,并提供一组核心布局 widget 作为自定义 widget 的基础。布局和定位依赖 Web 内置能力,比如 flexbox、grid 布局,以及通过 overflow:scroll 实现的浏览器滚动等。 Widgets + 自定义布局:这个原型包含 Flutter 的布局系统(由 RenderObject 提供),但把 render object 直接映射为 HTML 元素。 Flutter Web 引擎:这个原型保留 dart:ui 之上的所有层,并提供一个能在浏览器中运行的 dart:ui 实现。 Flutter 最有价值的特性之一就是跨平台可移植。虽然你可以(有时也应该)编写平台特定代码,但不需要因平台而异的部分可以共享,从而用一套代码库开发面向多个平台的应用。 在把几个示例应用移植到 Web 之后,我们发现原型 1 和 2 无法达到 Flutter 开发者早已习惯的可移植程度。因此我们选择了原型 3,即 Flutter Web 引擎方案,因为它能在框架层实现最高的跨平台代码复用: Flutter Web 架构(Hummingbird) 确定了要实现完整的 dart:ui API 之后,接下来要挑选一组 Web 技术作为实现基础。Flutter 逐帧渲染 UI。在每一帧内,Flutter 构建 widget、执行布局,最后把它们绘制到屏幕上。 构建 widget widget 构建机制不依赖应用运行的环境。这个过程只是在内存中实例化对象、跟踪其状态,并在状态变化时为下层(布局和绘制)计算最小的必要更新。把这部分移植到 Web 很顺利。在 Dart 团队为 dart2js 实现了 super-mixin 支持之后,编译器几乎毫无障碍地把所有 widget 和 widget 框架编译成了 JavaScript。 布局 布局系统要棘手一些,最大的挑战是文本布局。其余部分——Center、Row、Column、Stack、Scrollable、Padding、Wrap 等等——都由框架负责布局,因此无需修改即可编译到 Web。 在 Flutter 中,排版一段文本的方式是创建 Paragraph 对象并调用它的 layout() 方法。遗憾的是,Web 没有直接的文本布局 API。我们测量文本布局属性的技巧是让浏览器去排版,然后从 DOM 元素中读回相关属性。 排版一段文本时,Flutter 会测量该段落的高度、宽度、最大内在宽度、最小内在宽度,以及字母基线和表意文字基线。这些属性如下图所示。 段落布局属性 更多细节可参阅 Flutter 的 Paragraph 文档。 测量这些属性时,我们先把段落放进一个 HTML DOM 元素,再读取该元素的尺寸,这会触发浏览器排版。例如,通过调用 offsetWidth 及其对应的 offsetHeight 获取元素的宽高。测量基线时,我们把段落放进一个配置为 flex row 布局的元素中,并在段落旁边放置一个叫 "probe" 的元素。由于 probe 与文本基线对齐,对它调用 getBoundingClientRect 就能得到基线位置。我们用类似的技巧测量最小和最大内在宽度。 绘制 最后,我们需要绘制 widget。这是我们探索过程中变动最大的领域,至今仍是最活跃的研究方向之一。到帧结束时,所有 widget 都要变成屏幕上的像素。在浏览器里,这意味着要把它们归结为 HTML/CSS、Canvas、SVG 和 WebGL 的某种组合。 我们还没有研究 WebGL,主要因为它太底层,需要重新实现浏览器已经能做的事(如文本布局和 2D 图形光栅化),也因为还没弄清无障碍访问、文本选择以及与非 Flutter 组件的组合在 WebGL 下如何工作。 我们的早期原型之一曾为每个 RenderObject 生成一个 HTML 元素。结果确实不错,但会造成太大的破坏性 API 变更,我们不得不维护与 Flutter 之间庞大的代码差异,于是搁置了这个方案。 目前我们同时在探索两种方案: HTML+CSS+Canvas、CSS Paint API HTML+CSS+Canvas 这种方案下,我们把框架产生的画面分成两类:能用 HTML+CSS 表达的,和能用 Canvas 2D 表达的,然后输出组合了 HTML、CSS 和 2D canvas 的 HTML DOM。 我们更倾向于 HTML+CSS,因为它由浏览器的显示列表(display list)支撑。这意味着我们可以把画面光栅化的优化交给浏览器渲染引擎,还能应用任意变换(尤其是旋转和缩放)而不必担心像素化。我们把这种 canvas 实现称为 DomCanvas。 当画面无法用 HTML+CSS 表达时,我们退回到 canvas。Canvas 2D 几乎能完成所有 Flutter 绘图命令。对比 Flutter 的 Canvas 和 Web 的 CanvasRenderingContext2D,会发现很多相似之处。在 canvas 上绘制很高效,因为它不像 HTML DOM 或 SVG 那样需要长期维护一棵可变的节点树。 2D canvas 的一个挑战在于,浏览器把它表示为位图——一块存储 Width x Height 像素的内存缓冲区。因此缩放 canvas 会导致像素化。如果缩放导致画面尺寸变化,我们就需要调整 canvas 的大小。我们发现分配 canvas 相当昂贵,调整大小也一样。此外,把多个 canvas 合成到同一页面时,浏览器要执行光栅合成,这在我们的性能分析中也有体现。光栅合成的工作方式与显示列表不同:多个显示列表可以绘制到同一块内存缓冲区上。我们把基于 Canvas 2D 的实现称为 BitmapCanvas,目前正在研究让位图 canvas 更高效的方法。 为了表达 Flutter 的 opacity、transform、offset、clip rect 等图层,我们使用普通的 HTML 元素。例如,opacity 层变成带有 opacity CSS 属性的 元素,transform 层变成带有 transform CSS 属性的 元素,clip rect 则变成带 overflow: hidden 的 。 最终,一帧会以一棵 HTML 元素树的形式渲染到页面上,DomCanvas 和 BitmapCanvas 作为叶节点。例如: 一帧的 HTML DOM 结构示例 Flutter Engine 中等价的图层树(称为 flow layer)则是这样: Flutter Engine 图层结构示例 两者结构上非常相似。最大的区别在于,Web 端我们需要根据画面内容选择不同的实现。 HTML+CSS+Canvas 在所有现代浏览器中都能工作。不过我们已经开始着眼未来: CSS Paint API CSS Paint 是一个新的 Web API,属于 Houdini 这个更大项目的一部分。Houdini 是众多浏览器厂商的合作项目,旨在把 CSS 引擎的某些部分暴露给开发者。具体来说,CSS Paint API 允许开发者在 HTML 元素请求绘制时,用自定义图形来绘制它们。例如,你可以把元素背景的绘制交给一个自定义 CSS painter。它非常类似 canvas,但有几个重要区别: 绘制不是在主 JavaScript isolate 中完成,而是由所谓的 paint worklet 执行。它有点像 web worker,拥有自己的内存空间。paint worklet 在浏览器绘制阶段执行,即 DOM 变更提交之后。 CSS paint 由显示列表支撑,而不是位图。这让我们两全其美——既拥有类似 2D canvas 的绘制效率,又不会像素化。 目前 CSS paint 还不支持绘制文本。 截至本文撰写时,只有 Chrome 和 Opera 在生产环境中支持 CSS Paint,不过其他浏览器也正在以不同进度陆续推出各自的实现。 Flutter for Web 已经实验性地支持 CSS Paint API,效果已经很不错,性能方面尤其突出。我们的实现只是把绘制命令序列化到一个自定义 CSS 属性中,paint worklet 读取这些命令并执行。文本则用普通的

和 HTML 元素渲染。 目前的序列化机制效率不算高——是把嵌套列表树转成 JSON——但 Houdini 项目的一部分就是增加对 typed array 的支持。等它可用后,我们会用 typed array 而非 JSON 字符串来编码绘制命令。Typed array 是可转移的(transferable),意味着可以通过引用把它从主 isolate 传给 paint worklet,完全不需要复制内存。 互操作与嵌入 从 Flutter 调用 Dart 库 Flutter Web 应用可以完全访问现有所有能在 Web 上运行的 Dart 库。 从 Flutter 调用 JavaScript 库 Flutter Web 应用完全支持 Dart 的 JS 互操作包:package:js 和 dart:js。 在 Flutter Web 应用中使用 CSS 目前,为保证正确性和性能,Flutter 假定自己对网页有完全的控制。例如,我们只使用遵循特定性能准则的少量 CSS,如 https://csstriggers.com/ 所列。在页面上放任意 CSS 可能导致 Flutter 行为不可预测。 避免在 Flutter for Web 应用中使用 CSS 的另一个原因是,Flutter 在设计上需要在渲染每一帧时掌握所有布局属性,而 CSS 像一个黑盒。例如,要显示一个可滚动的 widget 列表,你必须为所有 widget 实例化并生成 HTML,应用必要的 CSS 属性(如 flex-direction row 和 overflow: scroll),然后由浏览器完成全部排版并渲染到屏幕,应用代码无法参与布局过程。 最后,本着保持 Flutter 代码跨平台可移植的精神,我们避免使用 CSS,以便同一份代码能在 Android 和 iOS 上原生运行。 把 Flutter 嵌入现有 Web 应用 我们还没有为此提供正式支持,但计划在未来探索。正在考虑的方案包括