一、小程序基礎知識
小程序是基於WEB規范,采用HTML、CSS和JS等搭建的一套框架,微信官方給它們取的名字:WXML、WXSS,但本質上還是在整個WEB體系之下構建的。WXML說到底就是xml的一個子集。WXML采用微信自定義的少量標簽WXSS,大家可以理解為就是自定義的CSS。實現邏輯部分的JS還是通用的ES規范,並且runtime還是Webview(IOS WKWEBVIEW、ANDROID X5)
1、小程序的組成結構
一個完整的小程序主要由以下幾部分組成:
一個入口文件:app.js
一個全局樣式:app.wxss
一個全局配置:app.json
頁面:pages下,每個頁面再按文件夾划分,每個頁面4個文件
(1)視圖層:wxml,wxss
(2)邏輯層:js,json(頁面配置,不是必須)
注:pages里面還可以再根據模塊划分子目錄,孫子目錄,只需要在app.json里注冊時填寫路徑就行。
2、小程序項目打包:
編輯器它本身也是基於WEB技術體系實現的,nwjs+react,nwjs簡單是說就是node+webkit,node提供給我們本地api能力,而webkit提供給我們web能力,兩者結合就能讓我們使用JS+HTML實現本地應用程序。既然有nodejs,那上面的打包選項里的功能就好實現了。
(1)ES6轉ES5:引入babel-core的node包
(2)CSS補全:引入postcss和autoprefixer的node包(postcss和autoprefixer的原理看這里)
(3)代碼壓縮:引入uglifyjs的node包
打包后目錄結構:

所有的小程序基本都最后都被打成上面的結構:
(1)WAService.js 框架JS庫,提供邏輯層基礎的API能力
(2)WAWebview.js 框架JS庫,提供視圖層基礎的API能力
(3)WAConsole.js 框架JS庫,控制台
(4)app-config.js 小程序完整的配置,包含我們通過app.json里的所有配置,綜合了默認配置型
(5)app-service.js 我們自己的JS代碼,全部打包到這個文件
(6)page-frame.html 小程序視圖的模板文件,所有的頁面都使用此加載渲染,且所有的WXML都拆解為JS實現打包到這里
(7)pages 所有的頁面,這個不是我們之前的wxml文件了,主要是處理WXSS轉換,使用js插入到header區域。
3、與H5頁面的區別
小程序和普通的 h5 頁面到底有什么區別呢?
(1)運行環境:小程序基於瀏覽器內核重構的內置解析器,而 h5 的宿主環境是瀏覽器。所以小程序中沒有 DOM 和 BOM 的相關 API , jQuery 和一些 NPM 包都不能在小程序中使用;
普通網頁開發可以使用各種瀏覽器提供的 DOM API,進行 DOM 操作,而小程序的邏輯層和渲染層是分開的,邏輯層運行在 JSCore 中,並沒有一個完整瀏覽器對象,因而缺少相關的DOM API和BOM API。
(2)系統權限:小程序能獲得更多的系統權限,如網絡通信狀態、數據緩存能力等;
(3)渲染機制:小程序的邏輯層和渲染層是分開的,而 h5 頁面 UI 渲染跟 JavaScript 的腳本執行都在一個單線程中,互斥。所以 h5 頁面中長時間的腳本運行可能會導致頁面失去響應。
普通網頁開發渲染線程和腳本線程是互斥的,這也是為什么長時間的腳本運行可能會導致頁面失去響應,而在小程序中,二者是分開的,分別運行在不同的線程中。
此外,小程序面對的是 iOS 和 Android 微信客戶端和輔助開發的小程序開發者工具。根據官方文檔,這三大運行環境也是有所區別的:

所以微信小程序介於 web 端和原生 App 之間,能夠豐富調用功能接口,同時又跨平台。
二、小程序架構
1、雙線程模型
微信小程序的框架包含兩部分:View視圖層、App Service邏輯層。View層用來渲染頁面結構,App Service層用來邏輯處理、數據請求、接口調用,它們在兩個進程(兩個Webview)里運行。
視圖層和邏輯層通過系統層的JSBridage進行通信,邏輯層把數據變化通知到視圖層,觸發視圖層頁面更新,視圖層把觸發的事件通知到邏輯層進行業務處理。


小程序的渲染層和邏輯層分別由2個線程管理:
(1)視圖層:界面渲染相關的任務全都在 WebView 線程里執行。一個小程序存在多個界面,所以渲染層存在多個 WebView 線程。
(2)邏輯層:采用 JsCore 線程運行JS腳本。
視圖層和邏輯層通過系統層的 WeixinJsBridage 進行通信:邏輯層把數據變化通知到視圖層,觸發視圖層頁面更新,視圖層把觸發的事件通知到邏輯層進行業務處理。
2、渲染流程
把開發者的 JS 邏輯代碼放到單獨的線程去運行,但在 Webview 線程里,開發者就沒法直接操作 DOM。
那要怎么去實現動態更改界面呢?
如上圖所示,邏輯層和試圖層的通信會由 Native (微信客戶端)做中轉,邏輯層發送網絡請求也經由 Native 轉發。
這也就是說,我們可以把 DOM 的更新通過簡單的數據通信來實現。
Virtual DOM 相信大家都已有了解,大概是這么個過程:用 JS 對象模擬 DOM 樹 -> 比較兩棵虛擬 DOM 樹的差異 -> 把差異應用到真正的 DOM 樹上。

頁面渲染的具體流程是:在渲染層,宿主環境會把 WXML 轉化成對應的 JS 對象,在邏輯層發生數據變更的時候,我們需要通過宿主環境提供的 setData 方法把數據從邏輯層傳遞到渲染層,再經過對比前后差異,把差異應用在原來的Dom樹上,渲染出正確的UI界面。
(1)在渲染層把 WXML 轉化成對應的 JS 對象。
(2)在邏輯層發生數據變更的時候,通過宿主環境提供的 setData 方法把數據從邏輯層傳遞到 Native,再轉發到渲染層。
(3)經過對比前后差異,把差異應用在原來的 DOM 樹上,更新界面。
我們通過把 WXML 轉化為數據,通過 Native 進行轉發,來實現邏輯層和渲染層的交互和通信。
3、雙線程模型設計的好處
雙線程模型是小程序框架與業界大多數前端 Web 框架不同之處。基於這個模型,可以更好地管控以及提供更安全的環境。缺點是帶來了無處不在的異步問題(任何數據傳遞都是線程間的通信,也就是都會有一定的延時),不過小程序在框架層面已經封裝好了異步帶來的時序問題。
為什么要這樣設計呢,前面也提到了管控和安全,為了解決這些問題,我們需要阻止開發者使用一些,例如瀏覽器的window對象,跳轉頁面、操作DOM、動態執行腳本的開放性接口。
我們可以使用客戶端系統的 JavaScript 引擎(iOS 下的 JavaScriptCore 框架,安卓下騰訊 x5 內核提供的 JsCore 環境),這個沙箱環境只提供純 JavaScript 的解釋執行環境,沒有任何瀏覽器相關接口,這就是小程序雙線程模型的由來。
三、組件系統
我們知道小程序是有自己的組件的,這些基本組件就是基於 Exparser 框架。 Exparser 基於 WebComponents 的 ShadowDOM 模型,但是不依賴瀏覽器的原生支持,而且可在 純 JS 環境中運行。
1、Exparser框架
Exparser是微信小程序的組件組織框架,內置在小程序基礎庫中,為小程序的各種組件提供基礎的支持。小程序內的所有組件,包括內置組件和自定義組件,都由Exparser組織管理。
Exparser的主要特點包括以下幾點:
(1)基於Shadow DOM模型:模型上與WebComponents的ShadowDOM高度相似,但不依賴瀏覽器的原生支持,也沒有其他依賴庫;實現時,還針對性地增加了其他API以支持小程序組件編程。
(2)可在純JS環境中運行:這意味着邏輯層也具有一定的組件樹組織能力。
(3)高效輕量:性能表現好,在組件實例極多的環境下表現尤其優異,同時代碼尺寸也較小。
小程序中,所有節點樹相關的操作都依賴於Exparser,包括WXML到頁面最終節點樹的構建、createSelectorQuery調用和自定義組件特性等。
2、內置組件
基於Exparser框架,小程序內置了一套組件,提供了視圖容器類、表單類、導航類、媒體類、開放類等幾十種組件。有了這么豐富的組件,再配合WXSS,可以搭建出任何效果的界面。在功能層面上,也滿足絕大部分需求。
3、原生組件
在內置組件中,有一些組件並不完全在 Exparser 的渲染體系下,而是由客戶端原生參與組件的渲染。比如說 Map 組件,它渲染的層級比在 WebView 層渲染的普通組件要高。
四、運行機制
(1)啟動
熱啟動:假如用戶已經打開過某小程序,然后在一定時間內再次打開該小程序,此時無需重新啟動,只需將后台態的小程序切換到前台,這個過程就是熱啟動;
冷啟動:用戶首次打開或小程序被微信主動銷毀后再次打開的情況,此時小程序需要重新加載啟動,即冷啟動。
小程序沒有重啟的概念
當小程序進入后台,客戶端會維持一段時間的運行狀態,超過一定時間后(目前是5分鍾)會被微信主動銷毀
當短時間內(5s)連續收到兩次以上收到系統內存告警,會進行小程序的銷毀

(2)銷毀
只有當小程序進入后台一定時間,或者系統資源占用過高,才會被真正的銷毀。
(3)更新機制
開發者在后台發布新版本之后,無法立刻影響到所有現網用戶,但最差情況下,也在發布之后 24 小時之內下發新版本信息到用戶。
小程序每次冷啟動時,都會檢查是否有更新版本,如果發現有新版本,將會異步下載新版本的代碼包,並同時用客戶端本地的包進行啟動,即新版本的小程序需要等下一次冷啟動才會應用上。
所以如果想讓用戶使用最新版本的小程序,可以利用 wx.getUpdateManager 做個檢查更新的功能:
checkNewVersion() { const updateManager = wx.getUpdateManager(); updateManager.onCheckForUpdate((res) => { console.log('hasUpdate', res.hasUpdate); // 請求完新版本信息的回調
if (res.hasUpdate) { updateManager.onUpdateReady(() => { this.setData({ hasNewVersion: true }); }); } }); }
五、小程序的技術實現
小程序的UI視圖和邏輯處理是用多個webview實現的,邏輯處理的JS代碼全部加載到一個Webview里面,稱之為AppService,整個小程序只有一個,並且整個生命周期常駐內存,而所有的視圖(wxml和wxss)都是單獨的Webview來承載,稱之為AppView。
所以一個小程序打開至少就會有2個webview進程,正式因為每個視圖都是一個獨立的webview進程,考慮到性能消耗,小程序不允許打開超過5個層級的頁面,當然同是也是為了體驗更好。
1、AppService
可以理解AppService即一個簡單的頁面,主要功能是負責邏輯處理部分的執行,底層提供一個WAService.js的文件來提供各種api接口,主要是以下幾個部分:
消息通信封裝為WeixinJSBridge(開發環境為window.postMessage, IOS下為WKWebview的window.webkit.messageHandlers.invokeHandler.postMessage,android下用WeixinJSCore.invokeHandler)
日志組件Reporter封裝
wx對象下面的api方法
全局的App,Page,getApp,getCurrentPages等全局方法
還有就是對AMD模塊規范的實現
然后整個頁面就是加載一堆JS文件,包括小程序配置config,上面的WAService.js(調試模式下有asdebug.js),剩下就是我們自己寫的全部的js文件,一次性都加載。
2、線上環境
而在上線后是應用部分會打包為2個文件,名稱app-config.json和app-service.js,然后微信會打開webview去加載。線上部分應該是微信自身提供了相應的模板文件,在壓縮包里沒有找到。
WAService.js(底層支持)
app-config.json(應用配置)
app-service.js(應用邏輯)
然后運行在JavaScriptCore引擎里面。
3、AppView
這里可以理解為h5的頁面,提供UI渲染,底層提供一個WAWebview.js來提供底層的功能,具體如下:
消息通信封裝為WeixinJSBridge(開發環境為window.postMessage, IOS下為WKWebview的window.webkit.messageHandlers.invokeHandler.postMessage,android下用WeixinJSCore.invokeHandler)
日志組件Reporter封裝
wx對象下的api,這里的api跟WAService里的還不太一樣,有幾個跟那邊功能差不多,但是大部分都是處理UI顯示相關的方法
小程序組件實現和注冊
VirtualDOM,Diff和Render UI實現
頁面事件觸發
在此基礎上,AppView有一個html模板文件,通過這個模板文件加載具體的頁面,這個模板主要就一個方法,$gwx,主要是返回指定page的VirtualDOM,而在打包的時候,會事先把所有頁面的WXML轉換為ViirtualDOM放到模板文件里,而微信自己寫了2個工具wcc(把WXML轉換為VirtualDOM)和wcsc(把WXSS轉換為一個JS字符串的形式通過style標簽append到header里)。
4、Service和View通信
使用消息publish和subscribe機制實現兩個Webview之間的通信,實現方式就是統一封裝一個WeixinJSBridge對象,而不同的環境封裝的接口不一樣,具體實現的技術如下:
(1)windows環境
通過window.postMessage實現(使用chrome擴展的接口注入一個contentScript.js,它封裝了postMessage方法,實現webview之間的通信,並且也它通過chrome.runtime.connect方式,也提供了直接操作chrome native原生方法的接口)
發送消息:window.postMessage(data, ‘*’); // data里指定 webviewID 接收消息:window.addEventListener(‘message’, messageHandler);// 消息處理並分發,同樣支持調用nwjs的原生能力。
(2)IOS
通過 WKWebview的window.webkit.messageHandlers.NAME.postMessage實現微信navite代碼里實現了兩個handler消息處理器:
invokeHandler: 調用原生能力
publishHandler: 消息分發
六、性能優化
主要的優化策略可以歸納為三點:
(1)精簡代碼,降低WXML結構和JS代碼的復雜性;
(2)合理使用setData調用,減少setData次數和數據量;
(3)必要時使用分包優化。
1、setData 工作原理
小程序的視圖層目前使用 WebView 作為渲染載體,而邏輯層是由獨立的 JavascriptCore 作為運行環境。
在架構上,WebView 和 JavascriptCore 都是獨立的模塊,並不具備數據直接共享的通道。
當前,視圖層和邏輯層的數據傳輸,實際上通過兩邊提供的 evaluateJavascript 所實現。即用戶傳輸的數據,需要將其轉換為字符串形式傳遞,同時把轉換后的數據內容拼接成一份 JS 腳本,再通過執行 JS 腳本的形式傳遞到兩邊獨立環境。
而 evaluateJavascript 的執行會受很多方面的影響,數據到達視圖層並不是實時的。
2、常見的 setData 操作錯誤
(1)頻繁的去 setData
在我們分析過的一些案例里,部分小程序會非常頻繁(毫秒級)的去setData,其導致了兩個后果:Android下用戶在滑動時會感覺到卡頓,操作反饋延遲嚴重,因為 JS 線程一直在編譯執行渲染,未能及時將用戶操作事件傳遞到邏輯層,邏輯層亦無法及時將操作處理結果及時傳遞到視圖層;渲染有出現延時,由於 WebView 的 JS 線程一直處於忙碌狀態,邏輯層到頁面層的通信耗時上升,視圖層收到的數據消息時距離發出時間已經過去了幾百毫秒,渲染的結果並不實時;
(2)每次 setData 都傳遞大量新數據
由setData的底層實現可知,我們的數據傳輸實際是一次 evaluateJavascript 腳本過程,當數據量過大時會增加腳本的編譯執行時間,占用 WebView JS 線程
(3)后台態頁面進行setData
當頁面進入后台態(用戶不可見),不應該繼續去進行setData,后台態頁面的渲染用戶是無法感受的,另外后台態頁面去setData也會搶占前台頁面的執行。
參考文章:
