微信小程序webview与h5通过postMessage实现实时通讯的实现


Posted in Javascript onAugust 20, 2019

在做 React Native 应用时,如果需要在 App 里面内嵌 H5 页面,那么 H5 与 App 之间可以通过 Webview 的 PostMessage 功能实现实时的通讯,但是在小程序里面,虽然也提供了一个 webview 组件,但是,在进行 postMessage 通讯时,官方文档里面给出了一条很变态的说明:

网页向小程序 postMessage 时,会在特定时机(小程序后退、组件销毁、分享)触发并收到消息。e.detail = { data },data 是多次 postMessage 的参数组成的数组
这里面已经说的很明白了,不管我们从 H5 页面里面 postMessage 多少次,小程序都是收不到的,除非:

  1. 用户做了回退到上一页的操作
  2. 组件销毁
  3. 用户点击了分享

这里面其实我没有完全说对,官方其实说的是 小程序后退,并没有说是用户做回退操作,经过我的实测,确实人家表达得很清楚了,我们通过微信官方的SDK调起的回退也是完全可行的:

wx.miniProgram.navigateBack()

大体思路

从上面的分析和实测中我们可以知道,要实现无需要用户操作即可完成的通讯,第三种情况我们是完全不需要考虑了的,那么来仔细考虑第 1 和第 2 种场景。

第 1 种方式:回退

当我们想通过网页向小程序发送数据,同时还可以回退到上一个页面时,我们可以在 wx.miniProgram.postMessage 之后,立马调用一次 wx.miniProgram.navigateBack(),此时小程序的操作是:

  1. 处理 postMessage 信息
  2. 回退到上一页

我们在处理 postMessage 的时候做一些特殊操作,可以将这些数据保存下来

第 2 种方式:组件销毁

这是我感觉最合适的一种方式,可以让小程序拿到数据,同时还保留在当前页面,只需要销毁一次 webview 即可,大概的流程就是:

  1. 小程序 postMessage
  2. 小程序 navigateTo 将小程序页面导向一个特殊的页面
  3. 小程序的那个特殊页面立马回退到 webview 所在的页面
  4. webview 所在的页面的 onShow 里面,做一次处理,将 webview 销毁,然后再次打开
  5. 触发 onMessage 拿到数据
  6. H5 页面再次被打开

这种方式虽然变态,但是至少可以做到实时拿到数据,同时还保留在当前 H5 页面,唯一需要解决的是,在做这整套操作前,H5 页面需要做好状态的缓存,要不然,再次打开之后,H5 的数据就清空了。

第 1 种方式:通过回退,将数据提交给小程序之后传递给 webview 的上一页面

这种方式实现起来其实是很简单的,我们现在新建两个页面:

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 页面的实现

我的 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 实现实时通讯
接下来,咱就开始本文的重点了,比较变态的方式,但是也没想到更好的办法,所以,大家将就着交流吧。

H5 页面的改变

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}}" />

流程回顾与总结

  1. 打开 webview 页面,打开 h5
  2. h5 页面生成 canvas 图,并转为 base64 字符
  3. 通过 wx.miniProgram.postMessage 将 base64 发送给小程序
  4. 调用 wx.miniProgram.navigateTo 将页面导向一个特殊页面
  5. 在特殊页面中,将 webview 所在页面的 shouldReattachWebview 设置为 true
  6. 在特殊页面中回退至 webview 所在页面
  7. webview 所在页面的 onShow 事件被触发
  8. 在 onShow 事件检测 shouldReattachWebview 是否为 true,若为 true
  9. 将 hideWebview 设置为 true,引起 web-view 组件的销毁
  10. handlePostMessage 被触发,解析所有的 message 之后交给 handleMessage 逐条处理
  11. handleMessage 发现 action === 'saveCanvas' 的事件,拿到 data
  12. 根据 data 计算 checksum ,以 checksum 为 key 缓存下来数据,并将这个 checksum 保存到 canvasData 对象中
  13. 此时 hideWebview 被 onShow 里面 setData 的回调中的 setData 重新置为 false,web-view 重新加 attach,H5页面重新加载
  14. webview 重新 attach 之后, this.handleCanvasData 被触发,
  15. handleCanvasData 检测是否有需要保存的 canvas 数据,如果有,保存,修改 canvasData 状态

整个流程看旧去很繁琐,但是写起来其实还好,这里面最主要的是需要注意,数据去重,微信的 postMessage 里面拿到的永远 都是 H5 页面从被打开到关闭的所有数据。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
web css实现整站样式互相切换
Oct 29 Javascript
JQuery选择器、过滤器大整理
May 26 Javascript
JavaScript实现数据类型的相互转换
Mar 06 Javascript
基于bootstrap风格的弹框插件
Dec 28 Javascript
深入理解Commonjs规范及Node模块实现
May 17 Javascript
node中Express 动态设置端口的方法
Aug 04 Javascript
使用 vue.js 构建大型单页应用
Feb 10 Javascript
vuex提交state&amp;&amp;实时监听state数据的改变方法
Sep 16 Javascript
在vue项目中使用codemirror插件实现代码编辑器功能
Aug 27 Javascript
LayUI数据接口返回实体封装的例子
Sep 12 Javascript
Node.js API详解之 vm模块用法实例分析
May 27 Javascript
9个JavaScript日常开发小技巧
Oct 06 Javascript
JS中的算法与数据结构之队列(Queue)实例详解
Aug 20 #Javascript
JS中的算法与数据结构之栈(Stack)实例详解
Aug 20 #Javascript
小程序如何在不同设备上自适应生成海报的实现方法
Aug 20 #Javascript
使用 Vue 实现一个虚拟列表的方法
Aug 20 #Javascript
基于vue手写tree插件的那点事儿
Aug 20 #Javascript
详解基于原生JS验证表单组件xy-form
Aug 20 #Javascript
详解微信小程序图片地扯转base64解决方案
Aug 18 #Javascript
You might like
如何对PHP程序中的常见漏洞进行攻击(下)
2006/10/09 PHP
php 多关键字 高亮显示实现代码
2012/04/23 PHP
WordPress中用于获取搜索表单的PHP函数使用解析
2016/01/05 PHP
PHP 文件锁与进程锁的使用示例
2017/08/07 PHP
灵活应用js调试技巧解决样式问题的步骤分享
2012/03/15 Javascript
解析URI与URL之间的区别与联系
2013/11/22 Javascript
jQuery操作select下拉框的text值和value值的方法
2014/05/31 Javascript
JavaScript中Null与Undefined的区别解析
2015/06/30 Javascript
javascript引用类型之时间Date和数组Array
2015/08/27 Javascript
JS实现为排序好的字符串找出重复行的方法
2016/03/02 Javascript
基于JavaScript实现自定义滚动条
2017/01/25 Javascript
Angularjs实现上传图片预览功能
2017/09/01 Javascript
Vue中的v-for循环key属性注意事项小结
2018/08/12 Javascript
js实现网页同时进行多个倒计时功能
2019/02/25 Javascript
在element-ui的select下拉框加上滚动加载
2019/04/18 Javascript
更强大的vue ssr实现预取数据的方式
2019/07/19 Javascript
解决Vue的文本编辑器 vue-quill-editor 小图标样式排布错乱问题
2020/08/03 Javascript
[01:12]快闪回顾DOTA2亚洲邀请赛(DAC) 静候2018新征程开启
2018/03/11 DOTA
Python中的super()方法使用简介
2015/08/14 Python
python脚本实现xls(xlsx)转成csv
2016/04/10 Python
Python模块搜索路径代码详解
2018/01/29 Python
python 获取当天凌晨零点的时间戳方法
2018/05/22 Python
Python3实现取图片中特定的像素替换指定的颜色示例
2019/01/24 Python
python 获取毫秒数,计算调用时长的方法
2019/02/20 Python
PyQt5 closeEvent关闭事件退出提示框原理解析
2020/01/08 Python
基于selenium及python实现下拉选项定位select
2020/07/22 Python
韩国家庭购物网上商店:Nsmall
2017/05/07 全球购物
美国餐厅用品和厨房设备批发网站:KaTom Restaurant Supply
2018/01/27 全球购物
统计每一学生的平均成绩
2014/06/06 面试题
中专毕业生求职简历的自我评价
2013/10/21 职场文书
宠物店的创业计划书范文
2014/01/11 职场文书
预备党员对照检查材料思想汇报
2014/09/24 职场文书
群众路线自查自纠工作情况报告
2014/10/28 职场文书
2015年学校信息技术工作总结
2015/05/25 职场文书
2019安全宣传标语大全
2019/08/14 职场文书
Tomcat配置访问日志和线程数
2022/05/06 Servers