簡介: 面對多種多樣的跨端訴求,有哪些跨端方案?跨端的本質是什麼?作為業務技術開發者,應該怎麼做?本文分享阿里巴巴ICBU技術部在跨端開發上的一些思考,介紹了當前主流的跨端方案,以及跨端開發的經驗心得。
跨端Write once, run everywhere。
我們都聽說過這句經典的宣傳用語,後來我們都知道,沒有什麼東西是可以真正 run everywhere 的,充其量也只能做到 debug everywhere。
而當我們談論一次編寫多端執行時,顯然不可能真的指跨一切所有端,大多數情況下你不會需要在電腦和手環上同步開發一個功能。
跨 PC 和無線端。跨多 Native 平臺:例如跨 Android 和 iOS,甚至跨 Windows。跨投放 APP:隨著超級 APP 越來越多,很多業務需要在多個 APP 中投放同一個頁面。跨 Web 和 APP:Web 在很多情況下仍然是不可避免的,我們的頁面可能需要分享、SEO 或者投放到 M 站上等等,這時候就需要同時能在 Web 和 APP 內執行。跨 Web、多小程式、QuickApp 等:其實本來類似跨 APP,但是奈何小程式本身是各家控制的封閉生態,故而有了開發一次適配到多種封閉生態的訴求。其他端的跨端訴求:例如跨 POS 機,手錶等。與我們多種多樣的跨端訴求相對應的,是百花齊放的跨端方案。
百花齊放的跨端方案以 Web 為基礎的 H5 Hybrid 方案這類方案最為直接,簡單來說就是用網頁來跨端。由於我們絕大多數端上(甚至包括封閉的小程式生態)都支援 Webview,所以只要開發網頁然後投放到多個端即可,在桌面端對應的方案就是 Electron。
為什麼不直接全用 Web?
從開發成本低、標準統一、生態繁榮上來說,H5 Hybrid 方案基本是不二之選。然而這種方案難以避免在效能和體驗上存在差距。Web 的生態繁榮來自於其良好的歷史相容性,也意味著沉重的歷史包袱。
W3C 標準作為開放技術標準,歷史包袱多,邏輯複雜。Web 標準在設計上不是 Design for Performance 的,導致很多地方難以進一步改善,例如 JS 執行和 Layout、渲染互斥無法並行,導致過長的 JS 執行任務會執行正常的渲染導致卡頓。Web 的標準化在推進上也比較慢,新的能力可能要比較長的時間才能使用。React-Native/Weex 類方案在移動平臺上尤其是早期 WebView 的效能體驗非常糟糕,前面我們也提到這種差距主要來自於 Web 生態本身沉重的歷史負擔。
而 React-Native/Weex 這類方案透過儘可能的取長補短,透過結合 Web 的生態和 Native 的元件,讓 JS 執行程式碼後用 Native 的元件進行渲染。由於拋棄了 Web 的歷史包袱,這類方案可以做一些大刀闊斧的改動。
例如 RN 就如下圖中,把 JS 執行、佈局(Yoga)和渲染(Native 元件)放在三個程序分開執行,避免了 JS 執行復雜任務時介面卡頓。透過拋棄 CSS 中的大量標準,只支援部分 flex 佈局能力來減少佈局和渲染的複雜度。
這種方案同樣存在一些缺陷:
iOS/Android 雙端本身不一致的元件和佈局機制,讓雙端一致性難以得到保障。依賴於 Native 機制也讓一些 CSS 屬性實現起來比較困難,例如老大難的 z-index 問題。而最麻煩的一點在於,這套方案意味著非常高的維護支援成本。
借用了 Web 的生態但並不完全是 Web 生態,很多地方不一致,最常見的吐槽就是慣用的 CSS 佈局方式無法使用。相比於瀏覽器新增一個感測器 API 都要配套完善的 devtool,這類方案大部分情況下的開發體驗保障可以說是刀耕火種(下圖為 Chrome 的方向感測器 API 的 devtool)。在 WebView 效能差距逐漸縮小的今天,維護這一套複雜方案的 ROI 是否值得,需要根據我們場景的具體訴求考量。
FlutterFlutter 要解決的問題和上面的方案不同,完全不打算繼續在 Web 生態上借力,從設計之初也並沒有把 Web 生態考慮進去。相比於 RN 依賴 Native View 渲染,Flutter 則是自繪的元件,直接透過 Skia 繪製到螢幕上。
由於可以完全發揮 GPU 的能力,也不需要去 Native 繞一圈。Flutter 理論上能做到更好的效能和兩端一致性,這一意味著理論上未來可能基於 Flutter 的 JS 動態化方案能夠在樣式上支援的比 WEEX 更好。
從前端的視角看仍然更像是一個 Native 開發方案而非跨端方案(雖然其實是跨 Android/iOS 的)。目前最主要的問題是 Flutter for Web 從技術原理上來說離生產可用可能還非常遙遠。除此之外動態化能力的確實也會讓部分場景不適用。
研發框架 for 小程式小程式是被創造出來的問題,各家小程式出於商業上的考量主動在 Web 生態的基礎上構造了相對封閉的生態。導致和 Web 生態格格不入。然而有多端小程式投放,或者同時投放小程式和 Web 端的場景難以接受使用。
由於小程式的端封閉且不受控,要解決小程式的跨端問題往往只能從研發框架層面出發。
編譯時方案比較知名的編譯時方案是 Taro,大致的原理可以解釋為將 JSX 編譯到小程式的 WXML/WXSS/JS 上,而這類框架的實現原理其實並非真的是一個 React 或者類 React 框架,而是把看起來像是 JSX 的模板透過靜態編譯的方式翻譯成小程式自身的模板。
這樣做的限制非常明顯,那就是 JSX 是 JavaScript 的拓展語言(React Blog 寫的是 is a syntax extension to JavaScript),而小程式所採用的 WXML 卻是一個表達能力非常受限的模板語言,我們不可能完成從一個通用程式語言到模板語言的編譯。
而靜態編譯類框架為了做到這一點,採取的方式就是限制開發者的寫法,這也是為什麼 taro 對 JSX 的寫法做出了諸多限制。這一點直接導致了無窮盡的維護成本和嚴重受損的開發體驗,而後 taro/next 也轉向了執行時方案 + 靜態編譯最佳化的結合。
執行時方案不謙虛的說,針對小程式的執行時方案應該是最早我在寫下 remax 第一個 issue 時[1]提出的。
透過 React Reconciler(類似於 Rax Driver)我們可以讓執行在小程式容器中的 React 不去直接操作 DOM,而是把操作的資料透過 setData 傳遞給小程式的 View 層對映到最後的介面上。
雖然 Remax、Rax 執行時、Taro Next 等幾種方案不盡相同,但是思路大同小異,就是利用小程式模板一定程度上的動態化能力 + 類 React 框架的 VirtualDOM 來進行渲染。當然這種做法相對於小程式原生的渲染方式存在一定的效能損耗。
remax 的支付寶端效能測試
在部分場景下,這種損耗是值得的。這些執行時框架也都在陸續透過允許關掉編譯產生的模板中的不用的屬性、部分靜態編譯、虛擬列表等方式來改進效能。
當然了,最後內嵌 Webview 仍然是一個方案。
作為業務技術團隊,我們該做什麼上面介紹的都是針對某些具體的場景的一些解決方案,然而對於業務技術團隊來說,跨端的本質是提效。針對新的變化提出新的方案是一方面,更重要的如何讓這種提效真的長治久安,讓我們的提效不會變成從一個新方案跳到另外一個新方案。
讓我們重新看上面這張圖,可以確定的是,跨端的訴求和與之對應的方案仍然會處於頻繁的變化中,也不會出現一個解決所有跨端問題的方案。而其中相對不變的部分是值得我們為了長治久安必須要投入的。
WebView & H5 HybridWebView 可能是眾多容器中最為特殊的一個,雖然很難滿足部分場景對於效能和體驗的極致要求,但是會是最穩定、長期存在且得到支援的方案。
從開發效率和未來長期的維護演變來看,在能夠滿足效能體驗要求的前提下,Web 方案仍然是最優先應該考慮的。
同時,在 APP 的 WebView 容器上我們能做更多的工作,例如透過容器來提供一些端內的能力,結合 Native 能力實現的並行資料載入,頁面保活等等。
基礎建設無論採用何種跨端方案,在哪個容器中,效能、穩定性、效能都是繞不開的三駕馬車。
效能對於不同的方案往往存在不同的效能方案,上面我們也提到在小程式的執行時方案中就會有減少編譯模板產出的欄位這樣的最佳化。然而,其實除了這種特定方案的最佳化外,大部分最佳化手段是大同小異的:離線快取、資料預取、快照、SSR、NSR 等等方案。
對於不同的端和容器,對於效能問題的度量和發現也應該是一致的,我們需要對頁面在不同端的效能究竟如何有明確的感知和橫向對比。
效能的端側建設(端能力、具體到某一個端的效能測算方案、效能打點等)可能需要根據不同的端、不同的跨端方案而不同。但效能的基礎建設(首屏標準、資料分析、基礎最佳化能力)在跨端中應該是相對穩定的。
在端側能力方面,ICBU 早期在 WEEX 效能最佳化時引入了並行載入的能力,透過 wh_prefetch 協議來使用容器的並行載入能力。而後在新的容器(WebView、瀏覽器)中,雖然底層能力存在差異,但仍然識別相同的協議。
在資料採集和分析方面,我們透過統一跨端基礎庫,讓不同端不同技術方案可以在同樣的標準下分析、度量和對比。
穩定性建設在無線端我們常常把效能 & 穩定性並稱為 “高可用”,穩定性主要涵蓋的範圍包括灰度能力、業務監控、報警、錯誤監控、白屏檢測等等。
這些能力相比起來對具體端和跨端方案的依賴更少,除了在端側的資料採集邏輯稍有區別外其他建設部分相對也是比較穩定的。集團針對這些場景也存在一些跨端可用的方案、例如 iTrace 等。
工程基建對於不同場景的跨端,雖然在方案上存在一定的差異,但是我們的工程基建是可以保持統一
容器層提供統一的 API 和文件能力統一的研發流程研發工具提供統一的抓包、debug 能力等一致的工具庫等等這樣,當有新的容器或者方案出現時,我們只需要按照相應的能力進行對齊,就能讓我們上層的業務程式碼和開發體驗維持相對穩定的狀態。
業務邏輯跨端相對來說,我們會發現在多種跨端方案的演化中,如何渲染、如何佈局等 UI 層面的變化要遠遠大於業務邏輯層面。甚至是小程式和 Flutter,其大致的開發正規化都沒有發生太大的改變。例如 Flutter 開發正規化和 React 非常相似,同樣是宣告式 UI,同樣存在 VirtualDOM。
考慮到 SEO 和效能等問題和 Flutter 本身基於 Skia 的渲染模式,Flutter for Web 在相當長一段時間內可能都不會是一個生產環境可用的方案。
而在統一了業務邏輯程式碼的組織方式後,我們可以透過 Hooks for ALL 的方案讓 Flutter 和 Web 端可以共享一份基於 Hooks 的業務邏輯程式碼。
有時候你不需要真的 run everywhere,能夠提高效能、保持一致就已經達到目的了。
檢視層目前來看檢視層的跨端仍然充滿了變數,在我們的業務邏輯層跨端做的足夠原子化後,也許我們部分互動邏輯不是特別重的檢視層能夠透過 DX + 繫結原子化邏輯 + 資料引數的方式覆蓋更多的跨端場景。從而同時滿足效能、效能方面的訴求。
然而對於通用場景的檢視跨端,仍然沒有銀彈。
總結總體來說,跨端處於且將長期處於多方案並存且不斷變化的狀態。除了針對新的變化新的場景選擇或創造合適的方案,我們更要做好這種動態變化中長治久安的部分: