微信小程序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 相关文章推荐
自己的js工具 Cookie 封装
Aug 21 Javascript
IE的fireEvent方法概述及应用
Feb 22 Javascript
JavaScript动态创建div属性和样式示例代码
Oct 09 Javascript
jquery实现滑动图片自己测试的例子
Nov 05 Javascript
jQuery简单实现input文本框内灰色提示文本效果的方法
Dec 02 Javascript
javascript单页面手势滑屏切换原理详解
Mar 21 Javascript
js实现下一页页码效果
Mar 07 Javascript
原生JS与jQuery编写简单选项卡
Oct 30 jQuery
利用JQUERY实现多个AJAX请求等待的实例
Dec 14 jQuery
vue路由拦截及页面跳转的设置方法
May 24 Javascript
基于JavaScript 实现拖放功能
Sep 12 Javascript
详谈vue中router-link和传统a链接的区别
Jul 22 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 存储文本换行实现方法
2010/01/05 PHP
PHP中strtotime函数使用方法分享
2012/01/10 PHP
PHP中CURL的CURLOPT_POSTFIELDS参数使用细节
2014/03/17 PHP
php写入、删除与复制文件的方法
2015/06/20 PHP
利用PHP判断是否是连乘数字串的方法示例
2017/07/03 PHP
jquery 读取页面load get post ajax 四种方式代码写法
2011/04/02 Javascript
jquery mobile事件多次绑定示例代码
2013/09/13 Javascript
JavaScript数据结构之二叉树的计数算法示例
2017/04/13 Javascript
jQuery实现动态生成表格并为行绑定单击变色动作的方法
2017/04/17 jQuery
Iphone手机、安卓手机浏览器控制默认缩放大小的方法总结(附代码)
2017/08/18 Javascript
JQuery特殊效果和链式调用操作示例
2019/05/13 jQuery
聊聊Vue 中 title 的动态修改问题
2019/06/11 Javascript
[57:50]DOTA2上海特级锦标赛主赛事日 - 4 胜者组决赛Secret VS Liquid第二局
2016/03/05 DOTA
[01:00:59]VP VS VG Supermajor小组赛胜者组第二轮 BO3第二场 6.2
2018/06/03 DOTA
[03:08]TI9战队档案 - Vici Gaming
2019/08/20 DOTA
[40:31]Secret vs Alliacne 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/17 DOTA
Python深入学习之特殊方法与多范式
2014/08/31 Python
python中__call__方法示例分析
2014/10/11 Python
Windows和Linux下使用Python访问SqlServer的方法介绍
2015/03/10 Python
Python+matplotlib实现计算两个信号的交叉谱密度实例
2018/01/08 Python
Pandas标记删除重复记录的方法
2018/04/08 Python
详解Python3.6的py文件打包生成exe
2018/07/13 Python
Python3.5装饰器原理及应用实例详解
2019/04/30 Python
pycharm中显示CSS提示的知识点总结
2019/07/29 Python
python读写csv文件的方法
2019/08/13 Python
Python对接支付宝支付自实现功能
2019/10/10 Python
python TK库简单应用(实时显示子进程输出)
2019/10/29 Python
pycharm实现在子类中添加一个父类没有的属性
2020/03/12 Python
Python3爬虫里关于识别微博宫格验证码的知识点详解
2020/07/30 Python
Python3 ffmpeg视频转换工具使用方法解析
2020/08/10 Python
python把一个字符串切开的实例方法
2020/09/27 Python
程序员跳槽必看面试题总结
2013/06/28 面试题
三严三实民主生活会发言稿
2014/10/13 职场文书
2015年幼儿园国庆节活动总结
2015/07/30 职场文书
员工保密协议范本,您一定得收藏!很有用!
2019/08/08 职场文书
ant design charts 获取后端接口数据展示
2022/05/25 Javascript