從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

wonyun發表於2019-07-22

wxml與wxss的轉換

開啟小程式開發者工具,在除錯控制檯輸入openVendor就會開啟小程式的WeappVendor目錄,該目錄包括以下幾個主要內容:

  • wcc可執行程式,用於將wxml內容轉換為js內容,執行方式:wcc xxx.wxml
  • wcsc可執行程式,用於將wxss內容轉換為檢視可使用css內容,執行方式 wcsc xxx.wxss
  • 不同版本小程式基礎庫x.x.x.wxvpkg, 裡面包含小程式基礎庫WAService和WAWebview

1、wxml使用wcc轉換

正如上面分析的,通過呼叫小程式內建的可執行程式執行wcc xxx.wxml,將指定的wxml轉換為js指令碼內容。其具體用法可以--help檢視,如下圖:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

小程式開發者工具底層會將小程式專案中所有wxml轉為js內容,可以理解為為每個頁面wxml進行了註冊。例如我們小程式demo有兩個頁面index.wxml和logs.wxml,其中index.wxml內容如下圖:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現
通過wcc可執行程式生成的相關頁面註冊的程式碼如下圖所示:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

從頁面轉換的js內容來看,主要記錄標籤的屬性及其值等。另外,轉化的js指令碼提供最核心的方法是$gwx方法,可以在開發者工具開發控制檯訪問到,其方法簽名如下:

$gwx = function(path, global) {
    ...
    return function(env,dd,global){
        ...
    }
}

該方法根據傳入具體的頁面wxml路徑,找到對應的頁面,然後返回一個函式,向該函式傳入頁面渲染需要的資料(即Page中data物件)就能得到該頁面wxml對應的js物件形式表示的dom樹。其實每個小程式頁面在頁面準備初始化渲染時會呼叫這個$gwx方法,呼叫如下圖所示:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

另外,我們直接在開發者工具的控制檯直接呼叫,輸入如下語句,可以得到的js物件表示如下圖:

 $gwx('./pages/index/index.wxml')({show: true});

從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

2、wxss使用wcsc轉換

wcsc可執行程式用於處理wxss,小程式底層使用該可執行程式轉換為js內容來處理頁面css的引用。首先我們來看下wxss提供功能,如下圖:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

小程式底層使用wcsc -db -pc來轉換對應wxss檔案的,其生成的js內容如下圖eval函式中的字串所示:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

生成是js主要作用:

  • 新增尺寸單位rpx轉換,可根據螢幕寬度自適應
  • 提供setCssToHead方法將轉換後的css內容新增到header

開發者工具主入口

小程式開發者工具的主入口也是小程式的啟動入口,是整個小程式開發者工具的控制層,例如建立或者銷燬webview等。它主要包括小程式的檢視層的webview,業務邏輯層webview,偵錯程式的webview和編輯區的webview幾大塊;我們只需關心檢視層和業務邏輯層的webview。啟動入口對應這一個index.html頁面,裡面引入主入口js,如下:

<div id=container class=container></div>
<script src=../js/core/index.js> </script> 

最終初次進入小程式主頁後,主入口index.html的渲染html中有關檢視層和業務邏輯層結果如下圖所示:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

由此可以證明,小程式開發者工具業務邏輯層是在webview中執行的,該webview雖然提供瀏覽器相關介面,但是小程式只是在其中單純的執行js程式碼。

在我們小程式demo中有index首頁navigateTo到logs日誌頁時,可以看主入口dom的變化,見下圖:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

從dom變化可以看出,呼叫navigateTo相當於新開啟一個webview載入另一個頁面檢視,隨著開啟的頁面越來越多,記憶體就比較吃緊。這也是為什麼小程式對開啟頁面數量有限制的原因。從圖中可能也看出了,為啥多載入了一個pageframe.html的webview,這個是幹什麼用的?後面會說到它的作用。

檢視層頁面的實現

我們在寫小程式頁面檢視時,貌似並不關心webview中的html結構,這些都是小程式底層幫我們實現, 我們只需要寫頁面ui和業務邏輯即可。下面我們來看看view檢視層小程式幫我們做了什麼。先來看一下檢視層pageframe.html的模板:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現
其中,模板中的註釋佔位符經過後臺服務處理會注入不同js指令碼,主要js內容:

  • <!-- deviceinfo -->: 暫時無用的佔位符,會被空字串""替換
  • <!-- jsdebug -->: 提供檢視層的WeixinJSBridge模擬實現以及一些事件的處理如enablePullDownRefresh,其對應的js內容為extensions/pageframe/index.js.
  • <!-- plugincode -->: 小程式外掛相關的程式碼,若小程式使用外掛則會注入
  • <!-- wxmlcode -->: 呼叫wcc可執行命令生成的小程式註冊所有頁面wxml對應的js指令碼內容
  • <!-- wxsscode -->: 呼叫wcss可執行命令生成的js指令碼內容,提供注入css到頁面的js方法;該內容會提前注入全域性的css。
  • <!-- wxappcode -->: 小程式當前檢視頁面相關的配置json內容以及wxml和wxss轉換為js的內容,可在控制檯輸入__wxAppCode__看相關資訊
    從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現
  • <!-- vendorlist -->: 小程式為檢視層注入的基礎庫功能,包括WAWebview.js、WARemoteDebug.js和hls.js

檢視層頁面實現技術細節

本節來詳細介紹下小程式檢視層實現的一些技術細節

檢視層快速開啟原理

首先看一下小程式官網頁面層級準備小節描述的一段內容:

wx.navigateTo會建立一個新的頁面層級,對於每一個新的頁面層級,檢視層都需要進行一些額外的準備工作。在小程式啟動前,微信會提前準備好一個頁面層級用於展示小程式的首頁。除此以外,每當一個頁面層級被用於渲染頁面,微信都會提前開始準備一個新的頁面層級,使得每次呼叫wx.navigateTo都能夠儘快展示一個新的頁面。

正如上文提到的,我們在開啟pages/logs/logs檢視頁面時,發現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地址http://127.0.0.1:${global.proxyPort}/__pageframe__/pageframe.html
    從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

載入完成後,設定其src為pageframe.html:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

  • 新的src內容載入完成後再次觸發onLoadCommit事件但根據條件不會執行reload方法。

  • pageframe.html頁面在dom ready之後觸發注入並執行具體頁面相關的程式碼,此時通過history.pushState方法修改webview的src但是webview並不會傳送頁面請求。
    從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

pageframe.html模板生成的內容除小程式基礎庫檢視層的底層功能之外,還包括小程式所有頁面的模板資訊、配置資訊以及樣式內容,這些都可以在生成pageframe.html的dom結構中窺探一二。

從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

那麼,既然每個檢視層頁面由pageframe模板生成,那麼小程式每個頁面獨有的頁面內容如dom和樣式等如何生成呢,這主要是利用nw.js的executeScript方法來執行一段js指令碼來注入只與當前頁面相關的程式碼,包括當前頁面的配置,注入當前頁的css以及當前頁面的virtual dom的生成,注入的程式碼如下:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

最終生成的js程式碼(拿pages/index/index為例)如下圖:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

其中:

history.pushState('','', 'http://127.0.0.1:59524/__pageframe__/pages/index/index')

這句程式碼的作用修改當前webview的src,因為檢視層的webview的src為pageframe.html,通過這句程式碼將其變更為具體的頁面地址。

另外,需要注意的是nw.js的executeScript方法注入的程式碼是需要時機的,需要等到檢視層的初始化工作準備ready之後才行,那麼這個時機如何知道呢?細心的讀者可能發現,在pageframe模板的最後一個script的內容:

<script>alert("DOCUMENT_READY")</script>

這個從字面意思可以看出此時應該是頁面dom ready的一個時機,通過alert來進行通知。
alert能通知訊息?當然可以的,在nw.js的webview中alert、prompt對應的彈框是被會阻止的,那麼通過為webview繫結dialog事件來知道是那種彈框型別,以及提示內容,具體可以檢視這篇文章。例如小程式開發者工具繫結事件部分程式碼(便於檢視有修改):

this.webview.on('dialog', (a) => {
   a.preventDefault();
   const b = a.messageType || '',
            c = a.messageText,
            d = a.dialog;
    if ('alert' === b) {
        c === 'DOCUMENT_READY' && (this.documentReady = !0, this.loadPage())
    }
    ...
})

這樣方法loadPage就會觸發nw注入並執行頁面相關的程式碼。最終生成的頁面檢視對應dom結構如下圖:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

可以看出,檢視頁面生成的dom結構中,document.body已無pageframe.html模板中對應body中的script內容,這是因為檢視層的WAWebview.js在通過virtual dom生成真實dom過程中,它會掛載到頁面的document.body上,覆蓋掉pageframe.html模板中對應document.body的內容。

業務邏輯層頁面的實現

小程式將所有業務程式碼置於同一個執行緒中執行,小程式開發者工具是在一個webview中執行;webview中的appservice.html引入業務程式碼js之外,還有後臺服務內嵌的一些基礎功能程式碼,appservice.html對應的模板內容如下:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現
經過後臺服務的處理,模板中的各種佔位符就被對應的js內容注入,下面就來簡單說幾個重要的注入內容:

  • <!-- wxconfig -->: 小程式的配置項,包括使用者自定義與系統預設的整合結果。在控制檯輸入__wxConfig可以看出列印結果
    從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現
  • <!--devtoolsconfig-->:小程式開發者配置,包括開發者工具版本,設定的請求域名、預設開發者工具的設定以及訪問Native方法需要permission的方法。控制檯輸入__devtoolsconfig可以看到其對應的資訊
    從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現
  • <!-- wxmlxcjs -->: 呼叫wcc可執行命令生成的小程式註冊所有頁面wxml對應的js指令碼內容,js指令碼提供$gwx方法。
  • <!-- asdebug -->: 提供業務邏輯層的WeixinJSBridge模擬以及一些針對開發者工具的介面,如在控制檯輸入help可以看到提供的介面。其內容為extensions/appservice/index.js.
  • <!-- vendorlist -->: 為業務邏輯層注入WAService.js,為業務邏輯層提供小程式底層基礎庫的功能

此外,開發者工具服務還在appservice.html的body注入一段指令碼,指令碼的作用是將業務邏輯程式碼通過script動態注入到head中執行,這段程式碼如下:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

最終生成的appservice.html中的head情況如下圖所示:
從微信小程式開發者工具原始碼看實現原理(二)- - 小程式技術實現

通過上圖可以看出,我們寫的頁面邏輯都引入到頁面中,並且分別從app.js開始一一執行;小程式程式碼呼叫Page構造器的時候,小程式基礎庫會記錄頁面的基礎資訊,如初始資料(data)、方法等。需要注意的是,如果一個頁面被多次建立,並不會使得這個頁面所在的JS檔案被執行多次,而僅僅是根據初始資料多生成了一個頁面例項(this),在頁面JS檔案中直接定義的變數,在所有這個頁面的例項間是共享的。

參考

相關文章