1. 前言
在《用PhoneGap+jQueryMobile开发Android应用实例》中,我们讲到PhoneGap(以下称Cordova)开发环境的搭建,以及如何整合出一个基本的Android应用框架(并给出了范例代码)。于是乎,我们便开始日夜兼程,披星戴月的炮制我们的第一个手机应用了。
但实际上,除了常见的API调用规范(有且仅有自查手册一途)引起的问题之外,我们仍然会遇到其他形形色色的各种问题。那么在这篇文章中,我们谈谈java与js之间的交互问题(哦,目前仅关注Android,所以只能谈java了)。当然,二者之间的交互目的,原因会有种种不同,但应该还是以发挥语言各自的优势,提供接口给对方调用的意图居多。
我们知道,在Android平台下,Cordova是通过内置WebKit内核的方式来实现界面容器的(事实上,在其他平台也是如此)。我们也同样知道,Cordova是一个桥接框架,其目的就是为原生API和js建立桥接,互通有无的。为了便于我们扩展自己的应用,Cordova还提供了插件(PlugIn)机制(当然,我们还可以直接修改Cordova开源代码)。只要遵循一定的规则(恩恩,事实上这个规则很简单),就可以扩展出丰富的功能特效来。
2. Cordova插件与 WebView.addJavascriptInterface
恩恩,本文的主题是java与js的交互(差点跑了)。刚提到的,Cordova有插件机制,可以通过插件的形式,实现java与js交互,为什么还要提到addJavascriptInterface?
Cordova插件确实可以实现二者的交互,而且是异步的,非常方便。但基于一些特殊的原因,例如:一个回调需要被多次调用(啊哈,或许是我太菜?使用PlugIn注册的回调都只能被调用一次)。又或者不想写插件,想直接点。
总之,插件也并不是时时处处都符合我们的需求(我们的欲望无穷大啊),总是要找办法解决,寻点不同的路出来(个人不是特别认同Cordova的Hack方式,遑论其插件;而且Cordova目前的状态有点怪异,版本更新是很快,但文档更新不同步)。要真正成熟,还是有一段路要走的。
addJavascriptInterface则是WebKit的原生API,属于WebView对象的公共方法,用于暴露一个java对象给js,使得js可以直接调用java方法。当然,我们要实现java与js的双向交互,还需要另一个方法loadUrl(同属于WebView对象,Cordova也是采用的这个方法调用js的)的配合。
当然,这两种方式互有优劣(只有实践时,才会明白啊)。Cordova插件的不足刚才已经提过;而addJavascriptInterface也有些问题,一是Android平台封装WebKit内核时,不同的版本中有些许不一致;其次,直接使用loadUrl加载js实在是让人头疼。
其实应该有更好的方法,比如扩展js引擎(我更喜欢这种方式),但这种方式相对而言,涉及的内容繁杂,暂时不纳入这次的话题。
3. Cordova插件的实现
Cordova插件分为两个部分(额,Cordova本身也是分为两个部分的,别扭不?),一部分由java实现,另一部分由js实现。
1) java部分
Cordova插件的java部分很简单,继承Cordova.Plugin,实现execute方法就可以了:
public classNotificationClient extends Plugin {
private static final String TAG ="NotificationClient";
private String callbackId ="";
public PluginResult execute(Stringaction, JSONArray args, String callbackId) {
PluginResult.Status status =PluginResult.Status.OK;
if(action.equals("register")){
try {
register(args.getString(0),args.getString(1));
} catch(JSONException e) {
status =PluginResult.Status.JSON_EXCEPTION;
}
} elseif(action.equals("watch")) {
this.callbackId =callbackId;
PluginResult r = newPluginResult(PluginResult.Status.NO_RESULT);
r.setKeepCallback(true);
return r;
} else {
status =PluginResult.Status.INVALID_ACTION;
}
return newPluginResult(status);
}
public Object onMessage(String id,Object data) {
Log.d(TAG,"onMessage(" + id + ").");
if(id.equals("onClientNotification")){
if(!callbackId.equals("")){
this.success("true",callbackId);
}
}
return data;
}
private void register(String username,String phone) {
Log.d(TAG,"register(" + username + ", " + phone + ").");
}
}
嗯,就这样,作为一个Cordova插件java部分的范例,他已经完成了使命(原谅我为了节省篇幅,删掉了注释和空行;不必太多介怀,参考资源里有很多范例工程可以学习)。
不得不说,Cordova还是做了很多工作的,为了减轻插件开发的工作量,对js的调用进行了很多的包装(回头看看loadUrl是多么的贫瘠的时候,才会有如此感慨吧)。
2) js部分
唉,让我浑身别扭的部分来了。说到js部分,我接触过的版本里(当然,我也仅仅接触过3个版本而已:1.0、2.0、2.1)已经有两种写法。嗯,从执行效果上来说,2.0是兼容1.0的写法的(哦哦,前提是我做了一些改动,虽然改动很小);美中不足的是,跟踪脚本时还是会报错,虽然不影响脚本的继续加载。
先来看看第一种写法吧(1.0的写法):
functionNotificationClient() { }
NotificationClient.prototype.register= function(userName, phone) {
PhoneGap.exec(null, null,"NotificationClient", "register", [ userName, phone ]);
};
NotificationClient.prototype.watch= function(fn) {
PhoneGap.exec(fn, null,"NotificationClient", "watch", []);
};
PhoneGap.addConstructor(function(){
if(typeof navigator.notificationClient== "undefined")
navigator.notificationClient= new NotificationClient();
});
网上的教程都是这么弄的,事实上运行时会报错:找不到PhoneGap对象;更严重的是navigator.notificationClient在运行时根本无法访问。
当然,如果你改成这样:
// PhoneGap.addConstructor(function(){
if(typeof navigator.notificationClient== "undefined")
navigator.notificationClient= new NotificationClient();
// });
程序是可以正常运行的,虽然仍然会报错。
OK,再来看看第二种(2.0的写法):
cordova.define("cordova/plugin/notificationClient",function(require, exports, module){
var exec = require('cordova/exec');
var NotificationClient = function() {};
NotificationClient.prototype.register =function(userName, phone) {
exec(null, null,"NotificationClient", "register", [ userName, phone ]);
};
NotificationClient.prototype.watch =function(fn) {
exec(fn, null,"NotificationClient", "watch", []);
};
var notificationClient = newNotificationClient();
module.exports = notificationClient;
});
if(!window.plugins) {
window.plugins = { };
}
if(!window.plugins.notificationClient) {
window.plugins.notificationClient =cordova.require("cordova/plugin/notificationClient");
}
恩,这种写法没有任何错误了,而且能正常运行,开心。
3) 注册插件
把插件写完之后,还需要注册,插件才能在Cordova下使用。找到工程目录下的res\xml目录,1.0打开plugins.xml文件,2.0打开config.xml文件,在plugins节点下加入:
至此,NotificationClient插件就可以在js中调用了。
1.0的用法:
navigator.notificationClient.register("azhi","15810108888");
2.0的用法:
window.plugins.notificationClient.register("azhi","15810108888");
4. WebView.addJavascriptInterface实现
啊,终于到addJavascriptInterface了,每次文档写到一半左右都手酸呐(看文档的人是不是也暗叹了一声:终于来了)。
addJavascriptInterface比起Cordova插件来更加的简单,首先我们来定义一个类:
public classNotificationClient {
private static final String TAG ="NotificationClient";
private Context context = null;
private CordovaWebView view = null;
private String callback = "";
public NotificationClient(Contextcontext, CordovaWebView view) {
this.context = context;
this.view = view;
}
public void register(String user,String mobile, String callback) {
Log.d(TAG, "register(user: " + user + ", mobile: " + mobile + ", callback:" + callback + " )");
this.callback = callback;
checkMessage();
}
public void checkMessage() {
SharedPreferences sp =context.getSharedPreferences("NotificationClient", 0);
int message =Integer.valueOf(sp.getInt("Message", 0));
Log.d(TAG,"checkMessage(): " + message);
if(message > 0) {
Editor editor =sp.edit();
editor.putInt("Message",0);
editor.commit();
newHandler().post(new Runnable() {
public voidrun() {
view.sendJavascript(callback);
}
});
}
}
};
这个类的意图很简单(嗯,跟上面Cordova插件的NotificationClient插件很相似对不对?):提供一个register方法供js调用,传入相应的参数增加了一个callback,在合适的时机(通过SharedPreferences检查Message标志,大于0则认为是合适的时机了),从java端调用这个callback(当然,代码里使用了SharedPreferences、Handler等其他的Android原生对象,大家暂时忽略就是)。
嚯,我是不是没有用loadUrl,而是用的sendJavascript?sendJavascript是Cordova对WebView封装后提供的方法,其实把那一句改成:
view.loadUrl("javascript:"+ this.callback);
效果是一样的(当然,如果你不是用Cordova,而是自己写的Activity,那么你就必须得这么写了)。
好了,类写完了,下面就应该把这个类暴露给js了:
appView.getSettings().setJavaScriptEnabled(true); // 暴露之前,先开启javascript
appView.addJavascriptInterface(newNotificationClient(this, appView), "notificationClient");
嗯,这里用到了appView(DroidGap的成员变量),我们使用的Cordova嘛,所以用这个没有罪过的。如果是直接实现的Activity,就要自己内嵌WebView了,把appView改成自己的WebView对象即可。
再就是js里的用法了:
window.notificationClient.register("azhi","15810108888","OnMessage();");
大家看到了,在js中调用时,还是比较方便的,不需要预先建立js类对象,通过addJavascriptInterface添加的对象直接就附加在window对象上了。但弊端也是很明显,看看我们的callback,是以代码形式传入的(当然了,其实是可以改良的,但今天就不聊这个了)。
呵呵,稍微来点结束语:就这样吧,希望大家都有所收获。
5. 参考资源
PhoneGap插件范例:
addJavaScriptInterface应用范例:
WebKit API: