原文:https://pantao.parcmg.com/pre...html
在作 React Native 应用时,若是须要在 App 里面内嵌 H5 页面,那么 H5 与 App 之间能够经过 Webview 的 PostMessage 功能实现实时的通信,可是在小程序里面,虽然也提供了一个 webview
组件,可是,在进行 postMessage
通信时,官方文档里面给出了一条很变态的说明:web
网页向小程序postMessage
时,会在特定时机(小程序后退、组件销毁、分享)触发并收到消息。e.detail = { data }
,data
是屡次postMessage
的参数组成的数组
这里面已经说的很明白了,无论咱们从 H5 页面里面 postMessage
多少次,小程序都是收不到的,除非:canvas
这里面其实我没有彻底说对,官方其实说的是 小程序后退,并无说是用户作回退操做,通过个人实测,确实人家表达得很清楚了,咱们经过微信官方的SDK调起的回退也是彻底可行的:小程序
wx.miniProgram.navigateBack()
从上面的分析和实测中咱们能够知道,要实现无须要用户操做便可完成的通信,第三种状况咱们是彻底不须要考虑了的,那么来仔细考虑第 1 和第 2 种场景。数组
当咱们想经过网页向小程序发送数据,同时还能够回退到上一个页面时,咱们能够在 wx.miniProgram.postMessage
以后,立马调用一次 wx.miniProgram.navigateBack()
,此时小程序的操做是:缓存
postMessage
信息咱们在处理 postMessage
的时候作一些特殊操做,能够将这些数据保存下来微信
这是我感受最合适的一种方式,可让小程序拿到数据,同时还保留在当前页面,只须要销毁一次 webview
便可,大概的流程就是:数据结构
postMessage
navigateTo
将小程序页面导向一个特殊的页面webview
所在的页面webview
所在的页面的 onShow
里面,作一次处理,将 webview
销毁,而后再次打开onMessage
拿到数据这种方式虽然变态,可是至少能够作到实时拿到数据,同时还保留在当前 H5 页面,惟一须要解决的是,在作这整套操做前,H5 页面须要作好状态的缓存,要否则,再次打开以后,H5 的数据就清空了。app
webview
的上一页面这种方式实现起来实际上是很简单的,咱们如今新建两个页面:webapp
sandbox/canvas-by-webapp/index.js
const app = getApp(); Page({ data: { url: '', dimension: null, mime: '', }, handleSaveTap: function() { wx.navigateTo({ url: '/apps/browser/index', events: { receiveData: data => { console.log('receiveData from web browser: ', data); if (typeof data === 'object') { const { url, mime, dimension } = data; if (url && mime && dimension) { this.setData({ url, dimension, mime, }); this.save(data); } } } } }) }, save: async function({ url, mime, dimension }) { try { await app.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)) { const messages = data.map(item => { try { const object = JSON.parse(item); this.handleMessage(object); return object; } catch (error) { return item; } }); this.setData({ messages: [...messages], }); } }, })
其实,onLoad
方法中,咱们使用了自微信 SDK 2.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 主要就是使用 html2canvas
库生成 Canvas 图(没办法,本身在小程序里面画太麻烦了),可是这个不在本文讨论过程当中,咱们就当是已经生成了 canvas
图片了,将其转为 base64
文本了,而后像下面这样作:
wx.miniProgram.postMessage({ data: JSON.stringify({ action: 'postData', data: 'BASE 64 IMAGE STRING' }) }); wx.miniProgram.navigateBack()
将数据 postMessage
以后,当即 navigateBack()
,来触发一次回退,也就触发了 bindmessage
事件。
webview
实现实时通信接下来,咱就开始本文的重点了,比较变态的方式,可是也没想到更好的办法,因此,你们将就着交流吧。
wx.miniProgram.postMessage({ data: JSON.stringify({ action: 'postData', data: 'BASE 64 IMAGE STRING' }) }); 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) { const pages = getCurrentPages(); const webviewPage = pages[pages.length - 2]; webviewPage.setData( { shouldReattachWebview: true }, () => { app.wechat.navigateBack(); } ); }, });
咱们一行一行来看:
const pages = getCurrentPages();
这个能够拿到当前整个小程序的页面栈,因为这个页面咱们只容许从小程序的 Webview
页面过来,因此,它的上一个页面必定是 webview
所在的页面:
const webviewPage = 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)) { const messages = data.map(item => { try { const object = JSON.parse(item); this.handleMessage(object); return object; } catch (error) { return item; } }); this.setData({ messages: [...messages], }); } }, // 处理每一条消息 handleMessage: function(message) { const {action, data} = message // 若是 saveCanvas action if (action === 'saveCanvas') { // 将数据先缓存进 Snap 中 const { canvasData } = this.data; // app.checksum 是我本身封装的方法,计算任何数据的 checksum,我拿它来看成 key // 这能够保证同一条数据只会被处理一次 const snapKey = 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: async function handleCanvasData() { const { canvasData } = this.data; // 从 canvasData 中拿到全部的 key,并过滤到已经处理过的数据 const keys = Object.keys(canvasData).filter(key => canvasData[key] === false); if (keys.length === 0) { return; } for (let i = 0; i < keys.length; i += 1) { try { const key = keys[i]; const { url } = app.snap(key); // 经过本身封装的方法,将 url(也就是Base64字符)保存至相册 const saved = await app.saveImages(url); // 更新 canvasData 对象 canvasData[key] = true this.setData({ canvasData }) console.log('saved: ', saved); } catch (error) { app.toast(error.message); return; } } }, })
对应的 index.wxml
文件内容以下:
<web-view src="{{src}}" wx:if="{{src}}" bindmessage="handlePostMessage" wx:if="{{!hideWebview}}" />
webview
页面,打开 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 页面从被打开到关闭的全部数据。