[譯] Hummingbird: Web端執行Flutter

JasonWuuu發表於2019-02-28

筆者注: 隨著 Flutter 1.0 穩定版本的釋出,跨平臺開發的趨勢收到越來越多人的關注,然而 Flutter 的野心不止在跨移動平臺 Android 和 iOS,在2019的當下,Flutter將逐步開展跨 Web 端的工作並取得了初步結論和經驗,本文原文為 Flutter 團隊釋出的 Web 端執行 Flutter 試驗性成果報告,內容豐富且極具啟發性,因此特意將其翻譯為中文供大家學習。   原文:Hummingbird: Building Flutter for the Web

在今天的 Flutter Live 上,我們宣佈正嘗試在 Web 端執行 Flutter 。 在這篇文章中,我們描述了我們如何應對挑戰以及的當前的技術狀況。 在帖子的最後,你將找到有關互動操作和嵌入問題的答案。

[譯] Hummingbird: Web端執行Flutter
讓我們快速回顧一下 Flutter 的架構。 Flutter 是一個多層系統,這樣更高的層更容易使用,並允許你用很少的程式碼來實現更多的功能,而較低的層提供更多的控制,代價是必須處理一些複雜性。 當較高層不能滿足開發人員的需求時,它們可以降到較低層。 開發人員可以訪問 Flutter Engine 上方的所有層。

Flutter for Mobile Architecture

Flutter Engine 作為 Flutter 中最低階別暴露的庫,dart:ui。它對 widgets、物理 (physics)、動畫或佈局(文字佈局除外)一無所知。它所知道的是如何將 pictures 組合到螢幕上並將它們變成畫素。在 dart:ui 上直接編寫應用程式是很困難的。這就是建立更高層級的原因。

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

Flutter 還提供外掛系統。外掛是用一種語言編寫的程式碼,可以直接訪問移動生態系統隨著時間累積的 OEM 庫和第三方庫。要為 Android 建立外掛,你可以編寫 Java 或 Kotlin。 iOS 外掛是使用 Objective-C 或 Swift 編寫的。

你好,Web

Web 平臺已經發展了數十年,包括許多技術和規範。有一些總括性術語用於描述大量相關功能:HTML,CSS,SVG,JavaScript,WebGL。為了在 Web 上執行 Flutter,我們需要:

  • 編譯 Dart 程式碼:Flutter 是用 Dart 編寫的,我們需要在 Web 上執行 Dart。
  • 選擇要在 Web 上執行的 Flutter 子集 (subset):在 Web 上執行所有 Flutter 程式碼是不實際或有用的。其中一些是特定於平臺的,例如 Android 和 iOS。
  • 選擇足夠的 Web 功能子集 (subset):隨著時間的推移,Web 平臺會累積功能重疊的功能。例如,你可以使用 HTML + CSS,SVG,Canvas 和 WebGL 繪製圖形。

只要語言存在,Dart 就一直在編譯 JavaScript。許多重要的應用程式從 Dart 編譯為 JavaScript,並在今天的產品中執行。 Flutter 的編譯策略依賴於同樣的基礎。

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

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

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

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

Flutter 最有價值的功能之一是它可以跨平臺移植。跨平臺可以共享相同的程式碼,雖然你可以(有時甚至鼓勵)編寫自定義平臺特定程式碼。這允許使用單個程式碼庫編寫面向多個平臺的應用程式。

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

Flutter for the Web Architecture (Hummingbird)

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

構建 widgets

Widgets 構建機制不依賴於應用程式執行的環境。該過程只是例項化記憶體中的物件,跟蹤它們的狀態,以及狀態更改何時,計算系統的較低層級、佈局、繪製所需的最小更新。 將此部分移植到 Web 上非常簡單。 在Dart 團隊在 dart2js 中實現了 super-mixin 支援後,編譯器將所有 widgets 和 widgets 框架編譯為JavaScript,幾乎沒有任何問題。

佈局 (Layout)

佈局系統有點棘手。 最大的挑戰是文字佈局。 其他所有內容 - 中心,行,列,堆疊,可滾動,填充,換行等 - 由框架佈局,因此無需修改即可編譯到Web。

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

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

Paragraph layout attributes

你可以在 Paragraph documentation 中找到更多詳細資訊。

要測量這些屬性,我們首先在 HTML DOM 元素中放置一個段落,然後我們讀取元素的維度。 這會導致瀏覽器將其佈局。 例如,要獲取元素的寬度和高度,我們呼叫 offsetWidthoffsetHeight。 為了測量基線,我們將段落放置在一個元素中,該元素被配置為使用 flex 行進行自我佈局。 在段落旁邊,我們放置另一個名為 “probe” 的元素。 因為 probe 與文字的基線對齊,所以呼叫 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 canvases 的 HTML DOM。

我們更喜歡 HTML + CSS,因為它有瀏覽器 display list 的支援。這意味著我們可以優化圖片的光柵化到瀏覽器的渲染引擎。這也意味著我們可以應用任意變換,尤其是旋轉和縮放,而不必擔心畫素化。我們將此 Canvas 實現稱為 DomCanvas

如果我們無法使用 HTML + CSS 表達圖片,我們會回到 canvas。 Canvas 2D 允許我們繪製幾乎所有的 Flutter 繪圖命令。如果你將 Flutter 的 Canvas 與 Web 的 CanvasRenderingContext2D 進行比較,你會發現許多相似之處。在 canvas 上繪製是有效的,因為它不會建立需要隨時間維護的可變樹節點,如 HTML DOM 或 SVG。

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

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

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

Sample HTML DOM structure of a frame

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

Sample Flutter Engine layer structure

在結構上它們非常相似。最大的區別是,在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 canvas 般的繪製效率和無畫素化。

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

在撰寫本文時,Chrome 和 Opera 是唯一支援 CSS Paint 生產的瀏覽器。但是,其他瀏覽器處於實現過程的各個階段

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

我們當前的序列化機制不是特別有效——它是一個巢狀列表轉換為 JSON 的樹——但 Houdini 專案的一部分是新增對型別化陣列 (typed arrays) 的支援。當它變得可用時,我們將繪製命令編碼為型別化陣列而不是 JSON 字串。型別化陣列是可轉換的 (Transferable) ,這意味著它們可以通過引用從主隔離區傳遞到繪製工作區。不涉及複製記憶體。

互動操作和嵌入

從 Flutter 呼叫 Dart 庫

Flutter Web 應用程式可以完全訪問當今在 Web 上執行的所有現有 Dart 庫。

從 Flutter 呼叫 JavaScript 庫

Flutter Web 應用程式完全支援 Dart 的 JS-interop 包:package:jsdart:js

在 Flutter Web 應用程式中使用 CSS

目前,Flutter 假設完全控制網頁的正確性和效能。例如,我們只使用遵循某些效能指南的一小部分 CSS,如https://csstriggers.com/。在頁面上放置任意 CSS 可能會導致 Flutter 表現不可預測。

在 Flutter for Web 應用程式中避免使用 CSS 的另一個原因是,在設計時,Flutter 需要在呈現框架時知道所有佈局屬性。 CSS 像是個黑盒子。例如,如果要顯示可滾動的 widgets 列表,則必須例項化併為所有 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 瀏覽器允許的操作。 例如,如果你的 Flutter 應用程式使用沒有 Web 實現的本機外掛(例如 ARCore ),你將無法在Web上執行該應用程式。 同樣,沒有直接訪問檔案系統或低階網路的許可權。

當前狀態

我們構建了足夠的 Web 引擎來渲染大部分的 Flutter Gallery。 我們還沒有移植 Cupertino widget,但所有Material widget、Material Theming,以及 Shrine 和 Contact Profile 的 demo 都在 Web 上執行。

原始碼在哪裡?

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

結論

我希望這篇文章能讓你瞭解我們正在解決的問題,以使 Flutter 在 Web 上很好的執行。 我們歡迎你的想法和創意。

請繼續關注Google I / O 2019!

如果喜歡的話可以點個關注或收藏吧!本文為個人原創翻譯,轉載請註明出處。

相關文章