翻译:https://medium.com/flutter-io/hummingbird-building-flutter-for-the-web-e687c2a023a8
在今天的Flutter Live上,我们宣布我们正在尝试在网上运行Flutter。在这篇文章中,我们描述了我们如何应对挑战以及技术的当前状态。在帖子的最后,您将找到有关互操作和嵌入的问题的答案。
让我们快速回顾一下Flutter的架构。Flutter是一个多层系统,这样更高的层更容易使用,并允许您用很少的代码表达很多,而较低的层为您提供更多的控制,代价是必须处理一些复杂性。当较高层不能满足开发人员的需求时,它们可以降到较低层。开发人员可以访问Flutter Engine上方的所有层。
Flutter Engine作为Flutter中最低级别的库暴露,dart:ui。它对小部件,物理,动画或布局(文本布局除外)一无所知。它所知道的是如何将图片组合到屏幕上并将它们变成像素。在dart:ui上直接编写应用程序是很困难的。这就是创建更高层的原因。
以上所有事情:ui是我们所谓的“框架”。它下面的一切都是“引擎”。该框架完全使用Dart编程语言编写。大多数引擎都是用C ++编写的,特定于Android的部分用Java编写,而iOS特定的部分用Objective-C编写。dart中的一些基本类和函数:ui是用Dart编写的,主要用作Dart和C ++之间的桥梁。
Flutter还提供插件系统。插件是用一种语言编写的代码,可以直接访问移动生态系统随着时间累积的OEM库和第三方库。要为Android创建插件,您可以编写Java或Kotlin。iOS插件是使用Objective-C或Swift编写的。
你好,网络
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技术。我们制作了三个原型:
-
只是小部件:这个原型实现了Flutter的小部件框架,并提供了一组核心布局小部件作为构建自定义小部件的基础。对于布局和定位,它依赖于Web的内置功能,例如flexbox,网格布局,浏览器滚动浏览
overflow:scroll
等。 -
小部件+自定义布局:此原型包括Flutter的布局系统(由提供
RenderObject
),但将渲染对象直接映射到HTML元素。 - Flutter Web Engine:这个原型保留了dart:ui之上的所有层,并提供了一个在浏览器中运行的dart:ui实现。
Flutter最有价值的功能之一是它可以跨平台移植。虽然您可以(有时甚至鼓励)编写自定义平台特定代码,但可以共享跨平台不需要不同的代码。这允许使用单个代码库编写面向多个平台的应用程序。
在尝试将几个示例应用程序移植到Web之后,我们意识到原型#1和#2不能提供Flutter开发人员喜欢的可移植性级别。因此,我们决定使用Flutter Web Engine设计的原型#3,因为这将允许平台之间最高的框架级代码重用:
现在我们知道我们想要实现整个dart:ui API,我们需要选择一组Web技术来构建。Flutter一次呈现一帧UI。在每个框架内,Flutter 构建小部件,执行布局,最后在屏幕上绘制它们。
构建小部件
窗口小部件构建机制不依赖于应用程序运行的环境。该过程只是实例化内存中的对象,跟踪它们的状态,以及状态更改何时计算系统的较低级别,布局和绘制所需的最小更新。将此部分移植到Web上非常简单。在Dart团队在dart2js中实现了super-mixin支持后,编译器将所有小部件和小部件框架编译为JavaScript,几乎没有任何问题。
布局
布局系统有点棘手。最大的挑战是文本布局。其他所有内容 - 中心,行,列,堆栈,可滚动,填充,换行等 - 由框架布局,因此无需修改即可编译到Web。
在Flutter中,您可以通过创建Paragraph对象并调用其layout()方法来布置一段文本。不幸的是,Web缺少直接的文本布局API。我们用来测量文本布局属性的技巧是让浏览器将其布局,然后从DOM元素中读回相关属性。
在布置一段文本时,Flutter测量段落的高度,宽度,最大内在宽度,最小内在宽度以及字母和表意基线。这些属性如下所示。
您可以在Flutter的段落文档中找到更多详细信息。
要测量这些属性,我们首先在HTML DOM元素中放置一个段落,然后我们读取元素的维度。这会导致浏览器将其布局。例如,要获取元素的宽度和高度,我们调用offsetWidth及其兄弟offsetHeight。为了测量基线,我们将段落放置在一个元素中,该元素被配置为使用flex行进行自我布局。在段落旁边,我们放置另一个名为“probe”的元素。因为探针与文本的基线对齐,所以调用getBoundingClientRect就可以得到基线。我们使用类似的技巧来测量最小和最大内在宽度。
绘画
最后但并非最不重要的是,我们需要绘制小部件。这个区域在我们的探索中经历了最多的流失,它仍然是我们研究中最活跃的领域之一。在框架结束时,我们所有的小部件都需要在屏幕上变成像素。在浏览器中,这意味着它们必须归结为HTML / CSS,Canvas,SVG和WebGL的某种组合。
我们还没有看过WebGL,主要是因为它是低级别的并且要求我们重新实现浏览器已经可以做的事情,例如文本布局和光栅化2D图形,还因为我们还没有弄清楚可访问性,文本选择,使用非Flutter组件的组合可以与WebGL一起使用。
我们的早期原型之一为每个RenderObject生成了一个HTML元素。我们确实获得了有希望的结果,但结果证明API变化太大了。我们必须用Flutter维持一个巨大的代码增量,所以我们搁置了这个想法。
我们目前正在同时探索两种方法:
- HTML + CSS +帆布
- CSS Paint API
HTML + CSS +帆布
通过这种方法,我们将框架生成的图片分类为使用HTML + CSS表达的图片,以及使用Canvas 2D表达的图片。然后,我们输出结合了HTML,CSS和2D画布的HTML DOM。
我们更喜欢HTML + CSS,因为它由浏览器的显示列表支持。这意味着我们可以优化图片的光栅化到浏览器的渲染引擎。这也意味着我们可以应用任意变换,尤其是旋转和缩放,而不必担心像素化。我们将此画布实现称为DomCanvas。
如果我们无法使用HTML + CSS表达图片,我们会回到画布。Canvas 2D允许我们绘制几乎所有的Flutter绘图命令。如果您将Flutter的Canvas与Web的CanvasRenderingContext2D进行比较,您会发现许多相似之处。在画布上绘画是有效的,因为它不会创建需要随时间维护的可变树节点,如HTML DOM或SVG。
2D画布的一个挑战是浏览器将其表示为位图,即存储宽度 x 高度像素的内存缓冲区。因此,缩放画布会导致像素化。如果缩放导致调整图片大小,我们需要调整画布大小。我们发现分配画布相当昂贵,因此调整它们的大小。最重要的是,当将多个画布合成到同一页面上时,浏览器必须执行栅格合成,这也会显示在我们的配置文件中。合成栅格与显示列表的工作方式不同。您可以将多个显示列表绘制到同一个内存缓冲区中。我们调用Canvas 2D支持的canvas实现BitmapCanvas。我们正在研究使位图画布更有效的方法。
为了表达Flutter的不透明度,变换,偏移,剪辑矩形和其他图层,我们使用纯HTML元素。例如,不透明层变为<flt-opacity>
具有opacity
CSS属性的元素,变换层变为<flt-transform>
具有transform
CSS属性的元素,剪辑rect变为<flt-clip-rect>
with overflow: hidden
。
完成所有操作后,框架将作为HTML元素树呈现在页面上,其中DomCanvas和BitmapCanvas作为叶节点。例如:
Flutter Engine中的等效Flutter层树(称为流层)如下所示:
在结构上它们非常相似。最大的区别是,在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画布般的绘画效率和无像素化。
- 目前CSS绘画不支持绘画文本。
在撰写本文时,Chrome和Opera是唯一支持CSS Paint生产的浏览器。但是,其他浏览器处于运送其实现的各个阶段。
我们在Flutter for Web中对CSS Paint API进行了实验性支持,它已经显示出良好的结果,特别是在性能方面。我们的实现只是将paint命令序列化为自定义CSS属性。paint worklet读取这些命令并执行它们。我们使用普通<p>
和<span>
HTML元素渲染文本。
我们当前的序列化机制不是特别有效 - 它是一个嵌套列表转换为JSON的树 - 但Houdini项目的一部分是添加对类型化数组的支持。当它变得可用时,我们将绘制命令编码为类型化数组而不是JSON字符串。类型化数组是可转换的,这意味着它们可以通过引用从主隔离区传递到绘制工作区。不涉及复制内存。
互操作和嵌入
从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充当黑盒子。例如,如果要显示可滚动的窗口小部件列表,则必须实例化并为所有窗口小部件生成HTML并应用必要的CSS属性(例如,flex-direction row和overflow:scroll)。然后浏览器将所有内容都放出并将其呈现为屏幕。应用程序代码不参与布局过程。
最后,本着保持Flutter代码可跨平台移植的精神,我们避免使用CSS,因此我们可以在Android和iOS上本机运行相同的代码。
将Flutter嵌入现有的Web应用程序中
我们尚未为此添加适当的支持,但我们打算在将来进行探索。我们正在考虑的几种方法是<iframe>
影子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小部件,但所有Material小部件,Material Theming,以及Shrine和Contact Profile演示应用程序都在Web上运行。