在今天的Flutter Live上,我们宣布正尝试在Web上运行Flutter。在这篇文章中,描述了我们如何应对尝试过程中遇到的挑战以及当前该技术的状态。在本文的最后,您将找到有关互操作和嵌入的问题及答案;
让我们快速回顾一下Flutter的架构。 Flutter是一个多层系统,更高的层级更方便开发者使用及操作,并允许开发者用更少的代码完成更多的功能,而较低的层级虽然为开发者提供更多的控制能力,然而这种控制也是有代价的:开发者必须处理一些更复杂的事项,当较高层不能满足开发人员的需求时,他们可以深入到较低层。开发人员可以访问Flutter Engine上方的所有层代码;
Flutter 移动端架构
Flutter Engine作为Flutter暴露出来的最底层库,比如dart:ui。它对widgets,物理模拟(如重力等),动画或布局(文本布局除外)一无所知。它所知道的是如何将图片组合到屏幕上并将它们变成像素呈现。在dart:ui上直接编写应用程序是很困难的,这就是我们为开发者创建更高层的原因;
dart:ui之上的层级就是我们所谓的“框架”层。它下面的层级我们称之为“引擎”。该框架完全使用Dart编程语言编写。大多数引擎都是用C ++编写的,特定于Android的部分用 Java 编写,而iOS特定的部分用Objective-C编写。 dart:ui中的一些基本类和函数是用Dart编写的,主要用作Dart和C ++之间的桥梁。
Flutter还提供系统插件。插件是用一种语言编写的代码,它可以直接访问移动生态系统的OEM库和第三方库。要为Android创建插件,您可以使用Java或Kotlin编写代码。 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语言诞生起,Dart语言就可以被编译成Javascript语言。许多重要的应用程序从Dart编译为Javascript,并已经被投放到生产环境中运行。 Flutter的编译策略依赖于同样的基础设施。
当我们开始探索时,我们面临着UI渲染的几种选择。我们很快意识到我们想要支持的特定Flutter层决定了我们将采用何种Web技术实现。我们制作了三个原型:
-
只是Widgets:这个原型实现了Flutter的Widget框架,并提供了一组核心布局Widget作为构建自定义Widget的基础。对于布局和定位,它依赖于Web的内置功能,例如flexbox,网格布局,浏览器滚动等。
-
Widgets+自定义布局:此原型包括Flutter的布局系统(由RenderObject提供),但将渲染对象直接映射到HTML元素。
-
Flutter Web Engine:这个原型保留了dart:ui之上的所有层,并提供了一个在浏览器中运行的dart:ui实现;
Flutter最有价值的功能之一是它可以跨平台移植。您可以(我们鼓励您)编写自定义平台特定代码,该代码可以共享到各跨平台而不需要有任何差别。这允许使用单个代码库编写面向多个平台的应用程序;
在尝试将几个示例应用程序移植到Web之后,我们意识到原型#1和#2不能提供Flutter开发人员喜欢(可接受)的可移植性级别。因此,我们决定使用原型#3:Flutter Web Engine,因为这将允许最高的框架级代码在不同平台之间重用:
Flutter for the Web Architecture (Hummingbird)
既然知道了我们想要实现整个dart:ui API,我们需要选择一组Web技术来构建。 Flutter一次呈现一帧UI。在每一帧内,Flutter构建Widget,执行布局,最后在屏幕上绘制它们。
创建Widgets
Widget的构建机制不依赖于应用程序运行的环境。该过程只需要实例化内存中的对象并跟踪它们的状态,当状态改变时,计算布局和绘画所需的最小更新。将此部分移植到Web上非常简单,当Dart团队在dart2js中实现了super-mixin支持之后,编译器将所有widget和widget框架编译为Javascript时几乎没有任何问题;
布局
布局系统有点棘手。最大的挑战是文本布局。其他所有Widget - Center,Row, Column, Stack, Scrollable, Padding, Wrap等 - 这些widget都由框架布局,因此无需修改即可编译到Web上运行。
在Flutter中,您可以通过创建Paragraph对象并调用其layout()方法来放置一段文本。不幸的是,Web缺少直接的文本布局API。我们用来测量文本布局属性的技巧是让浏览器将其布局,然后从DOM元素中读回相关属性。
放置文本段落时,Flutter测量段落的高度,宽度,最大内在宽度,最小内在宽度以及字母和表意基线(下图黄色虚线)。这些属性如下所示。
Paragraph layout attributes
您可以在Flutter的段落文档中找到更多详细信息。
要测量这些属性,我们首先在HTML DOM元素中放置一个段落,然后我们读取元素的尺寸。这会导致浏览器将其布局。例如,要获取元素的宽度和高度,我们调用offsetWidth及offsetHeight。为了测量基线,我们将段落放置在一个元素中,该元素配置为使用flex row进行自我布局。在段落旁边,我们放置另一个名为“探针”的元素。因为探针与文本的基线对齐,所以调用 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画布的HTML DOM。
我们更喜欢HTML + CSS,因为它有浏览器的显示列表支持。这意味着我们可以优化图片的光栅化在浏览器上的渲染引擎。这也意味着我们可以应用任意变换,尤其是旋转和缩放,而不必担心像素化。我们将此画布实现称为“DomCanvas”。
如果我们无法使用HTML + CSS表达图片,我们会回到画布。 Canvas 2D允许我们绘制几乎所有的Flutter绘图命令。如果将Flutter的Canvas与Web的 CanvasRenderingContext2D 进行比较,您会发现许多相似之处,在画布上绘画是高效的,因为它不会创建需要随时间维护的可变树节点,如HTML DOM或SVG。
2D画布的一个挑战是浏览器将其表示为位图,即存储宽度x高度像素的内存缓冲区。因此,缩放画布会导致像素化。如果缩放导致图片大小的变形,我们需要调整画布大小。我们发现分配画布相当昂贵,因此调整它们的大小。最重要的是,当将多个画布合成到同一页面上时,浏览器必须执行栅格合成,合成栅格与显示列表的工作方式不同。您可以将多个显示列表绘制到同一个内存缓冲区中。我们调用Canvas 2D支持的canvas实现BitmapCanvas。我们正在研究使位图画布更有效的方法。
为了表达Flutter的 opacity, transform, offset, clip rect和其他图层,我们使用纯HTML元素。例如,不透明度层变为元素,其上具有不透明度CSS属性,变换层变为带有变换CSS属性的元素,而clip rect变为带有overflow:hidden的。
完成所有操作后,帧将被渲染在作为HTML元素树的页面上呈现,其中DomCanvas和BitmapCanvas作为叶节点。例如:
Sample HTML DOM structure of a frame
Flutter Engine中的等效Flutter层级树(称为 流层 )如下所示:
Sample Flutter Engine layer structure
在结构上它们非常相似。最大的区别是,在Web上,我们必须根据内容选择不同的图片实现。
HTML + CSS + Canvas适用于所有现代浏览器。但是,我们已经在展望未来:
CSS Paint API
CSS Paint是一个新的Web API,是Houdini的一部分。 Houdini是指许多浏览器供应商之间的合作,旨在向开发人员展示CSS引擎的某些部分。特别是,CSS Paint API允许开发人员在这些元素请求绘制时将自定义图形绘制成HTML元素。例如,您可以将元素背景的绘制任务分配给自定义CSS 画笔(painter)完成。它与canvas非常相似,但有以下重要区别:
在撰写本文时,Chrome和Opera是目前仅有的支持CSS Paint的浏览器。但是, 其他浏览器处于实现过程中的各个阶段 。
我们在Web上运行Flutter中的CSS Paint API得到了实验性支持,它已经显示出良好的结果,特别是在性能方面。我们的实现只是将paint命令序列化为自定义CSS属性。paint worklet读取这些命令并执行它们。我们使用普通的
和 HTML元素渲染文本。
我们当前的序列化机制不是特别有效 - 它是一个将嵌套列表转换为JSON的树 - 但一部分Houdini项目的作法是添加对类型化数组的支持。要让它变成可用的话,我们需要将绘制命令编码为类型化数组而不是JSON字符串。类型化数组是可转移的,这意味着它们可以通过引用从主隔离传递到paint工作,这个操作不涉及复制内存。
Interop and embedding(互操作及嵌入)
从Flutter调用Dart库
Flutter Web应用程序可以完全访问当今在Web上运行的所有现有Dart库。
从Flutter调用Javascript库
Flutter Web应用程序完全支持Dart的JS-interop包:package:js和dart:js。
在Flutter Web应用程序中使用CSS
目前,Flutter假设完全控制网页的正确性和性能。例如,我们只使用遵循某些性能指南的一小部分CSS,例如csstriggers.com/。在页面上放置任意CSS可能会导致Flutter表现不可预测。
另一个在Flutter Web应用程序中避免使用CSS的原因是,在设计时,Flutter需要在渲染帧时知道所有布局属性。 CSS充当黑盒子的角色。例如,如果要显示可滚动的widget列表,则必须实例化并为所有widget生成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,如上所述,它可能会有问题。需要更多的研究。
可移植性
我们的目标是尽可能多地将flutter框架移植到Web上。但是,这并不意味着任何Flutter应用程序可以不用更改代码就可以在Web上运行。Flutter Web用程序仍然是一个Web应用程序;它在浏览器中被沙箱化,只能执行Web浏览器允许的操作。例如,如果您的Flutter应用程序使用到了没有Web实现的Native 插件,例如ARCore,您将无法在Web上运行该应用程序。同样,它也没有直接访问文件系统或底级网络的权限。
当前状态
我们构建了足够的Web引擎来渲染大部分Flutter Gallery示例中的内容。我们还没有移植Cupertino Widget,但所有Material Widgets,Material Theming,以及Shrine和Contact Profile演示应用程序都已经可以在Web上运行。(演示视频地址)
源代码放置在何处?
我们计划很快开源这个项目,并很高兴与开源社区分享。该项目最初是作为Google内部源代码树的一项探索而开始的。我们的代码稳定后,我们打算将开发转移到GitHub,我们有机会将其与内部基础架构分开,与此同时,如果您在 github.com/flutter 看到与Web相关的拉取请求,请不要感到惊讶!
结论
我希望这篇文章能让您了解到我们正在解决的问题,以使Flutter在Web上良好运行。我们欢迎您的想法和主意。 请继续关注Google I / O 2019!
https://medium.com/flutter-io/hummingbird-building-flutter-for-the-web-e687c2a023a8