[译] Hummingbird: Web端运行Flutter

2,686 阅读14分钟

笔者注: 随着 Flutter 1.0 稳定版本的发布,跨平台开发的趋势收到越来越多人的关注,然而 Flutter 的野心不止在跨移动平台 Android 和 iOS,在2019的当下,Flutter将逐步开展跨 Web 端的工作并取得了初步结论和经验,本文原文为 Flutter 团队发布的 Web 端运行 Flutter 试验性成果报告,内容丰富且极具启发性,因此特意将其翻译为中文供大家学习。   原文:Hummingbird: Building Flutter for the Web

在今天的 Flutter Live 上,我们宣布正尝试在 Web 端运行 Flutter 。 在这篇文章中,我们描述了我们如何应对挑战以及的当前的技术状况。 在帖子的最后,你将找到有关交互操作和嵌入问题的答案。

让我们快速回顾一下 Flutter 的架构。 Flutter 是一个多层系统,这样更高的层更容易使用,并允许你用很少的代码来实现更多的功能,而较低的层提供更多的控制,代价是必须处理一些复杂性。 当较高层不能满足开发人员的需求时,它们可以降到较低层。 开发人员可以访问 Flutter Engine 上方的所有层。

Flutter for Mobile Architecture

Flutter Engine 作为 Flutter 中最低级别暴露的库,dart:ui。它对 widgets、物理 (physics)、动画或布局(文本布局除外)一无所知。它所知道的是如何将 pictures 组合到屏幕上并将它们变成像素。在 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。为了在 Web 上运行 Flutter,我们需要:

  • 编译 Dart 代码:Flutter 是用 Dart 编写的,我们需要在 Web 上运行 Dart。
  • 选择要在 Web 上运行的 Flutter 子集 (subset):在 Web 上运行所有 Flutter 代码是不实际或有用的。其中一些是特定于平台的,例如 Android 和 iOS。
  • 选择足够的 Web 功能子集 (subset):随着时间的推移,Web 平台会累积功能重叠的功能。例如,你可以使用 HTML + CSS,SVG,Canvas 和 WebGL 绘制图形。

只要语言存在,Dart 就一直在编译 JavaScript。许多重要的应用程序从 Dart 编译为 JavaScript,并在今天的产品中运行。 Flutter 的编译策略依赖于同样的基础。

当我们开始探索时,我们面临着 UI 渲染的几种选择。我们很快意识到我们想要支持的特定 Flutter 层决定了我们将用于实现的 Web 技术。我们制作了三个原型

  1. 只有 widgets:这个原型实现了 Flutter 的 widget 框架,并提供了一组核心布局 widgets 作为构建自定义 widget 的基础。对于布局和定位,它依赖于 Web 的内置功能,例如 flexbox,grid layout,浏览器滚动通过 overflow:scroll 等。

  2. Widgets + 自定义布局:此原型包括 Flutter 的布局系统(由 RenderObject 提供),但将渲染对象直接映射到 HTML 元素。

  3. Flutter Web 引擎:这个原型保留了 dart:ui 之上的所有层,并提供了一个在浏览器中运行的 dart:ui 实现。

Flutter 最有价值的功能之一是它可以跨平台移植。跨平台可以共享相同的代码,虽然你可以(有时甚至鼓励)编写自定义平台特定代码。这允许使用单个代码库编写面向多个平台的应用程序。

在尝试将几个示例应用程序移植到 Web 之后,我们意识到原型#1和#2不能提供 Flutter 开发人员喜欢的可移植性级别。因此,我们决定使用 Flutter Web Engine 设计的原型#3,因为这将允许平台之间最高框架级的代码重用:

Flutter for the Web Architecture (Hummingbird)

现在我们知道我们想要实现整个 dart:ui API,我们需要选择一组Web技术来构建。 Flutter 一次呈现一帧 UI。 在每个框架内,Flutter 构建 widgets,执行布局,最后在屏幕上绘制它们。

构建 widgets

Widgets 构建机制不依赖于应用程序运行的环境。该过程只是实例化内存中的对象,跟踪它们的状态,以及状态更改何时,计算系统的较低层级、布局、绘制所需的最小更新。 将此部分移植到 Web 上非常简单。 在Dart 团队在 dart2js 中实现了 super-mixin 支持后,编译器将所有 widgets 和 widgets 框架编译为JavaScript,几乎没有任何问题。

布局 (Layout)

布局系统有点棘手。 最大的挑战是文本布局。 其他所有内容 - 中心,行,列,堆栈,可滚动,填充,换行等 - 由框架布局,因此无需修改即可编译到Web。

在 Flutter 中,你可以通过创建 Paragraph 对象并调用其 layout() 方法来布置一段文本。 不幸的是,Web 缺少直接的文本布局 API。 我们用来测量文本布局属性的技巧是让浏览器将其布局,然后从 DOM 元素中读回相关属性。

在布置一段文本时,Flutter测量段落的高度、宽度、最大内在宽度、最小内在宽度以及字母和表意基线。 这些属性如下所示。

Paragraph layout attributes

你可以在 Paragraph documentation 中找到更多详细信息。

要测量这些属性,我们首先在 HTML DOM 元素中放置一个段落,然后我们读取元素的维度。 这会导致浏览器将其布局。 例如,要获取元素的宽度和高度,我们调用 offsetWidthoffsetHeight。 为了测量基线,我们将段落放置在一个元素中,该元素被配置为使用 flex 行进行自我布局。 在段落旁边,我们放置另一个名为 “probe” 的元素。 因为 probe 与文本的基线对齐,所以调用 getBoundingClientRect 就可以得到基线。 我们使用类似的技巧来测量最小和最大内在宽度。

绘制 (Painting)

最后,我们需要绘制 widgets。这部分的探索中我们花费了最大的功夫,它仍然是我们研究中最活跃的部分之一。在框架结束时,我们所有的 widgets 都需要在屏幕上变成像素。在浏览器中,这意味着它们必须归结为 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 canvases 的 HTML DOM。

我们更喜欢 HTML + CSS,因为它有浏览器 display list 的支持。这意味着我们可以优化图片的光栅化到浏览器的渲染引擎。这也意味着我们可以应用任意变换,尤其是旋转和缩放,而不必担心像素化。我们将此 Canvas 实现称为 DomCanvas

如果我们无法使用 HTML + CSS 表达图片,我们会回到 canvas。 Canvas 2D 允许我们绘制几乎所有的 Flutter 绘图命令。如果你将 Flutter 的 Canvas 与 Web 的 CanvasRenderingContext2D 进行比较,你会发现许多相似之处。在 canvas 上绘制是有效的,因为它不会创建需要随时间维护的可变树节点,如 HTML DOM 或 SVG。

2D canvas 的一个挑战是浏览器将其表示为位图,即存储宽度 x 高度像素的内存缓冲区。因此,缩放 canvas 会导致像素化。如果缩放导致调整图片大小,我们需要调整 canvas 大小。我们发现分配 canvases 代价很高,因此调整它们的大小。最重要的是,当将多个 canvases 合成到同一页面上时,浏览器必须执行栅格合成,这也会显示在我们的配置文件中。合成栅格与显示列表的工作方式不同。你可以将多个显示列表绘制到同一个内存缓冲区中。我们调用 Canvas 2D 支持的 canvas 实现 BitmapCanvas。我们正在研究使位图画布更有效的方法。

为了表达 Flutter 的不透明度、变换、偏移、剪辑矩形和其他图层,我们使用纯 HTML 元素。例如,不透明层变为 <flt-opacity> 元素,其上具有 opacity CSS 属性,变换图层变为带有变换 CSS 属性的 <flt-transform> 元素,剪辑 rect 变为带 overflow: hidden<flt-clip-rect >

完成所有操作后,框架将作为 HTML 元素树呈现在页面上,其中 DomCanvas 和 BitmapCanvas 作为叶节点。例如:

Sample HTML DOM structure of a frame

Flutter Engine 中的等效Flutter 层树(称为 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 元素。例如,你可以将元素 background 的绘制分配给自定义 CSS 绘制工具。它与 canvas 非常相似,但有以下重要区别:

  • 这个绘制不是由主要的 JavaScript 独立完成的,而是由一个叫做 paint worklet 的东西完成的。它有点像 Web 工作者,因为它有自己的内存空间。在提交 DOM 更改之后,在浏览器的绘制阶段执行绘制工作。

  • CSS 绘制由显示列表支持,而不是位图。这为我们提供了两全其美 - 2D canvas 般的绘制效率和无像素化。

  • 目前 CSS 绘制不支持绘制文本。

在撰写本文时,Chrome 和 Opera 是唯一支持 CSS Paint 生产的浏览器。但是,其他浏览器处于实现过程的各个阶段

我们在 Flutter for Web 中对 CSS Paint API 进行了实验性支持,它已经显示出良好的结果,特别是在性能方面。我们的实现只是将 paint 命令序列化为自定义 CSS 属性。paint worklet读取这些命令并执行它们。我们使用普通的 <p><span> HTML元素渲染文本。

我们当前的序列化机制不是特别有效——它是一个嵌套列表转换为 JSON 的树——但 Houdini 项目的一部分是添加对类型化数组 (typed arrays) 的支持。当它变得可用时,我们将绘制命令编码为类型化数组而不是 JSON 字符串。类型化数组是可转换的 (Transferable) ,这意味着它们可以通过引用从主隔离区传递到绘制工作区。不涉及复制内存。

交互操作和嵌入

从 Flutter 调用 Dart 库

Flutter Web 应用程序可以完全访问当今在 Web 上运行的所有现有 Dart 库。

从 Flutter 调用 JavaScript 库

Flutter Web 应用程序完全支持 Dart 的 JS-interop 包:package:jsdart:js

在 Flutter Web 应用程序中使用 CSS

目前,Flutter 假设完全控制网页的正确性和性能。例如,我们只使用遵循某些性能指南的一小部分 CSS,如https://csstriggers.com/。在页面上放置任意 CSS 可能会导致 Flutter 表现不可预测。

在 Flutter for Web 应用程序中避免使用 CSS 的另一个原因是,在设计时,Flutter 需要在呈现框架时知道所有布局属性。 CSS 像是个黑盒子。例如,如果要显示可滚动的 widgets 列表,则必须实例化并为所有 widgets 生成 HTML 并应用必要的 CSS 属性(例如,flex-direction row 和 overflow:scroll)。然后浏览器将所有内容都放出并将其呈现为屏幕。应用程序代码不参与布局过程。

最后,本着保持Flutter代码可跨平台移植的精神,我们避免使用 CSS,因此我们可以在 Android 和 iOS上本机运行相同的代码。

将 Flutter 嵌入现有的 Web 应用程序中

我们尚未为此添加适当的支持,但我们打算在将来进行探索。 我们正在考虑的几种方法是 <iframe> 和 shadow DOM。

在 Flutter 中嵌入非 Flutter 组件

我们尚未添加对在 Flutter Web 应用程序中嵌入非 Flutter 组件(自定义元素、React 组件、Angular 组件)的支持,但我们打算在将来探讨这一点。 一种可能的途径是使用平台视图将外来内容放入 Flutter Web 应用程序中。 需要考虑的一个重要方面是外来内容可能对应用程序的性能和正确性产生何种影响。 因为非 Flutter 组件可能包含任意 CSS,如上所述,它可能会有问题。 需要更多的研究。

可移植性

我们的目标是尽可能多地将框架移植到 Web 上。 但是,这并不意味着任何 Flutter 应用程序将在 Web 上运行而不会更改代码。 Flutter 网络应用程序仍然是一个 Web 应用程序; 它在浏览器中被沙盒化,只能执行 Web 浏览器允许的操作。 例如,如果你的 Flutter 应用程序使用没有 Web 实现的本机插件(例如 ARCore ),你将无法在Web上运行该应用程序。 同样,没有直接访问文件系统或低级网络的权限。

当前状态

我们构建了足够的 Web 引擎来渲染大部分的 Flutter Gallery。 我们还没有移植 Cupertino widget,但所有Material widget、Material Theming,以及 Shrine 和 Contact Profile 的 demo 都在 Web 上运行。

源代码在哪里?

我们计划很快开源这个项目,并且很高兴与开源社区分享。 该项目最初是作为 Google 内部源代码树的一项探索而开始的。 我们的代码稳定后,我们打算将开发转移到 GitHub,我们有机会将其从内部基础架构中移出来。与此同时,如果你在 github.com/flutter 组织下看到与 Web 相关的拉取请求,请不要感到惊讶!

结论

我希望这篇文章能让你了解我们正在解决的问题,以使 Flutter 在 Web 上很好的运行。 我们欢迎你的想法和创意。

请继续关注Google I / O 2019!

如果喜欢的话可以点个关注或收藏吧!本文为个人原创翻译,转载请注明出处。