寫作背景
接觸小程式有一段時間了,總得來說小程式開發門檻比較低,但其中基本的執行機制和原理還是要懂的。“比如我在面試的時候問到一個關於小程式的問題,問小程式有window物件嗎?他說有吧”,但其實是沒有的。感覺他並沒有瞭解小程式底層的一些東西,歸根結底來說應該只能算會使用這個工具,但並不明白其中的道理。
小程式與普通網頁開發是有很大差別的,這就要從它的技術架構底層去剖析了。還有比如習慣Vue,react開發的開發者會吐槽小程式新建頁面的繁瑣,page必須由多個檔案組成、元件化支援不完善、每次更改 data 裡的資料都得setData、沒有像Vue方便的watch監聽、不能操作Dom,對於複雜性場景不太好,之前不支援npm,不支援sass,less預編譯處理語言。
“有的人說小程式就像被閹割的Vue”,哈哈當然了,他們從設計的出發點就不同,我們也得理解小程式設計的初衷,通過它的使用場景,它為什麼採用這種技術架構,這種技術架構有什麼好處,相信在你瞭解完這些之後,就會理解了。下面我會從以下幾個角度去分析小程式的執行機制和它的整體技術架構。
瞭解小程式的由來
在小程式沒有出來之前,最初微信WebView逐漸成為移動web重要入口,微信釋出了一整套網頁開發工具包,稱之為 JS-SDK,給所有的 Web 開發者開啟了一扇全新的窗戶,讓所有開發者都可以使用到微信的原生能力,去完成一些之前做不到或者難以做到的事情。
但JS-SDK 的模式並沒有解決使用移動網頁遇到的體驗不良的問題,比如受限於裝置效能和網路速度,會出現白屏的可能。因此又設計了一個增強版JS-SDK,也就是“微信 Web 資源離線儲存”,但在複雜的頁面上依然會出現白屏的問題,原因表現在頁面切換的生硬和點選的遲滯感。這個時候需要一個 JS-SDK 所處理不了的,使使用者體驗更好的一個系統,小程式應運而生。
- 快速的載入
- 更強大的能力
- 原生的體驗
- 易用且安全的微信資料開放
- 高效和簡單的開發
小程式與普通網頁開發的區別
小程式的開發同普通的網頁開發相比有很大的相似性,小程式的主要開發語言也是 JavaScript,但是二者還是有些差別的。
- 普通網頁開發可以使用各種瀏覽器提供的 DOM API,進行 DOM 操作,小程式的邏輯層和渲染層是分開的,邏輯層執行在 JSCore
中,並沒有一個完整瀏覽器物件,因而缺少相關的DOM API和BOM
API。 - 普通網頁開發渲染執行緒和指令碼執行緒是互斥的,這也是為什麼長時間的指令碼執行可能會導致頁面失去響應,而在小程式中,二者是分開的,分別執行在不同的執行緒中。
- 網頁開發者在開發網頁的時候,只需要使用到瀏覽器,並且搭配上一些輔助工具或者編輯器即可。小程式的開發則有所不同,需要經過申請小程式帳號、安裝小程式開發者工具、配置專案等等過程方可完成。
- 小程式的執行環境
小程式架構
一、技術選型
一般來說,渲染介面的技術有三種:
- 用純客戶端原生技術來渲染
- 用純 Web 技術來渲染
- 用客戶端原生技術與 Web 技術結合的混合技術(簡稱 Hybrid 技術)來渲染
通過以下幾個方面分析,小程式採用哪種技術方案
- 開發門檻:Web 門檻低,Native 也有像 RN 這樣的框架支援
- 體驗:Native 體驗比 Web 要好太多,Hybrid 在一定程度上比 Web 接近原生體驗
- 版本更新:Web 支援線上更新,Native 則需要打包到微信一起稽核釋出
- 管控和安全:Web 可跳轉或是改變頁面內容,存在一些不可控因素和安全風險
由於小程式的宿主環境是微信,如果用純客戶端原生技術來編寫小程式,那麼小程式程式碼每次都需要與微信程式碼一起發版,這種方式肯定是不行的。
所以需要像web技術那樣,有一份隨時可更新的資源包放在雲端,通過下載到本地,動態執行後即可渲染出介面。如果用純web技術來渲染小程式,在一些複雜的互動上可能會面臨一些效能問題,這是因為在web技術中,UI渲染跟JavaScript的指令碼執行都在一個單執行緒中執行,這就容易導致一些邏輯任務搶佔UI渲染的資源。
所以最終採用了兩者結合起來的Hybrid 技術來渲染小程式,可以用一種近似web的方式來開發,並且可以實現線上更新程式碼,同時引入元件也有以下好處:
- 擴充套件 Web 的能力。比如像輸入框元件(input, textarea)有更好地控制鍵盤的能力
- 體驗更好,同時也減輕 WebView 的渲染工作
- 繞過 setData、資料通訊和重渲染流程,使渲染效能更好
- 用客戶端原生渲染內建一些複雜元件,可以提供更好的效能
二、雙執行緒模型
小程式的渲染層和邏輯層分別由 2 個執行緒管理:檢視層的介面使用了 WebView 進行渲染,邏輯層採用 JsCore 執行緒執行 JS指令碼。
那麼為什麼要這樣設計呢,前面也提到了管控和安全,為了解決這些問題,我們需要阻止開發者使用一些,例如瀏覽器的window物件,跳轉頁面、操作DOM、動態執行指令碼的開放性介面。
我們可以使用客戶端系統的 JavaScript 引擎,iOS 下的 JavaScriptCore 框架,安卓下騰訊 x5 核心提供的 JsCore 環境。
這個沙箱環境只提供純 JavaScript 的解釋執行環境,沒有任何瀏覽器相關介面。
這就是小程式雙執行緒模型的由來:
- 邏輯層:建立一個單獨的執行緒去執行 JavaScript,在這裡執行的都是有關小程式業務邏輯的程式碼,負責邏輯處理、資料請求、介面呼叫等
- 檢視層:介面渲染相關的任務全都在 WebView 執行緒裡執行,通過邏輯層程式碼去控制渲染哪些介面。一個小程式存在多個介面,所以檢視層存在多個 WebView 執行緒
- JSBridge 起到架起上層開發與Native(系統層)的橋樑,使得小程式可通過API使用原生的功能,且部分元件為原生元件實現,從而有良好體驗
三、雙執行緒通訊
把開發者的 JS 邏輯程式碼放到單獨的執行緒去執行,但在 Webview 執行緒裡,開發者就沒法直接操作 DOM。
那要怎麼去實現動態更改介面呢?
如上圖所示,邏輯層和試圖層的通訊會由 Native (微信客戶端)做中轉,邏輯層傳送網路請求也經由 Native 轉發。
這也就是說,我們可以把 DOM 的更新通過簡單的資料通訊來實現。
Virtual DOM 相信大家都已有了解,大概是這麼個過程:用 JS 物件模擬 DOM 樹 -> 比較兩棵虛擬 DOM 樹的差異 -> 把差異應用到真正的 DOM 樹上。
如圖所示:
1. 在渲染層把 WXML 轉化成對應的 JS 物件。
2. 在邏輯層發生資料變更的時候,通過宿主環境提供的 setData 方法把資料從邏輯層傳遞到 Native,再轉發到渲染層。
3. 經過對比前後差異,把差異應用在原來的 DOM 樹上,更新介面。
我們通過把 WXML 轉化為資料,通過 Native 進行轉發,來實現邏輯層和渲染層的互動和通訊。
而這樣一個完整的框架,離不開小程式的基礎庫。
四、小程式的基礎庫
小程式的基礎庫可以被注入到檢視層和邏輯層執行,主要用於以下幾個方面:
- 在檢視層,提供各類元件來組建介面的元素
- 在邏輯層,提供各類 API 來處理各種邏輯
- 處理資料繫結、元件系統、事件系統、通訊系統等一系列框架邏輯
由於小程式的渲染層和邏輯層是兩個執行緒管理,兩個執行緒各自注入了基礎庫。
小程式的基礎庫不會被打包在某個小程式的程式碼包裡邊,它會被提前內建在微信客戶端。
這樣可以:
- 降低業務小程式的程式碼包大小
- 可以單獨修復基礎庫中的 Bug,無需修改到業務小程式的程式碼包
五、Exparser 框架
Exparser是微信小程式的元件組織框架,內建在小程式基礎庫中,為小程式的各種元件提供基礎的支援。小程式內的所有元件,包括內建元件和自定義元件,都由Exparser組織管理。
Exparser的主要特點包括以下幾點:
- 基於Shadow
DOM模型:模型上與WebComponents的ShadowDOM高度相似,但不依賴瀏覽器的原生支援,也沒有其他依賴庫;實現時,還針對性地增加了其他API以支援小程式元件程式設計。 - 可在純JS環境中執行:這意味著邏輯層也具有一定的元件樹組織能力。
- 高效輕量:效能表現好,在元件例項極多的環境下表現尤其優異,同時程式碼尺寸也較小。
小程式中,所有節點樹相關的操作都依賴於Exparser,包括WXML到頁面最終節點樹的構建、createSelectorQuery呼叫和自定義元件特性等。
內建元件
基於Exparser框架,小程式內建了一套元件,提供了檢視容器類、表單類、導航類、媒體類、開放類等幾十種元件。有了這麼豐富的元件,再配合WXSS,可以搭建出任何效果的介面。在功能層面上,也滿足絕大部分需求。
六、執行機制
小程式啟動會有兩種情況,一種是「冷啟動」,一種是「熱啟動」。假如使用者已經開啟過某小程式,然後在一定時間內再次開啟該小程式,此時無需重新啟動,只需將後臺狀態的小程式切換到前臺,這個過程就是熱啟動;冷啟動指的是使用者首次開啟或小程式被微信主動銷燬後再次開啟的情況,此時小程式需要重新載入啟動。
- 小程式沒有重啟的概念
- 當小程式進入後臺,客戶端會維持一段時間的執行狀態,超過一定時間後(目前是5分鐘)會被微信主動銷燬
- 當短時間內(5s)連續收到兩次以上收到系統記憶體告警,會進行小程式的銷燬
七、更新機制
小程式冷啟動時如果發現有新版本,將會非同步下載新版本的程式碼包,並同時用客戶端本地的包進行啟動,即新版本的小程式需要等下一次冷啟動才會應用上。 如果需要馬上應用最新版本,可以使用 wx.getUpdateManager API 進行處理。
八、效能優化
主要的優化策略可以歸納為三點:
- 精簡程式碼,降低WXML結構和JS程式碼的複雜性;
- 合理使用setData呼叫,減少setData次數和資料量;
- 必要時使用分包優化。
1、setData 工作原理
小程式的檢視層目前使用 WebView 作為渲染載體,而邏輯層是由獨立的 JavascriptCore 作為執行環境。在架構上,WebView 和 JavascriptCore 都是獨立的模組,並不具備資料直接共享的通道。當前,檢視層和邏輯層的資料傳輸,實際上通過兩邊提供的 evaluateJavascript 所實現。即使用者傳輸的資料,需要將其轉換為字串形式傳遞,同時把轉換後的資料內容拼接成一份 JS 指令碼,再通過執行 JS 指令碼的形式傳遞到兩邊獨立環境。
而 evaluateJavascript 的執行會受很多方面的影響,資料到達檢視層並不是實時的。
2、常見的 setData 操作錯誤
- 頻繁的去 setData在我們分析過的一些案例裡,部分小程式會非常頻繁(毫秒級)的去setData,其導致了兩個後果:Android下使用者在滑動時會感覺到卡頓,操作反饋延遲嚴重,因為 JS 執行緒一直在編譯執行渲染,未能及時將使用者操作事件傳遞到邏輯層,邏輯層亦無法及時將操作處理結果及時傳遞到檢視層;渲染有出現延時,由於 WebView 的 JS 執行緒一直處於忙碌狀態,邏輯層到頁面層的通訊耗時上升,檢視層收到的資料訊息時距離發出時間已經過去了幾百毫秒,渲染的結果並不實時;
- 每次 setData 都傳遞大量新資料由setData的底層實現可知,我們的資料傳輸實際是一次 evaluateJavascript
- 指令碼過程,當資料量過大時會增加指令碼的編譯執行時間,佔用 WebView JS 執行緒, 後臺態頁面進行
setData當頁面進入後臺態(使用者不可見),不應該繼續去進行setData,後臺態頁面的渲染使用者是無法感受的,另外後臺態頁面去setData也會搶佔前臺頁面的執行。
總結
大致從以上幾個角度分析了小程式的底層架構,從小程式的由來、到雙執行緒的出現、設計、通訊、到基礎庫、Exparser 框架、再到執行機制、效能優化等等,都是一個個相關而又相互影響的選擇。關於小程式的底層框架設計,應該還有很多,每一個框架的誕生都有其意義,我們作為開發者能做的不只是會使用這個工具,還應理解它的設計模式。只有這樣才不會被工具左右,才能走的更遠!