導語 | 隨着小程序的發展,Web 端和小程序同構的呼聲也愈來愈大,爲此微信官方提供了 Kbone 這一套方案。旨在讓開發者能夠用最熟悉的方式來完成一個多端 APP 的開發,下降開發門檻。本文是Kbone做者june在雲加社區微信羣中的分享整理總結而成(編輯:尾尾)。
html
你們好,我是來自騰訊微信小程序團隊的前端開發工程師:june。小程序做爲一種新興地連接用戶與服務的方式,相信你們都或多或少接觸過。對於開發者來講,它是一種相似 Web 但又不一樣於 Web 的開發模式,它提供了一套自定義的 API 和文件組織方式,這無疑帶給開發者必定的學習成本和維護成本,因此咱們也在嘗試可否提供一個方案來抹平這個差別。前端
接下來就進入我今天要分享的話題:Kbone——微信小程序同構方案新思路。本次分享包括四個部分:背景、方案、應用和結語,首先咱們先進入背景部分。vue
之因此會有 Kbone 這個方案出現,源自於一個需求:微信開放社區當時只有 Web 端,爲了讓信息能夠更方便地傳播、分享和使用,但願實現社區小程序版,交互體驗儘可能貼近於 Web 端。react
(微信開放社區連接developers.weixin.qq.com/community/d…)git
這次同構到小程序端須要考慮幾個因素:多端代碼複用、儘量支持已有的特性和性能要有保證。其實最主要的就是要在儘可能不改動現有代碼的狀況下來完成小程序的開發。github
接下來就來探討下具體方案的實現。vue-router
社區 Web 端是基於 Vue 實現的,使用了 Vue-router、Vuex 等插件。Vue 想必你們挺熟悉的了,它是市面上一款很是流行的 Web 框架,提供組件化等特性,其原理大體以下:小程序
Vue 模板能夠認爲是一種附加了一些特殊語法的 HTML 片斷,通常來講一份 Vue 模板對應一個組件,在構建階段編譯成調用 Dom 接口的 JS 函數,調用此 JS 函數就會建立出組件對應的 Dom 樹片斷進而渲染到瀏覽器上。微信小程序
小程序裏是支持運行 JS 的,可是這裏用到的 Dom 接口和渲染到瀏覽器上的功能小程序不具有,因此沒法直接將 Web 端社區代碼移植到小程序中。緣由就在於小程序爲了安全和性能而採用了雙線程的架構,運行用戶 JS 代碼的邏輯層是一個純粹的 JSCore,沒有任何瀏覽器相關的實現,這裏得想辦法將 Web 端代碼轉成小程序代碼。瀏覽器
那麼問題來了,如何將 Vue 代碼轉成小程序代碼?這裏先看下業界常見的作法:將 Vue 模板直接轉成小程序的 WXML 模板。
使用作法至關於拋棄了瀏覽器中建 Dom 樹的過程,而是直接交由小程序來對模板進行編譯建立出小程序的模板樹,進而渲染到小程序頁面中。
通常來講這個作法對於普通場景是夠用的,可是對於一些更復雜的場景就很很差處理了,好比社區中的一個簡單例子:社區帖子詳情展現富文本內容,點擊內容中的圖片可預覽。
這主要是由於 Vue 模板和 WXML 模板的語法並非直接對等的,Vue 的特性設計也和小程序的設計沒法劃等號,這天然就致使了部分 Vue 特性的丟失。好比像 Vue 中的 v-html 指令、ref 獲取 Dom 節點、過濾器等就統統用不了。固然不止是 Vue 自身的特性,一些本來依賴 Dom/Bom 接口的 Vue 插件也沒法使用,好比 Vue-router 等,而這些正是社區高度依賴的,在不對社區代碼作大範圍改造的話是沒法使用此方案的。
此路不通,那還有其餘的方法麼?
答案是有的,這裏咱們就得換一種思路來解決這個問題。回到最初的點上,咱們沒法將 Web 端代碼移植到小程序中是由於小程序沒有 Dom 接口,那麼咱們想辦法作出一個適配層,將這個差別給抹掉不就好了麼?
有了想法就要實施,仿造出 Dom 接口並不難,事實上在 Nodejs 端就有人作過相似的事,好比 jsDom 這個庫的實現,讓咱們能夠在沒有真實瀏覽器環境下能夠對一些依賴 Dom 接口的 Web 端代碼進行測試。
仿造了 Dom 接口給 Vue 調用,進而建立出了仿造 Dom 樹。根據前面提到的小程序架構,用戶的 JS 代碼是執行在邏輯層的,也就是說咱們建立出的 Dom 樹也是存在與邏輯層的內存之中,接下來要解決的難題是如何將這棵 Dom 樹渲染到小程序頁面中。
這裏須要先簡單介紹一下小程序的渲染原理:小程序的雙線程架構,邏輯層會執行用戶的 JS 代碼進而產生一組數據,這組數據會發往視圖層;視圖層接收到數據後,結合用戶的 WXML 模板建立出組件樹,以後小程序再將組件樹渲染出來。這裏的組件樹和 Dom 樹很相似,只是它是由官方內置組件或自定義組件拼接而成而不是 Dom 節點。這裏咱們能不能將仿造出來的 Dom 樹映射到小程序的組件樹上?
小程序組件樹是根據 WXML 模板建立出來的,而仿造 Dom 樹結構是不穩定的,咱們沒法提早預知它會生成什麼樣的結構,也就沒法提早準備後能夠描述任意 Dom 樹的 WXML 模板,除非直接將 Vue 模板轉換成 WXML 模板,但這樣又繞回前面的問題上了。
小程序組件樹中的組件有兩種:內置組件和自定義組件,內置組件是由官方提供的如 video、map 這樣的組件,而自定義組件是一種支持由用戶利用現有組件自行組裝的組件,可否利用它來作些什麼?
使用 Web 端概念來作個簡單解釋,內置組件就像是 div、span 這些 HTML 標籤,而自定義組件就像是 Web 中的 Vue 組件。Vue 組件能夠將 HTML 標籤以及其餘的 Vue 組件進行組裝,自定義組件同理,主要用於功能模塊的抽象、封裝和複用。不過自定義組件有個很奇妙的特性,它支持自引用,也就是說它能夠本身引用本身來進行組裝。
自定義組件能夠本身引用本身,那麼咱們就能夠利用這個特性來進行遞歸建立組件,進而建立出一棵組件樹:
好比上圖的例子,咱們封裝了一個 custom-dom 組件,這個組件裏面也使用了 custom-dom 組件用於渲染子組件。那麼只要咱們執行一下 setData,把 children 數據傳遞過去就能夠建立出子組件,子組件自己也是 custom-dom 組件,它一樣能夠執行這個邏輯把各自的子組件建立出來,這樣就實現了組件的遞歸建立,只要咱們擁有完整的 Dom 樹結構,就能夠建立出相對應的一棵組件樹。
這裏遞歸的終止條件是遇到特定節點、文本節點或者孩子節點爲空。而後在建立出組件樹後,將 Dom 節點和自定義組件實例進行綁定以便後續的 Dom 更新和操做便可。
接下來,若是用戶在界面上進行了操做,觸發了一些事件的話,那麼代碼中要如何監聽這些事件呢?小程序自己有本身的事件系統,它和 Web 端事件系統相似,可是出於如下幾個緣由致使咱們沒法直接使用小程序的事件系統:
小程序支持的事件表現和 Web 端不一致,好比 input 事件在小程序中不可冒泡。
小程序的捕獲冒泡是在 Webview 端,所以邏輯層在整個捕獲冒泡流程中各個節點接收到的事件不是同一個對象。
小程序事件對象和 Web 端事件對象結構不同。
小程序事件的捕獲冒泡以及阻止冒泡等操做必須在 WXML 模板中聲明,沒法使用接口實現。
小程序自己是基於 Web Component 特性來實現的組件體系,其事件來源只能斷定來自於當前 shadow tree 下的哪一個節點,而不能跨 shadow tree 判斷。
綜上所述,最好的解決方法就是把事件系統也仿造一份,在仿造 Dom 樹上進行捕獲冒泡。當自定義組件監聽到用戶的操做後,就將事件發往仿造 Dom 樹,後續自定義組件監聽到的同一個事件的冒泡就直接忽略。而 Dom 樹接收到事件後,再進行捕獲和冒泡,讓事件在各個節點觸發,這樣的話整套體系均可以按照 Web 端的方式進行實現,對於用戶來講,只管按照 Web 端的用法來進行事件監聽便可。
整套方案的大體思路即是如此,接下來介紹幾個實現過程當中比較重要的細節,其一:如何將 Dom 樹傳遞給視圖層?
這其實就是自定義組件要如何作 setData 的問題。咱們一開始想到的方式是直接將整棵 Dom 樹傳遞給自定義組件,而後自定義組件在遞歸建立子組件時一步步透傳下去。這個作法的好處是一勞永逸,只有在最頂層的自定義組件須要管理 Dom 樹和 setData,其餘自定義組件只管接收數據進行渲染便可,可是這樣也帶了問題:每次更新須要作大範圍的 diff,由於 setData 是從根組件發起的;當遇到一些局部更新時可能須要 setData 大量的數據,也就是會傳輸一些沒必要要的數據。
那麼天然而然的,咱們便想到讓每一個自定義組件只 setData 當前節點的數據,每一個自定義組件只考慮當前綁定的 Dom 節點,而後建立出子節點,這樣雖然會增長 setData 的數量,可是帶來的好處即是能夠作到最小範圍 diff,同時每次 setData 的數據量也能夠降到最小。
細節其二:自定義組件實例的建立實際上是會有比較大開銷的,有沒有辦法減小一些自定義組件實例的建立?
按照先前的構想,一個自定義組件綁定一個 Dom 節點,因此自定義組件實例數量等於 Dom 節點數量。
其中一個思路是對 Dom 節點進行刪減,這個實現比較簡單,只要是不展現在頁面上的節點,直接從 Dom 樹上幹掉就能夠了,這樣自定義組件數量也會相應減小。
另外一個思路是調整映射關係,讓一個自定義組件綁定多個 Dom 節點。咱們能夠對 Dom 樹按照必定規則進行裁剪,拆分紅多棵子樹,而後每一個自定義組件管理一棵子樹,這樣的話也能夠減小大部分自定義組件的建立。
除此以外,咱們能夠考慮對葉子節點也進行一些處理。咱們使用自定義組件來渲染的初衷就是爲了能夠動態遞歸建立出子節點,而當一個節點沒有子節點的狀況下,咱們就不須要使用自定義組件來渲染了,因此葉子節點能夠合併到父級棵子樹中(如上圖的藍色節點合併到黃色節點所在的子樹中),直接使用 view 內置組件來渲染便可。
固然還有其餘的一些細節,好比 Dom 對象複用、對象延遲建立等等,這裏就不一一展開說明了,有興趣的朋友能夠經過源碼來了解。
對於這個方案,性能也須要有必定的保證,咱們隨機模擬了一些相似社區首頁的 Dom 樹,對其首次渲染耗時進行測算,其對好比下:
能夠看到在 500 節點內的兩個方案自己性能差很少,不過由於自定義組件實例建立的開銷,在千節點往上的狀況下會落後於靜態模板方案,由於 Kbone 自己是經過犧牲性能來換取更全面的 Web 端兼容,而一般一個小程序頁面的節點數在 100-500 這個區間浮動,所以這個表現是符合預期的。
以上就是 Kbone 這個適配器方案的大體設計思路,咱們將其概括爲兩個模塊:仿造接口和自定義組件。正由於這個方案是經過提供適配器的方式來仿造出 Web 環境,因此用戶代碼不須要作任何魔改,大部分特性均可以繼續使用不須要被刪減,好比 vue-router、window.location 操做等。
方案部分以及介紹完畢,接下來講說這個方案要如何應用到咱們一開始的背景——微信開放社區上。
前面有簡單提到,本來 Web 端代碼是基於 Vue 來搭建的,其中還用到了諸多插件/庫,如 Vue-router、Vuex、Markdown-it 等,同時還支持了服務端渲染。可是無論 Web 端是怎麼實現的,底層終究是調瀏覽器的那些接口,因此對於用戶層面的代碼咱們不作任何調整,只是將瀏覽器那一層替換掉便可。
整個構建流程是基於 Webpack 來實現的,使用 Kbone 構建出小程序代碼也是基於 Webpack 來實現,只須要在本來 Web 端構建流程上實現一個 Webpack 插件,在構建本來 Web 端代碼到小程序端時追加 Kbone 和一些小程序相關的代碼便可。
在整套方案應用的過程當中,確定也會有些定製化的需求,好比但願小程序端頭部和 H5 端不一樣,不一樣端使用不一樣的交互設計:
咱們能夠構建的時候就注入環境變量,在小程序端將 process.env.isMiniprogram 設爲 true,這樣用戶代碼層面能夠經過判斷這個變量來判斷不一樣環境,進而執行不一樣的邏輯。
除此以外,還但願使用小程序的一些特性,好比小程序端支持使用小程序的分享,那麼除了上述的環境變量外,還須要用到小程序的 button 內置組件來實現分享按鈕。在 Kbone 上可使用一個特殊的標籤 wx-button 來表示 button 內置組件,在調 Kbone 的仿造 Dom 接口時會將其 wx- 前綴的標籤識別成內置組件,進而進行特殊處理。
整個社區小程序的功能完善以後,便要思忖一下代碼體積的問題,由於小程序自己有個 2M 限制。縮減代碼體積的方式你們應該都瞭解了不少了,如:壓縮混淆、代碼分割和公共代碼複用、tree shaking、使用分包等等。
還有就是考慮到小程序端是直接複用 Web 端代碼,可是並非全部 Web 端代碼都須要在小程序端作到,那麼在處理模塊依賴時能夠作點手腳。由於都使用的 Webpack 構建,因此能夠編寫一個 loader,在 import/require 的時候追加上,它能夠根據前面注入的環境變量來判斷要不要將代碼進行打包。
這樣就能夠很方便地指定哪些代碼不要構建到小程序端。
總體實現出來的效果以下,左邊是 H5 端,右邊是小程序端:
Web 端連接:developers.weixin.qq.com/community/d…
小程序碼:
這一整套方案的實現和應用大體如此,其原理並不算複雜,只是用了另外一種思路來實現。目前這一套方案即名爲 Kbone,現已整理並開源到 GitHub 上:github.com/wechat-mini…。
考慮到這個方案自己是經過最底層的適配方式來完成同構,那麼除了 Vue 外,它其實也能夠很輕鬆地移植到其餘的 Web 框架上,好比 React、Preact、Omi 等,下面是一些基於這些框架的簡單 demo:
在上述 GitHub 倉庫內也能夠找到這些框架的 demo,儘管各個 Web 框架的實現、語法都有所不一樣,但畢竟其本質上是相同的,最終都會轉化爲 Dom 接口調用來渲染頁面。
也正因如此,能夠看到 Kbone 這套方案最大的優點:擴展性強、對各個特性的支持全面、對代碼編寫的要求少以及自由度高、不須要魔改 Web 框架的底層實現,這樣對於代碼的維護、升級也都更爲簡單方便。
個人分享就到這裏了,謝謝各位!
Q:目前支持到vue那個版本?Vue3.0支持嗎?
A:目前主要的測試用例都是 vue 2.x 版本,大部分特性都能完整使用。vue 3.x 版本的支持在規劃中,由於尚未完整的測試還不清楚直接上 vue 3.x 版本會有哪些坑,不過理論上只要底層仍舊是調用那些基礎的 dom 接口,那就是支持的。
Q:小程序的插件支持嗎?
A:插件目前暫不支持。
Q:請問wxs支持嗎?
A:wxs 目前暫不支持,使用 wxs 有不少狀況下就是爲了實現過濾器和一些簡單的純函數句柄,這些 vue 自己就已經支持了,就不是頗有必要再使用 wxs 了,否則再反向兼容到 Web 端就會很困難。wxs 響應動畫 =》 wxs 響應事件來實現動畫
不過 wxs 響應動畫這塊是一個性能優化點,這個將來會考慮支持的。
Q:小程序原生對位置經緯度的獲取好像不太精準,有其餘好的處理方案嗎?這個在我畢業設計的答辯中差點翻車。
A:增長了高精度定位的參數
Q:這塊的實現對小程序事件響應的性能有影響嗎?「 綜上所述,最好的解決方法就是把事件系統也仿造一份,在仿造 Dom 樹上進行捕獲冒泡。當自定義組件監聽到用戶的操做後,就將事件發往仿造 Dom 樹,後續自定義組件監聽到的同一個事件的冒泡就直接忽略。而 Dom 樹接收到事件後,再進行捕獲和冒泡,讓事件在各個節點觸發,這樣的話整套體系均可以按照 Web 端的方式進行實現,對於用戶來講,只管按照 Web 端的用法來進行事件監聽便可。」
A:和原生的小程序事件相比會有一點損耗但影響不大,小程序事件自己也不是直接使用 Web 端的事件冒泡機制,而是在視圖層的組件樹上本身實現的一套事件系統進行冒泡。kbone 的作法至關於把最初的那一個事件接過邏輯層來本身作一遍 Dom 樹上的冒泡,後續小程序本身的冒泡事件就忽略掉。簡單來講,至關於把冒泡這一套流程從視圖層拿到邏輯層來作。
Q:小程序開放接口或小程序獨有的API(例如:受權,文件操做等),應該如何處理?直接再vue中使用wx.***嗎?
A:是的,小程序環境的接口直接照常使用便可,好比 wx.xxx 等接口。可是若是要同構兼容到 Web 端的話,可能須要判斷一下環境,一般咱們能夠在構建時注入一個 process.env.isMiniprogram,這樣在 Vue 代碼裏就能夠經過判斷環境來作兼容處理。後續這邊也會嘗試提供一些兼容兩個環境的 API,好比現有的 wx.setStorage 等就能夠直接使用 localStorage 來代替,kbone 底層會將 localStorage 的實現轉成 wx.setStorage 等 API。
Q: kbone有開發交流羣或者客服羣嗎?
A:這個先前也有人提過,在近期會提供開發交流羣來方便開發者們交流。
本文是Kbone做者june在雲加社區微信羣中的分享整理總結而成,加羣請關注「雲加社區」公衆號,回覆「加羣」。