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