概要
本文包括的內(nèi)容:
- 小程序在微信開發(fā)者工具中婿脸,通過構(gòu)建生成真正的執(zhí)行代碼和安裝包魂奥,****.wxapkg终抽。wxml和wxss在構(gòu)建這一步就被轉(zhuǎn)換成了html和css(virtual-DOM)势木。微信開發(fā)者工具中可以得到構(gòu)建腳本和各個版本的js運行SDK文件蛛倦。
- 小程序是以wxapkg文件的形式下發(fā)的,可以拿到wxapkg后解壓拿到執(zhí)行代碼啦桌。同時解壓微信.ipa拿到當(dāng)前的js運行SDK文件,主要是WAWebView.js和WAService.js溯壶。
- 小程序在App中執(zhí)行時的時候分為三個不同的模塊,View/Service/Natvie,各司其職。
- 實現(xiàn)過程遇到大量的細節(jié)和坑江场。
介紹小程序原理的文章比較多缤言,這篇講的比較細:微信小程序架構(gòu)分析。這篇文章的作者也成功的實現(xiàn)了wept,讓小程序運行在自己的webapp里傲武。
參考最多的是微店的Hera,完成度非常高的小程序框架,能夠?qū)⑿〕绦虻膁emo代碼在web/iOS/android運行起來,而且實現(xiàn)了很多工具慨蓝。Hera的問題是開發(fā)于比較早期的版本,不兼容最新的版本了端幼。Hera還有一個問題是他修改了小程序構(gòu)建之后的目錄結(jié)構(gòu)礼烈,采用了service.html作為service部分的入口,跟小程序本身的實現(xiàn)尚有有一些區(qū)別婆跑。所以Hera只能夠構(gòu)建執(zhí)行自己編寫的小程序此熬,不能執(zhí)行別人編寫的小程序。
我的目標(biāo)是能夠運行其它人開發(fā)的app滑进,意味著我只能通過逆向的方式拿到wxapkg犀忱。但是因為拿不到源碼,所以要盡可能在構(gòu)建環(huán)節(jié)跟小程序保持一致扶关。
經(jīng)過數(shù)周的掙扎阴汇,目前已經(jīng)實現(xiàn)了運行官方demo。已經(jīng)達到"可行"的階段驮审,但是還遠遠談不上“可用”鲫寄,因為需要實現(xiàn)小程序大量的API吉执,這是個體力活,依賴個人的力量難以完成地来。
構(gòu)建
官方demo小程序原先的目錄分為幾類文件:
- 配置: 在這個目錄中戳玫,app.json里保存了頁面信息、tabbar信息未斑、網(wǎng)絡(luò)超時配置等等咕宿。 config.js保存了騰訊云后臺服務(wù)解決方案的配置。
- js文件: js定義了各種函數(shù)接口邏輯
- wxml: 類似于html,定義了頁面結(jié)構(gòu)
- 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
├── ...
經(jīng)過小程序的開發(fā)環(huán)境構(gòu)建后蜡秽,生成了一個*.wxapkg文件府阀。
這個文件可以通過從越獄的iPhone或者root的安卓手機上拿到。有部分人用charles通過https抓包拿到了下載鏈接芽突,也拿到了包试浙。
拿到后要進行解包。有大神已經(jīng)通過反編譯安卓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
轉(zhuǎn)換過程可以分為三部分:
- 從wxml/wxss到html挟秤。 page-frame.html里定義了所有的virtural-dom壹哺,來自所有的wxss和wxml的轉(zhuǎn)換,文件非常大艘刚。 page.html里很簡單管宵,就是從page-frame.html里提取對應(yīng)的virtual-dom。包括wxss和wxml對應(yīng)的邏輯攀甚。
- app-service.js是從之前所有的js文件轉(zhuǎn)換而來箩朴。
- app-config.json是從app.json轉(zhuǎn)換而來。
openVendor
命令可以在小程序中獲取到構(gòu)建腳本wcc和wcsc, 以及各個版本小程序的執(zhí)行SDK ****.wxvpkg,這個SDK也可以用wechat-app-unpack解開秋度,解開后里面就是WAService.js和WAWebView.js等代碼隧饼。
wxml/wxss的構(gòu)建原理
wxss 轉(zhuǎn)換成了css,wxml轉(zhuǎn)換成了inject_js,實際上就是virtual_dom静陈。
是用什么工具轉(zhuǎn)換的?小程序里是叫wcc和wcsc诞丽。在開源工具hera自己實現(xiàn)了一套wxss-transpiler和wxml-transpiler鲸拥。而hera的前身wept是直接使用wcc和wcsc。
我們?yōu)榱藴p少維護成本僧免,直接采用wcc和wcsc刑赶。
因為我們沒有wcc和wcsc的源碼,所以只能借助wxss-transpiler和wxml-transpiler來幫助我們理解wxml/wxss的構(gòu)建原理懂衩。
wxss-transpiler調(diào)用了一個PostCSS的插件撞叨,用來處理wxss金踪。
PostCSS 提供了一種方式用 JavaScript 代碼來處理 CSS。它負責(zé)把 CSS 代碼解析成抽象語法樹結(jié)構(gòu)(Abstract Syntax Tree牵敷,AST)胡岔,再交由插件來進行處理。插件基于 CSS 代碼的 AST 所能進行的操作是多種多樣的枷餐,比如可以支持變量和混入(mixin)靶瘸,增加瀏覽器相關(guān)的聲明前綴,或是把使用將來的 CSS 規(guī)范的樣式規(guī)則轉(zhuǎn)譯(transpile)成當(dāng)前的 CSS 規(guī)范支持的格式毛肋。
wxml-transpiler:實現(xiàn)了一個轉(zhuǎn)譯器的工作怨咪,比如postcss也是轉(zhuǎn)譯器,包括解釋器(parser),代碼轉(zhuǎn)換器(Transformer),代碼生成器(Generator)润匙。這個是閉源的诗眨。
- 根據(jù)輸入的列表,讀取所有文件
- 調(diào)用VUE的HTML Parser孕讳,解析輸入的標(biāo)簽及屬性匠楚,生成一顆DOM樹。vue解決不了的js語言卫病,用babylon庫來處理油啤。(Parse)
- 在解析組件的標(biāo)簽時,對其上包含的屬性值進行解析(邊Parse邊Transform)
- 根據(jù)已有的AST生成JS文件(Generate)
更多實現(xiàn)原理見這篇文章
模塊之前的通信
小程序在App中執(zhí)行時的時候分為三個不同的模塊蟀苛,View/Service/Native益咬,各司其職。
View和Service都在WKWebView中執(zhí)行帜平,互相無法調(diào)用幽告。他們之間通過Native層通信。
Native和WebView之間通過webkit.messagehandler和evaluateJavascript互相調(diào)用裆甩。
WeixinJSBridge.publish: view和service之間的透傳冗锁,在WKWebView之間傳遞消息。
WeixinJSBridge.subscribe: 注冊監(jiān)聽嗤栓,監(jiān)聽view和service之間的消息調(diào)用冻河。
WeixinJSBridge.invoke: View或者Service傳遞消息到Native,然后Native使用邏輯調(diào)用js callback。
WeixinJSBridge.on:監(jiān)聽Native的事件茉帅。
如何執(zhí)行
這里以iOS為例介紹Native執(zhí)行過程叨叙。安卓類似。
通過解壓微信的ipa可以拿到WAService.js和WAWebView.js兩個基礎(chǔ)庫文件堪澎,文件內(nèi)容與hera的service.js/view.js已經(jīng)有了較大的區(qū)別擂错。
我們采用小程序的架構(gòu)和hera的兩個webView的方案,盡可能模仿小程序的執(zhí)行過程樱蛤。
View部分
View部分是比較直觀的钮呀,就是WKWebView加載web頁剑鞍。這里需要在app-config.json里讀取到首頁的路徑,然后加載該頁面爽醋。這個路徑下的xxx/index.html是無法直接加載的蚁署,需要做一些處理。要引入本地執(zhí)行SDK里的index.css和view.js, 然后把page-frame.html
里的virtual-dom全部塞進該頁面子房。 然后loadHTML即可形用。
View所有的WKWebView也是要注冊WKUserContentController的,用于通信证杭。
通過反匯編可以得知這個類在微信中叫YYWAWebView
田度,調(diào)用js是直接調(diào)用-evaluateJavaScript:completionHandler:
方法的。
view.js中包含的邏輯:
- WeixinJSBridge 對象處理消息通信: invoke invokeCallbackHandler on publish subscribe subscribe subscribeHandler解愤。
- Reporter 對象
- wxparser 對象镇饺,提供 dom 到 wx element 對象之間的映射操作,提供元素操作管理和事件管理功能送讲。
- virtual dom 渲染算法實現(xiàn)奸笤,提供 diff apply render 等方法,該模塊接口基本與 virtual-dom 一致哼鬓,這里特別的地方在于它所 diff 和生成的并不是原生 DOM监右,而是各種模擬了 DOM 接口的 wx element 對象
Service部分
Service部分的實現(xiàn),Hera和微信小程序采取的了不同的架構(gòu)异希。
Hera的實現(xiàn)較為簡潔健盒,跟View部分保持一致,采用了WKWebView称簿,調(diào)用-evaluateJavaScript:completionHandler:
方法執(zhí)行js扣癣,js回調(diào)OC時使用WKScriptMessageHandler。
通過反匯編可以得知這個類在微信中叫WAJSCoreService
憨降,js和OC之間的調(diào)用是采用JavascriptCore互相調(diào)用父虑。
JavascriptCore它首先要加載app-config.json并把這個配置賦給一個全局對象__wxConfig。然后他要加載service.js是SDK基礎(chǔ),再然后他要加載app-service.js授药,這里面包含了用戶編寫的js邏輯士嚎。最后它發(fā)出全局消息
WeixinJSBridge.publish('serviceReady',,);</script>
喚起小程序app的初始化。
Service.js中包含的邏輯:
- 跟 view.js 一樣的 WeixinJSBridge 兼容模塊
- view.js 一樣的 Reporter 模塊
- appServiceEngine 模塊悔叽,提供 Page航邢,App,GetApp 接口
Native部分
Native執(zhí)行的問題比較復(fù)雜骄蝇,因為基本是黑盒,里面發(fā)生了什么并不知道操骡。
hera的方案在構(gòu)建過程就已經(jīng)跟小程序?qū)嶋H的方案有所區(qū)別九火,會提高維護成本赚窃。所以我們只能靠猜測來實現(xiàn)Native的執(zhí)行過程。
Native部分就是作為入口岔激,運行環(huán)境勒极,跳轉(zhuǎn),轉(zhuǎn)發(fā)消息虑鼎,實現(xiàn)擴展辱匿。包括網(wǎng)絡(luò)模塊/攝像頭/tabbar實現(xiàn)的都是擴展。
我們可以得知的是消息傳遞的協(xié)議炫彩。然后只能通過safari來調(diào)試webView匾七,根據(jù)協(xié)議的名稱和出入?yún)聿聹y協(xié)議的內(nèi)容。
主要的困難點
- 由于壓縮后的view.js和service.js基本不具備可讀性江兢,而virtual-dom又徹底不具備可讀性...所以就算猜中了協(xié)議昨忆,最后也往往是魔改∩荚剩可維護性極差邑贴。
- 工作量不小,因為調(diào)試困難叔磷,無法閱讀拢驾。之后隨著小程序SDK升級,能用多久也不可知改基。加上也有各種微逆向的操作繁疤,小程序封上任何一個接口,都會導(dǎo)致?lián)浣至攘选嵌洼?沙掷m(xù)性極差》馇。總之非常佩服微店的同學(xué)能把Hera搞出來:)
小程序的性能啟發(fā)
小程序是顛覆我對Web的固有印象,最初還以為是類似weex或者rn的調(diào)用原生的方式麻养,沒想到幾乎完全是運行在WKWebView之上的。
- 采用virtual DOM诺舔,操作JS比html性能高很多鳖昌,因為是diff后再操作dom,不需要全部重新渲染低飒,快很多许昨。
- WKWebView,滑動60fps褥赊,在獨立于App之外的進程執(zhí)行糕档。
- 部分邏輯Native化,比如收發(fā)網(wǎng)絡(luò)請求拌喉,比如數(shù)據(jù)持久化速那。 逐步用native組件來替換h5組件俐银。比如tabbar。
- 重用webView以及提前初始化webView等等技巧端仰。
存在的問題:
- 看消息傳遞的原理就能發(fā)現(xiàn)捶惜,傳遞的過程太長了,尤其是
setData:
這種傳遞整個model對象荔烧,是兩次對象的深拷貝吱七,可能會增加兩次json的序列化和反序列化,如果model對象很復(fù)雜對性能影響比較大鹤竭。 - 頁面初始化/響應(yīng)速度/UI細節(jié)還是跟原生有差距踊餐。
當(dāng)然已經(jīng)比純web頁強很多了。目前來看還是只適合輕量化的應(yīng)用诺擅。受制于架構(gòu)以及微信的平臺市袖,個人認為是對Web的替代和改善。但是就算在可見的未來烁涌,還是很難跟native抗衡苍碟。
現(xiàn)代瀏覽器和操作系統(tǒng)之間的界限越來越模糊。App的"下載/安裝"過程本身就是一種妥協(xié)撮执。只要小程序的體驗足夠好,應(yīng)該沒有人會拒絕微峰。