上次已经初步完成了Js调用Android的过程,但是是Android方法如果有返回参数,比如支付成功啊,获取用户token给到H5怎么处理?还是那个协议
nicole://util:callbackId01/scan/params'sJson(参数的json)
之前一篇文章有写到这个callbackId01。这个callbackId01其实只是做一个标记用的,js端为什么要传递这个过来?Android方法的完成怎么样才能让Js端知道完成了?
不难想象,肯定是Android端方法执行完成,主动告知Js端,那么其实就是主动调用Js端的方法,并且传递对应的方法参数过去告知完成。而又因为这个方法有很多,不可能每个方法都去写对应一个对应的回调方法,所以需要进行封装。Js本地有一个类似Map的集合,key是方法的标记,value是方法。Js本地在调用Android的时候,如果是需要回调的,会把回调的那个标记也就是callbackId01传递给Android,Android在执行方法后,调用loadUrl(),或者evaluateJavascript(),执行相关方法的通用方法,Js端内部在通过map找到需要执行的方法,这样就完成了回调。有点绕,下面贴上代码,Js部分代码忽略。
//这个代码之前有贴过,再贴一遍。
@ModuleName("util")
public class JsUtil implements JsModuleApi {
/**
* 二维码扫描
*/
public void scanQRCode(WebView webView, String json, JavaCallback methodCallback, JsViewInterface jsViewInterface) {
//伪代码
QRCodeScanActivity.startQRCodeScanner(webView.getContext(), new OnQRScanListenerImpl() {
@Override
public void onScanQRCodeSuccess(String result) {
HashMap<String, Object> params = new HashMap<>(2);
params.put("resultData", result);
methodCallback.onSuccess(webView, params);
}
});
}
}
这里着重看下JavaCallback 这个类,所传递的数据也是统一格式code,message,data的形式。
public final class JavaCallback {
private String responseId;
public JavaCallback(String responseId) {
this.responseId = responseId;
}
public void onSuccess(WebView webView) {
onSuccess(webView, "");
}
public void onSuccess(WebView webView, Map<String, Object> paramsMap) {
onCallback(webView, paramsMap, true, new ErrorMessage("成功"));
}
public void onSuccess(WebView webView, Object obj) {
onCallback(webView, obj, true, new ErrorMessage("成功"));
}
public void onFail(WebView webView, ErrorMessage errorMessage) {
onCallback(webView, "", false, errorMessage);
}
public void onFail(WebView webView, Map<String, Object> paramsMap, ErrorMessage errorMessage) {
onCallback(webView, paramsMap, false, errorMessage);
}
private void onCallback(WebView webView, Map<String, Object> paramsMap, boolean isSuccess, ErrorMessage errorMessage) {
JsMessage jsMessage = new JsMessage();
JsMessage.JsData jsData = new JsMessage.JsData();
jsMessage.setResponseId(responseId);
if (paramsMap != null) {
jsData.setResult(paramsMap);
}
jsData.setMessage(errorMessage.getErrorMessage());
if (isSuccess) {
jsData.setCode(1);
} else {
jsData.setCode(0);
}
jsMessage.setResponseData(jsData);
String params = JsonUtil.objectToJson(jsMessage);
String formatJsCode = String.format(JsConstants.JS_CODE, params);
CLog.debug(params);
webView.evaluateJavascript(formatJsCode, null);
}
private void onCallback(WebView webView, Object obj, boolean isSuccess, ErrorMessage errorMessage) {
JsMessage jsMessage = new JsMessage();
JsMessage.JsData jsData = new JsMessage.JsData();
jsMessage.setResponseId(responseId);
if (obj!=null) {
jsData.setResult(obj);
}
jsData.setMessage(errorMessage.getErrorMessage());
if (isSuccess) {
jsData.setCode(1);
} else {
jsData.setCode(0);
}
jsMessage.setResponseData(jsData);
String params = JsonUtil.objectToJson(jsMessage);
String formatJsCode = String.format(JsConstants.JS_CODE, params);
CLog.d(params);
webView.evaluateJavascript(formatJsCode, null);
}
public String getResponseId() {
return responseId;
}
}
截止到现在这个CallbackId还没用上,其实在上一篇,反射执行模块方法的时候已经把id传过来了,看下之前部分模块的代码。
//JsBridgeEngine类里面,callBackId是解析传递过来的uri里面的
method.invoke(jsModuleApi, webView, decode, new JavaCallback(callBackId), jsViewInterface);
同样的Android调用Js端的方法,也可以通过传递Android本地的回调Id给Js,然后Js执行相关代码后再主动调用Android的方法进行回调。这样JsBridge的桥梁就搭建起来了。
H5的WebView的响应速度是不及原生的,尤其是页面显示的情况。通常如果网速比较慢,如果不做任何处理,基本就是一片空白。怎么去优化?html里面很多的Css,js,图片资源,等这些资源全部得到结果返回后原生的webview才会展示出界面。这就导致万一核心的资源没加载完成,界面就一直无法正确加载。那部分资源本地化就是比较好的方式。怎么让资源本地化?还是两个字“拦截”!为了区分方法调用和资源,我们用了WebChromeClient的onJsPrompt()拦截url调用方法,WebViewClient的shouldInterceptRequest拦截url加载资源。
public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) {
上面的这个方法会在比如Js一些资源是用src的方式引入url资源的时候触发。
如果是本地化的情况,比如某个js文件的引入
<script type="text/javascript" src="https://xxxxxx/xxxx.js></script>
这个时候其实可以把src换成
<script type="text/javascript" src="nicole://xxxxxx/xxxx.js></script>
这样会去触发WebViewClient的shouldInterceptRequest方法,第二个参数Request指的是网页发出的请求。截止目前,可以把我们Android看成一个小型服务器,网页之前要去远程服务器获取资源文件,现在改为从Android端获取。那下面就简单了,直接拦截其发出的url找到对应资源,通过文件流的形式返回资源给H5。下面还是代码。
@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) {
Uri uri = request.getUrl();
HashMap<String, String> map = new HashMap<>(2);
String scheme = uri.getScheme();
if (TextUtils.equals(scheme, JsConstants.SCHEMA)) {
String authority = uri.getAuthority();
WebResourceResponse response = null;
if (TextUtils.equals(authority, JsConstants.RESOURCE_LOCAL_ID)) {
try {
//目前就是图片
List<String> pathSegments = uri.getPathSegments();
String localId = pathSegments.get(0);
InputStream is = new FileInputStream(new File(LocalResSingleton.get().getLocalResource(localId)));
response = new WebResourceResponse(JsConstants.MIME_PNG, JsConstants.MIME_UTF_8, is);
} catch (Exception e) {
e.printStackTrace();
}
return response;
} else if (TextUtils.equals(authority, JsConstants.JS_ASSET)) {
String url = uri.toString();
CLog.debug("传过来的资源:" + url);
String assetFile = url.replace(JsConstants.JS_ASSET_ROOT_PATH, FileUtil.getWebDataPath());
// LocalResSingleton.get().getLocalAssetFile(url);
File file = new File(assetFile);
if (file == null || !file.exists()) {
return new WebResourceResponse("", JsConstants.MIME_UTF_8, null);
}
try {
InputStream is = new FileInputStream(file);
if (url.endsWith(JsConstants.SUFFIX_SVG)) {
response = new WebResourceResponse(JsConstants.MIME_SVG, JsConstants.MIME_UTF_8, is);
} else if (url.endsWith(JsConstants.SUFFIX_JS)) {
response = new WebResourceResponse(JsConstants.MIME_JS, JsConstants.MIME_UTF_8, is);
} else if (url.endsWith(JsConstants.SUFFIX_CSS)) {
response = new WebResourceResponse(JsConstants.MIME_CSS, JsConstants.MIME_UTF_8, is);
} else if (url.endsWith(JsConstants.SUFFIX_WOFF)) {
response = new WebResourceResponse(JsConstants.MIME_WOFF, JsConstants.MIME_UTF_8, is);
map.put(JsConstants.MIME_HEADER, JsConstants.MIME_DOT);
response.setResponseHeaders(map);
} else if (url.endsWith(JsConstants.SUFFIX_WOFF2)) {
response = new WebResourceResponse(JsConstants.MIME_WOFF2, JsConstants.MIME_UTF_8, is);
map.put(JsConstants.MIME_HEADER, JsConstants.MIME_DOT);
response.setResponseHeaders(map);
} else if (url.endsWith(JsConstants.SUFFIX_TTF)) {
response = new WebResourceResponse(JsConstants.MIME_TTF, JsConstants.MIME_UTF_8, is);
map.put(JsConstants.MIME_HEADER, JsConstants.MIME_DOT);
response.setResponseHeaders(map);
} else if (url.endsWith(JsConstants.SUFFIX_OTF)) {
response = new WebResourceResponse(JsConstants.MIME_OTF, JsConstants.MIME_UTF_8, is);
map.put(JsConstants.MIME_HEADER, JsConstants.MIME_DOT);
response.setResponseHeaders(map);
} else if (url.endsWith(JsConstants.SUFFIX_EOT)) {
response = new WebResourceResponse(JsConstants.MIME_EOT, JsConstants.MIME_UTF_8, is);
map.put(JsConstants.MIME_HEADER, JsConstants.MIME_DOT);
response.setResponseHeaders(map);
} else {
response = new WebResourceResponse("", JsConstants.MIME_UTF_8, is);
}
} catch (Exception e) {
e.printStackTrace();
}
return response;
} else {
return super.shouldInterceptRequest(webView, request);
}
} else {
return super.shouldInterceptRequest(webView, request);
}
}
上面的代码再解释下,如果拦截到了,就把response返回。如果不需要拦截,就让系统自己去处理。另外每种类型的文件都要有一个mimeType。从WebResourceResponse的构造方法可以看出来。
/**
* Constructs a resource response with the given MIME type, encoding, and
* input stream. Callers must implement
* {@link InputStream#read(byte[]) InputStream.read(byte[])} for the input
* stream.
*
* @param mimeType the resource response's MIME type, for example text/html
* @param encoding the resource response's encoding
* @param data the input stream that provides the resource response's data. Must not be a
* StringBufferInputStream.
*/
public WebResourceResponse(String mimeType, String encoding,
InputStream data) {
mMimeType = mimeType;
mEncoding = encoding;
setData(data);
}
MimeType我给大家整理下,这里自己看下就好。
//js mime type
public static final String MIME_JS = "application/javascript";
public static final String MIME_CSS = "text/css";
public static final String MIME_PNG = "image/png";
public static final String MIME_SVG = "image/svg+xml";
public static final String MIME_WOFF = "application/x-font-woff";
public static final String MIME_WOFF2 = "application/x-font-woff2";
public static final String MIME_TTF = "application/x-font-truetype";
public static final String MIME_OTF = "application/x-font-opentype";
public static final String MIME_EOT = "application/vnd.ms-fontobject";
public static final String MIME_HEADER = "Access-Control-Allow-Origin";
public static final String MIME_DOT = "*";
public static final String MIME_UTF_8 = "UTF-8";
关于资源还有几个问题说下
1.你的当前网页的网址如果是https的但是资源是http的话,这样是不允许的。
报错一般如下Mixed Content: The page at ’ was loaded over HTTPS, but requested an insecure resource ‘http://xxxxxx. This request has been blocked; the content must be served over HTTPS.
解决方式:
WebSettings settings = this.getSettings();
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
官方不太建议这种方式,会引发安全问题,所以尽量让H5那边统一,而且IOS端好像是没有混用的,我们项目最后是统一了。
2.跨域问题。报错如下: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://xxxxxxxx.ex.com is therefore not allowed access. 问题在网上搜了很多都没怎么搜到,都是说调用下面的两个api就好了。如果是android16以下的要做一个反射执行。
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);
我试了很多遍都没办法成功。最后想了想,既然本地是个服务器,那只能在response上面做文章了,我看了下response对象里面还真有一个api,关于header的。那么久比较简单了。
map.put("Access-Control-Allow-Origin","*")
response.setResponseHeaders(map);
暂时先写到这里吧,连同上篇文章,只是提供了一些思路,和遇到的几个问题分享,具体的项目工程还涉及到h5的版本控制,本地api更新,以及方法调用的各种回调的问题就不在这里细述了,有兴趣的可以一起讨论。文章是原创的,如有雷同,视你盗版。