原文: 小程序同構方案kbone分析與適配 | AlloyTeam
做者:flyfu wang
在微信小程序的開發的過程當中,咱們會存在小程序和 H5頁面共存的場景,而讓小程序原生和web h5獨立開發,每每會遇到須要兩套人力去維護。對開發者而言,加大了工做量成本,對於產品而言,容易出現展現形態同步不及時問題。在這種狀況下,咱們急須要找到一個既能平衡性能,也能知足快速迭代的方案。javascript
web-view組件是一個承載網頁的容器,最簡單的方案就是使用原h5的代碼,經過 web-view組件進行展現。其優勢是業務邏輯無需額外開發與適配,只須要處理小程序特有的邏輯,而後經過jssdk 與原生小程序通訊。html
使用 webview 加載 h5的問題也很是明顯,首先是體驗問題,用戶見到頁面會通過如下環節:加載小程序包,初始化小程序,再加載 webview中的 html頁面,而後加載相關資源,渲染h5頁面,最後進行展現。最終致使的結果是打開體驗很是差。另外其餘缺點是小程序對web-view部分特性有限制,好比組件會自動鋪滿整個小程序頁面,不支持自定義導航效果等。前端
靜態編譯是最爲主流的小程序同構方案,相似的有 taro, mpvue等。其思路是在構建打包過程,把一種結構化語言,轉換成另外一種結構化語言。好比,taro 把jsx在構建時進行詞法分析,解析代碼獲取AST,而後將 AST遞歸遍歷生成wxml目標代碼。vue
靜態編譯的好處是很是明顯,一套代碼,經過編譯分別轉h5和小程序,兼具性能與跨平臺。另外一方面,隨着這種方案的流行,你們也感覺到了其明顯的問題,首先,因爲小程序自己的限制,好比沒法dom操做,js 與 webview 雙線程通訊等,致使靜態編譯語法轉換,不能作到完全的兼容,開發體驗受制於框架自己的支持程度,相信踩過坑的同窗應該很是有痛的感悟。其次,靜態編譯轉換邏輯須要與小程序最新的特性保持同步,不斷升級。java
靜態編譯的方案實現了同構,但它只是以一種中間態的結構化語法去編碼,非真正的web,犧牲了大量的靈活性。咱們來看下另一種更靈活的方案———運行時兼容。node
咱們回到小程序自己的限制上來。因爲小程序採用雙線程機制,內部經過一個 webview 用於承載頁面渲染,但小程序屏蔽了它本來的 DOM/BOM接口,本身定義了一套組件規範;另外一方面,使用獨立的js-core 負責對 javascript 代碼進行解析,讓頁面和js-core之間進行相互通訊(setData),從而達到執行與渲染的分離。而瀏覽器的 DOM接口是大量web得以顯示的底層依賴,這也是h5代碼沒法直接在小程序中運行的主要緣由。react
那麼如何突破小程序對DOM接口的屏蔽呢? 最直接的思路就是用JS實現和仿造一層瀏覽器環境DOM相關的標準接口,讓用戶的JS代碼能夠無感知的自由操做DOM。經過仿造的底層DOM接口,web 代碼執行完後,最終生成一層仿造的 DOM樹,而後將這棵 DOM 樹轉換成小程序的wxml構成的DOM樹,最後由小程序原生去負責正確的渲染出來。webpack
kbone 是微信官方出一套小程序運行時兼容方案,目前已經接入的小程序有小程序官方社區,及騰訊課堂新人禮包等。而且有專人維護,反饋及時~~。ios
kbone方案核心主要有兩大模塊,第一是miniprogram-render實現了對瀏覽器環境下dom/bom的仿造,構建dom樹,及模擬 web 事件機制。第二個模塊是miniprogram-element是原生小程序渲染入口,主要監聽仿造dom樹的變化,生成對應的小程序的dom 樹,另一個功能是監聽原生小程序事件,派發到仿造的事件中心處理。git
DOM、BOM相關的接口模擬,主要是按照web標準構建 widow、document、node節點等相關 api,思路比較清晰,咱們簡單看下其流程。
首先在用戶層有一個配置文件miniprogram.config,裏面有必要信息origin、entry等須要配置。在 miniprogram-render 的入口文件createPage方法中,配置會初始化到一個全局cache對象中,而後根據配置初始化 Window 和 Document 這兩個重要的對象。Location、Navigator、Screen、History等 BOM 實例都是在 window初始化過程當中完成。DOM 節點相關 api 都是在Document 類中初始化。全部生成的節點和對象都會經過全局的pageMap管理,在各個流程中都能獲取到。
miniprogram-element 負責監聽仿造DOM仿造的變化,而後生成對應小程序組件。因爲小程序中提供的組件和 web 標準並不徹底同樣,而咱們經過 html 生成的 dom 樹結構千差萬別,如和保證任意的html dom樹能夠映射到小程序渲染的dom樹上呢?kbone 經過小程序自定義組件去作了這件事情。
簡單說下什麼是自定義組件,既將特定的代碼抽象成一個模塊,能夠組裝和複用。以 react 爲例,div、span 等標籤是原生組件,經過react.Component將div 和 span 組合成一個特定的 react 組件,在小程序中用自帶的 view、image 等標籤經過Component寫法就能組合成小程序自定義組件。
和大部分 web 框架的自定義組件相似,小程序自定義組件也可以本身遞歸地調用本身,經過將僞造的dom結構數據傳給自定義組件做爲子組件,而後再遞歸的調用,直到沒有子節點爲止,這樣就完成了一個小程序 dom 樹的生成。
大量小程序自定義組件會有額外的性能損耗,kbone 在實現時提供了一些優化。其中最基本的一個優化是將多層小程序原生標籤做爲一個自定義組件。dom 子樹做爲自定義組件渲染的層級數是能夠經過配置傳入,理論上層級越多,使用自定義組件數量越少,性能也就越好。
以上邏輯就是經過DOM_SUB_TREE_LEVEL 層級數對節點過濾,更新後,檢測是否還有節點,再觸發更新。
在頁面onUnload卸載的過程當中,kbone會將當前節點放入緩存池中,方便下次初始化的時候優先從緩存中讀取。
kbone 做爲一種運行時兼容方案,適配成本相對於靜態編譯方案而言會低不少,整體來講對原代碼侵入性很是少,目前接入過程比較順利(期間遇到的坑,感謝 做者june 第一時間幫忙更新發布[玫瑰])
小程序不支持 svg,對於使用 svg 標籤做爲圖片資源的應用而言,須要從底層適配。在一開始咱們想到的方案有經過 肝王的cax進行兼容,但評估後不太靠譜,cax 經過 解析svg 繪製成 canvas,大量 icon會面臨比較嚴重的性能問題。那麼最直接暴力的辦法就是使用 webpack 構建過程直接把 svg 轉 png?後面一位給力的小夥伴想到經過把 svg 標籤轉成Data URI做爲背景圖顯示,最終實踐驗證很是可靠,具體能夠參考kbone svg 適配。
微信小程序環境擁有本身定義的一套 wx.request API, web 中的XMLHttpRequest對象是沒法直接使用。因爲咱們代碼中使用了 axios,因此在預言階段直接簡單經過axios-miniprogram-adapter進行適配器,後面發現部分業務沒有使用 axios,兼容並不夠完全。因而直接從底層構建了一個XMLHttpRequest模塊,將web網絡請求適配到 wx.request。同時作了 cookie 的自動存取邏輯適配(小程序網絡請求默認不帶 cookie)。這一層等完善好了看是否能 pull request到 kbone代碼倉庫中。
部分web 中的接口在小程序沒法徹底得到模擬,好比getBoundingClientRect在小程序中只能經過異步的方式實現。相似的有removeProperty、stopImmediatePropagation
等接口在 kbone 中沒有實現,performance等web特有的全局變量的須要兼容。這些擴展API能夠經過kbone對外暴露的dom/bom 擴展 API進行適配。
對於元素的的高度height offsetHeight獲取,咱們只能經過$getBoundingClientRect異步接口,若是是body scroll-view 實現的,getBoundingClientRect 返回的是scrollHeight。
web的全局滾動事件默認是沒法觸發,須要經過配置windowScroll來監聽,啓用這個特性會影響性能。
global: { windowScroll: true },
kbone 樣式有一個坑,就是它會將標籤選擇器轉換成類選擇器去適配小程序環境,好比
span { } => .h5-span{ }
這樣帶來的反作用就是選擇器的權重會被自動提高,對選擇器權重依賴的標籤樣式須要去手動調整兼容。
注意使用標準的style屬性,好比有webkit-transform會不支持,及小程序樣式和web差別性兼容等。
style: { 'WebkitTransform': 'translate(' + x + 'px, 0)' // 正確 // '-webkit-transform': 'translate(' + x + 'px, 0)' 報錯 }
在初始化路由階段,曾經遇到過Redux 更新dom後偶現節點銷燬,最終定位到是kbone對Location等BOM實例化過晚,最終在june幫忙及時調整了順序,更新了一個版本,現最新本全部BOM對象會在業務執行前準備好。
//初始化dom this.window.$$miniprogram.init() ... //初始化業務 init(this.window, this.document)
在模擬XMLHttpRequest模塊的過程當中遇到一個問題,何時初始化這個對象,咱們能夠選擇在網絡請求庫初始化前引入它,掛載在仿造的 window 對象下。但仍然會出現一個問題,第三放庫直接使用的是XMLHttpRequest 對象,而非經過 window 訪問。
var request = new XMLHttpRequest() // 報錯 var request = new window.XMLHttpRequest() // 正確
在正常的 web 環境,window 是默認的頂層做用域,而小程序中隱式的使用window 對象則會報錯。
爲了解決這一問題,能夠經過配置文件的globalVars字段,將 XMLHttpRequest 直接進行定義。
globalVars: [ ['XMLHttpRequest', 'require("libs/xmlhttprequest.js")'] ]
構建的過程當中會在全部依賴前轉成以下代碼 :
var XMLHttpRequest = require("libs/xmlhttprequest.js")
這樣作解決了隱式訪問 window 做用域問題。但又面臨另外一個問題,那就是xmlhttprequest模塊自己內部由依賴仿造window對象,好比 cookie 訪問,而此時由於require的模塊獨立的做用域沒法訪問到其餘模塊的仿造window 對象。因而最終經過導入一個 function 傳入 window 做用域,而後初始化xmlhttprequest。
globalVars: [ ['XMLHttpRequest', 'require("libs/xmlhttprequest.js").init(window, document)'] ]
小程序和web端須要的資源及部分邏輯是有差別,經過webpck配置進行差別化處理,具體能夠參考文檔編寫kbone webpack 配置。
大概是這樣的區分跨端配置:
分離打包入口文件:
小程序打包入口依賴的 dom 節點,須要主動建立。詳細示例參照官方demo.
export default function createApp() { initialize(function() { let Root = require('./root/index').default; const container = document.createElement('div') container.id = 'pages'; document.body.appendChild(container); render(<Root />, container) }) }
因爲小程序自己是沒有真正userAgent,kbone內部是是根據當前環境進行仿造。
//miniprogram-render/src/bom/navigator#45 this.$_userAgent = `${this.appCodeName}/${appVersion} (${platformContext}) AppleWebKit/${appleWebKitVersion} (KHTML, like Gecko) Mobile MicroMessenger/${this.$_wxVersion} Language/${this.language}`
在業務中有須要區分小程序平臺的場景,咱們能夠經過webpack DefinePlugin插件進行注入,而後經過定義變量進行判斷。
if (!process.env.isWxMiniProgram) { render( <Root />, document.getElementById('pages') ); }
在騰訊文檔的小程序中,有一個獨立的小程序倉庫。 而文檔管理列表是另一個獨立的H5項目,嵌入到小程序webview動態加載。經過kbone轉原生打包後,這部分代碼須要繼承到小程序倉庫中。
首先咱們能夠經過腳本,在webpack構建過程,將kbone 編譯後的包copy到獨立小程序倉庫的目錄下,合併小程序相關配置,從而實現功能合併。同時經過FileWebpackPlugin過濾掉無用的web平臺資源。
這樣遇到一個問題是主包大小仍然超過限制,最後經過小程序分包能夠解決這個問題,將原小程序非首屏頁面所有放分包之中,配置preloadRule 字段再預加載分包。
"subpackages": [ { "root": "packageA", "pages": [ "pages/cat" ] } ] "preloadRule": { "pages/index": { "network": "all", "packages": ["important"] } }
經過對目前各類小程序同構方案的對比與實踐,kbone是一種很是值得推薦的新思路,新方法,兼具性能與靈活。惟一不足的地方就是目前仍有很多底層工做須要適配,更多的問題在繼續探索中,相信隨着不斷迭代及採坑後的反饋,kbone會變得越來穩定和成熟。
(最後感謝做者junexie及dntzhang大神的鼎力支持~~也歡迎你們一塊兒參與共建kbone)
AlloyTeam 歡迎優秀的小夥伴加入。
簡歷投遞: alloyteam@qq.com
詳情可點擊 騰訊AlloyTeam招募Web前端工程師(社招)