Hummingbird: 在Web上執行Flutter應用

Jothy發表於2019-03-03

Hummingbird: 在Web上執行Flutter應用


原文作者:Yegor Jbanov

譯者:UC 國際研發 Jothy


寫在最前:歡迎你來到“UC國際技術”公眾號,我們將為大家提供與客戶端、服務端、演算法、測試、資料、前端等相關的高質量技術文章,不限於原創與翻譯。


今天,我們在 Flutter Live 上宣佈了一個訊息:我們正嘗試在 Web 上執行 Flutter。 這篇文章描述了我們應對挑戰的方式,以及該技術的當前狀態。 在文末,我們附上了協同工作和嵌入等問題的答案。

Hummingbird: 在Web上執行Flutter應用

讓我們快速回顧一下 Flutter 的架構。 Flutter 是一個多層系統,這樣高的層更易用,用很少的程式碼就能表達很多,而較低的層能提供更多的控制,代價是必須處理一些複雜性。 當較高層不能滿足開發者的需求時,它們可以降到較低層。 開發者可以訪問 Flutter Engine 之上的所有層。

Hummingbird: 在Web上執行Flutter應用Flutter 的 Mobile 架構

在 Flutter 中,Flutter Engine 作為最低階別的庫 dart:ui 暴露。它不關心 元件,物理實現,動畫或佈局(文字佈局除外)。它所關心的是如何將圖片組合到螢幕上,渲染變成畫素。在 dart:ui 上直接編寫應用是很困難的。這正是我們建立更高層的原因。

dart:ui 之上的一切是我們所謂的“框架”。它下面的一切都是“引擎”。該框架完全使用 Dart 語言編寫。大多數引擎都是用 C++ 編寫的,特定於 Android 的部分用 Java 編寫,而 iOS 特定的部分用 Objective-C 編寫。 dart:ui 中的一些基本類和函式是用 Dart 編寫的,主要用作 Dart 和 C++ 之間的橋樑。

Flutter 還提供外掛系統。外掛使用指定語言編寫,可以直接訪問移動生態系統日積月累的 OEM 庫和第三方庫。你可以使用 Java 或 Kotlin 為 Android 建立外掛。 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 誕生之初,它就一直在編譯 JavaScript。 現在有許多重要的應用都從 Dart 編譯為 JavaScript,並在生產環境中執行。 Flutter 的編譯策略依賴於同樣的基礎設施。

當我們開始探索時,我們面臨著 UI 渲染的幾種選擇。 我們很快意識到,要想支援的特定 Flutter 層,決定了我們將用什麼 Web 技術。 我們構建了三個原型:

  • 僅僅是 Widgets(元件):這個原型實現了 Flutter 的 widget 框架,並提供了一組核心佈局 widget 作為構建自定義 widget 的基礎。 對於佈局和定位,它依賴於 Web 的內建功能,例如 flexbox,grid 佈局,瀏覽器滾動(通過 overflow:scroll 實現)等。

  • Widgets + 自定義佈局:此原型包括 Flutter 的佈局系統(由 RenderObject 提供),但將渲染物件直接對映到 HTML 元素。

  • Flutter Web Engine:這個原型保留了 dart:ui 之上的所有層,並提供了一個在瀏覽器中執行的 dart:ui 實現。

Flutter 最有價值的功能之一是它可以跨平臺移植。 你完全可以(有時甚至被鼓勵)編寫自定義的特定平臺程式碼,程式碼無需跨平臺定製即可共享。 意味著使用單個程式碼庫就可以編寫面向多個平臺的應用。

在嘗試將幾個示例應用移植到 Web 之後,我們意識到原型 #1 和 #2 不能提供 Flutter 開發者喜歡的可移植性級別。 因此,我們決定使用 Flutter Web Engine 設計的原型 #3,因為它有著平臺之間最高的框架級程式碼重用:

Hummingbird: 在Web上執行Flutter應用Flutter的Web架構 (Hummingbird)

既然我們知道我們想要實現整個 dart:ui API,我們需要選擇一組 Web 技術來構建。 Flutter 一次渲染一幀 UI。 在每個幀內,Flutter 會構建 widgets,執行佈局,最後在螢幕上繪製它們。

構建 Widgets

Widget 構建機制不依賴於應用執行的環境。該過程只是例項化記憶體中的物件,跟蹤其狀態、以及狀態變更何時計算系統低階別的最小更新,佈局和繪製等。 將此部分移植到 Web 上非常簡單。 在 Dart 團隊用 dart2js 中實現了 super-mixin 支援之後,編譯器將所有 widget 和 widget frame 都編譯成了 JavaScript,幾乎沒有 issue 產生。


佈局

佈局系統有點棘手。 最大的挑戰是文字佈局。 除了 Center,Row, Column,Stack,Scrollable,Padding,Wrap 等之外的所有內容都由框架佈局,因此無需修改即可編譯到 Web。

在 Flutter 中,你可以建立 Paragraph 物件並呼叫其 layout() 方法來實現文字佈局。 不幸的是,Web 缺少直接的文字佈局 API。 我們用來測量文字佈局屬性的技巧是:先讓瀏覽器佈局,然後從 DOM 元素中讀回相關屬性。

佈局文字段落時,Flutter 會測量段落的高度,寬度,最大內在寬度,最小內在寬度以及字母和表意基線。 這些屬性如下所示。

Hummingbird: 在Web上執行Flutter應用Paragraph layout attributes

你可以在 Flutter 的 Paragraph 文件中找到更多詳細資訊。

要測量這些屬性,我們首先在 HTML DOM 元素中放置一個段落,然後讀取元素的維度。 這會引起瀏覽器佈局。 例如,要獲取元素的寬度和高度,我們呼叫 offsetWidth 及 offsetHeight。 為了測量基線,我們將段落放置在一個元素中,該元素配置為使用 flex 行進行佈局。 在段落旁邊,我們放置另一個名為 probe 的元素。 因為 probe 與文字的基線對齊,所以呼叫 getBoundingClientRect 就可以得到基線。 我們使用類似的技巧來測量最小和最大固有寬度。


Painting(繪製)

不得不提的是,我們得繪製 widgets。 對這個區域的探索最是麻煩,它仍然是我們的研究中最活躍的領域。 在框架最後,我們所有的 widget 都需要在螢幕上繪製成畫素。 在瀏覽器中,這意味著它們必須歸結為 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。 Canvas 2D 允許我們繪製幾乎所有的 Flutter 繪圖命令。如果將 Flutter 的 Canvas 與 Web 的 CanvasRenderingContext2D 進行比較,你會發現許多相似之處。在 Canvas 上繪畫很高效,因為它不會建立需要隨時間維護的可變樹節點,如 HTML DOM 或 SVG。

2D Canvas 的一個挑戰是瀏覽器將其表示為點陣圖,即儲存 Width x Height 畫素的記憶體緩衝區。因此,縮放 canvas 會導致畫素化。如果縮放導致圖片大小變化,我們也需要調整 canvas 大小。我們發現分配 canvas 操作相當昂貴,因此方案改成調整它們的大小。最重要的是,當將多個 canvas 合成到同一頁面上時,瀏覽器必須執行柵格合成,這也需要配置。合成柵格與顯示列表的方式不同。你可以將多個顯示列表繪製到同一個記憶體緩衝區中。我們呼叫 Canvas 2D 支援的 canvas 實現 BitmapCanvas。我們正在發掘使點陣圖 canvas 更高效的方法。

為了表達 Flutter 的不透明度,變換,偏移,剪輯矩形和其他圖層,我們使用純 HTML 元素。例如,不透明度層變為 <flt-opacity> 元素,其上具有 opacity CSS 屬性,變換圖層變為帶有 transform CSS 屬性的 <flt-transform> 元素,剪輯 rect 變為使用 overflow: hidden 的 <flt-clip-rect >。

完成所有操作後,框架將作為 HTML 元素樹呈現在頁面上,其中 DomCanvas 和 BitmapCanvas 作為葉節點。舉個例子?:

Hummingbird: 在Web上執行Flutter應用Sample HTML DOM structure of a frame


Flutter Engine 中的等效 Flutter layer tree(稱為flow layer)如下所示:

Hummingbird: 在Web上執行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 繪製器。 它與 canvas 非常像,但有以下重要區別:

  • 這個繪畫不是由核心 JavaScript 獨立完成的,而是由一個叫做 paint worklet 的東西完成的。 它有點像 web worker,因為它有自己的記憶體空間。 它會在 DOM 更改提交之後,在瀏覽器的繪製階段執行繪製工作。

  • CSS paint 由顯示列表支援,而不是點陣圖。 這真是兩全其美 - 2D canvas 般的繪畫效率和無畫素化。

  • 目前 CSS paint 不支援繪製文字。

在撰寫本文時,Chrome 和 Opera 是唯一在正式版本中支援 CSS Paint 的瀏覽器。 而其他瀏覽器正處於釋出各自實現的不同階段。

我們在 Flutter for Web 中對 CSS Paint API 進行了實驗性支援,它已經展現出良好的結果,特別是在效能方面。 我們的實現只是將 paint 命令序列化為自定義 CSS 屬性。 paint worklet 讀取這些命令並執行它們。 我們使用像 <p> 和 <span> 這樣普通的 HTML 元素來渲染文字。

我們當前的序列化機制不是特別有效 - 它是一個巢狀列表轉換成的 JSON 樹 - 但 Houdini 專案的一部分是新增對型別化陣列的支援。 當它可用時,我們會將繪製命令編碼為型別化陣列而不是 JSON 字串。 型別化陣列是可轉移的,這意味著它們可以通過引用從主 JavaScript 傳遞到 paint worklet,而不復制記憶體。



Hummingbird: 在Web上執行Flutter應用協同和嵌入

從 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 列表,則必須例項化併為所有 widgets 生成 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,如上所述,它可能會有問題。 需要更多的研究。


可移植性

我們的目標是儘可能多地將框架移植到 Web 上。 但是,這並不意味著任何 Flutter 應用將在 Web 上執行而不更改程式碼。 Flutter Web 應用仍然是一個 Web 應用; 它在瀏覽器中被沙箱化,只能執行 Web 瀏覽器允許的操作。 例如,如果你的 Flutter 應用使用 Web 未實現的本機外掛(例如 ARCore),你將無法在 Web 上執行該應用。 同樣,也無許可權直接訪問檔案系統或低階網路。


當前狀態

我們構建了足夠的 Web 引擎來渲染大部分 Flutter Gallery。 我們還未移植 Cupertino widgets,但所有 Material widgets,Material Theming,以及 Shrine 和 Contact Profile 演示應用均已執行在 Web 上。

Flutter running in desktop Chrome 演講視訊

https://www.youtube.com/watch?v=5IrPi2Eo-xM

原始碼在哪裡?

我們計劃很快開源這個專案,並很高興與開源社群分享。 該專案最初是作為 Google 內部原始碼樹的一項探索而開始的。 待程式碼穩定後,我們打算將開發轉移到 GitHub,我們有機會將其從內部基礎架構中剝離出來。 與此同時,如果您在 github.com/flutter 組織下看到與 Web 相關的 pull 請求,請不要感到驚訝!


結論

希望這篇文章能讓你瞭解我們正在解決的問題,以幫助 Flutter 在 Web 上更好執行。 歡迎表達您的觀點和意見。

請繼續關注 Google I/O 2019!


英文原文:

https://medium.com/flutter-io/hummingbird-building-flutter-for-the-web-e687c2a023a8



好文推薦:

我想學Flutter,但是我不知道應該如何開始?
Google Flutter團隊成員李宇騫 確認出席第13屆D2前端技術論壇




“UC國際技術”致力於與你共享高質量的技術文章

歡迎關注我們的公眾號、將文章分享給你的好友

Hummingbird: 在Web上執行Flutter應用



相關文章