隨著各大平臺小程序的快速放量,開發(fā)者遇到越來越多的平臺適配問題。各平臺小程序的性能優(yōu)化方法也各不相同,我們該如何應(yīng)對?DCloud CTO 崔紅保在 GMTC 深圳 2019(全球大前端技術(shù)大會)分享了《小程序的未來方向》,介紹了小程序技術(shù)架構(gòu)、性能卡點以及各平臺優(yōu)化方案,對于小程序未來的技術(shù)更迭,提出了小程序在未來可能的發(fā)展方向。本文根據(jù)演講內(nèi)容整理而成。
簡單介紹一下我自己,中年碼農(nóng),跨平臺開發(fā)領(lǐng)域的老兵。在那個翻蓋摩托羅拉手機代表著先進和時髦的年代,我就開始參與 “window mobile/j2me/symbain” 等系統(tǒng)的跨平臺研發(fā)管理工作,可能很多同學(xué)都沒見過那些手機。到后來的移動互聯(lián)網(wǎng)時代及當(dāng)下的小程序時代,我也一直在深度參與其中,持續(xù)輸出“Hybrid App”引擎、前端 UI 庫(mui)及小程序跨端開發(fā)框架(uni-app)。目前在 DCloud 任職 CTO,同時兼 “uni-app” 產(chǎn)品負責(zé)人。
羅馬不是一天建成的,小程序也不是一天發(fā)明的。小程序這種介于 H5 和 Native App 之間的特殊應(yīng)用形態(tài),從探索到成熟,經(jīng)歷了哪些過程?我們首先帶大家回顧梳理一下。然后,從現(xiàn)有技術(shù)架構(gòu)出發(fā),分析小程序當(dāng)下幾個主要性能坑點。各家小程序引擎為解決這些坑點,做了哪些完善工作。比如,大家知道小程序是以 Web 渲染為主、原生渲染為輔,那引入原生渲染后,引發(fā)了哪些新的問題?為解決這些問題,微信提出了同層渲染的方案,同層渲染在技術(shù)層面上又是如何實現(xiàn)的?最后從當(dāng)前已知問題出發(fā),對于小程序未來的技術(shù)更迭,拋出一些我們認為的可能方向,供大家參考。
一、小程序歷史
HTML5 于 2007 年在 W3C 立項,與 iPhone 發(fā)布同年。
喬布斯曾期待 HTML5 能幫助 iPhone 打造起應(yīng)用生態(tài)系統(tǒng)。但 HTML5 的發(fā)展速度并不如預(yù)期,雖然它成功地打破了 IE Flash 壟斷的局面,卻沒有達到承載優(yōu)秀的移動互聯(lián)網(wǎng)體驗的地步。
蘋果公司在 iPhone 站穩(wěn)腳跟后,緊接著發(fā)布了自己的 App Store,開啟了移動互聯(lián)網(wǎng)的原生應(yīng)用時代。
大家知道現(xiàn)在手機端主要是 iOS、Android 兩大系統(tǒng),實際上在早期有 3 大系統(tǒng)競爭,還有一個就是諾基亞的 MeeGo 系統(tǒng),MeeGo 采用 C HTML5 的雙模應(yīng)用生態(tài)策略。然而,C 的開發(fā)難度太大,HTML5 體驗又不行,所以后來 MeeGo 就掉隊了;與之對應(yīng),Android 依靠 Java 技術(shù)生態(tài),在競爭中脫穎而出。
于是在移動互聯(lián)網(wǎng)初期,應(yīng)用生態(tài)被定了基調(diào) —— 原生開發(fā)。
國內(nèi)有一批做瀏覽器的廠商,嘗試去改進 HTML5。比如,百度在 2013 年的百度世界大會上發(fā)布了輕應(yīng)用,通過給 WebView 擴展原生能力,補充 JS API,讓 HTML5 應(yīng)用可以實現(xiàn)更多功能。
這類業(yè)務(wù)發(fā)展的頂峰,是微信在 2015 年初發(fā)布的微信 JS SDK,作為國內(nèi)事實上最大的手機瀏覽器,微信為它的瀏覽器內(nèi)核擴充了大量 JS API,讓開發(fā)者可以用 JS 調(diào)用微信支付、掃碼等眾多 HTML5 做不到的功能。
不過這類業(yè)務(wù)沒有取得成功,HTML5 的問題不止是功能不足,性能體驗是更嚴重的問題。而體驗問題,不是簡單地擴展 JS 能力能搞定的。
與瀏覽器不同,Hybrid 應(yīng)用是另一個細分領(lǐng)域,開發(fā)者使用 JS 編寫應(yīng)用,為了讓 JS 應(yīng)用更接近原生應(yīng)用的功能體驗,這個行業(yè)的從業(yè)者做出了很多嘗試。我們 DCloud 公司是業(yè)內(nèi)主流 Hybrid App 引擎提供方之一,我們提出了改進 HTML5 的“性能功能”障礙的解決方案 —— 通過工具、引擎優(yōu)化、開發(fā)模式調(diào)整,讓開發(fā)者可以通過 JS 寫出更接近原生 App 體驗的應(yīng)用。
多 WebView 模式,原生接管轉(zhuǎn)場動畫、下拉刷新、Tab 分頁,預(yù)載 WebView……各種優(yōu)化技術(shù)不停迭代,終于讓 Hybrid 應(yīng)用取得了性能體驗的突破。
Hybrid 應(yīng)用和輕應(yīng)用、微信 JS SDK 等基于瀏覽器增加方案相比,還有一個巨大的差別:一個是 Client/Server,一個是 Browser/Server。簡單來說,Hybrid 應(yīng)用是 JS 編寫的需要安裝的 App,而輕應(yīng)用是在線網(wǎng)頁。
C/S 的應(yīng)用在每次頁面加載時,僅需要聯(lián)網(wǎng)獲取 JSON 數(shù)據(jù);而 B/S 應(yīng)用除了 JSON 數(shù)據(jù)外,還需要每次從服務(wù)器加載頁面 DOM、樣式、邏輯代碼,所以 B/S 應(yīng)用的頁面加載很慢,體驗很差。
可是這樣的 C/S 應(yīng)用雖然體驗好,卻失去了 HTML5 的動態(tài)性,仍然需要安裝、更新,無法即點即用、直達二級頁面。
那么 C/S 應(yīng)用的動態(tài)性是否可以解決呢?對此, DCloud 率先提出了“流應(yīng)用”概念,把之前 Hybrid 應(yīng)用里的運行于客戶端的 JS 代碼,先打包發(fā)布到服務(wù)器,制定流式加載協(xié)議,手機端引擎動態(tài)下載這些 JS 代碼到本地,并且為了第一次加載速度更快,實現(xiàn)了應(yīng)用的邊下載邊運行。
就像流媒體的邊下邊播一樣,應(yīng)用也可以實現(xiàn)邊用邊下。
在這套方案的保障下,終于解決了之前的各種難題:讓 JS 應(yīng)用功能體驗達到原生,并且可即點即用、直達二級頁面。
接著就是微信小程序,最初的名字實際上是微信應(yīng)用號,之后改名為小程序,2016 年 9 月份內(nèi)測,2017 年 1 月正式發(fā)行,再之后阿里巴巴、手機廠商聯(lián)盟、百度、今日頭條,陸續(xù)推出了自己的小程序平臺,小程序時代滾滾而來。
2018 年 9 月,微信推出云開發(fā),這個功能我們認為是小程序發(fā)展歷史上的一個重要節(jié)點,它可以讓前端工程師從前到后將所有業(yè)務(wù)閉環(huán)實現(xiàn),減少前后端的溝通成本、人力成本、運維成本,屬于開發(fā)模式的重大升級。與之前的前端同學(xué)既可通過 JS/CSS 編寫前端 UI,又可通過 “Node.js” 寫后端業(yè)務(wù),這種所謂全棧開發(fā)模式相比,云開發(fā)有更好的優(yōu)勢,因為前端同學(xué)對于 DB 優(yōu)化、彈性擴容、攻擊防護、災(zāi)備處理等方面還是有經(jīng)驗欠缺的,但云開發(fā)將這些都封裝好了,真正做到僅專注業(yè)務(wù)實現(xiàn),其它都委托云廠商服務(wù)。
二、小程序架構(gòu)
這是一個比較通用的小程序架構(gòu),目前幾家小程序架構(gòu)設(shè)計大致都是這樣的(快應(yīng)用的區(qū)別是視圖層只有原生渲染)。
大家知道小程序是一個邏輯、視圖層分離的架構(gòu)。
邏輯層就是上圖左上角這塊,小程序中開發(fā)的所有頁面 JS 代碼,最后都會打包合并到邏輯層,邏輯層除了執(zhí)行開發(fā)者的業(yè)務(wù) JS 代碼外,還需處理小程序框架的內(nèi)置邏輯,比如 App 生命周期管理。
視圖層就是上圖右上角這塊,用戶可見的 UI 效果、可觸發(fā)的交互事件在視圖層完成,視圖層包含 Web 組件、原生組件兩種,也就是小程序是原生 Web 混合渲染的模式,這塊后面會詳細講。
邏輯層最后運行在 JS CORE 或 V8 環(huán)境中;JS CORE 既不是 DOM 環(huán)境,也不是 Node 環(huán)境,你是無法使用 JS 中的 DOM 或 BOM 對象的,你能調(diào)用的僅僅是 ECMAScript 標(biāo)準(zhǔn)規(guī)范中所給出的方法。
那如果你要發(fā)送網(wǎng)絡(luò)請求怎么辦?window.XMLHttpRequest 是無法使用的(當(dāng)然即使可以調(diào)用,在 iOS 的 WKWebView 中也存在更嚴格的跨域限制,會有問題)。這時候,網(wǎng)絡(luò)請求就需要通過原生的網(wǎng)絡(luò)模塊來發(fā)送,JS CORE 和原生之間呢,就需要這個 JS Bridge 來通訊。
三、架構(gòu)引發(fā)的性能坑點
小程序這種架構(gòu),最大的好處是新頁面加載可以并行,讓頁面加載更快,且不卡轉(zhuǎn)場動畫;但同時也引發(fā)了部分性能坑點,今天主要介紹 3 點:
1. 邏輯層 / 視圖層通訊阻塞
我們從“swipeaction”這個例子講起,需求是用戶在列表項上向左滑動,右側(cè)隱藏的菜單跟隨用戶手勢平滑移動。
若想在小程序架構(gòu)上實現(xiàn)流暢的跟手滑動,是很困難的,為什么?
回顧一下小程序架構(gòu),小程序的運行環(huán)境分為邏輯層和視圖層,分別由 2 個線程管理,小程序在視圖層與邏輯層兩個線程間提供了數(shù)據(jù)傳輸和事件系統(tǒng)。這樣的分離設(shè)計,帶來了顯而易見的好處:
環(huán)境隔離,既保證了安全性,同時也是一種性能提升的手段,邏輯和視圖分離,即使業(yè)務(wù)邏輯計算非常繁忙,也不會阻塞渲染和用戶在視圖層上的交互。
但同時也帶來了明顯的壞處:
視圖層(WebView)中不能運行 JS,而邏輯層 JS 又無法直接修改頁面 DOM,數(shù)據(jù)更新及事件系統(tǒng)只能靠線程間通訊,但跨線程通信的成本極高,特別是需要頻繁通信的場景。
基于這樣的架構(gòu)設(shè)計,我們回到“swipeaction”,分析一次 touchmove 的操作,小程序內(nèi)部的響應(yīng)過程:
(1)用戶拖動列表項,視圖層觸發(fā) touchmove 事件,經(jīng) Native 層中轉(zhuǎn)通知邏輯層(邏輯層、視圖層不是直接通訊的,需 Native 中轉(zhuǎn)),即下圖中的?、?兩步;
(2)邏輯層計算需移動的位置,然后再通過 setData 傳遞位置數(shù)據(jù)到視圖層,中間同樣會由微信客戶端(Native)做中轉(zhuǎn),即下圖中的?、?兩步。
實際上,用戶滑動過程中,touchmove 的回調(diào)觸發(fā)是非常頻繁的,每次回調(diào)都需要 4 個步驟的通訊過程,高頻率回調(diào)導(dǎo)致通訊成本大幅增加,極有可能導(dǎo)致頁面卡頓或抖動。為什么會卡頓,因為通訊太過頻繁,視圖層無法在 16ms 內(nèi)完成 UI 更新。
為解決這種通訊阻塞的問題,各家小程序都在逐步提供對應(yīng)的解決方案,比如微信的 WXS、支付寶的 SJS、百度的 Filter,但每家小程序支持情況不同,詳細見下表。
另外,微信的“關(guān)鍵幀動畫”、百度的“animation-view” Lottie 動畫,也是為減少頻繁通訊的一種變更方式。
其實,通訊阻塞是業(yè)界普遍存在的一個問題,不止小程序,“React Native”“Weex”等同樣存在通訊阻塞的問題。只不過“React Native”“Weex”的視圖層是原生渲染,而小程序是 Web 渲染。我們下面以“Weex”為例來說明。
大家知道,“Weex”底層使用的 JS-Native Bridge,這個 Bridge 使得 JS 和 Native 之間的通信會有固定的性能損耗。
繼續(xù)以上述“swipeaction”為例,要實現(xiàn)列表項菜單的跟手滑動,大致需經(jīng)如下流程:
(1)在 UI 視圖上綁定 touch 事件(或 pan 事件);
(2)當(dāng)手勢觸發(fā)時, Native UI 層將手勢事件通過 Bridge 傳遞給 JS 邏輯層 , 這產(chǎn)生了一次 Native UI 到 JS 邏輯的通信,即下圖中的?、?兩步 ;
(3)JS 邏輯在接收到事件后,根據(jù)手指移動的偏移量驅(qū)動界面變化,這又會產(chǎn)生一次 JS 到 Native UI 的通信,即下圖中的?、?兩步。
同樣,手勢回調(diào)事件觸發(fā)的頻率是非常高的,頻繁的的通信帶來的時間成本很可能導(dǎo)致界面無法在 16ms 中完成繪制,卡頓也就產(chǎn)生了。
“Weex”為解決通訊阻塞,提供了“BindingX”解決方案,這是一種稱之為“Expression Binding”的機制,簡要介紹一下:
(1)接收手勢事件的視圖,在移動過程中的偏移量以“x,y”兩個變量表示;
(2)期望改變(跟隨移動)的視圖,變化的屬性為“translateX”和“translateY”,對應(yīng)變化的偏移量以“f(x),f(y)”表達式表示;
(3)將”交互行為 " 以表達式的方式描述,并提前預(yù)置到 Native UI 層;
(4)交互觸發(fā)時,Native UI 根據(jù)其內(nèi)置的表達式解析引擎,去執(zhí)行表達式,并根據(jù)表達式執(zhí)行的結(jié)果驅(qū)動視圖變換,這個過程無需和 JS 邏輯通訊。
偽代碼 – 摘錄自 Weex 官網(wǎng)
復(fù)制代碼
{ anchor: foo_view.ref // ----> 這是 " 產(chǎn)生手勢的視圖 " 的引用 props: [ { element: foo_view.ref, // ----> 這是 " 期望改變的視圖 " 的引用 expression: f(x) = x, // ----> 這是具體的表達式 property: translateX // ----> 這是期望改變的屬性 }, { element: foo_view.ref, expression: f(y) = y, // ----> y 屬性 property: translateY } ]}
“React Native”同樣存在類似問題,為避免頻繁的通信,“React Native”生態(tài)也有對應(yīng)方案,比如“Animated”組件及 Lottie 動畫支持。以 “Animated”組件為例,為實現(xiàn)流暢的動畫效果,該組件采用了聲明式的 API,在 JS 端僅定義了輸入與輸出以及具體的 transform 行為,而真正的動畫是通過 Native Driver 在 Native 層執(zhí)行,這樣就避免了頻繁的通信。然而,聲明式的方式能夠定義的行為有限,無法勝任交互場景。
“uni-app”在 App 端同樣面臨通訊阻塞的問題,我們目前的方案是采用類似微信 WXS 的機制(內(nèi)部叫“renderjs”),但放開了 WXS 中無法獲取頁面 DOM 元素的限制,比如下圖中多個小球同時移動的 canvas 動畫,“uni-app”在 App 端的實現(xiàn)方案是:
(1)renderjs 中獲取 canvas 對象;
(2)基于 web 的 canvas 繪制動畫,而非原生 canvas 繪制。
Tips:大家需要注意,并不是所有場景都是原生性能更好,小程序架構(gòu)下,如上多球同時移動的動畫,原生 canvas 并不如在 WXS(renderjs)中直接調(diào)用 Web canvas
下表總結(jié)了跨端框架在通訊阻塞方面的解決方案:
2. 數(shù)據(jù) / 組件差量更新
小程序架構(gòu)存在通訊阻塞問題,廠商為解決這個問題,創(chuàng)造了“WXS”腳本語言及關(guān)鍵幀動畫等方式,但這些都是廠商維度的優(yōu)化方案。我們作為小程序開發(fā)者,在性能優(yōu)化方面,又能做哪些工作呢?
小程序開發(fā)性能優(yōu)化,核心就是“setData”的調(diào)用,你能做只有兩件事情:
- 盡量少調(diào)用“setData”;
- 每次調(diào)用“setData”,傳遞盡可能少的數(shù)據(jù)量,即數(shù)據(jù)差量更新。
(1)減少 setData 調(diào)用次數(shù)
假設(shè)我們有更改多個變量值的需求,示例如下:
change:function(){? ? this.setData({a:1});? ? ... // 其它業(yè)務(wù)邏輯? ? this.setData({b:2});? ? ... // 其它業(yè)務(wù)邏輯? ? this.setData({c:3});? ? ... // 其它業(yè)務(wù)邏輯? ? this.setData({d:4});}
如上,4 次調(diào)用“setData”,會引發(fā) 4 次邏輯層、視圖層數(shù)據(jù)通訊。這種場景,開發(fā)者需意識到“setData”有極高的調(diào)用代價,自己需手動調(diào)整代碼,合并數(shù)據(jù),減少數(shù)據(jù)通訊次數(shù)。
部分小程序三方框架已內(nèi)置數(shù)據(jù)合并的能力,比如“uni-app”在 Vue runtime 上進行了深度定制,開發(fā)者無需關(guān)注“setData”的調(diào)用代價,可放心編寫如下代碼:
change:function(){? ? this.a = 1;? ? ... // 其它業(yè)務(wù)邏輯? ? this.b = 2;? ? ... // 其它業(yè)務(wù)邏輯? ? this.c = 3;? ? ... // 其它業(yè)務(wù)邏輯? ? this.d = 4;}
如上 4 次賦值,uni-app 運行時會自動合并成“{“a”:1,“b”:2,“c”:3,“d”:4}”一條記錄,調(diào)用一次“setData”完成所有數(shù)據(jù)傳遞,大幅降低 setData 的調(diào)用頻次,結(jié)果如下圖:
減少“setData”調(diào)用次數(shù),還有個注意點:后臺頁面(用戶不可見的頁面)應(yīng)避免調(diào)用“setData”。
(2)數(shù)據(jù)差量更新
假設(shè)我們有一個 “列表頁 上拉加載” 的場景,初始化列表項為 “item1 ~ item4”,用戶上拉后要向列表追加 4 條新記錄 “item5 ~ item8”,小程序代碼如下:
page({? ? data:{? ? ? ? list:['item1','item2','item3','item4']? ? },? ? change:function(){? ? ? ? let newData = ['item5','item6','item7','item8'];? ? ? ? this.data.list.push(...newData); // 列表項新增記錄? ? ? ? this.setData({? ? ? ? ? ? list:this.data.list? ? ? ? })? ? }})
如上代碼,change 方法執(zhí)行時,會將 list 中的 “item1 ~ item8”8 個列表項通過“setData”全部傳輸過去,而實際上變化的數(shù)據(jù)只有“item5 ~ item8”。
開發(fā)者在這種場景下,應(yīng)通過差量計算,僅通過“setData”傳遞變化的數(shù)據(jù),如下是一個示例代碼:
page({? ? data:{? ? ? ? list:['item1','item2','item3','item4']? ? },? ? change:function(){? ? ? ? // 通過長度獲取下一次渲染的索引? ? ? ? let index = this.data.list.length;? ? ? ? let newData = ['item5','item6','item7','item8'];? ? ? ? let newDataObj = {};// 變化的數(shù)據(jù)? ? ? ? newData.forEach((item) => {? ? ? ? ? ? newDataObj['list[' (index ) ']'] = item;// 通過 list 下標(biāo)精確控制變更內(nèi)容? ? ? ? });? ? ? ? this.setData(newDataObj) // 設(shè)置差量數(shù)據(jù)? ? }})
每次都手動計算差量變更數(shù)據(jù)是繁瑣的,新手不理解小程序原理的話,也容易忽略這些性能點,給 App 埋下性能坑點。
此處,建議開發(fā)者選擇成熟的第三方小程序框架,這些框架已經(jīng)自動封裝差量數(shù)據(jù)計算,對開發(fā)者更友好。比如,“uni-app”借鑒了 “westore JSON Diff”庫,在調(diào)用 setData 之前,會先比對歷史數(shù)據(jù),精確高效計算出有變化的差量數(shù)據(jù),然后再調(diào)用 setData,僅傳輸變化的數(shù)據(jù),這樣可實現(xiàn)傳遞數(shù)據(jù)量的最小化,提升通訊性能。如下,是一個示例代碼:
export default{? ? data(){? ? ? ? return {? ? ? ? ? ? list:['item1','item2','item3','item4']? ? ? ? }? ? },? ? methods:{? ? ? ? change:function(){? ? ? ? ? ? let newData = ['item5','item6','item7','item8'];? ? ? ? ? ? this.list.push(...newData) // 直接賦值,框架會自動計算差量數(shù)據(jù)? ? ? ? }? ? }}
Tips:如上 change 方法執(zhí)行時,僅會將 list 中的 “item5 ~ item8”4 個新增列表項傳輸過去,實現(xiàn)了 setData 傳輸量的極簡化。
(3)組件差量更新
下圖是一個微博列表截圖:
假設(shè)當(dāng)前有 200 條微博,用戶對某條微博點贊,需實時變更其點贊數(shù)據(jù)(狀態(tài));在傳統(tǒng)模式下,一條微博的點贊狀態(tài)變更,會將整個頁面 (Page) 的數(shù)據(jù)全部通過 setData 傳遞過去,這個消耗是非常高的;而即使通過之前介紹,通過差量計算的方式獲取變更數(shù)據(jù),這個 Diff 遍歷范圍也很大,計算效率極低。
如何實現(xiàn)更高性能的微博點贊?這其實就是組件更新的典型場景。
合適的方式應(yīng)該是,將每條微博封裝成一個組件,用戶點贊后,僅在當(dāng)前組件范圍內(nèi)計算差量數(shù)據(jù)(可理解為 Diff 范圍縮小為原來的 1/200),這樣效率才是最高的。
提醒大家注意,并不是所有小程序三方框架都已實現(xiàn)自定義組件,只有在基于自定義組件模式封裝的框架中,性能才會大幅提升;如果三方框架是基于老的“template”模板封裝的組件開發(fā),則性能并不會有明顯改善,其 Diff 對比范圍依然是 Page 頁面級的。
3. 混合渲染
大家知道,小程序當(dāng)中有一類特殊的內(nèi)置組件——原生組件,這類組件有別于 WebView 渲染的內(nèi)置組件,他們是由原生客戶端渲染的。
小程序中的原生組件,從使用方式上來說,主要分為三類:
- 通過配置項創(chuàng)建的:選項卡、導(dǎo)航欄,還有下拉刷新;
- 通過組件名稱創(chuàng)建的,比如:camera、canvas、input、live-player、live-pusher、map、textarea、video;
- 通過 API 接口創(chuàng)建的,比如:showModal、showActionSheet 等。
除了上面提到的這些之外,其它基本都是 Web 渲染。所以說,小程序是混合渲染模式,Web 渲染為主,原生渲染為輔。
(1)為什么要引入混合渲染
接下來的問題,為什么要引入原生渲染?以及為什么僅針對這幾個組件提供了原生增強?其他組件為什么沒有做原生實現(xiàn)?
這就需要我們針對每個組件單獨進行分析思考,這里舉了幾個例子:
- tabs/navigationbar:避免切換頁面白屏,提升新窗口進入時的用戶體驗。雖然不使用原生的 tabbar 和導(dǎo)航欄,可以做出更靈活的界面,但在切換頁面那短短 300ms 內(nèi),想保證頁面不白屏,還是需要使用渲染更快的原生 tabbar 和導(dǎo)航欄;
- video:全屏后的滑動控制(聲音、進度、亮度等);
- map:更流暢的雙指縮放、位置拖動;
- input:Web 端的 input,鍵盤彈出時,只有“完成”按鈕,無法讓鍵盤顯示“發(fā)送”“下一個”這樣的按鍵。
提到“input”控件的原生化,可以稍微發(fā)散一下。
小程序中原生 input 控件的通用做法是,未獲取焦點時以 Web 控件顯示,但在獲取焦點時,繪制一個原生 input,蓋在 Web input 上方,此時,用戶看見的鍵盤即為原生 input 所對應(yīng)的鍵盤,原生彈出鍵盤是可自定義按鈕(如上圖中下一步、send 按鈕)。這種做法存在一個缺陷: Web 和原生,畢竟不同渲染引擎,在鍵盤彈出和關(guān)閉時,對應(yīng) input 的“placeholder”會閃爍。
在 Android 平臺,還有一種做法是基于 WebKit 改造,定制彈出鍵盤樣式;這種方案,在鍵盤彈出和關(guān)閉時,input 控件都是 Web 實現(xiàn)的,故不存在“placeholder”閃爍的問題。
(2)混合渲染引發(fā)的問題
原生組件雖然帶來了更豐富的特性及更好的性能,但同時也引入了一些新的問題,比如:
- 層級問題:原生永遠在最高層,無法通過“z-index”設(shè)置不同元素的層級,無法與 view、image 等內(nèi)置組件相互覆蓋,不支持在“picker-view”“scroll-view”“swiper”等組件中使用;
- 通訊問題:比如一個長列表中內(nèi)嵌視頻組件,頁面滾動時,需通知原生的視頻組件一起滾動,通訊阻塞,可能導(dǎo)致組件抖動或拖影;
- 字體問題:在 Android 手機上,調(diào)整系統(tǒng)主題字體,所有原生渲染的控件的字體都會變化,而 Web 渲染的字體則不會變化。如下圖,系統(tǒng) rom 字體為一款“你的名字”的三方字體,設(shè)置后,小程序頂部標(biāo)題字體變了,底部選項卡字體也變了,但小程序中間內(nèi)容區(qū)字體不變,這就是比較尷尬的一種情況,一個頁面,兩種字體。
當(dāng)然,并不是所有小程序都存在這種問題,部分小程序通過修改自帶的 WebView 內(nèi)核,實現(xiàn)了 WebView 也可以使用 rom 主題字體,比如微信、QQ、支付寶;其他小程序(百度、頭條),WebView 仍然無法渲染為 rom 主題字體。
(3) 混合渲染改進方案
既然混合渲染有這些問題,對應(yīng)就會有解決方案,目前已有的方案如下。
- 方案?:創(chuàng)造層級更高的組件
既然其它組件無法覆蓋到原生組件上,那就創(chuàng)造出一種新的組件,讓這個新組件可以覆蓋到 video 或 map 上?!癱over-view/cover-image”就是基于這種需求創(chuàng)造出來的新組件;其實它們也是原生組件,只不過層級略高,可以覆蓋在 map、video、canvas、camera 等原生組件上。
目前除了字節(jié)跳動外,其它幾家小程序均已支持“cover-view/cover-image”。
cover-view/cover-image 在一定程度上緩解了分層覆蓋的問題,但也有部分限制,比如嚴格的嵌套順序。
- 方案?:消除分層,同層渲染
既然分層有問題,那就消除分層,從 2 層變成 1 層,所有組件都在一個層中,“z-index”豈不就可生效了?
這個小目標(biāo)說起來簡單,具體實現(xiàn)還是很復(fù)雜的。
4. 同層渲染
拋開小程序當(dāng)前架構(gòu)實現(xiàn),解決混合渲染最直接的方案,應(yīng)該更換渲染引擎,全部基于原生渲染,video/map 和 image/view 均為原生控件,層級相同,層級遮蓋問題自然消失。這正是“uni-app”在 App 端的推薦方案。
當(dāng)前 Web 渲染為主、原生渲染為輔的主流小程序現(xiàn)狀,如何實現(xiàn)同層渲染?
基于我們的分析研究,這里簡單講解一下同層渲染實現(xiàn)的方案,和微信真實實現(xiàn)可能會有出入(目前僅微信一家實現(xiàn)了同層渲染)。
(1) iOS 平臺
小程序在 iOS 端使用 WKWebView 進行渲染,WKWebView 在內(nèi)部采用的是分層渲染,一般會將多個 DOM 節(jié)點,合并到一個層上進行渲染。因此,DOM 節(jié)點和層之間不存在一一對應(yīng)關(guān)系。但是,一旦將一個 DOM 節(jié)點的 CSS 屬性設(shè)置為 “overflow: scroll” 后,WKWebView 便會為其生成一個 WKChildScrollView,且 WebKit 內(nèi)核已經(jīng)處理了 WKChildScrollView 與其他 DOM 節(jié)點之間的層級關(guān)系,這時 DOM 節(jié)點就和層之間有一一對應(yīng)關(guān)系了。
小程序 iOS 端的同層渲染可基于 WKChildScrollView 實現(xiàn),主要流程如下:
- 創(chuàng)建一個 DOM 節(jié)點并設(shè)置其 CSS 屬性為 overflow: scroll;
- 通知原生層查找到該 DOM 節(jié)點對應(yīng)的原生 WKChildScrollView 組件;
- 將原生組件掛載到該 WKChildScrollView 節(jié)點上作為其子 View。
(2)Android 平臺
小程序在 Android 端采用 Chromium 作為 WebView 渲染層,和 iOS 的 WKWebView 不同,是統(tǒng)一渲染的,不會分層渲染。但 Chromium 支持 WebPlugin 機制,WebPlugin 是瀏覽器內(nèi)核的一個插件機制,可用來解析“< embed >”。Android 端的同層渲染可基于 “< embed >”加 Chromium 內(nèi)核擴展來實現(xiàn),大致流程如下:
- 原生層創(chuàng)建一個原生組件(如 video);
- WebView 創(chuàng)建一個 “< embed >”節(jié)點并指定其類型為 video;
- Chromium 內(nèi)核創(chuàng)建一個 WebPlugin 實例 * 并生成一個 RenderLayer;
- 原生層將原生組件的畫面繪制到 RenderLayer 所綁定的 SurfaceTexture 上;
- Chromium 渲染該 RenderLayer。
這個流程相當(dāng)于給 WebView 添加了一個外置插件,且“< embed >”節(jié)點是真正的 DOM 節(jié)點,可將更多的樣式作用于該節(jié)點上。
四、未來可能
如果要探討小程序接下來的技術(shù)升級方向,我們認為應(yīng)該在用戶體驗、開發(fā)效率兩個方向上努力。
1. 更優(yōu)秀的用戶體驗
先說用戶體驗的問題,主要也是兩個方面:
- 解決現(xiàn)有的性能坑點,比如前面分析的這幾項,通訊阻塞、分層限制等,這里不再贅述;
- 支持更多 App 的體驗,更自由靈活的配置,比如,高斯模糊。
如果你也想快速搭建的自己的小程序引擎,并更優(yōu)的解決如上體驗問題,該怎么辦?
uni-app 發(fā)行到 App 端,實際上就是一個完整的小程序引擎,DCloud 會在近期將這個引擎完整開源,歡迎大家基于 uni-app 小程序 SDK 快速打造自己的小程序平臺。
uni-app 小程序 SDK 具備如下幾個特征:
- 性能:支持 Native 渲染,擴展 WXS,更高的通訊性能;
- 開放性:更靈活的配置,支持更多 App 的體驗;
- 開源不受限:無需簽訂任何協(xié)議,拿走就用;
- 生態(tài)豐富:支持微信小程序自定義組件,支持所有“uni-app”插件,且其插件市場目前已有上千款成熟插件。
2. 開發(fā)效率
開發(fā)效率應(yīng)該從跨端、跨云兩個維度進行分析。
(1)跨端開發(fā)
目前的小程序都帶有明顯的廠家屬性,每個廠家各不相同。比如,阿里內(nèi)部有多套小程序(支付寶、淘寶、釘釘?shù)龋?,幸好阿里?nèi)部目前已基本統(tǒng)一。但騰訊體系下,微信和 QQ 小程序依然是兩隊人馬,兩套規(guī)范。
小程序之前是手機端的,2019 年 360 出了 PC 端小程序。
接下來,會不會還有其它廠家推出自己的小程序?會不會有新的端的出現(xiàn)?比如,面向電視的小程序、面向車載的小程序?
一切皆有可能。
逐水草而居是人類的本能,追求流量依然是互聯(lián)網(wǎng)的制勝法寶。當(dāng)前的小程序宿主,都是億級流量入口,且各家流量政策不同。比如,微信的流量雖然很大,但有各種限制;百度和頭條是支持廣告投放的,通過廣告投放,可以快速獲得大量較為精準(zhǔn)的用戶;百度小程序還有個 Web 化的功能,可以將 Web 的搜索流量,轉(zhuǎn)化成小程序的流量。
面對眾多小程序平臺及各自巨大的入口流量,開發(fā)者如何應(yīng)對?
等待 W3C 的小程序標(biāo)準(zhǔn)統(tǒng)一,短期不太現(xiàn)實。當(dāng)下,若想將業(yè)務(wù)快速觸達多家小程序,借助跨端框架應(yīng)該是唯一可行的方案。
(2)跨云開發(fā)
開發(fā)商借助“uni-app”或其它跨端框架,雖然已可以開發(fā)所有前端應(yīng)用。但仍然需要雇傭 PHP 或 Java 等后臺開發(fā)人員,既有后端人員成本,又有前 / 后端溝通成本。
騰訊、阿里、百度小程序雖陸續(xù)上線了云開發(fā),但它們均只支持自己的小程序,無法跨端,分散的服務(wù)器對開發(fā)商更不可取。
故我們認為跨廠商的 Serverless 是接下來的一個重點需求,開發(fā)者在一個云端存儲所有業(yè)務(wù)數(shù)據(jù)及后端邏輯,然后將前端小程序發(fā)行到各家小程序平臺,也就是“一云多端”模式。
五、小結(jié)
基于小程序的現(xiàn)狀,我們也許可以總結(jié)一下小程序技術(shù)上的可能方向:
- 其它小程序拉齊與微信的差距,讓開發(fā)者可以做出足夠高性能的應(yīng)用服務(wù);
- 所有小程序應(yīng)拉齊和 App 的體驗差距,雖然功能 API 方面仍有不足,但操作性能和交互體驗,不應(yīng)該弱于 App;
- 跨端框架 Serverless,讓開發(fā)者更輕松,讓企業(yè)更高效。
作者介紹:
崔紅保,DCloud CTO,Uni-App 團隊負責(zé)人,開發(fā)了 2 個 Github Star 上萬的流行項目。有 10 年以上研發(fā)管理經(jīng)驗,在跨平臺引擎、前端 UI、小程序性能優(yōu)化等方面有豐富的實踐經(jīng)驗。
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻,該文觀點僅代表作者本人。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實,本站將立刻刪除。