原文作者:Yegor Jbanov
译者:UC 国际研发 Jothy
今天,我们在 Flutter Live 上宣布了一个消息:我们正尝试在 Web 上运行 Flutter。 这篇文章描述了我们应对挑战的方式,以及该技术的当前状态。 在文末,我们附上了协同工作和嵌入等问题的答案。
让我们快速回顾一下 Flutter 的架构。 Flutter 是一个多层系统,这样高的层更易用,用很少的代码就能表达很多,而较低的层能提供更多的控制,代价是必须处理一些复杂性。 当较高层不能满足开发者的需求时,它们可以降到较低层。 开发者可以访问 Flutter Engine 之上的所有层。
Flutter 的 Mobile 架构
在 Flutter 中,Flutter Engine 作为最低级别的库 dart:ui 暴露。它不关心 组件,物理实现,动画或布局(文本布局除外)。它所关心的是如何将图片组合到屏幕上,渲染变成像素。在 dart:ui 上直接编写应用是很困难的。这正是我们创建更高层的原因。
dart:ui 之上的一切是我们所谓的“框架”。它下面的一切都是“引擎”。该框架完全使用 Dart 语言编写。大多数引擎都是用 C++ 编写的,特定于 Android 的部分用 Java 编写,而 iOS 特定的部分用 Objective-C 编写。 dart:ui 中的一些基本类和函数是用 Dart 编写的,主要用作 Dart 和 C++ 之间的桥梁。
Flutter 还提供插件系统。插件使用指定语言编写,可以直接访问移动生态系统日积月累的 OEM 库和第三方库。你可以使用 Java 或 Kotlin 为 Android 创建插件。 iOS 插件开发是使用 Objective-C 或 Swift。
Hello, The Web
Web 平台已经发展了数十年,包含了许多技术和规范。 有一些涵盖性术语用于描述大量相关功能:HTML,CSS,SVG,JavaScript,WebGL。 为了在 Web 上运行 Flutter,我们需要:
- 编译 Dart 代码:Flutter 是用 Dart 编写的,我们需要在 Web 上运行 Dart。
- 选择要在 Web 上运行的 Flutter 子集:在 Web 上运行所有 Flutter 代码是不切实际的。 其中一些是特定于平台的,例如 Android 和 iOS。
- 选择足够的 Web 功能子集:随着时间的推移,Web 平台会累积重复的功能。 例如,你可以使用 HTML + CSS,SVG,Canvas 和 WebGL 绘制图形。
从 Dart 诞生之初,它就一直在编译 JavaScript。 现在有许多重要的应用都从 Dart 编译为 JavaScript,并在生产环境中运行。 Flutter 的编译策略依赖于同样的基础设施。
当我们开始探索时,我们面临着 UI 渲染的几种选择。 我们很快意识到,要想支持的特定 Flutter 层,决定了我们将用什么 Web 技术。 我们构建了三个原型:
- 仅仅是 Widgets(组件):这个原型实现了 Flutter 的 widget 框架,并提供了一组核心布局 widget 作为构建自定义 widget 的基础。 对于布局和定位,它依赖于 Web 的内置功能,例如 flexbox,grid 布局,浏览器滚动(通过 overflow:scroll 实现)等。
- Widgets + 自定义布局:此原型包括 Flutter 的布局系统(由 RenderObject 提供),但将渲染对象直接映射到 HTML 元素。
- Flutter Web Engine:这个原型保留了 dart:ui 之上的所有层,并提供了一个在浏览器中运行的 dart:ui 实现。
One of the most valuable features of Flutter is that it is portable across platforms. While you can (and sometimes are encouraged to) write custom platform-specific code, the code that does not need to be different across platforms can be shared. This allows writing applications targeting multiple platforms with a single codebase.
Flutter 最有价值的功能之一是它可以跨平台移植。 你完全可以(有时甚至被鼓励)编写自定义的特定平台代码,代码无需跨平台定制即可共享。 意味着使用单个代码库就可以编写面向多个平台的应用。
在尝试将几个示例应用移植到 Web 之后,我们意识到原型 #1 和 #2 不能提供 Flutter 开发者喜欢的可移植性级别。 因此,我们决定使用 Flutter Web Engine 设计的原型 #3,因为它有着平台之间最高的框架级代码重用:
Flutter的Web架构 (Hummingbird)
既然我们知道我们想要实现整个 dart:ui API,我们需要选择一组 Web 技术来构建。 Flutter 一次渲染一帧 UI。 在每个帧内,Flutter 会构建 widgets,执行布局,最后在屏幕上绘制它们。
构建 Widgets
Widget 构建机制不依赖于应用运行的环境。该过程只是实例化内存中的对象,跟踪其状态、以及状态变更何时计算系统低级别的最小更新,布局和绘制等。 将此部分移植到 Web 上非常简单。 在 Dart 团队用 dart2js 中实现了 super-mixin 支持之后,编译器将所有 widget 和 widget frame 都编译成了 JavaScript,几乎没有 issue 产生。
布局
布局系统有点棘手。 最大的挑战是文本布局。 除了 Center,Row, Column,Stack,Scrollable,Padding,Wrap 等之外的所有内容都由框架布局,因此无需修改即可编译到 Web。
在 Flutter 中,你可以创建 Paragraph 对象并调用其 layout() 方法来实现文本布局。 不幸的是,Web 缺少直接的文本布局 API。 我们用来测量文本布局属性的技巧是:先让浏览器布局,然后从 DOM 元素中读回相关属性。
布局文本段落时,Flutter 会测量段落的高度,宽度,最大内在宽度,最小内在宽度以及字母和表意基线。 这些属性如下所示。
Paragraph layout attributes
你可以在 Flutter 的 Paragraph 文档中找到更多详细信息。
要测量这些属性,我们首先在 HTML DOM 元素中放置一个段落,然后读取元素的维度。 这会引起浏览器布局。 例如,要获取元素的宽度和高度,我们调用 offsetWidth 及 offsetHeight。 为了测量基线,我们将段落放置在一个元素中,该元素配置为使用 flex 行进行布局。 在段落旁边,我们放置另一个名为 probe 的元素。 因为 probe 与文本的基线对齐,所以调用 getBoundingClientRect 就可以得到基线。 我们使用类似的技巧来测量最小和最大固有宽度。
- Painting(绘制) *
不得不提的是,我们得绘制 widgets。 对这个区域的探索最是麻烦,它仍然是我们的研究中最活跃的领域。 在框架最后,我们所有的 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 画布的 HTML DOM。
我们更喜欢 HTML + CSS,因为它受浏览器的显示列表支持。这意味着我们可以把图片的光栅化优化留给浏览器的渲染引擎去做。并且,我们还可以应用任意变换,尤其是旋转和缩放,而不必担心像素化。我们将此画布实现称为 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 支持的 canvas 实现 BitmapCanvas。我们正在发掘使位图 canvas 更高效的方法。
为了表达 Flutter 的不透明度,变换,偏移,剪辑矩形和其他图层,我们使用纯 HTML 元素。例如,不透明度层变为 元素,其上具有 opacity CSS 属性,变换图层变为带有 transform CSS 属性的 元素,剪辑 rect 变为使用 overflow: hidden 的 。
完成所有操作后,框架将作为 HTML 元素树呈现在页面上,其中 DomCanvas 和 BitmapCanvas 作为叶节点。举个例子:
Sample HTML DOM structure of a frame
Flutter Engine 中的等效 Flutter layer tree(称为 flow layer)如下所示:
Sample Flutter Engine layer structure
它们的结构非常相似。 最大的区别是,在 Web 上,我们必须根据内容选择不同的图片实现。
HTML + CSS + Canvas 适用于所有现代浏览器。 但是,我们已经在展望未来:
CSS Paint API
CSS Paint 是一个新的 Web API,是 Houdini 的更大组成部分。 Houdini 是多个浏览器厂商合作的项目,旨在向开发者展示 CSS 引擎的某些部分。 特别的是,CSS Paint API 允许开发者在这些元素请求绘制时将自定义图形绘制成 HTML 元素。 例如,你可以将元素背景的绘制分配给自定义 CSS 绘制器。 它与 canvas 非常像,但有以下重要区别:
这个绘画不是由核心 JavaScript 独立完成的,而是由一个叫做 paint worklet 的东西完成的。 它有点像 web worker,因为它有自己的内存空间。 它会在 DOM 更改提交之后,在浏览器的绘制阶段执行绘制工作。
CSS paint 由显示列表支持,而不是位图。 这真是两全其美 - 2D canvas 般的绘画效率和无像素化。
目前 CSS paint 不支持绘制文本。
在撰写本文时,Chrome 和 Opera 是唯一在正式版本中支持 CSS Paint 的浏览器。 而其他浏览器正处于发布各自实现的不同阶段。
我们在 Flutter for Web 中对 CSS Paint API 进行了实验性支持,它已经展现出良好的结果,特别是在性能方面。 我们的实现只是将 paint 命令序列化为自定义 CSS 属性。 paint worklet 读取这些命令并执行它们。 我们使用像
和 这样普通的 HTML 元素来渲染文本。
我们当前的序列化机制不是特别有效 - 它是一个嵌套列表转换成的 JSON 树 - 但 Houdini 项目的一部分是添加对类型化数组的支持。 当它可用时,我们会将绘制命令编码为类型化数组而不是 JSON 字符串。 类型化数组是可转移的,这意味着它们可以通过引用从主 JavaScript 传递到 paint worklet,而不复制内存。
协同和嵌入
从 Flutter 调用 Dart 库
Flutter Web 应用可以访问当前在 Web 上运行的所有 Dart 库。
从 Flutter 调用 JavaScript 库
Flutter Web 应用完全支持 Dart 的 JS-interop 软件包:package:js和 dart:js。
在 Flutter Web 应用中使用 CSS
目前,Flutter 假定完全控制网页的正确性和性能。 例如,我们只使用遵循某些性能指南的一小部分 CSS,例如 https://csstriggers.com/。 在页面上随意使用 CSS 可能会导致 Flutter 出现不可预期的后果。
在 Flutter for Web 应用中避免使用 CSS 的另一个原因是,在设计时,Flutter 需要在渲染框架时知道所有布局属性。 CSS 充当黑盒。 例如,如果要显示可滚动的窗口 widget 列表,则必须实例化并为所有 widgets 生成 HTML 并应用必要的 CSS 属性(例如,flex-direction row 和 overflow:scroll)。 然后浏览器将所有内容都布局并将其渲染到屏幕上。 应用代码不参与布局过程。
最后,本着保持 Flutter 代码可跨平台移植的精神,我们尽量避免使用 CSS,因此我们可以在 Android 和 iOS 上本机运行相同的代码。
将 Flutter 嵌入现有的 Web 应用中
我们还没有为此添加适当的支持,但我们打算在将来探索。 我们正在考虑的方法是 和 shadow DOM。
在 Flutter 中嵌入非 Flutter 组件
我们还未支持在 Flutter Web 应用中嵌入非 Flutter 组件 - 自定义元素、React 组件、Angular 组件,但我们打算在将来探索。 有可能是使用平台视图将外部内容放入 Flutter Web 应用中。需要考虑的是外部内容可能对应用的性能和正确性产生影响。 因为非 Flutter 组件可能包含任意 CSS,如上所述,它可能会有问题。 需要更多的研究。
可移植性
我们的目标是尽可能多地将框架移植到 Web 上。 但是,这并不意味着任何 Flutter 应用将在 Web 上运行而不更改代码。 Flutter Web 应用仍然是一个 Web 应用; 它在浏览器中被沙箱化,只能执行 Web 浏览器允许的操作。 例如,如果你的 Flutter 应用使用 Web 未实现的本机插件(例如 ARCore),你将无法在 Web 上运行该应用。 同样,也无权限直接访问文件系统或低级网络。
当前状态
我们构建了足够的 Web 引擎来渲染大部分 Flutter Gallery。 我们还未移植 Cupertino widgets,但所有 Material widgets,Material Theming,以及 Shrine 和 Contact Profile 演示应用均已运行在 Web 上。
Flutter running in desktop Chrome 演讲视频
https://www.youtube.com/watch?v=5IrPi2Eo-xM
源代码在哪里?
我们计划很快开源这个项目,并很高兴与开源社区分享。 该项目最初是作为 Google 内部源代码树的一项探索而开始的。 待代码稳定后,我们打算将开发转移到 GitHub,我们有机会将其从内部基础架构中剥离出来。 与此同时,如果您在 github.com/flutter 组织下看到与 Web 相关的 pull 请求,请不要感到惊讶!
结论
希望这篇文章能让你了解我们正在解决的问题,以帮助 Flutter 在 Web 上更好运行。 欢迎表达您的观点和意见。
请继续关注 Google I/O 2019!
英文原文:
https://medium.com/flutter-io/hummingbird-building-flutter-for-the-web-e687c2a023a8