原文來自公眾號-騰訊IMWeb前端團隊,@hope
1. 小程式發展史
1.1 Native App
在智慧機剛興起的時代,網路還不是很發達,網頁瀏覽速度也很慢,以文字為主。市面上的應用以 Native App 為主。
Native App 是基於 iOS 或者安卓的原生應用,特點是開發成本高,迭代慢,但是效能和體驗很好,訊息推送及時,比如 qq,微信等。
1.2 H5
2014 年 HTML5 完成標準定製,他的設計目的是為了在移動裝置上支援多媒體,引入了 Video、Audio 等技術。
在網頁上瀏覽影片變得很方便,特點是開發和釋出成本低,開啟方便,無需下載到本地,但是效能受瀏覽器的處理能力的限制,相較於原生 App 來說差了一些,訊息推送不及時。
1.3 Hybrid App
Hybrid App 就是混合式的 App,也就是在移動端原生應用的基礎上,透過 JSBrdige 等方法,訪問原生應用的 API 進行 JS 的互動,並透過 WebView 等技術實現 HTML 與 CSS 的渲染。
WebView 可以理解為嵌套了一個瀏覽器核心(比如 webkit)的移動端元件。
這種技術實現的應用一般都是跨平臺的,並且維護起來比較容易,效能介於 H5 和原生應用之間。
1.4 小程式
小程式是一種不需要下載安裝即可使用的應用,它實現了應用 “觸手可及” 的夢想,使用者掃一掃或者搜一下即可開啟應用。也體現了 “用完即走” 的理念,使用者不用關心是否安裝太多應用的問題。應用將無處不在,隨時可用,但又無需安裝解除安裝。—— 百度百科
自 2017 年 1 月微信小程式正式釋出以來,目前網際網路上已經有多種小程式:
微信小程式 |
百度智慧小程式 |
支付寶小程式 |
QQ 小程式 |
|
|
|
|
2017.1 |
2018.7 |
2018.9 |
2019.6 |
基於小程式幾乎相同的技術原理,以及小程式的方便快捷的特性,還衍生出了多款小程式,比如抖音小程式、快手小程式、京東小程式、美團小程式等,幫助各大廠商更好的為使用者提供便捷的服務。
2018 年微信小程式 “跳一跳” 爆火,記得當年食堂排隊打飯的時候很多同學都在玩,助力了微信小程式在使用者中的擴張,也激發了其他廠商開發小程式的熱潮。
2. 原理分析
2.1 雙執行緒模型
無論是微信小程式還是支付寶小程式還是百度智慧小程式等等,他們的總體架構都是基於雙執行緒的。
其中用於處理業務邏輯的 JS 程式碼執行在單獨的執行緒裡,渲染層(template、css)則執行在另外一個單獨的執行緒裡。
以微信小程式為例:
雙執行緒模型不同於單執行緒模型,邏輯層與渲染層的資料互動需要透過 JSBridge,二者是透過釋出訂閱,基於當前比較比較著名的 MVVM,來實現資料的雙向繫結的,從而實現資料通訊。
這樣我們在微信小程式中透過在邏輯層中 setData 來改變 Model 層的資料就能夠實現檢視資料的非同步更新。
以下是微信小程式的生命週期:
2.2 整體架構
注:以下所有內容均圍繞微信開發者工具展開。
開啟微信開發者工具的原始碼,他是基於 NW.js 執行的,所以下圖中的 package.nw 就是我們要重點鑽研的物件:
這裡面有很多程式碼,都是經過混淆與壓縮的,將程式碼在 VSCode 中開啟,並安裝 Prettier - Code formatter 外掛可以實現對原始碼的格式化。但此時程式碼中已經不具有語義化的變量了,只能透過 API 大致推斷程式碼是幹什麼的。
原始碼中有一個 vendor 資料夾是值得注意的,透過它可以快速新建一個示例專案,同時裡面有一個十分重要的 2.17.0.wxvpkg 包,它是微信小程式的基礎庫,包含了下文所提及的 WebService 與 WebView 等邏輯層與渲染層的處理。
2.2.1 WAWebview
小程式檢視層基礎庫,提供檢視層的基礎能力:
var __wxLibrary = {
fileName: 'WAWebview.js',
envType: 'WebView',
contextType: 'others',
execStart: Date.now()
};
var __WAWebviewStartTime__ = Date.now();
var __libVersionInfo__ = {
"updateTime": "2020.4.4 10:25:02",
"version": "2.10.4"
};
/**
* core-js 模組
*/
!function(n, o, Ye) {
...
}, function(e, t, i) {
var n = i(3),
o = "__core-js_shared__",
r = n[o] || (n[o] = {});
e.exports = function(e) {
return r[e] || (r[e] = {})
}
...
}(1, 1);
var __wxConfig;
var __wxTest__ = false;
var wxRunOnDebug = function(e) {
e()
};
/**
* 基礎模組
*/
var Foundation = function(i) {
...
}]).default;
var nativeTrans = function(e) {
...
}(this);
/**
* 訊息通訊模組
*/
var WeixinJSBridge = function(e) {
...
}(this);
/**
* 監聽 nativeTrans 相關事件
*/
!function() {
...
}();
/**
* 解析配置
*/
!function(r) {
...
__wxConfig = _(__wxConfig), __wxConfig = v(__wxConfig), Foundation.onConfigReady(function() {
m()
}), n ? __wxConfig.__readyHandler = A : d ? Foundation.onBridgeReady(function() {
WeixinJSBridge.on("onWxConfigReady", A)
}) : Foundation.onLibraryReady(A)
}(this);
/**
* 異常捕獲(error、onunhandledrejection)
*/
!function(e) {
function t(e) {
Foundation.emit("unhandledRejection", e) || console.error("Uncaught (in promise)", e.reason)
}
"object" == typeof e && "function" == typeof e.addEventListener ? (e.addEventListener("unhandledrejection", function(e) {
t({
reason: e.reason,
promise: e.promise
}), e.preventDefault()
}), e.addEventListener("error", function(e) {
var t;
t = e.error, Foundation.emit("error", t) || console.error("Uncaught", t), e.preventDefault()
})) : void0 === e.onunhandledrejection && Object.defineProperty(e, "onunhandledrejection", {
value: function(e) {
t({
reason: (e = e || {}).reason,
promise: e.promise
})
}
})
}(this);
/**
* 原生緩衝區
*/
var NativeBuffer = function(e) {
...
}(this);
var WeixinNativeBuffer = NativeBuffer;
var NativeBuffer = null;
/**
* 日誌模組:wxConsole、wxPerfConsole、wxNativeConsole、__webviewConsole__
*/
var wxConsole = ["log", "info", "warn", "error", "debug", "time", "timeEnd", "group", "groupEnd"].reduce(function(e, t) {
return e[t] = function() {}, e
}, {});
var wxPerfConsole = ["log", "info", "warn", "error", "time", "timeEnd", "trace", "profile", "profileSync"].reduce(function(e, t) {
return e[t] = function() {}, e
}, {});
var wxNativeConsole = function(i) {
...
}([function(e, t, i) {
...
}]).default;
var __webviewConsole__ = function(i) {
...
}([function(e, t, i) {
...
}]);
/**
* 上報模組
*/
var Reporter = function(i) {
...
}([function(e, L, O) {
...
}]).default;
var Perf = function(i) {
...
}([function(e, t, i) {
...
}]).default;
/**
* 檢視層 API
*/
var __webViewSDK__ = function(i) {
...
}([function(e, L, O) {
...
}]).default;
var wx = __webViewSDK__.wx;
/**
* 元件系統
*/
var exparser = function(i) {
...
}([function(e, t, i) {
...
}]);
/**
* 框架粘合層
*
* 使用 exparser.registerBehavior 和 exparser.registerElement 方法註冊內建元件
* 轉發 window、wx 物件上到事件轉發到 exparser
*/
!function(i) {
...
}([function(e, t) {
...
}, function(e, t) {}, , function(e, t) {}]);
/**
* Virtual DOM
*/
var __virtualDOMDataThread__ = false;
var __virtualDOM__ = function(i) {
...
}([function(e, t, i) {
...
}]);
/**
* __webviewEngine__
*/
var __webviewEngine__ = function(i) {
...
}([function(e, t, i) {
...
}]);
/**
* 注入預設樣式到頁面
*/
!function() {
...
function e() {
var e = i('...');
__wxConfig.isReady ? void0 !== __wxConfig.theme && i(t, e.nextElementSibling) : __wxConfig.onReady(function() {
void0 !== __wxConfig.theme && i(t, e.nextElementSibling)
})
}
window.document && "complete" === window.document.readyState ? e() : window.onload = e
}();
var __WAWebviewEndTime__ = Date.now();
typeof __wxLibrary.onEnd === 'function' && __wxLibrary.onEnd();
__wxLibrary = undefined;
WAWebview 主要由以下幾個部分元件:
- Foundation:基礎模組
- WeixinJSBridge:訊息通訊模組
- exparser:元件系統模組
- __virtualDOM__:Virtual DOM 模組
- __webViewSDK__:WebView SDK 模組
- Reporter:日誌上報模組 (異常和效能統計資料)
2.2.2 WAService
小程式邏輯層基礎庫,提供邏輯層基礎能力:
var __wxLibrary = {
fileName: 'WAService.js',
envType: 'Service',
contextType: 'App:Uncertain',
execStart: Date.now()
};
var __WAServiceStartTime__ = Date.now();
(function(global) {
var __exportGlobal__ = {};
var __libVersionInfo__ = {
"updateTime": "2020.4.4 10:25:02",
"version": "2.10.4"
};
var __Function__ = global.Function;
var Function = __Function__;
/**
* core-js 模組
*/
!function(r, o, Ke) {
}(1, 1);
var __wxTest__ = false;
var wxRunOnDebug = function(e) {
e()
};
var __wxConfig;
/**
* 基礎模組
*/
var Foundation = function(n) {
...
}([function(e, t, n) {
...
}]).default;
var nativeTrans = function(e) {
...
}(this);
/**
* 訊息通訊模組
*/
var WeixinJSBridge = function(e) {
...
}(this);
/**
* 監聽 nativeTrans 相關事件
*/
!function() {
...
}();
/**
* 解析配置
*/
!function(i) {
...
}(this);
/**
* 異常捕獲(error、onunhandledrejection)
*/
!function(e) {
...
}(this);
/**
* 原生緩衝區
*/
var NativeBuffer = function(e) {
...
}(this);
WeixinNativeBuffer = NativeBuffer;
NativeBuffer = null;
var wxConsole = ["log", "info", "warn", "error", "debug", "time", "timeEnd", "group", "groupEnd"].reduce(function(e, t) {
return e[t] = function() {}, e
}, {});
var wxPerfConsole = ["log", "info", "warn", "error", "time", "timeEnd", "trace", "profile", "profileSync"].reduce(function(e, t) {
return e[t] = function() {}, e
}, {});
var wxNativeConsole = function(n) {
...
}([function(e, t, n) {
...
}]).default;
/**
* Worker 模組
*/
var WeixinWorker = function(e) {
...
}(this);
/**
* JSContext
*/
var JSContext = function(n) {
...
}([
...
}]).default;
var __appServiceConsole__ = function(n) {
...
}([function(e, N, R) {
...
}]).default;
var Protect = function(n) {
...
}([function(e, t, n) {
...
}]);
var Reporter = function(n) {
...
}([function(e, N, R) {
...
}]).default;
var __subContextEngine__ = function(n) {
...
}([function(e, t, n) {
...
}]);
var __waServiceInit__ = function() {
...
}
function __doWAServiceInit__() {
var e;
"undefined" != typeof wx && wx.version && (e = wx.version), __waServiceInit__(), e && "undefined" != typeof __exportGlobal__ && __exportGlobal__.wx && (__exportGlobal__.wx.version = e)
}
__subContextEngine__.isIsolateContext();
__subContextEngine__.isIsolateContext() || __doWAServiceInit__();
__subContextEngine__.initAppRelatedContexts(__exportGlobal__);
})(this);
var __WAServiceEndTime__ = Date.now();
typeof __wxLibrary.onEnd === 'function' && __wxLibrary.onEnd();
__wxLibrary = undefined;
WAService 基本組成:
- Foundation:基礎模組
- WeixinJSBridge:訊息通訊模組
- WeixinNativeBuffer:原生 Buffer
- WeixinWorker:Worker 執行緒
- JSContext:JS Engine Context
- Protect:JS 保護的物件
- __subContextEngine__:提供 App、Page、Component、Behavior、getApp、getCurrentPages 等方法
2.2.3 虛擬 DOM
微信小程式在 WAService 裡面實現了小程式的 __virtualDOM__,透過 __virtualDOM__ 模組,可以實現 JS 物件到 DOM 物件的對映。
但是這個虛擬 DOM 透過 diff 和 patch 後並不是轉換成原生的 DOM 元素,而是微信小程式裡面自定義的 DOM 元素,這些 DOM 元素的操作透過 Exparser 模組來統一管理:
在 WAWebview 中包含了所有的 wx 自定義標籤:
同時,__virtualDOM__ 模組提供了很多的基礎 API,比如:
- getAll:獲取所有 Node
- getNodeById:根據 Id 獲取 Node
- getNodeId:獲取 NodeId
- addNode:新增節點
- removeNode:刪除節點
- getExparser:獲取 Exparser 物件(基於 WebComponent 的 shadow DOM 模型,可以在 JS 環境中執行,所有與節點樹相關的操作都依賴於他)
- ...
(更多的 API 定義可以在 WAService.js 裡面去查詢)
2.2.4 WeiXinJSBridge
WeixinJSBridge 提供了檢視層 JS 與 Native、檢視層與邏輯層之間訊息通訊的機制,提供瞭如下幾個方法:
裡面最重要的便是 on 和 invoke,透過 on 來註冊事件,透過 invoke 來觸發相應的事件。
2.3 微信開發者工具
微信開發者工具中的小程式是跑在 NW.js 中的,這裡是他的官方 API 文件:https://nwjs.readthedocs.io/en/latest/
他是基於 Chromium 和 Node.js 的,因此我們編譯後的虛擬 DOM 轉換成真實 DOM 後,透過他來執行。
2.3.1 一些反編譯技巧
我們可以透過開發者工具,在 Devtools 裡輸入 help 可以得到很多指令:
其中比較有用的是 openVendor。這個函式可以開啟當前專案的原始碼,其實也就是包含了 wcc 和 wcsc 編譯工具的一個資料夾:
有了這些檔案之後,對我們之後的分析會很有幫助。
我們可以將這些檔案複製到一個單獨的目錄,在 VSCode 中開啟該專案,並安裝以下外掛:
這個外掛可以將微信開發者工具中的所有以 .wxvpkg 結尾的檔案進行解壓縮。
同時透過他來將 quickstart 中的 miniProgramJs.wxvpkg 進行解壓,得到我們在開發者工具中的原始碼檔案。
2.3.2 編譯原理
2.3.2.1 wcc 編譯 wxml
微信小程式提供了 wcc 工具來編譯 wxml 程式碼。透過上面得到的程式碼,我們可以實現對 wxml 的編譯,以開發者工具建立的 Demo 專案中的首頁為例:
<view class="container">
<view class="userinfo">
<block wx:if="{{canIUseOpenData}}">
<view class="userinfo-avatar" bindtap="bindViewTap">
<open-data type="userAvatarUrl"></open-data>
</view>
<open-data type="userNickName"></open-data>
</block>
<block wx:elif="{{!hasUserInfo}}">
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 獲取頭像暱稱 </button>
<button wx:elif="{{canIUse}}" open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 獲取頭像暱稱 </button>
<view wx:else> 請使用1.4.4及以上版本基礎庫 </view>
</block>
<block wx:else>
<image bindtap="bindViewTap" class="userinfo-avatar" class="lazyload" data-src="{{userInfo.avatarUrl}}" src="/assets/loading.gif" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</block>
</view>
<view class="usermotto">
<text class="user-motto">{{motto}}</text>
</view>
</view>
經過以下指令編譯:
./wcc ./quickstart/miniProgramJs.unpack/pages/index/index.wxml > index.js
會得到 JS 描述檔案:
它會宣告一個 $gwx 函式,透過它可以得到 Virtual DOM。接著我們在這個檔案裡新增幾行程式碼去呼叫它,並透過 Node.js 或者 NW.js 執行這個檔案:
var data = $gwx('./quickstart/miniProgramJs.unpack/pages/index/index.wxml')();
console.log(JSON.stringify(data, null, 2));
可以得到我們想要的最終的 Virtual DOM 結構:
{
"tag": "wx-page",
"children": [
{
"tag": "wx-view",
"attr": {
"class": "container"
},
"children": [
{
"tag": "wx-view",
"attr": {
"class": "userinfo"
},
"children": [
{
"tag": "wx-view",
"attr": {},
"children": [
" 請使用1.4.4及以上版本基礎庫 "
],
"raw": {},
"generics": {}
}
],
"raw": {},
"generics": {}
},
{
"tag": "wx-view",
"attr": {
"class": "usermotto"
},
"children": [
{
"tag": "wx-text",
"attr": {
"class": "user-motto"
},
"children": [
""
],
"raw": {},
"generics": {}
}
],
"raw": {},
"generics": {}
}
],
"raw": {},
"generics": {}
}
]
}
然後透過 window.exparser.registerElemtent 方法將這些 tag 轉換成真實 DOM:
比如說,以上的 wx-text 就會被轉換成類似於以下 DOM:
<span>
<span style="display: none"></span>
<span>{{這裡是具體的文字內容}}</span>
</span>
2.3.2.2 wcsc 編譯 wxss
同樣的,以 Demo 專案裡的 index.wxss 為例,執行一下指令:
./wcsc ./quickstart/miniProgramJs.unpack/pages/index/index.wxss -js -o ./css.js
可以將該檔案從 wxss 格式的內容,轉換成 JS 的內容:
這裡面會生成 setCssToHead 方法,用於將相應的 css 轉換後(如 rpx 轉 px 等等),透過 style 標籤插入到文件的 head 裡面。
2.4 通訊原理
小程式邏輯層和渲染層的通訊會由 Native (微信客戶端)做中轉,邏輯層傳送網路請求也經由 Native 轉發。
檢視層元件:
內建元件中有部分元件是利用到客戶端原生提供的能力,既然需要客戶端原生提供的能力,那就會涉及到檢視層與客戶端的互動通訊。這層通訊機制在 iOS 和安卓系統的實現方式並不一樣,iOS 是利用了 WKWebView 的提供 messageHandlers 特性,而在安卓則是往 WebView 的 window 物件注入一個原生方法,最終會封裝成 WeiXinJSBridge 這樣一個相容層,主要提供了呼叫(invoke)和監聽(on)這兩種方法。
邏輯層介面:
邏輯層與客戶端原生通訊機制與渲染層類似,不同在於,iOS 平臺可以往 JavaScriptCore 框架注入一個全域性的原生方法,而安卓方面則是跟渲染層一致的。
無論是檢視層還是邏輯層,開發者都是間接地呼叫到與客戶端原生通訊的底層介面。一般微信小程式會對邏輯層介面做層封裝後,才暴露給開發者,封裝的細節可能是統一入參、做些引數校驗、相容各平臺或版本問題等等。
2.5 啟動機制
小程式有冷啟動與熱啟動兩種方式:
- 假如使用者已經開啟過某小程式,然後在一定時間內再次開啟該小程式,此時無需重新啟動,只需將後臺態的小程式切換到前臺,這個過程就是熱啟動。
- 冷啟動指的是使用者首次開啟或小程式被微信主動銷燬後再次開啟的情況,此時小程式需要重新載入啟動。
小程式沒有重啟的概念:
- 當小程式進入後臺,客戶端會維持一段時間的執行狀態,超過一定時間後(目前是 5 分鐘)會被微信主動銷燬。
- 當短時間內(5s)連續收到兩次以上收到系統記憶體告警,會進行小程式的銷燬。
啟動流程:
3. 總結
- 小程式擁有接近原生 App 的體驗。
- 小程式並不是真正的 “無需下載”,只是小程式的體積很小,在當今高速的網路環境下能夠快速下載,使用者感知不到,更確切的來說是 “無感下載”。
- 基於移動端佈局的侷限性,可以高效且簡單的開發,迭代快速。
- 小程式是雙執行緒模型,邏輯層和渲染層分別執行在不同的執行緒中,透過 JSBridge 進行通訊。
- 在小程式裡有實現專門的 JSBridge 來實現 JS 和 Native 的雙向呼叫。
- wxml 檔案透過 wcc 編譯:wxml => JS => VirtualDOM。
- wxss 檔案透過 wcsc 編譯:wxss => JS => style。
- 小程式其實也是一種 hybrid 技術,但是他圍繞宿主應用,實現了更為強大的生態,提供更為便捷的服務。
4. 參考文獻
- 微信小程式技術原理分析(https://zhaomenghuan.js.org/blog/wechat-miniprogram-principle-analysis.html#%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B)
- 淺談小程式的歷史發展和現狀以及一些多端解決方案(https://juejin.cn/post/6948602456003575821)
- 阿拉丁研究院 - 網際網路行業:2020 年小程式網際網路發展白皮書(https://pdf.dfcfw.com/pdf/H3_AP202101151450898869_1.pdf?1610717870000.pdf)
- 一篇文章瞭解 JsBridge(https://juejin.cn/post/6844903567560540173)
5.原文地址
https://mp.weixin.qq.com/s/5nQqBFFWwxtcf8S2Ba9PRA