微信小程序webview与h5通过postMessage实现实时通讯的实现
在做ReactNative应用时,如果需要在App里面内嵌H5页面,那么H5与App之间可以通过Webview的PostMessage功能实现实时的通讯,但是在小程序里面,虽然也提供了一个webview组件,但是,在进行postMessage通讯时,官方文档里面给出了一条很变态的说明:
网页向小程序postMessage时,会在特定时机(小程序后退、组件销毁、分享)触发并收到消息。e.detail={data},data是多次postMessage的参数组成的数组
这里面已经说的很明白了,不管我们从H5页面里面postMessage多少次,小程序都是收不到的,除非:
- 用户做了回退到上一页的操作
- 组件销毁
- 用户点击了分享
这里面其实我没有完全说对,官方其实说的是小程序后退,并没有说是用户做回退操作,经过我的实测,确实人家表达得很清楚了,我们通过微信官方的SDK调起的回退也是完全可行的:
wx.miniProgram.navigateBack()
大体思路
从上面的分析和实测中我们可以知道,要实现无需要用户操作即可完成的通讯,第三种情况我们是完全不需要考虑了的,那么来仔细考虑第1和第2种场景。
第1种方式:回退
当我们想通过网页向小程序发送数据,同时还可以回退到上一个页面时,我们可以在wx.miniProgram.postMessage之后,立马调用一次wx.miniProgram.navigateBack(),此时小程序的操作是:
- 处理postMessage信息
- 回退到上一页
我们在处理postMessage的时候做一些特殊操作,可以将这些数据保存下来
第2种方式:组件销毁
这是我感觉最合适的一种方式,可以让小程序拿到数据,同时还保留在当前页面,只需要销毁一次webview即可,大概的流程就是:
- 小程序postMessage
- 小程序navigateTo将小程序页面导向一个特殊的页面
- 小程序的那个特殊页面立马回退到webview所在的页面
- webview所在的页面的onShow里面,做一次处理,将webview销毁,然后再次打开
- 触发onMessage拿到数据
- H5页面再次被打开
这种方式虽然变态,但是至少可以做到实时拿到数据,同时还保留在当前H5页面,唯一需要解决的是,在做这整套操作前,H5页面需要做好状态的缓存,要不然,再次打开之后,H5的数据就清空了。
第1种方式:通过回退,将数据提交给小程序之后传递给webview的上一页面
这种方式实现起来其实是很简单的,我们现在新建两个页面:
sandbox/canvas-by-webapp/index.js
constapp=getApp(); Page({ data:{ url:'', dimension:null, mime:'', }, handleSaveTap:function(){ wx.navigateTo({ url:'/apps/browser/index', events:{ receiveData:data=>{ console.log('receiveDatafromwebbrowser:',data); if(typeofdata==='object'){ const{url,mime,dimension}=data; if(url&&mime&&dimension){ this.setData({ url, dimension, mime, }); this.save(data); } } } } }) }, save:asyncfunction({url,mime,dimension}){ try{ awaitapp.saveImages([url]); app.toast('保存成功!'); }catch(error){ console.log(error); app.toast(error.message||error); } }, });
上面的代码中,核心点,就在于wx.navigateTo调用时,里面的events参数,这是用来进行与/apps/browser/index页面通讯,接收数据用的。
apps/browser/index.js
我省略了绝大多数与本文无关的代码,保存最主要的三个:
Page({ onLoad(){ if(this.getOpenerEventChannel){ this.eventChannel=this.getOpenerEventChannel(); } }, handleMessage:function(message){ const{action,data}=message; if(action==='postData'){ if(this.eventChannel){ this.eventChannel.emit('receiveData',data); } } }, handlePostMessage:function(e){ const{data}=e.detail; if(Array.isArray(data)){ constmessages=data.map(item=>{ try{ constobject=JSON.parse(item); this.handleMessage(object); returnobject; }catch(error){ returnitem; } }); this.setData({ messages:[...messages], }); } }, })
其实,onLoad方法中,我们使用了自微信SDK2.7.3版本开始提供的getOpenerEventChannel方法,它可以创建一个与上一个页面的事件通讯通道,这个我们会在handleMessage中使用。
handlePostMessage就是被bindmessage至webview上面的方法,它用于处理从H5页面中postMessage过来的消息,由于小程序是将多次postMessage的消息放在一起发送过来的,所以,与其它的Webview不同点在于,我们拿到的是一个数组:e.detail.data,handlePostMessage的作用就是遍历这个数组,取出每一条消息,然后交由handleMessage处理。
handleMessage在拿到message对象之后,将message.action与message.data取出来(*这里需要注意,这是我们在H5里面的设计的一种数据结构,你完全可以在自己的项目中设计自己的结构),根据action作不同的操作,我在这里面的处理是,当action==='postData'时,就通过getOpenerEventChannel得到的消息通道this.eventChannel将数据推送给上一级页面,也就是/sandbox/canvas-by-webapp,但是不需要自己执行navigateBack,因为这个需要交由H5页面去执行。
H5页面的实现
我的H5主要就是使用html2canvas库生成Canvas图(没办法,自己在小程序里面画太麻烦了),但是这个不在本文讨论过程中,我们就当是已经生成了canvas图片了,将其转为base64文本了,然后像下面这样做:
wx.miniProgram.postMessage({ data:JSON.stringify({ action:'postData', data:'BASE64IMAGESTRING' }) }); wx.miniProgram.navigateBack()
将数据postMessage之后,立即navigateBack(),来触发一次回退,也就触发了bindmessage事件。
使用销毁webview实现实时通讯
接下来,咱就开始本文的重点了,比较变态的方式,但是也没想到更好的办法,所以,大家将就着交流吧。
H5页面的改变
wx.miniProgram.postMessage({ data:JSON.stringify({ action:'postData', data:'BASE64IMAGESTRING' }) }); wx.miniProgram.navigateTo('/apps/browser/placeholder');
H5页面只是将wx.miniProgram.navigateBack()改成了wx.miniProgram.navigateTo('/apps/browser/placeholder'),其它的事情就先都交由小程序处理了。
/apps/browser/placeholder
这个页面的功能其实很简单,当打开它了之后,做一点点小操作,立马回退到上一个页面(就是webview所在的页面。
Page({ data:{loading:true}, onLoad(options){ constpages=getCurrentPages(); constwebviewPage=pages[pages.length-2]; webviewPage.setData( { shouldReattachWebview:true }, ()=>{ app.wechat.navigateBack(); } ); }, });
我们一行一行来看:
constpages=getCurrentPages();
这个可以拿到当前整个小程序的页面栈,由于这个页面我们只允许从小程序的Webview页面过来,所以,它的上一个页面一定是webview所在的页面:
constwebviewPage=pages[pages.length-2];
拿到webviewPage这个页面对象之后,调用它的方法setData更新一个值:
webviewPage.setData( { shouldReattachWebview:true }, ()=>{ app.wechat.navigateBack(); } );
shouldReattachWebview这个值为true的时候,表示需要重新attach一次webview,这个页面的事件现在已经做完了,回到webview所在的页面
apps/browser/index.js页面
我同样只保留最核心的代码,具体的逻辑,我就直接写进代码里面了。
Page({ data:{ shouldReattachWebview:false,//是否需要重新attach一次webview组件 webviewReattached:false,//是否已经attach过一次webview了 hideWebview:false//是否隐藏webview组件 }, onShow(){ //如果webview需要重新attach if(this.data.shouldReattachWebview){ this.setData( { //隐藏webview hideWebview:true, }, ()=>{ this.setData( { //隐藏之后立马显示它,此时完成一次webview的销毁,拿到了postMessage中的数据 hideWebview:false, webviewReattached:true, }, ()=>{ //拿到数据之后,处理canvasData this.handleCanvasData(); } ); } ); } }, //当webview被销毁时,该方法被触发 handlePostMessage:function(e){ const{data}=e.detail; if(Array.isArray(data)){ constmessages=data.map(item=>{ try{ constobject=JSON.parse(item); this.handleMessage(object); returnobject; }catch(error){ returnitem; } }); this.setData({ messages:[...messages], }); } }, //处理每一条消息 handleMessage:function(message){ const{action,data}=message //如果saveCanvasaction if(action==='saveCanvas'){ //将数据先缓存进Snap中 const{canvasData}=this.data; //app.checksum是我自己封装的方法,计算任何数据的checksum,我拿它来当作key //这可以保证同一条数据只会被处理一次 constsnapKey=app.checksum(data); //只要未处理过的数据,才需要再次数据 if(canvasData[snapKey]!==true){ if(canvasData[snapKey]===undefined){ //将数据从缓存进`snap`中 //这也是我自己封装的一个方法,可以将数据缓存起来,并且只能被读取一次 app.snap(snapKey,data); //设置canvasData中snapKey字段为`false` canvasData[snapKey]=false; this.setData({ canvasData, }); } } } }, //当webview被重新attach之后,canvas数据已经被保存进snap中了, handleCanvasData:asyncfunctionhandleCanvasData(){ const{canvasData}=this.data; //从canvasData中拿到所有的key,并过滤到已经处理过的数据 constkeys=Object.keys(canvasData).filter(key=>canvasData[key]===false); if(keys.length===0){ return; } for(leti=0;i对应的index.wxml文件内容如下:
流程回顾与总结
- 打开webview页面,打开h5
- h5页面生成canvas图,并转为base64字符
- 通过wx.miniProgram.postMessage将base64发送给小程序
- 调用wx.miniProgram.navigateTo将页面导向一个特殊页面
- 在特殊页面中,将webview所在页面的shouldReattachWebview设置为true
- 在特殊页面中回退至webview所在页面
- webview所在页面的onShow事件被触发
- 在onShow事件检测shouldReattachWebview是否为true,若为true
- 将hideWebview设置为true,引起web-view组件的销毁
- handlePostMessage被触发,解析所有的message之后交给handleMessage逐条处理
- handleMessage发现action==='saveCanvas'的事件,拿到data
- 根据data计算checksum,以checksum为key缓存下来数据,并将这个checksum保存到canvasData对象中
- 此时hideWebview被onShow里面setData的回调中的setData重新置为false,web-view重新加attach,H5页面重新加载
- webview重新attach之后,this.handleCanvasData被触发,
- handleCanvasData检测是否有需要保存的canvas数据,如果有,保存,修改canvasData状态
整个流程看旧去很繁琐,但是写起来其实还好,这里面最主要的是需要注意,数据去重,微信的postMessage里面拿到的永远都是H5页面从被打开到关闭的所有数据。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。