Android WebView H5 Hybrid 混和开发

对于故乡,我忽然有了新的理解:人的故乡,并不止于一块特定的土地,而是一种辽阔无比的心情,不受空间和时间的限制;这心情一经唤起,就是你已经回到了故乡。——《记忆与印象》

前言

移动互联网发展至今,Android开发模式在不断更迭, 目前主要有三种开发模式 :原生开发、Hybrid开发以及跨平台开发。

  • 原生开发: 移动终端的开发主要分为两大阵营, Android(Java、Kotlin) 研发与 IOS(Swift)研发。
  • Hybrid开发: 多种技术栈混合开发App, 在Android中主要指Native与前端(JavaScript)技术的混合开发方式。
  • 跨平台研发: 同一个技术栈, 同一套代码可以在不同的终端上运行,极大的缩减了研发成本, 比如当下比较火的Flutter。

首先,我们需要做一些准备工作:为应用添加一个启用了 JavaScript 的 WebView,声明 INTERNET 权限(WebView 需此权限才能加载页面,即使页面内容为本地资源),在 Assets 资源文件夹中放置页面并加载。

Layout

    ...
    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    />
    ...

XML

Manifest

    <manifest ... >
        <uses-permission android:name="android.permission.INTERNET" />
        ...
    </manifest>

XML

MainActivity

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.webkit.WebView;
    ...
    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        WebView mWebView = findViewById(R.id.webview);
        mWebView.getSettings().setDefaultTextEncodingName("utf-8");
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to load the files in the assets folder
    }
    ...

WebView & H5 Hybrid混合开发基础知识

H5 Runtime支撑 - 浏览器内核

对于Java来说, 最大的一个优点是build once run anywhere(一处编译处处运行), 这一优点主要是通过JVM在不同的平台解释执行(在Android端使用的是基于JVM针对低性能小内存的设备优化的dalvik和art虚拟机)。

对于前端技术栈来说, Runtime依赖浏览器的支持, 浏览器主要依赖内核驱动,内核的两个主要功能一个是界面渲染, 一个是JavaScript 引擎(JS语法解析),当前的主流浏览器以及内核:

浏览器渲染内核JS引擎
IE/Edge(微软)Trident; EdgeHtmlJScript; Chakra
Safari(苹果)Webkit/Webkit2JavaScripCore/Nitro(4+)
Chrome(Google)Chromium(Webkit);BlinkV8
FireFoxGeckoSpiderMonkey(❤️.0);TackMonkey(<4.0);JaegerMonkey(4.0+)
OperaPresto;BlinkFuthark(9.5-10.2);CaraKan(10.5)

Chromium 是 Google 公司一个开源浏览器项目,使用 Blink 渲染引擎,V8 是 Blink 内置的JavaScript 引擎, Android端的WebView是基于Chromium的移动端浏览器组件。当前Android和IOS移动端的浏览器内核说到底都是基于Webkit。

!

WebKit主要分为四个部分:

  • 最上层 WebKit Embedding API 是 Browser UI 进行交互的 API 接口
  • 最下层 Platform API 提供与底层驱动的交互,如网络,字体渲染,影音文件解码,渲染引擎等
  • WebCore 它实现了对文档的模型化,包括了 CSS, DOM, Render 等的实现
  • JSCore 是专门处理 JS 脚本的引擎, 以及Hybrid通信支持

WebKit 所包含的绘制引擎 和 JS引擎,均是从KDE的KHTML及KJS引擎衍生而来。它们都是自由软件,在GPL条约下授权,同时支持BSD系统的开发。所以Webkit也是自由软件,同时开放源代码。

KDE: K桌面环境(K Desktop Environment)的缩写。一种著名的运行于 Linux、Unix 以及FreeBSD 等操作系统上的自由图形桌面环境

GNU: 通用公共许可协议(英语:GNU General Public License,缩写GNU GPL 或 GPL),是被广泛使用的自由软件许可证,给予了终端用户运行、学习、共享和修改软件的自由。

BSD: Berkeley Software Distribution,伯克利软件套件,是Unix的衍生系统,在1977至1995年间由加州大学伯克利分校开发和发布的。

JSBridge

JSBridge 是一座 Native 与 JavaScript 进行通讯的桥梁,它的核心是 构建 Native 和 JavaScript 双向通信的通道。

在这里插入图片描述

所谓 双向通信的通道:

  • JS 向 Native 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等。
  • Native 向 JS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。

JavascriptInterface

在 Android 和 Web 混合开发中,免不了 Java 与 JavaScript 代码相互调用,而 WebView 就给我们提供了这样一个接口:JavascriptInterface

public abstract @interface JavascriptInterface implements Annotation

Annotation that allows exposing methods to JavaScript. Starting from API level Build.VERSION_CODES.JELLY_BEAN_MR1 and above, only methods explicitly marked with this annotation are available to the Javascript code.

简单来说,在 Android 4.2 Jelly Bean(API 17)后,应用需要在方法中声明 @JavascriptInterface 注解,并将其所在类添加到 WebView 中,允许应用内启用了 JavaScript 的 WebView 直接调用其类成员方法。

MainActivity

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.widget.Toast;
    ...
    @SuppressLint("StaticFieldLeak")
    private static Context mContext;

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        WebView mWebView = findViewById(R.id.webview);
        mWebView.getSettings().setDefaultTextEncodingName("utf-8");
        mWebView.getSettings().setJavaScriptEnabled(true);

        mWebView.addJavascriptInterface(new JavaScriptBridge(), "Android"); // Export class JavaScriptBridge to WebView and map it to window.Android object in JavaScript

        mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to access the assets folder, or use file://android_res/ to access the res folder
    }

    @SuppressWarnings("unused")
    public static class JavaScriptBridge {

        @JavascriptInterface
        public void makeToast(final String message) {
            Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
        }
    }
    ...

WebPage

...
<script type="text/javascript">
    "use strict";
    window.Android.makeToast("Hello world");
</script>
...

HTML

上述示例代码将允许 JavaScript 通过 window.Android 对象,调用 JavaScriptBridge 类中声明了 @JavascriptInterface 注解的 makeToast 方法。运行后显示一个内容为 Hello world 的 Toast。


链接访问拦截

WebViewClient 提供了 shouldOverrideUrlLoading 事件,可以让我们在 URL 加载时做一些事情,比如拦截某个链接。

public boolean shouldOverrideUrlLoading (WebView view, WebResourceRequest request)

Give the host application a chance to take control when a URL is about to be loaded in the current WebView. If a WebViewClient is not provided, by default WebView will ask Activity Manager to choose the proper handler for the URL. If a WebViewClient is provided, returning true causes the current WebView to abort loading the URL, while returning false causes the WebView to continue loading the URL as usual.

MainActivity

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
    ...
    @SuppressLint("StaticFieldLeak")
    private static Context mContext;

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        WebView mWebView = findViewById(R.id.webview);
        mWebView.getSettings().setDefaultTextEncodingName("utf-8");
        mWebView.getSettings().setJavaScriptEnabled(true);

        mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                if (request.getUrl().toString().equalsIgnoreCase("https://www.google.cn/")) {
                    view.loadUrl("https://www.google.com/ncr");
                    return true;
                } else if (request.getUrl().toString().startsWith("meowcat://open_settings")) {
                    final Intent intent = mContext.getPackageManager().getLaunchIntentForPackage("com.android.settings");
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
                    mContext.startActivity(intent);
                    return true;
                }
                return false;
            }
        });

        mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to access the assets folder, or use file://android_res/ to access the res folder
    }
    ...

上述示例代码将在加载 https://www.google.cn/ 时跳转到 https://www.google.com/ncr*1,或在链接为 meowcat://open_settings 时打开系统设置。

除示例代码外,也可以直接 return true; 来中断页面加载。

注:该方法不适用于 POST 请求,页面在进行表单提交等 POST 请求时不会调用。


在页面内执行外部 JavaScript 代码

出于调试需求,我们可能需要通过 Java 代码在页面内执行一些 JavaScript 代码,使用 loadUrl(String)evaluateJavascript(String, ValueCallback<String>) 方法即可轻松实现该需求。若代码需要在页面加载完毕后执行,WebViewClient 也为我们提供了 onPageFinished 事件。

public void loadUrl (String url)

Loads the given URL.
Also see compatibility note on evaluateJavascript(String, ValueCallback).

public void evaluateJavascript (String script, ValueCallback resultCallback)

Asynchronously evaluates JavaScript in the context of the currently displayed page. If non-null, resultCallback will be invoked with any result returned from that execution. This method must be called on the UI thread and the callback will be made on the UI thread.
Compatibility note. Applications targeting Build.VERSION_CODES.N or later, JavaScript state from an empty WebView is no longer persisted across navigations like loadUrl(java.lang.String). For example, global variables and functions defined before calling loadUrl(java.lang.String) will not exist in the loaded page. Applications should use addJavascriptInterface(Object, String) instead to persist JavaScript objects across navigations.

public void onPageFinished (WebView view, String url)

Notify the host application that a page has finished loading. This method is called only for main frame. Receiving an onPageFinished() callback does not guarantee that the next frame drawn by WebView will reflect the state of the DOM at this point. In order to be notified that the current DOM state is ready to be rendered, request a visual state callback with WebView#postVisualStateCallback and wait for the supplied callback to be triggered.

MainActivity

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
    ...
    @SuppressLint("StaticFieldLeak")
    private static Context mContext;

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        WebView mWebView = findViewById(R.id.webview);
        mWebView.getSettings().setDefaultTextEncodingName("utf-8");
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.setWebViewClient(new WebViewClient() {

            @Override
            public void onPageFinished(WebView view, String url) {
                if (url.startsWith("https://www.google.")) {
                    view.loadUrl("javascript:(() => {window.location = 'https://www.google.com/ncr';})();");
                    // Equals with
                    // view.evaluateJavascript("window.location = 'https://www.google.com/ncr';", null);
                }
                super.onPageFinished(view, url);
            }

        });
        mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to access the assets folder, or use file://android_res/ to access the res folder
    }
    ...

上述示例代码将在页面加载完毕后,打开 https://www.google.cn/,而后被 shouldOverrideUrlLoading 方法跳转到 https://www.google.com/ncr

代码中 loadUrlevaluateJavascript 的示例等价,选用其一即可。

注:若使用 evaluateJavascript 方法的回调功能,则此方法与回调方法都必须在主线程(UI 线程)中执行或声明。


本地资源加载

在上面的示例代码中,我们使用了 file:///android_asset/ 来直接加载 assets 资源文件夹中的资源。但由于一些强制执行的安全策略(Content Security Policy)限制,使得该非同源 URL 无法正常被加载,这时候就可以使用 WebViewClient 提供的 shouldInterceptRequest 事件来辅助加载。

public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)

Notify the host application of a resource request and allow the application to return the data. If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the return response and data will be used.
This callback is invoked for a variety of URL schemes (e.g., http(s):, data:, file:, etc.), not only those schemes which send requests over the network. This is not called for javascript: URLs, blob: URLs, or for assets accessed via file:///android_asset/ or file:///android_res/ URLs.
In the case of redirects, this is only called for the initial resource URL, not any subsequent redirect URLs.

MainActivity

import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.webkit.JavascriptInterface;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

import java.io.IOException;
    ...
    @SuppressLint("StaticFieldLeak")
    private static Context mContext;

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        WebView mWebView = findViewById(R.id.webview);
        mWebView.getSettings().setDefaultTextEncodingName("utf-8");
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.addJavascriptInterface(new JavaScriptBridge(), "Android"); // Export class JavaScriptBridge to WebView and map it to window.Android object in JavaScript
        mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(WebView view, String url) {
                view.loadUrl("javascript:(() => {const script = document.createElement('script'); script.src = '/MeowCat-Android-Asset/www/js/main.js'; document.body.append(script);})();");
                super.onPageFinished(view, url);
            }

            @Override
            public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest webResourceRequest) {
                String url = webResourceRequest.getUrl().toString();
                Uri uri = Uri.parse(url);
                String key = uri.getScheme() + "://" + uri.getHost() + "/MeowCat-Android-Asset/";
                if (url.contains(key)) {
                    String assetsPath = url.replace(key, "");
                    try {
                        return new WebResourceResponse("text/plain", "UTF-8", getAssets().open(assetsPath));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                return super.shouldInterceptRequest(view, webResourceRequest);
            }

        });
        mWebView.loadUrl("https://www.google.com/ncr");
    }

    @SuppressWarnings("unused")
    private static class JavaScriptBridge {

        @JavascriptInterface
        public void makeToast(final String message) {
            Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
        }
    }
    ...

Main

"use strict";
window.Android.makeToast("Hello world");

JavaScript

上述示例代码将打开 https://www.google.com/ncr(因 shouldInterceptRequest 方法不会在加载特殊 Schemes 时被调用,故选用 Google 作为示例),页面加载完毕后插入 script 标签,加载并执行位于 file://android_asset/www/js/main.js 中的代码。运行后显示一个内容为 Hello world 的 Toast。

注:在 Android 官方开发文档 中,还有另一种使用 WebViewAssetLoader 的本地资源加载方式,感兴趣的可以自行研究一下,本文不再赘述。


JavaScript 弹窗提示

上面的示例代码已经可以帮助我们完成大多数需求,但在实际应用中发现了另外一个问题,JavaScript 的 alert() comfirm() prompt() 函数全部失效,这不是我们期望的行为。WebChromeClient 为我们提供了 onJsAlert onJsConfirm onJsPrompt 事件,分别对应上述函数,我们需要自行实现上述方法。

MainActivity

import androidx.appcompat.app.AlertDialog;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
    ...
    @SuppressLint("StaticFieldLeak")
    private static Context mContext;

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        WebView mWebView = findViewById(R.id.webview);
        mWebView.getSettings().setDefaultTextEncodingName("utf-8");
        mWebView.getSettings().setJavaScriptEnabled(true);

        mWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
                onJsDialog(DialogType.ALERT, view, url, message, result, null, null);
                return true;
            }

            @Override
            public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
                onJsDialog(DialogType.CONFIRM, view, url, message, result, null, null);
                return true;
            }

            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
                onJsDialog(DialogType.PROMPT, view, url, message, null, defaultValue, result);
                return true;
            }
        });

        mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to access the assets folder, or use file://android_res/ to access the res folder
    }

    private enum DialogType {
        ALERT,
        CONFIRM,
        PROMPT
    }

    private static void onJsDialog(DialogType type, WebView view, String url, String message, final JsResult result, String defaultValue, final JsPromptResult promptResult) {
        AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
        String[] content = message.split(":", 2);
        builder.setTitle(content[0]);
        builder.setMessage(content[1] + "\n" + url);
        builder.setCancelable(false);
        switch (type) {
            case PROMPT:
                builder.setPositiveButton(android.R.string.ok, (dialog, which) -> promptResult.confirm(defaultValue)); // TODO: Input
                break;
            case CONFIRM:
                builder.setCancelable(true);
                builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> result.cancel());
            case ALERT:
            default:
                builder.setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm());
        }
        builder.create().show();
    }
    ...

HTML

...
<script type="text/javascript">
    "use strict";
    alert("Alert Title:This is an alert");
    confirm("Confirm Title:This is a confirm") ? alert("Alert Title (Confirm):You confirmed the dialog") : alert("Alert Title (Confirm):You canceled the dialog");
    alert("Alert Title (Prompt):Prompt content is " + prompt("Prompt Title:This is a prompt", "Hello world"));
</script>
...

HTML

上述示例代码中,onJsDialog 方法统一处理了来自 WebChromeClient 的 onJsAlert onJsConfirm onJsPrompt 事件,添加了标题(JavaScript 函数仅支持信息传参,这里以第一个 : 作为标题和信息的分隔符),弹出对话框并返回;DialogType 用于判断事件类型。

运行后依次弹出对话框,内容分别为:

Alert Title
This is an alert
file:///android_asset/www/index.html
                                    [OK]
Confirm Title
This is a confirm
file:///android_asset/www/index.html
                    [CANCEL] [OK]

若点击了 OK

Alert Title (Confirm)
You confirmed the dialog
file:///android_asset/www/index.html
                                    [OK]

若点击了 CANCEL

Alert Title (Confirm)
You canceled the dialog
file:///android_asset/www/index.html
                                    [OK]
Prompt Title
This is a prompt
file:///android_asset/www/index.html
                                    [OK]
Alert Title (Prompt)
Prompt content is Hello world
file:///android_asset/www/index.html
                                    [OK]

亦可根据其他需求定制对话框的样式和(或)功能。

注:onJsDialog 方法仅作为示例,并未实现 prompt() 函数的输入功能,以默认值返回。


实战:在页面中插入 vConsole 并在成功插入后弹出提示对话框

vConsole 是腾讯出品的一个轻量、可拓展、针对手机网页的前端开发者调试面板,可以在 Vue、React 或其他任何框架中使用。用于移动设备调试非常好用,下面的实例将使用本文所介绍的所有技巧,在页面底部插入 vConsole。

下载 vconsole.min.js 并保存至 assets 资源文件夹中:https://cdn.jsdelivr.net/npm/vconsole@latest/dist/vconsole.min.js

Java

import androidx.appcompat.app.AlertDialog;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.webkit.JavascriptInterface;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

import java.io.IOException;
    ...
    @SuppressLint("StaticFieldLeak")
    private static Context mContext;

    @SuppressLint("SetJavaScriptEnabled")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        WebView mWebView = findViewById(R.id.webview);
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.addJavascriptInterface(new JavaScriptBridge(), "Android"); // Export class JavaScriptBridge to WebView and map it to window.Android object in JavaScript
        mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                if (request.getUrl().toString().equalsIgnoreCase("https://www.google.cn/")) {
                    view.loadUrl("https://www.google.com/ncr");
                    return true;
                } else if (request.getUrl().toString().startsWith("meowcat://open_settings")) {
                    final Intent intent = mContext.getPackageManager().getLaunchIntentForPackage("com.android.settings");
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
                    mContext.startActivity(intent);
                    return true;
                }
                return false;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                view.loadUrl("javascript:(() => {const script = document.createElement('script'); script.src='/MeowCat-Android-Asset/www/js/vconsole.min.js'; document.body.append(script); script.onload = () => {alert('vConsole:Loaded!'); if (typeof VConsole !== 'undefined') {new VConsole({onReady: () => {const vc = document.getElementById('__vconsole'); const vc_switch = vc.querySelector('.vc-switch'); vc.style.position = 'relative'; vc.style.zIndex = 9999999999; vc_switch.style.opacity = 'opacity' in this ? this.opacity : .5;},});}};})();");
                super.onPageFinished(view, url);
            }

            @Override
            public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest webResourceRequest) {
                String url = webResourceRequest.getUrl().toString();
                Uri uri = Uri.parse(url);
                String key = uri.getScheme() + "://" + uri.getHost() + "/MeowCat-Android-Asset/";
                if (url.contains(key)) {
                    String assetsPath = url.replace(key, "");
                    try {
                        return new WebResourceResponse("text/plain", "UTF-8", getAssets().open(assetsPath));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                return super.shouldInterceptRequest(view, webResourceRequest);
            }
        });
        mWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
                onJsDialog(DialogType.ALERT, view, url, message, result, null, null);
                return true;
            }

            @Override
            public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
                onJsDialog(DialogType.CONFIRM, view, url, message, result, null, null);
                return true;
            }

            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
                onJsDialog(DialogType.PROMPT, view, url, message, null, defaultValue, result);
                return true;
            }
        });
        mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to load the files in the assets folder
    }

    private enum DialogType {
        ALERT,
        CONFIRM,
        PROMPT
    }

    private static void onJsDialog(DialogType type, WebView view, String url, String message, final JsResult result, String defaultValue, final JsPromptResult promptResult) {
        AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
        String[] content = message.split(":", 2);
        builder.setTitle(content[0]);
        builder.setMessage(content[1] + "\n" + url);
        builder.setCancelable(false);
        switch (type) {
            case PROMPT:
                builder.setPositiveButton(android.R.string.ok, (dialog, which) -> promptResult.confirm(defaultValue)); // TODO: Input
                break;
            case CONFIRM:
                builder.setCancelable(true);
                builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> result.cancel());
            case ALERT:
            default:
                builder.setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm());
        }
        builder.create().show();
    }

    @SuppressWarnings("unused")
    private static class JavaScriptBridge {

        @JavascriptInterface
        public void makeToast(final String message) {
            Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
        }
    }
    ...

WebPage

...
<script type="text/javascript">
    "use strict";
    alert("Alert Title:This is an alert");
    confirm("Confirm Title:This is a confirm") ? alert("Alert Title (Confirm):You confirmed the dialog") : alert("Alert Title (Confirm):You canceled the dialog");
    alert("Alert Title (Prompt):Prompt content is " + prompt("Prompt Title:This is a prompt", "Hello world"));
    window.Android.makeToast("Hello world");
    window.location = "https://www.google.cn/";
</script>
...

HTML

运行代码,最终您将能够看到如下提示:

vConsole
Loaded!
https://www.google.com/
                                    [OK]

然后在页面的右下角,会出现一个绿色按钮,上面写着 vConsole。我们做到了,那正是我们想要的。

常见问题

1. 前端如何调试WebView

  • 首先,要在WebView页面打开可以debug的设置。(不过只支持KITKAT以上版本)
scss 代码解读复制代码if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
   mWeb.setWebContentsDebuggingEnabled(true);
}
  • Android端需要开启开发者模式, 然后打开usb调试, 最后插上电脑。
  • 在Chrome地址栏输入:Chrome://inspect。你会看到如下界面。

img

正常的话在App中打开WebView时,chrome中会监听到并显示页面。

  • 点击页面下的inspect,就可以实时看到手机上WebView页面的显示状态了。

在这里插入图片描述

2.JS 如何传递 Uint8Array到 Android端:

  • 方法1: 注入参数为String data 的方法。通过Base64作为传输载体, 前端将Uint8Array数据转Base64, Native侧将Base64解析为byte[]。
  • 方法2: 注入参数为byte[] bytes 的方法。

直接传递字符串, 无论字符串多长,传递时间都在 10ms内, 推断字符串传递可能采用内存映射, 直接传递内存地址.

传递uint8array, 数据越长时间越长, 推断可能底层涉及某些转换操作, 从 js uint8 到 java byte。

3.Android端 如何加载本地前端资源

  • 资源文件放置Assert文件夹中

标签加载

ini 代码解读复制代码<script type="module" 
    crossorigin src="/android_asset/parkingtest/dist/assets/index.34d4f8c4.js"/>
<link rel="stylesheet" 
    href="/android_asset/parkingtest/dist/assets/index.cf521aaf.css">

代码加载URL

arduino

 代码解读
复制代码"file:///android_asset/xxx/xxx/src.js"
  • 资源文件放在本地SD存储

通过请求拦截方式, 拦截前端资源请求, 获取需要加载的文件名称,通过JAVA IO 加载 File 返回给前端。

加载代码(伪代码)

scala 代码解读复制代码 public class MyWebViewClient extends WebViewClient {
        @Nullable
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
            String url = request.getUrl().toString();
            String fileName = Fileurl.getFileName();
            ByteArrayInputStream fileStream = JavaIO.loadFile(filePath + fileName);
            return new WebResourceResponse(
            mimeType, 
            encoding,
            statusCode, 
            reasonPhrase, 
            responseHeaders, 
            byteArrayInputStream);
        }
    }

引申阅读:在 Android 开发者文档 中,还有更多关于 Android WebView 混合开发的内容。


*1: NCR: No Country Redirect,Google 支持禁用地区跳转功能。

参考:Android WebView & H5 Hybrid开发知识点整理
DSBridge for Android
Java & V8 通讯
深入理解JSCore

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/882315.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python | Leetcode Python题解之第415题字符串相加

题目&#xff1a; 题解&#xff1a; class Solution:def addStrings(self, num1: str, num2: str) -> str:res ""i, j, carry len(num1) - 1, len(num2) - 1, 0while i > 0 or j > 0:n1 int(num1[i]) if i > 0 else 0n2 int(num2[j]) if j > 0 e…

Dify创建自定义工具,调用ASP.NET Core WebAPI时的注意事项(出现错误:Reached maximum retries (3) for URL ...)

1、要配置Swagger using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Models;var builder WebApplication.CreateBuilder(args);builder.Services.AddCors(options > {options.AddPolicy("AllowSpecificOrigin",builder > builder.WithOrigins("…

SpringSecurity6.x整合手机短信登录授权

前言&#xff1a;如果没有看过我的这篇文章的Springboot3.x.x使用SpringSecurity6(一文包搞定)_springboot3整合springsecurity6-CSDN博客需要看下&#xff0c;大部分多是基于这篇文章的基础上实现的。 明确点我们的业务流程&#xff1a; 需要有一个发送短信的接口&#xff0…

【C++】10道经典面试题带你玩转二叉树

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:C ⚙️操作环境:Leetcode/牛客网 目录 一.根据二叉树创建字符串 二.二叉树的层序遍历 三.二叉树的层序遍历 II 四.二叉树的最近公共祖先 五.二叉搜索树与双向链表 六.从前序与中序遍历序列构造二叉树 七.从中序与后序遍历…

基于yolov8的无人机检测系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv8的无人机检测系统是一项前沿技术&#xff0c;结合了YOLOv8深度学习模型的强大目标检测能力与无人机的灵活性。YOLOv8作为YOLO系列的最新版本&#xff0c;在检测精度和速度上均有显著提升&#xff0c;特别适用于复杂和高动态的场景。 该系统通过捕获实…

【QML 基础】QML ——描述性脚本语言,用于用户界面的编写

文章目录 1. QML 定义2. QML 1. QML 定义 &#x1f427; QML全称为Qt Meta-Object Language&#xff0c;QML是一种描述性的脚本语言&#xff0c;文件格式以.qml结尾。支持javascript形式的编程控制。QML是Qt推出的Qt Quick技术当中的一部分&#xff0c;Qt Quick是 Qt5中用户界…

C++笔记---set和map

1. 序列式容器与关联式容器 前面我们已经接触过STL中的部分容器如&#xff1a;string、vector、list、deque、array、forward_list等&#xff0c;这些容器统称为序列式容器&#xff0c;因为逻辑结构为线性序列的数据结构&#xff0c;两个位置存储的值之间一般没有紧密的关联关…

U盘格式化了怎么办?这4个工具能帮你恢复数据。

如果你思维U盘被格式化了&#xff0c;也不用太过担心&#xff0c;其实里面的数据并没有被删除&#xff0c;只是被标记为了可覆盖的状态。只要我们及时采取正确的数据恢复措施&#xff0c;就有很大的机会可以将数据找回。比如使用专业得的数据恢复软件&#xff0c;我也可以跟大家…

缓存的思考与总结

缓存的思考与总结 什么是缓存缓存命中率数据一致性旁路模式 Cache aside双写模式直写模式 write through异步写 Write Behind 旁路和双写 案例 新技术或中间的引入&#xff0c;一定是解决了亟待解决的问题或是显著提升了系统性能&#xff0c;并且这种改变所带来的增幅&#xff…

python新手的五个练习题

代码 # 1. 定义一个变量my_Number,将其设置为你的学号&#xff0c;然后输出到终端。 my_Number "20240001" # 假设你的学号是20240001 print("学号:", my_Number) # 2. 计算并输出到终端:两个数(例如3和5)的和、差、乘积和商。 num1 3 num2 5 print(&…

nodejs基于vue电子产品商城销售网站的设计与实现 _bugfu

目录 技术栈具体实现截图系统设计思路技术可行性nodejs类核心代码部分展示可行性论证研究方法解决的思路Express框架介绍源码获取/联系我 技术栈 该系统将采用B/S结构模式&#xff0c;开发软件有很多种可以用&#xff0c;本次开发用到的软件是vscode&#xff0c;用到的数据库是…

论文集搜索网站-dblp 详细使用方法

分享在dblp论文集中的两种论文搜索方式&#xff1a;关键字搜索&#xff0c;指定会议/期刊搜索。 关键字搜索 进入dblp官方网址dblp: computer science bibliography&#xff0c;直接在上方搜索栏&#xff0c;搜索关键字&#xff0c;底下会列出相关论文。 指定会议/期刊搜索 …

三菱FX5U PLC故障处理(各种出错的内容、原因及处理方法进行说明。)

对使用系统时发生的各种出错的内容、原因及处理方法进行说明。 故障排除的步骤 发生故障时&#xff0c;按以下顺序实施故障排除。 1.确认各模块是否正确安装或正确配线。 2、确认CPU模块的LED。 3.确认各智能功能模块的LED。(各模块的用户手册) 4、连接工程工具&#xff0c;启…

从数据仓库到数据中台再到数据飞轮:我了解的数据技术进化史

这里写目录标题 前言数据仓库&#xff1a;数据整合的起点数据中台&#xff1a;数据共享的桥梁数据飞轮&#xff1a;业务与数据的双向驱动结语 前言 在当今这个数据驱动的时代&#xff0c;企业发展离不开对数据的深度挖掘和高效利用。从最初的数据仓库&#xff0c;到后来的数据…

828华为云征文|华为Flexus云服务器搭建Cloudreve私人网盘

一、华为云 Flexus X 实例&#xff1a;开启高效云服务新篇&#x1f31f; 在云计算的广阔领域中&#xff0c;资源的灵活配置与卓越性能犹如璀璨星辰般闪耀。华为云 Flexus X 实例恰似一颗最为耀眼的新星&#xff0c;将云服务器技术推向了崭新的高度。 华为云 Flexus X 实例基于…

使用SpringCloud构建可伸缩的微服务架构

Spring Cloud是一个用于构建分布式系统的开源框架。它基于Spring Boot构建&#xff0c;并提供了一系列的工具和组件&#xff0c;用于简化开发分布式系统的难度。Spring Cloud可以帮助开发人员快速构建可伸缩的微服务架构。 要使用Spring Cloud构建可伸缩的微服务架构&#xff0…

对接阿里asr和Azure asr

1&#xff1a;对接阿里asr 1.1&#xff1a;pom <dependency><groupId>com.alibaba.nls</groupId><artifactId>nls-sdk-recognizer</artifactId><version>2.2.1</version> </dependency>1.2&#xff1a;生成token package c…

C++之STL—vector容器基础篇

头文件 #include <vector> //vector容器 #include <algorithm> //算法 基本用法&&概念 vector<int> v; v.push_back(10); vector<int >::iterator v.begin(); v.end(); 三种遍历方式 #include <vector> #include <algorithm>…

基于区块链的相亲交易系统源码解析

随着区块链技术的成熟与发展&#xff0c;其去中心化、不可篡改的特性逐渐被应用于各行各业。特别是在婚恋市场中&#xff0c;区块链技术的应用为相亲平台带来了新的可能性 。本文将探讨如何利用区块链技术构建一个透明、高效的相亲交易系统&#xff0c;并提供部分源码示例。 区…

大模型的实践应用30-大模型训练和推理中分布式核心技术的应用

大家好,我是微学AI,今天给大家介绍一下大模型的实践应用30-大模型训练和推理中分布式核心技术的应用。本文深入探讨了大模型训练和推理中分布式核心技术的应用。首先介绍了项目背景,阐述了大模型发展对高效技术的需求。接着详细讲解了分布式技术的原理,包括数据并行、模型并…