
作者|張翰(門柳)
出品|阿裡巴巴新零售淘系技術部
導讀:Flutter 設計之初是不考慮 Web 生态的,原因很簡單:兩種技術設計理念不同,強行融合很可能讓彼此都喪失了優勢。但是業界又有很多團隊在做這種嘗試,說明需求是存在的。今天,阿裡無線開發專家門柳就來手把手教如何實作 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 的開發模式和前端架構比較像(可以說就是抄的 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 可以分為 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 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。
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 解掉,長線的優化方案,官方有兩種嘗試:
使用 CSS Painting API 做繪制。
這是還處于提案狀态的新标準,可以用 JS 實作一些繪制功能,自定義 CSS 屬性。
目前還未實作,需要等浏覽器先把 CSS Houdini 支援好。
使用 WebAssembly 版本的 Skia 做繪制
https://skia.org/user/modules/canvaskit這樣可以發揮 wasm 的性能優勢,并且保持 skia 功能的一緻。但是目前 wasm 在浏覽器環境裡未必有性能優勢,這裡不展開讨論了。
已經部分實作,參考這裡的配置啟用功能:
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 接口都是不變的。
切分在這一層,Framework 和 Engine 之間的互動就不再是幾何圖形而是節點資訊,Widget 的組合、setState 響應式更新、Widget diff 都還在 Dart 中,展開後的 RenderObject 的布局、繪制、裁剪、動畫全都在 C++ 中,不僅有更好的性能,還可以與 Engine 有更好的結合。
或者說,還原本保留 Engine 的設計,把下沉的這部分邏輯上劃分成 Renderer,就有了如下三層的結構:
這樣劃分出來的每一層都有明确的定位:
- Framework: 開發架構。為開發者提供可程式設計 API,實作響應式的開發模式,提供細粒度 Widget 供開發者自由封裝群組合。
- Renderer: 渲染引擎。專門實作布局、繪制、動畫、手勢的的處理,這部分功能相對獨立,是可以與開發架構解耦的,也不必與特定語言綁定。
- Engine: 圖形引擎。實作跨平台一緻的圖形接口,合成輸入的層并繪制到螢幕上,處理好平台力的接入和适配。
這樣切分除了有性能優勢以外,也使得渲染引擎擺脫了對 Dart 的依賴,能夠支援多種語言,也能支援多種開發模式。對接到 Dart VM 就可以用 Dart 寫代碼,對接到 JS 引擎就可以用 JS 寫代碼,對接到 JVM 還可以寫 Java,但是無論怎麼寫,底層的渲染能力是一樣的,一套統一的布局算法,動畫和手勢的處理行為也是一緻的。
在這樣的架構下,對接 Web 生态就更容易了。Dart 和 Widget 是前端不想要的,希望能換成 JS 和 CSS,但是又想要底層的跨平台一緻渲染引擎,那從 Renderer 層開始對接就好了,繞過了所有不想要的,也保留了所有想要的。
要實作 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 而孵化生出來的商業模式嗎?
今日話題
對于 Flutter 和 Web 的融合你怎麼看?
歡迎留言區探讨!
We are hiring
歡迎大家加入淘系基礎平台的跨平台技術部!是支撐小程式、小遊戲、Flutter 等跨平台技術的核心團隊,有技術廣度和也有技術深度,我們需要 iOS、Android、C++、Flutter、Canvas、WebGL、WebAssembly 等各方面的人才。如果你善于學習,這是一個很好的接觸跨領域知識的機會!我本人就是一個 title 從 “前端” 變成 “無線” 的案例,歡迎對技術有追求的同學加入!
關注【淘系技術】,一個有内容,有溫度的微信公衆号!