背景
在一个程序中,或多或少的会有非主线功能的嵌入。例如电商软件的活动页(砸金蛋,抢红包等),其与软件的主线关联较少,并且需要具有快速迭代、灵活的特性,当考虑到这部分功能的开发时,大多数人会考虑到将这类项目分离出来,例如考虑iframe。而在小程序端,腾讯所提供的webview组件是很友好的,目前为止对于展示类的webview的开发所提供的api基本上能满足我们的需求。但是,一向老沉的腾讯当然不会在小程序到webview的通讯上给我们很好的支持,让我们看看文档:
诚然,webview组件提供了bindmessage的方法让网页向小程序传递消息,可是却会在特定时机(小程序后退/组件销毁/分享)触发并收到消息,这种异步的消息抵达机制显然不符合我们的需求。那么,既然网页=>小程序这条路已经不可行了,不妨想想另一条路:小程序=>网页是否有可能。而从小程序向网页传递信息也是必须依赖于腾讯的api的,通过阅读文档,我们发现
web-view网页中可使用JSSDK 1.3.2提供的接口返回小程序页面支持的接口有:wx.miniProgram.navigateTo
在往常的小程序开发中,页面之间的通信就是通过wx.navigateTo实现的。若如法炮制,或许能找到突破口。
功能实现
1.支付
在前端调起微信支付的方法十分简单,只需向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页面在支付完成后立即刷新结果(或者触发任何一个事件)是一个比较棘手的问题。经过讨论,我们想到了两种方案:
(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)中的弹窗方法。
2.鉴权
其实这个问题应该在支付之前就解决了的。既然是一个功能性的webview应用,那用户的行为被记录下来是必然的需求(这也是支付的前提)。本次门店培训师项目的后端与小程序商家端本质上是独立的,而用户登录的逻辑是在小程序商家端的服务器上进行的。因此,本次后端才用了转发的(包一层)的方法,所有的请求会先到达小程序商家端的服务器上,在鉴权完成后,请求会被转发到门店培训师的服务器。 在前端这边最重要的是将代表用户信息的userKey传递给h5(实际上这也是本项目唯一一个小程序向至h5方向通信的例子):
this.setData({
url: `${this.data.url}?storeId=${storeId}&userKey=${userKey}`
//webview组件的src
})
复制代码
值得注意的是,此种通信的方式虽然达到了从小程序向h5通信的需求,但是webview组件的src一旦变更,之前所存在的页面栈会被销毁,所有我们只能在h5初始化的时候将代表用户信息的userKey传递过去,在h5页面被销毁之前,任何对webview组件src的操作都会导致上述问题。
3.如果我还想用其他小程序的api...
其实这个看起来像一个伪命题:既然这么想用小程序的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)
}
})
}
复制代码
Review
1.优雅的路由传参
复习一下:在小程序中开发功能性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));
...
复制代码
2.组件化
若我们想在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;
...
}
},
复制代码
3.对业务的思考
其实业务才是最本质的问题,前面我们所提及的都是关于怎么去做一件事情,但是为什么去做却没有提及。从公司目前的业务考虑,比如我们的用户端小程序中会时不时增加一些与主线业务无关的应用:服务经理报名,门店线上报名...不断增加的功能会持续性地增加小程序的体积,对于一个承载用户量十分巨大的程序,新功能开发频繁的更新迭代所导致的发版也是十分麻烦的。目前很多场景下,都是通过再增加一个小程序,并在原来的主小程序中增加一个入口达到的,所以我们不断地注册小程序... 然而,这些不断增加的"小小程序"虽然达到了粒度的要求,但却只能局限小程序的生态中。如果我们想在公司官网,亦或是公众号也增加一个服务经理报名的入口,就只能通过贴小程序二维码这种方式达到了。在跨场景的问题上,h5应用还是具有很大的优势的。但是移植到其他环境时,在需要利用小程序api的地方,我们则需要针对不同的环境对实现方法进行改写适配(所以很需要面向对象的写法以及代码的粒度)。虽然本文讲述的是如何进行功能性小程序webview页面开发,但目前我还是觉得最好不要在webview界面嵌入一个功能性过多的h5应用,如果有这个需求,可以看看此文。