在一個程序中,或多或少的會有非主線功能的嵌入。例如電商軟件的活動頁(砸金蛋,搶紅包等),其與軟件的主線關聯較少,而且須要具備快速迭代、靈活的特性,當考慮到這部分功能的開發時,大多數人會考慮到將這類項目分離出來,例如考慮iframe。而在小程序端,騰訊所提供的webview組件是很友好的,目前爲止對於展現類的webview的開發所提供的api基本上能知足咱們的需求。可是,一貫老沉的騰訊固然不會在小程序到webview的通信上給咱們很好的支持,讓咱們看看文檔:前端
誠然,webview組件提供了bindmessage的方法讓網頁向小程序傳遞消息,但是卻會在特定時機(小程序後退/組件銷燬/分享)觸發並收到消息,這種異步的消息抵達機制顯然不符合咱們的需求。那麼,既然網頁=>小程序這條路已經不可行了,不妨想一想另外一條路:小程序=>網頁是否有可能。而從小程序向網頁傳遞信息也是必須依賴於騰訊的api的,經過閱讀文檔,咱們發現vue
web-view網頁中可以使用JSSDK 1.3.2提供的接口返回小程序頁面支持的接口有:wx.miniProgram.navigateToweb
在往常的小程序開發中,頁面之間的通訊就是經過wx.navigateTo實現的。若如法炮製,或許能找到突破口。小程序
在前端調起微信支付的方法十分簡單,只需向server發送訂單參數獲得包括appId,nonceStr,package,paySign,signType,timeStamp這六個參數,而後在小程序環境經過 wx.requestPayment調起便可,基於上述的條件,咱們想到的方案就是當用戶點擊支付按鈕時,在webview中經過wx.miniProgram.navigateTo將從server獲取的6個支付參數傳遞到另外一個小程序頁面,並在callback中返回當前webview所在的頁面:後端
handlePay(data){
let _this = this;
new Promise((resolve, reject) => {
wx.requestPayment({
...data,
success(res) {
resolve("成功!")
},
fail(err) {
resolve("失敗!")
}
})
}).then(outcome => {
wx.navigateBack({
delta: 1,
success() {
wx.showToast({
title: '支付' + outcome,
icon: "none"
})
}
})
})
}
複製代碼
若是咱們在小程序的onLoad鉤子中調用handlePay方法,便可讓用戶在此頁面只進行支付的操做,並在支付完成(成功或失敗)後當即跳回webview頁面。 在傳統的vue項目中,路由的跳轉會觸發組件的生命週期鉤子,咱們將數據請求放在這些鉤子中以觸發頁面數據的更新。然而上述的回跳本質上是在小程序中的路由變化,在webview頁面的onShow鉤子會被調用,而後對於嵌入其中的h5,只是被存入了內存棧而已。由於,讓h5頁面在支付完成後當即刷新結果(或者觸發任何一個事件)是一個比較棘手的問題。通過討論,咱們想到了兩種方案:api
(1)跨域
this.$wx.miniProgram.navigateTo({
url: "/pages/training/counter/counter?task=pay¶m="+param
});
/*在h5中經過wx的sdk跳到小程序支付界面,將支付參數傳遞*/
setTimeout(() => {
this.$refs.dlg.showDialog();
}, 1000);
/*通過1s後,彈出一個點擊才能關閉的dialog,讓用戶經過點擊觸發頁面的刷新*/
複製代碼
(2)瀏覽器
this.$wx.miniProgram.navigateTo({
url: "/pages/training/counter/counter?task=pay¶m="+param
});
/*在h5中經過wx的sdk跳到小程序支付界面,將支付參數傳遞*/
document.addEventListener("visibilitychange", function() {
/*加入頁面刷新方法*/
});
複製代碼
第(2)種方法在小程序開發工具中沒法使用,在真機調試中奏效(沒有進行各類機型的測試);考慮到此方法涉及到操縱document對象,兼容性有待考量,而且在用戶回退的過程當中可能會引發誤操做的可能。最終,咱們採用了(1)中的彈窗方法。bash
其實這個問題應該在支付以前就解決了的。既然是一個功能性的webview應用,那用戶的行爲被記錄下來是必然的需求(這也是支付的前提)。本次門店培訓師項目的後端與小程序商家端本質上是獨立的,而用戶登陸的邏輯是在小程序商家端的服務器上進行的。所以,本次後端才用了轉發的(包一層)的方法,全部的請求會先到達小程序商家端的服務器上,在鑑權完成後,請求會被轉發到門店培訓師的服務器。 在前端這邊最重要的是將表明用戶信息的userKey傳遞給h5(實際上這也是本項目惟一一個小程序向至h5方向通訊的例子):服務器
this.setData({
url: `${this.data.url}?storeId=${storeId}&userKey=${userKey}`
//webview組件的src
})
複製代碼
值得注意的是,此種通訊的方式雖然達到了從小程序向h5通訊的需求,可是webview組件的src一旦變動,以前所存在的頁面棧會被銷燬,全部咱們只能在h5初始化的時候將表明用戶信息的userKey傳遞過去,在h5頁面被銷燬以前,任何對webview組件src的操做都會致使上述問題。
其實這個看起來像一個僞命題:既然這麼想用小程序的api,爲何還要寫h5。不過,先看一個例子:
在這個界面,有這許多功能的需求,好比複製文字,下載圖片與下載視頻,在H5中解決這個問題比較繁瑣,而且兼容性又是一個坑點。當我遇到這個需求的時候固然仍是第一時間想到了利用了簡潔,兼容性友好的小程序api幫我完整任務。其實實現起來跟支付是相似的,在這裏貼一個下載的實現方式:onLoad: function (options) {
let _this = this;
let { url, type } = options;
wx.navigateBack({//用神不知鬼不覺的速度跳回去
delta: 1,
success() {
/*
由於保存圖片,視頻的操做是須要用戶在小程序中受權後才能進行的,全部優先解決這個問題。
canSave()將在下載操做前返回用戶的受權狀況
*
_this.canSave().then(res =>  {
if (res == "ok") {
_this.wxDownload(url, type);
wx.showToast({
title: '開始下載',
icon: "none"
})
}
}).catch(err => {
console.log(err)
})
},
fail(err) {
throw (err)
}
})
},
canSave() {
return new Promise((reslove,reject) => {
wx.getSetting({
success: (res) => {
if (!res.authSetting['scope.writePhotosAlbum']) {
wx.authorize({//受權彈窗
scope: 'scope.writePhotosAlbum',
success: () => {
reslove("ok")
},
fail: () => {
//第一次拒絕受權後,因爲騰訊的奇怪設定,第二次會直接進fail回調,咱們在此劫持fail函數
wx.showModal({
title: '提示',
content: '請前往設置完成相冊受權操做',
success(res) {
if (res.confirm) {
wx.openSetting({//打開小程序設置權限界面
success(res) {
if (res.authSetting['scope.writePhotosAlbum']) {
//用戶在設置界面贊成了相冊受權
reslove("ok")
}
},
fail(err) {
reject(err)
}
})
} else if (res.cancel) {
reject('用戶點擊取消')
}
}
})
}
})
} else {
reslove("ok")
}
}
})
})
},
wxDownload(url, type) {
wx.downloadFile({
url: url, //下載資源的地址網絡
success: function (res) {
if (res.statusCode === 200) {
wx.playVoice({
filePath: res.tempFilePath
})
}
// 保存視頻到本地
switch (type) {
case "img":
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success:
function (data) {
wx.showToast({
title: '圖片下載成功',
})
},
});
break;
case "video":
wx.saveVideoToPhotosAlbum({
filePath: res.tempFilePath,
success:
function (data) {
wx.showToast({
title: '視頻下載成功'
})
},
});
}
},
fail: function (err) {
console.log(err)
}
})
}
複製代碼
複習一下:在小程序中開發功能性webview頁面,關鍵點在於跨域通訊(我以爲更像是'跨瀏覽器通訊'),而通訊的實現是在h5中利用微信sdkwx.miniProgram.navigateTo
。 以往在作小程序項目中,咱們這麼寫
wx.navigateTo({
url: `/pages/b?id=996`
});
複製代碼
那麼,在跳轉支付的操做中咱們也這麼寫
wx.navigateTo({
url: `/pages/pay?appId=${appId}&nonceStr=${nonceStr}&package=${package}&paySign=${paySign}&timeStamp=${timeStamp}`
});
複製代碼
可是在這樣操做一下,發現傳遞到小程序頁面的參數老是不對的,最後發現:package參數中,含有"=",而它在路由中會被轉義。由於,咱們須要讓它變得優雅一些:
let param = encodeURIComponent(JSON.stringify(res));//對服務端返回的res支付參數加密
this.$wx.miniProgram.navigateTo({
url: "/pages/training/counter/counter?task=pay¶m=" + param
});
//我想,咱們能夠本身封裝一個小程序的路由api,使它能像vue router那樣傳參
複製代碼
而後在小程序的頁面中解密
/*onLoad鉤子*/
let {task,param} = options;
let data = JSON.parse(decodeURIComponent(param));
...
複製代碼
若咱們想在h5中使用小程序所提供的友好api,方法只有一個:跳到一個小程序頁面,再跳回來。若是每使用一個功能,咱們就須要創建一個小程序的頁面,這麼作,顯然是不nice的。能不能讓咱們創造一個小程序頁面(能夠理解爲一個封裝好的組件),讓它只作一件事:輸出可能在h5應用中被用到的全部小程序api。那麼,咱們應該在組件裏這麼寫
onLoad: function(options) {
//利用task參數傳遞須要調用的api
let {task,param} = options;
let data = JSON.parse(decodeURIComponent(param));
switch(task){
case "pay":
this.handlePay(data);
break;
case "download":
this.handleDownload(data);
break;
case "copy":
this.handleCopy(data);
break;
case "preview":
this.handlePreview(data);
break;
...
}
},
複製代碼
其實業務纔是最本質的問題,前面咱們所說起的都是關於怎麼去作一件事情,可是爲何去作卻沒有說起。從公司目前的業務考慮,好比咱們的用戶端小程序中會時不時增長一些與主線業務無關的應用:服務經理報名,門店線上報名...不斷增長的功能會持續性地增長小程序的體積,對於一個承載用戶量十分巨大的程序,新功能開發頻繁的更新迭代所致使的發版也是十分麻煩的。目前不少場景下,都是經過再增長一個小程序,並在原來的主小程序中增長一個入口達到的,因此咱們不斷地註冊小程序... 然而,這些不斷增長的"小小程序"雖然達到了粒度的要求,但卻只能侷限小程序的生態中。若是咱們想在公司官網,亦或是公衆號也增長一個服務經理報名的入口,就只能經過貼小程序二維碼這種方式達到了。在跨場景的問題上,h5應用仍是具備很大的優點的。可是移植到其餘環境時,在須要利用小程序api的地方,咱們則須要針對不一樣的環境對實現方法進行改寫適配(因此很須要面向對象的寫法以及代碼的粒度)。雖然本文講述的是如何進行功能性小程序webview頁面開發,但目前我仍是以爲最好不要在webview界面嵌入一個功能性過多的h5應用,若是有這個需求,能夠看看此文。