原文: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
這裏面其實我沒有徹底說對,官方其實說的是 小程序後退,並無說是用戶作回退操做,通過個人實測,確實人家表達得很清楚了,咱們經過微信官方的SDK調起的回退也是徹底可行的:小程序
wx.miniProgram.navigateBack()
從上面的分析和實測中咱們能夠知道,要實現無須要用戶操做便可完成的通信,第三種狀況咱們是徹底不須要考慮了的,那麼來仔細考慮第 1 和第 2 種場景。數組
當咱們想經過網頁向小程序發送數據,同時還能夠回退到上一個頁面時,咱們能夠在 wx.miniProgram.postMessage
以後,立馬調用一次 wx.miniProgram.navigateBack()
,此時小程序的操做是:緩存
postMessage
信息咱們在處理 postMessage
的時候作一些特殊操做,能夠將這些數據保存下來微信
這是我感受最合適的一種方式,可讓小程序拿到數據,同時還保留在當前頁面,只須要銷燬一次 webview
便可,大概的流程就是:數據結構
postMessage
navigateTo
將小程序頁面導向一個特殊的頁面webview
所在的頁面webview
所在的頁面的 onShow
裏面,作一次處理,將 webview
銷燬,而後再次打開onMessage
拿到數據這種方式雖然變態,可是至少能夠作到實時拿到數據,同時還保留在當前 H5 頁面,惟一須要解決的是,在作這整套操做前,H5 頁面須要作好狀態的緩存,要否則,再次打開以後,H5 的數據就清空了。app
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
就是被 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 主要就是使用 html2canvas
庫生成 Canvas 圖(沒辦法,本身在小程序裏面畫太麻煩了),可是這個不在本文討論過程當中,咱們就當是已經生成了 canvas
圖片了,將其轉爲 base64
文本了,而後像下面這樣作:
wx.miniProgram.postMessage({ data: JSON.stringify({ action: 'postData', data: 'BASE 64 IMAGE STRING' }) }); wx.miniProgram.navigateBack()
將數據 postMessage
以後,當即 navigateBack()
,來觸發一次回退,也就觸發了 bindmessage
事件。
webview
實現實時通信接下來,咱就開始本文的重點了,比較變態的方式,可是也沒想到更好的辦法,因此,你們將就着交流吧。
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}}" />
webview
頁面,打開 h5
canvas
圖,並轉爲 base64
字符wx.miniProgram.postMessage
將 base64
發送給小程序wx.miniProgram.navigateTo
將頁面導向一個特殊頁面webview
所在頁面的 shouldReattachWebview
設置爲 true
webview
所在頁面webview
所在頁面的 onShow
事件被觸發onShow
事件檢測 shouldReattachWebview
是否爲 true
,若爲 true
hideWebview
設置爲 true
,引發 web-view
組件的銷燬handlePostMessage
被觸發,解析全部的 message
以後交給 handleMessage
逐條處理handleMessage
發現 action === 'saveCanvas'
的事件,拿到 data
data
計算 checksum
,以 checksum
爲 key
緩存下來數據,並將這個 checksum
保存到 canvasData
對象中hideWebview
被 onShow
裏面 setData
的回調中的 setData
從新置爲 false
,web-view
從新加 attach
,H5頁面從新加載webview
從新 attach
以後, this.handleCanvasData
被觸發,handleCanvasData
檢測是否有須要保存的 canvas
數據,若是有,保存,修改 canvasData
狀態整個流程看舊去很繁瑣,可是寫起來其實還好,這裏面最主要的是須要注意,數據去重,微信的 postMessage
裏面拿到的永遠 都是 H5 頁面從被打開到關閉的全部數據。