1、背景
公司项目是通过webView.addJavascriptInterface(Object obj, String interfaceName)进行web和原生的交互的,android端通常会定义类似以下的多个方法,这样做的好处是便于阅读,一看便知方法是干什么的,需要什么参数;坏处是不便于扩展,一旦web端传错参数,或者调了一个android端没有的方法时,会导致各种问题,每当需要扩展时,web端总是需要考虑android端的版本兼容问题,因此考虑一个通用的方法来处理双方的交互(正好前些日子学了Javapoet和apt没东西练手hhhhh)。
@JavascriptInterface
public void func(){ }
@JavascriptInterface
public void func(int a, int b){}
@JavascriptInterface
public void func1(int a, String b){ }
2、交互
Android端只暴露一个方法给web端调用,双方约定好数据类型,比如一个json字符串,action代替原来的方法用作请求标志,data作为扩展参数,如
{
"action" : “REQUEST_PLAY_VIDEO”,
"data" : {
"url" : "http://xxxx"
}
}
同时暴露一个接口出来用作交互,如
@JavascriptInterface
public void request(String request) {}
这样,交互问题就解决了,当web需要扩展时,只需往json字符串里添加额外的参数即可,android端这边解析json,由于低版本没有解析额外的参数因此会忽略掉,不会导致网页报错等问题,高版本或者补丁包添加参数处理即可。到这里基本可以了,但,还是会存在一些问题:
- 参数是json字符串,要解析
- 解析后要判断action,做相应的处理,如果带数据data,还要解析转成Java对象
- 简单的判断action免不了switch,每添加一个方法就要写一个case过于麻烦
基于以上问题,把交互方式封装一下。
3、实现
封装一个简单的JsBridge,要实现的功能如下:
- 自动解析json,android端拿到的是对应的Java对象
- 简单的扩展action的方式
- 简单的绑定方式,如JsBridge.bind(webview, object)
- 其他,如错误处理等
思路很简单:通过注解标记与web交互的类和action对应的实现方法,通过Javapoet和apt生成相应的类,该类包含了真正的交互方法(@JavascriptInterface标记的),在该方法里解析json参数得到action,switch判断action对应的实现方法,再调用即可。
3.1、定义一些约束
3.1.1、注解
/**
* js和android互相交互的js名
* author : pxq
* date : 19-10-24 下午9:39
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Bridge {
//js交互名
String name();
//交互方法
String jsMethod();
}
/**
* js请求android端的action
* author : pxq
* date : 19-10-24 下午9:38
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface JsAction {
//action名
String value();
}
/**
* Js Request异常,如解析失败等
* author : pxq
* date : 19-10-26 下午2:27
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface JsError {
}
/**
* 未处理的Js Request
* author : pxq
* date : 19-10-26 下午2:25
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface UnHandle {
}
3.1.2、获取interfaceName的接口
注意到webView.addJavascriptInterface(Object obj, String interfaceName)需要一个interfaceName,为此我们还需要定义一个接口,让生成的类去实现它,以便通过强转的方式获取该值。
/**
* 约束类,用来获取webview.addJavascriptInterface(obj, name)的name
* author : pxq
* date : 19-10-26 上午2:20
* @see android.webkit.WebView#addJavascriptInterface(Object, String)
*/
public interface IJsBridge {
//获取JavascriptInterface名
String getName();
}
这里生成的类有点讲究,务必生成在同一个包下,同时加上后缀加以区分,接下来就是javapoet的使用了。类生成之后,最后是绑定到webview上,有了前面的基础,这个就简单了,直接看代码:
/**
* 向webview添加处理器,允许多个
* @param webView
* @param handlers
*/
@SuppressLint({"SetJavaScriptEnabled"})
public static void bind(WebView webView, Object... handlers){
try {
mWebViewRef = new WeakReference<>(webView);
webView.getSettings().setJavaScriptEnabled(true);
for (Object handler : handlers) {
addJavascriptInterface(webView, handler);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 向webview添加处理器
* @param webView
* @param handler
* @throws Exception
*/
@SuppressLint("JavascriptInterface")
private static void addJavascriptInterface(WebView webView, Object handler) throws Exception{
String className = handler.getClass().getCanonicalName() + JS_BRIDGE_SUFFIX;
Class<?> jsBridgeClazz = Class.forName(className);
IJsBridge jsBridge = (IJsBridge) jsBridgeClazz.getConstructor(handler.getClass()).newInstance(handler);
webView.addJavascriptInterface(jsBridge, jsBridge.getName());
Log.d(TAG, "addJavascriptInterface: " + jsBridge.getName());
}
4、添加Java调用JS方法
没想到什么好的封装方式,这里简单的提供一个接口给外部调用。
/**
* java调用js方法
* @param function
* @param params
*/
public static void callJS(final String function, final String params){
if (mWebViewRef != null && mWebViewRef.get() != null){
sHandler.post(new Runnable() {
@Override
public void run() {
mWebViewRef.get().loadUrl(String.format("javascript:%s('%s')", function, params));
}
});
}
}
5、测试
5.1、测试JS调用Java方法
定义个简单的类测试一下:
//action对应的实现方法
@Bridge(name = "android", jsMethod = "request")
public class JsCallJavaBridge {
private static final String TAG = "JsCallJavaBridge";
@JsAction("test")
public void test(){
Log.i(TAG, "test: ");
}
@JsAction("testData")
public void testData(TestBean test){
Log.i(TAG, "testData: " + test.name +" " + test.data);
}
@UnHandle
public void UnHandle(String request){
Log.w(TAG, "UnHandle: " + request);
}
@JsError
public void error(String request, Exception e){
Log.e(TAG, "error: " +request, e);
}
}
生成的交互类:
public class JsCallJavaBridge$$Bridge implements IJsBridge {
public JsCallJavaBridge mHandler;
private IJsonParser mActionParser;
public JsCallJavaBridge$$Bridge(JsCallJavaBridge mHandler) {
this.mHandler = mHandler;
this.mActionParser = new ObjParser();
}
@JavascriptInterface
public void request(String request) {
try {
handleRequest(request);
} catch (Exception e) {
mHandler.error(request, e);
}
}
public String getName() {
return "android";
}
void handleRequest(String request) throws Exception {
String action = mActionParser.getAction(request);
switch(action) {
case "test":
mHandler.test();
break;
case "testData":
mHandler.testData(mActionParser.getData(request, TestBean.class));
break;
default : mHandler.UnHandle(request);
}
}
}
写一个简单的html
test.html
<html>
<body>
<input type="button" value="无数据" onclick="call()" style="height:40px;" >
<p>
<input type="button" value="带数据" onclick="callWithData()" style="height:40px;">
</p>
<p>
<input type="button" value="默认处理(UnHandle)" onclick="callUnHandle()" style="height:40px;">
</p>
<p>
<input type="button" value="错误处理(Error)" onclick="callError()" style="height:40px;">
</p>
<script>
function call(){
var x = "{\"action\":\"test\"}"
callAndroid(x);
}
function callWithData() {
var x = "{\"action\":\"testData\", \"data\": {\"name\" : \"pxq\", \"data\" : \"testdata\"}}"
callAndroid(x);
}
function callUnHandle() {
var x = "{\"action\":\"UnHandleTest\", \"data\": {\"name\" : \"pxq\"}}"
callAndroid(x);
}
function callError() {
var x = "{\"actions\":\"ErrorTest\", \"data\": {\"name\" : \"pxq\"}}" //这里把action 改成了 actions会导致解析失败
callAndroid(x);
}
function callAndroid(x) {
window.android.request(x);
}
</script>
</body>
</html>
绑定一下
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebView webView = new WebView(this);
JsBridge.bind(webView, new BridgeTest());
setContentView(webView);
webView.loadUrl("file:///android_asset/test.html");
}
}
5.2、测试Java调用JS方法
测试类:
/**
* Description: 测试类:测试Java调用JS方法
* Author : pxq
* Date : 20-1-18 下午1:17
*/
@Bridge(name = "callback", jsMethod = "request")
public class JavaCallJsBridge {
private static final String TAG = "JavaCallJsBridge";
@JsAction("testData")
public void testData(TestBean test){
Log.i(TAG, "testData: " + test.name +" " + test.data);
JsBridge.callJS("callBack", test.data);
}
}
html添加一下js方法,打印Java端传过来的参数
function callBack(params){
console.log("web got it : " + params);
}
绑定处理类:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebView webView = new WebView(this);
//绑定两个处理器
JsBridge.bind(webView, new JsCallJavaBridge(), new JavaCallJsBridge());
setContentView(webView);
webView.loadUrl("file:///android_asset/test.html");
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
Log.e(TAG, "onConsoleMessage: " + consoleMessage.message());
return super.onConsoleMessage(consoleMessage);
}
});
}
效果:
最后带上github链接:JsBridge