引言
移動互聯(lián)網(wǎng)的初期,囿于設(shè)備硬件性能限制粘茄,流量以原生App為主,iOS秕脓、Android是當時兩大平臺柒瓣。
隨著硬件及OS的更新?lián)Q代,H5可承載的體驗逐步完善吠架,為提高開發(fā)效率芙贫、節(jié)約資源(復(fù)用代碼)以及熱更新等目的,Hybrid模式成為主流傍药;以及輕應(yīng)用磺平、服務(wù)號等平臺的助推,H5網(wǎng)頁流量暴漲拐辽,成為第三大平臺拣挪。
2017年1月9日,微信發(fā)布小程序俱诸,歷經(jīng)3年發(fā)展菠劝,在今年主題為”未完成 Always Beta“的微信公開課 PRO上,微信團隊披露睁搭,2019年小程序日活躍用戶超過 3 億赶诊,全年累計成交額達8000億,同比增長超160%园骆。
看到小程序如此驚人的增長力舔痪,我們有理由相信,有中國特色的小程序互聯(lián)網(wǎng)時代已經(jīng)到來锌唾,微信小程序也已成為繼iOS锄码、Android、H5之后的第四大流量平臺晌涕。
平臺分裂滋捶,為不同平臺編寫相同的業(yè)務(wù)代碼,是件無趣的事情渐排。
有追求的程序員炬太,一直在探索代碼復(fù)用的方案,Hybrid App即是代表驯耻。
而在如今的小程序時代亲族,對于同樣基于WEB技術(shù)的H5和小程序炒考,如何實現(xiàn)代碼復(fù)用,是很多前端工程師探索的方向霎迫。業(yè)內(nèi)也已有不少成熟方案斋枢,從場景上來說,大致分為三類:
- 基于跨端框架知给,從頭開發(fā)瓤帚,一套代碼,發(fā)行多個平臺涩赢,比如DCloud出品的
uni-app
戈次、京東凹凸實驗室的taro
- 復(fù)用H5代碼,轉(zhuǎn)換H5代碼在小程序環(huán)境中執(zhí)行筒扒;適用于有H5平臺沉淀怯邪,未開發(fā)小程序或小程序完善度較低的開發(fā)者;
美團的mpvue框架是早期探索解決這個問題的代表花墩,但因小程序不支持dom操作悬秉,故mpvue適用于vue的無dom操作的H5代碼轉(zhuǎn)換;
-
最近微信官方推出的kbone冰蘑,也是為了解決“把 Web 端的代碼挪到小程序環(huán)境內(nèi)執(zhí)行”和泌;不過,kbone 相比 mpvue 前進了一步(當然也有了新的性能缺陷)祠肥,因為:
kbone實現(xiàn)了一個適配器武氓,在適配層里模擬出了瀏覽器環(huán)境,讓 Web 端的代碼可以不做什么改動便可運行在小程序里搪柑。
- 復(fù)用小程序代碼聋丝,轉(zhuǎn)換小程序代碼在web環(huán)境中運行;適用于有小程序代碼沉淀工碾,未開發(fā)H5或H5平臺完善度較低的開發(fā)者;這個方向業(yè)內(nèi)成熟的方案還比較少百姓。
uni-app
近期支持了小程序自定義組件運行到H5平臺渊额,是對如上第三種場景的一種探索。
需求場景
鑒于小程序的低成本獲客特征垒拢,很多廠商選擇先開發(fā)小程序旬迹,驗證業(yè)務(wù)模式后,再擴展至H5求类、App等其它平臺奔垦。
開發(fā)者雖可借助轉(zhuǎn)換器將小程序代碼轉(zhuǎn)換為uni-app
項目(或其它跨端框架項目),快速實現(xiàn)多平臺發(fā)行尸疆;但不少開發(fā)者是不敢輕易決策將跨端版本替換之前線上的小程序版本的椿猎,畢竟線上版本已穩(wěn)定運行了一段時間惶岭。
常選的方案是:讓原生小程序版本和uni-app
跨端版本并行一段時間,微信平臺繼續(xù)使用原生版本犯眠,其它平臺使用uni-app
跨端版本按灶;經(jīng)過一段時間驗證uni-app
版本穩(wěn)定后,再使用uni-app
版替換掉原生小程序版本筐咧。
在這段并行的時間內(nèi)鸯旁,開發(fā)者需要同時維護微信原生、uni-app
兩個版本量蕊,新增業(yè)務(wù)需編寫兩份邏輯相同的代碼铺罢,重復(fù)勞動,成本疊加残炮,如何改善韭赘?
借助uni-app
支持將微信小程序組件運行到H5平臺的特性,我們給出一種思路:
- 開發(fā)者在原生小程序項目中吉殃,將新增業(yè)務(wù)以自定義組件的方式開發(fā)辞居,優(yōu)先上線小程序;
- 拷貝小程序組件的
wxml/wxss/js/json
文件到uni-app
項目下蛋勺,通過uni-app
的編譯器及運行時瓦灶,保證小程序自定義組件在H5平臺的正確運行。
這個方案的好處是:
優(yōu)先小程序開發(fā)抱完,畢竟小程序早已上線贼陶,有存量用戶
復(fù)用小程序組件,新增業(yè)務(wù)僅需開發(fā)一套代碼即可巧娱,降低開發(fā)成本
不止自己開發(fā)的小程序組件碉怔,業(yè)內(nèi)開源的三方小程序組件,均可復(fù)制到uni-app
項目項目中禁添,運行到H5平臺仿滔。
另外,部分公司的產(chǎn)品經(jīng)理肖爵,會要求不同平臺有不同的交互舔哪,但核心業(yè)務(wù)邏輯是相同的,開發(fā)者常會通過維護不同項目的方式來滿足產(chǎn)品經(jīng)理需求铺峭。此時墓怀,采取如上方案,同樣可滿足多個項目復(fù)用相同業(yè)務(wù)邏輯的訴求卫键。
實際上傀履,uni-app
之前已支持將小程序自定義組件運行到App平臺,對于有小程序組件沉淀或優(yōu)先小程序的開發(fā)者來說莉炉,這是個好消息钓账,一套業(yè)務(wù)組件碴犬,快速運行到iOS、Android官扣、H5翅敌、微信小程序這四大流量平臺(實際上也可運行到QQ小程序平臺)。
uni-app 引用小程序組件演示
uni-app
項目中使用自定義組件的方法很簡單惕蹄,分為三步:
1蚯涮、拷貝小程序自定義組件到uni-app
項目根目錄下的wxcomponents
文件夾下
2、在 pages.json
對應(yīng)頁面的 style -> usingComponents
引入組件卖陵,如:
{
"pages": [
{
"path": "index/index",
"style": {
"usingComponents": {
"custom": "/wxcomponents/custom/index"
}
}
}
]
}
3遭顶、在頁面中使用自定義組件,如:
<!-- 頁面模板 (index.vue) -->
<view>
<!-- 在頁面中對自定義組件進行引用 -->
<custom name="uni-app"></custom>
</view>
方案實現(xiàn)思路
簡單介紹下uni-app
的多端發(fā)行原理泪蔫。
uni-app
基于Vue.js
runtime棒旗,頁面文件遵循Vue
.js 單文件組件 (SFC) 規(guī)范,天然對H5的支持比較好撩荣,發(fā)行到H5平臺時铣揉,先通過vue-loader
解析.vue
文件,導(dǎo)出Vue.js
組件選項對象餐曹,然后在運行時補充規(guī)范實現(xiàn):
- 組件:框架提供內(nèi)置組件(view/swiper/picker等)的實現(xiàn)逛拱,保證平臺UI及交互的一致性
- 接口:在H5平臺封裝框架接口,比如路由跳轉(zhuǎn)台猴,showToast等界面交互
- 生命周期:Vue.js的理念是一切皆為組件朽合,沒有應(yīng)用和頁面的概念;框架需創(chuàng)造出應(yīng)用及頁面的概念饱狂,模擬onLaunch曹步、onShow等鉤子
uni-app發(fā)行到小程序平臺時,邏輯又有不同休讳,主要工作有2塊:
- 編譯器:將
.vue
文件拆分成wxml/wxss/js/json
4個原生頁面文件 - 運行時:
Vue.js
和小程序都是邏輯視圖層框架讲婚,都有數(shù)據(jù)綁定功能;運行時會實現(xiàn)Vue.js
到小程序的數(shù)據(jù)同步俊柔,及小程序到Vue.js
的事件代理
小程序自定義組件類似小程序原生的頁面開發(fā)磺樱,一個自定義組件同樣由wxml/wxss/js/json
4個文件組成,另有單獨的組件規(guī)范(如Component
構(gòu)造器婆咸、Behaviors
特性等)。
所以芜辕,小程序自定義組件運行到H5平臺尚骄,可借助uni-app
已有平臺功能快速實現(xiàn):
- 編譯階段:將
wxml/wxss/js/json
4個文件合并為.vue
文件(類似uni-app
發(fā)行到小程序的逆過程),然后調(diào)用uni-app
發(fā)行H5平臺的編譯過程侵续,通過vue-loader
解析.vue
文件倔丈,導(dǎo)出Vue.js
組件選項對象 - 運行階段:實現(xiàn)
Component
構(gòu)造器憨闰、Behaviors
特性,模擬自定義組件特有的生命周期
編譯:轉(zhuǎn)換文件(mp2vue)
小程序自定義組件發(fā)行到H5平臺需五,在編譯環(huán)節(jié)主要有2項工作:
將自定義組件的
wxml/wxss/js/json
4個文件組成鹉动,編譯轉(zhuǎn)換成.vue
文件,即小程序轉(zhuǎn)vue宏邮,可簡寫為mp2vue
通過
vue-loader
解析.vue
文件泽示,導(dǎo)出Vue.js
組件選項對象
其中,步驟2是Vue.js
項目的標準編譯過程蜜氨,略過不提械筛;我們重點闡述步驟1。
mp2vue
將4個獨立wxml/wxss/js/json
的文件合并成一個.vue
文件飒炎,并組裝成template
埋哟、script
、style
這種三段式的結(jié)構(gòu)郎汪,流程包括:
-
wxml
文件生成template
節(jié)點赤赊,同時完成指令、事件等模板語法轉(zhuǎn)換 -
js/json
文件生成script
節(jié)點煞赢,同時完成組件注冊過 -
wxss
文件生成style
節(jié)點抛计,自動轉(zhuǎn)換部分css兼容語法 - 合并為
.vue
文件
具體實現(xiàn)上,uni-app
編譯前先掃描wxcomponents
目錄耕驰,若存在則認為有小程序自定義組件爷辱,啟動文件轉(zhuǎn)換工作(uni-migration
插件來完成):
//加載轉(zhuǎn)換器
const migrate = require('@dcloudio/uni-migration')
//掃描wxcomponents目錄
const wxcomponents = path.resolve(process.env.UNI_INPUT_DIR, 'wxcomponents')
if (fs.existsSync(wxcomponents)) {
migrate(wxcomponents, false, {
silent: true
}) // 轉(zhuǎn)換 mp-weixin 小程序組件
}
接著開始對wxml/wxss/js/json
文件逐個解析,并合并為一個.vue
文件:
module.exports = function transformFile(input, options) {
//首先轉(zhuǎn)換json文件朦肘,判斷是否為組件
const [jsCode, isComponent] = transformJsonFile(filepath + '.json', deps)
options.isComponent = isComponent
//轉(zhuǎn)換 wxml 文件
const [templateCode, wxsCode = '', wxsFiles = []] = transformTemplateFile(filepath + templateExtname, options)
//轉(zhuǎn)換wxss文件
const styleCode = transformStyleFile(filepath + styleExtname, options, deps) || ''
//轉(zhuǎn)換js文件
const scriptCode = transformScriptFile(filepath + '.js', jsCode, options, deps)
// 生成合并后的.vue文件源碼
return [
`${commentsCode}<template>
${templateCode}
</template>
${wxsCode}
<script>
${scriptCode}
</script>
<style platform="mp-weixin">
${styleCode}
</style>`,
deps,
wxsFiles
]
}
進一步細節(jié)說明饭弓,wxml文件轉(zhuǎn)為template節(jié)點時,需完成各項指令媒抠、事件等模板語法的轉(zhuǎn)換弟断,例如:
小程序自定義組件 | Vue組件 | 描述 |
---|---|---|
wx:if | v-if | 條件渲染 |
wx:for | v-for | 列表渲染 |
bindtap | @click | 元素點擊事件 |
將一個最簡自定義組件,按照如上流程轉(zhuǎn)換趴生,結(jié)果示意如下:
運行時:模擬小程序組件環(huán)境
uni-app
的編譯器并不轉(zhuǎn)換小程序組件的 JS 代碼阀趴,依然保留Component
構(gòu)造器的寫法,甚至其中的API依然是wx.
開頭的方式苍匆,這些都依賴uni-app
在H5平臺的運行時來解決刘急,主要有如下幾部分內(nèi)容:
-
Component
構(gòu)造器:解析小程序組件的各種選項配置,轉(zhuǎn)換為Vue
組件定義浸踩,包括變通實現(xiàn)其中的差異部分叔汁,如小程序組件特有的”組件所在頁面的生命周期“ -
Behaviors
特性:轉(zhuǎn)換為Vue的混入(mixin) - 數(shù)據(jù)響應(yīng):在H5平臺實現(xiàn)
setData
接口及this.data.xx = yy
的數(shù)據(jù)通訊機制 - API前綴:可在運行時通過代理機制,自動將
wx.xx
替換為uni.xx
,這個比較簡單据块,不詳述
Component構(gòu)造器
uni-app
在H5平臺定義了一個Component
函數(shù)码邻,執(zhí)行到小程序的Component
構(gòu)造器函數(shù)后,開始循環(huán)解析其屬性另假,并轉(zhuǎn)換成Vue組件屬性像屋,流程示意代碼如下:
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)
// 返回 Vue 組件
return vueComponentOptions
}
在這個過程中,需處理小程序自定義組件和 Vue組件的屬性對應(yīng)關(guān)系及細節(jié)差異边篮,如小程序組件的lifetimes
:
小程序自定義組件 | Vue/uni-app | 描述 |
---|---|---|
created | onServiceCreated | 小程序的created 觸發(fā)時己莺,可以訪問子組件信息,而Vue 的created 訪問不到苟耻,故需uni-app 框架映射到其它時機(onServiceCreated)執(zhí)行 |
attached | onServiceAttached | 同上 |
ready | mounted | Vue 生命周期 |
moved | - | Vue中不存在該鉤子篇恒,暫不支持轉(zhuǎn)換 |
detached | destroyed | Vue 生命周期 |
小程序的pageLifetimes
(組件所在頁面的生命周期)在Vue中是沒有的,需要映射為uni-app
封裝的頁面生命周期:
小程序自定義組件 | uni-app | 描述 |
---|---|---|
ready | onPageShow | 頁面被展示時執(zhí)行 |
hide | onPageHide | 頁面被隱藏時執(zhí)行 |
resize | onPageResize | 頁面尺寸變化時執(zhí)行 |
Behaviors
特性的實現(xiàn)過程凶杖,類似Component
構(gòu)造器胁艰,不再贅述。
數(shù)據(jù)響應(yīng)
Vue
和小程序都有一套數(shù)據(jù)綁定系統(tǒng)智蝠,但機制不同腾么,比如在Vue體系下,數(shù)據(jù)賦值是這樣的:
this.a = 1
但在小程序中杈湾,數(shù)據(jù)賦值方式則是這樣的:
this.setData({
a:1
}) //響應(yīng)式
this.data.a = 2 //非響應(yīng)式
另外解虱,小程序和Vue
在數(shù)據(jù)的properties
、observer
等方面都存在不少差異漆撞,經(jīng)過我們評估殴泰,若將小程序的數(shù)據(jù)響應(yīng)用法直接映射到Vue
體系下,復(fù)雜度較高且有性能壓力浮驳,故uni-app
在H5平臺按照微信的語法規(guī)范悍汛,單獨實現(xiàn)了一套數(shù)據(jù)響應(yīng)系統(tǒng)。
// 小程序的setData在H5平臺的實現(xiàn)
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();//數(shù)據(jù)變化至会,強制視圖更新(響應(yīng)式)
isFn(callback) && this.$nextTick(callback);
}
將setData
掛載到 vm 對象上离咐,可通過this.setData
這種小程序的方式調(diào)用;同時將數(shù)據(jù)綁定到data屬性上奉件,支持this.data.xx
的訪問方式宵蛀。
export function initState (vm) {
const instanceData = JSON.parse(JSON.stringify(vm.$options.mpOptions.data || {}))
vm[SOURCE_KEY] = instanceData
//vm對象上掛載 setData 方法,實現(xiàn)小程序方法
vm.setData = setData
const propertyDefinition = {
get () {
return vm[SOURCE_KEY]
},
set (value) {
vm[SOURCE_KEY] = value
}
}
//小程序用法县貌,可通過this.data.xx訪問
Object.defineProperties(vm, {
data: propertyDefinition,
properties: propertyDefinition
})
Object.keys(instanceData).forEach(key => {
proxy(vm, SOURCE_KEY, key)
})
}
雖然數(shù)據(jù)響應(yīng)是uni-app
自己實現(xiàn)的术陶,但渲染依然使用了Vue框架的render
函數(shù),此時需小程序規(guī)范中的this.data.xx
和Vue規(guī)范中的this.xx
保持一致煤痕,通過代理的方式實現(xiàn):
// 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);
}
這里僅列出了主要的幾步瞳别,中間涉及細節(jié)很多征候;部分無法通過Vue
擴展機制實現(xiàn)的功能,只好修改Vue.js
的內(nèi)核源碼祟敛,比如updateProperties
支持、小程序wxs
兆解、externalClasses
等功能在H5平臺的支持馆铁,都需要定制部分 Vue.js runtime 源碼。
結(jié)語
本文分享了uni-app
將微信小程序自定義組件發(fā)行到H5平臺的實現(xiàn)思路锅睛,希望對大家有所啟發(fā)埠巨。
但這種方案,歸根到底是為了解決多套項目并存時的業(yè)務(wù)重復(fù)開發(fā)的問題现拒。
如果你是從頭開發(fā)辣垒,我們建議直接選擇業(yè)內(nèi)成熟的跨端框架,既可以保持一套代碼印蔬,更省力的維護勋桶,還可以借助框架的成熟生態(tài)(如跨端UI庫及插件市場),基于成熟輪子侥猬,快速完成業(yè)務(wù)的上線開發(fā)例驹;
uni-app
框架代碼,包括小程序組件發(fā)行到H5平臺的代碼退唠,全部開源在github鹃锈,如果大家對本文邏輯有疑問,歡迎提交issue交流瞧预。