Categories
程式開發

打破重重阻礙,Flutter 和 Web 生態如何對接?


先說結論:

不要對接!不要對接!不要對接!

開個玩笑,以上僅代表個人觀點,大家也知道這種“三體式警告”根本沒有用的,我自己也研究如何對接,說不定做完後就覺得“真香”了。

為什麼要對接?

首先討論一下為什麼要把 Flutter 對接到 Web 生態。

Flutter 現在是一個炙手可熱的跨平台技術,能夠一套代碼運行在 Android、iOS、PC、IoT 以及瀏覽器上,被認為是下一代跨平台技術。相比於 Weex 和 React Native 可以很好地解決多平台一致性問題,原生渲染性能相近,上層沒有 JS 那麼厚的封裝層次,整體性能會略好一些。

但是大部分興沖衝去學 Flutter 的人疑惑的第一個問題就是:為什麼 Flutter 要用 Dart?一個全新的語言意味著新的學習成本,難道 JS 不香嗎? JS 不香不是還有 TypeScript 嗎!事實上Flutter 拋棄的豈止是JS 這門語言,也拋棄了HTML 和CSS,設計了一套解耦得更好的Widget 體系,Flutter 拋棄的是整個Web,致力於打造一個新的生態,但是這個生態無法復用Web 生態的代碼和解決方案。尤其是之前所有跨平台方案 Hybrid、React Native、Weex 都是對接 Web 生態的,這讓 Flutter 顯得有些格格不入,也讓大部分前端開發者望而卻步。

下面是我整理出來的,前端開發者使用 Flutter 的各方面成本:

打破重重阻礙,Flutter 和 Web 生態如何對接? 1

因為Flutter 的開發模式和前端框架比較像(可以說就是抄的React),所以框架的學習成本並不高,稍微高一些的是Dart 語言的學習成本,另外還要學習如何用Widget 組裝UI,雖然很多佈局Widget 設計得和CSS 很像,靈活度還是差了很多。要想在真實項目中用起來,還要改造整個工具鏈,以“Native First”的視角做開發,開發 Flutter 和開發原生應用的鏈路是比較像的,和開發前端頁面有較大差異。最高的還是生態成本,前端生態的積累無論是代碼還是技術方案都很難復用,這是最痛的一點,生態也是 Flutter 最弱的一環。

無論是為了先進的技術理念還是出於商業私心,先不管Flutter 為什麼拋棄Web 生態,現實問題是最大的UI 開發者群體是前端,最豐富的生態是Web 生態,我覺得Web 技術也是開發UI 最高效的方式。如果能在上層使用 Web 技術棧開發,在底層使用 Flutter 實現跨平台渲染,不是可以很好的兼顧開發效率、性能和跨平台一致性嗎?還能複用 Web 技術棧大量的技術積累。

可能這些理由也不夠充分,暫且先照著這個假設繼續分析,最後再重新討論到底該不該對接。

關於 Flutter 和 Web 生態的對接涉及兩個方面:

  • 從 Web 到 Flutter。就是使用 Web 技術棧來開發,然後對接到 Flutter 上實現跨平台渲染。對 Web 來說是解決性能和跨平台一致性問題,對 Flutter 來說是解決生態復用問題。
  • 從 Flutter 到 Web。就是官方已經實現的 Web support for Flutter,把已經用 Dart 開發好的 App 編譯成 HTML/JS/CSS 然後運行在瀏覽器上,可以用於降級和外投場景。

如何實現“從 Web 到 Flutter”?

首先分析一下 Flutter 的架構圖,看看可以從哪裡下手。

打破重重阻礙,Flutter 和 Web 生態如何對接? 2

Flutter 可以分為 Framework 和 Engine 兩部分,Engine 部分比較底層也比較穩定了,最好不要動,需要改的是用 Dart 實現的 Framework。要想對接 Web 生態的話,JS 引擎肯定是要引入的,至於是否保留 Dart VM 有待討論。圖中最上面 Material 和 Cupertino 兩個 UI 庫前端是不需要的,前端有自己的。關鍵是 Widget 這部分,是替換成 HTML/CSS 的方式寫 UI,還是繼續保留 Widget 但是把語言換成 JS,不同方案給出的解法也不一樣。

有不少方案可以實現對接,業界有挺多嘗試的,我總結了下面三種方式:

  • TS 魔改:用 JS 引擎替換掉 Dart VM,用 JS/TS 重新實現 Flutter Framework(或者直接 dart2js 編譯過來)。
    JS 對接:引入 JS 引擎同時保留 Dart VM,用前端框架對接 Flutter Framework。
  • C++ 魔改:用 JS 引擎替換掉 Dart VM,用 C++ 重新實現 Flutter Framework。

TS 魔改

TS 魔改就是完全拋棄掉 Dart VM,用 TypeScript 重新實現一遍用 Dart 寫的 Flutter Framework。

為啥是 TS 而不是 JS?這不是因為 TS 是個大熱門嘛,而且向下兼容 JS,現在幾乎所有時髦的框架都要用 TS 重寫了。

這種方案的出發點是“如果能把Flutter 的Dart 換成JS 就好了”,最容易想到的路就是把Dart 翻譯成TS,或者直接用dart2js 把代碼編譯成js,但是編譯出來的代碼包含很多dart:ui 之類的庫的封裝,生成的包也挺大的,也比較難定制需要導出的接口,不如乾脆用TS 重寫一遍,工具鏈更熟悉一些,還可以加一些定制。

理論上講翻譯之後 Flutter 絕大部分功能都依然支持,可以復用各種 npm 包,還可以動態化,但是喪失了 AOT 能力,JS 語言的執行性能應該是不如 Dart 的。而且所有節點的佈局運算都發生在 JS,底層只需要提供基礎的圖形能力就好了,就好像是基於 Canvas API 寫了一套 UI 框架,性能未必有現存前端框架的性能高。

此外最大的問題是如何與官方 Flutter 保持一致,假如現在是從 v1.13 版本翻譯過來的,以後官方升級到了 v1.15 要不要同步更新?這個過程沒啥技術含量,而且需要持續投入,做起來比較噁心。

另外還需要考慮上層是用 Widget 的方式寫 UI,還是用前端熟悉的 HTML+CSS。如果依然用 Widget 的話,那大部分前端組件還是用不了的,UI 還是得重寫一遍。反正要重寫的話,成本也沒降下來,那就用 Dart 重寫唄…… 直接用官方原版 Flutter 也避免每次更新都要翻譯一遍 Dart 代碼。所以既然選擇了對接前端生態,那就要對接 CSS,不然就沒有足夠的價值。然而 CSS 和 Widget 的對接也是很繁瑣的過程,而且存在完備性問題。

JS 對接

翻譯代碼的方式不夠優雅,那就保留 Dart,把 JS/CSS 對接到 Widget 上面不就好了?

當然可以,這種方式是僅把 Flutter 當做了底層的渲染引擎,上層保持前端框架的寫法,僅把渲染部分對接到 Flutter。現存的很多前端框架都把底層渲染能力做了抽象,可以對接到不同渲染引擎上,如 Vue/Rax 同時支持瀏覽器和 Weex,用同樣的方式,可以再支持一個 Flutter。

這種方式對前端框架的兼容性比較好,但是鏈路太長了,業務代碼調用前端框架接口做渲染,一頓操作之後發出了渲染指令,這個渲染指令要基於通信的方式傳給Flutter Framework,這中間涉及一次JS 到C++ 再到Dart 的跨語言轉換,然後再接收到渲染指令之後還要轉成相應的Widget 樹,從CSS 到Widget 的轉換依然很繁瑣。而且Widget 本身是可以帶有狀態的,本身就是響應式更新的,在更新時會重新生成widget 並diff,如果在前端更新UI 的話,前端框架在js 裡diff 一次vdom,傳到Flutter 之後又diff 一次widget。

如果要繞過Widget 直接對接圖中的Rendering 這一層,可以繞過widget diff 但是得改Flutter Framework 的渲染鏈路,既然要改Flutter Framework 那為什麼不直接用TS 魔改呢,還繞過了JS到Dart 的通信,又回到了第一種方案。

總結來說,這個方案的優點是:實現簡單、能最大化保留前端開發體驗,缺點是:渲染鏈路長、通信成本高、響應式邏輯衝突、CSS 轉 Widget 不完備等。

C++ 魔改

想要幹掉Dart VM,就需要用其他語言重新實現用Dart 開發的Framework,用JS/TS 可以,用C++ 當然可以,最硬核的方式就是用C++ 重新實現Flutter 的Framework,然後接入JS 引擎,通過binding 把C++ 接口透出到JS 環境,上層應用還是用JS 做開發。

把 Framework 層下沉到 C++ 之後,不僅會有更好的性能,也能支持更多語言。原本Flutter Framework 是在Dart VM 之上的,必須依賴Dart VM 才能運行,所以對Dart 有強依賴;用C++ 重新實現之後,JS 引擎是在C++ 版Framework 之上的,框架本身並不依賴JS 引擎,還可以對接其他各種語言,如對接了JVM 之後可以支持Java 和Kotlin,對接回Dart VM 可以繼續支持Dart。

這個方案可以增強性能,也能保持和 Flutter 的一致性,但是改造成本和維護成本都相當高。 C++ 的開發效率肯定不如 Dart,當 Flutter 快速迭代之後如何跟進是很大的問題,如果跟進不及時或者實現不一致那很可能就分化了。從 CSS 到 Widget 的轉換也是不得不面對的問題。

幾種方案對比

把上面幾種方案畫在同一張圖裡是這個樣子的:

打破重重阻礙,Flutter 和 Web 生態如何對接? 3

圖中實線部分錶示了跨語言的通信,太過頻繁會影響性能,虛線部分錶示了其他對接可能性。

從下到上,Flutter Engine 是不需要動的,這一層是跨平台的關鍵。 Framework 則有三種語言版本,JS/TS、Dart、C++,性能是 C++ 版本最好,成本是 Dart 版本最低。然後還需要向上處理HTML/CSS 和Widget 的問題,可以直接對接一個前端框架,也可以直接在C++ 層實現(不然需要透出的binding 接口就太多了,用通信的方式也太過頻繁了) 。

如何實現“從 Flutter 到 Web”?

這個功能官方已經實現了,可以把使用 Dart 開發的 App 編譯成 Web App 運行在瀏覽器上,官方文檔以介紹用法和 API 為主,我這裡簡單分析一下內部具體的實現方案。

實現原理

結合 Flutter 的架構圖來看,要實現 Web 到 Flutter 需要改造的是上層 Framework,要實現 Flutter 到 Web 需要改造的則是底層 Engine。

打破重重阻礙,Flutter 和 Web 生態如何對接? 4

Framework 對 Engine 的核心依賴是 dart:ui,這是庫是在 Engine 裡實現的,抽像出了繪製 UI 圖層的接口,底層對接 skia 的實現,向上透出 Dart 語言的接口。這樣來看,對接方式就比較簡單了:

  • 使用 dart2js 把 Framework 編譯成 JS 代碼。
  • 基於瀏覽器的 API 重新實現 dart:ui,即 dart:web_ui。

把 Dart 編譯成 JS 沒什麼問題,性能可能會有一點影響,功能都是可以完全保留的,關鍵是 dart:web_ui 的實現。在原生Engine 中,dart:ui 依賴skia 透出的SkCanvas 實現繪製,這是一套很底層的圖形接口,只定義了畫線、畫多邊形、貼圖之類的底層能力,用瀏覽器接口實現這一套接口還是很有挑戰的。上圖可以看到Web 版Engine 是基於DOM 和Canvas 實現的,底層定義了DomCanvas 和BitmapCanvas 兩種圖形接口,會把傳來的layer tree 渲染成瀏覽器的Element tree,但是節點上僅包含了position, transform, opacity 之類的樣式,只用到CSS 很小的一個子集,一些更複雜的繪製直接用2D 實現。

存在的問題

我編譯了一個還算複雜的 demo 試了一下,性能很不理想,滑動不流暢,有時候圖片還會閃動。生成出來的 js 代碼有 1.1MB (minify 之後,未 gzip),節點層次也比較深,我評估這個頁面用前端寫不會超過 300KB,節點數可以少一半以上。

另外再看一下Flutter 倉庫的issue,過濾出platfrom-web 相關的,可以看到大量:文字編輯失效、找不到光標、ListView 在ios 上不可滾動、checkbox/button 行為不正常、安卓滾動卡頓圖片閃爍、字體失效、某些機型視頻無法播放、文字選中後無法複製、無法調試…… 感覺flutter for web 已經陷入泥潭,讓人回想起前端當年處理各種瀏覽器兼容性的噩夢。

這些性能和兼容性問題,核心原因是瀏覽器未暴露足夠的底層能力,以及瀏覽器處理手勢、用戶輸入和方式和 Flutter 差異巨大。

實現Flutter Engine 需要的是底層的圖形接口和系統能力,雖然提供了相似的圖形接口,如果全部用canvas 實現的話很難處理可訪問性、文本選擇、手勢、表單等問題,也會存在很多兼容性問題。所以真實方案裡用的是 Canvas + DOM 混合的方式,封裝層次太高了,渲染鏈路太長。就好像Flutter Framework 裡進行了一頓猛如虎的操作之後,節點生成好了、佈局算好了、繪製屬性也處理好了,就差一個畫布畫出來了,然後交到瀏覽器手裡,又生成一遍Element,再算一遍布局,在處理一遍繪製,最終才交給了底層的圖形庫畫出來。

再比如長頁面的滾動,瀏覽器裡只要一條CSS (overflow:scroll) 就可以讓元素可滾動,手勢的監聽以及頁面的滾動以及滾動動畫都是瀏覽器原生實現的,不需要與JS 交互,甚至不需要重新layout 和paint,只需要compositing。如上圖所示,在Flutter 中Animation 和Gesture 是用Dart 實現的,編譯過來就是JS 實現的,瀏覽器本身並不知道這個元素是否可滾,只是不斷派發touchmove 事件,JS 根據事件屬性計算節點偏移,然後運算動畫,​​然後把transform 或者新的position 作用到節點上,然後瀏覽器再來一遍完整的渲染流程……

優化方案

性能和兼容性的問題還是要解決的,短期內先把 issue 解掉,長線的優化方案,官方有兩種嘗試:

  1. 使用 CSS Painting API 做繪製。

a, 這是還處於提案狀態的新標準,可以用 JS 實現一些繪製功能,自定義 CSS 屬性。

b. 目前還未實現,需要等瀏覽器先把 CSS Houdini 支持好。

  1. 使用 WebAssembly 版本的 Skia 做繪製 https://skia.org/user/modules/canvaskit

a, 這樣可以發揮 wasm 的性能優勢,並且保持 skia 功能的一致。但是目前 wasm 在瀏覽器環境裡未必有性能優勢,這裡不展開討論了。

b. 已經部分實現,參考這裡的配置啟用功能: https://github.com/flutter/flutter/issues/41062#issuecomment-533952994

這兩個方案都是想更多的利用到瀏覽器的底層能力,只有瀏覽器暴露了更多底層能力,才能更好的實現 Flutter 的 Web Engine。不過這個要等挺久的時間,我們也參與不了,現階段想要使用 flutter for web,還是得保持現有架構,一起參與進去把 issue 解決掉,優先保障功能,其次優化性能。

一種適應性更好的架構

如果理想化一點,能不能從架構角度讓 Flutter 和 Web 生態融合的更好一些呢?

回顧文章最開始的官方架構圖,上面是 Framework(Dart),下面是 Engine(C++),切分在 Foundation 這一層,雙方之間的交互是幾何圖形信息。如果還保持這個架構,把切分層次劃分的更靠上一些,如下圖所示,劃分在Widgets 和Rendering 這一層,理論上講對Flutter 的開發者來說是無感知的,因為上層的開發語言和Widget 接口都是不變的。

打破重重阻礙,Flutter 和 Web 生態如何對接? 5

切分在這一層,Framework 和Engine 之間的交互就不再是幾何圖形而是節點信息,Widget 的組合、setState 響應式更新、Widget diff 都還在Dart 中,展開後的RenderObject 的佈局、繪製、裁剪、動畫全都在C++ 中,不僅有更好的性能,還可以與Engine 有更好的結合。

或者說,還原本保留 Engine 的設計,把下沉的這部分邏輯上劃分成 Renderer,就有瞭如下三層的結構:

打破重重阻礙,Flutter 和 Web 生態如何對接? 6

這樣劃分出來的每一層都有明確的定位:

  • Framework: 開發框架。為開發者提供可編程 API,實現響應式的開發模式,提供細粒度 Widget 供開發者自由封裝和組合。
  • Renderer: 渲染引擎。專門實現佈局、繪製、動畫、手勢的的處理,這部分功能相對獨立,是可以與開發框架解耦的,也不必與特定語言綁定。
  • Engine: 圖形引擎。實現跨平台一致的圖形接口,合成輸入的層並繪製到屏幕上,處理好平台力的接入和適配。

這樣切分除了有性能優勢以外,也使得渲染引擎擺脫了對 Dart 的依賴,能夠支持多種語言,也能支持多種開發模式。對接到Dart VM 就可以用Dart 寫代碼,對接到JS 引擎就可以用JS 寫代碼,對接到JVM 還可以寫Java,但是無論怎麼寫,底層的渲染能力是一樣的,一套統一的佈局算法,動畫和手勢的處理行為也是一致的。

在這樣的架構下,對接 Web 生態就更容易了。 Dart 和Widget 是前端不想要的,希望能換成JS 和CSS,但是又想要底層的跨平台一致渲染引擎,那從Renderer 層開始對接就好了,繞過了所有不想要的,也保留了所有想要的。

打破重重阻礙,Flutter 和 Web 生態如何對接? 7

要實現 Flutter for Web 也更簡單了一些。在Engine 層做對接,一直苦於瀏覽器透出的底層能力不夠,如果是在Renderer 之上做對接就更容易一些,基於JS/CSS/DOM/Canvas 的能力封裝出一套Rendering 接口,供Widget 調用就好了,這樣可以使渲染鏈路更短一些,但是依然要處理Widget 和DOM/CSS 之間的兼容性問題。

再討論一遍:為什麼要對接?

技術上已經分析完了,要想搞定 Flutter 生態和 Web 生態的對接,需要投入很大的成本,所以真正決定做之前,要先討論清楚為什麼要做對接?到底要不要做對接?

首先 Google 官方對 Flutter 的定位就是個問題。 Flutter 設計之初就是不考慮 Web 生態的,甚至在刻意迴避,倡導的是更貼近原生的開發方式。我之所以在開頭說不要對接,原因也很簡單:兩種技術設計理念不同,不是朝著一個方向發展的,生態不通,技術方案不通,強行融合很可能讓彼此都喪失了優勢。但是業界又有很多團隊在做這種嘗試,說明需求是存在的,如果 Google 抵制這個方向,那就不好做了。不過現在官方已經支持了 Flutter for Web,已經向 Web 生態邁了一步,未來是否進一步與 Web 融合,也是有可能的。

另外就是跨平台技術本身的問題,瀏覽器發展了二三十年,已經是個很強大的跨平台產品了,幾乎是 Web 的代名詞了,這一點無人能敵。但是也臃腫不堪,有大量歷史包袱,性能和體驗不夠好,和 Native 的結合度差,尤其在移動和 IoT 平台。雖然硬件性能在不斷提升,但這是所有軟件共享的,瀏覽器的性能和體驗總會比 Native 差一些,差的這一些很可能就是新業務和新場景的發揮空間。觀察一下近幾年新誕生的業務場景,很多都是利用到了Native 新提供的能力才火爆起來的,如AI/AR/視頻/直播等,有因為新的Web API 而孵化生出來的商業模式嗎?

本文轉載自公眾號淘系技術(ID:AlibabaMTT)。

原文鏈接

https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650405725&idx=1&sn=0b7476f7c7c01df7fdafda578f9ceb98&chksm=83953345b4e2ba53917ac30b709c07be15bd1c2fd5ae2a8ecfbb129b3813f771621b8fac95ca&scene=27#wechat_redirect