動態

詳情 返回 返回

微信小程序 webview 與 h5 通過 postMessage 實現實時通訊的實現 - 動態 詳情

原文:https://pantao.parcmg.com/pre...

在做 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 就是被 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 頁面從被打開到關閉的所有數據。

Add a new 評論

Some HTML is okay.