打破重重阻礙,F(xiàn)lutter 和 Web 生態(tài)如何對接?
阿里妹導(dǎo)讀:Flutter 設(shè)計之初是不考慮 Web 生態(tài)的,原因很簡單:兩種技術(shù)設(shè)計理念不同,強行融合很可能讓彼此都喪失了優(yōu)勢。但是業(yè)界又有很多團(tuán)隊在做這種嘗試,說明需求是存在的。今天,阿里無線開發(fā)專家門柳就來手把手教如何實現(xiàn) Flutter 和 Web 生態(tài)的對接?
先說結(jié)論:
不要對接!不要對接!不要對接!
開個玩笑,以上僅代表個人觀點,大家也知道這種“三體式警告”根本沒有用的,我自己也研究如何對接,說不定做完后就覺得“真香”了。
為什么要對接?
首先討論一下為什么要把 Flutter 對接到 Web 生態(tài)。
Flutter 現(xiàn)在是一個炙手可熱的跨平臺技術(shù),能夠一套代碼運行在 Android、iOS、PC、IoT 以及瀏覽器上,被認(rèn)為是下一代跨平臺技術(shù)。相比于 Weex 和 React Native 可以很好地解決多平臺一致性問題,原生渲染性能相近,上層沒有 JS 那么厚的封裝層次,整體性能會略好一些。
但是大部分興沖沖去學(xué) Flutter 的人疑惑的第一個問題就是:為什么 Flutter 要用 Dart?一個全新的語言意味著新的學(xué)習(xí)成本,難道 JS 不香嗎?JS 不香不是還有 TypeScript 嗎!事實上 Flutter 拋棄的豈止是 JS 這門語言,也拋棄了 HTML 和 CSS,設(shè)計了一套解耦得更好的 Widget 體系,F(xiàn)lutter 拋棄的是整個 Web,致力于打造一個新的生態(tài),但是這個生態(tài)無法復(fù)用 Web 生態(tài)的代碼和解決方案。尤其是之前所有跨平臺方案 Hybrid、React Native、Weex 都是對接 Web 生態(tài)的,這讓 Flutter 顯得有些格格不入,也讓大部分前端開發(fā)者望而卻步。
下面是我整理出來的,前端開發(fā)者使用 Flutter 的各方面成本:
因為 Flutter 的開發(fā)模式和前端框架比較像(可以說就是抄的 React),所以框架的學(xué)習(xí)成本并不高,稍微高一些的是 Dart 語言的學(xué)習(xí)成本,另外還要學(xué)習(xí)如何用 Widget 組裝 UI,雖然很多布局 Widget 設(shè)計得和 CSS 很像,靈活度還是差了很多。要想在真實項目中用起來,還要改造整個工具鏈,以“Native First”的視角做開發(fā),開發(fā) Flutter 和開發(fā)原生應(yīng)用的鏈路是比較像的,和開發(fā)前端頁面有較大差異。最高的還是生態(tài)成本,前端生態(tài)的積累無論是代碼還是技術(shù)方案都很難復(fù)用,這是最痛的一點,生態(tài)也是 Flutter 最弱的一環(huán)。
無論是為了先進(jìn)的技術(shù)理念還是出于商業(yè)私心,先不管 Flutter 為什么拋棄 Web 生態(tài),現(xiàn)實問題是最大的 UI 開發(fā)者群體是前端,最豐富的生態(tài)是 Web 生態(tài),我覺得 Web 技術(shù)也是開發(fā) UI 最高效的方式。如果能在上層使用 Web 技術(shù)棧開發(fā),在底層使用 Flutter 實現(xiàn)跨平臺渲染,不是可以很好的兼顧開發(fā)效率、性能和跨平臺一致性嗎?還能復(fù)用 Web 技術(shù)棧大量的技術(shù)積累。
可能這些理由也不夠充分,暫且先照著這個假設(shè)繼續(xù)分析,最后再重新討論到底該不該對接。
關(guān)于 Flutter 和 Web 生態(tài)的對接涉及兩個方面:
- 從 Web 到 Flutter。就是使用 Web 技術(shù)棧來開發(fā),然后對接到 Flutter 上實現(xiàn)跨平臺渲染。對 Web 來說是解決性能和跨平臺一致性問題,對 Flutter 來說是解決生態(tài)復(fù)用問題。
- 從 Flutter 到 Web。就是官方已經(jīng)實現(xiàn)的 Web support for Flutter,把已經(jīng)用 Dart 開發(fā)好的 App 編譯成 HTML/JS/CSS 然后運行在瀏覽器上,可以用于降級和外投場景。
如何實現(xiàn)“從 Web 到 Flutter”?
首先分析一下 Flutter 的架構(gòu)圖,看看可以從哪里下手。
Flutter 可以分為 Framework 和 Engine 兩部分,Engine 部分比較底層也比較穩(wěn)定了,最好不要動,需要改的是用 Dart 實現(xiàn)的 Framework。要想對接 Web 生態(tài)的話,JS 引擎肯定是要引入的,至于是否保留 Dart VM 有待討論。圖中最上面 Material 和 Cupertino 兩個 UI 庫前端是不需要的,前端有自己的。關(guān)鍵是 Widget 這部分,是替換成 HTML/CSS 的方式寫 UI,還是繼續(xù)保留 Widget 但是把語言換成 JS,不同方案給出的解法也不一樣。
有不少方案可以實現(xiàn)對接,業(yè)界有挺多嘗試的,我總結(jié)了下面三種方式:
- TS 魔改:用 JS 引擎替換掉 Dart VM,用 JS/TS 重新實現(xiàn) Flutter Framework(或者直接 dart2js 編譯過來)。
- JS 對接:引入 JS 引擎同時保留 Dart VM,用前端框架對接 Flutter Framework。
- C++ 魔改:用 JS 引擎替換掉 Dart VM,用 C++ 重新實現(xiàn) Flutter Framework。
TS 魔改
TS 魔改就是完全拋棄掉 Dart VM,用 TypeScript 重新實現(xiàn)一遍用 Dart 寫的 Flutter Framework。
為啥是 TS 而不是 JS?這不是因為 TS 是個大熱門嘛,而且向下兼容 JS,現(xiàn)在幾乎所有時髦的框架都要用 TS 重寫了。
這種方案的出發(fā)點是“如果能把 Flutter 的 Dart 換成 JS 就好了”,最容易想到的路就是把 Dart 翻譯成 TS,或者直接用 dart2js 把代碼編譯成 js,但是編譯出來的代碼包含很多 dart:ui 之類的庫的封裝,生成的包也挺大的,也比較難定制需要導(dǎo)出的接口,不如干脆用 TS 重寫一遍,工具鏈更熟悉一些,還可以加一些定制。
理論上講翻譯之后 Flutter 絕大部分功能都依然支持,可以復(fù)用各種 npm 包,還可以動態(tài)化,但是喪失了 AOT 能力,JS 語言的執(zhí)行性能應(yīng)該是不如 Dart 的。而且所有節(jié)點的布局運算都發(fā)生在 JS,底層只需要提供基礎(chǔ)的圖形能力就好了,就好像是基于 Canvas API 寫了一套 UI 框架,性能未必有現(xiàn)存前端框架的性能高。
此外最大的問題是如何與官方 Flutter 保持一致,假如現(xiàn)在是從 v1.13 版本翻譯過來的,以后官方升級到了 v1.15 要不要同步更新?這個過程沒啥技術(shù)含量,而且需要持續(xù)投入,做起來比較惡心。
另外還需要考慮上層是用 Widget 的方式寫 UI,還是用前端熟悉的 HTML+CSS。如果依然用 Widget 的話,那大部分前端組件還是用不了的,UI 還是得重寫一遍。反正要重寫的話,成本也沒降下來,那就用 Dart 重寫唄…… 直接用官方原版 Flutter 也避免每次更新都要翻譯一遍 Dart 代碼。所以既然選擇了對接前端生態(tài),那就要對接 CSS,不然就沒有足夠的價值。然而 CSS 和 Widget 的對接也是很繁瑣的過程,而且存在完備性問題。
JS 對接
翻譯代碼的方式不夠優(yōu)雅,那就保留 Dart,把 JS/CSS 對接到 Widget 上面不就好了?
當(dāng)然可以,這種方式是僅把 Flutter 當(dāng)做了底層的渲染引擎,上層保持前端框架的寫法,僅把渲染部分對接到 Flutter?,F(xiàn)存的很多前端框架都把底層渲染能力做了抽象,可以對接到不同渲染引擎上,如 Vue/Rax 同時支持瀏覽器和 Weex,用同樣的方式,可以再支持一個 Flutter。
這種方式對前端框架的兼容性比較好,但是鏈路太長了,業(yè)務(wù)代碼調(diào)用前端框架接口做渲染,一頓操作之后發(fā)出了渲染指令,這個渲染指令要基于通信的方式傳給 Flutter Framework,這中間涉及一次 JS 到 C++ 再到 Dart 的跨語言轉(zhuǎn)換,然后再接收到渲染指令之后還要轉(zhuǎn)成相應(yīng)的 Widget 樹,從 CSS 到 Widget 的轉(zhuǎn)換依然很繁瑣。而且 Widget 本身是可以帶有狀態(tài)的,本身就是響應(yīng)式更新的,在更新時會重新生成 widget 并 diff,如果在前端更新 UI 的話,前端框架在 js 里 diff 一次 vdom,傳到 Flutter 之后又 diff 一次 widget。
如果要繞過 Widget 直接對接圖中的 Rendering 這一層,可以繞過 widget diff 但是得改 Flutter Framework 的渲染鏈路,既然要改 Flutter Framework 那為什么不直接用 TS 魔改呢,還繞過了 JS 到 Dart 的通信,又回到了第一種方案。
總結(jié)來說,這個方案的優(yōu)點是:實現(xiàn)簡單、能最大化保留前端開發(fā)體驗,缺點是:渲染鏈路長、通信成本高、響應(yīng)式邏輯沖突、CSS 轉(zhuǎn) Widget 不完備等。
C++ 魔改
想要干掉 Dart VM,就需要用其他語言重新實現(xiàn)用 Dart 開發(fā)的 Framework,用 JS/TS 可以,用 C++ 當(dāng)然可以,最硬核的方式就是用 C++ 重新實現(xiàn) Flutter 的 Framework,然后接入 JS 引擎,通過 binding 把 C++ 接口透出到 JS 環(huán)境,上層應(yīng)用還是用 JS 做開發(fā)。
把 Framework 層下沉到 C++ 之后,不僅會有更好的性能,也能支持更多語言。原本 Flutter Framework 是在 Dart VM 之上的,必須依賴 Dart VM 才能運行,所以對 Dart 有強依賴;用 C++ 重新實現(xiàn)之后,JS 引擎是在 C++ 版 Framework 之上的,框架本身并不依賴 JS 引擎,還可以對接其他各種語言,如對接了 JVM 之后可以支持 Java 和 Kotlin,對接回 Dart VM 可以繼續(xù)支持 Dart。
這個方案可以增強性能,也能保持和 Flutter 的一致性,但是改造成本和維護(hù)成本都相當(dāng)高。C++ 的開發(fā)效率肯定不如 Dart,當(dāng) Flutter 快速迭代之后如何跟進(jìn)是很大的問題,如果跟進(jìn)不及時或者實現(xiàn)不一致那很可能就分化了。從 CSS 到 Widget 的轉(zhuǎn)換也是不得不面對的問題。
幾種方案對比
把上面幾種方案畫在同一張圖里是這個樣子的:
圖中實線部分表示了跨語言的通信,太過頻繁會影響性能,虛線部分表示了其他對接可能性。
從下到上,F(xiàn)lutter Engine 是不需要動的,這一層是跨平臺的關(guān)鍵。Framework 則有三種語言版本,JS/TS、Dart、C++,性能是 C++ 版本最好,成本是 Dart 版本最低。然后還需要向上處理 HTML/CSS 和 Widget 的問題,可以直接對接一個前端框架,也可以直接在 C++ 層實現(xiàn)(不然需要透出的 binding 接口就太多了,用通信的方式也太過頻繁了)。
如何實現(xiàn)“從 Flutter 到 Web”?
這個功能官方已經(jīng)實現(xiàn)了,可以把使用 Dart 開發(fā)的 App 編譯成 Web App 運行在瀏覽器上,官方文檔以介紹用法和 API 為主,我這里簡單分析一下內(nèi)部具體的實現(xiàn)方案。
實現(xiàn)原理
結(jié)合 Flutter 的架構(gòu)圖來看,要實現(xiàn) Web 到 Flutter 需要改造的是上層 Framework,要實現(xiàn) Flutter 到 Web 需要改造的則是底層 Engine。
Framework 對 Engine 的核心依賴是 dart:ui,這是庫是在 Engine 里實現(xiàn)的,抽象出了繪制 UI 圖層的接口,底層對接 skia 的實現(xiàn),向上透出 Dart 語言的接口。這樣來看,對接方式就比較簡單了:
- 使用 dart2js 把 Framework 編譯成 JS 代碼。
- 基于瀏覽器的 API 重新實現(xiàn) dart:ui,即 dart:web_ui。
把 Dart 編譯成 JS 沒什么問題,性能可能會有一點影響,功能都是可以完全保留的,關(guān)鍵是 dart:web_ui 的實現(xiàn)。在原生 Engine 中,dart:ui 依賴 skia 透出的 SkCanvas 實現(xiàn)繪制,這是一套很底層的圖形接口,只定義了畫線、畫多邊形、貼圖之類的底層能力,用瀏覽器接口實現(xiàn)這一套接口還是很有挑戰(zhàn)的。上圖可以看到 Web 版 Engine 是基于 DOM 和 Canvas 實現(xiàn)的,底層定義了 DomCanvas 和 BitmapCanvas 兩種圖形接口,會把傳來的 layer tree 渲染成瀏覽器的 Element tree,但是節(jié)點上僅包含了 position, transform, opacity 之類的樣式,只用到 CSS 很小的一個子集,一些更復(fù)雜的繪制直接用2D <canvas> 實現(xiàn)。
存在的問題
我編譯了一個還算復(fù)雜的 demo 試了一下,性能很不理想,滑動不流暢,有時候圖片還會閃動。生成出來的 js 代碼有 1.1MB (minify 之后,未 gzip),節(jié)點層次也比較深,我評估這個頁面用前端寫不會超過 300KB,節(jié)點數(shù)可以少一半以上。
另外再看一下 Flutter 倉庫的 issue,過濾出 platfrom-web 相關(guān)的,可以看到大量:文字編輯失效、找不到光標(biāo)、ListView 在 ios 上不可滾動、checkbox/button 行為不正常、安卓滾動卡頓圖片閃爍、字體失效、某些機型視頻無法播放、文字選中后無法復(fù)制、無法調(diào)試…… 感覺 flutter for web 已經(jīng)陷入泥潭,讓人回想起前端當(dāng)年處理各種瀏覽器兼容性的噩夢。
這些性能和兼容性問題,核心原因是瀏覽器未暴露足夠的底層能力,以及瀏覽器處理手勢、用戶輸入和方式和 Flutter 差異巨大。
實現(xiàn) Flutter Engine 需要的是底層的圖形接口和系統(tǒng)能力,雖然 <canvas> 提供了相似的圖形接口,如果全部用 canvas 實現(xiàn)的話很難處理可訪問性、文本選擇、手勢、表單等問題,也會存在很多兼容性問題。所以真實方案里用的是 Canvas + DOM 混合的方式,封裝層次太高了,渲染鏈路太長。就好像 Flutter Framework 里進(jìn)行了一頓猛如虎的操作之后,節(jié)點生成好了、布局算好了、繪制屬性也處理好了,就差一個畫布畫出來了,然后交到瀏覽器手里,又生成一遍 Element,再算一遍布局,在處理一遍繪制,最終才交給了底層的圖形庫畫出來。
再比如長頁面的滾動,瀏覽器里只要一條 CSS (overflow:scroll) 就可以讓元素可滾動,手勢的監(jiān)聽以及頁面的滾動以及滾動動畫都是瀏覽器原生實現(xiàn)的,不需要與 JS 交互,甚至不需要重新 layout 和 paint,只需要 compositing。如上圖所示,在 Flutter 中 Animation 和 Gesture 是用 Dart 實現(xiàn)的,編譯過來就是 JS 實現(xiàn)的,瀏覽器本身并不知道這個元素是否可滾,只是不斷派發(fā) touchmove 事件,JS 根據(jù)事件屬性計算節(jié)點偏移,然后運算動畫,然后把 transform 或者新的 position 作用到節(jié)點上,然后瀏覽器再來一遍完整的渲染流程……
優(yōu)化方案
性能和兼容性的問題還是要解決的,短期內(nèi)先把 issue 解掉,長線的優(yōu)化方案,官方有兩種嘗試:
使用 CSS Painting API 做繪制。
- 這是還處于提案狀態(tài)的新標(biāo)準(zhǔn),可以用 JS 實現(xiàn)一些繪制功能,自定義 CSS 屬性。
- 目前還未實現(xiàn),需要等瀏覽器先把 CSS Houdini 支持好。
使用 WebAssembly 版本的 Skia 做繪制。https://skia.org/user/modules/canvaskit
- 這樣可以發(fā)揮 wasm 的性能優(yōu)勢,并且保持 skia 功能的一致。但是目前 wasm 在瀏覽器環(huán)境里未必有性能優(yōu)勢,這里不展開討論了。
- 已經(jīng)部分實現(xiàn),參考這里的配置啟用功能: https://github.com/flutter/flutter/issues/41062#issuecomment-533952994
這兩個方案都是想更多的利用到瀏覽器的底層能力,只有瀏覽器暴露了更多底層能力,才能更好的實現(xiàn) Flutter 的 Web Engine。不過這個要等挺久的時間,我們也參與不了,現(xiàn)階段想要使用 flutter for web,還是得保持現(xiàn)有架構(gòu),一起參與進(jìn)去把 issue 解決掉,優(yōu)先保障功能,其次優(yōu)化性能。
一種適應(yīng)性更好的架構(gòu)
如果理想化一點,能不能從架構(gòu)角度讓 Flutter 和 Web 生態(tài)融合的更好一些呢?
回顧文章最開始的官方架構(gòu)圖,上面是 Framework(Dart),下面是 Engine(C++),切分在 Foundation 這一層,雙方之間的交互是幾何圖形信息。如果還保持這個架構(gòu),把切分層次劃分的更靠上一些,如下圖所示,劃分在 Widgets 和 Rendering 這一層,理論上講對 Flutter 的開發(fā)者來說是無感知的,因為上層的開發(fā)語言和 Widget 接口都是不變的。
切分在這一層,F(xiàn)ramework 和 Engine 之間的交互就不再是幾何圖形而是節(jié)點信息,Widget 的組合、setState 響應(yīng)式更新、Widget diff 都還在 Dart 中,展開后的 RenderObject 的布局、繪制、裁剪、動畫全都在 C++ 中,不僅有更好的性能,還可以與 Engine 有更好的結(jié)合。
或者說,還原本保留 Engine 的設(shè)計,把下沉的這部分邏輯上劃分成 Renderer,就有了如下三層的結(jié)構(gòu):
這樣劃分出來的每一層都有明確的定位:
- Framework: 開發(fā)框架。為開發(fā)者提供可編程 API,實現(xiàn)響應(yīng)式的開發(fā)模式,提供細(xì)粒度 Widget 供開發(fā)者自由封裝和組合。
- Renderer: 渲染引擎。專門實現(xiàn)布局、繪制、動畫、手勢的的處理,這部分功能相對獨立,是可以與開發(fā)框架解耦的,也不必與特定語言綁定。
- Engine: 圖形引擎。實現(xiàn)跨平臺一致的圖形接口,合成輸入的層并繪制到屏幕上,處理好平臺力的接入和適配。
這樣切分除了有性能優(yōu)勢以外,也使得渲染引擎擺脫了對 Dart 的依賴,能夠支持多種語言,也能支持多種開發(fā)模式。對接到 Dart VM 就可以用 Dart 寫代碼,對接到 JS 引擎就可以用 JS 寫代碼,對接到 JVM 還可以寫 Java,但是無論怎么寫,底層的渲染能力是一樣的,一套統(tǒng)一的布局算法,動畫和手勢的處理行為也是一致的。
在這樣的架構(gòu)下,對接 Web 生態(tài)就更容易了。Dart 和 Widget 是前端不想要的,希望能換成 JS 和 CSS,但是又想要底層的跨平臺一致渲染引擎,那從 Renderer 層開始對接就好了,繞過了所有不想要的,也保留了所有想要的。
要實現(xiàn) Flutter for Web 也更簡單了一些。在 Engine 層做對接,一直苦于瀏覽器透出的底層能力不夠,如果是在 Renderer 之上做對接就更容易一些,基于 JS/CSS/DOM/Canvas 的能力封裝出一套 Rendering 接口,供 Widget 調(diào)用就好了,這樣可以使渲染鏈路更短一些,但是依然要處理 Widget 和 DOM/CSS 之間的兼容性問題。
再討論一遍:為什么要對接?
技術(shù)上已經(jīng)分析完了,要想搞定 Flutter 生態(tài)和 Web 生態(tài)的對接,需要投入很大的成本,所以真正決定做之前,要先討論清楚為什么要做對接?到底要不要做對接?
首先 Google 官方對 Flutter 的定位就是個問題。Flutter 設(shè)計之初就是不考慮 Web 生態(tài)的,甚至在刻意回避,倡導(dǎo)的是更貼近原生的開發(fā)方式。我之所以在開頭說不要對接,原因也很簡單:兩種技術(shù)設(shè)計理念不同,不是朝著一個方向發(fā)展的,生態(tài)不通,技術(shù)方案不通,強行融合很可能讓彼此都喪失了優(yōu)勢。但是業(yè)界又有很多團(tuán)隊在做這種嘗試,說明需求是存在的,如果 Google 抵制這個方向,那就不好做了。不過現(xiàn)在官方已經(jīng)支持了 Flutter for Web,已經(jīng)向 Web 生態(tài)邁了一步,未來是否進(jìn)一步與 Web 融合,也是有可能的。
另外就是跨平臺技術(shù)本身的問題,瀏覽器發(fā)展了二三十年,已經(jīng)是個很強大的跨平臺產(chǎn)品了,幾乎是 Web 的代名詞了,這一點無人能敵。但是也臃腫不堪,有大量歷史包袱,性能和體驗不夠好,和 Native 的結(jié)合度差,尤其在移動和 IoT 平臺。雖然硬件性能在不斷提升,但這是所有軟件共享的,瀏覽器的性能和體驗總會比 Native 差一些,差的這一些很可能就是新業(yè)務(wù)和新場景的發(fā)揮空間。觀察一下近幾年新誕生的業(yè)務(wù)場景,很多都是利用到了 Native 新提供的能力才火爆起來的,如 AI/AR/視頻/直播 等,有因為新的 Web API 而孵化生出來的商業(yè)模式嗎?
關(guān)于這個話題,希望大家能發(fā)表一下自己的看法。