webview 是 5+ SDK 的重要组成部分,也是 HBuilder 区别于其它“H5 混合模式”移动端开发方案的一个重要特色和利器。
在实际使用中,webview 也会给程序设计带来一些麻烦。因为一个 webview 实例就相当于桌面浏览器里的一个窗口页签,每个 webview 的 js 上下文都是相互独立的,并不能互相访问。
幸好 HTML5+ 规范中定义了 WebviewObject,evalJS() 这个接口,为两个平行宇宙之间的互相通信提供了可能。MUI 就利用这个接口实现了“自定义事件”功能,可以用来跨 webview 传递消息。
不过,evalJS() 只是底层实现的一个单向通信机制,MUI 的自定义事件也是单向消息,没有返回值。虽然这已经解决了很多问题,但还是有不少应用场景更适合用接近“函数调用”的方式来实现。
有鉴于此,我做了这个 hbuilder-rpc。分享在这里,希望能有用。如果这段代码将来有机会成为 MUI 的一部分,我将尤感欣慰。
演示界面截图:
![](http://img-cdn-tc.dcloud.net.cn/uploads/article/20151128/445581ee5497836b57ab24e33914a5c2.png)
相关程序代码:
/**
* 以当前的 WebView 为媒介,向其它 WebView 中的 js 提供服务接口。
*/
window.RpcServer = {
/**
* 服务提供者调用此函数注册一个服务接口。
* @param {String} service_name 接口名称
* @param {Function} fnService 注册的服务函数,具有如下形式:
* function(params, finish) {
* // params 是调用参数。
* // finish 是回调函数,应该在服务完成后调用,并传入唯一参数表示服务执行结果。
* // 即使服务出错,也要确保回调函数被调用,并用传入参数来表示错误状态。
* }
*/
expose: function(service_name, fnService) {
var me = this;
if (me.exposed[service_name] != undefined) {
throw new Error('RpcServer.expose: service already exists: ' + service_name);
}
me.exposed[service_name] = fnService;
},
exposed: {}, // 注册的服务接口
/**
* RpcClient 通过 evalJS() 调用此函数,访问服务接口。
* @param {String} service_name 服务接口名称
* @param {Mixed} params 入口参数
* @param {String} vw_id 调用源的 webview id
* @param {String} cb_id 调用源的 callback id
*/
invoke: function(service_name, params, vw_id, cb_id) {
var fn = this.exposed[service_name];
if (typeof fn != 'function') {
throw new Error('RpcServer.invoke: service not found: ' + service_name);
}
fn(params, function(ret) {
var vw = plus.webview.getWebviewById(vw_id);
if (!vw) return;
var js = 'RpcClient.callback(' + JSON.stringify(cb_id);
js += ',' + JSON.stringify(ret);
js += ')';
vw.evalJS(js);
});
}
};
/**
* 远程访问 RpcServer 提供的服务接口。
*/
window.RpcClient = {
/**
* 调用一个远程服务接口。
* @param {String} server_id rpc server 的 webview id。
* @param {String} service_name 服务接口名称。
* @param {Mixed} params 服务入口参数。
* @param {Function} callback 回调函数,用于回传服务执行结果。
*/
invoke: function(server_id, service_name, params, callback) {
var me = this;
var cs = plus.webview.getWebviewById(server_id);
if (!cs) throw new Error('RpcServer view not found: ' + server_id);
var js = 'RpcServer.invoke(' + JSON.stringify(service_name);
js += ',' + JSON.stringify(params);
if (typeof callback == 'function') {
js += ',' + JSON.stringify(plus.webview.currentWebview().id);
js += ',' + me.next_callback_id;
me.callbacks[me.next_callback_id] = callback;
me.next_callback_id ++;
}
js += ')';
cs.evalJS(js);
},
next_callback_id: 1,
callbacks: {},
callback: function(cb_id, ret) {
var me = this;
var cb = me.callbacks[cb_id];
if (typeof cb != 'function') return;
cb.call(undefined, ret);
delete me.callbacks[cb_id];
}
};
// 通过 RpcServer.expose() 暴露一个服务函数供其它 WebView 中的 js 调用
RpcServer.expose('demo-rpc-service', function(params, finish) {
// 入口参数
mui('#rpc-call-params')[0].innerText = JSON.stringify(params, undefined, ' ');
// 服务功能完成后,调用 finish() 把结果发回给调用者
finish({
success: true,
result: {
reply: 'hi, ' + params.from + '.',
num: window._call_num = (window._call_num || 0) + 1
}
});
});
// 通过 RpcClient.invoke() 调用另一个 WebView 中的服务函数
RpcClient.invoke('demo-rpc-server', 'demo-rpc-service', {
greeting: 'hi !',
from: plus.webview.currentWebview().id,
num: window._call_num = (window._call_num || 0) + 1
}, function(resp) {
// resp 是服务执行结果
mui('#rpc-call-resp')[0].innerText = JSON.stringify(resp, undefined, ' ');
});
演示项目源代码可通过附件下载。