先看下接下来将要实现的效果图:
一、JsBridge的基本概念
Android4.4以前,谷歌的webview存在安全漏洞,网站可以通过js注入就可以随便拿到客户端的重要信息,甚至轻而易举的调用本地代码进行流氓行为,谷歌在4.4以后增加了防御措施,如果用js调用本地代码,开发者必须在代码声明JavascriptInterface, 4.4之前我们要使得webView加载js只需如下代码:
mWebView.addJavascriptInterface(new JsToJava(), “myfunction”);
4.4之后使用时, 需要在调用Java方法加入@JavascriptInterface注解,如果代码无此声明,那么js就不生效,这样就可以避免恶意网页利用js对客户端的进行窃取和攻击。 但使用比较繁琐,要做一些判断和限制,在比较复杂的Hybrid模式下,需要js和native之间进行交互通讯,原生的JavascriptInterface 难以维护,基于JavascriptInterface 封装的WebViewJavascriptBridge框架,很好的解决了这一问题。
WebViewJavascriptBridge是移动UIView和Html交互通信的桥梁,用于替代WebView自带的JavascriptInterface接口,使开发者可以简单安全的实现js和native交互。
二、以上图为案例介绍JSBridge的使用方法
首先在Module的build.gradle里引入JSBridge所需要的包,也可以下载源码引入项目自定义库文件
repositories {
maven { url "https://jitpack.io" }
}
dependencies {
...
compile 'com.github.lzyzsd:jsbridge:1.0.4'
}
在xml文件中使用控件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="20dp"
android:background="#9f9f9f"
tools:context="com.liuw.jsbridge.MainActivity">
<EditText
android:id="@+id/edit_view"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#ffffff"
android:gravity="top"
android:textSize="14sp"
android:textColor="@color/colorAccent"
android:text="来自于js将要展示的数据"/>
<Button
android:id="@+id/btn_send"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_marginTop="10dp"
android:text="发送数据到webView"/>
<Button
android:id="@+id/btn_reset"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_marginTop="10dp"
android:text="重置浏览器"/>
<com.github.lzyzsd.jsbridge.BridgeWebView
android:id="@+id/bridge_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp">
</com.github.lzyzsd.jsbridge.BridgeWebView>
</LinearLayout>
MainActivity主界面中使用
下面先简单介绍下重点几步操作:
1、加载网页地址
mBridgeWebview.loadUrl("file:///android_asset/web.html");
2、注册handler,接收来自js的数据data,并可通过onCallBack回传数据
mBridgeWebview.registerHandler("submitFromWeb", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
if (!TextUtils.isEmpty(data)) {
mEditText.setText("通过调用Native方法接收数据:\n" + data);
}
function.onCallBack("Native已经接收到数据:" + data + ",请确认!");
}
});
对应的js代码如下
function useClick() {
var name = document.getElementById("uname").value;
var pwd = document.getElementById("psw").value;
var data = "name = " + name + ", password = " + pwd;
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb', {'info':data},//data发送给native
function(responseData) {//接收onCallBack回传回来的数据
document.getElementById("show").innerHTML = responseData;
}
);
}
上面这种方法是通过native和js定义相同的标识进行通信,还有种方法可直接在js中通过send方法发送,并在native中创建DefaultHandler接收
mBridgeWebview.setDefaultHandler(new DefaultHandler(){
@Override
public void handler(String data, CallBackFunction function) {
function.onCallBack("Native已收到消息!");
}
});
对应js代码如下:
//h5直接通过send向Native发送消息,在DefaultHandler的handler方法里接收,并可通过onCallBack方法回传
function sendClick() {
var name = document.getElementById("uname").value;
var pwd = document.getElementById("psw").value;
var data = "name = " + name + ", password = " + pwd;
window.WebViewJavascriptBridge.send(
data,
function(responseData) {
document.getElementById("show").innerHTML = responseData
}
);
}
3、定义callHandler,多用于在初始化界面时native向js发送数据渲染界面,同时也可获取来自js的数据
mBridgeWebview.callHandler("functionInJs", new Gson().toJson(new UserInfo("liuw", "123456")), new CallBackFunction() {
@Override
public void onCallBack(String data) {
mEditText.setText("向h5发送初始化数据成功,接收h5返回值为:\n" + data);
}
});
对应的js代码如下
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge)
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
callback(WebViewJavascriptBridge)
},
false
);
}
}
// 第一连接时初始化bridage
connectWebViewJavascriptBridge(function(bridge) {
//注册handler等待java代码调用
//初始化时获取数据是调用此处代码
//参数:标识,要传递到JAVA的数据,回调方法。
//JAVA代码响应的方法:mBridgeWebview.callHandler("functionInJs", new Gson().toJson(实体类对象), new CallBackFunction(){onCallBack(String data)}
bridge.registerHandler("functionInJs", function(data, responseCallback) {
document.getElementById("show").innerHTML = ("data from Java: = " + data);
var responseData = "I am javascript, Data reception success!";
responseCallback(responseData);
});
})
4、Native中send方法的使用
这里需要注意下,该方法对应js中的bridge.init处理,此处需加CallBackFunction方法,如果只使用mBridgeWebview.send("");如:mBridgeWebview.send("hello");,会导致js中只收到通知,接收不到值
mBridgeWebview.send("来自java的发送消息!!!", new CallBackFunction() {
@Override
public void onCallBack(String data) {
Toast.makeText(MainActivity.this, "bridge.init初始化数据成功" + data, Toast.LENGTH_SHORT).show();
}
});
对应的js代码如下
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge)
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
callback(WebViewJavascriptBridge)
},
false
);
}
}
// 第一连接时初始化bridage
connectWebViewJavascriptBridge(function(bridge) {
//也注册默认的Handler,用来接收java调用的send(string,CallBackFunction)方法
bridge.init(function(message, responseCallback) {
console.log('JS got a message', message);
var data = {
'Javascript Responds': '测试中文!'
};
console.log('JS responding with', data);
responseCallback(data);
});
})
5、再介绍一种常规的Native直接向js传递数据的方法
//直接调用nativeFunction方法向H5发送数据
mBridgeWebview.loadUrl("javascript:nativeFunction('" + data + "')");
对应的js代码如下
function nativeFunction(data) {
document.getElementById("show").innerHTML = data;
}
只需通过方法名直接调用即可,是不是很简单啊~
三、下面上完整代码
package com.liuw.jsbridge;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.github.lzyzsd.jsbridge.BridgeHandler;
import com.github.lzyzsd.jsbridge.BridgeWebView;
import com.github.lzyzsd.jsbridge.CallBackFunction;
import com.github.lzyzsd.jsbridge.DefaultHandler;
import com.google.gson.Gson;
import com.liuw.jsbridge.bean.UserInfo;
import butterknife.ButterKnife;
import butterknife.InjectView;
public class MainActivity extends AppCompatActivity {
@InjectView(R.id.edit_view)
EditText mEditText;
@InjectView(R.id.btn_send)
Button btnSend;
@InjectView(R.id.bridge_webview)
BridgeWebView mBridgeWebview;
@InjectView(R.id.btn_reset)
Button btnReset;
private String TAG = "MainActivity";
private MyHandlerCallBack.OnSendDataListener mOnSendDataListener;
private ValueCallback<Uri> mUploadMessage;
;
private ValueCallback<Uri[]> mUploadCallbackAboveL;
private final static int FILECHOOSER_RESULTCODE = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.inject(this);
initListener();
initWebView();
}
private void initWebView() {
//辅助WebView设置处理关于页面跳转,页面请求等操作
mBridgeWebview.setWebViewClient(new MyWebViewClient(mBridgeWebview, MainActivity.this));
//Handler做为通信桥梁的作用,接收处理来自H5数据及回传Native数据的处理,当h5调用send()发送消息的时候,调用MyHandlerCallBack
mBridgeWebview.setDefaultHandler(new MyHandlerCallBack(mOnSendDataListener));
//WebChromeClient主要辅助WebView处理Javascript的对话框、网站图标、网站title、加载进度等比等,不过它还能处理文件上传操作
mBridgeWebview.setWebChromeClient(new MyChromeWebClient());
// 如果不加这一行,当点击界面链接,跳转到外部时,会出现net::ERR_CACHE_MISS错误
// 需要在androidManifest.xml文件中声明联网权限
// <uses-permission android:name="android.permission.INTERNET"/>
if (Build.VERSION.SDK_INT >= 19) {
mBridgeWebview.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}
//加载网页地址
mBridgeWebview.loadUrl("file:///android_asset/web.html");
//有方法名的都需要注册Handler后使用
mBridgeWebview.registerHandler("submitFromWeb", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
Log.i("liuw", "html返回数据为:" + data);
if (!TextUtils.isEmpty(data)) {
mEditText.setText("通过调用Native方法接收数据:\n" + data);
}
function.onCallBack("Native已经接收到数据:" + data + ",请确认!");
}
});
mBridgeWebview.registerHandler("functionOpen", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
Toast.makeText(MainActivity.this, "网页在打开你的文件预览", Toast.LENGTH_SHORT).show();
}
});
//应用启动后初始化数据调用,js处理方法connectWebViewJavascriptBridge(function(bridge)
mBridgeWebview.callHandler("functionInJs", new Gson().toJson(new UserInfo("liuw", "123456")), new CallBackFunction() {
@Override
public void onCallBack(String data) {
mEditText.setText("向h5发送初始化数据成功,接收h5返回值为:\n" + data);
}
});
//对应js中的bridge.init处理,此处需加CallBackFunction,如果只使用mBridgeWebview.send("");会导致js中只收到通知,接收不到值
mBridgeWebview.send("来自java的发送消息!!!", new CallBackFunction() {
@Override
public void onCallBack(String data) {
Toast.makeText(MainActivity.this, "bridge.init初始化数据成功" + data, Toast.LENGTH_SHORT).show();
}
});
}
private void initListener() {
btnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String data = mEditText.getText().toString();
//直接调用nativeFunction方法向H5发送数据
mBridgeWebview.loadUrl("javascript:nativeFunction('" + data + "')");
}
});
btnReset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mBridgeWebview.loadUrl("file:///android_asset/web.html");
}
});
mOnSendDataListener = new MyHandlerCallBack.OnSendDataListener() {
@Override
public void sendData(String data) {
mEditText.setText("通过webview发消息接收到数据:\n" + data);
}
};
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == FILECHOOSER_RESULTCODE) {
if (null == mUploadMessage && null == mUploadCallbackAboveL) {
return;
}
Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
if (mUploadCallbackAboveL != null) {
onActivityResultAboveL(requestCode, resultCode, data);
} else if (mUploadMessage != null) {
mUploadMessage.onReceiveValue(result);
mUploadMessage = null;
}
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void onActivityResultAboveL(int requestCode, int resultCode, Intent data) {
if (requestCode != FILECHOOSER_RESULTCODE
|| mUploadCallbackAboveL == null) {
return;
}
Uri[] results = null;
if (resultCode == Activity.RESULT_OK) {
if (data == null) {
} else {
String dataString = data.getDataString();
ClipData clipData = data.getClipData();
if (clipData != null) {
results = new Uri[clipData.getItemCount()];
for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
results[i] = item.getUri();
}
}
if (dataString != null) {
results = new Uri[]{Uri.parse(dataString)};
}
}
}
mUploadCallbackAboveL.onReceiveValue(results);
mUploadCallbackAboveL = null;
}
//自定义 WebChromeClient 辅助WebView处理图片上传操作【<input type=file> 文件上传标签,点击会自动调用】
public class MyChromeWebClient extends WebChromeClient {
// For Android 3.0-
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg)");
mUploadMessage = uploadMsg;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
MainActivity.this.startActivityForResult(Intent.createChooser(i, "File Chooser"), FILECHOOSER_RESULTCODE);
}
// For Android 3.0+
public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
Log.d(TAG, "openFileChoose( ValueCallback uploadMsg, String acceptType )");
mUploadMessage = uploadMsg;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
MainActivity.this.startActivityForResult(
Intent.createChooser(i, "File Browser"),
FILECHOOSER_RESULTCODE);
}
//For Android 4.1
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg, String acceptType, String capture)");
mUploadMessage = uploadMsg;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
MainActivity.this.startActivityForResult(Intent.createChooser(i, "File Browser"), FILECHOOSER_RESULTCODE);
}
// For Android 5.0+会调用此方法
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
Log.d(TAG, "onShowFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture)");
mUploadCallbackAboveL = filePathCallback;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
MainActivity.this.startActivityForResult(
Intent.createChooser(i, "File Browser"),
FILECHOOSER_RESULTCODE);
return true;
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// 处理返回键,在webview界面,按下返回键,不退出程序
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (mBridgeWebview != null && mBridgeWebview.canGoBack()) {
mBridgeWebview.goBack();
return true;
}else {
System.exit(0);
}
}
return super.onKeyDown(keyCode, event);
}
}
自定义MyHandlerCallBack类
package com.liuw.jsbridge;
import android.text.TextUtils;
import android.util.Log;
import com.github.lzyzsd.jsbridge.BridgeHandler;
import com.github.lzyzsd.jsbridge.CallBackFunction;
/**
* Created by liuw on 2018/3/6.
* 自定义Handler回调
*/
class MyHandlerCallBack implements BridgeHandler {
private OnSendDataListener mSendDataListener;
public MyHandlerCallBack(OnSendDataListener mSendDataListener){
this.mSendDataListener = mSendDataListener;
}
@Override
public void handler(String data, CallBackFunction function) {
Log.e("liuw","接收数据为:" + data);
if (!TextUtils.isEmpty(data) && mSendDataListener != null) {
mSendDataListener.sendData(data);
}
function.onCallBack("Native已收到消息!");
}
public interface OnSendDataListener {
void sendData(String data);
}
}
自定义MyWebViewClient类
package com.liuw.jsbridge;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.webkit.WebView;
import com.github.lzyzsd.jsbridge.BridgeWebView;
import com.github.lzyzsd.jsbridge.BridgeWebViewClient;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
/**
* Created by liuw on 2018/3/6.
* 自定义WebViewClient
*/
class MyWebViewClient extends BridgeWebViewClient {
private Context mContext;
public MyWebViewClient(BridgeWebView mBridgeWebview, Context context) {
super(mBridgeWebview);
mContext = context;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.i("liuw", "url地址为:" + url);
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//默认操作url地址yy://__QUEUE_MESSAGE__/
if (url.trim().startsWith("yy:")) {
return super.shouldOverrideUrlLoading(view, url);
}
//特殊情况tel,调用系统的拨号软件拨号【<a href="tel:110">拨打电话110</a>】
if(url.trim().startsWith("tel")){
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
mContext.startActivity(i);
}else {
//特殊情况【调用系统浏览器打开】<a href="https://www.csdn.net">调用系统浏览器</a>
if(url.contains("csdn")){
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
mContext.startActivity(i);
} else {//其它非特殊情况全部放行
view.loadUrl(url);
}
}
return true;
}
}
创建web.html并放入assets资源文件夹下,同时将WebViewJavascriptBridge.js也拷入
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type">
<title>
js调用java
</title>
</head>
<body>
<p>
<xmp id="show">Native将要回传回来的数据展示</xmp>
</p>
<p>
<input type="text" id="uname" value="用户名"/>
</p>
<p>
<input type="text" id="psw" value="密码"/>
</p>
<p>
<input type="button" id="send" value="发消息给Native" οnclick="sendClick();"
/>
</p>
<p>
<input type="button" id="use" value="调用Native方法" οnclick="useClick();"
/>
</p>
<p>
<input type="file" id="open" value="打开文件" οnclick="onOpen();"/>
</p>
<p>
<a href="tel:110">拨打电话110</a>
</p>
<p>
<a href="http://www.baidu.com">webview浏览器</a>
</p>
<p>
<a href="https://www.csdn.net">调用系统浏览器</a>
</p>
</body>
<script>
//Native方法直接调用,示例代码:mBridgeWebview.loadUrl("javascript:nativeFunction('" + data + "')");
function nativeFunction(data) {
document.getElementById("show").innerHTML = data;
}
//h5直接通过send向Native发送消息,在MyHandlerCallBack的Handler里接收,并可通过onCallBack方法回传
function sendClick() {
var name = document.getElementById("uname").value;
var pwd = document.getElementById("psw").value;
var data = "name = " + name + ", password = " + pwd;
window.WebViewJavascriptBridge.send(
data,
function(responseData) {
document.getElementById("show").innerHTML = responseData
}
);
}
//h5通过和Native统一命名方法名,如submitFromWeb来调取java代码,如mBridgeWebview.registerHandler("submitFromWeb", new BridgeHandler() {...});
function useClick() {
var name = document.getElementById("uname").value;
var pwd = document.getElementById("psw").value;
var data = "name = " + name + ", password = " + pwd;
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb', {'info':data},
function(responseData) {
document.getElementById("show").innerHTML = responseData;
}
);
}
function onOpen() {
var data = "调用文件";
//call native method
window.WebViewJavascriptBridge.callHandler(
'functionOpen'
, {'param': data }
, function(responseData) {
document.getElementById("open").innerHTML = responseData;
}
);
}
/****************************************************************/
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge)
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
callback(WebViewJavascriptBridge)
},
false
);
}
}
// 第一连接时初始化bridage
connectWebViewJavascriptBridge(function(bridge) {
//也注册默认的Handler,用来接收java调用的send(string,CallBackFunction)方法
bridge.init(function(message, responseCallback) {
console.log('JS got a message', message);
var data = {
'Javascript Responds': '测试中文!'
};
console.log('JS responding with', data);
responseCallback(data);
});
//注册handler等待java代码调用
//初始化时获取数据是调用此处代码
//参数:标识,要传递到JAVA的数据,回调方法。
//JAVA代码响应的方法:mBridgeWebview.callHandler("functionInJs", new Gson().toJson(实体类对象), new CallBackFunction(){onCallBack(String data)}
bridge.registerHandler("functionInJs", function(data, responseCallback) {
document.getElementById("show").innerHTML = ("data from Java: = " + data);
var responseData = "I am javascript, Data reception success!";
responseCallback(responseData);
});
})
/****************************************************************/
</script>
</html>
最后不要忘了加网络权限哦
<uses-permission android:name="android.permission.INTERNET"/>
四、总结
1、如果使用registerHandler,那么native和js中定义的标识必须一致
2、大家如果有出现上传文件图片失败,可能是适配原因,安卓系统5.0以上需要在WebChromeClient中实现onShowFileChooser()方法,详情请看代码示例