在今天的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元素。例如,不透明度层变为<flt-opacity>元素,其上具备不透明度CSS属性,变换层变为带有变换CSS属性的<flt-transform>元素,而clip rect变为带有overflow:hidden的<flt-clip-rect>。
完成全部操做后,帧将被渲染在做为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很是类似,但有如下重要区别:
-
这个绘画不是由主JavaScript虚拟机实例完成的(
此处感谢评论一楼 dilbert2指正,经查
“An isolate is only an instance of a JavaScript VM and it only has its own JavaScript heap.”),而是由一个叫作paint worklet的东西完成的。它有本身的内存空间,在提交DOM更改以后,在浏览器的绘制阶段执行绘制工做。
-
CSS绘制由显示列表支持,而不是位图。这给了咱们一箭双鵰的好处 - 兼顾2D画布般的绘画效率和没有像素化。
-
目前CSS绘画不支持绘画文本。
咱们在Web上运行Flutter中的CSS Paint API获得了实验性支持,它已经显示出良好的结果,特别是在性能方面。咱们的实现只是将paint命令序列化为自定义CSS属性。paint worklet读取这些命令并执行它们。咱们使用普通的<p>和<span> 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应用程序中
咱们尚未为此添加适当的支持,但咱们打算在未来进行探索。咱们正在考虑的几种方法是使用<iframe>和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!