uni-app黑魔法:小程序自定義組件運行到H5平臺

引言

移動互聯網的初期,囿於設備硬件性能限制,流量以原生App為主,iOS、Android是當時兩大平臺。

隨著硬件及OS的更新換代,H5可承載的體驗逐步完善,為提高開發效率、節約資源(複用代碼)以及熱更新等目的,Hybrid模式成為主流;以及輕應用、服務號等平臺的助推,H5網頁流量暴漲,成為第三大平臺。

2017年1月9日,微信發佈小程序,歷經3年發展,在今年主題為”未完成 Always Beta“的微信公開課 PRO上,微信團隊披露,2019年小程序日活躍用戶超過 3 億,全年累計成交額達8000億,同比增長超160%。

看到小程序如此驚人的增長力,我們有理由相信,有中國特色的小程序互聯網時代已經到來,微信小程序也已成為繼iOS、Android、H5之後的第四大流量平臺。

平臺分裂,為不同平臺編寫相同的業務代碼,是件無趣的事情。

有追求的程序員,一直在探索代碼複用的方案,Hybrid App即是代表。

而在如今的小程序時代,對於同樣基於WEB技術的H5和小程序,如何實現代碼複用,是很多前端工程師探索的方向。業內也已有不少成熟方案,從場景上來說,大致分為三類:

  1. 基於跨端框架,從頭開發,一套代碼,發行多個平臺,比如DCloud出品的uni-app、京東凹凸實驗室的taro
  2. 複用H5代碼,轉換H5代碼在小程序環境中執行;適用於有H5平臺沉澱,未開發小程序或小程序完善度較低的開發者;
  • 美團的mpvue框架是早期探索解決這個問題的代表,但因小程序不支持dom操作,故mpvue適用於vue的無dom操作的H5代碼轉換;
  • 最近微信官方推出的kbone,也是為了解決“把 Web 端的代碼挪到小程序環境內執行”;不過,kbone 相比 mpvue 前進了一步(當然也有了新的性能缺陷),因為:kbone實現了一個適配器,在適配層裡模擬出了瀏覽器環境,讓 Web 端的代碼可以不做什麼改動便可運行在小程序裡。
  1. 複用小程序代碼,轉換小程序代碼在web環境中運行;適用於有小程序代碼沉澱,未開發H5或H5平臺完善度較低的開發者;這個方向業內成熟的方案還比較少。

uni-app近期支持了小程序自定義組件運行到H5平臺,是對如上第三種場景的一種探索。

需求場景

鑑於小程序的低成本獲客特徵,很多廠商選擇先開發小程序,驗證業務模式後,再擴展至H5、App等其它平臺。

開發者雖可藉助轉換器將小程序代碼轉換為uni-app項目(或其它跨端框架項目),快速實現多平臺發行;但不少開發者是不敢輕易決策將跨端版本替換之前線上的小程序版本的,畢竟線上版本已穩定運行了一段時間。

常選的方案是:讓原生小程序版本和uni-app跨端版本並行一段時間,微信平臺繼續使用原生版本,其它平臺使用uni-app跨端版本;經過一段時間驗證uni-app版本穩定後,再使用uni-app版替換掉原生小程序版本。

在這段並行的時間內,開發者需要同時維護微信原生、uni-app兩個版本,新增業務需編寫兩份邏輯相同的代碼,重複勞動,成本疊加,如何改善?

藉助uni-app 支持將微信小程序組件運行到H5平臺的特性,我們給出一種思路:

  • 開發者在原生小程序項目中,將新增業務以自定義組件的方式開發,優先上線小程序;
  • 拷貝小程序組件的wxml/wxss/js/json文件到uni-app 項目下,通過uni-app的編譯器及運行時,保證小程序自定義組件在H5平臺的正確運行。

這個方案的好處是:

  • 優先小程序開發,畢竟小程序早已上線,有存量用戶
  • 複用小程序組件,新增業務僅需開發一套代碼即可,降低開發成本

不止自己開發的小程序組件,業內開源的三方小程序組件,均可複製到uni-app項目項目中,運行到H5平臺。

另外,部分公司的產品經理,會要求不同平臺有不同的交互,但核心業務邏輯是相同的,開發者常會通過維護不同項目的方式來滿足產品經理需求。此時,採取如上方案,同樣可滿足多個項目複用相同業務邏輯的訴求。

實際上,uni-app之前已支持將小程序自定義組件運行到App平臺,對於有小程序組件沉澱或優先小程序的開發者來說,這是個好消息,一套業務組件,快速運行到iOS、Android、H5、微信小程序這四大流量平臺(實際上也可運行到QQ小程序平臺)。

uni-app黑魔法:小程序自定義組件運行到H5平臺

uni-app 引用小程序組件演示

uni-app項目中使用自定義組件的方法很簡單,分為三步:

1、拷貝小程序自定義組件到uni-app項目根目錄下的wxcomponents文件夾下

2、在 pages.json 對應頁面的 style -> usingComponents引入組件,如:

<code>{
  

"pages"

: [       {          

"path"

:

"index/index"

,          

"style"

: {              

"usingComponents"

: {                    

"custom"

:

"/wxcomponents/custom/index"

              }           }       }   ] }/<code>
<code> 

<

view

>

   

<

custom

name

=

"uni-app"

>

custom

>

view

>

/<code>
<code> 

const

migrate =

require

(

'@dcloudio/uni-migration'

const

wxcomponents = path.resolve(process.env.UNI_INPUT_DIR,

'wxcomponents'

if

(fs.existsSync(wxcomponents)) {  migrate(wxcomponents,

false

, {  

silent

:

true

  }) }/<code>
<code>

module

.exports =

function

transformFile

(

input, options

)

{

const

[jsCode, isComponent] = transformJsonFile(filepath +

'.json'

, deps) options.isComponent = isComponent

const

[templateCode, wxsCode =

''

, wxsFiles = []] = transformTemplateFile(filepath + templateExtname, options)

const

styleCode = transformStyleFile(filepath + styleExtname, options, deps) ||

''

const

>'.js'

, jsCode, options, deps)

return

[  

`

${commentsCode}

${templateCode}

${wxsCode}

`

,   deps,   wxsFiles ] }/<code>
<code>export 

function

Component

(options)

{

const

componentOptions = parseComponent(options) componentOptions.mixins.unshift(polyfill) componentOptions.mpOptions.path =

global

[

'__wxRoute'

] initRelationsHandler(componentOptions)

global

[

'__wxComponents'

][

global

[

'__wxRoute'

]] = componentOptions } ​ export

function

parseComponent

(mpComponentOptions)

{

const

{   data,   options,   methods,   behaviors,   lifetimes,   observers,   relations,   properties,   pageLifetimes,   externalClasses } = mpComponentOptions ​

const

vueComponentOptions = {   mixins: [],   props: {},   watch: {},   mpOptions: {     mpObservers: []   } } ​ parseData(data, vueComponentOptions) parseOptions(options, vueComponentOptions) parseMethods(methods, vueComponentOptions) parseBehaviors(behaviors, vueComponentOptions) parseLifetimes(lifetimes, vueComponentOptions) parseObservers(observers, vueComponentOptions) parseRelations(relations, vueComponentOptions) parseProperties(properties, vueComponentOptions) parsePageLifetimes(pageLifetimes, vueComponentOptions) parseExternalClasses(externalClasses, vueComponentOptions) parseLifecycle(mpComponentOptions, vueComponentOptions) parseDefinitionFilter(mpComponentOptions, vueComponentOptions)

return

vueComponentOptions }/<code>
<code>

this.a

=

1

/<code>
<code>

this

.setData({ a:

1

})

this

.

data

.a =

2

/<code>
<code> 
function setData (

data

, callback) {

if

(!isPlainObject(

data

)) {  

return

} Object.keys(

data

).forEach(key => {  

if

(setDataByExprPath(key,

data

[key],

this

.

data

)) {     !hasOwn(

this

, key) && proxy(

this

, SOURCE_KEY, key);   } });

this

.$forceUpdate(); isFn(callback) &&

this

.$nextTick(callback); }/<code>
<code>

export

function

initState

(

vm

)

{

const

instanceData =

JSON

.parse(

JSON

.stringify(vm.$options.mpOptions.data || {})) vm[SOURCE_KEY] = instanceData   vm.setData = setData 

const

propertyDefinition = {  

get

() {    

return

vm[SOURCE_KEY]   },  

set

(value) {     vm[SOURCE_KEY] = value   } }

Object

.defineProperties(vm, {  

data

: propertyDefinition,  

properties

: propertyDefinition }) ​

Object

.keys(instanceData).forEach(

key

=>

{   proxy(vm, SOURCE_KEY, key) }) }/<code>
<code>// mp/polyfill/state/proxy.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true
};
​
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
  return this[

sourceKey

][

key

] }; sharedPropertyDefinition.set = function proxySetter (val) {   this[

sourceKey

][

key

] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); }/<code>

uni-app框架代碼,包括小程序組件發行到H5平臺的代碼,全部開源在github,如果大家對本文邏輯有疑問,歡迎提交issue交流。

如果你是從頭開發,我們建議直接選擇業內成熟的跨端框架,既可以保持一套代碼,更省力的維護,還可以藉助框架的成熟生態(如跨端UI庫及插件市場),基於成熟輪子,快速完成業務的上線開發;

但這種方案,歸根到底是為了解決多套項目並存時的業務重複開發的問題。

本文分享了uni-app將微信小程序自定義組件發行到H5平臺的實現思路,希望對大家有所啟發。

結語

這裡僅列出了主要的幾步,中間涉及細節很多;部分無法通過Vue擴展機制實現的功能,只好修改Vue.js的內核源碼,比如updateProperties支持、小程序wxs、externalClasses等功能在H5平臺的支持,都需要定製部分 Vue.js runtime 源碼。

雖然數據響應是uni-app自己實現的,但渲染依然使用了Vue框架的render函數,此時需小程序規範中的this.data.xx和Vue規範中的this.xx保持一致,通過代理的方式實現:

將setData掛載到 vm 對象上,可通過this.setData這種小程序的方式調用;同時將數據綁定到data屬性上,支持this.data.xx的訪問方式。

另外,小程序和Vue在數據的properties、observer等方面都存在不少差異,經過我們評估,若將小程序的數據響應用法直接映射到Vue體系下,複雜度較高且有性能壓力,故uni-app在H5平臺按照微信的語法規範,單獨實現了一套數據響應系統。

但在小程序中,數據賦值方式則是這樣的:

Vue和小程序都有一套數據綁定系統,但機制不同,比如在Vue體系下,數據賦值是這樣的:

數據響應

Behaviors特性的實現過程,類似Component構造器,不再贅述。

小程序自定義組件uni-app描述readyonPageShow頁面被展示時執行hideonPageHide頁面被隱藏時執行resizeonPageResize頁面尺寸變化時執行

小程序的pageLifetimes(組件所在頁面的生命週期)在Vue中是沒有的,需要映射為uni-app封裝的頁面生命週期:

小程序自定義組件Vue/uni-app描述createdonServiceCreated小程序的created觸發時,可以訪問子組件信息,而Vue的created訪問不到,故需uni-app框架映射到其它時機(onServiceCreated)執行attachedonServiceAttached同上readymountedVue 生命週期moved-Vue中不存在該鉤子,暫不支持轉換detacheddestroyedVue 生命週期

在這個過程中,需處理小程序自定義組件和 Vue組件的屬性對應關係及細節差異,如小程序組件的lifetimes:

uni-app在H5平臺定義了一個Component函數,執行到小程序的Component構造器函數後,開始循環解析其屬性,並轉換成Vue組件屬性,流程示意代碼如下:

Component構造器

  • Component構造器:解析小程序組件的各種選項配置,轉換為Vue組件定義,包括變通實現其中的差異部分,如小程序組件特有的”組件所在頁面的生命週期“
  • Behaviors特性:轉換為Vue的混入(mixin)
  • 數據響應:在H5平臺實現setData接口及this.data.xx = yy的數據通訊機制
  • API前綴:可在運行時通過代理機制,自動將wx.xx替換為uni.xx,這個比較簡單,不詳述

uni-app的編譯器並不轉換小程序組件的 JS 代碼,依然保留Component構造器的寫法,甚至其中的API依然是wx.開頭的方式,這些都依賴uni-app在H5平臺的運行時來解決,主要有如下幾部分內容:

運行時:模擬小程序組件環境

uni-app黑魔法:小程序自定義組件運行到H5平臺

將一個最簡自定義組件,按照如上流程轉換,結果示意如下:

小程序自定義組件Vue組件描述wx:ifv-if條件渲染wx:forv-for列表渲染bindtap@click元素點擊事件

進一步細節說明,wxml文件轉為template節點時,需完成各項指令、事件等模板語法的轉換,例如:

接著開始對wxml/wxss/js/json文件逐個解析,併合併為一個.vue文件:

具體實現上,uni-app編譯前先掃描wxcomponents目錄,若存在則認為有小程序自定義組件,啟動文件轉換工作(uni-migration插件來完成):

  1. wxml文件生成template節點,同時完成指令、事件等模板語法轉換
  2. js/json文件生成script節點,同時完成組件註冊過
  3. wxss文件生成style節點,自動轉換部分css兼容語法
  4. 合併為.vue文件

mp2vue將4個獨立wxml/wxss/js/json 的文件合併成一個.vue文件,並組裝成template、script、style 這種三段式的結構,流程包括:

其中,步驟2是Vue.js項目的標準編譯過程,略過不提;我們重點闡述步驟1。

  1. 將自定義組件的wxml/wxss/js/json 4個文件組成,編譯轉換成.vue文件,即小程序轉vue,可簡寫為mp2vue
  2. 通過vue-loader解析.vue文件,導出 Vue.js 組件選項對象

小程序自定義組件發行到H5平臺,在編譯環節主要有2項工作:

編譯:轉換文件(mp2vue)

  • 編譯階段:將wxml/wxss/js/json4個文件合併為.vue文件(類似 uni-app 發行到小程序的逆過程),然後調用uni-app發行H5平臺的編譯過程,通過vue-loader解析.vue文件,導出 Vue.js 組件選項對象
  • 運行階段:實現 Component 構造器、Behaviors特性,模擬自定義組件特有的生命週期

所以,小程序自定義組件運行到H5平臺,可藉助uni-app已有平臺功能快速實現:

小程序自定義組件類似小程序原生的頁面開發,一個自定義組件同樣由wxml/wxss/js/json 4個文件組成,另有單獨的組件規範(如Component 構造器、Behaviors特性等)。

  • 編譯器:將.vue文件拆分成wxml/wxss/js/json4個原生頁面文件
  • 運行時:Vue.js和小程序都是邏輯視圖層框架,都有數據綁定功能;運行時會實現Vue.js到小程序的數據同步,及小程序到Vue.js的事件代理

uni-app發行到小程序平臺時,邏輯又有不同,主要工作有2塊:

  • 組件:框架提供內置組件(view/swiper/picker等)的實現,保證平臺UI及交互的一致性
  • 接口:在H5平臺封裝框架接口,比如路由跳轉,showToast等界面交互
  • 生命週期:Vue.js的理念是一切皆為組件,沒有應用和頁面的概念;框架需創造出應用及頁面的概念,模擬onLaunch、onShow等鉤子

uni-app基於Vue.js runtime,頁面文件遵循Vue.js 單文件組件 (SFC) 規範,天然對H5的支持比較好,發行到H5平臺時,先通過vue-loader解析.vue文件,導出Vue.js 組件選項對象,然後在運行時補充規範實現:

簡單介紹下uni-app的多端發行原理。

方案實現思路

3、在頁面中使用自定義組件,如:


分享到:


相關文章: