在 WebView 中构建 Web 应用

使用 WebView 在客户端应用中提供 Web 应用或网页。WebView 类是 Android 的 View 类的扩展,可让您将网页显示为 activity 布局的一部分。它不包含功能全面的网络浏览器的功能,例如导航控件或地址栏。WebView 默认只显示网页。

WebView 可帮助您在应用中提供可能需要更新的信息,例如最终用户协议或用户指南。在 Android 应用中,您可以创建一个包含 WebViewActivity,然后使用它来显示在线托管的文档。

如果您的应用向用户提供需要互联网连接才能检索数据的数据(例如电子邮件),WebView 也可能会有所帮助。在这种情况下,您可能会发现相比于执行网络请求,然后解析数据并在 Android 布局中呈现数据,在 Android 应用中编译 WebView 以显示包含所有用户数据的网页更加轻松。您可以改为设计一个专为 Android 设备定制的网页,然后在加载该网页的 Android 应用中实现 WebView

本文档介绍了如何开始使用 WebView、如何将网页中的 JavaScript 绑定到 Android 应用中的客户端代码、如何处理网页导航,以及如何在使用 WebView 时管理窗口。

在较低版本的 Android 上使用 WebView

如需在应用运行的设备上安全地使用较新的 WebView 功能,请添加 AndroidX WebKit 库。这是一个静态库,您可以将其添加到应用中,以使用不适用于较低平台版本的 android.webkit API。

将其添加到 build.gradle 文件中,如下所示:

Kotlin

dependencies {
    implementation("androidx.webkit:webkit:1.8.0")
}

Groovy

dependencies {
    implementation ("androidx.webkit:webkit:1.8.0")
}

如需了解详情,请探索 GitHub 上的 WebView 示例

向应用中添加 WebView

如需向应用中添加 WebView,您可以在 activity 布局中添加 <WebView> 元素,或在 onCreate() 中将整个 Activity 窗口设置为 WebView

在 activity 布局中添加 WebView

如需在布局中为应用添加 WebView,请将以下代码添加到 activity 的布局 XML 文件中:

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

如需在 WebView 中加载网页,请使用 loadUrl(),如以下示例所示:

Kotlin

val myWebView: WebView = findViewById(R.id.webview)
myWebView.loadUrl("http://www.example.com")

Java

WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.loadUrl("http://www.example.com");

在 onCreate() 中添加 WebView

如需改为在 activity 的 onCreate() 方法中向应用添加 WebView,请使用类似如下的逻辑:

Kotlin

val myWebView = WebView(activityContext)
setContentView(myWebView)

Java

WebView myWebView = new WebView(activityContext);
setContentView(myWebView);

然后加载网页:

Kotlin

myWebView.loadUrl("http://www.example.com")

Java

myWebView.loadUrl("https://www.example.com");

或者通过 HTML 字符串加载网址:

Kotlin

// Create an unencoded HTML string, then convert the unencoded HTML string into
// bytes. Encode it with base64 and load the data.
val unencodedHtml =
     "<html><body>'%23' is the percent code for ‘#‘ </body></html>";
val encodedHtml = Base64.encodeToString(unencodedHtml.toByteArray(), Base64.NO_PADDING)
myWebView.loadData(encodedHtml, "text/html", "base64")

Java

// Create an unencoded HTML string, then convert the unencoded HTML string into
// bytes. Encode it with base64 and load the data.
String unencodedHtml =
     "<html><body>'%23' is the percent code for ‘#‘ </body></html>";
String encodedHtml = Base64.encodeToString(unencodedHtml.getBytes(),
        Base64.NO_PADDING);
myWebView.loadData(encodedHtml, "text/html", "base64");

您的应用必须能够访问互联网。如需获取互联网访问权限,请在清单文件中请求 INTERNET 权限,如以下示例所示:

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

您可以通过执行以下任一操作来自定义 WebView

  • 使用 WebChromeClient 启用全屏支持。如果 WebView 需要权限以更改主机应用的界面(例如创建或关闭窗口或向用户发送 JavaScript 对话框),也需要调用此类。如需详细了解如何在这种情况下进行调试,请参阅调试 Web 应用
  • 处理影响内容呈现的事件,例如提交表单时或使用 WebViewClient 导航时出现的错误。您也可以使用此子类拦截网址加载。
  • 通过修改 WebSettings 来启用 JavaScript。
  • 使用 JavaScript 访问已注入到 WebView 的 Android 框架对象。

在 WebView 中使用 JavaScript

如果您想在 WebView 中加载的网页使用 JavaScript,则必须为您的 WebView 启用 JavaScript。启用 JavaScript 后,您可以在应用代码和 JavaScript 代码之间创建接口。

启用 JavaScript

JavaScript 在 WebView 中默认处于停用状态。您可以通过附加到 WebViewWebSettings 启用它。使用 getSettings() 检索 WebSettings,然后使用 setJavaScriptEnabled() 启用 JavaScript。

请参阅以下示例:

Kotlin

val myWebView: WebView = findViewById(R.id.webview)
myWebView.settings.javaScriptEnabled = true

Java

WebView myWebView = (WebView) findViewById(R.id.webview);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);

WebSettings 提供对其他各种实用设置的访问权限。例如,如果您要开发专为 Android 应用中的 WebView 设计的 Web 应用,则可以使用 setUserAgentString() 定义自定义用户代理字符串,然后在网页中查询自定义用户代理,以验证请求网页的客户端是否为您的 Android 应用。

将 JavaScript 代码绑定到 Android 代码

在开发专为 Android 应用中的 WebView 设计的 Web 应用时,您可以在 JavaScript 代码和客户端 Android 代码之间创建接口。例如,您的 JavaScript 代码可以调用 Android 代码中的方法(而不是使用 JavaScript 的 alert() 函数)来显示 Dialog

如需在 JavaScript 代码与 Android 代码之间绑定新接口,请调用 addJavascriptInterface(),并传入类实例以绑定到 JavaScript 以及 JavaScript 可调用以访问类的接口名称。

例如,您可以在 Android 应用中包含以下类:

Kotlin

/** Instantiate the interface and set the context.  */
class WebAppInterface(private val mContext: Context) {

    /** Show a toast from the web page.  */
    @JavascriptInterface
    fun showToast(toast: String) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show()
    }
}

Java

public class WebAppInterface {
    Context mContext;

    /** Instantiate the interface and set the context. */
    WebAppInterface(Context c) {
        mContext = c;
    }

    /** Show a toast from the web page. */
    @JavascriptInterface
    public void showToast(String toast) {
        Toast.makeText(mContext, toast, Toast.LENGTH_SHORT).show();
    }
}

在此示例中,WebAppInterface 类允许网页使用 showToast() 方法创建 Toast 消息。

您可以使用 addJavascriptInterface() 将此类绑定到在 WebView 中运行的 JavaScript,如以下示例所示:

Kotlin

val webView: WebView = findViewById(R.id.webview)
webView.addJavascriptInterface(WebAppInterface(this), "Android")

Java

WebView webView = (WebView) findViewById(R.id.webview);
webView.addJavascriptInterface(new WebAppInterface(this), "Android");

这会为在 WebView 中运行的 JavaScript 创建名为 Android 的接口。此时,您的 Web 应用可以访问 WebAppInterface 类。例如,以下是用于在用户点按按钮时使用新接口创建提示消息的 HTML 和 JavaScript:

<input type="button" value="Say hello" onClick="showAndroidToast('Hello Android!')" />

<script type="text/javascript">
    function showAndroidToast(toast) {
        Android.showToast(toast);
    }
</script>

无需从 JavaScript 初始化 Android 接口。WebView 会自动将其提供给您的网页。因此,当用户点按该按钮时,showAndroidToast() 函数会使用 Android 接口调用 WebAppInterface.showToast() 方法。

处理网页导航

当用户在 WebView 中点按网页中的链接时,Android 的默认行为是启动处理网址的应用。默认网络浏览器通常会打开并加载目标网址。不过,您可以为 WebView 替换此行为,以便在 WebView 内打开链接。然后,您可以允许用户向后/向前浏览由您的 WebView 维护的网页历史记录。

如需打开用户点按的链接,请使用 setWebViewClient() 为您的 WebView 提供 WebViewClient。用户点按的所有链接都会在您的 WebView 中加载。如果您希望更好地控制用户点击的链接的加载位置,请创建您自己的 WebViewClient 以替换 shouldOverrideUrlLoading() 方法。以下示例假定 MyWebViewClientActivity 的内部类。

Kotlin

private class MyWebViewClient : WebViewClient() {

    override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
        if (Uri.parse(url).host == "www.example.com") {
            // This is your website, so don't override. Let your WebView load
            // the page.
            return false
        }
        // Otherwise, the link isn't for a page on your site, so launch another
        // Activity that handles URLs.
        Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply {
            startActivity(this)
        }
        return true
    }
}

Java

private class MyWebViewClient extends WebViewClient {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        if ("www.example.com".equals(request.getUrl().getHost())) {
      // This is your website, so don't override. Let your WebView load the
      // page.
      return false;
    }
    // Otherwise, the link isn't for a page on your site, so launch another
    // Activity that handles URLs.
    Intent intent = new Intent(Intent.ACTION_VIEW, request.getUrl());
    startActivity(intent);
    return true;
  }
}

然后,为 WebView 创建这一新 WebViewClient 的实例:

Kotlin

val myWebView: WebView = findViewById(R.id.webview)
myWebView.webViewClient = MyWebViewClient()

Java

WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.setWebViewClient(new MyWebViewClient());

现在,当用户点按某个链接时,系统会调用 shouldOverrideUrlLoading() 方法,后者会检查网址主机是否与特定网域匹配(如上例所定义)。如果匹配,则该方法会返回 false,不会替换网址加载。它可让 WebView 照常加载网址。如果网址主机不匹配,则创建 Intent 以启动用于处理网址的默认 Activity(解析为用户的默认网络浏览器)。

处理自定义网址

WebView 会在请求资源和解析使用自定义网址架构的链接时应用限制。例如,如果您实现 shouldOverrideUrlLoading()shouldInterceptRequest() 之类的回调,那么 WebView 仅针对有效网址调用它们。

例如,WebView 可能不会针对如下链接调用您的 shouldOverrideUrlLoading() 方法:

<a href="showProfile">Show Profile</a>

WebView 中对无效网址(如上例所示)的处理方式不一致,因此我们建议改用格式正确的网址。您可以为贵组织控制的网域使用自定义架构或 HTTPS 网址。

您可以使用如下所示的自定义架构,而不是如上例所示在链接中使用简单字符串:

<a href="example-app:showProfile">Show Profile</a>

然后,您可以在 shouldOverrideUrlLoading() 方法中处理此网址,如下所示:

Kotlin

// The URL scheme must be non-hierarchical, meaning no trailing slashes.
const val APP_SCHEME = "example-app:"

override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
    return if (url?.startsWith(APP_SCHEME) == true) {
        urlData = URLDecoder.decode(url.substring(APP_SCHEME.length), "UTF-8")
        respondToData(urlData)
        true
    } else {
        false
    }
}

Java

// The URL scheme must be non-hierarchical, meaning no trailing slashes.
private static final String APP_SCHEME = "example-app:";

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (url.startsWith(APP_SCHEME)) {
        urlData = URLDecoder.decode(url.substring(APP_SCHEME.length()), "UTF-8");
        respondToData(urlData);
        return true;
    }
    return false;
}

shouldOverrideUrlLoading() API 主要用于针对特定网址启动 intent。在实现该 API 时,请务必针对 WebView 处理的网址返回 false。不过,您不局限于启动 intent。您可以在上述代码示例中将启动 intent 替换为任何自定义行为。

当您的 WebView 替换网址加载时,它会自动累积已访问网页的历史记录。您可以使用 goBack()goForward() 向后/向前浏览历史记录。

例如,下面显示了您的 Activity 是如何使用设备的返回按钮向后导航的:

Kotlin

override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    // Check whether the key event is the Back button and if there's history.
    if (keyCode == KeyEvent.KEYCODE_BACK && myWebView.canGoBack()) {
        myWebView.goBack()
        return true
    }
    // If it isn't the Back button or there isn't web page history, bubble up to
    // the default system behavior. Probably exit the activity.
    return super.onKeyDown(keyCode, event)
}

Java

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    // Check whether the key event is the Back button and if there's history.
    if ((keyCode == KeyEvent.KEYCODE_BACK) && myWebView.canGoBack()) {
        myWebView.goBack();
        return true;
    }
    // If it isn't the Back button or there's no web page history, bubble up to
    // the default system behavior. Probably exit the activity.
    return super.onKeyDown(keyCode, event);
}

如果您的应用使用 AndroidX AppCompat 1.6.0 或更高版本,您可以进一步简化上面的代码段:

Kotlin

onBackPressedDispatcher.addCallback {
    // Check whether there's history.
    if (myWebView.canGoBack()) {
        myWebView.goBack()
    }
}

Java

onBackPressedDispatcher.addCallback {
    // Check whether there's history.
    if (myWebView.canGoBack()) {
        myWebView.goBack();
    }
}

如果存在用户要访问的网页历史记录,则 canGoBack() 方法会返回 true。同样,您可以使用 canGoForward() 检查是否存在向前历史记录。如果您不执行此检查,那么当用户浏览到历史记录的末尾时,goBack()goForward() 将不执行任何操作。

处理设备配置变更

在运行时,activity 状态更改会在设备的配置发生更改时发生,例如用户旋转设备或关闭输入法 (IME) 时。这些更改会导致 WebView 对象的 activity 被销毁并创建新的 activity,而这也会创建新的 WebView 对象来加载已销毁对象的网址。如需修改 activity 的默认行为,您可以在清单中更改其处理 orientation 更改的方式。如需详细了解如何在运行时处理配置更改,请参阅处理配置更改

管理窗口

默认情况下,系统会忽略打开新窗口的请求。无论是通过 JavaScript 还是通过链接中的 target 属性打开新窗口,都是如此。您可以自定义 WebChromeClient 来自行提供用于打开多个窗口的行为。

为了提高应用的安全性,最好阻止弹出式窗口和新窗口打开。要实现此行为,最安全的方式是将 "true" 传入 setSupportMultipleWindows() 但不替换 onCreateWindow() 方法(setSupportMultipleWindows() 依赖此方法)。此逻辑会阻止加载在其链接中使用 target="_blank" 的任何网页。