原文:https://pantao.parcmg.com/pre...
在做 React Native 應用時,如果需要在 App 裏面內嵌 H5 頁面,那麼 H5 與 App 之間可以通過 Webview 的 PostMessage 功能實現實時的通訊,但是在小程序裏面,雖然也提供了一個 webview 組件,但是,在進行 postMessage 通訊時,官方文檔裏面給出了一條很變態的説明:
網頁向小程序postMessage時,會在特定時機(小程序後退、組件銷燬、分享)觸發並收到消息。e.detail = { data },data是多次postMessage的參數組成的數組
這裏面已經説的很明白了,不管我們從 H5 頁面裏面 postMessage 多少次,小程序都是收不到的,除非:
- 用户做了回退到上一頁的操作
- 組件銷燬
- 用户點擊了分享
這裏面其實我沒有完全説對,官方其實説的是 小程序後退,並沒有説是用户做回退操作,經過我的實測,確實人家表達得很清楚了,我們通過微信官方的SDK調起的回退也是完全可行的:
wx.miniProgram.navigateBack()
大體思路
從上面的分析和實測中我們可以知道,要實現無需要用户操作即可完成的通訊,第三種情況我們是完全不需要考慮了的,那麼來仔細考慮第 1 和第 2 種場景。
第 1 種方式:回退
當我們想通過網頁向小程序發送數據,同時還可以回退到上一個頁面時,我們可以在 wx.miniProgram.postMessage 之後,立馬調用一次 wx.miniProgram.navigateBack(),此時小程序的操作是:
- 處理
postMessage信息 - 回退到上一頁
我們在處理 postMessage 的時候做一些特殊操作,可以將這些數據保存下來
第 2 種方式:組件銷燬
這是我感覺最合適的一種方式,可以讓小程序拿到數據,同時還保留在當前頁面,只需要銷燬一次 webview 即可,大概的流程就是:
- 小程序
postMessage - 小程序
navigateTo將小程序頁面導向一個特殊的頁面 - 小程序的那個特殊頁面立馬回退到
webview所在的頁面 webview所在的頁面的onShow裏面,做一次處理,將webview銷燬,然後再次打開- 觸發
onMessage拿到數據 - 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}}" />
流程回顧與總結
- 打開
webview頁面,打開h5 - 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 頁面從被打開到關閉的所有數據。