概要
本文包括的內容:
- 小程式在微信開發者工具中,通過構建生成真正的執行程式碼和安裝包,****.wxapkg。wxml和wxss在構建這一步就被轉換成了html和css(virtual-DOM)。微信開發者工具中可以得到構建指令碼和各個版本的js執行SDK檔案。
- 小程式是以wxapkg檔案的形式下發的,可以拿到wxapkg後解壓拿到執行程式碼。同時解壓微信.ipa拿到當前的js執行SDK檔案,主要是WAWebView.js和WAService.js。
- 小程式在App中執行時的時候分為三個不同的模組,View/Service/Natvie,各司其職。
- 實現過程遇到大量的細節和坑。
介紹小程式原理的文章比較多,這篇講的比較細:微信小程式架構分析。這篇文章的作者也成功的實現了wept,讓小程式執行在自己的webapp裡。
參考最多的是微店的Hera,完成度非常高的小程式框架,能夠將小程式的demo程式碼在web/iOS/android執行起來,而且實現了很多工具。Hera的問題是開發於比較早期的版本,不相容最新的版本了。Hera還有一個問題是他修改了小程式構建之後的目錄結構,採用了service.html作為service部分的入口,跟小程式本身的實現尚有有一些區別。所以Hera只能夠構建執行自己編寫的小程式,不能執行別人編寫的小程式。
我的目標是能夠執行其它人開發的app,意味著我只能通過逆向的方式拿到wxapkg。但是因為拿不到原始碼,所以要儘可能在構建環節跟小程式保持一致。
經過數週的掙扎,目前已經實現了執行官方demo。已經達到"可行"的階段,但是還遠遠談不上“可用”,因為需要實現小程式大量的API,這是個體力活,依賴個人的力量難以完成。
構建
官方demo小程式原先的目錄分為幾類檔案:
- 配置: 在這個目錄中,app.json裡儲存了頁面資訊、tabbar資訊、網路超時配置等等。 config.js儲存了騰訊雲後臺服務解決方案的配置。
- js檔案: js定義了各種函式介面邏輯
- wxml: 類似於html,定義了頁面結構
- wxss: 類似css
demo
├── app.js
├── app.json
├── app.wxss
├── config.js
├── image
│ ├── green_tri.png
│ ├── ...
├── page
│ ├── API
│ │ ├── index.js
│ │ ├── index.json
│ │ ├── index.wxml
│ │ ├── index.wxss
│ │ ├── pages
│ │ │ ├── action-sheet
│ │ │ │ ├── action-sheet.js
│ │ │ │ ├── action-sheet.json
│ │ │ │ ├── action-sheet.wxml
│ │ │ │ └── action-sheet.wxss
│ │ │ ├── ...
│ │ └── resources
│ │ └── ...
│ ├── ...
├── project.config.json
├── util
│ └── util.js
└── vendor
└── qcloud-weapp-client-sdk
├── ...
複製程式碼
經過小程式的開發環境構建後,生成了一個*.wxapkg檔案。 這個檔案可以通過從越獄的iPhone或者root的安卓手機上拿到。有部分人用charles通過https抓包拿到了下載連結,也拿到了包。 拿到後要進行解包。有大神已經通過反編譯安卓apk的方式拿到了解包部分的程式碼,然後用python重寫了一遍。原始碼見wechat-app-unpack。
解包後得到的目錄如下:
1.wxapkg_dir
├── app-config.json
├── app-service.js
├── app-service.js.map
├── image
│ ├── green_tri.png
│ ├── ...
├── page
│ ├── API
│ │ ├── index.html
│ │ ├── pages
│ │ │ ├── action-sheet
│ │ │ │ └── action-sheet.html
│ │ │ ├── ...
│ │ └── resources
│ │ └── kind
│ │ ├── api.png
│ │ ├── ...
│ ├── ...
└── page-frame.html
複製程式碼
轉換過程可以分為三部分:
- 從wxml/wxss到html。 page-frame.html裡定義了所有的virtural-dom,來自所有的wxss和wxml的轉換,檔案非常大。 page.html裡很簡單,就是從page-frame.html裡提取對應的virtual-dom。包括wxss和wxml對應的邏輯。
- app-service.js是從之前所有的js檔案轉換而來。
- app-config.json是從app.json轉換而來。
openVendor
命令可以在小程式中獲取到構建指令碼wcc和wcsc, 以及各個版本小程式的執行SDK ****.wxvpkg,這個SDK也可以用wechat-app-unpack解開,解開后里面就是WAService.js和WAWebView.js等程式碼。
wxml/wxss的構建原理
wxss 轉換成了css,wxml轉換成了inject_js,實際上就是virtual_dom。 是用什麼工具轉換的?小程式裡是叫wcc和wcsc。在開源工具hera自己實現了一套wxss-transpiler和wxml-transpiler。而hera的前身wept是直接使用wcc和wcsc。 我們為了減少維護成本,直接採用wcc和wcsc。 因為我們沒有wcc和wcsc的原始碼,所以只能藉助wxss-transpiler和wxml-transpiler來幫助我們理解wxml/wxss的構建原理。
wxss-transpiler呼叫了一個PostCSS的外掛,用來處理wxss。 PostCSS 提供了一種方式用 JavaScript 程式碼來處理 CSS。它負責把 CSS 程式碼解析成抽象語法樹結構(Abstract Syntax Tree,AST),再交由外掛來進行處理。外掛基於 CSS 程式碼的 AST 所能進行的操作是多種多樣的,比如可以支援變數和混入(mixin),增加瀏覽器相關的宣告字首,或是把使用將來的 CSS 規範的樣式規則轉譯(transpile)成當前的 CSS 規範支援的格式。
wxml-transpiler:實現了一個轉譯器的工作,比如postcss也是轉譯器,包括直譯器(parser),程式碼轉換器(Transformer),程式碼生成器(Generator)。這個是閉源的。
- 根據輸入的列表,讀取所有檔案
- 呼叫VUE的HTML Parser,解析輸入的標籤及屬性,生成一顆DOM樹。vue解決不了的js語言,用babylon庫來處理。(Parse)
- 在解析元件的標籤時,對其上包含的屬性值進行解析(邊Parse邊Transform)
- 根據已有的AST生成JS檔案(Generate)
更多實現原理見這篇文章
模組之前的通訊
小程式在App中執行時的時候分為三個不同的模組,View/Service/Native,各司其職。
View和Service都在WKWebView中執行,互相無法呼叫。他們之間通過Native層通訊。
Native和WebView之間通過webkit.messagehandler和evaluateJavascript互相呼叫。
-
WeixinJSBridge.publish: view和service之間的透傳,在WKWebView之間傳遞訊息。
-
WeixinJSBridge.subscribe: 註冊監聽,監聽view和service之間的訊息呼叫。
-
WeixinJSBridge.invoke: View或者Service傳遞訊息到Native,然後Native使用邏輯呼叫js callback。
-
WeixinJSBridge.on:監聽Native的事件。
如何執行
這裡以iOS為例介紹Native執行過程。安卓類似。
通過解壓微信的ipa可以拿到WAService.js和WAWebView.js兩個基礎庫檔案,檔案內容與hera的service.js/view.js已經有了較大的區別。 我們採用小程式的架構和hera的兩個webView的方案,儘可能模仿小程式的執行過程。
View部分
View部分是比較直觀的,就是WKWebView載入web頁。這裡需要在app-config.json裡讀取到首頁的路徑,然後載入該頁面。這個路徑下的xxx/index.html是無法直接載入的,需要做一些處理。要引入本地執行SDK裡的index.css和view.js, 然後把page-frame.html
裡的virtual-dom全部塞進該頁面。 然後loadHTML即可。
View所有的WKWebView也是要註冊WKUserContentController的,用於通訊。
通過反彙編可以得知這個類在微信中叫YYWAWebView
,呼叫js是直接呼叫-evaluateJavaScript:completionHandler:
方法的。
view.js中包含的邏輯:
- WeixinJSBridge 物件處理訊息通訊: invoke invokeCallbackHandler on publish subscribe subscribe subscribeHandler。
- Reporter 物件
- wxparser 物件,提供 dom 到 wx element 物件之間的對映操作,提供元素操作管理和事件管理功能。
- virtual dom 渲染演算法實現,提供 diff apply render 等方法,該模組介面基本與 virtual-dom 一致,這裡特別的地方在於它所 diff 和生成的並不是原生 DOM,而是各種模擬了 DOM 介面的 wx element 物件
Service部分
Service部分的實現,Hera和微信小程式採取的了不同的架構。
Hera的實現較為簡潔,跟View部分保持一致,採用了WKWebView,呼叫-evaluateJavaScript:completionHandler:
方法執行js,js回撥OC時使用WKScriptMessageHandler。
通過反彙編可以得知這個類在微信中叫WAJSCoreService
,js和OC之間的呼叫是採用JavascriptCore互相呼叫。
JavascriptCore它首先要載入app-config.json並把這個配置賦給一個全域性物件__wxConfig。然後他要載入service.js是SDK基礎,再然後他要載入app-service.js,這裡麵包含了使用者編寫的js邏輯。最後它發出全域性訊息
WeixinJSBridge.publish('serviceReady',,);</script>
喚起小程式app的初始化。
Service.js中包含的邏輯:
- 跟 view.js 一樣的 WeixinJSBridge 相容模組
- view.js 一樣的 Reporter 模組
- appServiceEngine 模組,提供 Page,App,GetApp 介面
Native部分
Native執行的問題比較複雜,因為基本是黑盒,裡面發生了什麼並不知道。 hera的方案在構建過程就已經跟小程式實際的方案有所區別,會提高維護成本。所以我們只能靠猜測來實現Native的執行過程。
Native部分就是作為入口,執行環境,跳轉,轉發訊息,實現擴充套件。包括網路模組/攝像頭/tabbar實現的都是擴充套件。 我們可以得知的是訊息傳遞的協議。然後只能通過safari來除錯webView,根據協議的名稱和出入參來猜測協議的內容。
主要的困難點
- 由於壓縮後的view.js和service.js基本不具備可讀性,而virtual-dom又徹底不具備可讀性...所以就算猜中了協議,最後也往往是魔改。可維護性極差。
- 工作量不小,因為除錯困難,無法閱讀。之後隨著小程式SDK升級,能用多久也不可知。加上也有各種微逆向的操作,小程式封上任何一個介面,都會導致撲街。可持續性極差。總之非常佩服微店的同學能把Hera搞出來:)
小程式的效能啟發
小程式是顛覆我對Web的固有印象,最初還以為是類似weex或者rn的呼叫原生的方式,沒想到幾乎完全是執行在WKWebView之上的。
- 採用virtual DOM,操作JS比html效能高很多,因為是diff後再操作dom,不需要全部重新渲染,快很多。
- WKWebView,滑動60fps,在獨立於App之外的程式執行。
- 部分邏輯Native化,比如收發網路請求,比如資料持久化。 逐步用native元件來替換h5元件。比如tabbar。
- 重用webView以及提前初始化webView等等技巧。
存在的問題:
- 看訊息傳遞的原理就能發現,傳遞的過程太長了,尤其是
setData:
這種傳遞整個model物件,是兩次物件的深拷貝,可能會增加兩次json的序列化和反序列化,如果model物件很複雜對效能影響比較大。 - 頁面初始化/響應速度/UI細節還是跟原生有差距。
當然已經比純web頁強很多了。目前來看還是隻適合輕量化的應用。受制於架構以及微信的平臺,個人認為是對Web的替代和改善。但是就算在可見的未來,還是很難跟native抗衡。
現代瀏覽器和作業系統之間的界限越來越模糊。App的"下載/安裝"過程本身就是一種妥協。只要小程式的體驗足夠好,應該沒有人會拒絕。