在 WebView 中构建 Web 应用

使用 WebView 将 Web 应用或网页作为客户端应用的一部分进行呈现。 WebView 类是 Android View 类的扩展,可让您在 Activity 布局中显示网页。它不包含成熟 Web 浏览器(如带有导航控件或地址栏的浏览器)的所有功能。默认情况下,WebView 仅用于显示网页。

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

当您的应用为用户提供需要连接互联网才能获取的数据(例如电子邮件)时,WebView 也能派上用场。在这种情况下,您可能会发现,在 Android 应用中构建一个显示包含所有用户数据的网页的 WebView,要比执行网络请求、解析数据并将其渲染到 Android 布局中更简单。相反,您可以设计一个专为 Android 设备定制的网页,然后在 Android 应用中实现一个加载该网页的 WebView

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

在早期版本的 Android 上使用 WebView

为了在应用运行的设备上安全使用较新的 WebView 功能,请添加 Jetpack 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 字符串加载 URL

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 进行导航。您还可以使用此子类来拦截 URL 加载。
  • 通过修改 WebSettings 启用 JavaScript。
  • 使用 JavaScript 访问您注入到 WebView 中的 Android 框架对象。

在 WebView 中使用 JavaScript

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

启用 JavaScript

默认情况下,WebView 中的 JavaScript 是禁用的。您可以通过附加到 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 代码中的方法来显示 Dialog,而不是使用 JavaScript 的 alert() 函数。

要绑定 JavaScript 和 Android 代码之间的新接口,请调用 addJavascriptInterface(),并向其传递一个要绑定到 JavaScript 的类实例,以及一个 JavaScript 可以调用以访问该类的接口名称。

要了解有关 JavaScript 与原生代码之间通信的更多信息(包括更现代且更安全的 API),请参阅使用 JSBridge 访问原生 API

例如,您可以在 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,当用户点击按钮时,它会使用新接口创建 Toast 消息

<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 默认会启动一个处理 URL 的应用。通常,默认 Web 浏览器会打开并加载目标 URL。但是,您可以为 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() 方法,该方法会检查 URL 主机是否与前例中定义的特定域匹配。如果匹配,则该方法返回 false 且不会覆盖 URL 加载。它允许 WebView 照常加载该 URL。如果 URL 主机不匹配,则会创建一个 Intent 来启动用于处理 URL 的默认 Activity,该 Activity 会解析为用户的默认 Web 浏览器。

处理自定义 URL

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

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

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

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

与其在链接中使用简单的字符串,不如使用如下的自定义方案

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

然后,您可以在 shouldOverrideUrlLoading() 方法中按如下方式处理此 URL

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 主要用于启动特定 URL 的 Intent。实现此 API 时,请确保为 WebView 处理的 URL 返回 false。不过,您并不局限于启动 Intent。您可以将启动 Intent 替换为上述代码示例中的任何自定义行为。

当您的 WebView 覆盖 URL 加载时,它会自动积累访问过的网页历史记录。您可以使用 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);
}

如果您的应用使用 Jetpack 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() 将不执行任何操作。

处理设备配置更改

在运行时,当设备配置发生更改时(例如用户旋转设备或关闭输入法编辑器 (IME) 时),会发生 Activity 状态更改。这些更改会导致 WebView 对象的 Activity 被销毁并创建一个新 Activity,同时还会创建一个新的 WebView 对象来加载被销毁对象的 URL。要修改 Activity 的默认行为,您可以更改其在清单文件中处理 orientation(屏幕方向)更改的方式。要了解有关在运行时处理配置更改的更多信息,请阅读处理配置更改

管理窗口

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

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