微信小程序 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 頁面從被打開到關閉的全部數據。

相關文章
相關標籤/搜索