Flutter入门系列(七)---Hummingbird: Web 里的 Flutter

作者 / Yegor Jbanov, Flutter 开发工程师, Google

 

相信关注 Flutter Live 的朋友们已经知道了我们正在将 Flutter 带进 Web。在本文中我们会和您分享一下我们的工作历程,以及当前的项目进展。另外,您还会看到一些使用以及实现的细节以及在 Web 页面中和其他代码 / 元件进行互操作的说明,请耐心阅读到最后。

 

让我们来快速回顾一下 Flutter 的架构。Flutter 是一个多层系统,您可以在更高的层级上用很少的代码就开发出很丰富的内容,而如果您需要控制更多的系统行为,您也可以深入到较低的层去进行控制,但相应的,复杂度也会更高一些。当较高层的代码无法满足开发者的需求时,开发者们随时可以深入到较低的层中去完成自己的开发目标,如下图,开发者可以访问 Flutter Engine 上方的所有层。

△ Flutter 架构

正如大家所看到的,Flutter Engine 是 Flutter 中最低级别的库,dart:ui。它不涉及任何 widget、物理效果、动画或布局 (文本布局除外),而只负责将一个个 picture 类放到屏幕上,并将它们绘制成像素。在 dart:ui 上直接编写应用是很困难的,这就是我们需要在此之上创建更多层代码的理由。

 

  • Flutter 官方文档: Picture 类

    https://docs.flutter.io/flutter/dart-ui/Picture-class.html

 

我们将位于 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 编写。

 

 

Hello, Web

 

Web 平台发展至今已有数十年了,包括了为数众多的技术和规范。我们可以使用一些总括性术语来描述大量相关功能,如 HTML、CSS、SVG、JavaScript、WebGL 等。为了在 Web 上运行 Flutter,我们需要:

  • 编译 Dart 代码: Dart 是 Flutter 的开发语言,我们需要在 Web 上运行 Dart 代码。

  • 确定要在 Web 上运行的 Flutter 子集: 在 Web 上运行所有 Flutter 代码并不实际,也谈不上有用,毕竟其中一些代码是专属某个平台的,例如 Android 和 iOS。

  • 选择支持的 Web 功能子集: 随着时间的推移,Web 平台会累积一些彼此重叠的功能。例如,如果想绘制图形,您用 HTML + CSS、SVG、Canvas 和 WebGL 都可以做到。
     

自 Dart 诞生以来,它一直支持编译为 JavaScript今天很多重要的应用都是从 Dart 编译为 JavaScript,并且在生产环境中运行良好。因此,Flutter 的编译策略同样依存于这套机制。

 

在启动这个项目时,我们面临着 UI 渲染方式的几种选项。但我们很快意识到,我们想要支持的 Flutter 层级和代码同时也决定了我们会支持哪些 Web 技术。为此我们制作了三个原型: 

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

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

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

 

Flutter 最有价值的功能之一是它可以跨平台移植。虽然您可以编写专属某个平台的代码 (有时我们甚至会鼓励您这样做),但也可以共享那些在不同平台上都能保持一致的代码。这样一来,您只需用一个代码库即可编写面向多个平台的应用。

 

在三个原型开发完成后,我们尝试将几个样本应用移植到 Web,我们发现 1 号和 2 号原型无法为 Flutter 开发者提供足够的可移植性。因此,我们决定使用基于 Flutter Web Engine 的 3 号原型。这样一来,平台之间的框架级代码就可以重复使用了,如下图: 

△ Web 版 Flutter (即 Hummingbird) 架构

现在我们明确了我们需要选择需要的 Web 技术,并在其上实现我们需要的整套 dart:ui API。

 

接下来是 widget。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 会测量 Paragraph 的高度 (height)、宽度 (width)、最大内宽 (maximum intrinsic width)、最小内宽 (minimum intrinsic width)、字母基线 (alphabetic baseline) 以及降部基线 (ideographic baseline)。这些属性如下图所示: 

△ 字母基线是指字母书写时的对齐线,而上图中 Howdy 的 “y” 向下超出了字母基线,超出的部分称作 “降部”。

  • Flutter 官方文档: Paragraph 类

    https://docs.flutter.io/flutter/dart-ui/Paragraph-class.html

 

想要测量这些属性,我们就要首先在 HTML DOM 中放置一个文本段落,然后读取相关的数据。例如,为了获取元素的宽度和高度,我们会调用 offsetWidth 和 offsetHeight。为了测量基线,我们将段落放置在一个使用 flex row 进行布局的元素中,在段落旁边,我们会放置另一个名为 “probe (探测器)” 的元素。因为 probe 与文本的基线对齐,所以调用 getBoundingClientRect 就可以得到基线数据。我们也使用了类似的技巧来测量最小和最大内宽。

 

 

绘制

 

最后,我们来谈一谈 widget 的绘制。我们在这方面进行了最多的探索,它现在仍然是我们研究中最活跃的课题之一。毕竟这整个框架说到底,是要让我们的每一个 widget 都在屏幕上变成像素。在浏览器中,这意味着它们必须映射为 HTML / CSS、Canvas、SVG 和 WebGL 这些元素的某种组合。

 

我们暂时先不考虑 WebGL,主要是因为它的层级较低,并且要求我们重新实现浏览器已经可以做的事情,例如文本布局和栅格化 2D 图形。此外,我们还没有弄清楚可访问性、文本选择操作,以及那些非 Flutter 组件应该如何与 WebGL 协同工作。

 

我们的早期原型之一为每个 RenderObject 生成了一个 HTML 元素。我们获得的结果看起来确实不错,但后来却证明,API 的变化太大了 (即开发者最担心的 API breaking change),这让我们必须和 Flutter 同步维持巨大的代码增量更新,所以我们放弃了这个想法。

 

目前我们正在同时探索两种实现方法: 

  • HTML + CSS + Canvas

  • CSS Paint API
     

HTML + CSS + Canvas

通过这种方法,我们将框架生成的图片分类为使用 HTML + CSS 表达的图片,以及使用 Canvas 2D 表达的图片。然后输出为结合了 HTML、CSS 和 2D canvas 的 HTML DOM。

 

我们会优先使用 HTML + CSS,因为它由浏览器的显示列表 (display list*) 支持。这意味着我们可以让浏览器的渲染引擎来对图片的栅格化进行优化。这也意味着我们可以应用任意图形变换,尤其是旋转和缩放,而不必担心像素化的处理。我们将此画布实现称为 DomCanvas。

 

* Display list,指用于显示图形的绘图指令集。

 

如果我们遇到无法使用 HTML + CSS 渲染的图形,我们会退回到 Canvas 来进行渲染处理。Canvas 2D 支持几乎所有的 Flutter 绘图指令。如果将 Flutter 的 Canvas 与 Web 的CanvasRenderingContext2D 进行比较,您会发现许多相似之处。在 Canvas 上渲染图形效率很高,因为它不会创建需要维护的可变树节点,如 HTML DOM 或 SVG。

 

  • CanvasRenderingContext2D: 

    https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D

 

2D canvas 的一个挑战是,浏览器将其作为位图处理,即存储宽度 x 高度像素的内存缓冲区。因此,缩放 canvas 会导致像素问题 (即出现 “马赛克效果”)。如果缩放操作导致需要调整一张图片的大小,我们就需要调整 canvas 的大小。我们发现实例化 / 分配 canvas 时占用资源过多,调整它们的尺寸时也存在类似的问题。此外,当将多个 canvas 合成到同一页面上时,浏览器必须执行栅格合成处理,这也在我们的资源优化过程中造成了显著的问题。栅格合成处理与显示列表的工作方式并不同——比如您可以将多个显示列表在同一个内存缓冲区中统一处理。我们把 Canvas 2D 支持的 canvas 称作 BitmapCanvas,目前我们正在研究让 BitmapCanvas 更高效运作的方法。

 

我们使用 HTML 原生功能来实现 Flutter 的透明度、形变、位移、遮罩以及其他图层内容。例如,带透明度的图层变为了具有 opacity CSS 属性的 <flt-opacity> 元素,形变则使用了带 transform CSS 属性的 <flt-transform> 元素,clip rect 使用带 overflow: hidden 的 <flt-clip-rect>。

 

  • Flutter Layer 类: 

    https://docs.flutter.io/flutter/rendering/Layer-class.html

 

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

作为对比,Flutter Engine 中的等效 Flutter 层级树 (flow layer) 如下所示: 

在结构上它们非常相似。最大的区别是,在 Web 环境下我们必须根据内容选择不同的图形实现方法。

 

HTML + CSS + Canvas 适用于所有现代浏览器。不过,我们并没有停留于此: 

 

CSS Paint API

CSS Paint 是一个新的 Web API,它属于 Houdini 的一部分。Houdini 由多家浏览器供应商共同提供支持,旨在向开发者开放 CSS 引擎的特定内容。特别是,CSS Paint API 允许开发者在这些元素请求绘制时将自定义图形绘制成 HTML 元素。例如,您可以将元素 background 属性的绘制工作分配给自定义的 CSS 绘制器。

 

  • 了解 Houdini: 

    https://developers.google.com/web/updates/2016/05/houdini

 

它与 canvas 非常相似,但存在以下重要区别: 

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

  • CSS paint 由浏览器 display list 支持,而不是位图 (栅格图)。这就让我们实现了两全其美的效果——2D canvas 般的绘制效率,并且没有像素化问题。

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

 

在撰写本文时,Chrome 和 Opera 是唯二支持 CSS Paint 操作的浏览器。不过,其他浏览器也正在实现这些功能,只是进度不一。

 

  • 了解主流浏览器支持 Houdini 的进度: 

    https://ishoudinireadyyet.com/

 

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

 

我们当前的序列化机制不是特别高效,它只是一个转换为 JSON 的嵌套树状结构,但是 Houdini 项目的目标就包括添加对类型化数组的支持。一旦这个支持得以实现,我们会将绘制命令编码为类型化数组,而不是 JSON 字符串。类型化数组支持 Transferable 接口,这意味着它们可以通过引用直接从主 isolate 传递到 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 列表,则必须实例化并为所有 widget 生成 HTML ,并赋上必要的 CSS 属性 (例如,flex-direction row 和 overflow:scroll)。然后浏览器将所有内容渲染到屏幕上。应用自身的代码不参与布局过程。

 

最后,为了保持 Flutter 代码的跨平台可移植性,我们避免使用 CSS,这样我们就能够在 Android 和 iOS 设备本地运行相同的代码。

 

将 Flutter 嵌入现有的 Web 应用中

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

 

在 Flutter 中嵌入非 Flutter 组件

在 Flutter Web 应用中嵌入非 Flutter 组件,这种操作我们还没有进行支持——包括自定义元素、React 组件、Angular 组件的支持——但我们打算在将来考虑这种操作。一种可能的途径是,使用 PlatformView 将外来内容放入 Flutter Web 应用中。需要考虑的一个重要方面是,外来内容可能会对应用的性能和正确性产生哪些影响。

 

  • 了解 addPlatformView 方法:

    https://master-docs-flutter-io.firebaseapp.com/flutter/dart-ui/SceneBuilder/addPlatformView.html

 

因为非 Flutter 组件可能包含 CSS,如上所述,它同样可能会导致问题。这也需要我们进行进一步的研究。

 

 

可移植性

 

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值