V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
BarZu
V2EX  ›  Android

Android WebView 实现 js 与 Java 交互

  •  
  •   BarZu · 2017-09-16 10:46:28 +08:00 · 10420 次点击
    这是一个创建于 2659 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文链接:https://my.oschina.net/cjlice/blog/1531256

    刚学 Android 用 WebView 来做应用

    Android Studio 下载: http://www.android-studio.org/

    写此文时最新稳定版是 2.3.3,预览版 3.0.0,3.0.0 支持使用 Kotlin 进行开发,有点坑,毕竟还不够成熟

    安卓开发文档:https://developer.android.google.cn/develop/index.html

    网上比较流行的 js 与 java 交互解决方案:https://github.com/lzyzsd/JsBridge

    百度、Google 过好多文章,都是对addJavascriptInterfaceevaluateJavascript很粗暴的解释,很少具体实例代码,所以自己整理了一下,以下方案仅对 android4.4 以上可用

    照着安卓官方开发文档新建一个安卓工程出来不难,就不详说了,直接上代码,看注释

    MainActivity.java

    package com.example.demo.mydemo;
    
    import android.os.Build;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.KeyEvent;
    import android.webkit.JavascriptInterface;
    import android.webkit.ValueCallback;
    import android.webkit.WebResourceRequest;
    import android.webkit.WebView;
    import android.webkit.WebViewClient;
    
    public class MainActivity extends AppCompatActivity {
        WebView view;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            view = new WebView(this);
            super.onCreate(savedInstanceState);
            // 注释默认视图,稍候将我们创建的 WebView 渲染出来
            // setContentView(R.layout.activity_main);
            // 开启对 javascript 支持,WebView 默认是不支持 javascript 的
            view.getSettings().setJavaScriptEnabled(true);
            // 允许 chrome 浏览器进行远程调试
            view.setWebContentsDebuggingEnabled(true);
            // 如果不重写 shouldOverrideUrlLoading 这个方法,默认使用外部浏览器打开网页
            view.setWebViewClient(new WebViewClient() {
                @Override
                public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                    if (Build.VERSION.SDK_INT >= 21) {
                        view.loadUrl(request.getUrl().toString());
                    } else {
                        view.loadUrl(request.toString());
                    }
                    return false;
                }
            });
            // 注入可在 webview 用 js 调用 java 方法的对象和变量 Android
            view.addJavascriptInterface(new MyJs(), "Android");
            // 加载首页
            view.loadUrl("file:///android_asset/index.html");
    
            // 使 activity 显示 webview 的内容
            setContentView(view);
        }
    
        public class MyJs {
            // 注册给 js 用的方法
            @JavascriptInterface
            public void showToast (final String id, final String context) {
                // 在 Android Monitor 的 logcat 可以查看这句 java 的输出
                Log.i("toast", context);
    
                // 调用 js 的全局方法将数据返回给 js (必须用 UI 线程,否则 app 会崩溃)
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                    // 返回给 js 的回调数据,
                    // id 用于给 js 找到对应的局部回调函数,
                    // context 应该是 java 获取到的数据,这里为了方便,直接将 js 传过来的数据返回
                    String data = "{id:" + id + ", result: '" + context + "'}";
    
                    //执行 js 的方法(只能执行 windows 域下的全局方法)
                    view.evaluateJavascript("exec(" + data + ")", new ValueCallback<String>() {
                        @Override
                        public void onReceiveValue(String s) {
                            // 当 exec 方法执行完成,会回调这个 java 方法
                            // s 是 exec 的 return 值(一般很少用到)
                            Log.i("callback", s);
                        }
                    });
                    }
                });
            }
        }
    
        // 处理返回键
        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            if(keyCode==KeyEvent.KEYCODE_BACK)
            {
                if(view.canGoBack())
                {
                    view.goBack();//返回上一页面
                    return true;
                }
                else
                {
                    System.exit(0);//退出程序
                }
            }
            return super.onKeyDown(keyCode, event);
        }
    }
    
    

    在 WebView 里必须使用 UI 线程的官方原文:https://developer.android.google.cn/guide/webapps/migrating.html#Threads

    If you call methods on WebView from any thread other than your app's UI thread, it can cause unexpected results.

    我自己翻译:如果你使用了非 UI 线程的其它线程去调用 WebView 里的方法,将会产生不被期待的结果。

    上文我说 APP 会崩溃是实践所得。

    创建index.htmlindex.js文件,放到 assets 文件夹里,如果项目里没 assets 文件夹,可以这样创建:File > New > Folder > Assets Folder

    index.html

    <html>
        <head>
            <meta charset="utf-8" />
            <title>Hello Demo</title>
        </head>
        <body>
            <button id="a">A</button>
            <script src="index.js"></script>
        </body>
    </html>
    

    index.js

    // 用于区别回调
    var id = 0
    // 回调池,存放局部回调
    window.pool = []
    // 给 java 调用的全局方法
    window.exec = function (data) {
      for (var i = 0; i < pool.length; i++) {
        if (pool[i][0] == data.id) {
          pool[i][1](data.result)
          pool.splice(i, 1)
          return true
        }
      }
    }
    // 封闭 java 方法的全局空间,方便给 js 调用
    window.orz = {
      showToast: function (msg, callback) {
        Android.showToast(++id, msg)
        pool.push([id, callback])
      }
    }
    document.getElementById('a').onclick = function () {
      // 经过上面的一层封装,调用原生并执行回调就很方便了
      orz.showToast('Call', function (txt) {
        // 这里用了 console 输出,需要在谷歌浏览器 Remote devices 对应的 Inspect 的 Console 查看,不能用 alert,alert 方法还没实现
        console.log(txt)
      })
      orz.showToast('Me', function (txt) {
        console.log(txt)
      })
      orz.showToast('Superman', function (txt) {
        console.log(txt)
      })
    }
    

    总结:js 和 java 交互的原理主要是用addJavascriptInterface给 WebView 注入 java 方法,让 js 可以调用,然后用evaluateJavascript执行 js 方法将数据传回 js,也可以用view.loadUrl("javascript:...")来代替evaluateJavascript

    3 条回复    2017-09-17 08:58:39 +08:00
    skyshy
        1
    skyshy  
       2017-09-16 11:02:58 +08:00
    如果 index.html 不是为 **filte://** app 本地文件,而为 **http(s)://**的一个 URL 文件路径,如果页面里引用很多其他资源( js、css、images 和 iframe 等),怎么让页面加载到 [`DOMContentLoaded]( https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded)` 的时候就有 webview 注入的 js 呢?
    skyshy
        2
    skyshy  
       2017-09-16 11:05:15 +08:00
    目前知道的是 webView.loadUrl 加载 http 页面的时候,webView.onPageFinished 后才能注入 js,这个对应页面的 window.load。
    BarZu
        3
    BarZu  
    OP
       2017-09-17 08:58:39 +08:00
    @skyshy 先 view.addJavascriptInterface 再 view.loadUrl 可不可以?我也是新手,一起学习
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3210 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 10:48 · PVG 18:48 · LAX 02:48 · JFK 05:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.