微信小程序基礎(chǔ)架構(gòu)淺析
微信小程序,簡(jiǎn)稱小程序,英文 mini program。是一種不需要下載安裝即可在微信中使用的應(yīng)用,用戶掃描小程序碼或搜索小程序即可打開,觸手可及,用完即走,不用關(guān)心是否安裝太多應(yīng)用的問題。
小程序技術(shù)演進(jìn)
內(nèi)部開放微信原生能力
使用 WeixinJSBridge 預(yù)覽圖片
此類 API 最初是提供給騰訊內(nèi)部一些業(yè)務(wù)使用,很多外部開發(fā)者發(fā)現(xiàn)了之后,依葫蘆畫瓢地使用了,逐漸成為微信中網(wǎng)頁(yè)開發(fā)的事實(shí)標(biāo)準(zhǔn)。
JS-SDK 發(fā)布
2015 年初,微信發(fā)布了一整套網(wǎng)頁(yè)開發(fā)工具包,稱之為 JS-SDK,開放了拍攝、錄音、語音識(shí)別、二維碼、地圖、支付、分享、卡券等幾十個(gè) API。讓所有開發(fā)者都可以使用到微信的原生能力。
使用 JS-SDK 調(diào)用圖片預(yù)覽組件
JS-SDK 解決了移動(dòng)網(wǎng)頁(yè)使用微信能力不足的問題,通過暴露微信的接口使得 Web 開發(fā)者能夠擁有更多的能力,然而在更多的能力之外,JS-SDK 的模式并沒有解決使用移動(dòng)網(wǎng)頁(yè)遇到的體驗(yàn)不良的問題。
JS-SDK 的不足
用戶在訪問網(wǎng)頁(yè)的時(shí)候,在瀏覽器開始顯示之前都會(huì)有一個(gè)白屏的過程,在移動(dòng)端,受限于設(shè)備性能和網(wǎng)絡(luò)速度,白屏?xí)用黠@。除了白屏,影響 Web 體驗(yàn)的問題還有缺少操作的反饋,主要表現(xiàn)在兩個(gè)方面:頁(yè)面切換的生硬和點(diǎn)擊的遲滯感。
加載白屏,切換不流暢
此外一些開發(fā)者會(huì)使用 JS-SDK 做一些,比如假紅包,偽造的官方活動(dòng)等。并利用 JS-SDK 的分享能力變相的去裂變分享到各個(gè)群或者朋友圈,由于 JS-SDK 是根據(jù)域名來賦予 api 權(quán)限的,運(yùn)營(yíng)人員封了一個(gè)域名后,他們立馬用別的域名又繼續(xù)做壞,要知道注冊(cè)一個(gè)新的域名的成本是很低的。
那么小程序是通過怎樣的設(shè)計(jì)來改進(jìn) JS-SDK 的體驗(yàn)和管控上的不足?
小程序雙線程架構(gòu)
具體實(shí)現(xiàn)上小程序采用了類 web + 離線包的形式。開發(fā)上與 web 類似,門檻較低,開發(fā)效率較高。離線下載和頁(yè)面預(yù)渲染功能增強(qiáng)了用戶體驗(yàn),提升了加載速度,解決了 JS-SDK 加載白屏的問題 1。小程序提供了云端更新離線包的功能,可動(dòng)態(tài)更新頁(yè)面,相對(duì)于 app 的更新和發(fā)布更為靈活。此外,小程序在離線包的基礎(chǔ)上對(duì)切換動(dòng)畫進(jìn)行優(yōu)化,降低了切換頁(yè)面導(dǎo)致的遲滯感,緩解了切換不流暢的問題 2。
小程序web+離線包模式
小程序在架構(gòu)方面最大的特點(diǎn)是采用了雙線程的開發(fā)模式,隔離了 JS 邏輯和 UI 渲染。小程序的渲染層和邏輯層分別由 2 個(gè)線程管理:渲染層的界面使用了 WebView 進(jìn)行渲染,邏輯層采用 JsCore 線程運(yùn)行 JS 腳本。
邏輯層:創(chuàng)建一個(gè)單獨(dú)的線程去執(zhí)行 JavaScript,在這個(gè)環(huán)境下執(zhí)行的都是有關(guān)小程序業(yè)務(wù)邏輯的代碼;
渲染層:界面渲染相關(guān)的任務(wù)全都在 WebView 線程里執(zhí)行,通過邏輯層代碼去控制渲染哪些界面。一個(gè)小程序存在多個(gè)界面,所以渲染層存在多個(gè) WebView 線程;
通信:這兩個(gè)線程的通信會(huì)經(jīng)由微信客戶端(下文中也會(huì)采用 Native 來代指微信客戶端)做中轉(zhuǎn),邏輯層發(fā)送網(wǎng)絡(luò)請(qǐng)求也經(jīng)由 Native 轉(zhuǎn)發(fā),小程序的通信模型下圖所示。
小程序雙線程架構(gòu)
JS 邏輯層運(yùn)行在 JSCore 中,并沒有一個(gè)完整瀏覽器對(duì)象,因而缺少相關(guān)的 DOM API 和 BOM API,無法操作頁(yè)面元素,能達(dá)到管控的目的,但也限制了開發(fā)者的權(quán)限:
- 不允許開發(fā)者把頁(yè)面跳轉(zhuǎn)到其他在線網(wǎng)頁(yè)
- 不允許開發(fā)者直接訪問 DOM
- 不允許開發(fā)者隨意使用 window 上的某些未知的可能有危險(xiǎn)的 API
這樣的邏輯層與 UI 層的隔離,加上小程序的審核和舉報(bào)機(jī)制,使得微信加強(qiáng)對(duì)小程序的管控,解決了問題 3。但這也使得開發(fā)者無法靈活的進(jìn)行頁(yè)面渲染。
小程序頁(yè)面渲染
上面已經(jīng)說了邏輯層無法操作 DOM 變更,那小程序是如何進(jìn)行頁(yè)面的渲染呢?小程序基于數(shù)據(jù)驅(qū)動(dòng)的架構(gòu)模式,基于 Virtual Dom(React 引入,真實(shí) DOM 的一種 JS 描述方式)的概念,業(yè)務(wù)側(cè)只需要改變數(shù)據(jù)即可引起界面變化。其中渲染層提供了帶有數(shù)據(jù)綁定語法的 WXML,邏輯層提供了setData 等等 API,開發(fā)者需要進(jìn)行界面變化時(shí),只需要通過在邏輯層執(zhí)行 setData 把變化的數(shù)據(jù)通過 Native 層傳遞到渲染層,小程序會(huì)進(jìn)行 Dom Diff(DOM 結(jié)構(gòu)對(duì)比并進(jìn)行最小化變更的算法)等流程,最后把正確的結(jié)果更新在 Dom 樹上。
小程序Virtual DOM渲染
完整的通信流程大致如下:
- 邏輯層調(diào)用宿主環(huán)境的 setData 方法。
- 邏輯層將待傳輸數(shù)據(jù)轉(zhuǎn)換成字符串,并拼接到特定的 JS 腳本,最后將數(shù)據(jù)傳輸?shù)戒秩緦印?/li>
- 渲染層接收到后,WebView JS 線程會(huì)對(duì)腳本進(jìn)行編譯,得到待更新數(shù)據(jù)后進(jìn)入渲染隊(duì)列,等待 WebView 線程空閑時(shí)進(jìn)行頁(yè)面渲染。
- WebView 線程開始執(zhí)行渲染時(shí),待更新數(shù)據(jù)會(huì)合并到視圖層保留的原始 data 數(shù)據(jù),并將新數(shù)據(jù)套用在 WXML 片段中得到新的虛擬節(jié)點(diǎn)樹。經(jīng)過新虛擬節(jié)點(diǎn)樹與當(dāng)前節(jié)點(diǎn)樹的 diff 對(duì)比,將差異部分更新到 UI 視圖。同時(shí)將新的節(jié)點(diǎn)樹替換舊節(jié)點(diǎn)樹,用于下一次重渲染。
小程序方案與 React Native 對(duì)比
那么小程序與現(xiàn)有的混合開發(fā)技術(shù)類型的異同點(diǎn)在哪?尤其是與 React Native 的區(qū)別,小程序技術(shù)架構(gòu)為什么沒有使用 React Native?
混合開發(fā)技術(shù)類型
現(xiàn)有的混合開發(fā)類型,基于 UI 渲染的分類來看,主要有兩類:
- 基于 WebView UI 的基礎(chǔ)方案。市面上主流,例如微信 JS-SDK,通過 JSBridge 完成 H5 和 Native 的雙向通訊,從而賦予 H5 一定的原生能力。
- 基于 Native UI 的方案,例如 React-Native、Weex、Flutter 等。在賦予 H5 原生 API 能力的基礎(chǔ)上,進(jìn)一步通過 JSBridge 將 JS 解析成虛擬 DOM 傳遞到 Native,并使用原生渲染。
小程序也屬于類型 1,本次我們主要以類型 2 中的 React Native 作為對(duì)比分析。
React Native 技術(shù)架構(gòu)
框架
React Native 框架主要有三層:
- JS 層:該層提供了各種供開發(fā)者使用的組件以及一些工具庫(kù)(事件分發(fā)等)。
- C++層:主要處理 java/OC 與 JS 的通信(JSBridge)以及執(zhí)行 JavaScript(JS 腳本引擎)。
- Native 層(Object C/Java 層):主要包括 UI 渲染器、網(wǎng)絡(luò)通信等工具庫(kù)。根據(jù)不同操作系統(tǒng)有不同的實(shí)現(xiàn)。
UI 渲染
React Native 基于 react 框架(Virtual Dom)來進(jìn)行 UI 渲染,具體的流程大致如下:
- 首先 JS 層通過 JSX 編寫的 Virtual Dom 來構(gòu)建 Component
- Native 層將其轉(zhuǎn)成真實(shí) DOM 插入到原生 App 的頁(yè)面中。
- 當(dāng)有變更,通過 diff 算法生成差異對(duì)象
- 最終由 Native 層將差異對(duì)象應(yīng)用到原生 App 的頁(yè)面元素上。
通信
React Native 基于 JSCore 實(shí)現(xiàn) js 與 java/oc 交互,具體流程大致如下:
- 把 JSX 代碼解析成 javaScript 代碼
- 讀取 JS 文件,并利用利用 JS 腳本引擎執(zhí)行
- 返回一個(gè)數(shù)組,數(shù)組中會(huì)描述 OC/Java 對(duì)象,描述對(duì)象屬性和所需要執(zhí)行的方法,這樣就能讓這個(gè)對(duì)象設(shè)置屬性,并且調(diào)用方法。
Reactive Native架構(gòu)
React Native 優(yōu)缺點(diǎn)
優(yōu)勢(shì)
- 原生渲染,性能更好,用戶體驗(yàn)較好;
- React 生態(tài)較好,對(duì)前端開發(fā)友好;
- hybrid 技術(shù)跨平臺(tái)開發(fā),成本及難度低于原生;
- 可熱更新,能夠方便迭代。
劣勢(shì)
- 支持的樣式是 CSS 的子集,會(huì)滿足不了 Web 開發(fā)者日漸增長(zhǎng)的需求;
- 現(xiàn)有能力下還存在的一些不穩(wěn)定問題,比如性能、Bug 等;
- 把渲染工作全都交由客戶端原生渲染,會(huì)有更接近原生的體驗(yàn),但實(shí)際上一些簡(jiǎn)單的界面元素使用 Web 技術(shù)渲染完全能勝任;
- React Native 之前爆出了一個(gè)開源協(xié)議問題(Facebook BSD+Patents ,大致內(nèi)容是使用基于 Facebook BSD+Patents 協(xié)議的開源項(xiàng)目的開發(fā)者,未來要是因?yàn)閷@麊栴}與 Facebook 產(chǎn)生糾紛,那么 Facebook 將有權(quán)停止你使用該開源項(xiàng)目),這對(duì)于之后也是存在隱患的。
小程序不選擇 React Native 原因
據(jù)小程序開發(fā)人員告知的原因如下:
- React Native 只支持 CSS 的子集,作為一個(gè)開放的生態(tài),需要告知開發(fā)者哪些 CSS 屬性能用,哪些不能用,這樣的開發(fā)體驗(yàn)較差;(對(duì)應(yīng)上面的劣勢(shì) 1)
- React Native 本身存在一些問題,這些依賴 RN 的修復(fù),同時(shí)這樣就變成太過依賴客戶端發(fā)版本去解決開發(fā)者那邊的 Bug,修復(fù)周期太長(zhǎng)。(對(duì)應(yīng)上面的劣勢(shì) 2)
- React Native 前陣子還搞出了一個(gè)開源協(xié)議問題,來說也是存在隱患的。(對(duì)應(yīng)上面的劣勢(shì) 4)
小程序與 React Native 相同點(diǎn)
- 都具有 hybrid 技術(shù)的優(yōu)點(diǎn):接近原生的體驗(yàn),跨平臺(tái)開發(fā)
- 使用 Web 相關(guān)技術(shù)框架來編寫業(yè)務(wù)代碼,React Native 為 React 框架,小程序?yàn)樾〕绦蜷_發(fā)框架。
- 各自實(shí)現(xiàn)了跨語言通訊方案完成 Native(Java/Objective-c/…)端與 JavaScript(小程序中為渲染層和邏輯層)的通訊
小程序與 React Native 不同點(diǎn)
小程序使用瀏覽器內(nèi)核 WebView 來渲染界面(小部分原生組件由客戶端參與渲染),界面主要由成熟的 Web 技術(shù)渲染,輔之大量的接口提供豐富的客戶端原生能力,而 React Native 是客戶端原生渲染。理論上 React Native 相對(duì)于 WebView 的性能更好,但小程序的類 web 開發(fā)對(duì)開發(fā)來說入門相對(duì)簡(jiǎn)單,像是一種開發(fā)效率與性能的雙刃劍。
小程序開發(fā)注意事項(xiàng)
基于上面的架構(gòu)分析,我們?cè)陂_發(fā)中需要注意是:
- 避免使用操作操作 DOM 的 npm 包。由于邏輯層和渲染層隔離,邏輯層無法操作 DOM/BOM API,所以需要使用 DOM/BOM API 相關(guān)的 npm 包和庫(kù)中不可使用。
- 避免頻繁調(diào)用setData。由于setData中的數(shù)據(jù)不僅需要通過 Native 層傳遞到渲染層,通過 DOM diff 算法等渲染成最終頁(yè)面,所以需要盡量減少setData的使用以避免性能問題。
- 避免setData傳遞大量的新數(shù)據(jù)。數(shù)據(jù)的傳輸會(huì)經(jīng)歷跨線程傳輸和腳本編譯的過程,當(dāng)數(shù)據(jù)量過大,會(huì)增加腳本編譯的執(zhí)行時(shí)間,占用 WebView JS 線程,從而影響到最終的渲染性能。