Android与H5交互框架实践(下)

上次已经初步完成了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更新,以及方法调用的各种回调的问题就不在这里细述了,有兴趣的可以一起讨论。文章是原创的,如有雷同,视你盗版。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容