Android WebView H5 Hybrid 混和开发
对于故乡,我忽然有了新的理解:人的故乡,并不止于一块特定的土地,而是一种辽阔无比的心情,不受空间和时间的限制;这心情一经唤起,就是你已经回到了故乡。——《记忆与印象》
前言
移动互联网发展至今,Android开发模式在不断更迭, 目前主要有三种开发模式 :原生开发、Hybrid开发以及跨平台开发。
- 原生开发: 移动终端的开发主要分为两大阵营, Android(Java、Kotlin) 研发与 IOS(Swift)研发。
- Hybrid开发: 多种技术栈混合开发App, 在Android中主要指Native与前端(JavaScript)技术的混合开发方式。
- 跨平台研发: 同一个技术栈, 同一套代码可以在不同的终端上运行,极大的缩减了研发成本, 比如当下比较火的Flutter。
首先,我们需要做一些准备工作:为应用添加一个启用了 JavaScript 的 WebView,声明 INTERNET 权限(WebView 需此权限才能加载页面,即使页面内容为本地资源),在 Assets 资源文件夹中放置页面并加载。
Layout
...<WebViewandroid: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")@Overrideprotected 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; EdgeHtml | JScript; Chakra |
Safari(苹果) | Webkit/Webkit2 | JavaScripCore/Nitro(4+) |
Chrome(Google) | Chromium(Webkit);Blink | V8 |
FireFox | Gecko | SpiderMonkey(❤️.0);TackMonkey(<4.0);JaegerMonkey(4.0+) |
Opera | Presto;Blink | Futhark(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")@Overrideprotected 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 JavaScriptmWebView.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 {@JavascriptInterfacepublic 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 returningfalse
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")@Overrideprotected 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() {@Overridepublic 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 onevaluateJavascript(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 targetingBuild.VERSION_CODES.N
or later, JavaScript state from an empty WebView is no longer persisted across navigations likeloadUrl(java.lang.String)
. For example, global variables and functions defined before callingloadUrl(java.lang.String)
will not exist in the loaded page. Applications should useaddJavascriptInterface(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 withWebView#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")@Overrideprotected 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() {@Overridepublic 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
。
代码中 loadUrl
与 evaluateJavascript
的示例等价,选用其一即可。
注:若使用 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 forjavascript:
URLs,blob:
URLs, or for assets accessed viafile:///android_asset/
orfile:///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")@Overrideprotected 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 JavaScriptmWebView.setWebViewClient(new WebViewClient() {@Overridepublic 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);}@Overridepublic 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 {@JavascriptInterfacepublic 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")@Overrideprotected 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() {@Overridepublic boolean onJsAlert(WebView view, String url, String message, final JsResult result) {onJsDialog(DialogType.ALERT, view, url, message, result, null, null);return true;}@Overridepublic boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {onJsDialog(DialogType.CONFIRM, view, url, message, result, null, null);return true;}@Overridepublic 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: Inputbreak;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")@Overrideprotected 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 JavaScriptmWebView.setWebViewClient(new WebViewClient() {@Overridepublic 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;}@Overridepublic 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);}@Overridepublic 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() {@Overridepublic boolean onJsAlert(WebView view, String url, String message, final JsResult result) {onJsDialog(DialogType.ALERT, view, url, message, result, null, null);return true;}@Overridepublic boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {onJsDialog(DialogType.CONFIRM, view, url, message, result, null, null);return true;}@Overridepublic 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: Inputbreak;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 {@JavascriptInterfacepublic 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。你会看到如下界面。
正常的话在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@Overridepublic 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
相关文章:

Android WebView H5 Hybrid 混和开发
对于故乡,我忽然有了新的理解:人的故乡,并不止于一块特定的土地,而是一种辽阔无比的心情,不受空间和时间的限制;这心情一经唤起,就是你已经回到了故乡。——《记忆与印象》 前言 移动互联网发展…...

智源推出下一代检索增强大模型框架MemoRAG
北京智源人工智能研究院与中国人民大学高瓴人工智能学院联合发布了一款创新的人工智能模型框架——MemoRAG。该框架基于长期记忆,旨在推动检索增强生成(RAG)技术的发展,使其能够处理更复杂的任务,而不仅限于简单的问答…...

【AprilTag】视觉定位实战 | 使用 ROS 驱动的 USB 摄像头进行相机标定与 AprilTag 识别
写在前面: 🌟 欢迎光临 清流君 的博客小天地,这里是我分享技术与心得的温馨角落。📝 个人主页:清流君_CSDN博客,期待与您一同探索 移动机器人 领域的无限可能。 🔍 本文系 清流君 原创之作&…...

[数据集][目标检测]俯拍航拍森林火灾检测数据集VOC+YOLO格式6116张2类别
数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):6116 标注数量(xml文件个数):6116 标注数量(txt文件个数):6116 标注…...
windows10下tomcat安装及配置教程
Apache Tomcat是一个开源的、轻量级的Servlet容器,广泛用于运行Java Web应用程序。以下是Tomcat安装及配置的基本步骤,根据搜索结果整理: 一、安装前的准备工作 确保你的计算机上已经安装了Java Development Kit (JDK),因为Tomc…...
Spring MVC设置请求头和响应头的Header
在Spring MVC中,动态设置请求头和响应头的方法有多种,以下是一些常见的方式: 设置请求头 使用RequestHeader注解 这个注解用于读取请求中的单个HTTP头部值,并将其作为一个参数传递给控制器方法。 RequestMapping("/examp…...

一个基于 laravel 和 amis 开发的后台框架, 友好的组件使用体验,可轻松实现复杂页面(附源码)
前言 随着互联网应用的发展,后台管理系统的复杂度不断增加,对于开发者而言,既要系统的功能完备,又要追求开发效率的提升。然而,传统的开发方式往往会导致大量的重复劳动,尤其是在构建复杂的管理页面时。有…...

HTML讲解(二)head部分
目录 1. 2.的使用 2.1 charset 2.2 name 2.2.1 describe关键字 2.2.2 keywords关键字 2.2.3 author关键字 2.2.4 http-equiv 小心!VS2022不可直接接触,否则!没这个必要,方源面色淡然一把抓住!顷刻炼化&#x…...
Linux(Ubuntu)(终端实现helloworld输出)
一、终端实现gcc编译 1.写好helloworld.h,helloworld.c,main.c后,打开终端,切换到保存这些文件的文件夹的目录,我把这些文件存放在helloworld的文件夹下,所以输入cd ~/helloworld 2.查看该目录下的文件&a…...
开源模型应用落地-qwen模型小试-调用Qwen2-VL-7B-Instruct-更清晰地看世界-集成vLLM(二)
一、前言 学习Qwen2-VL ,为我们打开了一扇通往先进人工智能技术的大门。让我们能够深入了解当今最前沿的视觉语言模型的工作原理和强大能力。这不仅拓宽了我们的知识视野,更让我们站在科技发展的潮头,紧跟时代的步伐。 Qwen2-VL 具有卓越的图像和视频理解能力,以及多语言支…...
【乐企-工具篇】有关乐企发票文件生成- OFD和PDF文件生成
有关乐企发票文件生成- OFD和PDF文件生成 本文主要是实现发票的OFD文件以及PDF文件的生成可以参考具体实现思路,具体情况需要根据自己业务进行改造! 具体的OFD文件模板可以从税局进行下载,下载之后放到resources资源目录下。 代码 package com.ruoyi.output.service.thi…...

llama网络结构及源码
目录 模型初始化 config lm_head transformer wte h rms_1/rms_2 attn c_attn c_proj 线性层mlp ln_f rope_cache mask_cache kv_caches tokenizer tokenizer初始化 tokennizer.encoder 位置编码和mask 确定最大文本长度 建立rope_cache 建立mask_cache …...

828华为云征文|Flexus云服务器X实例部署宝塔运维面板
本次华为云Flexus云服务器X实例部署宝塔运维面板教学,这次是推陈出新啊 之前的云耀云服务器L实例已经很不错了,大力赞叹华为云的 同时感谢华为云提供优惠卷,只能说白嫖真是太棒了 华为云近期正在筹办华为云828企业节活动,90款免…...
计算机网络 8.*结构化布线
第八章 结构化布线 第一节 结构化布线基础 一、认识结构化布线 1.定义:在建筑物或楼宇内安装的传输线路,是一个用于语音、数据、影像和其他信息技术的标准结构化布线系统。 2.任务:使语音和数据通信设备、交换设备和其他信息管理系统彼此相…...
c#的委托、事件
程序目的:实现对一个bool型变量的监视,当数据变化时,调用某一个函数,引申出委托、事件等基础概念。 方法一、在form1的类定义中,定义如下代码,这样定义是最直接的,也非常简单,没有涉…...
Day23笔记-Day21和Day22作业讲解单例类
Day22作业讲解 学生类Student:属性:学号,姓名,年龄,性别,成绩 班级类 Grade:属性:班级名称,班级中的学生 【使用列表存储学生】 方法:1.查看该班级中的所有学生的信息2.查看指定学号的学生信息3.查看班级中成绩不…...

k8s中的存储
目录 一 configmap 1.1 configmap的功能 1.2 configmap的使用场景 1.3 configmap创建方式 1.3.1 字面值创建 1.3.2 通过文件创建 1.3.3 通过目录创建 1.3.4 通过yaml文件创建 1.3.5 configmap的使用方式 1.3.5.1 使用configmap填充环境变量 1.3.5.2 通过数据卷使用c…...

【Linux进程控制】进程程序替换
目录 进程程序替换 替换函数 看现象 替换原理 多进程替换 exec*函数使用(部分),并且认识函数参数的含义 1.execl 2.execv 3.execvp 4.execvpe execlp 和execlpe 替换函数总结 进程程序替换 替换函数 有六种以exec开头的函数&am…...

02 ETH
以太坊与比特币有什么不同? 以太坊立足比特币创新之上,于 2015 年启动,两者之间有一些显著不同。 比特币就仅仅是比特币;以太坊包括以太币,以太币才是和比特币对等的存在。以太坊是可编程的,所以你可以在…...

web渗透—RCE
一:代码执行 相关函数 1、eval()函数 assert()函数 (1)原理:将用户提交或者传递的字符串当作php代码执行 (2)passby:单引号绕过:闭合注释;开启GPC的话就无法绕过(GPC就是将单引号转换为"反斜杠单引号"&a…...

深入浅出Asp.Net Core MVC应用开发系列-AspNetCore中的日志记录
ASP.NET Core 是一个跨平台的开源框架,用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET Core 中的日志记录 .NET 通过 ILogger API 支持高性能结构化日志记录,以帮助监视应用程序行为和诊断问题。 可以通过配置不同的记录提供程…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...

Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...

边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...

视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...

2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...

Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...

多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...