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

原文: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

  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 的数据就清空了。app

第 1 种方式:经过回退,将数据提交给小程序以后传递给 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 就是被 bindmessagewebview 上面的方法,它用于处理从 H5 页面中 postMessage 过来的消息,因为小程序是将屡次 postMessage 的消息放在一块儿发送过来的,因此,与其它的Webview不一样点在于,咱们拿到的是一个数组: e.detail.datahandlePostMessage 的做用就是遍历这个数组,取出每一条消息,而后交由 handleMessage 处理。

handleMessage 在拿到 message 对象以后,将 message.actionmessage.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.postMessagebase64 发送给小程序
  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 ,以 checksumkey 缓存下来数据,并将这个 checksum 保存到 canvasData 对象中
  13. 此时 hideWebviewonShow 里面 setData 的回调中的 setData 从新置为 falseweb-view 从新加 attach,H5页面从新加载
  14. webview 从新 attach 以后, this.handleCanvasData 被触发,
  15. handleCanvasData 检测是否有须要保存的 canvas 数据,若是有,保存,修改 canvasData 状态

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

相关文章
相关标签/搜索