翻譯: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上運行。