对于WebView 中的 H5 向 小程序 的单方向通讯方式,腾讯官方给出了 如下方案: 大家可以点击 这里 查看官方文档 。
看到官方提供了解决办法,焦虑感顿时降低了不少。但仔细一看,不免又发愁起来:
网页向小程序 postMessage 时,会在特定时机(小程序后退、组件销毁、分享)触发并收到消息。e.detail = { data },data是多次 postMessage 的参数组成的数组
这句话限定了小程序只能在应用后退、组件销毁、分享的时候才能捕捉到 H5 的消息,这个时候想要在执行什么操作,只怕用户已经不在之前的页面上了。更不用提如何将处理结果反馈给 H5 了。
对于应用的主体内容,或者全部内容都由H5提供的开发者而言,这样很不友好。因为这个时候,作为 “壳” 的小程序,需要像工具箱一样能够及时为 H5 提供 H5 无法取得,但小程序能够相对容易获取的数据。
那有没有什么办法让小程序能够及时捕捉到 H5 发出的消息,同时向 H5 反馈处理结果呢?简单来说,有。
为了能够及时捕捉H5发出的消息,微信小程序需要能够以某种形态监听到 “消息发出” 这一动作。虽然 H5 可以调用 postMessage
方法,但微信小程序并不能主动感知,因而不符合我们的要求。
通观腾讯的官方文档,发现 H5 中可以直接调用微信小程序的页面切换方法:navigateTo
,而微信小程序的页面能够通过 onLoad 句柄捕获此次跳转。两者之间甚至还可以像地址栏一样发收参数:
H5端调用
wx.miniProgram.navigateTo({
url: "/pages/index/index?a=1&b=2"
});
微信小程序端调用
Page({
onLoad: function (options) {
console.log(options); // -> {a: "1", b: "2"}
}
});
这两者组合起来,恰好就满足了 “H5 向 小程序 发送消息,小程序 能够及时捕获消息” 这一诉求了。更为系统性的示例如下所示:
H5端调用
/**
* 对象序列化
* @param {Object} data 要序列化的对象
* @returns {String}
*/
var serialize = function (data) {
var s = "";
for (var p in data)
s += "&" + p + "=" + encodeURIComponent(data[p]).replace(/\+/gm, "%2B");
s = s.length > 0 ? s.substring(1) : s;
return s;
};
/* 通过跳转页面实现发送消息的目的 */
wx.miniProgram.navigateTo({
url: "/pages/index/index?" + serialize({
cmd: JSON.stringify({
req: "H5Req_" + Date.now(), /* 本次消息的唯一编码,用于微信小程序将处理结果反馈给H5 */
api: "do-sth", /* 业务指令 */
params: params /* 业务指令的执行参数 */
})
})
});
微信小程序端调用
Page({
onLoad: function(options){
cmd = JSON.parse(decodeURIComponent(options.cmd));
var req = cmd.req,
api = cmd.api,
params = cmd.params;
switch(api){
case "do-sth":
/* do something with params */
doSomething(params, function(result){
/* 反馈处理结果 */
//....
/* 返回 webview 所在的页面 */
wx.navigateBack();
});
break;
}
})
});
你可能注意到了,这个方案牺牲了用户体验,借助了页面切换完成的消息传达。 我当然知道这种体验有多糟糕,但毕竟没有其它选择。我企图用样式重载页面的切换效果,希望能够透明实现页面切换,但发现并不可行 - 微信并没有提供技术通道,使得我可以这样做。但愿随着小程序版本的迭代,腾讯能够给我们提供更为正统的解决方案吧。
实现了消息的及时捕获,开发者就可以在微信小程序中根据 H5 发出的业务指令执行动作了。但动作结果如何向H5反馈呢?
看来看去,发现腾讯官方并没有提供从 微信小程序 到 H5 方向的消息反馈。那 webview 有没有其它能够为小程序所触发,同时H5能够感知的渠道呢?
想了想,还真有!
具体来说,就是微信小程序改写 webview 的 url 中的 hash 部分,将 hash 部分的取值替换为业务指令的处理结果。因为 hash 部分的改动既不会让 webview 离开或重新装载页面,也同时会让 webview 自动触发 hashchange
事件,而 H5 只要添加了该事件的监听句柄,就能够得到信息反馈。例如:
微信小程序端调用
/**
* 给指定的url设置hash
* @param {String} url 要设置hash的url
* @param {String} hash 要设置的hash
*/
var setUrlHash = function (url, hash) {
if (null == url || "" === (url = String(url).trim()))
return url;
var hashIndex = url.indexOf("#");
var urlWithoutQueryAndHash;
if (-1 == hashIndex) {
urlWithoutQueryAndHash = url;
} else {
urlWithoutQueryAndHash = url.substring(0, hashIndex);
}
return urlWithoutQueryAndHash + "#" + hash;
};
Page({
onLoad: fu
nction(options){
cmd = JSON.parse(decodeURIComponent(options.cmd));
var req = cmd.req,
api = cmd.api,
params = cmd.params;
switch(api){
case "do-sth":
/* do something with params */
doSomething(params, function(result){
/* 反馈处理结果 */
var pages = getCurrentPages();
var page = pages[pages.length - 1];/* 当前页面是接收 H5 消息的页面,上一个页面是 webview 所在页面 */
/**
* v=Date.now() 是为了加入干扰,保证每次的hash都会发生变化,
* 规避连续请求的 req 和 处理结果都相同导致hashchange 没有被触发的风险
*/
var newWebViewUrl = setUrlHash(
page.data.webviewSrc,
req + "=" + encodeURIComponent(result) + "&v=" + Date.now()
);
page.setData({
webviewSrc: newWebViewUrl
});
/* 返回 webview 所在的页面 */
wx.navigateBack();
});
break;
}
})
});
H5端调用
/**
* H5 当前发出的的业务指令的唯一ID
*/
var currentReq = null;
/**
* H5 发出业务指令时,指定的用于接收反馈消息的回调方法
*/
var currentCallback = null;
/**
* 调用微信小程序
* @param {String} api 业务指令
* @param {Object} params 业务参数
* @param {Function} [callback] 处理回调
*/
var callMiniProgram = function(api, params, callback){
currentReq = randomString("H5Req_");
currentCallback = typeof callback === "function"? callback: null;
wx.miniProgram.navigateTo({
url: "/pages/hybrid-page/hybrid?" + serialize({
cmd: JSON.stringify({
req: currentReq,
api: api,
params: params
})
})
});
};
/* 监听 hashchange 事件,用于接收微信小程序给出的处理反馈 */
window.addEventListener("hashchange", function(e){
/* 如果尚未发出指令,或 hash 不是微信小程序给出的操作反馈,那么 hashchange 可能是网页内部触发的,忽略即可 */
if(null == currentReq || !location.hash.startsWith("#" + currentReq + "="))
return;
/* 解析hash中包含的操作反馈(除操作反馈外,还有随机数等) */
var params = {};
location.hash.substring(1).split("&").forEach(function(pair){
var tmp = pair.split("=");
if(tmp.length < 1)
return;
params[decodeURIComponent(tmp[0].trim())] = decodeURIComponent(tmp[1].trim());
});
/* 取出操作反馈正文,并触发可能存在的回调方法 */
var result = params[currentReq];
if(typeof currentCallback !== "function"){
console.log(currentReq + " -> " + result);
}else
currentCallback(result);
});
/* 向微信小程序发出消息 */
callMiniProgram("do-sth", {
param1: "value1",
param2: true
});
对于上述逻辑,我已经抽取并定义好了方法,托管在 Github ,开发者按照给定操作步骤操作即可:
【H5 - 配置步骤】
- 网页引入 miniProgramHybrid.js
- 在需要通讯的地方调用API:miniProgramHybrid.call("业务指令", {参数集合}, 可选的回调方法)\
【微信小程序 - 配置步骤】
- 将 index,hybrid-lib 与 hybrid-page 并行存放至小程序工程的 pages 目录下
- 在微信小程序的 app.json 文件中添加页面:"pages/hybrid-page/hybrid" 和 "pages/index/index"
- 调整微信小程序 index 页面中 data 的 webviewSrc 字段取值为 webview 要打开的 H5 链接地址
【微信小程序 - 业务指令开发步骤】
- 在 hybrid-lib/handler.js 中根据样例定义业务指令
通讯模型借助了 H5 的 hashchange 事件(hash 将被更改为形如:#H5Req_xxxx&v=1567077985338的形态),如果H5页面的既有逻辑也有使用 hashchange,请注意区分、识别。