Android WebView 实现 js 与 Java 交互

2017-09-16 10:46:28 +08:00
 BarZu

原文链接: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

5914 次点击
所在节点    Android
3 条回复
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
2017-09-16 11:05:15 +08:00
目前知道的是 webView.loadUrl 加载 http 页面的时候,webView.onPageFinished 后才能注入 js,这个对应页面的 window.load。
BarZu
2017-09-17 08:58:39 +08:00
@skyshy 先 view.addJavascriptInterface 再 view.loadUrl 可不可以?我也是新手,一起学习

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/391159

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX