-
第一种方式:通过 addJavascriptInterface 方法进行添加对象映射
- 这种是使用最多的方式了,首先第一步我们需要设置一个属性:
mWebView.getSettings().setJavaScriptEnabled(true);
public class JSObject {
private Context mContext;
public JSObject(Context context) {
mContext = context;
}
@JavascriptInterface
public String showToast(String text) {
Toast.show(mContext, text, Toast.LENGTH_SHORT).show();
return "success";
}
/**
* 前端代码嵌入js:
* imageClick 名应和js函数方法名一致
*
* @param src 图片的链接
*/
@JavascriptInterface
public void imageClick(String src) {
Log.e("imageClick", "----点击了图片");
}
/**
* 网页使用的js,方法无参数
*/
@JavascriptInterface
public void startFunction() {
Log.e("startFunction", "----无参");
}
}
//特定版本下会存在漏洞
mWebView.addJavascriptInterface(new JSObject(this), "yc逗比");
-
JS 代码调用
- 这种方式的好处在于使用简单明了,本地和 JS 的约定也很简单,就是对象名称和方法名称约定好即可,缺点就是要提到的漏洞问题。
function showToast(){
var result = myObj.showToast("我是来自web的Toast");
}
function showToast(){
myObj.imageClick("图片");
}
function showToast(){
myObj.startFunction();
}
-
第二种方式:利用 WebViewClient 接口回调方法拦截 url
-
这种方式其实实现也很简单,使用的频次也很高,上面介绍到了 WebViewClient ,其中有个回调接口 shouldOverrideUrlLoading (WebView view, String url)) ,就是利用这个拦截 url,然后解析这个 url 的协议,如果发现是我们预先约定好的协议就开始解析参数,执行相应的逻辑。注意这个方法在 API24 版本已经废弃了,需要使用 shouldOverrideUrlLoading (WebView view, WebResourceRequest request)) 替代,使用方法很类似,我们这里就使用 shouldOverrideUrlLoading (WebView view, String url)) 方法来介绍一下:
- 代码很简单,这个方法可以拦截 WebView 中加载 url 的过程,得到对应的 url,我们就可以通过这个方法,与网页约定好一个协议,如果匹配,执行相应操作。
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//假定传入进来的 url = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri = Uri.parse(url);
String scheme = uri.getScheme();
//如果 scheme 为 js,代表为预先约定的 js 协议
if (scheme.equals("js")) {
//如果 authority 为 openActivity,代表 web 需要打开一个本地的页面
if (uri.getAuthority().equals("openActivity")) {
//解析 web 页面带过来的相关参数
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
}
//代表应用内部处理完成
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
function openActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";
}
- 存在问题:这个代码执行之后,就会触发本地的 shouldOverrideUrlLoading 方法,然后进行参数解析,调用指定方法。这个方式不会存在第一种提到的漏洞问题,但是它也有一个很繁琐的地方是,如果 web 端想要得到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 JS 方法把返回值传递回去,相关的代码如下:
//java
mWebView.loadUrl("javascript:returnResult(" + result + ")");
//javascript
function returnResult(result){
alert("result is" + result);
}
-
第三种方式:利用 WebChromeClient 回调接口的三个方法拦截消息
- 这个方法的原理和第二种方式原理一样,都是拦截相关接口,只是拦截的接口不一样:
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
//假定传入进来的 message = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri = Uri.parse(message);
String scheme = uri.getScheme();
if (scheme.equals("js")) {
if (uri.getAuthority().equals("openActivity")) {
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
//代表应用内部处理完成
result.confirm("success");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
function clickprompt(){
var result=prompt("js://openActivity?arg1=111&arg2=222");
alert("open activity " + result);
}
- 需要注意的是 prompt 里面的内容是通过 message 传递过来的,并不是第二个参数的 url,返回值是通过 JsPromptResult 对象传递。为什么要拦截 onJsPrompt 方法,而不是拦截其他的两个方法,这个从某种意义上来说都是可行的,但是如果需要返回值给 web 端的话就不行了,因为 onJsAlert 是不能返回值的,而 onJsConfirm 只能够返回确定或者取消两个值,只有 onJsPrompt 方法是可以返回字符串类型的值,操作最全面方便。
-
以上三种方案的总结和对比
- 以上三种方案都是可行的,在这里总结一下
- 第一种方式:是现在目前最普遍的用法,方便简洁,但是唯一的不足是在 4.2 系统以下存在漏洞问题;
- 第二种方式:通过拦截 url 并解析,如果是已经约定好的协议则进行相应规定好的操作,缺点就是协议的约束需要记录一个规范的文档,而且从 Native 层往 Web 层传递值比较繁琐,优点就是不会存在漏洞,iOS7 之下的版本就是使用的这种方式。
- 第三种方式:和第二种方式的思想其实是类似的,只是拦截的方法变了,这里拦截了 JS 中的三种对话框方法,而这三种对话框方法的区别就在于返回值问题,alert 对话框没有返回值,confirm 的对话框方法只有两种状态的返回值,prompt 对话框方法可以返回任意类型的返回值,缺点就是协议的制定比较麻烦,需要记录详细的文档,但是不会存在第二种方法的漏洞问题。
-
对于WebView加载一个网页过程中所产生的错误回调,大致有三种
/**
* 只有在主页面加载出现错误时,才会回调这个方法。这正是展示加载错误页面最合适的方法。
* 然而,如果不管三七二十一直接展示错误页面的话,那很有可能会误判,给用户造成经常加载页面失败的错觉。
* 由于不同的WebView实现可能不一样,所以我们首先需要排除几种误判的例子:
* 1.加载失败的url跟WebView里的url不是同一个url,排除;
* 2.errorCode=-1,表明是ERROR_UNKNOWN的错误,为了保证不误判,排除
* 3failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR_BAD_URL,排除
* @param webView webView
* @param errorCode errorCode
* @param description description
* @param failingUrl failingUrl
*/
@Override
public void onReceivedError(WebView webView, int errorCode,
String description, String failingUrl) {
super.onReceivedError(webView, errorCode, description, failingUrl);
// -12 == EventHandle.ERROR_BAD_URL, a hide return code inside android.net.http package
if ((failingUrl != null && !failingUrl.equals(webView.getUrl())
&& !failingUrl.equals(webView.getOriginalUrl())) /* not subresource error*/
|| (failingUrl == null && errorCode != -12) /*not bad url*/
|| errorCode == -1) { //当 errorCode = -1 且错误信息为 net::ERR_CACHE_MISS
return;
}
if (!TextUtils.isEmpty(failingUrl)) {
if (failingUrl.equals(webView.getUrl())) {
//做自己的错误操作,比如自定义错误页面
}
}
}
/**
* 只有在主页面加载出现错误时,才会回调这个方法。这正是展示加载错误页面最合适的方法。
* 然而,如果不管三七二十一直接展示错误页面的话,那很有可能会误判,给用户造成经常加载页面失败的错觉。
* 由于不同的WebView实现可能不一样,所以我们首先需要排除几种误判的例子:
* 1.加载失败的url跟WebView里的url不是同一个url,排除;
* 2.errorCode=-1,表明是ERROR_UNKNOWN的错误,为了保证不误判,排除
* 3failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR_BAD_URL,排除
* @param webView webView
* @param webResourceRequest webResourceRequest
* @param webResourceError webResourceError
*/
@Override
public void onReceivedError(WebView webView, WebResourceRequest webResourceRequest,
WebResourceError webResourceError) {
super.onReceivedError(webView, webResourceRequest, webResourceError);
}
/**
* 任何HTTP请求产生的错误都会回调这个方法,包括主页面的html文档请求,iframe、图片等资源请求。
* 在这个回调中,由于混杂了很多请求,不适合用来展示加载错误的页面,而适合做监控报警。
* 当某个URL,或者某个资源收到大量报警时,说明页面或资源可能存在问题,这时候可以让相关运营及时响应修改。
* @param webView webView
* @param webResourceRequest webResourceRequest
* @param webResourceResponse webResourceResponse
*/
@Override
public void onReceivedHttpError(WebView webView, WebResourceRequest webResourceRequest,
WebResourceResponse webResourceResponse) {
super.onReceivedHttpError(webView, webResourceRequest, webResourceResponse);
}
/**
* 任何HTTPS请求,遇到SSL错误时都会回调这个方法。
* 比较正确的做法是让用户选择是否信任这个网站,这时候可以弹出信任选择框供用户选择(大部分正规浏览器是这么做的)。
* 有时候,针对自己的网站,可以让一些特定的网站,不管其证书是否存在问题,都让用户信任它。
* 坑:有时候部分手机打开页面报错,绝招:让自己网站的所有二级域都是可信任的。
* @param webView webView
* @param sslErrorHandler sslErrorHandler
* @param sslError sslError
*/
@Override
public void onReceivedSslError(WebView webView, SslErrorHandler sslErrorHandler, SslError sslError) {
super.onReceivedSslError(webView, sslErrorHandler, sslError);
//判断网站是否是可信任的,与自己网站host作比较
if (WebViewUtils.isYCHost(webView.getUrl())) {
//如果是自己的网站,则继续使用SSL证书
sslErrorHandler.proceed();
} else {
super.onReceivedSslError(webView, sslErrorHandler, sslError);
}
}
-
首先载入js
//将js对象与java对象进行映射
webView.addJavascriptInterface(new ImageJavascriptInterface(context), "imagelistener");
-
html加载完成之后,添加监听图片的点击js函数,这个可以在onPageFinished方法中操作
@Override
public void onPageFinished(WebView view, String url) {
X5LogUtils.i("-------onPageFinished-------"+url);
//html加载完成之后,添加监听图片的点击js函数
//addImageClickListener();
addImageArrayClickListener(webView);
}
-
具体看addImageArrayClickListener的实现方法。
/**
* android与js交互:
* 首先我们拿到html中加载图片的标签img.
* 然后取出其对应的src属性
* 循环遍历设置图片的点击事件
* 将src作为参数传给java代码
* 这个循环将所图片放入数组,当js调用本地方法时传入。
* 当然如果采用方式一获取图片的话,本地方法可以不需要传入这个数组
* 通过js代码找到标签为img的代码块,设置点击的监听方法与本地的openImage方法进行连接
* @param webView webview
*/
private void addImageArrayClickListener(WebView webView) {
webView.loadUrl("javascript:(function(){" +
"var objs = document.getElementsByTagName(\"img\"); " +
"var array=new Array(); " +
"for(var j=0;j<objs.length;j++){" +
" array[j]=objs[j].src; " +
"}"+
"for(var i=0;i<objs.length;i++) " +
"{"
+ " objs[i].onclick=function() " +
" { "
+ " window.imagelistener.openImage(this.src,array); " +
" } " +
"}" +
"})()");
}
-
最后看看js的通信接口做了什么
public class ImageJavascriptInterface {
private Context context;
private String[] imageUrls;
public ImageJavascriptInterface(Context context,String[] imageUrls) {
this.context = context;
this.imageUrls = imageUrls;
}
public ImageJavascriptInterface(Context context) {
this.context = context;
}
/**
* 接口返回的方式
*/
@android.webkit.JavascriptInterface
public void openImage(String img , String[] imageUrls) {
Intent intent = new Intent();
intent.putExtra("imageUrls", imageUrls);
intent.putExtra("curImageUrl", img);
// intent.setClass(context, PhotoBrowserActivity.class);
context.startActivity(intent);
for (int i = 0; i < imageUrls.length; i++) {
Log.e("图片地址"+i,imageUrls[i].toString());
}
}
}