作者|張翰(門柳)
出品|阿里巴巴新零售淘系技術部
我在之前一篇文章《打破重重阻礙,Flutter 和 Web 生態如何對接?》提到了這麼一句話:“CSS 和 Widget 的對接也是很繁瑣的過程,而且存在完備性問題”。我直接給了結論,沒有給出原因。
現在填上這個坑,這篇文章專門對比 Flutter Widget 的佈局原理和 CSS 佈局原理的差異,分享在對接過程中會遇到的問題和解決方案,幫大家理一理思路,內容可以分為這幾部分:
- CSS 和 Widget 參賽選手介紹
- Let's Battle! 從五個角度正面硬剛
- Love & Peace 討論取長補短的可行性
- Happy Ending 最重要的環節
CSS
CSS 是 Cascading Style Sheets(層疊樣式表)的簡寫,是一種用來描述樣式的標記語言,最初的想法誕生自 1994 年,1996 年落成第一版規範(參考 20 Years of CSS)。HTML 描述了頁面的結構,CSS 描述頁面呈現出來的樣子,這對 CP 已經配合工作了二三十年,依然是佈局圈裡最高效的組合。
▐ CSS is Awesome
CSS 描述佈局很高效,這點無需質疑。後續出現的各種佈局方案,前端框架 CSS in JS、XML描述文件、Flutter 等等,即使沒有直接照搬 CSS 的功能,也深受 CSS 設計的影響。
CSS 很容易上手,經典的盒模型一學就會,文本相關的屬性看名字就知道怎麼回事,Flexbox 也是很好用的,但是 CSS 逐漸出現了一些很難駕馭的功能,多列布局就稍微難了一點,CSS Grid … 這代碼也太難寫了吧,我覺得這是給 AI 設計的,不是讓人手寫的,再加上 clip-path, filter, css houdini 等等,功能越來越強大,可以做濾鏡、畫皮卡丘、畫油畫、甚至還可以做遊戲。功能很強大,但是不實用,上述這些功能在生產環境基本上都是用 JS 實現的,沒時間去磨 CSS。
▐ 渲染布局流程
瀏覽器裡 CSS 的渲染過程,簡化一下可以總結為 加載、解析、查詢並作用到 DOM 節點、計算佈局 這四個過程。瀏覽器首先加載 HTML 文件,遇到
然後瀏覽器根據帶有 ComputedStyle 的 DOM 樹生成 LayoutTree,節點的 display 特性不同,生成的 LayoutObject 的類型也不同,然後佈局算法會多次遍歷這課樹,計算出每個節點的 Rect。這個過程是很複雜的,並不是一個 DOM 節點就對應一個 Layout 節點,要考慮 display:none、偽元素、文本節點、shadow dom 等等,而且整個過程是同步的,在解析出的 DOM 節點已經佈局完成後,如果瀏覽器又解析到一個
佈局完成之後是執行 Paint,把佈局信息一層一層的提交到 compositor 線程,然後再劃分成一塊一塊的交給 GPU 線程去繪製。我覺得最後這兩步是比較快的,在主線程裡的 Layout 和 Paint 才是最耗時間的,而且和 JS 代碼的執行攪在了一起。
Flutter Widget
和 CSS 不同,Flutter Widget 設計得很細緻,分類清晰功能明確,不像 CSS 那樣各種屬性耦合在一起,互相影響。Widget 的設計是比較原子化的,基本不會互相影響,為了保持佈局算法的高效,對 Widget 的嵌套方式有要求。
Flutter 設計得比較合理,有一個原因不容忽視,Flutter 在 Google 的開發團隊和 Chrome 有很大淵源,有些人已經參與制定 CSS 規範很多年,都是在這個領域裡很有經驗的人。整體上沒有 CSS 那麼多的冗餘和歷史包袱,新框架都可以吸取以前的教訓,站在巨人的肩膀上設計得越來越好用。換句話說,如果讓 CSS 的設計者們,拋開歷史包袱重新設計,不考慮向下兼容,很有可能就設計成了現在 Flutter Widget 的樣子。
▐ 渲染布局流程
關於 Flutter 的渲染流程,官方文檔Flutter 工作原理就是很好的學習資料,最大的亮點是次線性的佈局,有接近 O(n) 的性能,比 CSS 高效得多。
具體過程大家去官網學習吧,這裡就放一張圖,本文的重點是 Battle!
Let's Battle!
▐ Round 1: 背後的大佬
在真正開始比較之前,先看一下他們背後是什麼樣的組織在支撐和運營著他們。
CSS 背後是 W3C,這是業界認可的標準化組織,而且被各大瀏覽器實現,瀏覽器廠商也在積極的推到標準的發展。CSS 是個開放的技術,它背後的大佬是 W3C、Chrome、Safai、Firefox 等等一系列盈利或者非盈利組織,大家互惠互利共同發展。
但是 Flutter 背後是是隻有 Google,雖然也是開源的,但是設計與實現都是由 Google 的團隊來主導,其他人都是在使用,真正有能力有機會參與開發的很少,PR 都是小修小補。框架和標準的一個差別,就是會不會向下兼容,框架有可能明天宣佈推出 2.0,帶上牛逼的優化和 breaking change,不向下兼容,是司空見慣的事。
從這個角度看,CSS 雖然臃腫,但畢竟是個標準化的技術,生命力頑強。你現在寫的 CSS 代碼,五年之後依然可以運行,現在寫的 Flutter 代碼到五年後就不好說了。假如 Google 宣佈不維護 Flutter 了,很可能社區裡就瞬間喪失了信心,那 Flutter 就死了;假如 Chrome 宣佈不支持 CSS 了,CSS 依然活得好好的,Chrome 很可能會死(參考 IE),FireFox 要笑醒了。
▐ Round 2: 學習成本
要說學習成本,當然是 CSS 高效,先不討論原理,先從幾個側面的案例說明一下。
市面上能出現“零基礎,三個月成為前端高手!”的培訓班,也側面說明了前端學習成本低,其實他們的口號是錯的,三個月肯定學不會前端,能學會的只有用 CSS 切頁面。但是沒有培訓班開“三個月掌握Flutter”的課,一個熟練的前端開發者三個月學會是可能的,客戶端上手更快一點,沒有編程基礎的人學完肯定一臉懵逼的要求退錢了。
另外微信可以舉辦“青少年微信小程序編程創意營” ,面向中小學生,小程序的 UI 就是受限的 HTML + CSS 來寫的,但是 Flutter 要搞這種比賽的話,就得面向有熟練編程經驗的人了。
如果從語法角度考慮的話,CSS 容易學是因為它只是一種描述性語言,不含複雜的編程邏輯,設計目標就是用來描述“我想要的 UI 是什麼樣子”的,是面向結果的描述,而 Widget 是要通過寫 Dart 代碼來實現的,UI 和代碼邏輯寫一起,通過代碼一行行描述“我怎麼把 UI 組合出來”的,是面向過程的描述,所以 CSS 更直觀一些,寫出來的代碼更容易讓人理解。還有個小原因,就是像 Fluter Widget 這樣層層嵌套的代碼,寫起來和改起來都很麻煩,太依賴編輯器,複製粘貼不太方便(差不多的話,是可以抄一下代碼的嘛)。
另外 CSS 誕生了這麼多年,學習資料簡直多到爆!本身規範事無鉅細,還有 MDN 、CSS Tricks 、CodePen 等網站即授之以漁又授之於魚,各種線上培訓也把知識框架和學習路線都給你安排的明明白白的。相比之下,Flutter Widget 就只有官網,學習資料和社區生態都還差很多。
▐ Round 3: 開發效率
CSS 上手雖然簡單,但是很難掌握,沒有個幾年的開發經驗,沒被它虐過千百遍,是根本駕馭不住它的。
假如要畫出來 CSS 的學習曲線的話,初期肯定是快速上升,到了一定高度後就變慢了,甚至還迷之下降。但是 Flutter 剛開始學習時要了解很多概念,要轉變思維,上手稍微慢了一點,但是越學越快。說個不恰當的比喻,寫 CSS 可以看做是操作提線木偶,寫 Widget 就相當於是搭樂高積木。
因為 CSS 各種屬性之間是可以互相影響的,輸入和輸出不是簡單的對應關係,你以為你寫了 width: 100px 它的寬度就一定是 100px 了?就像是綁了幾百根線的木偶,讓你拉動其中的五根來做一個 OK 的手勢,你牽了其中一條線,動的可能不只是一個部位,而是整個上半身。
Flutter Widget 就更加原子化,而且對於誰可以嵌套誰是有要求的,就像是拼樂高積木,每塊積木都很小,但是有明確型號的卡口,要先符合它的設計,然後再發揮創意拼裝成各種造型。迫使你按照理想的方式去組合 Widget,避免寫出性能太差的代碼,CSS 就是任意屬性都可以和任意屬性在寫在一起,而且標準裡總能給出你一個合理的解釋,所以寫出來的代碼很混亂,也增大了佈局的難度。
所以 CSS 的開發效率初期較快,但是積累沉澱較難,大規模協作很難管理(耦合度高,全局作用域等),而 Flutter 的封裝性更好,有利於協作,開發效率會越來越快的。
▐ Round 4: 天下武功,唯快不破!
要問 Flutter 的 Widget 和 CSS 誰更快?大家的共識也是 Flutter 更快吧。我覺得 Flutter 和 CSS 佈局相比有兩大性能優勢:一個是次線性的佈局算法,一個是更合理的線程模型。
Widget 的佈局原理比 CSS 高效,這是犧牲了一部分靈活性換來的,以後擴展 Widget 時,都要遵循這些設計才能繼續保持高效。CSS 簡單的屬性背後可以深挖出特別複雜的細節,使得佈局模型越來越複雜,渲染管線越來越長,光 display 就有十幾二十個值,每一條都對佈局影響巨大,方便了開發者,但是給佈局性能帶來很大挑戰。
關於線程模型,也是瀏覽器一直被詬病的性能瓶頸,主線程太忙了,JS 的執行、HTML/CSS 的解析、DOM 的構建,佈局的運算,全都在主線程。相比之下 Flutter 劃分的四個線程就比較均衡,GPU 線程做的工作和瀏覽器差不多,但是宿主平臺(Android/iOS)的代碼跑在 Platform 線程裡,Flutter Framework 主要運行在 UI 線程裡,另外還有 IO 線程實現網絡和圖片、字體等文件的加載。
▐ Round 5: 未來的發展
分享幾個關於 CSS 現狀的數據,W3C 官方定義的 CSS 樣式有 520 條,Chrome 平臺上統計的樣式有 703 條,包括了一些帶前綴的樣式,有大量樣式的使用率很低。支付寶小程序的同學總結過 Top 100 小程序裡用到的不同 CSS 樣式,有 184 條。
總結一下就是:W3C 標準裡定義了 520 條樣式,Chrome 還額外支持 180+ 條私貨,但是常用的樣式不超過 200 條。
CSS 有大量歷史包袱,我自己也感覺大部分樣式我都用不到,有些甚至是最佳實踐裡禁止使用的,學會 50 條 CSS 後就可以寫 80% 種情況的佈局了,對於難寫的樣式,我多加幾層標籤再寫點 JS 也能實現。但是這些樣式不能廢棄,必須繼續支持,新增一條好用的屬性時,要解釋清楚和現在所有屬性的適配情況。
CSS 是負重前行,註定要越變越複雜。Widget 則是輕裝上陣,而且解耦比較好,是可插拔可組合的,如果未來想廢棄一些 Widget,把這部分 Widget 從主包裡拆出來放到獨立插件裡就行了,想用的話自行引入。整個迭代過程受歷史包袱的影響很小。
Love & Peace: 可否實現對接?
Battle 已經結束,友誼第一比賽第二,不討論勝負,下面進入 Love & Peace 環節。
Flutter 的壓線性佈局讓人眼饞, CSS 的靈活度又深受大家喜愛,可不可以取其精華去其糟粕,讓開發者寫 CSS 但是底層用 Flutter 來渲染?有這種想法的不是一個人,所以有很多方案把前端框架或小程序對接到 Flutter 上,在實現的時候,就會遇到如何把 CSS 轉換為 Widget 的問題。
▐ 技術可行性
技術上當然是可行的,我在前一篇文章《打破重重阻礙,Flutter 和 Web 生態如何對接?》裡介紹了各種實現方案,自己也寫代碼做過對接(用的C++魔改方案),跑通了整個渲染鏈路。
我介紹一下我的實現方式,可以簡單分成三步:
1. 解析 CSS 語法
我寫了個精簡版的 CSSOM,是標準 CSSOM 的子集。用來實現樣式表和樣式屬性和增刪改查、樣式值的解析與計算、選擇器的匹配和查詢等功能。(功能是獨立的,有需要的話自取)
CSSOM 主要是為了處理 CSS 的上層語法,轉成一致的數據格式,為下一步的轉換做準備,有一部分 CSS 的語法是在這個過程實現的,例如 CSS 選擇器,包括偽類選擇器和選擇器關係等,還有 @media 和 @keyframes 等功能,也可以實現 CSS variable 以及 calc()。無論上層是直接寫 CSS 還是 CSS in JS,都保證下一步轉換時輸入的數據格式一致,可以簡化後續的實現。
2. 實現 CSS 屬性和 Widget 數據格式的映射
這部分要把 CSS 的基礎數據格式轉成構建 Flutter Widget 所依賴的數據格式。例如 CSS 的 color 屬性會轉成 Flutter 的 Color 類,margin 和 padding 會轉成 EdgeInsets 類,flex-direction 將被轉成 Axis 枚舉,把文本相關的屬性轉換成 TextStyle 類。
這部分也是做原子性的轉換,技術上看起來比較簡單,只是轉換數據格式而已,但是需要對 CSS 和 Widget 的設計都比較瞭解,知道同一個概念在雙方語義中的對應關係,得搞清楚裡邊的技術細節。這個對應關係如果轉換錯了,後續的佈局怎麼調都調不對(過來人的經驗…)
3. 構建 Widget 樹
拿到 CSSOM 傳來的數據,掌握了 CSS 和 Widget 數據結構的語義轉換,然後在結合 HTML 定義的結構,就可以生成真正的 Widget 樹了。
構建 Widget 樹就不能只考慮 CSS 了, HTML + CSS 才和 Widget 對等。這裡還要處理不同類型 HTML 節點和 Widegt 的對應關係,節點上帶的佈局樣式不同,生成的節點也不同,Widget 的層次深度比 HTML 的要深。例如普通的 div 標籤,如果僅包含普通盒模型樣式,就轉換成 Container;如果包含了 flex 相關的屬性,根據具體配置的不同,轉成 Flex/Center/Row/Column 等;如果包含了絕對定位就要轉成 Positioned/Stack。這個過程也是很繁瑣,包含了大量細節,需要理解默認 HTML 標籤的語義、CSS 的層模型以及 BFC 等等,還需要理解 Flutter Widget 之間的嵌套限制,組合成 CSS 想要的效果。
▐ 使用限制
對於開頭提到的問題,前面的技術可行性分析迴應了「繁瑣」,下面討論一下「完備性」。
CSS 是靈活的,Widget 是受限的,把一個靈活的語法轉換成受限的實現,註定是不完備的。
在 CSS 裡,任意屬性可以和任意屬性寫在一起,W3C 標準裡總有一個明確的解釋方式,HTML 和 CSS 是沒有錯誤的,只有不符合預期。但是在 Flutter 裡,某個 Widget 裡可以放哪些 Widget 是明確的限制的,例如 Positioned 外層必須有個 Stack、Center 只能有一個子 Widget,不符合預期的嵌套是會報錯的,寫出的代碼不會出現匪夷所思的混用,所以佈局算法可以很快。從這個角度講,CSS 是 O(n!) 的複雜度,而 Widget 是多項式複雜度,用 Widget 去實現 CSS 註定是不完備的。(這難道是個 P 和 NP 問題……?)
在限制 CSS 寫法的情況下,能不能對接到 Widget 的實現?這個是可以的。
想要 Widget 次線性佈局的性能,就必須犧牲 CSS 的一部分靈活性。想要用技術突破這個限制?那就要改這套次線性的佈局算法了,改完之後就不再是 Flutter 了,性能優勢也沒有了。
以我的實踐經驗來看,Widget 只能支持一定範圍內的 CSS 樣式。它對 CSS 的使用限制不在於樣式條數,並不是說某個樣式實現不了,而是在於樣式的混用,即使支持了 500 條 CSS,但是某些屬性依然不能同時使用,外層用了樣式 A 內層再用樣式 B 就是無效的,C 和 D 寫一起就只有 C 有效。我評估 Widget 對 CSS 支持的範圍上限會比現在的 ReactNative/Weex 還要大一些,能夠滿足大部分業務和小程序的需求,然而業務需求是會增長的,達到支持範圍上限以後就很難再擴大了,就需要教育開發者了,對開發體驗有影響。
We are hiring
好了,PK 完畢,下面是大家最喜聞樂見的招聘環節。
歡迎大家加入淘系技術部基礎平臺部的小程序與跨平臺技術團隊!是支撐淘系小程序、小遊戲、Flutter 等跨平臺技術的核心團隊,有技術廣度和也有技術深度,我們需要 iOS、Android、C++、Flutter、Canvas、WebAssembly、WebGL 等各方面的人才。如果你善於學習,這是一個很好的接觸跨領域知識的機會!歡迎對技術有追求的同學加入!
簡歷請發送至郵箱:[email protected]