
眾所周知如今市面上端的形態(tài)多種多樣,手機(jī)Web、ReactNative、微信小程序, 支付寶小程序, 快應(yīng)用等,每一端都是巨大的流量入口,當(dāng)業(yè)務(wù)要求同時(shí)在不同的端都要求有所表現(xiàn)的時(shí)候,針對(duì)不同的端去編寫多套代碼的成本顯然非常高,這時(shí)候只編寫一套代碼就能夠適配到多端的能力就顯得極為需要。但面對(duì)目前市面上成熟的小程序第三方框架如何針對(duì)自己的需求進(jìn)行選擇也是一個(gè)麻煩事,本文針對(duì)當(dāng)前市面上的三大轉(zhuǎn)譯框架進(jìn)行一個(gè)綜合對(duì)比,希望能對(duì)大家的技術(shù)選擇有所幫助,如有哪里不妥的地方希望指正;
在這里我通過對(duì)目前已開源的三種常用小程序框架做一個(gè)綜合對(duì)比, 還有一個(gè)叫nanchi的基于react的小程序轉(zhuǎn)譯框架,由于沒來的及研究暫不做比較;
騰訊團(tuán)隊(duì)開源的一款類vue語法規(guī)范的小程序框架,借鑒了Vue的語法風(fēng)格和功能特性,支持了Vue的諸多特征,比如父子組件、組件之間的通信、computed屬性計(jì)算、wathcer監(jiān)聽器、props傳值、slot槽分發(fā),還有很多高級(jí)的特征支持:Mixin混合、攔截器等;WePY發(fā)布的第一個(gè)版本是2016年12月份,也就是小程序剛剛推出的時(shí)候,到目前為止,WePY已經(jīng)發(fā)布了52個(gè)版本, 最新版本為1.7.2;
美團(tuán)團(tuán)隊(duì)開源的一款使用 Vue.js 開發(fā)微信小程序的前端框架。使用此框架,開發(fā)者將得到完整的 Vue.js 開發(fā)體驗(yàn),同時(shí)為 H5 和小程序提供了代碼復(fù)用的能力。mpvue在發(fā)布后的幾天間獲得2.7k的star,上升速度飛起,截至目前為止已經(jīng)有13.7k的star;
京東凹凸實(shí)驗(yàn)室開源的一款使用 React.js 開發(fā)微信小程序的前端框架。它采用與 React 一致的組件化思想,組件生命周期與 React 保持一致,同時(shí)支持使用 JSX 語法,讓代碼具有更豐富的表現(xiàn)力,使用 Taro 進(jìn)行開發(fā)可以獲得和 React 一致的開發(fā)體驗(yàn)。,同時(shí)因?yàn)槭褂昧藃eact的原因所以除了能編譯h5, 小程序外還可以編譯為ReactNative;


同為vue規(guī)范的mpvue和wepy的生命周期和各種方法不盡相同
wepy生命周期基本與原生小程序相同,再此基礎(chǔ)上糅合了一些vue的特性; 對(duì)于WePY中的methods屬性,因?yàn)榕cVue中的使用習(xí)慣不一致,非常容易造成誤解,這里需要特別強(qiáng)調(diào)一下:WePY中的methods屬性只能聲明頁面wxml標(biāo)簽的bind、catch事件,不能聲明自定義方法,這與Vue中的用法是不一致的?! ?/p>

import wepy from 'wepy';
export default class MyPage extends wepy.page {
// export default class MyComponent extends wepy.component {
customData = {} // 自定義數(shù)據(jù)
customFunction () {} //自定義方法
onLoad () {} // 在Page和Component共用的生命周期函數(shù)
onShow () {} // 只在Page中存在的頁面生命周期函數(shù)
config = {}; // 只在Page實(shí)例中存在的配置數(shù)據(jù),對(duì)應(yīng)于原生的page.json文件
data = {}; // 頁面所需數(shù)據(jù)均需在這里聲明,可用于模板數(shù)據(jù)綁定
components = {}; // 聲明頁面中所引用的組件,或聲明組件中所引用的子組件
mixins = []; // 聲明頁面所引用的Mixin實(shí)例
computed = {}; // 聲明計(jì)算屬性(詳見后文介紹)
watch = {}; // 聲明數(shù)據(jù)watcher(詳見后文介紹)
methods = {}; // 聲明頁面wxml中標(biāo)簽的事件處理函數(shù)。注意,此處只用于聲明頁面wxml中標(biāo)簽的bind、catch事件,自定義方法需以自定義方法的方式聲明
events = {}; // 聲明組件之間的事件處理函數(shù)
}
|

mpvue
mpvue 除了 Vue 本身的生命周期外,還兼容了小程序生命周期,這部分生命周期鉤子的來源于微信小程序的 Page, 除特殊情況外,不建議使用小程序的生命周期 鉤子。

1Vue 3 beforeCreate 4 created 5 beforeMount 6 mounted 7 beforeUpdate 8 updated 9 activated 10 deactivated 11 beforeDestroy 12 destroyed 13 app 部分 15 onLaunch,初始化 16 onShow,當(dāng)小程序啟動(dòng),或從后臺(tái)進(jìn)入前臺(tái)顯示 17 onHide,當(dāng)小程序從前臺(tái)進(jìn)入后臺(tái) 18 page 部分 20 onLoad,監(jiān)聽頁面加載 21 onShow,監(jiān)聽頁面顯示 22 onReady,監(jiān)聽頁面初次渲染完成 23 onHide,監(jiān)聽頁面隱藏 24 onUnload,監(jiān)聽頁面卸載 25 onPullDownRefresh,監(jiān)聽用戶下拉動(dòng)作 26 onReachBottom,頁面上拉觸底事件的處理函數(shù) 27 onShareAppMessage,用戶點(diǎn)擊右上角分享 28 onPageScroll,頁面滾動(dòng) 29 onTabItemTap, 當(dāng)前是 tab 頁時(shí),點(diǎn)擊 tab 時(shí)觸發(fā) (mpvue 0.0.16 支持) |

簡單示例

new Vue({
data: {
a: 1
},
created () {
// `this` 指向 vm 實(shí)例
console.log('a is: ' + this.a)
},
onShow () {
// `this` 指向 vm 實(shí)例
console.log('a is: ' + this.a, '小程序觸發(fā)的 onshow')
}
})
// => "a is: 1"
|


class Clock extends Component {
constructor (props) {
super(props)
this.state = { date: new Date() }
}
componentDidMount() {
}
componentWillUnmount() {
}
render () {
return (
<View>
<Text>Hello, world!</Text>
<Text>現(xiàn)在的時(shí)間是 {this.state.date.toLocaleTimeString()}.</Text>
</View>
)
}
}
|

wepy當(dāng)需要循環(huán)渲染W(wǎng)ePY組件時(shí)(類似于通過wx:for循環(huán)渲染原生的wxml標(biāo)簽),必須使用WePY定義的輔助標(biāo)簽<repeat>

<template>
<!-- 注意,使用for屬性,而不是使用wx:for屬性 -->
<repeat for="{{list}}" key="index" index="index" item="item">
<!-- 插入<script>腳本部分所聲明的child組件,同時(shí)傳入item -->
<child :item="item"></child>
</repeat>
</template>
<script>
import wepy from 'wepy';
// 引入child組件文件
import Child from '../components/child';
export default class Index extends wepy.component {
components = {
// 聲明頁面中要使用到的Child組件的ID為child
child: Child
}
data = {
list: [{id: 1, title: 'title1'}, {id: 2, title: 'title2'}]
}
}
</script>
|

mpvue使用v-for與vue一致,只是需要注意一點(diǎn),嵌套列表渲染,必須指定不同的索引!

<!-- 在這種嵌套循環(huán)的時(shí)候, index 和 itemIndex 這種索引是必須指定,且別名不能相同,正確的寫法如下 -->
<template>
<ul v-for="(card, index) in list">
<li v-for="(item, itemIndex) in card">
{{item.value}}
</li>
</ul>
</template>
|

taro的列表循環(huán)用法基本與react相同,有一點(diǎn)需要注意,在 React 中,JSX 是會(huì)編譯成普通的 JS 的執(zhí)行,每一個(gè) JSX 元素,其實(shí)會(huì)通過 createElement 函數(shù)創(chuàng)建成一個(gè) JavaScript 對(duì)象(React Element),因此實(shí)際上你可以這樣寫代碼 React 也是完全能渲染的:
const list = this.state.list.map(l => {
if (l.selected) {
return <li>{l.text}</li>
}
}).filter(React.isValidElement)
|
但是 Taro 中,JSX 會(huì)編譯成微信小程序模板字符串,因此你不能把 map 函數(shù)生成的模板當(dāng)做一個(gè)數(shù)組來處理。當(dāng)你需要這么做時(shí),應(yīng)該先處理需要循環(huán)的數(shù)組,再用處理好的數(shù)組來調(diào)用 map 函數(shù)。例如上例應(yīng)該寫成:
const list = this.state.list
.filter(l => l.selected)
.map(l => {
return <li>{l.text}</li>
})
|
mpvue目前全支持小程序的事件處理器,引入了 Vue.js 的虛擬 DOM ,在前文模版中綁定的事件會(huì)被掛在到 vnode 上,同時(shí) compiler 在 wxml 上綁定了小程序的事件,并做了相應(yīng)的映射,所以你在真實(shí)點(diǎn)擊的時(shí)候通過 runtime 中 handleProxy 通過事件類型分發(fā)到 vnode 的事件上,同 Vue 在 WEB 的機(jī)制一樣,所以可以做到無損支持。同時(shí)還順便支持了自定義事件和 $emit 機(jī)制

// 事件映射表,左側(cè)為 WEB 事件,右側(cè)為 小程序 對(duì)應(yīng)事件
{
click: 'tap',
touchstart: 'touchstart',
touchmove: 'touchmove',
touchcancel: 'touchcancel',
touchend: 'touchend',
tap: 'tap',
longtap: 'longtap',
input: 'input',
change: 'change',
submit: 'submit',
blur: 'blur',
focus: 'focus',
reset: 'reset',
confirm: 'confirm',
columnchange: 'columnchange',
linechange: 'linechange',
error: 'error',
scrolltoupper: 'scrolltoupper',
scrolltolower: 'scrolltolower',
scroll: 'scroll'
}
|

踩坑注意(官方文檔):
wepy事件綁定區(qū)別于vue,根據(jù)原生小程序事件提供了語法優(yōu)化
綁定事件
bindtap="click" 替換為 @tap="click",
取消冒泡
原catchtap="click"替換為@tap.stop="click"。
捕獲監(jiān)聽事件
capture-bind:tap="click" 替換為 @tap.capture="click",
中斷捕獲監(jiān)聽
capture-catch:tap=“click"替換為 @tap.capture.stop="click"。
Taro 元素的事件處理和 DOM 元素的很相似。但是有一點(diǎn)語法上的不同:
Taro 事件綁定屬性的命名采用駝峰式寫法,而不是小寫。 如果采用 JSX 的語法你需要傳入一個(gè)函數(shù)作為事件處理函數(shù),而不是一個(gè)字符串 (DOM 元素的寫法)。 例如,傳統(tǒng)的微信小程序模板:
<button onclick="activateLasers"> Activate Lasers </button> |
Taro 中稍稍有點(diǎn)不同:
<button onClick={this.activateLasers}>
Activate Lasers
</button>
|
在 Taro 中另一個(gè)不同是你不能使用 catchEvent 的方式阻止事件冒泡。你必須明確的使用 stopPropagation。例如,阻止事件冒泡你可以這樣寫:

class Toggle extends React.Component {
constructor (props) {
super(props)
this.state = {isToggleOn: true}
}
onClick = (e) => {
e.stopPropagation()
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}))
}
render () {
return (
<button onClick={this.onClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
)
}
}
|

wepy對(duì)wx.request做了接受參數(shù)的修改,值得一提的是它提供了針對(duì)全局的intercapter攔截器

// 原生代碼:
wx.request({
url: 'xxx',
success: function (data) {
console.log(data);
}
});
// WePY 使用方式, 需要開啟 Promise 支持,參考開發(fā)規(guī)范章節(jié)
wepy.request('xxxx').then((d) => console.log(d));
// async/await 的使用方式, 需要開啟 Promise 和 async/await 支持,參考 WIKI
async function request () {
let d = await wepy.request('xxxxx');
console.log(d);
}
|

攔截器

import wepy from 'wepy';
export default class extends wepy.app {
constructor () {
// this is not allowed before super()
super();
// 攔截request請(qǐng)求
this.intercept('request', {
// 發(fā)出請(qǐng)求時(shí)的回調(diào)函數(shù)
config (p) {
// 對(duì)所有request請(qǐng)求中的OBJECT參數(shù)對(duì)象統(tǒng)一附加時(shí)間戳屬性
p.timestamp = +new Date();
console.log('config request: ', p);
// 必須返回OBJECT參數(shù)對(duì)象,否則無法發(fā)送請(qǐng)求到服務(wù)端
return p;
},
// 請(qǐng)求成功后的回調(diào)函數(shù)
success (p) {
// 可以在這里對(duì)收到的響應(yīng)數(shù)據(jù)對(duì)象進(jìn)行加工處理
console.log('request success: ', p);
// 必須返回響應(yīng)數(shù)據(jù)對(duì)象,否則后續(xù)無法對(duì)響應(yīng)數(shù)據(jù)進(jìn)行處理
return p;
},
//請(qǐng)求失敗后的回調(diào)函數(shù)
fail (p) {
console.log('request fail: ', p);
// 必須返回響應(yīng)數(shù)據(jù)對(duì)象,否則后續(xù)無法對(duì)響應(yīng)數(shù)據(jù)進(jìn)行處理
return p;
},
// 請(qǐng)求完成時(shí)的回調(diào)函數(shù)(請(qǐng)求成功或失敗都會(huì)被執(zhí)行)
complete (p) {
console.log('request complete: ', p);
}
});
}
}
|

taro對(duì)request進(jìn)行了二次封裝,可以使用Taro.request(OBJECT)發(fā)起網(wǎng)絡(luò)請(qǐng)求,支持 Promise 化使用。



import Taro from '@tarojs/taro'
Taro.request({
url: 'http://localhost:8080/test',
data: {
foo: 'foo',
bar: 10
},
header: {
'content-type': 'application/json'
}
})
.then(res => console.log(res.data))
復(fù)制代碼
|
mpvue沒有對(duì)request做特殊優(yōu)化,與原生相同,可以自己根據(jù)需要進(jìn)行封裝
wepy 可引用Redux和Mbox,目前wepy的腳手架內(nèi)已經(jīng)集成了redux,選擇需要即可;
mpVue使用vuex
taro使用Redux