用戶
 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

掃一掃,登錄網站

小程序社區 首頁 教程 實戰教程 查看內容

小程序「同層渲染」那些事(keng)?

Rolan 2020-10-10 10:48

本文主要討論「原生組件」存在的意義和風險、「同層渲染」實現的原理,以及分享在實際業務開發趟坑總結出的一些技巧。

小程序的內容大多是渲染在 WebView 上的,如果把 WebView 看成單獨的一層,那么由系統自帶的這些 「原生組件」 則位于另一個更高的層級。兩個層級是完全獨立的,因此無法簡單地通過使用 z-index 控制「原生組件」和非原生組件之間的相對層級。正如下左圖所示,非原生組件位于 WebView 層,而「原生組件」及 cover-viewcover-image 則位于另一個較高的層級。所以live-player組件把其他業務組件都覆蓋了,導致部分用戶只能看到“純凈模式”。

為了解決這個問題,得通過一定的技術手段把「原生組件」直接渲染到 WebView 層級上,正如上右圖所示,此時 「原生組件層」 已經不存在,「原生組件」 此時已被直接掛載到 WebView 節點上。正所謂,工欲善其事,必先利其器。你一定也想知道 「同層渲染」 背后究竟采用了什么技術。只有真正理解了「同層渲染」背后的機制,才能更高效地使用好這項能力。

本文后續的篇幅主要討論「原生組件」存在的意義和風險、「同層渲染」實現的原理,以及分享在實際業務開發趟坑總結出的一些技巧。

2. 「原生組件」存在的意義及風險

要想深刻理解「同層渲染」背后的機制,最好先了解一下「原生組件」的“前世今生”,這得從小程序的技術架構說起。

2.1 小程序技術架構

一般來說,渲染界面的技術有三種

  • 用純客戶端原生技術來渲染。
  • 用純 Web 技術來渲染。
  • 介于客戶端原生技術與 Web 技術之間的,互相結合各自特點的技術(下面統稱 Hybrid 技術)來渲染。

由于小程序的宿主是微信,所以不太可能用純客戶端原生技術來編寫小程序。如果這么做,那小程序代碼需要與微信代碼一起編包,跟隨微信發版本,這種方式跟開發節奏必然都是不對的。

小程序應該像 Web 技術那樣,有一份隨時可更新的資源包放在云端,通過下載到本地,動態執行后即可渲染出界面。但是,如果用純 Web 技術來渲染小程序,在一些有復雜交互的頁面上可能會面臨一些性能問題,這是因為在 Web 技術中,UI 渲染跟 JavaScript 的腳本執行都在一個單線程中執行,這就容易導致一些邏輯任務搶占 UI 渲染的資源。

最終,小程序采用類似于微信 JSSDK 這樣的 Hybrid 技術,即界面主要由成熟的 Web 技術渲染,輔之以大量的接口提供豐富的客戶端原生能力。同時,每個小程序頁面都是用不同的 WebView 去渲染,這樣可以提供更好的交互體驗,更貼近原生體驗,也避免了單個 WebView 的任務過于繁重。小程序的架構圖如下圖所示。

小程序的渲染層和邏輯層分別由兩個線程管理

  • 渲染層的界面使用 WebView 進行渲染,一個小程序存在多個界面,所以渲染層存在多個 WebView。
  • 邏輯層采用 JSCore 線程運行 JavaScript 腳本。

這兩個線程間的通信經由小程序 Native 側中轉,邏輯層發送網絡請求也經由 Native 側轉發。

如此設計的初衷是為了管控和安全,微信小程序阻止開發者使用一些瀏覽器提供的,諸如跳轉頁面、操作 DOM、動態執行腳本的開放性接口。將邏輯層與視圖層進行分離,視圖層和邏輯層之間只有數據的通信,可以防止開發者隨意操作界面,更好的保證了用戶數據安全。

三端的腳本執行環境以及用于渲染非原生組件的環境是各不相同的:

運行環境邏輯層渲染層
AndroidV8Chromium 定制內核
IOSJavaScriptCoreWKWebView
小程序開發者工具NWJSChrome WebView

小程序的視圖是在WebView里渲染的,那搭建視圖的方式自然就需要用到HTML語言。但是HTML語言標簽眾多,增加了理解成本,而且直接使用HTML語言,開發者可以利用標簽實現跳轉到其他在線網頁,也可以動畫執行JAVAScript,前面所提到的為解決管控與安全而建立的雙線程模型就成擺設了。

因此,小程序設計一套組件框架——Exparser?;谶@個框架,內置了一套組件,以涵蓋小程序的基礎功能,便于開發者快速搭建出任何界面。同時也提供了自定義組件的能力,開發者可以自行擴展更多的組件,以實現代碼復用。值得一提的是,內置組件有一部分較復雜組件是用客戶端原生渲染的,以提供更好的性能,這便是本文的主角——「原生組件」。

2.2 「原生組件」是一把雙刃劍

在內置組件中,有一些組件較為特殊,它們并不完全在 Exparser 的渲染體系下,而是由客戶端原生參與組件的渲染,這類組件稱為 “「原生組件」”,這也是小程序 Hybrid 技術的一個應用。比如小程序中的 camera、video、live-player、canvas、map、animation-view、textarea、input、cover-view、cover-image 這些都是「原生組件」。

<video src="{{pullUrl}}">video>
復制代碼

上述代碼展示的是一個視頻組件,這行代碼在渲染層開始運行時,會經歷以下幾個步驟:

  1. 組件被創建,包括組件屬性會依次賦值。
  2. 組件被插入到 DOM 樹里,瀏覽器內核會立即計算布局,此時可以讀取出組件相對頁面的位置(x, y 坐標)、寬高。
  3. 組件通知客戶端,客戶端在相同的位置上,根據寬高插入一塊原生區域,之后客戶端就在這塊區域渲染界面。
  4. 當位置或寬高發生變化時,組件會通知客戶端做相應的調整。

可以看出,「原生組件」在WebView這一層的渲染任務是很簡單,只需要渲染一個占位元素,之后客戶端在這塊占位元素之上疊了一層原生界面。因此,「原生組件」的層級會比所有在 WebView 層渲染的普通組件要高。

引入「原生組件」主要有 3 個好處:

  1. 擴展 Web 的能力。比如像輸入框組件 input, textarea 有更好地控制鍵盤的能力。
  2. 體驗更好,同時也減輕 WebView 的渲染工作。比如像地圖組件 map 這類較復雜的組件,其渲染工作不占用 WebView 線程,而交給更高效的客戶端原生處理。
  3. 繞過 setData、數據通信和重渲染流程,使渲染性能更好。比如像畫布組件 canvas 可直接用一套豐富的繪圖接口進行繪制。

然而,命運惠贈的禮物,早已在暗中標好了價格?!冈M件」并非十全十美。由于「原生組件」脫離在 webview 渲染流程外,因此在使用時有以下限制:

  1. 「原生組件」的層級是最高的,頁面中的其他組件無論設置 z-index 為多少,都無法蓋在「原生組件」上。后插入的「原生組件」可以覆蓋之前的「原生組件」。
  2. 部分 CSS 樣式無法應用于「原生組件」。比如無法對「原生組件」設置 CSS 動畫,無法定義「原生組件」為position: fixed,不能在父級節點使用 overflow: hidden 來裁剪「原生組件」的顯示區域。
  3. 「原生組件」無法在 scroll-view、swiper、picker-view、movable-view 中使用,因為如果開發者在可滾動的 DOM 區域,插入「原生組件」作為其子節點,由于「原生組件」是直接插入到 webview 外部的層級,與 DOM 之間沒有關聯,所以不會跟隨移動也不會被裁減。
  4. 「原生組件」在Android上,字體會渲染為rom的主題字體,而webview如果不經過單獨改造不會使用rom主題字體。

2.3 「原生組件」限制的局部解決方案

在小程序引入「同層渲染」之前,「原生組件」的層級總是最高,不受 z-index 屬性的控制,無法與 view、image 等內置組件相互覆蓋,cover-viewcover-image 組件的出現一定程度上緩解了覆蓋的問題,但這樣做,就像是寫css的時候,寫了一堆!important,并不是一個優雅的解決方案。

cover-viewcover-image 組件還具有如下限制:

  1. 無法覆蓋textarea、input「原生組件」。
  2. 只支持基本的定位、布局、文本樣式。不支持設置單邊的border、background-image、shadow、overflow: visible等。
  3. cover-view 支持 overflow: scroll,但不支持動態更新 overflow。
  4. cover-viewcover-imagearia-role僅可設置為 button,讀屏模式下才可以點擊,并朗讀出“按鈕”;為空時可以聚焦,但不可點擊。
  5. cover-viewcover-image的子節點如果溢出父節點,容易出現布局錯誤。
  6. 支持css transition動畫,transition-property 只支持transform (translateX, translateY)opacity。
  7. 自定義組件嵌套 cover-view 時,自定義組件的 slot 及其父節點暫不支持通過 wx:if 控制顯隱,否則會導致 cover-view 不顯示。

隨著小程序生態的發展,開發者對「原生組件」的使用場景不斷擴大,「原生組件」的這些問題也日趨顯現,為了徹底解決「原生組件」帶來的種種限制,微信官方對小程序「原生組件」進行了一次重構,引入了 「同層渲染」。

3 「同層渲染」那些事

為了解決「原生組件」的層級問題,同時盡可能保留NA組件(指代「原生組件」)的優勢,小程序客戶端、前端及瀏覽內核團隊一起制定了一套解決方案:由于此方案的控件并非繪制在 NA 貼片層,而是繪制在 WebView 所渲染的頁面中,與其他 HTML 控件在同一層級,因此稱為 「同層渲染」。在支持「同層渲染」后,「原生組件」與其它 「H5 組件」(指代 HTML5 語言編寫的 web 組件)可以隨意疊加,層級的限制將不復存在。

你一定也想知道 「同層渲染」 背后究竟采用了什么技術。只有真正理解了 「同層渲染」 背后的機制,才能更高效地使用好這項能力。實際上,小程序的「同層渲染」在 iOSAndroid 平臺下的實現不同,因此下面分成兩部分來分別介紹兩個平臺的實現方案。

3.1 iOS 端「同層渲染」原理

3.1.1 專業名詞解釋

WKWebView: 是 iOS 8 之后提供的一款瀏覽器組件,iOS 端使用 WKWebView 進行渲染,WKWebView 在內部采用的是分層的方式進行渲染。WKWebView 會將 WebKit 內核生成的 Compositing Layer(合成層)渲染成 iOS 上的一個 WKCompositingView(「原生組件」的一種)。

Compositing Layer: NA 合成層,內核一般會將多個webview內的 DOM 節點渲染到一個 Compositing Layer 上,因此合成層與 DOM 節點之間不存在一對一的映射關系。

WKChildScrollView: 「原生組件」的一種。當把一個 DOM 節點的 CSS 屬性設置為 overflow: scroll (低版本需同時設置 -webkit-overflow-scrolling: touch)之后,WKWebView 會為其生成一個 WKChildScrollView,與 DOM 節點存在映射關系,這是一個原生的 UIScrollView 的子類,也就是說 WebView 里的滾動實際上是由真正的原生滾動組件來承載的。WKWebView 這么做是為了可以讓 iOS 上的 WebView 滾動有更流暢的體驗。雖說 WKChildScrollView 也是「原生組件」,但 WebKit 內核已經處理了它與其他 DOM 節點之間的層級關系,因此你可以直接使用 WXSS 控制層級而不必擔心遮擋的問題。

3.1.2 渲染原理解析

小程序 iOS 端的「同層渲染」也正是基于 WKChildScrollView 實現的,「原生組件」在 attached 之后會直接掛載到預先創建好的 WKChildScrollView 容器下,大致的流程如下:

  1. 小程序前端,在 webview 內創建一個 DOM 節點并設置其 CSS 屬性為 overflow: hidden-webkit-overflow-scrolling: touch,生成一個containerId,并將這個WKChildScrollView的位置信息通知給客戶端。
  2. 前端通知客戶端遞歸搜索查找到該 DOM 節點對應的原生 WKChildScrollView 組件;
  3. 將「原生組件」掛載到該 WKChildScrollView 節點上作為其子 View;
  4. WebKit 內核已經處理了WKChildScrollView與對應 DOM 節點之間的層級關系。


通過上述流程,小程序的「原生組件」就被插入到 WKChildScrollView 了,也即是在 步驟 1 創建的那個 DOM 節點映射的原生 WKChildScrollView節點。此時,修改這個 DOM 節點的樣式屬性同樣也會應用到「原生組件」上。因此,「同層渲染」的「原生組件」與普通的 H5 組件表現并無二致。

3.2 Android 端「同層渲染」原理

3.2.1 專業名詞解釋

chromium:小程序在 Android 端采用 chromium 作為 WebView 渲染層,與 iOS 不同的是,Android 端的 WebView單獨進行渲染而不會在客戶端生成類似 iOS 那樣的 Compositing View (合成層),經渲染后的 WebView 是一個完整的視圖。

WebPlugin:chromium 支持 WebPlugin 機制,WebPlugin 是瀏覽器內核的一個插件機制,主要用來解析和描述 標簽。比如 Chrome 瀏覽器上的 pdf 預覽,它就是基于 標簽實現的。

3.2.2 內核渲染流程

內核的渲染流程可分為下面四個步驟。如下如所示:

  1. 解析。當內核收到HTML數據時,會構建一顆DOM Tree,并為每個節點計算樣式。
  2. 排版。遍歷DOM Tree,根據樣式構建一顆Layout Tree。DOM TreeLayout Tree上的節點并非一一對應,如果某個 DOM 節點不可見,則不會在Layout Tree上。
  3. 繪制。為了提升繪制效率,對Layout Tree中的節點按照一定的規則分為不同的圖層(Layer),這些圖層也構成一個樹狀結構,稱之為Layer Tree。繪制過程就是遍歷Layout Tree,將每個節點的內容繪制到其所在的 Layer 上。在 GPU 硬繪模式下,Layer 存儲后端是 GPU 中的紋理-Texture。
  4. 合成。內核的合成模塊(CC 層)負責將 Layer 按照一定的順序合成到一起,交給系統的FrameBuffer,最終輸出到屏幕上。

3.2.3 渲染原理解析

從內核渲染流程可以看出,要實現「原生組件」「同層渲染」,就要將「原生組件」作為一個Layer插入到Layer Tree中。 如果能夠將「原生組件」渲染到內核提供的Texture上,就可達到「同層渲染」的目的。Android 端的「同層渲染」就是基于 標簽結合 chromium 內核擴展來實現的, 大致流程如下:

<embed id="web-plugin" type="plugin/video" width="750" height="600" />
復制代碼
  1. WebView 側創建一個 embed DOM 節點并指定組件類型;
  2. chromium 內核會創建一個 WebPlugin 實例,并生成一個 RenderLayer;
  3. Android 客戶端初始化一個對應的「原生組件」;
  4. Android 客戶端將「原生組件」的畫面繪制到步驟 2 創建的 RenderLayer 所綁定的 SurfaceTexture 上;
  5. 繪制完成后內核收到SurfaceTexture內容更新的通知,通知 chromium 內核渲染該 RenderLayer;
  6. chromium 渲染該 embed 節點并上屏。
  7. 當「同層渲染」的節點收到事件時,會將事件轉發給 Native 組件模塊處理。如果 Native 組件不消費事件,內核會再將事件向上冒泡。

這樣就實現了把一個「原生組件」渲染到 WebView 上,這個流程相當于給 WebView 添加了一個外置的插件。

這種方式可以用于 map、video、canvas、camera 等「原生組件」的渲染,對于 inputtextarea,采用的方案是直接對 chromium 的組件進行擴展,來支持一些 WebView 本身不具備的能力。

對比 iOS 端的實現,Android 端的 「同層渲染」 真正將「原生組件」視圖加到了 WebView 的渲染流程中且 embed 節點是真正的 DOM 節點,理論上可以將任意 WXSS 屬性作用在該節點上。Android 端相對來說是更加徹底的「同層渲染」,但相應的重構成本也會更高一些。

3.3 「同層渲染」的小缺陷

「原生組件」的 「同層渲染」 能力可能會在特定情況下失效,一方面你需要在開發時稍加注意,另一方面「同層渲染」失敗會觸發 bindrendererror 事件,可在必要時根據該回調做好 UI 的 fallback。

  • 對 Android 端來說,如果用戶的設備沒有微信自研的 chromium 內核,則會無法切換至 「同層渲染」,此時會在組件初始化階段觸發 bindrendererror。
  • iOS 端的情況會稍復雜一些:如果在基礎庫創建同層節點時,節點發生了 WXSS 變化從而引起 WebKit 內核重排,此時可能會出現同層失敗的現象。這是因為設置了-webkit-overflow-scrolling屬性的 div 塊(container),在其下層同時會產生一個 div 塊(內層 div)作為渲染使用,在 iOS 設備表現是生成的UIScrollView會有一個子 view 是WKCompositingView,如果改變內層 div 的高度,可能會觸發重新渲染,進而導致「原生組件」組件可能被無感知的被移除掉。解決方法:應盡量避免在「原生組件」上頻繁修改節點的 WXSS 屬性,尤其要盡量避免修改節點的 position 屬性。如需對「原生組件」進行變換,強烈推薦使用 transform 而非修改節點的 position 屬性。

4 重溫 「原生組件」 的那些坑

如果你仔細閱讀前面的篇幅,想必你對「原生組件」會有更深刻的了解,之前在實踐開發中遇到的一些問題,也將迎刃而解。接下來,我們一起重溫一下「原生組件」 的那些坑。

4.1

鮮花
鮮花
雞蛋
雞蛋
分享至 : QQ空間
收藏
时时彩彩票app下载手机版