今天聊:如何將 Web 程式碼渲染成 Flutter

前端早早聊發表於2021-05-26

前端早早聊大會,與掘金聯合舉辦。加 codingdreamer 進大會技術群,贏在新的起跑線。


第二十七屆|前端 Flutter 專場,瞭解 Web 渲染引擎|UI 框架|效能優化,6-5 下午直播,6 位講師(淘寶/京東/閒魚等),點我上車? (報名地址):

大會海報.png

所有往期都有全程錄播,上手年票一次性解鎖全部


正文如下

本文是第十七屆 - 前端早早聊框架專場,也是早早聊第 117 場,來自飛豬-南麓 的分享

前言

當前,前端技術日新月異,僅看移動端上的渲染方案。從早期的 H5 Wap,到藉助客戶端能力通過離線包、prefetch、JSBridge 提升效能和擴充套件功能的 Hybrid 方案,再到前幾年大火的以 Weex/ReactNative 為代表的的大前端融合渲染方案,以及最近幾年各大廠商陸續推出的商業價值大於技術價值的小程式方案。前端技術的選型已由純粹的效能追求,演變到效能與效能、甚至業務價值的博弈,而每個新技術的誕生,也都有其背後的場景價值。

本文作者將就 Flutter 這一新的客戶端渲染方案和讀者分享一下對下一代高效能前端渲染思路 Web On Flutter 的思考。看看 Flutter 之於 Web 能碰撞出什麼樣的火花。

初識 Flutter

這裡先給不太瞭解 Flutter 的讀者做一下簡單介紹:Flutter 是由 Google 於2017年推出並開源的一個移動應用開發框架。其一大特點是基於 Skia 實現了一套自繪引擎,可以同時執行在移動端、 IOT 等多種平臺。Flutter 使用 Dart 開發,Dart 語言的一個特點是既支援 JIT(即時編譯)又支援 AOT(提前編譯),如此便可在開發階段採用 JIT 模式進行高效開發,同時在釋出階段享受 AOT 模式的高效能。

作為 Google 出品的拳頭級產品,Flutter 一經推出便大受歡迎,目前在 Github 中有超過 10W的 star,平均每月1.8次(2020年資料)的 Stable 版本迭代也足見維護力度。此外,Flutter 也備受大廠青睞,阿里、騰訊、位元組、美團等都在開展相關佈局建設,而在阿里內部,也有淘寶、閒魚、飛豬、盒馬等多個 BU 落地了實際業務。

Flutter 的優勢

那麼是什麼原因讓 Flutter 如此受追捧呢,我們對比一下當前移動端主流的渲染方案:

可以看到 Flutter 兼具 Native 的高效能和 WebView 的低開發成本,同時又因為自繪而具備極佳的渲染一致性,因此也就不難解釋為何大家都對 Flutter 抱有如此大的想象和期待。

Flutter 的技術特點

我們再簡單介紹一下 Flutter 的技術特點:

  • 自繪引擎

Flutter 基於 Skia 這一跨平臺圖形庫自建了一套渲染管線,而不是使用系統(Android、iOS)的原生控制元件或者 WebView 的渲染管線。

自繪帶來的好處顯而易見,根本上解決了跨端一致性問題(這裡可以對比 Weex,Weex 是將渲染一致性問題轉移到容器層解決,但這也導致了容器的愈加難以維護)。

  • 響應式框架

前面提到 Flutter 本質是一個開發框架,而從開發模式上說,它是一個響應式框架,這也是 Flutter 開發高效的一個原因(這裡我們暫且忘記它的元件地獄巢狀吧~)。我們看個示例,一個簡單的“數字增加”元件,在 Flutter 裡可以這麼實現:

對於前端同學來說是否有種熟悉感,沒錯,這個寫法和 JSX 非常相似!包括元件巢狀和狀態更新。

  • 一切皆widgets

“Widget 是 Flutter 應用程式使用者介面的基本構建塊。每個 Widget 都是使用者介面一部分的不可變宣告。與其他將檢視、控制器、佈局和其他屬性分離的框架不同,Flutter 具有一致的統一物件模型:Widget。”上面這段是 Flutter 官網的原話,翻譯到前端語境,Widget 既可以是 div、span 這種結構元件,也可以是 padding、opacity 這種樣式元件,同時也可以是 dialog 這種功能元件......

而這些 Widgets 會根據佈局形成一個層次結構,也就是一棵 Widgets 樹,這點讀者可以先留個印象,在後面的介紹中會有用到。

Flutter 與 Web

前文中,我們對 Flutter 有了初步瞭解,也簡單對比了它和其他主流移動端渲染方案的優劣勢。在本節中,我們將重點看一下 Flutter 對 Web(或者說前端)的衝擊,及可能碰撞的火花。

Flutter 對前端的衝擊與結合

一直以來,前端相較於客戶端,比較大的優勢體現在以下4個方面:

  • 人力成本: 1 vs 2(iOS + Android)
  • 開發效率:JIT vs AOT
  • 投放場景:跨端 vs 單端
  • 迭代頻次:隨時 vs 發版

但是與 Flutter 相比,前端的“人力成本”和“開放效率”優勢將極大減弱,不過依然保留著跨端投放和高頻迭代的優勢,那麼我們是否可以將二者的優勢結合起來呢?答案便是 Web On Flutter。

Web On Flutter 技術思路

既然是以 Web 的方式開發,用 Flutter 引擎渲染,那就意味著渲染流程的前半段是 Web,而後半段是 Flutter。如何橋接二者的渲染管線便成了破題的重點,這裡可以有3個切入點:

  • 切入點A:用 Web 的 DOM 模擬 Flutter 的 Widget。這個方案對前端開發有很強的約束,需要遵循 Flutter 的元件思維來開發頁面。目前社群裡的 MXFlutter 便是這個思路。
  • 切入點B:用 Flutter 的 Widget 模擬 Web 的 DOM。這個方案的難點在於精準的樣式對映,比較適合限定(W3C標準子集)的前端場景。目前飛豬的 Flugy 方案採用的正是這個思路。
  • 切入點C:將 Web 的 DOM 樹直接對映到 Flutter 的 RenderObject 樹上。這個方案的好處是,相比和 Widgets 樹的橋接,將 DOM 樹直接橋接到 RenderObject 樹可以更細力度的操作,理論上樣式還原的上限會更高點。但因其是在 Flutter 內部的 RenderObject 上進行了擴充套件,所以也會對 Flutter 版本有更大的敏感度。目前手淘的 Kraken 便是這個思路。
  • 切入點C Plus:這個切入點在圖中沒有表現,其實是對 Flutter 的更深入改造,既重寫 Flutter Rendering 層,好處是不用擔心 DOM 樹和 RenderObject 樹的不對齊,但相應的開發成本也是巨大的。目前手淘的 Unicorn 在嘗試這方面的探索。

通過下圖,讀者可以進一步理解這 4 種思路的區別:

Web On Flutter 實現原理

本節將以 Widget 模擬 DOM 的思路為例,分析一下可行的實現原理。我們先通覽一下該思路下的整體渲染鏈路:

前面提到整條鏈路的關鍵點就是將 Web 渲染鏈路和 Flutter 渲染鏈路橋接起來,那橋接的第一步就是可以雙向通訊,這裡涉及的關鍵技術點就是 JS Binding;而後就是用 Widget 來模擬 DOM,生成最後用來繪製的 Widgets 樹,這裡需要關注的技術關鍵點包括 DOM 樹對映及 CSS 樣式對映 ;同時我們也需要關注到如何進行事件繫結。接下來我就以上述 4 個技術關鍵點分別進行介紹。

  • JS Binding

在 WebView 中,我們常採用重寫 Alert / Prompt、攔截 URL 或 API 注入等方案來進行 JS 和 Native 的通訊。在 Web On Flutter 中,JS 和 Flutter 的通訊也可以採用類似 API 注入的方案。也就是 Flutter 通過 JS 引擎 向 JS 全域性上下文掛載變數,比如一個 function,而後 JS 便可以通過該 function 調到 Flutter 的方法,反過來,Flutter 也可以直接通過 JS 引擎訪問到 JS 全域性上下文中的變數。如此雙向通訊便可建立起來,下面用張圖來直觀的展示一下這個過程:

這裡擴充套件補充一下上圖的 C++ 膠水層的含義。Dart 程式碼可以通過 dart:ffi 庫來呼叫本地的 C API,但 JS 引擎又只能被 C++ 呼叫,所以我們需要編寫一層 C++ 膠水層來封裝所需使用的 JS 引擎 API,再通過 extern C 標記來編譯成 C 產物給到 Dart 來呼叫。

再進一步分析整條通訊鏈路,可以看到一大瓶頸是跨語言通訊(JS <=> C/C++ <=> Dart),作者曾做過實驗,JS 無參調 Dart 單次耗時在 0.05ms 左右,Dart 無參調 JS 則需要 0.08ms 左右,只看單次確實很快,但是考慮到一個真實的頁面的渲染指令數可能在 1000 量級,並且攜帶大量引數,所以最終耗時會很容易超過 100ms,所以我們還需要進一步優化,可行的方案如快取渲染指令進行批量呼叫等,這裡就不做展開了。

  • DOM 樹對映

我們再來看一下 Web 中的 DOM 樹如何轉成 Flutter 中的 Widgets 樹。我們知道 Dom 樹在前端是通過createElement、appendChild 等 DOM API 來進行建立的,整個建立過程實質就是一條條渲染指令。

對於createElement 指令,Flutter 在接收到後,會根據所要建立的 Element 的特性,用多個 Widget 組合出來。比如 body 元素,最外層需要一個 Container Widget 來包裹以便設定一些通用樣式,body 的子節點是縱向排布的,所以還需要一個 Column Widget,最後考慮到它是可滾動的,還需要一個SingleChildScrollView 來支援。注意這裡模擬 body 的 Widgets 組合只是簡單示例一下,實際實現要考慮到多種情況會複雜的多。

對於 appendChild 指令,還記得前面提到 Flutter 是響應式框架嗎,我們可以類比在 React 中增刪元件,用 setState 的方式來維護一個節點的子節點。

整體的流程可以參看下圖:

  • CSS 樣式對映

在完成 DOM 樹到 Widgets 樹轉換之後,我們需要考慮如何將 CSS 樣式在 Flutter 中還原,還記得前面提到的渲染指令嗎,除了告知 Flutter 需要建立什麼型別的 Element,它還會攜帶這個元素的屬性,其中就包括了樣式(通過 Webpack 等工具將 CSS 轉成內聯樣式)。我們所需要做的就是將這些樣式用一個或多個 Wdiget 來還原實現,這裡我們舉兩個例子,一個是絕對定位,一個是底部對齊(比如價格和¥符):

Flutter 提供了一個 Stack Widget,允許子元件堆疊,而 Positioned Widget 可以根據 Stack 的四個角來確定自身位置,如此便可以模擬 Web 中的絕對定位。

底部對齊的方案有很多,我們這裡說一下 Flex 方案,幸運的是 Flutter 提供了 Flex 元件,可以通過屬性設定很容易模擬 flex-direction、align-items 樣式。至於彈性空間的分配(flex: 1),也有Expanded Widget 可以模擬。

另外一些通用樣式,比如寬、高、背景色、邊框、圓角可以在 Container Widget 中設定;文字顏色、字型大小可以在 Text Widget 中設定;圖片填充模式可以在 DecorationImage Widget 中設定等等。

我們再通過下圖的一個常見商品卡片示例看下在 Flutter 中如何進行樣式還原:

  • 事件繫結

最後再看一下事件繫結如何來做:

  1. 在 Web 程式碼中,我們通過 Node.addEventListener(event, callback) 來監聽 DOM 互動事件,而這個監聽 API 會轉成 addEvent(nodeId, event) 指令調到 Flutter;
  2. Flutter 在捕獲到使用者的互動事件後,通過 nodeId 找到並觸發繫結在 Web 層 Node 上的事件回撥;

該過程用一張圖展示如下:

另外,Flutter 有著和 Web 類似的從內向外的事件冒泡機制,這也讓 Flutter 和 Web 之間的事件繫結更貼合,但可惜的 是 Flutter 沒有停止冒泡的機制,所以這塊還需我們自己去編碼模擬。

總結

我們做個簡單的回顧:

  1. Flutter 因其高效能、渲染一致的特性(自繪引擎),加之較高的開發效率(Dart 的 JIT 模式 + 響應式框架)和更低的人力成本(相比客戶端),廣受大家追捧;
  2. 面對 Flutter,前端的一些傳統優勢變得微弱,但我們可以嘗試將兩者的優點結合起來,也就是 Web On Flutter;
  3. Web On Flutter 的技術思路有多種,主要是看如何將 Web 的渲染流程和 Flutter 的渲染流程橋接起來;
  4. 這其中的技術關鍵點包括:JS Binding、Dom 樹對映、CSS 樣式對映、事件繫結;

順便打個廣告,作者所在的“飛豬-使用者前端和數字化經營團隊”HC多多,歡迎各位對旅行感興趣,或者對 Flutter、Serverless 、微前端、一體化開發、端渲染、互動營銷、招選投搭、智慧化、體驗技術、資料度量等等感興趣的同學加入我們。投遞郵箱:haonan.whn@ailbaba-inc.com。也歡迎關注我們的飛豬技術公眾號:Fliggy F2E,定期有高質量文章更新喔。


別忘了6-5 下午直播哦,點我上車? (報名地址):

大會海報.png

所有往期都有全程錄播,上手年票一次性解鎖全部


期待更多文章,點個贊

相關文章