javascript 使用 JavascriptInterface 时未捕获的 TypeError

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/7424510/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-26 00:05:21  来源:igfitidea点击:

Uncaught TypeError when using a JavascriptInterface

javascriptandroidandroid-webview

提问by Mr. S

I'm currently displaying a bunch of data to the user as HTML in a webview. I have some links below each entry that should call a method in my app when clicked. The Android WebView's javascript interface seems to be the best (only?) way of handling these things. However, whenever I click the link, I get this error message: ERROR/Web Console(6112): Uncaught TypeError: Object [my namespace]@4075ff10 has no method 'edit' at [base URL]:55

我目前在 web 视图中以 HTML 的形式向用户显示一堆数据。我在每个条目下方都有一些链接,单击时应该调用我的应用程序中的方法。Android WebView 的 javascript 界面似乎是处理这些事情的最佳(唯一?)方式。但是,每当我单击链接时,都会收到以下错误消息:ERROR/Web Console(6112): Uncaught TypeError: Object [my namespace]@4075ff10 has no method 'edit' at [base URL]:55

I have the following interface declared:

我声明了以下接口:

public class JavaScriptInterface {
    Context context;

    JavaScriptInterface(Context c) {
        context = c;
    }

    public void edit(String postid) {
        Log.d("myApp", "EDIT!");
        //do stuff
    }
}

I then add it to my WebView:

然后我将它添加到我的 WebView:

final WebView threadView = (WebView) findViewById(R.id.webViewThread);
threadView.getSettings().setJavaScriptEnabled(true);
threadView.addJavascriptInterface(new JavaScriptInterface(this), "Android");

And, finally, I call this within my HTML as follows:

最后,我在我的 HTML 中这样称呼它:

<div class="post-actions">
    <div class="right">
        <a onClick="Android.edit('4312244');">Edit</a>
    </div>
</div>

The real kicker is this all works when I'm debugging my app via the emulator or adb connection to my phone. When I build and publish the app, it breaks.

真正的问题是当我通过模拟器或 adb 连接到我的手机调试我的应用程序时,这一切都有效。当我构建和发布应用程序时,它会中断。

I'm at my wits end. Any help or advice would be greatly appreciated!

我不知所措。任何帮助或建议将不胜感激!

回答by pjw

Same problem for my 2.3.3 mobile phone. But as I knew one app that worked and another not, I was not happy with this workaround. And I find out the differnce of my two apps. The one with the broken JavaScriptInterface uses Proguard. After a little search, I find a solution.

我的 2.3.3 手机也有同样的问题。但因为我知道一个应用程序有效而另一个无效,我对这种解决方法并不满意。我发现了我的两个应用程序的不同之处。JavaScriptInterface 损坏的那个使用 Proguard。经过一番搜索,我找到了解决方案

Short summary: interface JavascriptCallback, which is implemented by JavaScriptInterface and added rules for Proguard in proguard.conf:

简短总结:接口 JavascriptCallback,它由 JavaScriptInterface 实现,并在 proguard.conf 中添加了 Proguard 规则:

public interface JavascriptCallback {

}

public class JavaScriptInterface implements JavascriptCallback {
    Context mContext;
    /** Instantiate the interface and set the context */
    JavaScriptInterface(Context c) {
        mContext = c;
    }
    /** Show a toast from the web page */
    public void showToast(String toast) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
    }
}

proguard.cfg:

proguard.cfg:

-keep public class YOURPACKAGENAMEHERE.JavascriptCallback
-keep public class * implements YOURPACKAGENAMEHERE.JavascriptCallback
-keepclassmembers class * implements YOURPACKAGENAMEHERE.JavascriptCallback {
    <methods>;
}

回答by Mr. S

So, I'm pleased to say that my problem has been solved. Basically, it's a known bugin Gingerbread, and is present on my 2.3.4 device. After some head scratching, I found this workaroundconcocted by Jason Shah at PhoneGap. The real kudos for this goes to him as my solution is a slightly modified version of the code in that post.

所以,我很高兴地说我的问题已经解决了。基本上,这是Gingerbread 中的一个已知错误,并且存在于我的 2.3.4 设备上。经过一番摸索,我发现这个解决方法是由 Jason Shah 在 PhoneGap 设计的。真正的荣誉归于他,因为我的解决方案是该帖子中代码的略微修改版本。

The WebView

网络视图

In my onLoad method, I call the following function.

在我的 onLoad 方法中,我调用了以下函数。

private void configureWebView() {
    try {
        if (Build.VERSION.RELEASE.startsWith("2.3")) {
            javascriptInterfaceBroken = true;
        }
    } catch (Exception e) {
        // Ignore, and assume user javascript interface is working correctly.
    }

    threadView = (WebView) findViewById(R.id.webViewThread);
    threadView.setWebViewClient(new ThreadViewClient());
    Log.d(APP_NAME, "Interface Broken? " + javascriptInterfaceBroken.toString());
    // Add javascript interface only if it's not broken
    iface = new JavaScriptInterface(this);
    if (!javascriptInterfaceBroken) {
        threadView.addJavascriptInterface(new JavaScriptInterface(this), "Android");
    }
}

There are several things going on here.

这里有几件事情正在发生。

  1. In contrast with the PhoneGap method, I'm using a startsWithcomparison against the version string. This is because Build.VERSION.RELEASE is 2.3.4 on my reference device. Rather than test against all releases in the 2.3 series, I'm comfortable painting all devices with one brushstroke.

  2. javascriptInterface is a boolinitialized to false. JavaScriptInterface, instantiated as iface, is the class that normally handles JS events in my WebView.

  3. ThreadViewClient is the meat and potatoes of my implementation. It's where all the logic for handling the workaround occurs.

  1. 与 PhoneGap 方法相比,我使用的startsWith是版本字符串的比较。这是因为 Build.VERSION.RELEASE 在我的参考设备上是 2.3.4。与其对 2.3 系列中的所有版本进行测试,我更愿意用一笔画画所有设备。

  2. javascriptInterface 被bool初始化为false. JavaScriptInterface,实例化为iface,是通常在我的WebView中处理JS事件的类。

  3. ThreadViewClient 是我的实现的主要部分。这是处理变通方法的所有逻辑发生的地方。

The WebViewClient

WebViewClient

In the class ThreadViewClient (which extends WebViewClient), I first account for the fact that the js handler that Android normally attaches isn't here. This means that, if I want to use the same javascript calls from within my WebView, I need to duplicate the interface. This is accomplished by inserting custom handlers into the content of your website once it has loaded...

在 ThreadViewClient 类(它扩展了 WebViewClient)中,我首先说明了一个事实,即 Android 通常附加的 js 处理程序不在这里。这意味着,如果我想在我的 WebView 中使用相同的 javascript 调用,我需要复制界面。这是通过在您的网站加载后将自定义处理程序插入到您的网站内容中来实现的...

@Override
public void onPageFinished(WebView view, String url) {
    super.onPageFinished(view, url);
    if (javascriptInterfaceBroken) {
        final String handleGingerbreadStupidity =
        "javascript:function shortSignature(id) { window.location='http://MyHandler:shortSignature:'+id; }; "
        + "javascript: function longSignature(text, username, forumnumber,threadnumber,pagenumber,postid) { var sep='[MyHandler]';"
            + "window.location='http://MyHandler:longSignature:' + encodeURIComponent(text + sep + username + sep + forumnumber + sep + threadnumber + sep + pagenumber + sep + postid);};"
      + "javascript: function handler() { this.shortSignature = shortSignature; this.longSignature = longSignature;}; "
      + "javascript: var Android = new handler();";
        view.loadUrl(handleGingerbreadStupidity);
    }
}

There's a lot to process there. In the javascript, I define an object handlerthat contains the functions that map to my js interface. An instance of it is then bound to "Android", which is the same interface name as that used by non-2.3 implementation. This allows for re-use of the code rendered within your webview content.

那里有很多东西要处理。在 javascript 中,我定义了一个对象handler,其中包含映射到我的 js 接口的函数。然后将它的一个实例绑定到“Android”,它与非 2.3 实现使用的接口名称相同。这允许重用在您的 webview 内容中呈现的代码。

The functions take advantage of the fact that Android allows one to intercept all navigation that occurs within a WebView. In order to communicate with the outside program, they alter the window location to one with a special signature. I'll get into this in a bit.

这些函数利用了 Android 允许拦截 WebView 中发生的所有导航这一事实。为了与外部程序通信,他们将窗口位置更改为具有特殊签名的位置。我稍后会讲到这个。

Another thing I'm doing is concatenating the parameters of functions with more than one parameter. This allows me to reduce the code complexity within the location handler.

我正在做的另一件事是将函数的参数与多个参数连接起来。这使我能够降低位置处理程序中的代码复杂性。

The location handler is also placed in ThreadViewClient...

位置处理程序也放置在 ThreadViewClient 中...

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    Method sMethod = null;
    Log.d(APP_NAME, "URL LOADING");
    if (javascriptInterfaceBroken) {
        if (url.contains("MyHandler")) {
            StringTokenizer st = new StringTokenizer(url, ":");
          st.nextToken(); // remove the 'http:' portion
          st.nextToken(); // remove the '//jshandler' portion
          String function = st.nextToken();
          String parameter = st.nextToken();
          Log.d(APP_NAME, "Handler: " + function + " " + parameter);
          try {
            if (function.equals("shortSignature")) {
                iface.shortSignature(parameter);
            } else if (function.equals("longSignature")) {
                iface.longSignature(parameter);
            } else {
                if (sMethod == null) {
                    sMethod = iface.getClass().getMethod(function, new Class[] { String.class });
                  }
                    sMethod.invoke(iface, parameter);
            }
        }
        //Catch & handle SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
          return true;
        }
    }
    startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
    return true;
}

Here I am intercepting all URL load events that occur in the WebView. If the destination URL contains a magic string, the app attempts to parse it to extract out the method call. Rather than using the tokenizer to extract the individual parameters, I'm passing it to version of my longSignaturemethod that can parse and handle it. This is detailed in the final part of this post.

在这里,我拦截了 WebView 中发生的所有 URL 加载事件。如果目标 URL 包含魔法字符串,应用程序会尝试解析它以提取方法调用。我没有使用标记器来提取单个参数,而是将它传递给longSignature可以解析和处理它的方法版本。这在本文的最后部分有详细说明。

If, by the time it has exited the "javascriptInterfaceBroken" block, execution has not be returned to the caller, this method treats the URL loading action as a normal link clicked event. In the case of my application I don't want to use the WebView for that, so I pass it off to the operating system via the ACTION_VIEW intent.

如果在它退出“javascriptInterfaceBroken”块时,执行尚未返回给调用者,则此方法将 URL 加载操作视为正常的链接单击事件。对于我的应用程序,我不想为此使用 WebView,因此我通过 ACTION_VIEW 意图将其传递给操作系统。

This is very similar to the implementation on Jason's blog. However I am bypassing reflection for the most part. I was attempting to use the method in the block with reflection to handle all of my bound functions, but due to my JavaScriptInterface being a nested class I was unable to look into it from another. However, since I defined the interface within the main Activity scope, its methods can be called directly.

这与 Jason 博客上的实现非常相似。但是,我在很大程度上绕过了反射。我试图使用块中的方法和反射来处理我所有的绑定函数,但由于我的 JavaScriptInterface 是一个嵌套类,我无法从另一个类中查看它。但是,由于我在主 Activity 范围内定义了接口,因此可以直接调用其方法。

Handling Concatenated Parameters

处理连接参数

Finally, in my JavaScriptInterface, I created a handler to deal with the case of a concatenated parameter...

最后,在我的 JavaScriptInterface 中,我创建了一个处理程序来处理连接参数的情况......

public void longSignature(String everything) {
    try {
            everything = URLDecoder.decode(everything, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            Log.e(APP_NAME, e);
        }
    final String[] elements = everything.split("\[MyHandler\]");
    if (elements.length != 6) {
        Toast.makeText(getApplicationContext(), "[" + elements.length + "] wrong number of parameters!", Toast.LENGTH_SHORT).show();
    }
    else {
        longSignature(elements[0], elements[1], elements[2], elements[3], elements[4], elements[5]);
    }
}

Hooray polymorphism!

多态万岁!



And that's my solution! There's a lot of room for improvement, but, for now, this is sufficient. Sorry if some of my conventions have raised your hackles - this is my first Android app and I am unfamiliar with some of the best practices and conventions. Good luck!

这就是我的解决方案!有很大的改进空间,但就目前而言,这已经足够了。对不起,如果我的一些约定引起了您的不满 - 这是我的第一个 Android 应用程序,我不熟悉一些最佳实践和约定。祝你好运!

回答by Raghu

You have to annotate (@JavascriptInterface) methods in Java class that you want to make available to JavaScript.

您必须在 Java 类中注释 (@JavascriptInterface) 方法,以便让 JavaScript 可用。

    public class JavaScriptInterface {
Context context;

@JavascriptInterface
JavaScriptInterface(Context c) {
    context = c;
}

@JavascriptInterface
public void edit(String postid) {
    Log.d("myApp", "EDIT!");
    //do stuff
}    }

Its worked for me. Try out this.

它对我有用。试试这个。

回答by twig

I've taken Jason Shah's and Mr S's implementation as the building block for my fix and improved upon it greatly.

我已经将 Jason Shah 和 Mr S 的实现作为我修复的基石,并对其进行了很大的改进。

There's just far too much code to put into this comment I'll just link to it.

有太多代码可以放入此评论中,我将链接到它。

Key points are:

关键点是:

  • Applies to all versions of Gingerbread (2.3.x)
  • Calls from JS to Android are now synchronous
  • No longer have to map out interface methods manually
  • Fixed possibility of string separators breaking code
  • Much easier to change JS signature and interface names
  • 适用于所有版本的 Gingerbread (2.3.x)
  • 从 JS 到 Android 的调用现在是同步的
  • 不再需要手动绘制接口方法
  • 修复了字符串分隔符破坏代码的可能性
  • 更容易更改 JS 签名和接口名称