當我們前端切圖崽網上沖浪的時候,會發現有很多技術文章都在分析vue框架,react框架,顯少有分析小程序框架的。那今天就通過這篇短小精悍的文章帶大家了解一下微信小程序的底層架構。(如無特殊說明,下文中提到的小程序都是微信小程序)
小程序的由來
我們先拋出一個問題,在沒有小程序的時候,企業們都在微信里怎么運營? 答案就是小程序的“前身”-公眾號,企業們普遍會把H5網站放在公眾號作為流量轉換的入口。但是h5確實讓公眾號遇到了一些問題。
首先就是白屏過程,對于一些復雜頁面,受限于設備性能和網絡速度,白屏會更加明顯;再就是缺少操作反饋,比如頁面切換生硬以及點擊所帶來的遲滯感等等;
微信團隊內部通過JS-SDK以及后來的增強JS-SDK已經能夠解決一些問題,但是對于上述問題是JS-SDK所處理不了的,急需一個全新的系統來完成,它需要具備以下能力:
- 快速加載
- 更強大的能力
- 原生的體驗
- 易用且安全的微信數據開放
- 高效,簡單的開發
于是,小程序誕生了。
雙線程架構
此處點題一下,本文我們討論的是小程序的底層架構,其實,雙線程架構就是小程序的核心。
那為什么要設計成雙線程架構呢?首先我們來回顧一下瀏覽器的線程模型,瀏覽器是一個單線程架構,主要原因是js允許訪問操作DOM,因此js線程和渲染線程只能互斥運行。
那小程序又是如何做到雙線程的呢,根本原因就是微信小程序禁止js操作DOM。
使用雙線程架構的優勢一目了然:
- 提高用戶體驗(ui和邏輯分離,避免頁面長時間阻塞和卡頓)
- 優化應用性能(運行在不同的線程中,可以同時渲染或者計算)
- 開發效率更高(解耦和松散耦合)
接下來就帶大家了解一下渲染層以及邏輯層的設計思路。
設計思路-渲染層
標簽實現
小程序使用的是Exparser組件模型,Exparser組件模型與Web Components中的shadow DOM高度相似,微信為什么使用自定義組件框架,而不使用Web Components呢?主要還是出于安全考慮,并且方便管控。既然Exparser組件框架與shadow DOM高度相似,那么我們首先來了解一下shadow DOM。
shadow DOM: Web Components的一個重要屬性是封裝-可以將標記結構、樣式和行為隱藏起來,并與頁面上的其他代碼相隔離,保證不同的部分不會混在一起,可使代碼更加干凈、整潔。其中,shadow DOM接口是關鍵所在,它可以將一個隱藏的,獨立的DOM附加到一個元素上。
shadow DOM允許將隱藏的DOM樹附加到常規的DOM樹中-它以shadow root節點為起始根節點,在根節點的下方,可以是任意元素,和普通的DOM一樣。
以上解釋來源于MDN,其實shadow DOM并不神秘,像我們非常熟悉的video標簽本質上就是用shadow DOM實現的。我們先打開chrome瀏覽器設置中的“打開用戶代理shadow DOM”,然后再點擊video標簽就能看到。
創建shadow DOM也非常簡單,直接使用attachShadow方法就可以創建。
var shadow = Element.attachShadow({ mode: 'closed'})
- 1.
Exparser組件模型:Exparser組件模型參考了shadow DOM并進行了一些修改,像事件系統就是完全復刻的,slot插槽,屬性傳遞等都基本一致。但同時它又具有一些特點:
- 基于shadow DOM模型:模型上與Web Components的shadow DOM高度相似,但不依賴瀏覽器的原生支持,也沒有其他依賴庫;實現時,還針對性地增加了其他API以支持小程序組件編程;
- 可在純JS環境中運行:這意味著邏輯層也具有一定的組件樹組織能力;
- 高效輕量:性能表現好,在組件實例極多的環境下表現尤其優異,同時代碼尺寸也較小;
WXML編譯
了解了小程序的組件系統之后,接下來看看WXML的編譯過程。小程序中的DOM編譯流程與vue類似,也會先將代碼字符串編譯為虛擬DOM,小程序中的虛擬DOM結構如下:WXML最終會被編譯為JS文件,然后插入到渲染層的script標簽中。
WXSS動態適配
WXSS是小程序中使用的樣式語言,WXSS具有css的大部分特性,同時它對CSS進行了擴充以及修改。
小程序中使用的尺寸單位為rpx(Responsive px),不同于h5中對于px的處理,需要使用postcss進行統一的轉換,小程序底層已經為開發者做好了這層轉換,那具體它是怎么做到的呢?
我們看它的這段源碼,其實它與阿里的flexible.js方案是類似的,不同的是它做了一個精度收攏的優化,主要是為了解決1px的問題。
WXSS同樣會經過編譯,最終的編譯產物為wxss.js,不同于WXML通過script標簽的形式插入到渲染層,wxss.js則是通過eval的方式注入到渲染層代碼中。
渲染層webview
全局變量: 渲染線程中存在著以下全局變量。
- webviewId:webview的唯一標識,當用戶打開一個小程序頁面的時候,相當于打開了一個webview,不同的webview用webviewid來區分;
- wxAppCode:整個頁面的json wxss wxml編譯之后都存儲在這里;
- Vd_version_info:版本信息;
- ./dev/wxconfig.js:小程序默認總配置項,包括用戶自定義與系統默認的整合結果。在控制臺輸入__wxConfig可以看出打印結果;
- ./dev/devtoolsconfig.js:小程序開發者配置,包括navigationBarHeight,標題欄的高度,狀態欄高度,等等,控制臺輸入__devtoolsConfig可以看到其對應的信息;
- ./dev/deviceinfo.js:設備信息,包含尺寸/像素點pixelRatio;
- ./dev/jsdebug.js:debug工具;
- ./dev/WAWebview.js:渲染層底層基礎庫;
- ./dev/hls.js:優秀的視頻流處理工具;
- ./dev/WARemoteDebug.js:底層基礎庫調試工具;
那小程序是如何快速啟動一個webview的呢?
我們在打開pages/index/index視圖頁面時,發現DOM中多加載了一個__pageframe__/pageframe.html的視圖層。這個視圖層的作用正是小程序提前為一個新的頁面層準備的。小程序每個視圖層頁面內容都是通過pageframe.html模板來生成的,包括小程序啟動的首頁。
下面來看看小程序為快速打開小程序頁面做的技術優化:
- 首頁啟動時,即第一次通過pageframe.html生成內容后,后臺服務會緩存pageframe.html模板首次生成的html內容;
- 非首次新打開頁面時,頁面請求的pageframe.html內容直接走后臺緩存;
- 非首次新打開頁面時,pageframe.html頁面引入的外鏈js資源走本地緩存; 這樣在后續新打開頁面時,都會走緩存的pageframe的內容,避免重復生成,快速打開一個新頁面。
視圖層打開新頁面的流程
在創建每個視圖層頁面的webview時,都會為其綁定了onLoadCommit事件(它會在頁面加載完成后觸發,包含當前文檔的導航和副框架的文檔加載)。初始時webview的src會被指定為空頁面地址http://127.0.0.1:${global.proxyPort}/aboutblank?${c},其中c為對應webview的id。webview從空頁面到具體頁面視圖的過程如下:
- 空頁面地址webview加載完畢后執行事件中的reload方法,即設置webview的src為pageframe地址;
- 加載完成后,設置其src為pageframe.html, 新的src內容加載完成后再次觸發onLoadCommit事件但根據條件不會執行reload方法;
- pageframe.html頁面在dom ready之后觸發注入并執行具體頁面相關的代碼,此時通過history.pushState方法修改webview的src但是webview并不會發送頁面請求;
設計思路-邏輯層
接下來我們看看小程序在邏輯層都做了哪些事情。
邏輯層與視圖層通信
在小程序中,邏輯層只有一個,但是渲染層有多個,渲染層和邏輯層之間是通過微信客戶端進行橋接通信的。那具體是怎么實現的呢?其實它使用的就是WeixinJSBridge通信機制。
在小程序執行的過程中,微信客戶端分別向渲染層和邏輯層注入WeixinJSBridge,WeixinJSBridge主要提供了以下幾個方法:
- invoke:調用native API;
- invokeCallbackHandler:Native 傳遞 invoke 方法回調結果;
- publish:渲染層用來向邏輯業務層發送消息,也就是說要調用邏輯層的事件方法;
- subscribe:訂閱邏輯層消息;
- subscribeHandler:視圖層和邏輯層消息訂閱轉發;
- setCustomPublishHandler:自定義消息轉發;
渲染層如何向邏輯層通信?
渲染層向邏輯層通信的方式就是采用事件系統,以上就是完整的事件系統流程。
開發者在DOM上通過@click綁定事件,WXML文件被編譯的時候,會通過$gwx函數生成虛擬DOM,然后小程序執行的時候渲染層底層基礎庫會對虛擬DOM進行解析,事件綁定最終會以attr屬性的形式生成到虛擬DOM中,所以底層基礎庫通過applyPropeties解析事件并通過addEventListener綁定到相應DOM并聲明回調。
用戶點擊相應DOM時,Exparser組件系統接收到這個事件,然后開始執行回調。回調函數在邏輯層,事件的觸發在渲染層,此時,小程序會通過setData發送數據到邏輯層,這個時候WeixinJSBridge就派上用場了,渲染層調用publish方法發送數據,邏輯層通過registercallback進行監聽,并執行相應的回調。此時,渲染層到邏輯層的通信流程結束。
那邏輯層又是如何將改變后的數據回傳給渲染層的呢?邏輯層改變數據之后,同樣是觸發setData方法,然后渲染層通過subscribe進行監聽,從eventname和觸發事件時候記錄的回調函數來判斷是哪個事件被觸發了,從而獲取動態數據。
第三方小程序框架
WXML,WXSS都是小程序的原生開發語言,使用原生語言開發還是存在諸多限制,尤其是17年小程序剛推出那會。因此,第三方小程序框架應運而生。第三方框架可以分為三大類。
第一類是預編譯框架,預編譯框架就是在執行前就進行編譯。像我司在17年開發“轉轉二手交易網”的時候使用的wepy框架就屬于預編譯框架。預編譯框架也有一些顯而易見的缺點,這類預編譯框架要么是類vue,要么是類React,如果后期vue或者React再出一些新特性的話,預編譯框架就要進行擴展編寫;還有一些兼容問題,對于小程序本身不支持的一些屬性,預編譯框架需要進行兼容;
第二類是半編譯半運行框架,像美團的mpvue就是此類框架,半編譯指的是vue的template需要單獨編譯為wxml,半運行講的是vue整體的特性都會在邏輯層中運行。為了符合小程序的渲染框架,修改了vue的框架;
第三類是運行時框架,像Remax就是運行時框架,它可以使開發者使用完整的React語法來開發小程序。因為小程序框架本身是不支持js直接操作DOM的,那Remax框架是如何解決這個問題的呢?其實它自己復刻了一套操作DOM的API,例如appendChild,innterHtml等,但是它真正操作的并不是dom,而是data中的數據結構。從而達到了操作DOM的目的。使得自己真正成了一個運行時框架;
結語
介紹到這里,小程序的底層框架原理基本已經介紹完了,想跟大家分享的是,小程序確實和h5非常類似,其實它相當于一個借助了native強大功能的加強版h5,小程序并不神秘,除了微信小程序之外,現在各大超級APP都已經推出了自己的小程序,原理應該都大差不差。
本篇文章其實相當于一個學習筆記,作者本身非常想搞清楚微信小程序的架構,但是微信小程序并沒有開源,某次偶然的機會逛掘金的時候看到這篇小冊,就整個學習了一下,在此感謝原作者!
參考
https://juejin.cn/book/6982013809212784676?enter_from=course_center&utm_source=course_center